Merge updated Nether portals, https://git.minetest.land/Wuzzy/MineClone2/issues/804
[MineClone/MineClone2.git] / mods / ENTITIES / mcl_mobs / api.lua
blobab2480f9d4f2559caaf062587185be2972156f4b
2 -- API for Mobs Redo: MineClone 2 Edition (MRM)
4 mobs = {}
5 mobs.mod = "mrm"
6 mobs.version = "20180531" -- don't rely too much on this, rarely updated, if ever
8 local MAX_MOB_NAME_LENGTH = 30
10 local MOB_CAP = {}
11 MOB_CAP.hostile = 70
12 MOB_CAP.passive = 10
13 MOB_CAP.ambient = 15
14 MOB_CAP.water = 15
16 -- Localize
17 local S = minetest.get_translator("mcl_mobs")
19 -- CMI support check
20 local use_cmi = minetest.global_exists("cmi")
23 -- Invisibility mod check
24 mobs.invis = {}
25 if minetest.global_exists("invisibility") then
26 mobs.invis = invisibility
27 end
30 -- creative check
31 function mobs.is_creative(name)
32 return minetest.is_creative_enabled(name)
33 end
36 -- localize math functions
37 local pi = math.pi
38 local sin = math.sin
39 local cos = math.cos
40 local abs = math.abs
41 local min = math.min
42 local max = math.max
43 local atann = math.atan
44 local random = math.random
45 local floor = math.floor
46 local atan = function(x)
47 if not x or x ~= x then
48 return 0
49 else
50 return atann(x)
51 end
52 end
55 -- Load settings
56 local damage_enabled = minetest.settings:get_bool("enable_damage")
57 local mobs_spawn = minetest.settings:get_bool("mobs_spawn", true) ~= false
59 local disable_blood = minetest.settings:get_bool("mobs_disable_blood")
60 local mobs_drop_items = minetest.settings:get_bool("mobs_drop_items") ~= false
61 local mobs_griefing = minetest.settings:get_bool("mobs_griefing") ~= false
62 local spawn_protected = minetest.settings:get_bool("mobs_spawn_protected") ~= false
63 local remove_far = false
64 local difficulty = tonumber(minetest.settings:get("mob_difficulty")) or 1.0
65 local show_health = false
66 local max_per_block = tonumber(minetest.settings:get("max_objects_per_block") or 64)
67 local mobs_spawn_chance = tonumber(minetest.settings:get("mobs_spawn_chance") or 2.5)
69 -- Peaceful mode message so players will know there are no monsters
70 if minetest.settings:get_bool("only_peaceful_mobs", false) then
71 minetest.register_on_joinplayer(function(player)
72 minetest.chat_send_player(player:get_player_name(),
73 S("Peaceful mode active! No monsters will spawn."))
74 end)
75 end
77 -- calculate aoc range for mob count
78 local aosrb = tonumber(minetest.settings:get("active_object_send_range_blocks"))
79 local abr = tonumber(minetest.settings:get("active_block_range"))
80 local aoc_range = max(aosrb, abr) * 16
82 -- pathfinding settings
83 local enable_pathfinding = true
84 local stuck_timeout = 3 -- how long before mob gets stuck in place and starts searching
85 local stuck_path_timeout = 10 -- how long will mob follow path before giving up
87 -- default nodes
88 local node_ice = "mcl_core:ice"
89 local node_snowblock = "mcl_core:snowblock"
90 local node_snow = "mcl_core:snow"
91 mobs.fallback_node = minetest.registered_aliases["mapgen_dirt"] or "mcl_core:dirt"
93 local mod_weather = minetest.get_modpath("mcl_weather") ~= nil
94 local mod_explosions = minetest.get_modpath("mcl_explosions") ~= nil
95 local mod_mobspawners = minetest.get_modpath("mcl_mobspawners") ~= nil
96 local mod_hunger = minetest.get_modpath("mcl_hunger") ~= nil
97 local mod_worlds = minetest.get_modpath("mcl_worlds") ~= nil
98 local mod_armor = minetest.get_modpath("mcl_armor") ~= nil
100 -- play sound
101 local mob_sound = function(self, soundname, is_opinion, fixed_pitch)
103 local soundinfo
104 if self.sounds_child and self.child then
105 soundinfo = self.sounds_child
106 elseif self.sounds then
107 soundinfo = self.sounds
109 if not soundinfo then
110 return
112 local sound = soundinfo[soundname]
113 if sound then
114 if is_opinion and self.opinion_sound_cooloff > 0 then
115 return
117 local pitch
118 if not fixed_pitch then
119 local base_pitch = soundinfo.base_pitch
120 if not base_pitch then
121 base_pitch = 1
123 if self.child and (not self.sounds_child) then
124 -- Children have higher pitch
125 pitch = base_pitch * 1.5
126 else
127 pitch = base_pitch
129 -- randomize the pitch a bit
130 pitch = pitch + math.random(-10, 10) * 0.005
132 minetest.sound_play(sound, {
133 object = self.object,
134 gain = 1.0,
135 max_hear_distance = self.sounds.distance,
136 pitch = pitch,
137 }, true)
138 self.opinion_sound_cooloff = 1
142 -- Reeturn true if object is in view_range
143 local function object_in_range(self, object)
144 if not object then
145 return false
147 local factor
148 -- Apply view range reduction for special player armor
149 if object:is_player() and mod_armor then
150 factor = armor:get_mob_view_range_factor(object, self.name)
152 -- Distance check
153 local dist
154 if factor and factor == 0 then
155 return false
156 elseif factor then
157 dist = self.view_range * factor
158 else
159 dist = self.view_range
161 if vector.distance(self.object:get_pos(), object:get_pos()) > dist then
162 return false
163 else
164 return true
168 -- attack player/mob
169 local do_attack = function(self, player)
171 if self.state == "attack" then
172 return
175 self.attack = player
176 self.state = "attack"
178 -- TODO: Implement war_cry sound without being annoying
179 --if random(0, 100) < 90 then
180 --mob_sound(self, "war_cry", true)
181 --end
185 -- move mob in facing direction
186 local set_velocity = function(self, v)
188 -- do not move if mob has been ordered to stay
189 if self.order == "stand" then
190 self.object:set_velocity({x = 0, y = 0, z = 0})
191 return
194 local yaw = (self.object:get_yaw() or 0) + self.rotate
195 local vel = self.object:get_velocity()
196 self.object:set_velocity({
197 x = sin(yaw) * -v,
198 y = (vel and vel.y) or 0,
199 z = cos(yaw) * v
204 -- calculate mob velocity
205 local get_velocity = function(self)
207 local v = self.object:get_velocity()
209 return (v.x * v.x + v.z * v.z) ^ 0.5
213 -- set and return valid yaw
214 local set_yaw = function(self, yaw, delay)
216 if not yaw or yaw ~= yaw then
217 yaw = 0
220 delay = delay or 0
222 if delay == 0 then
223 self.object:set_yaw(yaw)
224 return yaw
227 self.target_yaw = yaw
228 self.delay = delay
230 return self.target_yaw
233 -- global function to set mob yaw
234 function mobs:yaw(self, yaw, delay)
235 set_yaw(self, yaw, delay)
238 local add_texture_mod = function(self, mod)
239 local full_mod = ""
240 local already_added = false
241 for i=1, #self.texture_mods do
242 if mod == self.texture_mods[i] then
243 already_added = true
245 full_mod = full_mod .. self.texture_mods[i]
247 if not already_added then
248 full_mod = full_mod .. mod
249 table.insert(self.texture_mods, mod)
251 self.object:set_texture_mod(full_mod)
253 local remove_texture_mod = function(self, mod)
254 local full_mod = ""
255 local remove = {}
256 for i=1, #self.texture_mods do
257 if self.texture_mods[i] ~= mod then
258 full_mod = full_mod .. self.texture_mods[i]
259 else
260 table.insert(remove, i)
263 for i=#remove, 1 do
264 table.remove(self.texture_mods, remove[i])
266 self.object:set_texture_mod(full_mod)
269 -- set defined animation
270 local set_animation = function(self, anim)
272 if not self.animation
273 or not anim then return end
275 self.animation.current = self.animation.current or ""
277 if anim == self.animation.current
278 or not self.animation[anim .. "_start"]
279 or not self.animation[anim .. "_end"] then
280 return
283 self.animation.current = anim
285 self.object:set_animation({
286 x = self.animation[anim .. "_start"],
287 y = self.animation[anim .. "_end"]},
288 self.animation[anim .. "_speed"] or self.animation.speed_normal or 15,
289 0, self.animation[anim .. "_loop"] ~= false)
293 -- above function exported for mount.lua
294 function mobs:set_animation(self, anim)
295 set_animation(self, anim)
298 -- Returns true is node can deal damage to self
299 local is_node_dangerous = function(self, nodename)
300 local nn = nodename
301 if self.lava_damage > 0 then
302 if minetest.get_item_group(nn, "lava") ~= 0 then
303 return true
306 if self.fire_damage > 0 then
307 if minetest.get_item_group(nn, "fire") ~= 0 then
308 return true
311 if minetest.registered_nodes[nn].damage_per_second > 0 then
312 return true
314 return false
318 -- Returns true if node is a water hazard
319 local is_node_waterhazard = function(self, nodename)
320 local nn = nodename
321 if self.water_damage > 0 then
322 if minetest.get_item_group(nn, "water") ~= 0 then
323 return true
326 if minetest.registered_nodes[nn].drowning > 0 then
327 if self.breath_max ~= -1 then
328 -- check if the mob is water-breathing _and_ the block is water; only return true if neither is the case
329 -- this will prevent water-breathing mobs to classify water or e.g. sand below them as dangerous
330 if not self.breathes_in_water and minetest.get_item_group(nn, "water") ~= 0 then
331 return true
335 return false
339 -- check line of sight (BrunoMine)
340 local line_of_sight = function(self, pos1, pos2, stepsize)
342 stepsize = stepsize or 1
344 local s, pos = minetest.line_of_sight(pos1, pos2, stepsize)
346 -- normal walking and flying mobs can see you through air
347 if s == true then
348 return true
351 -- New pos1 to be analyzed
352 local npos1 = {x = pos1.x, y = pos1.y, z = pos1.z}
354 local r, pos = minetest.line_of_sight(npos1, pos2, stepsize)
356 -- Checks the return
357 if r == true then return true end
359 -- Nodename found
360 local nn = minetest.get_node(pos).name
362 -- Target Distance (td) to travel
363 local td = vector.distance(pos1, pos2)
365 -- Actual Distance (ad) traveled
366 local ad = 0
368 -- It continues to advance in the line of sight in search of a real
369 -- obstruction which counts as 'normal' nodebox.
370 while minetest.registered_nodes[nn]
371 and minetest.registered_nodes[nn].walkable == false do
373 -- Check if you can still move forward
374 if td < ad + stepsize then
375 return true -- Reached the target
378 -- Moves the analyzed pos
379 local d = vector.distance(pos1, pos2)
381 npos1.x = ((pos2.x - pos1.x) / d * stepsize) + pos1.x
382 npos1.y = ((pos2.y - pos1.y) / d * stepsize) + pos1.y
383 npos1.z = ((pos2.z - pos1.z) / d * stepsize) + pos1.z
385 -- NaN checks
386 if d == 0
387 or npos1.x ~= npos1.x
388 or npos1.y ~= npos1.y
389 or npos1.z ~= npos1.z then
390 return false
393 ad = ad + stepsize
395 -- scan again
396 r, pos = minetest.line_of_sight(npos1, pos2, stepsize)
398 if r == true then return true end
400 -- New Nodename found
401 nn = minetest.get_node(pos).name
405 return false
409 -- are we flying in what we are suppose to? (taikedz)
410 local flight_check = function(self)
412 local nod = self.standing_in
413 local def = minetest.registered_nodes[nod]
415 if not def then return false end -- nil check
417 local fly_in
418 if type(self.fly_in) == "string" then
419 fly_in = { self.fly_in }
420 elseif type(self.fly_in) == "table" then
421 fly_in = self.fly_in
422 else
423 return false
426 for _,checknode in pairs(fly_in) do
427 if nod == checknode then
428 return true
429 elseif checknode == "__airlike" and def.walkable == false and
430 (def.liquidtype == "none" or minetest.get_item_group(nod, "fake_liquid") == 1) then
431 return true
435 return false
439 -- custom particle effects
440 local effect = function(pos, amount, texture, min_size, max_size, radius, gravity, glow, go_down)
442 radius = radius or 2
443 min_size = min_size or 0.5
444 max_size = max_size or 1
445 gravity = gravity or -10
446 glow = glow or 0
447 go_down = go_down or false
449 local ym
450 if go_down then
451 ym = 0
452 else
453 ym = -radius
456 minetest.add_particlespawner({
457 amount = amount,
458 time = 0.25,
459 minpos = pos,
460 maxpos = pos,
461 minvel = {x = -radius, y = ym, z = -radius},
462 maxvel = {x = radius, y = radius, z = radius},
463 minacc = {x = 0, y = gravity, z = 0},
464 maxacc = {x = 0, y = gravity, z = 0},
465 minexptime = 0.1,
466 maxexptime = 1,
467 minsize = min_size,
468 maxsize = max_size,
469 texture = texture,
470 glow = glow,
474 local damage_effect = function(self, damage)
475 -- damage particles
476 if (not disable_blood) and damage > 0 then
478 local amount_large = math.floor(damage / 2)
479 local amount_small = damage % 2
481 local pos = self.object:get_pos()
483 pos.y = pos.y + (self.collisionbox[5] - self.collisionbox[2]) * .5
485 local texture = "mobs_blood.png"
486 -- full heart damage (one particle for each 2 HP damage)
487 if amount_large > 0 then
488 effect(pos, amount_large, texture, 2, 2, 1.75, 0, nil, true)
490 -- half heart damage (one additional particle if damage is an odd number)
491 if amount_small > 0 then
492 -- TODO: Use "half heart"
493 effect(pos, amount_small, texture, 1, 1, 1.75, 0, nil, true)
498 mobs.death_effect = function(pos, collisionbox)
499 local min, max
500 if collisionbox then
501 min = {x=collisionbox[1], y=collisionbox[2], z=collisionbox[3]}
502 max = {x=collisionbox[4], y=collisionbox[5], z=collisionbox[6]}
503 else
504 min = { x = -0.5, y = 0, z = -0.5 }
505 max = { x = 0.5, y = 0.5, z = 0.5 }
508 minetest.add_particlespawner({
509 amount = 40,
510 time = 0.1,
511 minpos = vector.add(pos, min),
512 maxpos = vector.add(pos, max),
513 minvel = {x = -0.2, y = -0.1, z = -0.2},
514 maxvel = {x = 0.2, y = 0.1, z = 0.2},
515 minexptime = 0.5,
516 maxexptime = 1.5,
517 minsize = 0.5,
518 maxsize = 1.5,
519 texture = "mcl_particles_smoke.png",
523 local update_tag = function(self)
524 self.object:set_properties({
525 nametag = self.nametag,
531 -- drop items
532 local item_drop = function(self, cooked)
534 -- no drops if disabled by setting
535 if not mobs_drop_items then return end
537 -- no drops for child mobs (except monster)
538 if (self.child and self.type ~= "monster") then
539 return
542 local obj, item, num
543 local pos = self.object:get_pos()
545 self.drops = self.drops or {} -- nil check
547 for n = 1, #self.drops do
549 if random(1, self.drops[n].chance) == 1 then
551 num = random(self.drops[n].min or 1, self.drops[n].max or 1)
552 item = self.drops[n].name
554 -- cook items when true
555 if cooked then
557 local output = minetest.get_craft_result({
558 method = "cooking", width = 1, items = {item}})
560 if output and output.item and not output.item:is_empty() then
561 item = output.item:get_name()
565 -- add item if it exists
566 obj = minetest.add_item(pos, ItemStack(item .. " " .. num))
568 if obj and obj:get_luaentity() then
570 obj:set_velocity({
571 x = random(-10, 10) / 9,
572 y = 6,
573 z = random(-10, 10) / 9,
575 elseif obj then
576 obj:remove() -- item does not exist
581 self.drops = {}
585 -- check if mob is dead or only hurt
586 local check_for_death = function(self, cause, cmi_cause)
588 -- has health actually changed?
589 if self.health == self.old_health and self.health > 0 then
590 return false
593 local damaged = self.health < self.old_health
594 self.old_health = self.health
596 -- still got some health?
597 if self.health > 0 then
599 -- make sure health isn't higher than max
600 if self.health > self.hp_max then
601 self.health = self.hp_max
604 -- play damage sound if health was reduced and make mob flash red.
605 if damaged then
606 add_texture_mod(self, "^[colorize:#FF000040")
607 minetest.after(.2, function(self)
608 if self and self.object then
609 remove_texture_mod(self, "^[colorize:#FF000040")
611 end, self)
612 mob_sound(self, "damage")
615 -- backup nametag so we can show health stats
616 if not self.nametag2 then
617 self.nametag2 = self.nametag or ""
620 if show_health
621 and (cmi_cause and cmi_cause.type == "punch") then
623 self.htimer = 2
624 self.nametag = "♥ " .. self.health .. " / " .. self.hp_max
626 update_tag(self)
629 return false
632 -- dropped cooked item if mob died in fire or lava
633 if cause == "lava" or cause == "fire" then
634 item_drop(self, true)
635 else
636 item_drop(self, nil)
639 mob_sound(self, "death")
641 local pos = self.object:get_pos()
643 -- execute custom death function
644 if self.on_die then
646 self.on_die(self, pos)
648 if use_cmi then
649 cmi.notify_die(self.object, cmi_cause)
652 self.object:remove()
654 return true
657 local collisionbox
658 if self.collisionbox then
659 collisionbox = table.copy(self.collisionbox)
662 -- default death function and die animation (if defined)
663 if self.animation
664 and self.animation.die_start
665 and self.animation.die_end then
667 local frames = self.animation.die_end - self.animation.die_start
668 local speed = self.animation.die_speed or 15
669 local length = max(frames / speed, 0)
671 self.attack = nil
672 self.v_start = false
673 self.timer = 0
674 self.blinktimer = 0
675 self.passive = true
676 self.state = "die"
677 self.object:set_properties({
678 pointable = false,
680 set_velocity(self, 0)
681 set_animation(self, "die")
683 minetest.after(length, function(self)
684 if not self.object:get_luaentity() then
685 return
687 if use_cmi then
688 cmi.notify_die(self.object, cmi_cause)
691 self.object:remove()
692 mobs.death_effect(pos)
693 end, self)
694 else
696 if use_cmi then
697 cmi.notify_die(self.object, cmi_cause)
700 self.object:remove()
701 mobs.death_effect(pos, collisionbox)
704 return true
708 -- check if within physical map limits (-30911 to 30927)
709 local within_limits = function(pos, radius)
711 if (pos.x - radius) > -30913
712 and (pos.x + radius) < 30928
713 and (pos.y - radius) > -30913
714 and (pos.y + radius) < 30928
715 and (pos.z - radius) > -30913
716 and (pos.z + radius) < 30928 then
717 return true -- within limits
720 return false -- beyond limits
724 -- is mob facing a cliff or danger
725 local is_at_cliff_or_danger = function(self)
727 if self.fear_height == 0 then -- 0 for no falling protection!
728 return false
731 if not self.object:get_luaentity() then
732 return false
734 local yaw = self.object:get_yaw()
735 local dir_x = -sin(yaw) * (self.collisionbox[4] + 0.5)
736 local dir_z = cos(yaw) * (self.collisionbox[4] + 0.5)
737 local pos = self.object:get_pos()
738 local ypos = pos.y + self.collisionbox[2] -- just above floor
740 local free_fall, blocker = minetest.line_of_sight(
741 {x = pos.x + dir_x, y = ypos, z = pos.z + dir_z},
742 {x = pos.x + dir_x, y = ypos - self.fear_height, z = pos.z + dir_z})
743 if free_fall then
744 return true
745 else
746 local bnode = minetest.get_node(blocker)
747 local danger = is_node_dangerous(self, bnode.name)
748 if danger then
749 return true
750 else
751 local def = minetest.registered_nodes[bnode.name]
752 return (not def and def.walkable)
756 return false
760 -- copy the 'mob facing cliff_or_danger check' from above, and rework to avoid water
761 local is_at_water_danger = function(self)
764 if not self.object:get_luaentity() then
765 return false
767 local yaw = self.object:get_yaw()
768 local dir_x = -sin(yaw) * (self.collisionbox[4] + 0.5)
769 local dir_z = cos(yaw) * (self.collisionbox[4] + 0.5)
770 local pos = self.object:get_pos()
771 local ypos = pos.y + self.collisionbox[2] -- just above floor
773 local free_fall, blocker = minetest.line_of_sight(
774 {x = pos.x + dir_x, y = ypos, z = pos.z + dir_z},
775 {x = pos.x + dir_x, y = ypos - 3, z = pos.z + dir_z})
776 if free_fall then
777 return true
778 else
779 local bnode = minetest.get_node(blocker)
780 local waterdanger = is_node_waterhazard(self, bnode.name)
782 waterdanger and (is_node_waterhazard(self, self.standing_in) or is_node_waterhazard(self, self.standing_on)) then
783 return false
784 elseif waterdanger and (is_node_waterhazard(self, self.standing_in) or is_node_waterhazard(self, self.standing_on)) == false then
785 return true
786 else
787 local def = minetest.registered_nodes[bnode.name]
788 return (not def and def.walkable)
792 return false
796 -- get node but use fallback for nil or unknown
797 local node_ok = function(pos, fallback)
799 fallback = fallback or mobs.fallback_node
801 local node = minetest.get_node_or_nil(pos)
803 if node and minetest.registered_nodes[node.name] then
804 return node
807 return minetest.registered_nodes[fallback]
811 -- environmental damage (water, lava, fire, light etc.)
812 local do_env_damage = function(self)
814 -- feed/tame text timer (so mob 'full' messages dont spam chat)
815 if self.htimer > 0 then
816 self.htimer = self.htimer - 1
819 -- reset nametag after showing health stats
820 if self.htimer < 1 and self.nametag2 then
822 self.nametag = self.nametag2
823 self.nametag2 = nil
825 update_tag(self)
828 local pos = self.object:get_pos()
830 self.time_of_day = minetest.get_timeofday()
832 -- remove mob if beyond map limits
833 if not within_limits(pos, 0) then
834 self.object:remove()
835 return true
839 -- Deal light damage to mob, returns true if mob died
840 local deal_light_damage = function(self, pos, damage)
841 if not (mod_weather and (mcl_weather.rain.raining or mcl_weather.state == "snow") and mcl_weather.is_outdoor(pos)) then
842 self.health = self.health - damage
844 effect(pos, 5, "mcl_particles_smoke.png")
846 if check_for_death(self, "light", {type = "light"}) then
847 return true
852 -- bright light harms mob
853 if self.light_damage ~= 0 and (minetest.get_node_light(pos) or 0) > 12 then
854 if deal_light_damage(self, pos, self.light_damage) then
855 return true
858 local _, dim = nil, "overworld"
859 if mod_worlds then
860 _, dim = mcl_worlds.y_to_layer(pos.y)
862 if self.sunlight_damage ~= 0 and (minetest.get_node_light(pos) or 0) >= minetest.LIGHT_MAX and dim == "overworld" then
863 if deal_light_damage(self, pos, self.sunlight_damage) then
864 return true
868 local y_level = self.collisionbox[2]
870 if self.child then
871 y_level = self.collisionbox[2] * 0.5
874 -- what is mob standing in?
875 pos.y = pos.y + y_level + 0.25 -- foot level
876 local pos2 = {x=pos.x, y=pos.y-1, z=pos.z}
877 self.standing_in = node_ok(pos, "air").name
878 self.standing_on = node_ok(pos2, "air").name
880 -- don't fall when on ignore, just stand still
881 if self.standing_in == "ignore" then
882 self.object:set_velocity({x = 0, y = 0, z = 0})
885 local nodef = minetest.registered_nodes[self.standing_in]
887 -- rain
888 if self.rain_damage > 0 and mod_weather then
889 if mcl_weather.rain.raining and mcl_weather.is_outdoor(pos) then
891 self.health = self.health - self.rain_damage
893 if check_for_death(self, "rain", {type = "environment",
894 pos = pos, node = self.standing_in}) then
895 return true
900 pos.y = pos.y + 1 -- for particle effect position
902 -- water damage
903 if self.water_damage > 0
904 and nodef.groups.water then
906 if self.water_damage ~= 0 then
908 self.health = self.health - self.water_damage
910 effect(pos, 5, "mcl_particles_smoke.png", nil, nil, 1, nil)
912 if check_for_death(self, "water", {type = "environment",
913 pos = pos, node = self.standing_in}) then
914 return true
918 -- lava damage
919 elseif self.lava_damage > 0
920 and (nodef.groups.lava) then
922 if self.lava_damage ~= 0 then
924 self.health = self.health - self.lava_damage
926 effect(pos, 5, "fire_basic_flame.png", nil, nil, 1, nil)
928 if check_for_death(self, "lava", {type = "environment",
929 pos = pos, node = self.standing_in}) then
930 return true
934 -- fire damage
935 elseif self.fire_damage > 0
936 and (nodef.groups.fire) then
938 if self.fire_damage ~= 0 then
940 self.health = self.health - self.fire_damage
942 effect(pos, 5, "fire_basic_flame.png", nil, nil, 1, nil)
944 if check_for_death(self, "fire", {type = "environment",
945 pos = pos, node = self.standing_in}) then
946 return true
950 -- damage_per_second node check
951 elseif nodef.damage_per_second ~= 0 then
953 self.health = self.health - nodef.damage_per_second
955 effect(pos, 5, "mcl_particles_smoke.png")
957 if check_for_death(self, "dps", {type = "environment",
958 pos = pos, node = self.standing_in}) then
959 return true
963 -- Drowning damage
964 if self.breath_max ~= -1 then
965 local drowning = false
966 if self.breathes_in_water then
967 if minetest.get_item_group(self.standing_in, "water") == 0 then
968 drowning = true
970 elseif nodef.drowning > 0 then
971 drowning = true
973 if drowning then
975 self.breath = math.max(0, self.breath - 1)
977 effect(pos, 2, "bubble.png", nil, nil, 1, nil)
978 if self.breath <= 0 then
979 local dmg
980 if nodef.drowning > 0 then
981 dmg = nodef.drowning
982 else
983 dmg = 4
985 damage_effect(self, dmg)
986 self.health = self.health - dmg
988 if check_for_death(self, "drowning", {type = "environment",
989 pos = pos, node = self.standing_in}) then
990 return true
992 else
993 self.breath = math.min(self.breath_max, self.breath + 1)
997 --- suffocation inside solid node
998 -- FIXME: Redundant with mcl_playerplus
999 if (self.suffocation == true)
1000 and (nodef.walkable == nil or nodef.walkable == true)
1001 and (nodef.collision_box == nil or nodef.collision_box.type == "regular")
1002 and (nodef.node_box == nil or nodef.node_box.type == "regular")
1003 and (nodef.groups.disable_suffocation ~= 1)
1004 and (nodef.groups.opaque == 1) then
1006 -- Short grace period before starting to take suffocation damage.
1007 -- This is different from players, who take damage instantly.
1008 -- This has been done because mobs might briefly be inside solid nodes
1009 -- when e.g. climbing up stairs.
1010 -- This is a bit hacky because it assumes that do_env_damage
1011 -- is called roughly every second only.
1012 self.suffocation_timer = self.suffocation_timer + 1
1013 if self.suffocation_timer >= 3 then
1014 -- 2 damage per second
1015 -- TODO: Deal this damage once every 1/2 second
1016 self.health = self.health - 2
1018 if check_for_death(self, "suffocation", {type = "environment",
1019 pos = pos, node = self.standing_in}) then
1020 return true
1023 else
1024 self.suffocation_timer = 0
1027 return check_for_death(self, "", {type = "unknown"})
1031 -- jump if facing a solid node (not fences or gates)
1032 local do_jump = function(self)
1034 if not self.jump
1035 or self.jump_height == 0
1036 or self.fly
1037 or (self.child and self.type ~= "monster")
1038 or self.order == "stand" then
1039 return false
1042 self.facing_fence = false
1044 -- something stopping us while moving?
1045 if self.state ~= "stand"
1046 and get_velocity(self) > 0.5
1047 and self.object:get_velocity().y ~= 0 then
1048 return false
1051 local pos = self.object:get_pos()
1052 local yaw = self.object:get_yaw()
1054 -- what is mob standing on?
1055 pos.y = pos.y + self.collisionbox[2] - 0.2
1057 local nod = node_ok(pos)
1059 if minetest.registered_nodes[nod.name].walkable == false then
1060 return false
1063 -- where is front
1064 local dir_x = -sin(yaw) * (self.collisionbox[4] + 0.5)
1065 local dir_z = cos(yaw) * (self.collisionbox[4] + 0.5)
1067 -- what is in front of mob?
1068 nod = node_ok({
1069 x = pos.x + dir_x,
1070 y = pos.y + 0.5,
1071 z = pos.z + dir_z
1074 -- this is used to detect if there's a block on top of the block in front of the mob.
1075 -- If there is, there is no point in jumping as we won't manage.
1076 local nodTop = node_ok({
1077 x = pos.x + dir_x,
1078 y = pos.y + 1.5,
1079 z = pos.z + dir_z
1080 }, "air")
1082 -- we don't attempt to jump if there's a stack of blocks blocking
1083 if minetest.registered_nodes[nodTop.name] == true then
1084 return false
1087 -- thin blocks that do not need to be jumped
1088 if nod.name == node_snow then
1089 return false
1092 if self.walk_chance == 0
1093 or minetest.registered_items[nod.name].walkable then
1095 if minetest.get_item_group(nod.name, "fence") == 0
1096 and minetest.get_item_group(nod.name, "fence_gate") == 0
1097 and minetest.get_item_group(nod.name, "wall") == 0 then
1099 local v = self.object:get_velocity()
1101 v.y = self.jump_height
1103 set_animation(self, "jump") -- only when defined
1105 self.object:set_velocity(v)
1107 -- when in air move forward
1108 minetest.after(0.3, function(self, v)
1109 if not self.object or not self.object:get_luaentity() then
1110 return
1112 self.object:set_acceleration({
1113 x = v.x * 2,
1114 y = 0,
1115 z = v.z * 2,
1117 end, self, v)
1119 if self.jump_sound_cooloff <= 0 then
1120 mob_sound(self, "jump")
1121 self.jump_sound_cooloff = 0.5
1123 else
1124 self.facing_fence = true
1127 -- if we jumped against a block/wall 4 times then turn
1128 if self.object:get_velocity().x ~= 0
1129 and self.object:get_velocity().z ~= 0 then
1131 self.jump_count = (self.jump_count or 0) + 1
1133 if self.jump_count == 4 then
1135 local yaw = self.object:get_yaw() or 0
1137 yaw = set_yaw(self, yaw + 1.35, 8)
1139 self.jump_count = 0
1143 return true
1146 return false
1150 -- blast damage to entities nearby
1151 local entity_physics = function(pos, radius)
1153 radius = radius * 2
1155 local objs = minetest.get_objects_inside_radius(pos, radius)
1156 local obj_pos, dist
1158 for n = 1, #objs do
1160 obj_pos = objs[n]:get_pos()
1162 dist = vector.distance(pos, obj_pos)
1163 if dist < 1 then dist = 1 end
1165 local damage = floor((4 / dist) * radius)
1166 local ent = objs[n]:get_luaentity()
1168 -- punches work on entities AND players
1169 objs[n]:punch(objs[n], 1.0, {
1170 full_punch_interval = 1.0,
1171 damage_groups = {fleshy = damage},
1172 }, pos)
1177 -- should mob follow what I'm holding ?
1178 local follow_holding = function(self, clicker)
1180 if mobs.invis[clicker:get_player_name()] then
1181 return false
1184 local item = clicker:get_wielded_item()
1185 local t = type(self.follow)
1187 -- single item
1188 if t == "string"
1189 and item:get_name() == self.follow then
1190 return true
1192 -- multiple items
1193 elseif t == "table" then
1195 for no = 1, #self.follow do
1197 if self.follow[no] == item:get_name() then
1198 return true
1203 return false
1207 -- find two animals of same type and breed if nearby and horny
1208 local breed = function(self)
1210 -- child takes 240 seconds before growing into adult
1211 if self.child == true then
1213 self.hornytimer = self.hornytimer + 1
1215 if self.hornytimer > 240 then
1217 self.child = false
1218 self.hornytimer = 0
1220 self.object:set_properties({
1221 textures = self.base_texture,
1222 mesh = self.base_mesh,
1223 visual_size = self.base_size,
1224 collisionbox = self.base_colbox,
1225 selectionbox = self.base_selbox,
1228 -- custom function when child grows up
1229 if self.on_grown then
1230 self.on_grown(self)
1231 else
1232 -- jump when fully grown so as not to fall into ground
1233 self.object:set_velocity({
1234 x = 0,
1235 y = self.jump_height,
1236 z = 0
1241 return
1244 -- horny animal can mate for 40 seconds,
1245 -- afterwards horny animal cannot mate again for 200 seconds
1246 if self.horny == true
1247 and self.hornytimer < 240 then
1249 self.hornytimer = self.hornytimer + 1
1251 if self.hornytimer >= 240 then
1252 self.hornytimer = 0
1253 self.horny = false
1257 -- find another same animal who is also horny and mate if nearby
1258 if self.horny == true
1259 and self.hornytimer <= 40 then
1261 local pos = self.object:get_pos()
1263 effect({x = pos.x, y = pos.y + 1, z = pos.z}, 8, "heart.png", 3, 4, 1, 0.1)
1265 local objs = minetest.get_objects_inside_radius(pos, 3)
1266 local num = 0
1267 local ent = nil
1269 for n = 1, #objs do
1271 ent = objs[n]:get_luaentity()
1273 -- check for same animal with different colour
1274 local canmate = false
1276 if ent then
1278 if ent.name == self.name then
1279 canmate = true
1280 else
1281 local entname = string.split(ent.name,":")
1282 local selfname = string.split(self.name,":")
1284 if entname[1] == selfname[1] then
1285 entname = string.split(entname[2],"_")
1286 selfname = string.split(selfname[2],"_")
1288 if entname[1] == selfname[1] then
1289 canmate = true
1295 if ent
1296 and canmate == true
1297 and ent.horny == true
1298 and ent.hornytimer <= 40 then
1299 num = num + 1
1302 -- found your mate? then have a baby
1303 if num > 1 then
1305 self.hornytimer = 41
1306 ent.hornytimer = 41
1308 -- spawn baby
1309 minetest.after(5, function(parent1, parent2, pos)
1310 if not parent1.object:get_luaentity() then
1311 return
1313 if not parent2.object:get_luaentity() then
1314 return
1317 -- custom breed function
1318 if parent1.on_breed then
1319 -- when false, skip going any further
1320 if parent1.on_breed(parent1, parent2) == false then
1321 return
1325 local child = mobs:spawn_child(pos, parent1.name)
1327 local ent_c = child:get_luaentity()
1330 -- Use texture of one of the parents
1331 local p = math.random(1, 2)
1332 if p == 1 then
1333 ent_c.base_texture = parent1.base_texture
1334 else
1335 ent_c.base_texture = parent2.base_texture
1337 child:set_properties({
1338 textures = ent_c.base_texture
1341 -- tamed and owned by parents' owner
1342 ent_c.tamed = true
1343 ent_c.owner = parent1.owner
1344 end, self, ent, pos)
1346 num = 0
1348 break
1355 -- find and replace what mob is looking for (grass, wheat etc.)
1356 local replace = function(self, pos)
1358 if not self.replace_rate
1359 or not self.replace_what
1360 or self.child == true
1361 or self.object:get_velocity().y ~= 0
1362 or random(1, self.replace_rate) > 1 then
1363 return
1366 local what, with, y_offset
1368 if type(self.replace_what[1]) == "table" then
1370 local num = random(#self.replace_what)
1372 what = self.replace_what[num][1] or ""
1373 with = self.replace_what[num][2] or ""
1374 y_offset = self.replace_what[num][3] or 0
1375 else
1376 what = self.replace_what
1377 with = self.replace_with or ""
1378 y_offset = self.replace_offset or 0
1381 pos.y = pos.y + y_offset
1383 if #minetest.find_nodes_in_area(pos, pos, what) > 0 then
1385 local oldnode = {name = what}
1386 local newnode = {name = with}
1387 local on_replace_return
1389 if self.on_replace then
1390 on_replace_return = self.on_replace(self, pos, oldnode, newnode)
1393 if on_replace_return ~= false then
1395 if mobs_griefing then
1396 minetest.set_node(pos, {name = with})
1404 -- check if daytime and also if mob is docile during daylight hours
1405 local day_docile = function(self)
1407 if self.docile_by_day == false then
1409 return false
1411 elseif self.docile_by_day == true
1412 and self.time_of_day > 0.2
1413 and self.time_of_day < 0.8 then
1415 return true
1420 local los_switcher = false
1421 local height_switcher = false
1423 -- path finding and smart mob routine by rnd, line_of_sight and other edits by Elkien3
1424 local smart_mobs = function(self, s, p, dist, dtime)
1426 local s1 = self.path.lastpos
1428 local target_pos = self.attack:get_pos()
1430 -- is it becoming stuck?
1431 if abs(s1.x - s.x) + abs(s1.z - s.z) < .5 then
1432 self.path.stuck_timer = self.path.stuck_timer + dtime
1433 else
1434 self.path.stuck_timer = 0
1437 self.path.lastpos = {x = s.x, y = s.y, z = s.z}
1439 local use_pathfind = false
1440 local has_lineofsight = minetest.line_of_sight(
1441 {x = s.x, y = (s.y) + .5, z = s.z},
1442 {x = target_pos.x, y = (target_pos.y) + 1.5, z = target_pos.z}, .2)
1444 -- im stuck, search for path
1445 if not has_lineofsight then
1447 if los_switcher == true then
1448 use_pathfind = true
1449 los_switcher = false
1450 end -- cannot see target!
1451 else
1452 if los_switcher == false then
1454 los_switcher = true
1455 use_pathfind = false
1457 minetest.after(1, function(self)
1458 if not self.object:get_luaentity() then
1459 return
1461 if has_lineofsight then self.path.following = false end
1462 end, self)
1463 end -- can see target!
1466 if (self.path.stuck_timer > stuck_timeout and not self.path.following) then
1468 use_pathfind = true
1469 self.path.stuck_timer = 0
1471 minetest.after(1, function(self)
1472 if not self.object:get_luaentity() then
1473 return
1475 if has_lineofsight then self.path.following = false end
1476 end, self)
1479 if (self.path.stuck_timer > stuck_path_timeout and self.path.following) then
1481 use_pathfind = true
1482 self.path.stuck_timer = 0
1484 minetest.after(1, function(self)
1485 if not self.object:get_luaentity() then
1486 return
1488 if has_lineofsight then self.path.following = false end
1489 end, self)
1492 if math.abs(vector.subtract(s,target_pos).y) > self.stepheight then
1494 if height_switcher then
1495 use_pathfind = true
1496 height_switcher = false
1498 else
1499 if not height_switcher then
1500 use_pathfind = false
1501 height_switcher = true
1505 if use_pathfind then
1506 -- lets try find a path, first take care of positions
1507 -- since pathfinder is very sensitive
1508 local sheight = self.collisionbox[5] - self.collisionbox[2]
1510 -- round position to center of node to avoid stuck in walls
1511 -- also adjust height for player models!
1512 s.x = floor(s.x + 0.5)
1513 s.z = floor(s.z + 0.5)
1515 local ssight, sground = minetest.line_of_sight(s, {
1516 x = s.x, y = s.y - 4, z = s.z}, 1)
1518 -- determine node above ground
1519 if not ssight then
1520 s.y = sground.y + 1
1523 local p1 = self.attack:get_pos()
1525 p1.x = floor(p1.x + 0.5)
1526 p1.y = floor(p1.y + 0.5)
1527 p1.z = floor(p1.z + 0.5)
1529 local dropheight = 12
1530 if self.fear_height ~= 0 then dropheight = self.fear_height end
1531 local jumpheight = 0
1532 if self.jump and self.jump_height >= 4 then
1533 jumpheight = math.min(math.ceil(self.jump_height / 4), 4)
1534 elseif self.stepheight > 0.5 then
1535 jumpheight = 1
1537 self.path.way = minetest.find_path(s, p1, 16, jumpheight, dropheight, "A*_noprefetch")
1539 self.state = ""
1540 do_attack(self, self.attack)
1542 -- no path found, try something else
1543 if not self.path.way then
1545 self.path.following = false
1547 -- lets make way by digging/building if not accessible
1548 if self.pathfinding == 2 and mobs_griefing then
1550 -- is player higher than mob?
1551 if s.y < p1.y then
1553 -- build upwards
1554 if not minetest.is_protected(s, "") then
1556 local ndef1 = minetest.registered_nodes[self.standing_in]
1558 if ndef1 and (ndef1.buildable_to or ndef1.groups.liquid) then
1560 minetest.set_node(s, {name = mobs.fallback_node})
1564 local sheight = math.ceil(self.collisionbox[5]) + 1
1566 -- assume mob is 2 blocks high so it digs above its head
1567 s.y = s.y + sheight
1569 -- remove one block above to make room to jump
1570 if not minetest.is_protected(s, "") then
1572 local node1 = node_ok(s, "air").name
1573 local ndef1 = minetest.registered_nodes[node1]
1575 if node1 ~= "air"
1576 and node1 ~= "ignore"
1577 and ndef1
1578 and not ndef1.groups.level
1579 and not ndef1.groups.unbreakable
1580 and not ndef1.groups.liquid then
1582 minetest.set_node(s, {name = "air"})
1583 minetest.add_item(s, ItemStack(node1))
1588 s.y = s.y - sheight
1589 self.object:set_pos({x = s.x, y = s.y + 2, z = s.z})
1591 else -- dig 2 blocks to make door toward player direction
1593 local yaw1 = self.object:get_yaw() + pi / 2
1594 local p1 = {
1595 x = s.x + cos(yaw1),
1596 y = s.y,
1597 z = s.z + sin(yaw1)
1600 if not minetest.is_protected(p1, "") then
1602 local node1 = node_ok(p1, "air").name
1603 local ndef1 = minetest.registered_nodes[node1]
1605 if node1 ~= "air"
1606 and node1 ~= "ignore"
1607 and ndef1
1608 and not ndef1.groups.level
1609 and not ndef1.groups.unbreakable
1610 and not ndef1.groups.liquid then
1612 minetest.add_item(p1, ItemStack(node1))
1613 minetest.set_node(p1, {name = "air"})
1616 p1.y = p1.y + 1
1617 node1 = node_ok(p1, "air").name
1618 ndef1 = minetest.registered_nodes[node1]
1620 if node1 ~= "air"
1621 and node1 ~= "ignore"
1622 and ndef1
1623 and not ndef1.groups.level
1624 and not ndef1.groups.unbreakable
1625 and not ndef1.groups.liquid then
1627 minetest.add_item(p1, ItemStack(node1))
1628 minetest.set_node(p1, {name = "air"})
1635 -- will try again in 2 seconds
1636 self.path.stuck_timer = stuck_timeout - 2
1637 elseif s.y < p1.y and (not self.fly) then
1638 do_jump(self) --add jump to pathfinding
1639 self.path.following = true
1640 -- Yay, I found path!
1641 -- TODO: Implement war_cry sound without being annoying
1642 --mob_sound(self, "war_cry", true)
1643 else
1644 set_velocity(self, self.walk_velocity)
1646 -- follow path now that it has it
1647 self.path.following = true
1653 -- specific attacks
1654 local specific_attack = function(list, what)
1656 -- no list so attack default (player, animals etc.)
1657 if list == nil then
1658 return true
1661 -- found entity on list to attack?
1662 for no = 1, #list do
1664 if list[no] == what then
1665 return true
1669 return false
1672 -- monster find someone to attack
1673 local monster_attack = function(self)
1675 if self.type ~= "monster"
1676 or not damage_enabled
1677 or minetest.is_creative_enabled("")
1678 or self.passive
1679 or self.state == "attack"
1680 or day_docile(self) then
1681 return
1684 local s = self.object:get_pos()
1685 local p, sp, dist
1686 local player, obj, min_player
1687 local type, name = "", ""
1688 local min_dist = self.view_range + 1
1689 local objs = minetest.get_objects_inside_radius(s, self.view_range)
1691 for n = 1, #objs do
1693 if objs[n]:is_player() then
1695 if mobs.invis[ objs[n]:get_player_name() ] or (not object_in_range(self, objs[n])) then
1696 type = ""
1697 else
1698 player = objs[n]
1699 type = "player"
1700 name = "player"
1702 else
1703 obj = objs[n]:get_luaentity()
1705 if obj then
1706 player = obj.object
1707 type = obj.type
1708 name = obj.name or ""
1712 -- find specific mob to attack, failing that attack player/npc/animal
1713 if specific_attack(self.specific_attack, name)
1714 and (type == "player" or type == "npc"
1715 or (type == "animal" and self.attack_animals == true)) then
1717 p = player:get_pos()
1718 sp = s
1720 dist = vector.distance(p, s)
1722 -- aim higher to make looking up hills more realistic
1723 p.y = p.y + 1
1724 sp.y = sp.y + 1
1727 -- choose closest player to attack
1728 if dist < min_dist
1729 and line_of_sight(self, sp, p, 2) == true then
1730 min_dist = dist
1731 min_player = player
1736 -- attack player
1737 if min_player then
1738 do_attack(self, min_player)
1743 -- npc, find closest monster to attack
1744 local npc_attack = function(self)
1746 if self.type ~= "npc"
1747 or not self.attacks_monsters
1748 or self.state == "attack" then
1749 return
1752 local p, sp, obj, min_player
1753 local s = self.object:get_pos()
1754 local min_dist = self.view_range + 1
1755 local objs = minetest.get_objects_inside_radius(s, self.view_range)
1757 for n = 1, #objs do
1759 obj = objs[n]:get_luaentity()
1761 if obj and obj.type == "monster" then
1763 p = obj.object:get_pos()
1764 sp = s
1766 local dist = vector.distance(p, s)
1768 -- aim higher to make looking up hills more realistic
1769 p.y = p.y + 1
1770 sp.y = sp.y + 1
1772 if dist < min_dist
1773 and line_of_sight(self, sp, p, 2) == true then
1774 min_dist = dist
1775 min_player = obj.object
1780 if min_player then
1781 do_attack(self, min_player)
1786 -- specific runaway
1787 local specific_runaway = function(list, what)
1789 -- no list so do not run
1790 if list == nil then
1791 return false
1794 -- found entity on list to attack?
1795 for no = 1, #list do
1797 if list[no] == what then
1798 return true
1802 return false
1806 -- find someone to runaway from
1807 local runaway_from = function(self)
1809 if not self.runaway_from then
1810 return
1813 local s = self.object:get_pos()
1814 local p, sp, dist
1815 local player, obj, min_player
1816 local type, name = "", ""
1817 local min_dist = self.view_range + 1
1818 local objs = minetest.get_objects_inside_radius(s, self.view_range)
1820 for n = 1, #objs do
1822 if objs[n]:is_player() then
1824 if mobs.invis[ objs[n]:get_player_name() ]
1825 or self.owner == objs[n]:get_player_name()
1826 or (not object_in_range(self, objs[n])) then
1827 type = ""
1828 else
1829 player = objs[n]
1830 type = "player"
1831 name = "player"
1833 else
1834 obj = objs[n]:get_luaentity()
1836 if obj then
1837 player = obj.object
1838 type = obj.type
1839 name = obj.name or ""
1843 -- find specific mob to runaway from
1844 if name ~= "" and name ~= self.name
1845 and specific_runaway(self.runaway_from, name) then
1847 p = player:get_pos()
1848 sp = s
1850 -- aim higher to make looking up hills more realistic
1851 p.y = p.y + 1
1852 sp.y = sp.y + 1
1854 dist = vector.distance(p, s)
1857 -- choose closest player/mpb to runaway from
1858 if dist < min_dist
1859 and line_of_sight(self, sp, p, 2) == true then
1860 min_dist = dist
1861 min_player = player
1866 if min_player then
1868 local lp = player:get_pos()
1869 local vec = {
1870 x = lp.x - s.x,
1871 y = lp.y - s.y,
1872 z = lp.z - s.z
1875 local yaw = (atan(vec.z / vec.x) + 3 * pi / 2) - self.rotate
1877 if lp.x > s.x then
1878 yaw = yaw + pi
1881 yaw = set_yaw(self, yaw, 4)
1882 self.state = "runaway"
1883 self.runaway_timer = 3
1884 self.following = nil
1889 -- follow player if owner or holding item, if fish outta water then flop
1890 local follow_flop = function(self)
1892 -- find player to follow
1893 if (self.follow ~= ""
1894 or self.order == "follow")
1895 and not self.following
1896 and self.state ~= "attack"
1897 and self.state ~= "runaway" then
1899 local s = self.object:get_pos()
1900 local players = minetest.get_connected_players()
1902 for n = 1, #players do
1904 if (object_in_range(self, players[n]))
1905 and not mobs.invis[ players[n]:get_player_name() ] then
1907 self.following = players[n]
1909 break
1914 if self.type == "npc"
1915 and self.order == "follow"
1916 and self.state ~= "attack"
1917 and self.owner ~= "" then
1919 -- npc stop following player if not owner
1920 if self.following
1921 and self.owner
1922 and self.owner ~= self.following:get_player_name() then
1923 self.following = nil
1925 else
1926 -- stop following player if not holding specific item
1927 if self.following
1928 and self.following:is_player()
1929 and follow_holding(self, self.following) == false then
1930 self.following = nil
1935 -- follow that thing
1936 if self.following then
1938 local s = self.object:get_pos()
1939 local p
1941 if self.following:is_player() then
1943 p = self.following:get_pos()
1945 elseif self.following.object then
1947 p = self.following.object:get_pos()
1950 if p then
1952 local dist = vector.distance(p, s)
1954 -- dont follow if out of range
1955 if (not object_in_range(self, self.following)) then
1956 self.following = nil
1957 else
1958 local vec = {
1959 x = p.x - s.x,
1960 z = p.z - s.z
1963 local yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
1965 if p.x > s.x then yaw = yaw + pi end
1967 set_yaw(self, yaw, 2.35)
1969 -- anyone but standing npc's can move along
1970 if dist > 3
1971 and self.order ~= "stand" then
1973 set_velocity(self, self.follow_velocity)
1975 if self.walk_chance ~= 0 then
1976 set_animation(self, "run")
1978 else
1979 set_velocity(self, 0)
1980 set_animation(self, "stand")
1983 return
1988 -- swimmers flop when out of their element, and swim again when back in
1989 if self.fly then
1990 local s = self.object:get_pos()
1991 if not flight_check(self, s) then
1993 self.state = "flop"
1994 self.object:set_velocity({x = 0, y = -5, z = 0})
1996 set_animation(self, "stand")
1998 return
1999 elseif self.state == "flop" then
2000 self.state = "stand"
2006 -- dogshoot attack switch and counter function
2007 local dogswitch = function(self, dtime)
2009 -- switch mode not activated
2010 if not self.dogshoot_switch
2011 or not dtime then
2012 return 0
2015 self.dogshoot_count = self.dogshoot_count + dtime
2017 if (self.dogshoot_switch == 1
2018 and self.dogshoot_count > self.dogshoot_count_max)
2019 or (self.dogshoot_switch == 2
2020 and self.dogshoot_count > self.dogshoot_count2_max) then
2022 self.dogshoot_count = 0
2024 if self.dogshoot_switch == 1 then
2025 self.dogshoot_switch = 2
2026 else
2027 self.dogshoot_switch = 1
2031 return self.dogshoot_switch
2035 -- execute current state (stand, walk, run, attacks)
2036 -- returns true if mob has died
2037 local do_states = function(self, dtime)
2039 local yaw = self.object:get_yaw() or 0
2041 if self.state == "stand" then
2043 if random(1, 4) == 1 then
2045 local lp = nil
2046 local s = self.object:get_pos()
2047 local objs = minetest.get_objects_inside_radius(s, 3)
2049 for n = 1, #objs do
2051 if objs[n]:is_player() then
2052 lp = objs[n]:get_pos()
2053 break
2057 -- look at any players nearby, otherwise turn randomly
2058 if lp then
2060 local vec = {
2061 x = lp.x - s.x,
2062 z = lp.z - s.z
2065 yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
2067 if lp.x > s.x then yaw = yaw + pi end
2068 else
2069 yaw = yaw + random(-0.5, 0.5)
2072 yaw = set_yaw(self, yaw, 8)
2075 set_velocity(self, 0)
2076 set_animation(self, "stand")
2078 -- npc's ordered to stand stay standing
2079 if self.type ~= "npc"
2080 or self.order ~= "stand" then
2082 if self.walk_chance ~= 0
2083 and self.facing_fence ~= true
2084 and random(1, 100) <= self.walk_chance
2085 and is_at_cliff_or_danger(self) == false then
2087 set_velocity(self, self.walk_velocity)
2088 self.state = "walk"
2089 set_animation(self, "walk")
2093 elseif self.state == "walk" then
2095 local s = self.object:get_pos()
2096 local lp = nil
2098 -- is there something I need to avoid?
2099 if (self.water_damage > 0
2100 and self.lava_damage > 0)
2101 or self.breath_max ~= -1 then
2103 lp = minetest.find_node_near(s, 1, {"group:water", "group:lava"})
2105 elseif self.water_damage > 0 then
2107 lp = minetest.find_node_near(s, 1, {"group:water"})
2109 elseif self.lava_damage > 0 then
2111 lp = minetest.find_node_near(s, 1, {"group:lava"})
2113 elseif self.fire_damage > 0 then
2115 lp = minetest.find_node_near(s, 1, {"group:fire"})
2119 local is_in_danger = false
2120 if lp then
2121 -- If mob in or on dangerous block, look for land
2122 if (is_node_dangerous(self, self.standing_in) or
2123 is_node_dangerous(self, self.standing_on)) or (is_node_waterhazard(self, self.standing_in) or is_node_waterhazard(self, self.standing_on)) and (not self.fly) then
2124 is_in_danger = true
2126 -- If mob in or on dangerous block, look for land
2127 if is_in_danger then
2128 -- Better way to find shore - copied from upstream
2129 lp = minetest.find_nodes_in_area_under_air(
2130 {x = s.x - 5, y = s.y - 0.5, z = s.z - 5},
2131 {x = s.x + 5, y = s.y + 1, z = s.z + 5},
2132 {"group:solid"})
2134 lp = #lp > 0 and lp[random(#lp)]
2136 -- did we find land?
2137 if lp then
2139 local vec = {
2140 x = lp.x - s.x,
2141 z = lp.z - s.z
2144 yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
2147 if lp.x > s.x then yaw = yaw + pi end
2149 -- look towards land and move in that direction
2150 yaw = set_yaw(self, yaw, 6)
2151 set_velocity(self, self.walk_velocity)
2156 -- A danger is near but mob is not inside
2157 else
2159 -- Randomly turn
2160 if random(1, 100) <= 30 then
2161 yaw = yaw + random(-0.5, 0.5)
2162 yaw = set_yaw(self, yaw, 8)
2166 yaw = set_yaw(self, yaw, 8)
2168 -- otherwise randomly turn
2169 elseif random(1, 100) <= 30 then
2171 yaw = yaw + random(-0.5, 0.5)
2172 yaw = set_yaw(self, yaw, 8)
2175 -- stand for great fall or danger or fence in front
2176 local cliff_or_danger = false
2177 if is_in_danger then
2178 cliff_or_danger = is_at_cliff_or_danger(self)
2180 if self.facing_fence == true
2181 or cliff_or_danger
2182 or random(1, 100) <= 30 then
2184 set_velocity(self, 0)
2185 self.state = "stand"
2186 set_animation(self, "stand")
2187 else
2189 set_velocity(self, self.walk_velocity)
2191 if flight_check(self)
2192 and self.animation
2193 and self.animation.fly_start
2194 and self.animation.fly_end then
2195 set_animation(self, "fly")
2196 else
2197 set_animation(self, "walk")
2201 -- runaway when punched
2202 elseif self.state == "runaway" then
2204 self.runaway_timer = self.runaway_timer + 1
2206 -- stop after 5 seconds or when at cliff
2207 if self.runaway_timer > 5
2208 or is_at_cliff_or_danger(self) then
2209 self.runaway_timer = 0
2210 set_velocity(self, 0)
2211 self.state = "stand"
2212 set_animation(self, "stand")
2213 else
2214 set_velocity(self, self.run_velocity)
2215 set_animation(self, "run")
2218 -- attack routines (explode, dogfight, shoot, dogshoot)
2219 elseif self.state == "attack" then
2221 -- calculate distance from mob and enemy
2222 local s = self.object:get_pos()
2223 local p = self.attack:get_pos() or s
2224 local dist = vector.distance(p, s)
2226 -- stop attacking if player invisible or out of range
2227 if not self.attack
2228 or not self.attack:get_pos()
2229 or not object_in_range(self, self.attack)
2230 or self.attack:get_hp() <= 0
2231 or (self.attack:is_player() and mobs.invis[ self.attack:get_player_name() ]) then
2233 self.state = "stand"
2234 set_velocity(self, 0)
2235 set_animation(self, "stand")
2236 self.attack = nil
2237 self.v_start = false
2238 self.timer = 0
2239 self.blinktimer = 0
2240 self.path.way = nil
2242 return
2245 if self.attack_type == "explode" then
2247 local vec = {
2248 x = p.x - s.x,
2249 z = p.z - s.z
2252 yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
2254 if p.x > s.x then yaw = yaw + pi end
2256 yaw = set_yaw(self, yaw)
2258 local node_break_radius = self.explosion_radius or 1
2259 local entity_damage_radius = self.explosion_damage_radius
2260 or (node_break_radius * 2)
2262 -- start timer when in reach and line of sight
2263 if not self.v_start
2264 and dist <= self.reach
2265 and line_of_sight(self, s, p, 2) then
2267 self.v_start = true
2268 self.timer = 0
2269 self.blinktimer = 0
2270 mob_sound(self, "fuse", nil, false)
2272 -- stop timer if out of reach or direct line of sight
2273 elseif self.allow_fuse_reset
2274 and self.v_start
2275 and (dist > self.reach
2276 or not line_of_sight(self, s, p, 2)) then
2277 self.v_start = false
2278 self.timer = 0
2279 self.blinktimer = 0
2280 self.blinkstatus = false
2281 remove_texture_mod(self, "^[brighten")
2284 -- walk right up to player unless the timer is active
2285 if self.v_start and (self.stop_to_explode or dist < 1.5) then
2286 set_velocity(self, 0)
2287 else
2288 set_velocity(self, self.run_velocity)
2291 if self.animation and self.animation.run_start then
2292 set_animation(self, "run")
2293 else
2294 set_animation(self, "walk")
2297 if self.v_start then
2299 self.timer = self.timer + dtime
2300 self.blinktimer = (self.blinktimer or 0) + dtime
2302 if self.blinktimer > 0.2 then
2304 self.blinktimer = 0
2306 if self.blinkstatus then
2307 remove_texture_mod(self, "^[brighten")
2308 else
2309 add_texture_mod(self, "^[brighten")
2312 self.blinkstatus = not self.blinkstatus
2315 if self.timer > self.explosion_timer then
2317 local pos = self.object:get_pos()
2319 if mod_explosions then
2320 if mobs_griefing and not minetest.is_protected(pos, "") then
2321 mcl_explosions.explode(self.object:get_pos(), self.explosion_strength, { drop_chance = 1.0 }, self.object)
2322 else
2323 minetest.sound_play(self.sounds.explode, {
2324 pos = pos,
2325 gain = 1.0,
2326 max_hear_distance = self.sounds.distance or 32
2327 }, true)
2329 entity_physics(pos, entity_damage_radius)
2330 effect(pos, 32, "mcl_particles_smoke.png", nil, nil, node_break_radius, 1, 0)
2333 self.object:remove()
2335 return true
2339 elseif self.attack_type == "dogfight"
2340 or (self.attack_type == "dogshoot" and dogswitch(self, dtime) == 2)
2341 or (self.attack_type == "dogshoot" and dist <= self.reach and dogswitch(self) == 0) then
2343 if self.fly
2344 and dist > self.reach then
2346 local p1 = s
2347 local me_y = floor(p1.y)
2348 local p2 = p
2349 local p_y = floor(p2.y + 1)
2350 local v = self.object:get_velocity()
2352 if flight_check(self, s) then
2354 if me_y < p_y then
2356 self.object:set_velocity({
2357 x = v.x,
2358 y = 1 * self.walk_velocity,
2359 z = v.z
2362 elseif me_y > p_y then
2364 self.object:set_velocity({
2365 x = v.x,
2366 y = -1 * self.walk_velocity,
2367 z = v.z
2370 else
2371 if me_y < p_y then
2373 self.object:set_velocity({
2374 x = v.x,
2375 y = 0.01,
2376 z = v.z
2379 elseif me_y > p_y then
2381 self.object:set_velocity({
2382 x = v.x,
2383 y = -0.01,
2384 z = v.z
2391 -- rnd: new movement direction
2392 if self.path.following
2393 and self.path.way
2394 and self.attack_type ~= "dogshoot" then
2396 -- no paths longer than 50
2397 if #self.path.way > 50
2398 or dist < self.reach then
2399 self.path.following = false
2400 return
2403 local p1 = self.path.way[1]
2405 if not p1 then
2406 self.path.following = false
2407 return
2410 if abs(p1.x-s.x) + abs(p1.z - s.z) < 0.6 then
2411 -- reached waypoint, remove it from queue
2412 table.remove(self.path.way, 1)
2415 -- set new temporary target
2416 p = {x = p1.x, y = p1.y, z = p1.z}
2419 local vec = {
2420 x = p.x - s.x,
2421 z = p.z - s.z
2424 yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
2426 if p.x > s.x then yaw = yaw + pi end
2428 yaw = set_yaw(self, yaw)
2430 -- move towards enemy if beyond mob reach
2431 if dist > self.reach then
2433 -- path finding by rnd
2434 if self.pathfinding -- only if mob has pathfinding enabled
2435 and enable_pathfinding then
2437 smart_mobs(self, s, p, dist, dtime)
2440 if is_at_cliff_or_danger(self) then
2442 set_velocity(self, 0)
2443 set_animation(self, "stand")
2444 else
2446 if self.path.stuck then
2447 set_velocity(self, self.walk_velocity)
2448 else
2449 set_velocity(self, self.run_velocity)
2452 if self.animation and self.animation.run_start then
2453 set_animation(self, "run")
2454 else
2455 set_animation(self, "walk")
2459 else -- rnd: if inside reach range
2461 self.path.stuck = false
2462 self.path.stuck_timer = 0
2463 self.path.following = false -- not stuck anymore
2465 set_velocity(self, 0)
2467 if not self.custom_attack then
2469 if self.timer > 1 then
2471 self.timer = 0
2473 if self.double_melee_attack
2474 and random(1, 2) == 1 then
2475 set_animation(self, "punch2")
2476 else
2477 set_animation(self, "punch")
2480 local p2 = p
2481 local s2 = s
2483 p2.y = p2.y + .5
2484 s2.y = s2.y + .5
2486 if line_of_sight(self, p2, s2) == true then
2488 -- play attack sound
2489 mob_sound(self, "attack")
2491 -- punch player (or what player is attached to)
2492 local attached = self.attack:get_attach()
2493 if attached then
2494 self.attack = attached
2496 self.attack:punch(self.object, 1.0, {
2497 full_punch_interval = 1.0,
2498 damage_groups = {fleshy = self.damage}
2499 }, nil)
2502 else -- call custom attack every second
2503 if self.custom_attack
2504 and self.timer > 1 then
2506 self.timer = 0
2508 self.custom_attack(self, p)
2513 elseif self.attack_type == "shoot"
2514 or (self.attack_type == "dogshoot" and dogswitch(self, dtime) == 1)
2515 or (self.attack_type == "dogshoot" and dist > self.reach and dogswitch(self) == 0) then
2517 p.y = p.y - .5
2518 s.y = s.y + .5
2520 local dist = vector.distance(p, s)
2521 local vec = {
2522 x = p.x - s.x,
2523 y = p.y - s.y,
2524 z = p.z - s.z
2527 yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
2529 if p.x > s.x then yaw = yaw + pi end
2531 yaw = set_yaw(self, yaw)
2533 set_velocity(self, 0)
2535 if self.shoot_interval
2536 and self.timer > self.shoot_interval
2537 and random(1, 100) <= 60 then
2539 self.timer = 0
2540 set_animation(self, "shoot")
2542 -- play shoot attack sound
2543 mob_sound(self, "shoot_attack")
2545 local p = self.object:get_pos()
2547 p.y = p.y + (self.collisionbox[2] + self.collisionbox[5]) / 2
2549 -- Shoot arrow
2550 if minetest.registered_entities[self.arrow] then
2552 local arrow, ent
2553 local v = 1
2554 if not self.shoot_arrow then
2555 arrow = minetest.add_entity(p, self.arrow)
2556 ent = arrow:get_luaentity()
2557 if ent.velocity then
2558 v = ent.velocity
2560 ent.switch = 1
2561 ent.owner_id = tostring(self.object) -- add unique owner id to arrow
2564 local amount = (vec.x * vec.x + vec.y * vec.y + vec.z * vec.z) ^ 0.5
2565 -- offset makes shoot aim accurate
2566 vec.y = vec.y + self.shoot_offset
2567 vec.x = vec.x * (v / amount)
2568 vec.y = vec.y * (v / amount)
2569 vec.z = vec.z * (v / amount)
2570 if self.shoot_arrow then
2571 vec = vector.normalize(vec)
2572 self:shoot_arrow(p, vec)
2573 else
2574 arrow:set_velocity(vec)
2583 -- falling and fall damage
2584 -- returns true if mob died
2585 local falling = function(self, pos)
2587 if self.fly then
2588 return
2591 if mcl_portals ~= nil then
2592 if mcl_portals.nether_portal_cooloff[self.object] then
2593 return false -- mob has teleported through Nether portal - it's 99% not falling
2597 -- floating in water (or falling)
2598 local v = self.object:get_velocity()
2600 if v.y > 0 then
2602 -- apply gravity when moving up
2603 self.object:set_acceleration({
2604 x = 0,
2605 y = -10,
2606 z = 0
2609 elseif v.y <= 0 and v.y > self.fall_speed then
2611 -- fall downwards at set speed
2612 self.object:set_acceleration({
2613 x = 0,
2614 y = self.fall_speed,
2615 z = 0
2617 else
2618 -- stop accelerating once max fall speed hit
2619 self.object:set_acceleration({x = 0, y = 0, z = 0})
2622 -- in water then float up
2623 if minetest.registered_nodes[node_ok(pos).name].groups.water then
2625 if self.floats == 1 then
2627 self.object:set_acceleration({
2628 x = 0,
2629 y = -self.fall_speed / (max(1, v.y) ^ 2),
2630 z = 0
2633 else
2635 -- fall damage onto solid ground
2636 if self.fall_damage == 1
2637 and self.object:get_velocity().y == 0 then
2639 local d = (self.old_y or 0) - self.object:get_pos().y
2641 if d > 5 then
2643 local add = minetest.get_item_group(self.standing_on, "fall_damage_add_percent")
2644 local damage = d - 5
2645 if add ~= 0 then
2646 damage = damage + damage * (add/100)
2648 damage = floor(damage)
2649 if damage > 0 then
2650 self.health = self.health - damage
2652 effect(pos, 5, "mcl_particles_smoke.png", 1, 2, 2, nil)
2654 if check_for_death(self, "fall", {type = "fall"}) then
2655 return true
2660 self.old_y = self.object:get_pos().y
2665 local teleport = function(self, target)
2666 if self.do_teleport then
2667 if self.do_teleport(self, target) == false then
2668 return
2674 -- deal damage and effects when mob punched
2675 local mob_punch = function(self, hitter, tflp, tool_capabilities, dir)
2677 -- custom punch function
2678 if self.do_punch then
2680 -- when false skip going any further
2681 if self.do_punch(self, hitter, tflp, tool_capabilities, dir) == false then
2682 return
2686 -- error checking when mod profiling is enabled
2687 if not tool_capabilities then
2688 minetest.log("warning", "[mobs] Mod profiling enabled, damage not enabled")
2689 return
2692 -- is mob protected?
2693 if self.protected and hitter:is_player()
2694 and minetest.is_protected(self.object:get_pos(), hitter:get_player_name()) then
2695 return
2699 -- punch interval
2700 local weapon = hitter:get_wielded_item()
2701 local punch_interval = 1.4
2703 -- exhaust attacker
2704 if mod_hunger and hitter:is_player() then
2705 mcl_hunger.exhaust(hitter:get_player_name(), mcl_hunger.EXHAUST_ATTACK)
2708 -- calculate mob damage
2709 local damage = 0
2710 local armor = self.object:get_armor_groups() or {}
2711 local tmp
2713 -- quick error check incase it ends up 0 (serialize.h check test)
2714 if tflp == 0 then
2715 tflp = 0.2
2718 if use_cmi then
2719 damage = cmi.calculate_damage(self.object, hitter, tflp, tool_capabilities, dir)
2720 else
2722 for group,_ in pairs( (tool_capabilities.damage_groups or {}) ) do
2724 tmp = tflp / (tool_capabilities.full_punch_interval or 1.4)
2726 if tmp < 0 then
2727 tmp = 0.0
2728 elseif tmp > 1 then
2729 tmp = 1.0
2732 damage = damage + (tool_capabilities.damage_groups[group] or 0)
2733 * tmp * ((armor[group] or 0) / 100.0)
2737 -- check for tool immunity or special damage
2738 for n = 1, #self.immune_to do
2740 if self.immune_to[n][1] == weapon:get_name() then
2742 damage = self.immune_to[n][2] or 0
2743 break
2747 -- healing
2748 if damage <= -1 then
2749 self.health = self.health - floor(damage)
2750 return
2753 if use_cmi then
2755 local cancel = cmi.notify_punch(self.object, hitter, tflp, tool_capabilities, dir, damage)
2757 if cancel then return end
2760 if tool_capabilities then
2761 punch_interval = tool_capabilities.full_punch_interval or 1.4
2764 -- add weapon wear manually
2765 -- Required because we have custom health handling ("health" property)
2766 if minetest.is_creative_enabled("") ~= true
2767 and tool_capabilities then
2768 if tool_capabilities.punch_attack_uses then
2769 -- Without this delay, the wear does not work. Quite hacky ...
2770 minetest.after(0, function(name)
2771 local player = minetest.get_player_by_name(name)
2772 if not player then return end
2773 local weapon = hitter:get_wielded_item(player)
2774 local def = weapon:get_definition()
2775 if def.tool_capabilities and def.tool_capabilities.punch_attack_uses then
2776 local wear = floor(65535/tool_capabilities.punch_attack_uses)
2777 weapon:add_wear(wear)
2778 hitter:set_wielded_item(weapon)
2780 end, hitter:get_player_name())
2784 local die = false
2786 -- only play hit sound and show blood effects if damage is 1 or over
2787 if damage >= 1 then
2789 -- weapon sounds
2790 if weapon:get_definition().sounds ~= nil then
2792 local s = random(0, #weapon:get_definition().sounds)
2794 minetest.sound_play(weapon:get_definition().sounds[s], {
2795 object = self.object, --hitter,
2796 max_hear_distance = 8
2797 }, true)
2798 else
2799 minetest.sound_play("default_punch", {
2800 object = self.object, --hitter,
2801 max_hear_distance = 5
2802 }, true)
2805 damage_effect(self, damage)
2807 -- do damage
2808 self.health = self.health - floor(damage)
2810 -- skip future functions if dead, except alerting others
2811 if check_for_death(self, "hit", {type = "punch", puncher = hitter}) then
2812 die = true
2815 -- knock back effect (only on full punch)
2816 if not die
2817 and self.knock_back
2818 and tflp >= punch_interval then
2820 local v = self.object:get_velocity()
2821 local r = 1.4 - min(punch_interval, 1.4)
2822 local kb = r * 2.0
2823 local up = 2
2825 -- if already in air then dont go up anymore when hit
2826 if v.y > 0
2827 or self.fly then
2828 up = 0
2831 -- direction error check
2832 dir = dir or {x = 0, y = 0, z = 0}
2834 -- check if tool already has specific knockback value
2835 if tool_capabilities.damage_groups["knockback"] then
2836 kb = tool_capabilities.damage_groups["knockback"]
2837 else
2838 kb = kb * 1.5
2841 self.object:set_velocity({
2842 x = dir.x * kb,
2843 y = dir.y * kb + up,
2844 z = dir.z * kb
2847 self.pause_timer = 0.25
2849 end -- END if damage
2851 -- if skittish then run away
2852 if not die and self.runaway == true then
2854 local lp = hitter:get_pos()
2855 local s = self.object:get_pos()
2856 local vec = {
2857 x = lp.x - s.x,
2858 y = lp.y - s.y,
2859 z = lp.z - s.z
2862 local yaw = (atan(vec.z / vec.x) + 3 * pi / 2) - self.rotate
2864 if lp.x > s.x then
2865 yaw = yaw + pi
2868 yaw = set_yaw(self, yaw, 6)
2869 self.state = "runaway"
2870 self.runaway_timer = 0
2871 self.following = nil
2874 local name = hitter:get_player_name() or ""
2876 -- attack puncher and call other mobs for help
2877 if self.passive == false
2878 and self.state ~= "flop"
2879 and (self.child == false or self.type == "monster")
2880 and hitter:get_player_name() ~= self.owner
2881 and not mobs.invis[ name ] then
2883 if not die then
2884 -- attack whoever punched mob
2885 self.state = ""
2886 do_attack(self, hitter)
2889 -- alert others to the attack
2890 local objs = minetest.get_objects_inside_radius(hitter:get_pos(), self.view_range)
2891 local obj = nil
2893 for n = 1, #objs do
2895 obj = objs[n]:get_luaentity()
2897 if obj then
2899 -- only alert members of same mob or friends
2900 if obj.group_attack
2901 and obj.state ~= "attack"
2902 and obj.owner ~= name then
2903 if obj.name == self.name then
2904 do_attack(obj, hitter)
2905 elseif type(obj.group_attack) == "table" then
2906 for i=1, #obj.group_attack do
2907 if obj.name == obj.group_attack[i] then
2908 do_attack(obj, hitter)
2909 break
2915 -- have owned mobs attack player threat
2916 if obj.owner == name and obj.owner_loyal then
2917 do_attack(obj, self.object)
2924 local mob_detach_child = function(self, child)
2926 if self.driver == child then
2927 self.driver = nil
2932 -- get entity staticdata
2933 local mob_staticdata = function(self)
2935 -- remove mob when out of range unless tamed
2936 if remove_far
2937 and self.can_despawn
2938 and self.remove_ok
2939 and ((not self.nametag) or (self.nametag == ""))
2940 and self.lifetimer <= 20 then
2942 minetest.log("action", "Mob "..name.." despawns in mob_staticdata at "..minetest.pos_to_string(self.object.get_pos()))
2943 self.object:remove()
2945 return ""-- nil
2948 self.remove_ok = true
2949 self.attack = nil
2950 self.following = nil
2951 self.state = "stand"
2953 if use_cmi then
2954 self.serialized_cmi_components = cmi.serialize_components(self._cmi_components)
2957 local tmp = {}
2959 for _,stat in pairs(self) do
2961 local t = type(stat)
2963 if t ~= "function"
2964 and t ~= "nil"
2965 and t ~= "userdata"
2966 and _ ~= "_cmi_components" then
2967 tmp[_] = self[_]
2971 return minetest.serialize(tmp)
2975 -- activate mob and reload settings
2976 local mob_activate = function(self, staticdata, def, dtime)
2978 -- remove monsters in peaceful mode
2979 if self.type == "monster"
2980 and minetest.settings:get_bool("only_peaceful_mobs", false) then
2982 self.object:remove()
2984 return
2987 -- load entity variables
2988 local tmp = minetest.deserialize(staticdata)
2990 if tmp then
2991 for _,stat in pairs(tmp) do
2992 self[_] = stat
2996 -- select random texture, set model and size
2997 if not self.base_texture then
2999 -- compatiblity with old simple mobs textures
3000 if type(def.textures[1]) == "string" then
3001 def.textures = {def.textures}
3004 self.base_texture = def.textures[random(1, #def.textures)]
3005 self.base_mesh = def.mesh
3006 self.base_size = self.visual_size
3007 self.base_colbox = self.collisionbox
3008 self.base_selbox = self.selectionbox
3011 -- for current mobs that dont have this set
3012 if not self.base_selbox then
3013 self.base_selbox = self.selectionbox or self.base_colbox
3016 -- set texture, model and size
3017 local textures = self.base_texture
3018 local mesh = self.base_mesh
3019 local vis_size = self.base_size
3020 local colbox = self.base_colbox
3021 local selbox = self.base_selbox
3023 -- specific texture if gotten
3024 if self.gotten == true
3025 and def.gotten_texture then
3026 textures = def.gotten_texture
3029 -- specific mesh if gotten
3030 if self.gotten == true
3031 and def.gotten_mesh then
3032 mesh = def.gotten_mesh
3035 -- set child objects to half size
3036 if self.child == true then
3038 vis_size = {
3039 x = self.base_size.x * .5,
3040 y = self.base_size.y * .5,
3043 if def.child_texture then
3044 textures = def.child_texture[1]
3047 colbox = {
3048 self.base_colbox[1] * .5,
3049 self.base_colbox[2] * .5,
3050 self.base_colbox[3] * .5,
3051 self.base_colbox[4] * .5,
3052 self.base_colbox[5] * .5,
3053 self.base_colbox[6] * .5
3055 selbox = {
3056 self.base_selbox[1] * .5,
3057 self.base_selbox[2] * .5,
3058 self.base_selbox[3] * .5,
3059 self.base_selbox[4] * .5,
3060 self.base_selbox[5] * .5,
3061 self.base_selbox[6] * .5
3065 if self.health == 0 then
3066 self.health = random (self.hp_min, self.hp_max)
3068 if self.breath == nil then
3069 self.breath = self.breath_max
3072 -- pathfinding init
3073 self.path = {}
3074 self.path.way = {} -- path to follow, table of positions
3075 self.path.lastpos = {x = 0, y = 0, z = 0}
3076 self.path.stuck = false
3077 self.path.following = false -- currently following path?
3078 self.path.stuck_timer = 0 -- if stuck for too long search for path
3080 -- Armor groups
3081 -- immortal=1 because we use custom health
3082 -- handling (using "health" property)
3083 local armor
3084 if type(self.armor) == "table" then
3085 armor = table.copy(self.armor)
3086 armor.immortal = 1
3087 else
3088 armor = {immortal=1, fleshy = self.armor}
3090 self.object:set_armor_groups(armor)
3091 self.old_y = self.object:get_pos().y
3092 self.old_health = self.health
3093 self.sounds.distance = self.sounds.distance or 10
3094 self.textures = textures
3095 self.mesh = mesh
3096 self.collisionbox = colbox
3097 self.selectionbox = selbox
3098 self.visual_size = vis_size
3099 self.standing_in = "ignore"
3100 self.standing_on = "ignore"
3101 self.jump_sound_cooloff = 0 -- used to prevent jump sound from being played too often in short time
3102 self.opinion_sound_cooloff = 0 -- used to prevent sound spam of particular sound types
3104 self.texture_mods = {}
3105 self.object:set_texture_mod("")
3107 self.v_start = false
3108 self.timer = 0
3109 self.blinktimer = 0
3110 self.blinkstatus = false
3112 -- check existing nametag
3113 if not self.nametag then
3114 self.nametag = def.nametag
3117 -- set anything changed above
3118 self.object:set_properties(self)
3119 set_yaw(self, (random(0, 360) - 180) / 180 * pi, 6)
3120 update_tag(self)
3121 set_animation(self, "stand")
3123 -- run on_spawn function if found
3124 if self.on_spawn and not self.on_spawn_run then
3125 if self.on_spawn(self) then
3126 self.on_spawn_run = true -- if true, set flag to run once only
3130 -- run after_activate
3131 if def.after_activate then
3132 def.after_activate(self, staticdata, def, dtime)
3135 if use_cmi then
3136 self._cmi_components = cmi.activate_components(self.serialized_cmi_components)
3137 cmi.notify_activate(self.object, dtime)
3142 -- main mob function
3143 local mob_step = function(self, dtime)
3145 if use_cmi then
3146 cmi.notify_step(self.object, dtime)
3149 local pos = self.object:get_pos()
3150 local yaw = 0
3152 -- Despawning: when lifetimer expires, remove mob
3153 if remove_far
3154 and self.can_despawn == true
3155 and ((not self.nametag) or (self.nametag == "")) then
3157 -- TODO: Finish up implementation of despawning rules
3159 self.lifetimer = self.lifetimer - dtime
3161 if self.lifetimer <= 0 then
3163 -- only despawn away from player
3164 local objs = minetest.get_objects_inside_radius(pos, 32)
3166 for n = 1, #objs do
3168 if objs[n]:is_player() then
3170 self.lifetimer = 20
3172 return
3176 minetest.log("action", "Mob "..name.." despawns in mob_step at "..minetest.pos_to_string(pos))
3177 self.object:remove()
3179 return
3183 if self.jump_sound_cooloff > 0 then
3184 self.jump_sound_cooloff = self.jump_sound_cooloff - dtime
3186 if self.opinion_sound_cooloff > 0 then
3187 self.opinion_sound_cooloff = self.opinion_sound_cooloff - dtime
3189 if falling(self, pos) then
3190 -- Return if mob died after falling
3191 return
3194 -- smooth rotation by ThomasMonroe314
3196 if self.delay and self.delay > 0 then
3198 local yaw = self.object:get_yaw() or 0
3200 if self.delay == 1 then
3201 yaw = self.target_yaw
3202 else
3203 local dif = abs(yaw - self.target_yaw)
3205 if yaw > self.target_yaw then
3207 if dif > pi then
3208 dif = 2 * pi - dif -- need to add
3209 yaw = yaw + dif / self.delay
3210 else
3211 yaw = yaw - dif / self.delay -- need to subtract
3214 elseif yaw < self.target_yaw then
3216 if dif > pi then
3217 dif = 2 * pi - dif
3218 yaw = yaw - dif / self.delay -- need to subtract
3219 else
3220 yaw = yaw + dif / self.delay -- need to add
3224 if yaw > (pi * 2) then yaw = yaw - (pi * 2) end
3225 if yaw < 0 then yaw = yaw + (pi * 2) end
3228 self.delay = self.delay - 1
3229 self.object:set_yaw(yaw)
3232 -- end rotation
3234 -- knockback timer
3235 if self.pause_timer > 0 then
3237 self.pause_timer = self.pause_timer - dtime
3239 return
3242 -- run custom function (defined in mob lua file)
3243 if self.do_custom then
3245 -- when false skip going any further
3246 if self.do_custom(self, dtime) == false then
3247 return
3251 -- attack timer
3252 self.timer = self.timer + dtime
3254 if self.state ~= "attack" then
3256 if self.timer < 1 then
3257 return
3260 self.timer = 0
3263 -- never go over 100
3264 if self.timer > 100 then
3265 self.timer = 1
3268 -- mob plays random sound at times
3269 if random(1, 100) == 1 then
3270 mob_sound(self, "random", true)
3273 -- environmental damage timer (every 1 second)
3274 self.env_damage_timer = self.env_damage_timer + dtime
3276 if (self.state == "attack" and self.env_damage_timer > 1)
3277 or self.state ~= "attack" then
3279 self.env_damage_timer = 0
3281 -- check for environmental damage (water, fire, lava etc.)
3282 if do_env_damage(self) then
3283 return
3286 -- node replace check (cow eats grass etc.)
3287 replace(self, pos)
3290 monster_attack(self)
3292 npc_attack(self)
3294 breed(self)
3296 if do_states(self, dtime) then
3297 return
3300 do_jump(self)
3302 runaway_from(self)
3304 if is_at_water_danger(self) and self.state ~= "attack" then
3305 if random(1, 10) <= 6 then
3306 set_velocity(self, 0)
3307 self.state = "stand"
3308 set_animation(self, "stand")
3309 yaw = yaw + random(-0.5, 0.5)
3310 yaw = set_yaw(self, yaw, 8)
3314 follow_flop(self)
3316 if is_at_cliff_or_danger(self) then
3317 set_velocity(self, 0)
3318 self.state = "stand"
3319 set_animation(self, "stand")
3325 -- default function when mobs are blown up with TNT
3326 local do_tnt = function(obj, damage)
3328 obj.object:punch(obj.object, 1.0, {
3329 full_punch_interval = 1.0,
3330 damage_groups = {fleshy = damage},
3331 }, nil)
3333 return false, true, {}
3337 mobs.spawning_mobs = {}
3339 -- Code to execute before custom on_rightclick handling
3340 local on_rightclick_prefix = function(self, clicker)
3341 local item = clicker:get_wielded_item()
3343 -- Name mob with nametag
3344 if not self.ignores_nametag and item:get_name() == "mcl_mobs:nametag" then
3346 local tag = item:get_meta():get_string("name")
3347 if tag ~= "" then
3348 if string.len(tag) > MAX_MOB_NAME_LENGTH then
3349 tag = string.sub(tag, 1, MAX_MOB_NAME_LENGTH)
3351 self.nametag = tag
3353 update_tag(self)
3355 if not mobs.is_creative(clicker:get_player_name()) then
3356 item:take_item()
3357 clicker:set_wielded_item(item)
3359 return true
3363 return false
3366 local create_mob_on_rightclick = function(on_rightclick)
3367 return function(self, clicker)
3368 local stop = on_rightclick_prefix(self, clicker)
3369 if (not stop) and (on_rightclick) then
3370 on_rightclick(self, clicker)
3375 -- register mob entity
3376 function mobs:register_mob(name, def)
3378 mobs.spawning_mobs[name] = true
3380 local can_despawn
3381 if def.can_despawn ~= nil then
3382 can_despawn = def.can_despawn
3383 else
3384 can_despawn = true
3387 local function scale_difficulty(value, default, min, special)
3388 if (not value) or (value == default) or (value == special) then
3389 return default
3390 else
3391 return max(min, value * difficulty)
3395 local collisionbox = def.collisionbox or {-0.25, -0.25, -0.25, 0.25, 0.25, 0.25}
3396 -- Workaround for <https://github.com/minetest/minetest/issues/5966>:
3397 -- Increase upper Y limit to avoid mobs glitching through solid nodes.
3398 -- FIXME: Remove workaround if it's no longer needed.
3399 if collisionbox[5] < 0.79 then
3400 collisionbox[5] = 0.79
3403 minetest.register_entity(name, {
3405 stepheight = def.stepheight or 0.6,
3406 name = name,
3407 type = def.type,
3408 attack_type = def.attack_type,
3409 fly = def.fly,
3410 fly_in = def.fly_in or {"air", "__airlike"},
3411 owner = def.owner or "",
3412 order = def.order or "",
3413 on_die = def.on_die,
3414 spawn_small_alternative = def.spawn_small_alternative,
3415 do_custom = def.do_custom,
3416 jump_height = def.jump_height or 4, -- was 6
3417 rotate = math.rad(def.rotate or 0), -- 0=front, 90=side, 180=back, 270=side2
3418 lifetimer = def.lifetimer or 57.73,
3419 hp_min = scale_difficulty(def.hp_min, 5, 1),
3420 hp_max = scale_difficulty(def.hp_max, 10, 1),
3421 breath_max = def.breath_max or 15,
3422 breathes_in_water = def.breathes_in_water or false,
3423 physical = true,
3424 collisionbox = collisionbox,
3425 selectionbox = def.selectionbox or def.collisionbox,
3426 visual = def.visual,
3427 visual_size = def.visual_size or {x = 1, y = 1},
3428 mesh = def.mesh,
3429 makes_footstep_sound = def.makes_footstep_sound or false,
3430 view_range = def.view_range or 16,
3431 walk_velocity = def.walk_velocity or 1,
3432 run_velocity = def.run_velocity or 2,
3433 damage = scale_difficulty(def.damage, 0, 0),
3434 light_damage = def.light_damage or 0,
3435 sunlight_damage = def.sunlight_damage or 0,
3436 water_damage = def.water_damage or 0,
3437 lava_damage = def.lava_damage or 8,
3438 fire_damage = def.fire_damage or 1,
3439 suffocation = def.suffocation or true,
3440 fall_damage = def.fall_damage or 1,
3441 fall_speed = def.fall_speed or -10, -- must be lower than -2 (default: -10)
3442 drops = def.drops or {},
3443 armor = def.armor or 100,
3444 on_rightclick = create_mob_on_rightclick(def.on_rightclick),
3445 arrow = def.arrow,
3446 shoot_interval = def.shoot_interval,
3447 sounds = def.sounds or {},
3448 animation = def.animation,
3449 follow = def.follow,
3450 jump = def.jump ~= false,
3451 walk_chance = def.walk_chance or 50,
3452 attacks_monsters = def.attacks_monsters or false,
3453 group_attack = def.group_attack or false,
3454 passive = def.passive or false,
3455 knock_back = def.knock_back ~= false,
3456 shoot_offset = def.shoot_offset or 0,
3457 floats = def.floats or 1, -- floats in water by default
3458 replace_rate = def.replace_rate,
3459 replace_what = def.replace_what,
3460 replace_with = def.replace_with,
3461 replace_offset = def.replace_offset or 0,
3462 on_replace = def.on_replace,
3463 timer = 0,
3464 env_damage_timer = 0,
3465 tamed = false,
3466 pause_timer = 0,
3467 horny = false,
3468 hornytimer = 0,
3469 gotten = false,
3470 health = 0,
3471 reach = def.reach or 3,
3472 htimer = 0,
3473 texture_list = def.textures,
3474 child_texture = def.child_texture,
3475 docile_by_day = def.docile_by_day or false,
3476 time_of_day = 0.5,
3477 fear_height = def.fear_height or 0,
3478 runaway = def.runaway,
3479 runaway_timer = 0,
3480 pathfinding = def.pathfinding,
3481 immune_to = def.immune_to or {},
3482 explosion_radius = def.explosion_radius, -- LEGACY
3483 explosion_damage_radius = def.explosion_damage_radius, -- LEGACY
3484 explosion_timer = def.explosion_timer or 3,
3485 allow_fuse_reset = def.allow_fuse_reset ~= false,
3486 stop_to_explode = def.stop_to_explode ~= false,
3487 custom_attack = def.custom_attack,
3488 double_melee_attack = def.double_melee_attack,
3489 dogshoot_switch = def.dogshoot_switch,
3490 dogshoot_count = 0,
3491 dogshoot_count_max = def.dogshoot_count_max or 5,
3492 dogshoot_count2_max = def.dogshoot_count2_max or (def.dogshoot_count_max or 5),
3493 attack_animals = def.attack_animals or false,
3494 specific_attack = def.specific_attack,
3495 runaway_from = def.runaway_from,
3496 owner_loyal = def.owner_loyal,
3497 facing_fence = false,
3498 _cmi_is_mob = true,
3500 -- MCL2 extensions
3501 teleport = teleport,
3502 do_teleport = def.do_teleport,
3503 spawn_class = def.spawn_class,
3504 ignores_nametag = def.ignores_nametag or false,
3505 rain_damage = def.rain_damage or 0,
3506 glow = def.glow,
3507 can_despawn = can_despawn,
3508 child = def.child or false,
3509 texture_mods = {},
3510 shoot_arrow = def.shoot_arrow,
3511 sounds_child = def.sounds_child,
3512 explosion_strength = def.explosion_strength,
3513 suffocation_timer = 0,
3514 follow_velocity = def.follow_velocity or 2.4,
3515 -- End of MCL2 extensions
3517 on_spawn = def.on_spawn,
3519 on_blast = def.on_blast or do_tnt,
3521 on_step = mob_step,
3523 do_punch = def.do_punch,
3525 on_punch = mob_punch,
3527 on_breed = def.on_breed,
3529 on_grown = def.on_grown,
3531 on_detach_child = mob_detach_child,
3533 on_activate = function(self, staticdata, dtime)
3534 return mob_activate(self, staticdata, def, dtime)
3535 end,
3537 get_staticdata = function(self)
3538 return mob_staticdata(self)
3539 end,
3541 harmed_by_heal = def.harmed_by_heal,
3545 if minetest.get_modpath("doc_identifier") ~= nil then
3546 doc.sub.identifier.register_object(name, "basics", "mobs")
3549 end -- END mobs:register_mob function
3552 -- count how many mobs of one type are inside an area
3553 local count_mobs = function(pos, mobtype)
3555 local num = 0
3556 local objs = minetest.get_objects_inside_radius(pos, aoc_range)
3558 for n = 1, #objs do
3560 local obj = objs[n]:get_luaentity()
3562 if obj and obj.name and obj._cmi_is_mob then
3564 -- count passive mobs only
3565 if mobtype == "!passive" then
3566 if obj.spawn_class == "passive" then
3567 num = num + 1
3569 -- count hostile mobs only
3570 elseif mobtype == "!hostile" then
3571 if obj.spawn_class == "hostile" then
3572 num = num + 1
3574 -- count ambient mobs only
3575 elseif mobtype == "!ambient" then
3576 if obj.spawn_class == "ambient" then
3577 num = num + 1
3579 -- count water mobs only
3580 elseif mobtype == "!water" then
3581 if obj.spawn_class == "water" then
3582 num = num + 1
3584 -- count mob type
3585 elseif mobtype and obj.name == mobtype then
3586 num = num + 1
3587 -- count total mobs
3588 elseif not mobtype then
3589 num = num + 1
3594 return num
3598 -- global functions
3600 function mobs:spawn_abm_check(pos, node, name)
3601 -- global function to add additional spawn checks
3602 -- return true to stop spawning mob
3606 function mobs:spawn_specific(name, nodes, neighbors, min_light, max_light,
3607 interval, chance, aoc, min_height, max_height, day_toggle, on_spawn)
3609 -- Do mobs spawn at all?
3610 if not mobs_spawn then
3611 return
3614 -- chance/spawn number override in minetest.conf for registered mob
3615 local numbers = minetest.settings:get(name)
3617 if numbers then
3618 numbers = numbers:split(",")
3619 chance = tonumber(numbers[1]) or chance
3620 aoc = tonumber(numbers[2]) or aoc
3622 if chance == 0 then
3623 minetest.log("warning", string.format("[mobs] %s has spawning disabled", name))
3624 return
3627 minetest.log("action",
3628 string.format("[mobs] Chance setting for %s changed to %s (total: %s)", name, chance, aoc))
3632 local spawn_action
3633 spawn_action = function(pos, node, active_object_count, active_object_count_wider, name)
3635 local orig_pos = table.copy(pos)
3636 -- is mob actually registered?
3637 if not mobs.spawning_mobs[name]
3638 or not minetest.registered_entities[name] then
3639 minetest.log("warning", "Mob spawn of "..name.." failed, unknown entity or mob is not registered for spawning!")
3640 return
3643 -- additional custom checks for spawning mob
3644 if mobs:spawn_abm_check(pos, node, name) == true then
3645 minetest.log("info", "Mob spawn of "..name.." at "..minetest.pos_to_string(pos).." failed, ABM check rejected!")
3646 return
3649 -- count nearby mobs in same spawn class
3650 local entdef = minetest.registered_entities[name]
3651 local spawn_class = entdef and entdef.spawn_class
3652 if not spawn_class then
3653 if entdef.type == "monster" then
3654 spawn_class = "hostile"
3655 else
3656 spawn_class = "passive"
3659 local in_class_cap = count_mobs(pos, "!"..spawn_class) < MOB_CAP[spawn_class]
3660 -- do not spawn if too many of same mob in area
3661 if active_object_count_wider >= max_per_block -- large-range mob cap
3662 or (not in_class_cap) -- spawn class mob cap
3663 or count_mobs(pos, name) >= aoc then -- per-mob mob cap
3664 -- too many entities
3665 minetest.log("info", "Mob spawn of "..name.." at "..minetest.pos_to_string(pos).." failed, too crowded!")
3666 return
3669 -- if toggle set to nil then ignore day/night check
3670 if day_toggle ~= nil then
3672 local tod = (minetest.get_timeofday() or 0) * 24000
3674 if tod > 4500 and tod < 19500 then
3675 -- daylight, but mob wants night
3676 if day_toggle == false then
3677 -- mob needs night
3678 minetest.log("info", "Mob spawn of "..name.." at "..minetest.pos_to_string(pos).." failed, mob needs light!")
3679 return
3681 else
3682 -- night time but mob wants day
3683 if day_toggle == true then
3684 -- mob needs day
3685 minetest.log("info", "Mob spawn of "..name.." at "..minetest.pos_to_string(pos).." failed, mob needs daylight!")
3686 return
3691 -- spawn above node
3692 pos.y = pos.y + 1
3694 -- only spawn away from player
3695 local objs = minetest.get_objects_inside_radius(pos, 10)
3697 for n = 1, #objs do
3699 if objs[n]:is_player() then
3700 -- player too close
3701 minetest.log("info", "Mob spawn of "..name.." at "..minetest.pos_to_string(pos).." failed, player too close!")
3702 return
3706 -- mobs cannot spawn in protected areas when enabled
3707 if not spawn_protected
3708 and minetest.is_protected(pos, "") then
3709 minetest.log("info", "Mob spawn of "..name.." at "..minetest.pos_to_string(pos).." failed, position is protected!")
3710 return
3713 -- are we spawning within height limits?
3714 if pos.y > max_height
3715 or pos.y < min_height then
3716 minetest.log("info", "Mob spawn of "..name.." at "..minetest.pos_to_string(pos).." failed, out of height limit!")
3717 return
3720 -- are light levels ok?
3721 local light = minetest.get_node_light(pos)
3722 if not light
3723 or light > max_light
3724 or light < min_light then
3725 minetest.log("info", "Mob spawn of "..name.." at "..minetest.pos_to_string(pos).." failed, bad light!")
3726 return
3729 -- do we have enough space to spawn mob?
3730 local ent = minetest.registered_entities[name]
3731 local width_x = max(1, math.ceil(ent.collisionbox[4] - ent.collisionbox[1]))
3732 local min_x, max_x
3733 if width_x % 2 == 0 then
3734 max_x = math.floor(width_x/2)
3735 min_x = -(max_x-1)
3736 else
3737 max_x = math.floor(width_x/2)
3738 min_x = -max_x
3741 local width_z = max(1, math.ceil(ent.collisionbox[6] - ent.collisionbox[3]))
3742 local min_z, max_z
3743 if width_z % 2 == 0 then
3744 max_z = math.floor(width_z/2)
3745 min_z = -(max_z-1)
3746 else
3747 max_z = math.floor(width_z/2)
3748 min_z = -max_z
3751 local max_y = max(0, math.ceil(ent.collisionbox[5] - ent.collisionbox[2]) - 1)
3753 for y = 0, max_y do
3754 for x = min_x, max_x do
3755 for z = min_z, max_z do
3756 local pos2 = {x = pos.x+x, y = pos.y+y, z = pos.z+z}
3757 if minetest.registered_nodes[node_ok(pos2).name].walkable == true then
3758 -- inside block
3759 minetest.log("info", "Mob spawn of "..name.." at "..minetest.pos_to_string(pos).." failed, too little space!")
3760 if ent.spawn_small_alternative ~= nil and (not minetest.registered_nodes[node_ok(pos).name].walkable) then
3761 minetest.log("info", "Trying to spawn smaller alternative mob: "..ent.spawn_small_alternative)
3762 spawn_action(orig_pos, node, active_object_count, active_object_count_wider, ent.spawn_small_alternative)
3764 return
3770 -- spawn mob 1/2 node above ground
3771 pos.y = pos.y + 0.5
3772 -- tweak X/Z spawn pos
3773 if width_x % 2 == 0 then
3774 pos.x = pos.x + 0.5
3776 if width_z % 2 == 0 then
3777 pos.z = pos.z + 0.5
3780 local mob = minetest.add_entity(pos, name)
3781 minetest.log("action", "Mob spawned: "..name.." at "..minetest.pos_to_string(pos))
3783 if on_spawn then
3785 local ent = mob:get_luaentity()
3787 on_spawn(ent, pos)
3791 local function spawn_abm_action(pos, node, active_object_count, active_object_count_wider)
3792 spawn_action(pos, node, active_object_count, active_object_count_wider, name)
3795 minetest.register_abm({
3796 label = name .. " spawning",
3797 nodenames = nodes,
3798 neighbors = neighbors,
3799 interval = interval,
3800 chance = floor(max(1, chance * mobs_spawn_chance)),
3801 catch_up = false,
3802 action = spawn_abm_action,
3807 -- compatibility with older mob registration
3808 function mobs:register_spawn(name, nodes, max_light, min_light, chance, active_object_count, max_height, day_toggle)
3810 mobs:spawn_specific(name, nodes, {"air"}, min_light, max_light, 30,
3811 chance, active_object_count, -31000, max_height, day_toggle)
3815 -- MarkBu's spawn function
3816 function mobs:spawn(def)
3818 local name = def.name
3819 local nodes = def.nodes or {"group:soil", "group:stone"}
3820 local neighbors = def.neighbors or {"air"}
3821 local min_light = def.min_light or 0
3822 local max_light = def.max_light or 15
3823 local interval = def.interval or 30
3824 local chance = def.chance or 5000
3825 local active_object_count = def.active_object_count or 1
3826 local min_height = def.min_height or -31000
3827 local max_height = def.max_height or 31000
3828 local day_toggle = def.day_toggle
3829 local on_spawn = def.on_spawn
3831 mobs:spawn_specific(name, nodes, neighbors, min_light, max_light, interval,
3832 chance, active_object_count, min_height, max_height, day_toggle, on_spawn)
3836 -- register arrow for shoot attack
3837 function mobs:register_arrow(name, def)
3839 if not name or not def then return end -- errorcheck
3841 minetest.register_entity(name, {
3843 physical = false,
3844 visual = def.visual,
3845 visual_size = def.visual_size,
3846 textures = def.textures,
3847 velocity = def.velocity,
3848 hit_player = def.hit_player,
3849 hit_node = def.hit_node,
3850 hit_mob = def.hit_mob,
3851 hit_object = def.hit_object,
3852 drop = def.drop or false, -- drops arrow as registered item when true
3853 collisionbox = {0, 0, 0, 0, 0, 0}, -- remove box around arrows
3854 timer = 0,
3855 switch = 0,
3856 owner_id = def.owner_id,
3857 rotate = def.rotate,
3858 automatic_face_movement_dir = def.rotate
3859 and (def.rotate - (pi / 180)) or false,
3861 on_activate = def.on_activate,
3863 on_step = def.on_step or function(self, dtime)
3865 self.timer = self.timer + 1
3867 local pos = self.object:get_pos()
3869 if self.switch == 0
3870 or self.timer > 150
3871 or not within_limits(pos, 0) then
3873 self.object:remove();
3875 return
3878 -- does arrow have a tail (fireball)
3879 if def.tail
3880 and def.tail == 1
3881 and def.tail_texture then
3883 minetest.add_particle({
3884 pos = pos,
3885 velocity = {x = 0, y = 0, z = 0},
3886 acceleration = {x = 0, y = 0, z = 0},
3887 expirationtime = def.expire or 0.25,
3888 collisiondetection = false,
3889 texture = def.tail_texture,
3890 size = def.tail_size or 5,
3891 glow = def.glow or 0,
3895 if self.hit_node then
3897 local node = node_ok(pos).name
3899 if minetest.registered_nodes[node].walkable then
3901 self.hit_node(self, pos, node)
3903 if self.drop == true then
3905 pos.y = pos.y + 1
3907 self.lastpos = (self.lastpos or pos)
3909 minetest.add_item(self.lastpos, self.object:get_luaentity().name)
3912 self.object:remove();
3914 return
3918 if self.hit_player or self.hit_mob or self.hit_object then
3920 for _,player in pairs(minetest.get_objects_inside_radius(pos, 1.0)) do
3922 if self.hit_player
3923 and player:is_player() then
3925 self.hit_player(self, player)
3926 self.object:remove();
3927 return
3930 local entity = player:get_luaentity()
3932 if entity
3933 and self.hit_mob
3934 and entity._cmi_is_mob == true
3935 and tostring(player) ~= self.owner_id
3936 and entity.name ~= self.object:get_luaentity().name then
3937 self.hit_mob(self, player)
3938 self.object:remove();
3939 return
3942 if entity
3943 and self.hit_object
3944 and (not entity._cmi_is_mob)
3945 and tostring(player) ~= self.owner_id
3946 and entity.name ~= self.object:get_luaentity().name then
3947 self.hit_object(self, player)
3948 self.object:remove();
3949 return
3954 self.lastpos = pos
3960 -- no damage to nodes explosion
3961 function mobs:safe_boom(self, pos, strength)
3962 minetest.sound_play(self.sounds and self.sounds.explode or "tnt_explode", {
3963 pos = pos,
3964 gain = 1.0,
3965 max_hear_distance = self.sounds and self.sounds.distance or 32
3966 }, true)
3967 local radius = strength
3968 entity_physics(pos, radius)
3969 effect(pos, 32, "mcl_particles_smoke.png", radius * 3, radius * 5, radius, 1, 0)
3973 -- make explosion with protection and tnt mod check
3974 function mobs:boom(self, pos, strength, fire)
3976 if mod_explosions then
3977 if mobs_griefing and not minetest.is_protected(pos, "") then
3978 mcl_explosions.explode(pos, strength, { drop_chance = 1.0, fire = fire }, self.object)
3979 else
3980 mobs:safe_boom(self, pos, strength)
3982 else
3983 mobs:safe_boom(self, pos, strength)
3988 -- Register spawn eggs
3990 -- Note: This also introduces the “spawn_egg” group:
3991 -- * spawn_egg=1: Spawn egg (generic mob, no metadata)
3992 -- * spawn_egg=2: Spawn egg (captured/tamed mob, metadata)
3993 function mobs:register_egg(mob, desc, background, addegg, no_creative)
3995 local grp = {spawn_egg = 1}
3997 -- do NOT add this egg to creative inventory (e.g. dungeon master)
3998 if no_creative == true then
3999 grp.not_in_creative_inventory = 1
4002 local invimg = background
4004 if addegg == 1 then
4005 invimg = "mobs_chicken_egg.png^(" .. invimg ..
4006 "^[mask:mobs_chicken_egg_overlay.png)"
4009 -- register old stackable mob egg
4010 minetest.register_craftitem(mob, {
4012 description = desc,
4013 inventory_image = invimg,
4014 groups = grp,
4016 _doc_items_longdesc = S("This allows you to place a single mob."),
4017 _doc_items_usagehelp = S("Just place it where you want the mob to appear. Animals will spawn tamed, unless you hold down the sneak key while placing. If you place this on a mob spawner, you change the mob it spawns."),
4019 on_place = function(itemstack, placer, pointed_thing)
4021 local pos = pointed_thing.above
4023 -- am I clicking on something with existing on_rightclick function?
4024 local under = minetest.get_node(pointed_thing.under)
4025 local def = minetest.registered_nodes[under.name]
4026 if def and def.on_rightclick then
4027 return def.on_rightclick(pointed_thing.under, under, placer, itemstack)
4030 if pos
4031 and within_limits(pos, 0)
4032 and not minetest.is_protected(pos, placer:get_player_name()) then
4034 local name = placer:get_player_name()
4035 local privs = minetest.get_player_privs(name)
4036 if mod_mobspawners and under.name == "mcl_mobspawners:spawner" then
4037 if minetest.is_protected(pointed_thing.under, name) then
4038 minetest.record_protection_violation(pointed_thing.under, name)
4039 return itemstack
4041 if not privs.maphack then
4042 minetest.chat_send_player(name, S("You need the “maphack” privilege to change the mob spawner."))
4043 return itemstack
4045 mcl_mobspawners.setup_spawner(pointed_thing.under, itemstack:get_name())
4046 if not mobs.is_creative(name) then
4047 itemstack:take_item()
4049 return itemstack
4052 if not minetest.registered_entities[mob] then
4053 return itemstack
4056 if minetest.settings:get_bool("only_peaceful_mobs", false)
4057 and minetest.registered_entities[mob].type == "monster" then
4058 minetest.chat_send_player(name, S("Only peaceful mobs allowed!"))
4059 return itemstack
4062 pos.y = pos.y + 1
4064 local mob = minetest.add_entity(pos, mob)
4065 local ent = mob:get_luaentity()
4067 -- don't set owner if monster or sneak pressed
4068 if ent.type ~= "monster"
4069 and not placer:get_player_control().sneak then
4070 ent.owner = placer:get_player_name()
4071 ent.tamed = true
4074 -- set nametag
4075 local nametag = itemstack:get_meta():get_string("name")
4076 if nametag ~= "" then
4077 if string.len(nametag) > MAX_MOB_NAME_LENGTH then
4078 nametag = string.sub(nametag, 1, MAX_MOB_NAME_LENGTH)
4080 ent.nametag = nametag
4081 update_tag(ent)
4084 -- if not in creative then take item
4085 if not mobs.is_creative(placer:get_player_name()) then
4086 itemstack:take_item()
4090 return itemstack
4091 end,
4097 -- No-op in MCL2 (capturing mobs is not possible).
4098 -- Provided for compability with Mobs Redo
4099 function mobs:capture_mob(self, clicker, chance_hand, chance_net, chance_lasso, force_take, replacewith)
4100 return false
4104 -- No-op in MCL2 (protecting mobs is not possible).
4105 function mobs:protect(self, clicker)
4106 return false
4110 -- feeding, taming and breeding (thanks blert2112)
4111 function mobs:feed_tame(self, clicker, feed_count, breed, tame)
4112 if not self.follow then
4113 return false
4116 -- can eat/tame with item in hand
4117 if follow_holding(self, clicker) then
4119 -- if not in creative then take item
4120 if not mobs.is_creative(clicker:get_player_name()) then
4122 local item = clicker:get_wielded_item()
4124 item:take_item()
4126 clicker:set_wielded_item(item)
4129 -- increase health
4130 self.health = self.health + 4
4132 if self.health >= self.hp_max then
4134 self.health = self.hp_max
4136 if self.htimer < 1 then
4137 self.htimer = 5
4141 self.object:set_hp(self.health)
4143 update_tag(self)
4145 -- make children grow quicker
4146 if self.child == true then
4148 self.hornytimer = self.hornytimer + 20
4150 return true
4153 -- feed and tame
4154 self.food = (self.food or 0) + 1
4155 if self.food >= feed_count then
4157 self.food = 0
4159 if breed and self.hornytimer == 0 then
4160 self.horny = true
4163 if tame then
4165 self.tamed = true
4167 if not self.owner or self.owner == "" then
4168 self.owner = clicker:get_player_name()
4172 -- make sound when fed so many times
4173 mob_sound(self, "random", true)
4176 return true
4179 return false
4182 -- Spawn a child
4183 function mobs:spawn_child(pos, mob_type)
4184 local child = minetest.add_entity(pos, mob_type)
4185 if not child then
4186 return
4189 local ent = child:get_luaentity()
4190 effect(pos, 15, "mcl_particles_smoke.png", 1, 2, 2, 15, 5)
4192 ent.child = true
4194 local textures
4195 -- using specific child texture (if found)
4196 if ent.child_texture then
4197 textures = ent.child_texture[1]
4200 -- and resize to half height
4201 child:set_properties({
4202 textures = textures,
4203 visual_size = {
4204 x = ent.base_size.x * .5,
4205 y = ent.base_size.y * .5,
4207 collisionbox = {
4208 ent.base_colbox[1] * .5,
4209 ent.base_colbox[2] * .5,
4210 ent.base_colbox[3] * .5,
4211 ent.base_colbox[4] * .5,
4212 ent.base_colbox[5] * .5,
4213 ent.base_colbox[6] * .5,
4215 selectionbox = {
4216 ent.base_selbox[1] * .5,
4217 ent.base_selbox[2] * .5,
4218 ent.base_selbox[3] * .5,
4219 ent.base_selbox[4] * .5,
4220 ent.base_selbox[5] * .5,
4221 ent.base_selbox[6] * .5,
4225 return child
4229 -- compatibility function for old entities to new modpack entities
4230 function mobs:alias_mob(old_name, new_name)
4232 -- spawn egg
4233 minetest.register_alias(old_name, new_name)
4235 -- entity
4236 minetest.register_entity(":" .. old_name, {
4238 physical = false,
4240 on_step = function(self)
4242 if minetest.registered_entities[new_name] then
4243 minetest.add_entity(self.object:get_pos(), new_name)
4246 self.object:remove()