Codebase list minetest-mod-mobs-redo / 0ad16b2
New upstream version 20181016 Julien Puydt 4 years ago
44 changed file(s) with 10936 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0
1 -- Intllib and CMI support check
2 local MP = minetest.get_modpath(minetest.get_current_modname())
3 local S, NS = dofile(MP .. "/intllib.lua")
4 local use_cmi = minetest.global_exists("cmi")
5
6 mobs = {
7 mod = "redo",
8 version = "20181005",
9 intllib = S,
10 invis = minetest.global_exists("invisibility") and invisibility or {},
11 }
12
13 -- creative check
14 local creative_cache = minetest.settings:get_bool("creative_mode")
15 function mobs.is_creative(name)
16 return creative_cache or minetest.check_player_privs(name, {creative = true})
17 end
18
19
20 -- localize math functions
21 local pi = math.pi
22 local square = math.sqrt
23 local sin = math.sin
24 local cos = math.cos
25 local abs = math.abs
26 local min = math.min
27 local max = math.max
28 local atann = math.atan
29 local random = math.random
30 local floor = math.floor
31 local atan = function(x)
32 if not x or x ~= x then
33 --error("atan bassed NaN")
34 return 0
35 else
36 return atann(x)
37 end
38 end
39
40
41 -- Load settings
42 local damage_enabled = minetest.settings:get_bool("enable_damage")
43 local mobs_spawn = minetest.settings:get_bool("mobs_spawn") ~= false
44 local peaceful_only = minetest.settings:get_bool("only_peaceful_mobs")
45 local disable_blood = minetest.settings:get_bool("mobs_disable_blood")
46 local mobs_drop_items = minetest.settings:get_bool("mobs_drop_items") ~= false
47 local mobs_griefing = minetest.settings:get_bool("mobs_griefing") ~= false
48 local creative = minetest.settings:get_bool("creative_mode")
49 local spawn_protected = minetest.settings:get_bool("mobs_spawn_protected") ~= false
50 local remove_far = minetest.settings:get_bool("remove_far_mobs") ~= false
51 local difficulty = tonumber(minetest.settings:get("mob_difficulty")) or 1.0
52 local show_health = minetest.settings:get_bool("mob_show_health") ~= false
53 local max_per_block = tonumber(minetest.settings:get("max_objects_per_block") or 99)
54 local mob_chance_multiplier = tonumber(minetest.settings:get("mob_chance_multiplier") or 1)
55
56 -- Peaceful mode message so players will know there are no monsters
57 if peaceful_only then
58 minetest.register_on_joinplayer(function(player)
59 minetest.chat_send_player(player:get_player_name(),
60 S("** Peaceful Mode Active - No Monsters Will Spawn"))
61 end)
62 end
63
64 -- calculate aoc range for mob count
65 local aoc_range = tonumber(minetest.settings:get("active_block_range")) * 16
66
67 -- pathfinding settings
68 local enable_pathfinding = true
69 local stuck_timeout = 3 -- how long before mob gets stuck in place and starts searching
70 local stuck_path_timeout = 10 -- how long will mob follow path before giving up
71
72 -- default nodes
73 local node_fire = "fire:basic_flame"
74 local node_permanent_flame = "fire:permanent_flame"
75 local node_ice = "default:ice"
76 local node_snowblock = "default:snowblock"
77 local node_snow = "default:snow"
78 mobs.fallback_node = minetest.registered_aliases["mapgen_dirt"] or "default:dirt"
79
80
81 -- play sound
82 local mob_sound = function(self, sound)
83
84 if sound then
85 minetest.sound_play(sound, {
86 object = self.object,
87 gain = 1.0,
88 max_hear_distance = self.sounds.distance
89 })
90 end
91 end
92
93
94 -- attack player/mob
95 local do_attack = function(self, player)
96
97 if self.state == "attack" then
98 return
99 end
100
101 self.attack = player
102 self.state = "attack"
103
104 if random(0, 100) < 90 then
105 mob_sound(self, self.sounds.war_cry)
106 end
107 end
108
109
110 -- calculate distance
111 local get_distance = function(a, b)
112
113 local x, y, z = a.x - b.x, a.y - b.y, a.z - b.z
114
115 return square(x * x + y * y + z * z)
116 end
117
118
119 -- collision function based on jordan4ibanez' open_ai mod
120 local collision = function(self)
121
122 local pos = self.object:get_pos()
123 local vel = self.object:get_velocity()
124 local x, z = 0, 0
125 local width = -self.collisionbox[1] + self.collisionbox[4] + 0.5
126
127 for _,object in ipairs(minetest.get_objects_inside_radius(pos, width)) do
128
129 if object:is_player()
130 or (object:get_luaentity()._cmi_is_mob == true and object ~= self.object) then
131
132 local pos2 = object:get_pos()
133 local vec = {x = pos.x - pos2.x, z = pos.z - pos2.z}
134
135 x = x + vec.x
136 z = z + vec.z
137 end
138 end
139
140 return({x, z})
141 end
142
143
144 -- move mob in facing direction
145 local set_velocity = function(self, v)
146
147 local c_x, c_y = 0, 0
148
149 -- can mob be pushed, if so calculate direction
150 if self.pushable then
151 c_x, c_y = unpack(collision(self))
152 end
153
154 -- halt mob if it has been ordered to stay
155 if self.order == "stand" then
156 self.object:set_velocity({x = 0, y = 0, z = 0})
157 return
158 end
159
160 local yaw = (self.object:get_yaw() or 0) + self.rotate
161
162 self.object:set_velocity({
163 x = (sin(yaw) * -v) + c_x,
164 y = self.object:get_velocity().y,
165 z = (cos(yaw) * v) + c_y,
166 })
167 end
168
169
170 -- calculate mob velocity
171 local get_velocity = function(self)
172
173 local v = self.object:get_velocity()
174
175 return (v.x * v.x + v.z * v.z) ^ 0.5
176 end
177
178
179 -- set and return valid yaw
180 local set_yaw = function(self, yaw, delay)
181
182 if not yaw or yaw ~= yaw then
183 yaw = 0
184 end
185
186 delay = delay or 0
187
188 if delay == 0 then
189 self.object:set_yaw(yaw)
190 return yaw
191 end
192
193 self.target_yaw = yaw
194 self.delay = delay
195
196 return self.target_yaw
197 end
198
199 -- global function to set mob yaw
200 function mobs:yaw(self, yaw, delay)
201 set_yaw(self, yaw, delay)
202 end
203
204
205 -- set defined animation
206 local set_animation = function(self, anim)
207
208 if not self.animation
209 or not anim then return end
210
211 self.animation.current = self.animation.current or ""
212
213 -- only set different animation for attacks when setting to same set
214 if anim ~= "punch" and anim ~= "shoot"
215 and string.find(self.animation.current, anim) then
216 return
217 end
218
219 -- check for more than one animation
220 local num = 0
221
222 for n = 1, 4 do
223
224 if self.animation[anim .. n .. "_start"]
225 and self.animation[anim .. n .. "_end"] then
226 num = n
227 end
228 end
229
230 -- choose random animation from set
231 if num > 0 then
232 num = random(0, num)
233 anim = anim .. (num ~= 0 and num or "")
234 end
235
236 if anim == self.animation.current
237 or not self.animation[anim .. "_start"]
238 or not self.animation[anim .. "_end"] then
239 return
240 end
241
242 self.animation.current = anim
243
244 self.object:set_animation({
245 x = self.animation[anim .. "_start"],
246 y = self.animation[anim .. "_end"]},
247 self.animation[anim .. "_speed"] or self.animation.speed_normal or 15,
248 0, self.animation[anim .. "_loop"] ~= false)
249 end
250
251 -- above function exported for mount.lua
252 function mobs:set_animation(self, anim)
253 set_animation(self, anim)
254 end
255
256
257 -- check line of sight (by BrunoMine, tweaked by Astrobe)
258 local line_of_sight = function(self, pos1, pos2, stepsize)
259
260 if not pos1 or not pos2 then return end
261
262 stepsize = stepsize or 1
263
264 local stepv = vector.multiply(vector.direction(pos1, pos2), stepsize)
265
266 local s, pos = minetest.line_of_sight(pos1, pos2, stepsize)
267
268 -- normal walking and flying mobs can see you through air
269 if s == true then return true end
270
271 -- New pos1 to be analyzed
272 local npos1 = {x = pos1.x, y = pos1.y, z = pos1.z}
273
274 local r, pos = minetest.line_of_sight(npos1, pos2, stepsize)
275
276 -- Checks the return
277 if r == true then return true end
278
279 -- Nodename found
280 local nn = minetest.get_node(pos).name
281
282 -- It continues to advance in the line of sight in search of a real
283 -- obstruction which counts as 'normal' nodebox.
284 while minetest.registered_nodes[nn]
285 and (minetest.registered_nodes[nn].walkable == false) do
286 -- or minetest.registered_nodes[nn].drawtype == "nodebox") do
287
288 npos1 = vector.add(npos1, stepv)
289
290 if get_distance(npos1, pos2) < stepsize then return true end
291
292 -- scan again
293 r, pos = minetest.line_of_sight(npos1, pos2, stepsize)
294
295 if r == true then return true end
296
297 -- New Nodename found
298 nn = minetest.get_node(pos).name
299 end
300
301 return false
302 end
303
304 -- global function
305 function mobs:line_of_sight(self, pos1, pos2, stepsize)
306
307 return line_of_sight(self, pos1, pos2, stepsize)
308 end
309
310
311 -- are we flying in what we are suppose to? (taikedz)
312 local flight_check = function(self, pos_w)
313
314 local def = minetest.registered_nodes[self.standing_in]
315
316 if not def then return false end -- nil check
317
318 if type(self.fly_in) == "string"
319 and self.standing_in == self.fly_in then
320
321 return true
322
323 elseif type(self.fly_in) == "table" then
324
325 for _,fly_in in pairs(self.fly_in) do
326
327 if self.standing_in == fly_in then
328
329 return true
330 end
331 end
332 end
333
334 -- stops mobs getting stuck inside stairs and plantlike nodes
335 if def.drawtype ~= "airlike"
336 and def.drawtype ~= "liquid"
337 and def.drawtype ~= "flowingliquid" then
338 return true
339 end
340
341 return false
342 end
343
344
345 -- custom particle effects
346 local effect = function(pos, amount, texture, min_size, max_size, radius, gravity, glow)
347
348 radius = radius or 2
349 min_size = min_size or 0.5
350 max_size = max_size or 1
351 gravity = gravity or -10
352 glow = glow or 0
353
354 minetest.add_particlespawner({
355 amount = amount,
356 time = 0.25,
357 minpos = pos,
358 maxpos = pos,
359 minvel = {x = -radius, y = -radius, z = -radius},
360 maxvel = {x = radius, y = radius, z = radius},
361 minacc = {x = 0, y = gravity, z = 0},
362 maxacc = {x = 0, y = gravity, z = 0},
363 minexptime = 0.1,
364 maxexptime = 1,
365 minsize = min_size,
366 maxsize = max_size,
367 texture = texture,
368 glow = glow,
369 })
370 end
371
372
373 -- update nametag colour
374 local update_tag = function(self)
375
376 local col = "#00FF00"
377 local qua = self.hp_max / 4
378
379 if self.health <= floor(qua * 3) then
380 col = "#FFFF00"
381 end
382
383 if self.health <= floor(qua * 2) then
384 col = "#FF6600"
385 end
386
387 if self.health <= floor(qua) then
388 col = "#FF0000"
389 end
390
391 self.object:set_properties({
392 nametag = self.nametag,
393 nametag_color = col
394 })
395
396 end
397
398
399 -- drop items
400 local item_drop = function(self)
401
402 -- check for nil or no drops
403 if not self.drops or #self.drops == 0 then
404 return
405 end
406
407 -- no drops if disabled by setting
408 if not mobs_drop_items then return end
409
410 -- no drops for child mobs
411 if self.child then return end
412
413 -- was mob killed by player?
414 local death_by_player = self.cause_of_death and self.cause_of_death.puncher
415 and self.cause_of_death.puncher:is_player() or nil
416
417 local obj, item, num
418 local pos = self.object:get_pos()
419
420 for n = 1, #self.drops do
421
422 if random(1, self.drops[n].chance) == 1 then
423
424 num = random(self.drops[n].min or 0, self.drops[n].max or 1)
425 item = self.drops[n].name
426
427 -- cook items on a hot death
428 if self.cause_of_death.hot then
429
430 local output = minetest.get_craft_result({
431 method = "cooking", width = 1, items = {item}})
432
433 if output and output.item and not output.item:is_empty() then
434 item = output.item:get_name()
435 end
436 end
437
438 -- only drop rare items (drops.min=0) if killed by player
439 if death_by_player then
440 obj = minetest.add_item(pos, ItemStack(item .. " " .. num))
441
442 elseif self.drops[n].min ~= 0 then
443 obj = minetest.add_item(pos, ItemStack(item .. " " .. num))
444 end
445
446 if obj and obj:get_luaentity() then
447
448 obj:set_velocity({
449 x = random(-10, 10) / 9,
450 y = 6,
451 z = random(-10, 10) / 9,
452 })
453
454 elseif obj then
455 obj:remove() -- item does not exist
456 end
457 end
458 end
459
460 self.drops = {}
461 end
462
463
464 -- check if mob is dead or only hurt
465 local check_for_death = function(self, cmi_cause)
466
467 -- has health actually changed?
468 if self.health == self.old_health and self.health > 0 then
469 return
470 end
471
472 self.old_health = self.health
473
474 -- still got some health? play hurt sound
475 if self.health > 0 then
476
477 mob_sound(self, self.sounds.damage)
478
479 -- make sure health isn't higher than max
480 if self.health > self.hp_max then
481 self.health = self.hp_max
482 end
483
484 -- backup nametag so we can show health stats
485 if not self.nametag2 then
486 self.nametag2 = self.nametag or ""
487 end
488
489 if show_health
490 and (cmi_cause and cmi_cause.type == "punch") then
491
492 self.htimer = 2
493 self.nametag = "♥ " .. self.health .. " / " .. self.hp_max
494
495 update_tag(self)
496 end
497
498 return false
499 end
500
501 self.cause_of_death = cmi_cause
502
503 -- drop items
504 item_drop(self)
505
506 mob_sound(self, self.sounds.death)
507
508 local pos = self.object:get_pos()
509
510 -- execute custom death function
511 if self.on_die then
512
513 self.on_die(self, pos)
514
515 if use_cmi then
516 cmi.notify_die(self.object, cmi_cause)
517 end
518
519 self.object:remove()
520
521 return true
522 end
523
524 -- default death function and die animation (if defined)
525 if self.animation
526 and self.animation.die_start
527 and self.animation.die_end then
528
529 local frames = self.animation.die_end - self.animation.die_start
530 local speed = self.animation.die_speed or 15
531 local length = max(frames / speed, 0)
532
533 self.attack = nil
534 self.v_start = false
535 self.timer = 0
536 self.blinktimer = 0
537 self.passive = true
538 self.state = "die"
539 set_velocity(self, 0)
540 set_animation(self, "die")
541
542 minetest.after(length, function(self)
543
544 if use_cmi and self.object:get_luaentity() then
545 cmi.notify_die(self.object, cmi_cause)
546 end
547
548 self.object:remove()
549 end, self)
550 else
551
552 if use_cmi then
553 cmi.notify_die(self.object, cmi_cause)
554 end
555
556 self.object:remove()
557 end
558
559 effect(pos, 20, "tnt_smoke.png")
560
561 return true
562 end
563
564
565 -- is mob facing a cliff
566 local is_at_cliff = function(self)
567
568 if self.fear_height == 0 then -- 0 for no falling protection!
569 return false
570 end
571
572 local yaw = self.object:get_yaw()
573 local dir_x = -sin(yaw) * (self.collisionbox[4] + 0.5)
574 local dir_z = cos(yaw) * (self.collisionbox[4] + 0.5)
575 local pos = self.object:get_pos()
576 local ypos = pos.y + self.collisionbox[2] -- just above floor
577
578 if minetest.line_of_sight(
579 {x = pos.x + dir_x, y = ypos, z = pos.z + dir_z},
580 {x = pos.x + dir_x, y = ypos - self.fear_height, z = pos.z + dir_z}
581 , 1) then
582
583 return true
584 end
585
586 return false
587 end
588
589
590 -- get node but use fallback for nil or unknown
591 local node_ok = function(pos, fallback)
592
593 fallback = fallback or mobs.fallback_node
594
595 local node = minetest.get_node_or_nil(pos)
596
597 if node and minetest.registered_nodes[node.name] then
598 return node
599 end
600
601 return minetest.registered_nodes[fallback]
602 end
603
604
605 -- environmental damage (water, lava, fire, light etc.)
606 local do_env_damage = function(self)
607
608 -- feed/tame text timer (so mob 'full' messages dont spam chat)
609 if self.htimer > 0 then
610 self.htimer = self.htimer - 1
611 end
612
613 -- reset nametag after showing health stats
614 if self.htimer < 1 and self.nametag2 then
615
616 self.nametag = self.nametag2
617 self.nametag2 = nil
618
619 update_tag(self)
620 end
621
622 local pos = self.object:get_pos()
623
624 self.time_of_day = minetest.get_timeofday()
625
626 -- remove mob if standing inside ignore node
627 if self.standing_in == "ignore" then
628 self.object:remove()
629 return
630 end
631
632 -- is mob light sensative, or scared of the dark :P
633 if self.light_damage ~= 0 then
634
635 local light = minetest.get_node_light(pos) or 0
636
637 if light >= self.light_damage_min
638 and light <= self.light_damage_max then
639
640 self.health = self.health - self.light_damage
641
642 effect(pos, 5, "tnt_smoke.png")
643
644 if check_for_death(self, {type = "light"}) then return end
645 end
646 end
647
648 local nodef = minetest.registered_nodes[self.standing_in]
649
650 pos.y = pos.y + 1 -- for particle effect position
651
652 -- water
653 if self.water_damage
654 and nodef.groups.water then
655
656 if self.water_damage ~= 0 then
657
658 self.health = self.health - self.water_damage
659
660 effect(pos, 5, "bubble.png", nil, nil, 1, nil)
661
662 if check_for_death(self, {type = "environment",
663 pos = pos, node = self.standing_in}) then return end
664 end
665
666 -- lava or fire or ignition source
667 elseif self.lava_damage
668 and nodef.groups.igniter then
669 -- and (nodef.groups.lava
670 -- or self.standing_in == node_fire
671 -- or self.standing_in == node_permanent_flame) then
672
673 if self.lava_damage ~= 0 then
674
675 self.health = self.health - self.lava_damage
676
677 effect(pos, 5, "fire_basic_flame.png", nil, nil, 1, nil)
678
679 if check_for_death(self, {type = "environment",
680 pos = pos, node = self.standing_in, hot = true}) then return end
681 end
682
683 -- damage_per_second node check
684 elseif nodef.damage_per_second ~= 0 then
685
686 self.health = self.health - nodef.damage_per_second
687
688 effect(pos, 5, "tnt_smoke.png")
689
690 if check_for_death(self, {type = "environment",
691 pos = pos, node = self.standing_in}) then return end
692 end
693 --[[
694 --- suffocation inside solid node
695 if self.suffocation ~= 0
696 and nodef.walkable == true
697 and nodef.groups.disable_suffocation ~= 1
698 and nodef.drawtype == "normal" then
699
700 self.health = self.health - self.suffocation
701
702 if check_for_death(self, {type = "environment",
703 pos = pos, node = self.standing_in}) then return end
704 end
705 ]]
706 check_for_death(self, {type = "unknown"})
707 end
708
709
710 -- jump if facing a solid node (not fences or gates)
711 local do_jump = function(self)
712
713 if not self.jump
714 or self.jump_height == 0
715 or self.fly
716 or self.child
717 or self.order == "stand" then
718 return false
719 end
720
721 self.facing_fence = false
722
723 -- something stopping us while moving?
724 if self.state ~= "stand"
725 and get_velocity(self) > 0.5
726 and self.object:get_velocity().y ~= 0 then
727 return false
728 end
729
730 local pos = self.object:get_pos()
731 local yaw = self.object:get_yaw()
732
733 -- what is mob standing on?
734 pos.y = pos.y + self.collisionbox[2] - 0.2
735
736 local nod = node_ok(pos)
737
738 --print ("standing on:", nod.name, pos.y)
739
740 if minetest.registered_nodes[nod.name].walkable == false then
741 return false
742 end
743
744 -- where is front
745 local dir_x = -sin(yaw) * (self.collisionbox[4] + 0.5)
746 local dir_z = cos(yaw) * (self.collisionbox[4] + 0.5)
747
748 -- what is in front of mob?
749 local nod = node_ok({
750 x = pos.x + dir_x,
751 y = pos.y + 0.5,
752 z = pos.z + dir_z
753 })
754
755 -- thin blocks that do not need to be jumped
756 if nod.name == node_snow then
757 return false
758 end
759
760 --print ("in front:", nod.name, pos.y + 0.5)
761
762 if self.walk_chance == 0
763 or minetest.registered_items[nod.name].walkable then
764
765 if not nod.name:find("fence")
766 and not nod.name:find("gate") then
767
768 local v = self.object:get_velocity()
769
770 v.y = self.jump_height
771
772 set_animation(self, "jump") -- only when defined
773
774 self.object:set_velocity(v)
775
776 -- when in air move forward
777 minetest.after(0.3, function(self, v)
778
779 if self.object:get_luaentity() then
780
781 self.object:set_acceleration({
782 x = v.x * 2,--1.5,
783 y = 0,
784 z = v.z * 2,--1.5
785 })
786 end
787 end, self, v)
788
789 if get_velocity(self) > 0 then
790 mob_sound(self, self.sounds.jump)
791 end
792 else
793 self.facing_fence = true
794 end
795
796 return true
797 end
798
799 return false
800 end
801
802
803 -- blast damage to entities nearby (modified from TNT mod)
804 local entity_physics = function(pos, radius)
805
806 radius = radius * 2
807
808 local objs = minetest.get_objects_inside_radius(pos, radius)
809 local obj_pos, dist
810
811 for n = 1, #objs do
812
813 obj_pos = objs[n]:get_pos()
814
815 dist = get_distance(pos, obj_pos)
816 if dist < 1 then dist = 1 end
817
818 local damage = floor((4 / dist) * radius)
819 local ent = objs[n]:get_luaentity()
820
821 -- punches work on entities AND players
822 objs[n]:punch(objs[n], 1.0, {
823 full_punch_interval = 1.0,
824 damage_groups = {fleshy = damage},
825 }, pos)
826 end
827 end
828
829
830 -- should mob follow what I'm holding ?
831 local follow_holding = function(self, clicker)
832
833 if mobs.invis[clicker:get_player_name()] then
834 return false
835 end
836
837 local item = clicker:get_wielded_item()
838 local t = type(self.follow)
839
840 -- single item
841 if t == "string"
842 and item:get_name() == self.follow then
843 return true
844
845 -- multiple items
846 elseif t == "table" then
847
848 for no = 1, #self.follow do
849
850 if self.follow[no] == item:get_name() then
851 return true
852 end
853 end
854 end
855
856 return false
857 end
858
859
860 -- find two animals of same type and breed if nearby and horny
861 local breed = function(self)
862
863 -- child takes 240 seconds before growing into adult
864 if self.child == true then
865
866 self.hornytimer = self.hornytimer + 1
867
868 if self.hornytimer > 240 then
869
870 self.child = false
871 self.hornytimer = 0
872
873 self.object:set_properties({
874 textures = self.base_texture,
875 mesh = self.base_mesh,
876 visual_size = self.base_size,
877 collisionbox = self.base_colbox,
878 selectionbox = self.base_selbox,
879 })
880
881 -- custom function when child grows up
882 if self.on_grown then
883 self.on_grown(self)
884 else
885 -- jump when fully grown so as not to fall into ground
886 self.object:set_velocity({
887 x = 0,
888 y = self.jump_height,
889 z = 0
890 })
891 end
892 end
893
894 return
895 end
896
897 -- horny animal can mate for 40 seconds,
898 -- afterwards horny animal cannot mate again for 200 seconds
899 if self.horny == true
900 and self.hornytimer < 240 then
901
902 self.hornytimer = self.hornytimer + 1
903
904 if self.hornytimer >= 240 then
905 self.hornytimer = 0
906 self.horny = false
907 end
908 end
909
910 -- find another same animal who is also horny and mate if nearby
911 if self.horny == true
912 and self.hornytimer <= 40 then
913
914 local pos = self.object:get_pos()
915
916 effect({x = pos.x, y = pos.y + 1, z = pos.z}, 8, "heart.png", 3, 4, 1, 0.1)
917
918 local objs = minetest.get_objects_inside_radius(pos, 3)
919 local num = 0
920 local ent = nil
921
922 for n = 1, #objs do
923
924 ent = objs[n]:get_luaentity()
925
926 -- check for same animal with different colour
927 local canmate = false
928
929 if ent then
930
931 if ent.name == self.name then
932 canmate = true
933 else
934 local entname = string.split(ent.name,":")
935 local selfname = string.split(self.name,":")
936
937 if entname[1] == selfname[1] then
938 entname = string.split(entname[2],"_")
939 selfname = string.split(selfname[2],"_")
940
941 if entname[1] == selfname[1] then
942 canmate = true
943 end
944 end
945 end
946 end
947
948 if ent
949 and canmate == true
950 and ent.horny == true
951 and ent.hornytimer <= 40 then
952 num = num + 1
953 end
954
955 -- found your mate? then have a baby
956 if num > 1 then
957
958 self.hornytimer = 41
959 ent.hornytimer = 41
960
961 -- spawn baby
962 minetest.after(5, function(self, ent)
963
964 if not self.object:get_luaentity() then
965 return
966 end
967
968 -- custom breed function
969 if self.on_breed then
970
971 -- when false skip going any further
972 if self.on_breed(self, ent) == false then
973 return
974 end
975 else
976 effect(pos, 15, "tnt_smoke.png", 1, 2, 2, 15, 5)
977 end
978
979 local mob = minetest.add_entity(pos, self.name)
980 local ent2 = mob:get_luaentity()
981 local textures = self.base_texture
982
983 -- using specific child texture (if found)
984 if self.child_texture then
985 textures = self.child_texture[1]
986 end
987
988 -- and resize to half height
989 mob:set_properties({
990 textures = textures,
991 visual_size = {
992 x = self.base_size.x * .5,
993 y = self.base_size.y * .5,
994 },
995 collisionbox = {
996 self.base_colbox[1] * .5,
997 self.base_colbox[2] * .5,
998 self.base_colbox[3] * .5,
999 self.base_colbox[4] * .5,
1000 self.base_colbox[5] * .5,
1001 self.base_colbox[6] * .5,
1002 },
1003 selectionbox = {
1004 self.base_selbox[1] * .5,
1005 self.base_selbox[2] * .5,
1006 self.base_selbox[3] * .5,
1007 self.base_selbox[4] * .5,
1008 self.base_selbox[5] * .5,
1009 self.base_selbox[6] * .5,
1010 },
1011 })
1012 -- tamed and owned by parents' owner
1013 ent2.child = true
1014 ent2.tamed = true
1015 ent2.owner = self.owner
1016 end, self, ent)
1017
1018 num = 0
1019
1020 break
1021 end
1022 end
1023 end
1024 end
1025
1026
1027 -- find and replace what mob is looking for (grass, wheat etc.)
1028 local replace = function(self, pos)
1029
1030 if not mobs_griefing
1031 or not self.replace_rate
1032 or not self.replace_what
1033 or self.child == true
1034 or self.object:get_velocity().y ~= 0
1035 or random(1, self.replace_rate) > 1 then
1036 return
1037 end
1038
1039 local what, with, y_offset
1040
1041 if type(self.replace_what[1]) == "table" then
1042
1043 local num = random(#self.replace_what)
1044
1045 what = self.replace_what[num][1] or ""
1046 with = self.replace_what[num][2] or ""
1047 y_offset = self.replace_what[num][3] or 0
1048 else
1049 what = self.replace_what
1050 with = self.replace_with or ""
1051 y_offset = self.replace_offset or 0
1052 end
1053
1054 pos.y = pos.y + y_offset
1055
1056 if #minetest.find_nodes_in_area(pos, pos, what) > 0 then
1057
1058 -- print ("replace node = ".. minetest.get_node(pos).name, pos.y)
1059
1060 local oldnode = {name = what}
1061 local newnode = {name = with}
1062 local on_replace_return
1063
1064 if self.on_replace then
1065 on_replace_return = self.on_replace(self, pos, oldnode, newnode)
1066 end
1067
1068 if on_replace_return ~= false then
1069
1070 minetest.set_node(pos, {name = with})
1071
1072 -- when cow/sheep eats grass, replace wool and milk
1073 if self.gotten == true then
1074 self.gotten = false
1075 self.object:set_properties(self)
1076 end
1077 end
1078 end
1079 end
1080
1081
1082 -- check if daytime and also if mob is docile during daylight hours
1083 local day_docile = function(self)
1084
1085 if self.docile_by_day == false then
1086
1087 return false
1088
1089 elseif self.docile_by_day == true
1090 and self.time_of_day > 0.2
1091 and self.time_of_day < 0.8 then
1092
1093 return true
1094 end
1095 end
1096
1097
1098 local los_switcher = false
1099 local height_switcher = false
1100
1101 -- path finding and smart mob routine by rnd, line_of_sight and other edits by Elkien3
1102 local smart_mobs = function(self, s, p, dist, dtime)
1103
1104 local s1 = self.path.lastpos
1105
1106 local target_pos = self.attack:get_pos()
1107
1108 -- is it becoming stuck?
1109 if abs(s1.x - s.x) + abs(s1.z - s.z) < .5 then
1110 self.path.stuck_timer = self.path.stuck_timer + dtime
1111 else
1112 self.path.stuck_timer = 0
1113 end
1114
1115 self.path.lastpos = {x = s.x, y = s.y, z = s.z}
1116
1117 local use_pathfind = false
1118 local has_lineofsight = minetest.line_of_sight(
1119 {x = s.x, y = (s.y) + .5, z = s.z},
1120 {x = target_pos.x, y = (target_pos.y) + 1.5, z = target_pos.z}, .2)
1121
1122 -- im stuck, search for path
1123 if not has_lineofsight then
1124
1125 if los_switcher == true then
1126 use_pathfind = true
1127 los_switcher = false
1128 end -- cannot see target!
1129 else
1130 if los_switcher == false then
1131
1132 los_switcher = true
1133 use_pathfind = false
1134
1135 minetest.after(1, function(self)
1136
1137 if self.object:get_luaentity() then
1138
1139 if has_lineofsight then
1140 self.path.following = false
1141 end
1142 end
1143 end, self)
1144 end -- can see target!
1145 end
1146
1147 if (self.path.stuck_timer > stuck_timeout and not self.path.following) then
1148
1149 use_pathfind = true
1150 self.path.stuck_timer = 0
1151
1152 minetest.after(1, function(self)
1153
1154 if self.object:get_luaentity() then
1155
1156 if has_lineofsight then
1157 self.path.following = false
1158 end
1159 end
1160 end, self)
1161 end
1162
1163 if (self.path.stuck_timer > stuck_path_timeout and self.path.following) then
1164
1165 use_pathfind = true
1166 self.path.stuck_timer = 0
1167
1168 minetest.after(1, function(self)
1169
1170 if self.object:get_luaentity() then
1171
1172 if has_lineofsight then
1173 self.path.following = false
1174 end
1175 end
1176 end, self)
1177 end
1178
1179 if abs(vector.subtract(s,target_pos).y) > self.stepheight then
1180
1181 if height_switcher then
1182 use_pathfind = true
1183 height_switcher = false
1184 end
1185 else
1186 if not height_switcher then
1187 use_pathfind = false
1188 height_switcher = true
1189 end
1190 end
1191
1192 if use_pathfind then
1193 -- lets try find a path, first take care of positions
1194 -- since pathfinder is very sensitive
1195 local sheight = self.collisionbox[5] - self.collisionbox[2]
1196
1197 -- round position to center of node to avoid stuck in walls
1198 -- also adjust height for player models!
1199 s.x = floor(s.x + 0.5)
1200 -- s.y = floor(s.y + 0.5) - sheight
1201 s.z = floor(s.z + 0.5)
1202
1203 local ssight, sground = minetest.line_of_sight(s, {
1204 x = s.x, y = s.y - 4, z = s.z}, 1)
1205
1206 -- determine node above ground
1207 if not ssight then
1208 s.y = sground.y + 1
1209 end
1210
1211 local p1 = self.attack:get_pos()
1212
1213 p1.x = floor(p1.x + 0.5)
1214 p1.y = floor(p1.y + 0.5)
1215 p1.z = floor(p1.z + 0.5)
1216
1217 local dropheight = 6
1218 if self.fear_height ~= 0 then dropheight = self.fear_height end
1219
1220 self.path.way = minetest.find_path(s, p1, 16, self.stepheight, dropheight, "Dijkstra")
1221
1222 --[[
1223 -- show path using particles
1224 if self.path.way and #self.path.way > 0 then
1225 print ("-- path length:" .. tonumber(#self.path.way))
1226 for _,pos in pairs(self.path.way) do
1227 minetest.add_particle({
1228 pos = pos,
1229 velocity = {x=0, y=0, z=0},
1230 acceleration = {x=0, y=0, z=0},
1231 expirationtime = 1,
1232 size = 4,
1233 collisiondetection = false,
1234 vertical = false,
1235 texture = "heart.png",
1236 })
1237 end
1238 end
1239 ]]
1240
1241 self.state = ""
1242 do_attack(self, self.attack)
1243
1244 -- no path found, try something else
1245 if not self.path.way then
1246
1247 self.path.following = false
1248
1249 -- lets make way by digging/building if not accessible
1250 if self.pathfinding == 2 and mobs_griefing then
1251
1252 -- is player higher than mob?
1253 if s.y < p1.y then
1254
1255 -- build upwards
1256 if not minetest.is_protected(s, "") then
1257
1258 local ndef1 = minetest.registered_nodes[self.standing_in]
1259
1260 if ndef1 and (ndef1.buildable_to or ndef1.groups.liquid) then
1261
1262 minetest.set_node(s, {name = mobs.fallback_node})
1263 end
1264 end
1265
1266 local sheight = math.ceil(self.collisionbox[5]) + 1
1267
1268 -- assume mob is 2 blocks high so it digs above its head
1269 s.y = s.y + sheight
1270
1271 -- remove one block above to make room to jump
1272 if not minetest.is_protected(s, "") then
1273
1274 local node1 = node_ok(s, "air").name
1275 local ndef1 = minetest.registered_nodes[node1]
1276
1277 if node1 ~= "air"
1278 and node1 ~= "ignore"
1279 and ndef1
1280 and not ndef1.groups.level
1281 and not ndef1.groups.unbreakable
1282 and not ndef1.groups.liquid then
1283
1284 minetest.set_node(s, {name = "air"})
1285 minetest.add_item(s, ItemStack(node1))
1286
1287 end
1288 end
1289
1290 s.y = s.y - sheight
1291 self.object:set_pos({x = s.x, y = s.y + 2, z = s.z})
1292
1293 else -- dig 2 blocks to make door toward player direction
1294
1295 local yaw1 = self.object:get_yaw() + pi / 2
1296 local p1 = {
1297 x = s.x + cos(yaw1),
1298 y = s.y,
1299 z = s.z + sin(yaw1)
1300 }
1301
1302 if not minetest.is_protected(p1, "") then
1303
1304 local node1 = node_ok(p1, "air").name
1305 local ndef1 = minetest.registered_nodes[node1]
1306
1307 if node1 ~= "air"
1308 and node1 ~= "ignore"
1309 and ndef1
1310 and not ndef1.groups.level
1311 and not ndef1.groups.unbreakable
1312 and not ndef1.groups.liquid then
1313
1314 minetest.add_item(p1, ItemStack(node1))
1315 minetest.set_node(p1, {name = "air"})
1316 end
1317
1318 p1.y = p1.y + 1
1319 node1 = node_ok(p1, "air").name
1320 ndef1 = minetest.registered_nodes[node1]
1321
1322 if node1 ~= "air"
1323 and node1 ~= "ignore"
1324 and ndef1
1325 and not ndef1.groups.level
1326 and not ndef1.groups.unbreakable
1327 and not ndef1.groups.liquid then
1328
1329 minetest.add_item(p1, ItemStack(node1))
1330 minetest.set_node(p1, {name = "air"})
1331 end
1332
1333 end
1334 end
1335 end
1336
1337 -- will try again in 2 second
1338 self.path.stuck_timer = stuck_timeout - 2
1339
1340 -- frustration! cant find the damn path :(
1341 mob_sound(self, self.sounds.random)
1342 else
1343 -- yay i found path
1344 mob_sound(self, self.sounds.war_cry)
1345 set_velocity(self, self.walk_velocity)
1346
1347 -- follow path now that it has it
1348 self.path.following = true
1349 end
1350 end
1351 end
1352
1353
1354 -- specific attacks
1355 local specific_attack = function(list, what)
1356
1357 -- no list so attack default (player, animals etc.)
1358 if list == nil then
1359 return true
1360 end
1361
1362 -- found entity on list to attack?
1363 for no = 1, #list do
1364
1365 if list[no] == what then
1366 return true
1367 end
1368 end
1369
1370 return false
1371 end
1372
1373
1374 -- general attack function for all mobs ==========
1375 local general_attack = function(self)
1376
1377 -- return if already attacking, passive or docile during day
1378 if self.passive
1379 or self.state == "attack"
1380 or day_docile(self) then
1381 return
1382 end
1383
1384 local s = self.object:get_pos()
1385 local objs = minetest.get_objects_inside_radius(s, self.view_range)
1386
1387 -- remove entities we aren't interested in
1388 for n = 1, #objs do
1389
1390 local ent = objs[n]:get_luaentity()
1391
1392 -- are we a player?
1393 if objs[n]:is_player() then
1394
1395 -- if player invisible or mob not setup to attack then remove from list
1396 if self.attack_players == false
1397 or (self.owner and self.type ~= "monster")
1398 or mobs.invis[objs[n]:get_player_name()]
1399 or not specific_attack(self.specific_attack, "player") then
1400 objs[n] = nil
1401 --print("- pla", n)
1402 end
1403
1404 -- or are we a mob?
1405 elseif ent and ent._cmi_is_mob then
1406
1407 -- remove mobs not to attack
1408 if self.name == ent.name
1409 or (not self.attack_animals and ent.type == "animal")
1410 or (not self.attack_monsters and ent.type == "monster")
1411 or (not self.attack_npcs and ent.type == "npc")
1412 or not specific_attack(self.specific_attack, ent.name) then
1413 objs[n] = nil
1414 --print("- mob", n, self.name, ent.name)
1415 end
1416
1417 -- remove all other entities
1418 else
1419 --print(" -obj", n)
1420 objs[n] = nil
1421 end
1422 end
1423
1424 local p, sp, dist, min_player
1425 local min_dist = self.view_range + 1
1426
1427 -- go through remaining entities and select closest
1428 for _,player in pairs(objs) do
1429
1430 p = player:get_pos()
1431 sp = s
1432
1433 dist = get_distance(p, s)
1434
1435 -- aim higher to make looking up hills more realistic
1436 p.y = p.y + 1
1437 sp.y = sp.y + 1
1438
1439 -- choose closest player to attack that isnt self
1440 if dist ~= 0
1441 and dist < min_dist
1442 and line_of_sight(self, sp, p, 2) == true then
1443 min_dist = dist
1444 min_player = player
1445 end
1446 end
1447
1448 -- attack closest player or mob
1449 if min_player and random(1, 100) > self.attack_chance then
1450 do_attack(self, min_player)
1451 end
1452 end
1453
1454
1455 -- specific runaway
1456 local specific_runaway = function(list, what)
1457
1458 -- no list so do not run
1459 if list == nil then
1460 return false
1461 end
1462
1463 -- found entity on list to attack?
1464 for no = 1, #list do
1465
1466 if list[no] == what then
1467 return true
1468 end
1469 end
1470
1471 return false
1472 end
1473
1474
1475 -- find someone to runaway from
1476 local runaway_from = function(self)
1477
1478 if not self.runaway_from then
1479 return
1480 end
1481
1482 local s = self.object:get_pos()
1483 local p, sp, dist, pname
1484 local player, obj, min_player, name
1485 local min_dist = self.view_range + 1
1486 local objs = minetest.get_objects_inside_radius(s, self.view_range)
1487
1488 for n = 1, #objs do
1489
1490 if objs[n]:is_player() then
1491
1492 pname = objs[n]:get_player_name()
1493
1494 if mobs.invis[pname]
1495 or self.owner == pname then
1496
1497 name = ""
1498 else
1499 player = objs[n]
1500 name = "player"
1501 end
1502 else
1503 obj = objs[n]:get_luaentity()
1504
1505 if obj then
1506 player = obj.object
1507 name = obj.name or ""
1508 end
1509 end
1510
1511 -- find specific mob to runaway from
1512 if name ~= "" and name ~= self.name
1513 and specific_runaway(self.runaway_from, name) then
1514
1515 p = player:get_pos()
1516 sp = s
1517
1518 -- aim higher to make looking up hills more realistic
1519 p.y = p.y + 1
1520 sp.y = sp.y + 1
1521
1522 dist = get_distance(p, s)
1523
1524 -- choose closest player/mob to runaway from
1525 if dist < min_dist
1526 and line_of_sight(self, sp, p, 2) == true then
1527 min_dist = dist
1528 min_player = player
1529 end
1530 end
1531 end
1532
1533 if min_player then
1534
1535 local lp = player:get_pos()
1536 local vec = {
1537 x = lp.x - s.x,
1538 y = lp.y - s.y,
1539 z = lp.z - s.z
1540 }
1541
1542 local yaw = (atan(vec.z / vec.x) + 3 * pi / 2) - self.rotate
1543
1544 if lp.x > s.x then
1545 yaw = yaw + pi
1546 end
1547
1548 yaw = set_yaw(self, yaw, 4)
1549 self.state = "runaway"
1550 self.runaway_timer = 3
1551 self.following = nil
1552 end
1553 end
1554
1555
1556 -- follow player if owner or holding item, if fish outta water then flop
1557 local follow_flop = function(self)
1558
1559 -- find player to follow
1560 if (self.follow ~= ""
1561 or self.order == "follow")
1562 and not self.following
1563 and self.state ~= "attack"
1564 and self.state ~= "runaway" then
1565
1566 local s = self.object:get_pos()
1567 local players = minetest.get_connected_players()
1568
1569 for n = 1, #players do
1570
1571 if get_distance(players[n]:get_pos(), s) < self.view_range
1572 and not mobs.invis[ players[n]:get_player_name() ] then
1573
1574 self.following = players[n]
1575
1576 break
1577 end
1578 end
1579 end
1580
1581 if self.type == "npc"
1582 and self.order == "follow"
1583 and self.state ~= "attack"
1584 and self.owner ~= "" then
1585
1586 -- npc stop following player if not owner
1587 if self.following
1588 and self.owner
1589 and self.owner ~= self.following:get_player_name() then
1590 self.following = nil
1591 end
1592 else
1593 -- stop following player if not holding specific item
1594 if self.following
1595 and self.following:is_player()
1596 and follow_holding(self, self.following) == false then
1597 self.following = nil
1598 end
1599
1600 end
1601
1602 -- follow that thing
1603 if self.following then
1604
1605 local s = self.object:get_pos()
1606 local p
1607
1608 if self.following:is_player() then
1609
1610 p = self.following:get_pos()
1611
1612 elseif self.following.object then
1613
1614 p = self.following.object:get_pos()
1615 end
1616
1617 if p then
1618
1619 local dist = get_distance(p, s)
1620
1621 -- dont follow if out of range
1622 if dist > self.view_range then
1623 self.following = nil
1624 else
1625 local vec = {
1626 x = p.x - s.x,
1627 z = p.z - s.z
1628 }
1629
1630 local yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
1631
1632 if p.x > s.x then yaw = yaw + pi end
1633
1634 yaw = set_yaw(self, yaw, 6)
1635
1636 -- anyone but standing npc's can move along
1637 if dist > self.reach
1638 and self.order ~= "stand" then
1639
1640 set_velocity(self, self.walk_velocity)
1641
1642 if self.walk_chance ~= 0 then
1643 set_animation(self, "walk")
1644 end
1645 else
1646 set_velocity(self, 0)
1647 set_animation(self, "stand")
1648 end
1649
1650 return
1651 end
1652 end
1653 end
1654
1655 -- swimmers flop when out of their element, and swim again when back in
1656 if self.fly then
1657 local s = self.object:get_pos()
1658 if not flight_check(self, s) then
1659
1660 self.state = "flop"
1661 self.object:set_velocity({x = 0, y = -5, z = 0})
1662
1663 set_animation(self, "stand")
1664
1665 return
1666 elseif self.state == "flop" then
1667 self.state = "stand"
1668 end
1669 end
1670 end
1671
1672
1673 -- dogshoot attack switch and counter function
1674 local dogswitch = function(self, dtime)
1675
1676 -- switch mode not activated
1677 if not self.dogshoot_switch
1678 or not dtime then
1679 return 0
1680 end
1681
1682 self.dogshoot_count = self.dogshoot_count + dtime
1683
1684 if (self.dogshoot_switch == 1
1685 and self.dogshoot_count > self.dogshoot_count_max)
1686 or (self.dogshoot_switch == 2
1687 and self.dogshoot_count > self.dogshoot_count2_max) then
1688
1689 self.dogshoot_count = 0
1690
1691 if self.dogshoot_switch == 1 then
1692 self.dogshoot_switch = 2
1693 else
1694 self.dogshoot_switch = 1
1695 end
1696 end
1697
1698 return self.dogshoot_switch
1699 end
1700
1701
1702 -- execute current state (stand, walk, run, attacks)
1703 local do_states = function(self, dtime)
1704
1705 local yaw = self.object:get_yaw() or 0
1706
1707 if self.state == "stand" then
1708
1709 if random(1, 4) == 1 then
1710
1711 local lp = nil
1712 local s = self.object:get_pos()
1713 local objs = minetest.get_objects_inside_radius(s, 3)
1714
1715 for n = 1, #objs do
1716
1717 if objs[n]:is_player() then
1718 lp = objs[n]:get_pos()
1719 break
1720 end
1721 end
1722
1723 -- look at any players nearby, otherwise turn randomly
1724 if lp then
1725
1726 local vec = {
1727 x = lp.x - s.x,
1728 z = lp.z - s.z
1729 }
1730
1731 yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
1732
1733 if lp.x > s.x then yaw = yaw + pi end
1734 else
1735 yaw = yaw + random(-0.5, 0.5)
1736 end
1737
1738 yaw = set_yaw(self, yaw, 8)
1739 end
1740
1741 set_velocity(self, 0)
1742 set_animation(self, "stand")
1743
1744 -- mobs ordered to stand stay standing
1745 if self.order ~= "stand"
1746 and self.walk_chance ~= 0
1747 and self.facing_fence ~= true
1748 and random(1, 100) <= self.walk_chance
1749 and is_at_cliff(self) == false then
1750
1751 set_velocity(self, self.walk_velocity)
1752 self.state = "walk"
1753 set_animation(self, "walk")
1754 end
1755
1756 elseif self.state == "walk" then
1757
1758 local s = self.object:get_pos()
1759 local lp = nil
1760
1761 -- is there something I need to avoid?
1762 if self.water_damage > 0
1763 and self.lava_damage > 0 then
1764
1765 lp = minetest.find_node_near(s, 1, {"group:water", "group:lava"})
1766
1767 elseif self.water_damage > 0 then
1768
1769 lp = minetest.find_node_near(s, 1, {"group:water"})
1770
1771 elseif self.lava_damage > 0 then
1772
1773 lp = minetest.find_node_near(s, 1, {"group:lava"})
1774 end
1775
1776 if lp then
1777
1778 -- if mob in water or lava then look for land
1779 if (self.lava_damage
1780 and minetest.registered_nodes[self.standing_in].groups.lava)
1781 or (self.water_damage
1782 and minetest.registered_nodes[self.standing_in].groups.water) then
1783
1784 lp = minetest.find_node_near(s, 5, {"group:soil", "group:stone",
1785 "group:sand", node_ice, node_snowblock})
1786
1787 -- did we find land?
1788 if lp then
1789
1790 local vec = {
1791 x = lp.x - s.x,
1792 z = lp.z - s.z
1793 }
1794
1795 yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
1796
1797 if lp.x > s.x then yaw = yaw + pi end
1798
1799 -- look towards land and jump/move in that direction
1800 yaw = set_yaw(self, yaw, 6)
1801 do_jump(self)
1802 set_velocity(self, self.walk_velocity)
1803 else
1804 yaw = yaw + random(-0.5, 0.5)
1805 end
1806
1807 else
1808
1809 local vec = {
1810 x = lp.x - s.x,
1811 z = lp.z - s.z
1812 }
1813
1814 yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
1815
1816 if lp.x > s.x then yaw = yaw + pi end
1817 end
1818
1819 yaw = set_yaw(self, yaw, 8)
1820
1821 -- otherwise randomly turn
1822 elseif random(1, 100) <= 30 then
1823
1824 yaw = yaw + random(-0.5, 0.5)
1825
1826 yaw = set_yaw(self, yaw, 8)
1827 end
1828
1829 -- stand for great fall in front
1830 local temp_is_cliff = is_at_cliff(self)
1831
1832 if self.facing_fence == true
1833 or temp_is_cliff
1834 or random(1, 100) <= 30 then
1835
1836 set_velocity(self, 0)
1837 self.state = "stand"
1838 set_animation(self, "stand")
1839 else
1840 set_velocity(self, self.walk_velocity)
1841
1842 if flight_check(self)
1843 and self.animation
1844 and self.animation.fly_start
1845 and self.animation.fly_end then
1846 set_animation(self, "fly")
1847 else
1848 set_animation(self, "walk")
1849 end
1850 end
1851
1852 -- runaway when punched
1853 elseif self.state == "runaway" then
1854
1855 self.runaway_timer = self.runaway_timer + 1
1856
1857 -- stop after 5 seconds or when at cliff
1858 if self.runaway_timer > 5
1859 or is_at_cliff(self)
1860 or self.order == "stand" then
1861 self.runaway_timer = 0
1862 set_velocity(self, 0)
1863 self.state = "stand"
1864 set_animation(self, "stand")
1865 else
1866 set_velocity(self, self.run_velocity)
1867 set_animation(self, "walk")
1868 end
1869
1870 -- attack routines (explode, dogfight, shoot, dogshoot)
1871 elseif self.state == "attack" then
1872
1873 -- calculate distance from mob and enemy
1874 local s = self.object:get_pos()
1875 local p = self.attack:get_pos() or s
1876 local dist = get_distance(p, s)
1877
1878 -- stop attacking if player invisible or out of range
1879 if dist > self.view_range
1880 or not self.attack
1881 or not self.attack:get_pos()
1882 or self.attack:get_hp() <= 0
1883 or (self.attack:is_player() and mobs.invis[ self.attack:get_player_name() ]) then
1884
1885 -- print(" ** stop attacking **", dist, self.view_range)
1886 self.state = "stand"
1887 set_velocity(self, 0)
1888 set_animation(self, "stand")
1889 self.attack = nil
1890 self.v_start = false
1891 self.timer = 0
1892 self.blinktimer = 0
1893 self.path.way = nil
1894
1895 return
1896 end
1897
1898 if self.attack_type == "explode" then
1899
1900 local vec = {
1901 x = p.x - s.x,
1902 z = p.z - s.z
1903 }
1904
1905 yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
1906
1907 if p.x > s.x then yaw = yaw + pi end
1908
1909 yaw = set_yaw(self, yaw)
1910
1911 local node_break_radius = self.explosion_radius or 1
1912 local entity_damage_radius = self.explosion_damage_radius
1913 or (node_break_radius * 2)
1914
1915 -- start timer when in reach and line of sight
1916 if not self.v_start
1917 and dist <= self.reach
1918 and line_of_sight(self, s, p, 2) then
1919
1920 self.v_start = true
1921 self.timer = 0
1922 self.blinktimer = 0
1923 mob_sound(self, self.sounds.fuse)
1924 -- print ("=== explosion timer started", self.explosion_timer)
1925
1926 -- stop timer if out of reach or direct line of sight
1927 elseif self.allow_fuse_reset
1928 and self.v_start
1929 and (dist > self.reach
1930 or not line_of_sight(self, s, p, 2)) then
1931 self.v_start = false
1932 self.timer = 0
1933 self.blinktimer = 0
1934 self.blinkstatus = false
1935 self.object:settexturemod("")
1936 end
1937
1938 -- walk right up to player unless the timer is active
1939 if self.v_start and (self.stop_to_explode or dist < 1.5) then
1940 set_velocity(self, 0)
1941 else
1942 set_velocity(self, self.run_velocity)
1943 end
1944
1945 if self.animation and self.animation.run_start then
1946 set_animation(self, "run")
1947 else
1948 set_animation(self, "walk")
1949 end
1950
1951 if self.v_start then
1952
1953 self.timer = self.timer + dtime
1954 self.blinktimer = (self.blinktimer or 0) + dtime
1955
1956 if self.blinktimer > 0.2 then
1957
1958 self.blinktimer = 0
1959
1960 if self.blinkstatus then
1961 self.object:settexturemod("")
1962 else
1963 self.object:settexturemod("^[brighten")
1964 end
1965
1966 self.blinkstatus = not self.blinkstatus
1967 end
1968
1969 -- print ("=== explosion timer", self.timer)
1970
1971 if self.timer > self.explosion_timer then
1972
1973 local pos = self.object:get_pos()
1974
1975 -- dont damage anything if area protected or next to water
1976 if minetest.find_node_near(pos, 1, {"group:water"})
1977 or minetest.is_protected(pos, "") then
1978
1979 node_break_radius = 1
1980 end
1981
1982 self.object:remove()
1983
1984 if minetest.get_modpath("tnt") and tnt and tnt.boom
1985 and not minetest.is_protected(pos, "") then
1986
1987 tnt.boom(pos, {
1988 radius = node_break_radius,
1989 damage_radius = entity_damage_radius,
1990 sound = self.sounds.explode,
1991 })
1992 else
1993
1994 minetest.sound_play(self.sounds.explode, {
1995 pos = pos,
1996 gain = 1.0,
1997 max_hear_distance = self.sounds.distance or 32
1998 })
1999
2000 entity_physics(pos, entity_damage_radius)
2001 effect(pos, 32, "tnt_smoke.png", nil, nil, node_break_radius, 1, 0)
2002 end
2003
2004 return
2005 end
2006 end
2007
2008 elseif self.attack_type == "dogfight"
2009 or (self.attack_type == "dogshoot" and dogswitch(self, dtime) == 2)
2010 or (self.attack_type == "dogshoot" and dist <= self.reach and dogswitch(self) == 0) then
2011
2012 if self.fly
2013 and dist > self.reach then
2014
2015 local p1 = s
2016 local me_y = floor(p1.y)
2017 local p2 = p
2018 local p_y = floor(p2.y + 1)
2019 local v = self.object:get_velocity()
2020
2021 if flight_check(self, s) then
2022
2023 if me_y < p_y then
2024
2025 self.object:set_velocity({
2026 x = v.x,
2027 y = 1 * self.walk_velocity,
2028 z = v.z
2029 })
2030
2031 elseif me_y > p_y then
2032
2033 self.object:set_velocity({
2034 x = v.x,
2035 y = -1 * self.walk_velocity,
2036 z = v.z
2037 })
2038 end
2039 else
2040 if me_y < p_y then
2041
2042 self.object:set_velocity({
2043 x = v.x,
2044 y = 0.01,
2045 z = v.z
2046 })
2047
2048 elseif me_y > p_y then
2049
2050 self.object:set_velocity({
2051 x = v.x,
2052 y = -0.01,
2053 z = v.z
2054 })
2055 end
2056 end
2057
2058 end
2059
2060 -- rnd: new movement direction
2061 if self.path.following
2062 and self.path.way
2063 and self.attack_type ~= "dogshoot" then
2064
2065 -- no paths longer than 50
2066 if #self.path.way > 50
2067 or dist < self.reach then
2068 self.path.following = false
2069 return
2070 end
2071
2072 local p1 = self.path.way[1]
2073
2074 if not p1 then
2075 self.path.following = false
2076 return
2077 end
2078
2079 if abs(p1.x-s.x) + abs(p1.z - s.z) < 0.6 then
2080 -- reached waypoint, remove it from queue
2081 table.remove(self.path.way, 1)
2082 end
2083
2084 -- set new temporary target
2085 p = {x = p1.x, y = p1.y, z = p1.z}
2086 end
2087
2088 local vec = {
2089 x = p.x - s.x,
2090 z = p.z - s.z
2091 }
2092
2093 yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
2094
2095 if p.x > s.x then yaw = yaw + pi end
2096
2097 yaw = set_yaw(self, yaw)
2098
2099 -- move towards enemy if beyond mob reach
2100 if dist > self.reach then
2101
2102 -- path finding by rnd
2103 if self.pathfinding -- only if mob has pathfinding enabled
2104 and enable_pathfinding then
2105
2106 smart_mobs(self, s, p, dist, dtime)
2107 end
2108
2109 if is_at_cliff(self) then
2110
2111 set_velocity(self, 0)
2112 set_animation(self, "stand")
2113 else
2114
2115 if self.path.stuck then
2116 set_velocity(self, self.walk_velocity)
2117 else
2118 set_velocity(self, self.run_velocity)
2119 end
2120
2121 if self.animation and self.animation.run_start then
2122 set_animation(self, "run")
2123 else
2124 set_animation(self, "walk")
2125 end
2126 end
2127
2128 else -- rnd: if inside reach range
2129
2130 self.path.stuck = false
2131 self.path.stuck_timer = 0
2132 self.path.following = false -- not stuck anymore
2133
2134 set_velocity(self, 0)
2135
2136 if not self.custom_attack then
2137
2138 if self.timer > 1 then
2139
2140 self.timer = 0
2141
2142 -- if self.double_melee_attack
2143 -- and random(1, 2) == 1 then
2144 -- set_animation(self, "punch2")
2145 -- else
2146 set_animation(self, "punch")
2147 -- end
2148
2149 local p2 = p
2150 local s2 = s
2151
2152 p2.y = p2.y + .5
2153 s2.y = s2.y + .5
2154
2155 if line_of_sight(self, p2, s2) == true then
2156
2157 -- play attack sound
2158 mob_sound(self, self.sounds.attack)
2159
2160 -- punch player (or what player is attached to)
2161 local attached = self.attack:get_attach()
2162 if attached then
2163 self.attack = attached
2164 end
2165 self.attack:punch(self.object, 1.0, {
2166 full_punch_interval = 1.0,
2167 damage_groups = {fleshy = self.damage}
2168 }, nil)
2169 end
2170 end
2171 else -- call custom attack every second
2172 if self.custom_attack
2173 and self.timer > 1 then
2174
2175 self.timer = 0
2176
2177 self.custom_attack(self, p)
2178 end
2179 end
2180 end
2181
2182 elseif self.attack_type == "shoot"
2183 or (self.attack_type == "dogshoot" and dogswitch(self, dtime) == 1)
2184 or (self.attack_type == "dogshoot" and dist > self.reach and dogswitch(self) == 0) then
2185
2186 p.y = p.y - .5
2187 s.y = s.y + .5
2188
2189 local dist = get_distance(p, s)
2190 local vec = {
2191 x = p.x - s.x,
2192 y = p.y - s.y,
2193 z = p.z - s.z
2194 }
2195
2196 yaw = (atan(vec.z / vec.x) + pi / 2) - self.rotate
2197
2198 if p.x > s.x then yaw = yaw + pi end
2199
2200 yaw = set_yaw(self, yaw)
2201
2202 set_velocity(self, 0)
2203
2204 if self.shoot_interval
2205 and self.timer > self.shoot_interval
2206 and random(1, 100) <= 60 then
2207
2208 self.timer = 0
2209 set_animation(self, "shoot")
2210
2211 -- play shoot attack sound
2212 mob_sound(self, self.sounds.shoot_attack)
2213
2214 local p = self.object:get_pos()
2215
2216 p.y = p.y + (self.collisionbox[2] + self.collisionbox[5]) / 2
2217
2218 if minetest.registered_entities[self.arrow] then
2219
2220 local obj = minetest.add_entity(p, self.arrow)
2221 local ent = obj:get_luaentity()
2222 local amount = (vec.x * vec.x + vec.y * vec.y + vec.z * vec.z) ^ 0.5
2223 local v = ent.velocity or 1 -- or set to default
2224
2225 ent.switch = 1
2226 ent.owner_id = tostring(self.object) -- add unique owner id to arrow
2227
2228 -- offset makes shoot aim accurate
2229 vec.y = vec.y + self.shoot_offset
2230 vec.x = vec.x * (v / amount)
2231 vec.y = vec.y * (v / amount)
2232 vec.z = vec.z * (v / amount)
2233
2234 obj:set_velocity(vec)
2235 end
2236 end
2237 end
2238 end
2239 end
2240
2241
2242 -- falling and fall damage
2243 local falling = function(self, pos)
2244
2245 if self.fly then
2246 return
2247 end
2248
2249 -- floating in water (or falling)
2250 local v = self.object:get_velocity()
2251
2252 if v.y > 0 then
2253
2254 -- apply gravity when moving up
2255 self.object:set_acceleration({
2256 x = 0,
2257 y = -10,
2258 z = 0
2259 })
2260
2261 elseif v.y <= 0 and v.y > self.fall_speed then
2262
2263 -- fall downwards at set speed
2264 self.object:set_acceleration({
2265 x = 0,
2266 y = self.fall_speed,
2267 z = 0
2268 })
2269 else
2270 -- stop accelerating once max fall speed hit
2271 self.object:set_acceleration({x = 0, y = 0, z = 0})
2272 end
2273
2274 -- in water then float up
2275 if self.standing_in
2276 and minetest.registered_nodes[self.standing_in].groups.water then
2277
2278 if self.floats == 1 then
2279
2280 self.object:set_acceleration({
2281 x = 0,
2282 y = -self.fall_speed / (max(1, v.y) ^ 8), -- 8 was 2
2283 z = 0
2284 })
2285 end
2286 else
2287
2288 -- fall damage onto solid ground
2289 if self.fall_damage == 1
2290 and self.object:get_velocity().y == 0 then
2291
2292 local d = (self.old_y or 0) - self.object:get_pos().y
2293
2294 if d > 5 then
2295
2296 self.health = self.health - floor(d - 5)
2297
2298 effect(pos, 5, "tnt_smoke.png", 1, 2, 2, nil)
2299
2300 if check_for_death(self, {type = "fall"}) then
2301 return
2302 end
2303 end
2304
2305 self.old_y = self.object:get_pos().y
2306 end
2307 end
2308 end
2309
2310
2311 -- is Took Ranks mod active?
2312 local tr = minetest.get_modpath("toolranks")
2313
2314 -- deal damage and effects when mob punched
2315 local mob_punch = function(self, hitter, tflp, tool_capabilities, dir)
2316
2317 -- mob health check
2318 if self.health <= 0 then
2319 return
2320 end
2321
2322 -- custom punch function
2323 if self.do_punch
2324 and self.do_punch(self, hitter, tflp, tool_capabilities, dir) == false then
2325 return
2326 end
2327
2328 -- error checking when mod profiling is enabled
2329 if not tool_capabilities then
2330 minetest.log("warning", "[mobs] Mod profiling enabled, damage not enabled")
2331 return
2332 end
2333
2334 -- is mob protected?
2335 if self.protected and hitter:is_player()
2336 and minetest.is_protected(self.object:get_pos(), hitter:get_player_name()) then
2337 minetest.chat_send_player(hitter:get_player_name(), S("Mob has been protected!"))
2338 return
2339 end
2340
2341 local weapon = hitter:get_wielded_item()
2342 local weapon_def = weapon:get_definition() or {}
2343 local punch_interval = 1.4
2344
2345 -- calculate mob damage
2346 local damage = 0
2347 local armor = self.object:get_armor_groups() or {}
2348 local tmp
2349
2350 -- quick error check incase it ends up 0 (serialize.h check test)
2351 if tflp == 0 then
2352 tflp = 0.2
2353 end
2354
2355 if use_cmi then
2356 damage = cmi.calculate_damage(self.object, hitter, tflp, tool_capabilities, dir)
2357 else
2358
2359 for group,_ in pairs( (tool_capabilities.damage_groups or {}) ) do
2360
2361 tmp = tflp / (tool_capabilities.full_punch_interval or 1.4)
2362
2363 if tmp < 0 then
2364 tmp = 0.0
2365 elseif tmp > 1 then
2366 tmp = 1.0
2367 end
2368
2369 damage = damage + (tool_capabilities.damage_groups[group] or 0)
2370 * tmp * ((armor[group] or 0) / 100.0)
2371 end
2372 end
2373
2374 -- check for tool immunity or special damage
2375 for n = 1, #self.immune_to do
2376
2377 if self.immune_to[n][1] == weapon_def.name then
2378
2379 damage = self.immune_to[n][2] or 0
2380 break
2381
2382 -- if "all" then no tool does damage unless it's specified in list
2383 elseif self.immune_to[n][1] == "all" then
2384 damage = self.immune_to[n][2] or 0
2385 end
2386 end
2387
2388 -- healing
2389 if damage <= -1 then
2390 self.health = self.health - floor(damage)
2391 return
2392 end
2393
2394 -- print ("Mob Damage is", damage)
2395
2396 if use_cmi
2397 and cmi.notify_punch(self.object, hitter, tflp, tool_capabilities, dir, damage) then
2398 return
2399 end
2400
2401 -- add weapon wear
2402 punch_interval = tool_capabilities.full_punch_interval or 1.4
2403
2404 -- toolrank support
2405 local wear = floor((punch_interval / 75) * 9000)
2406
2407 if mobs.is_creative(hitter:get_player_name()) then
2408
2409 if tr then
2410 wear = 1
2411 else
2412 wear = 0
2413 end
2414 end
2415
2416 if tr then
2417 if weapon_def.original_description then
2418 weapon:add_wear(toolranks.new_afteruse(weapon, hitter, nil, {wear = wear}))
2419 end
2420 else
2421 weapon:add_wear(wear)
2422 end
2423
2424 hitter:set_wielded_item(weapon)
2425
2426 -- only play hit sound and show blood effects if damage is 1 or over
2427 if damage >= 1 then
2428
2429 -- weapon sounds
2430 if weapon_def.sounds then
2431
2432 local s = random(0, #weapon_def.sounds)
2433
2434 minetest.sound_play(weapon_def.sounds[s], {
2435 object = self.object,
2436 max_hear_distance = 8
2437 })
2438 else
2439 minetest.sound_play("default_punch", {
2440 object = self.object,
2441 max_hear_distance = 5
2442 })
2443 end
2444
2445 -- blood_particles
2446 if not disable_blood and self.blood_amount > 0 then
2447
2448 local pos = self.object:get_pos()
2449
2450 pos.y = pos.y + (-self.collisionbox[2] + self.collisionbox[5]) * .5
2451
2452 -- do we have a single blood texture or multiple?
2453 if type(self.blood_texture) == "table" then
2454
2455 local blood = self.blood_texture[random(1, #self.blood_texture)]
2456
2457 effect(pos, self.blood_amount, blood, nil, nil, 1, nil)
2458 else
2459 effect(pos, self.blood_amount, self.blood_texture, nil, nil, 1, nil)
2460 end
2461 end
2462
2463 -- do damage
2464 self.health = self.health - floor(damage)
2465
2466 -- exit here if dead, check for tools with fire damage
2467 local hot = tool_capabilities and tool_capabilities.damage_groups
2468 and tool_capabilities.damage_groups.fire
2469
2470 if check_for_death(self, {type = "punch",
2471 puncher = hitter, hot = hot}) then
2472 return
2473 end
2474
2475 --[[ add healthy afterglow when hit (can cause hit lag with larger textures)
2476 minetest.after(0.1, function()
2477
2478 if not self.object:get_luaentity() then return end
2479
2480 self.object:settexturemod("^[colorize:#c9900070")
2481
2482 core.after(0.3, function()
2483 self.object:settexturemod("")
2484 end)
2485 end) ]]
2486
2487 end -- END if damage
2488
2489 -- knock back effect (only on full punch)
2490 if self.knock_back
2491 and tflp >= punch_interval then
2492
2493 local v = self.object:get_velocity()
2494 local kb = damage or 1
2495 local up = 2
2496
2497 -- if already in air then dont go up anymore when hit
2498 if v.y > 0
2499 or self.fly then
2500 up = 0
2501 end
2502
2503 -- direction error check
2504 dir = dir or {x = 0, y = 0, z = 0}
2505
2506 -- use tool knockback value or default
2507 kb = tool_capabilities.damage_groups["knockback"] or (kb * 1.5)
2508
2509 self.object:set_velocity({
2510 x = dir.x * kb,
2511 y = up,
2512 z = dir.z * kb
2513 })
2514
2515 self.pause_timer = 0.25
2516 end
2517
2518 -- if skittish then run away
2519 if self.runaway == true
2520 and self.order ~= "stand" then
2521
2522 local lp = hitter:get_pos()
2523 local s = self.object:get_pos()
2524 local vec = {
2525 x = lp.x - s.x,
2526 y = lp.y - s.y,
2527 z = lp.z - s.z
2528 }
2529
2530 local yaw = (atan(vec.z / vec.x) + 3 * pi / 2) - self.rotate
2531
2532 if lp.x > s.x then
2533 yaw = yaw + pi
2534 end
2535
2536 yaw = set_yaw(self, yaw, 6)
2537 self.state = "runaway"
2538 self.runaway_timer = 0
2539 self.following = nil
2540 end
2541
2542 local name = hitter:get_player_name() or ""
2543
2544 -- attack puncher and call other mobs for help
2545 if self.passive == false
2546 and self.state ~= "flop"
2547 and self.child == false
2548 and self.attack_players == true
2549 and hitter:get_player_name() ~= self.owner
2550 and not mobs.invis[ name ] then
2551
2552 -- attack whoever punched mob
2553 self.state = ""
2554 do_attack(self, hitter)
2555
2556 -- alert others to the attack
2557 local objs = minetest.get_objects_inside_radius(hitter:get_pos(), self.view_range)
2558 local obj = nil
2559
2560 for n = 1, #objs do
2561
2562 obj = objs[n]:get_luaentity()
2563
2564 if obj and obj._cmi_is_mob then
2565
2566 -- only alert members of same mob
2567 if obj.group_attack == true
2568 and obj.state ~= "attack"
2569 and obj.owner ~= name
2570 and obj.name == self.name then
2571 do_attack(obj, hitter)
2572 end
2573
2574 -- have owned mobs attack player threat
2575 if obj.owner == name and obj.owner_loyal then
2576 do_attack(obj, self.object)
2577 end
2578 end
2579 end
2580 end
2581 end
2582
2583
2584 -- get entity staticdata
2585 local mob_staticdata = function(self)
2586
2587 -- remove mob when out of range unless tamed
2588 if remove_far
2589 and self.remove_ok
2590 and self.type ~= "npc"
2591 and self.state ~= "attack"
2592 and not self.tamed
2593 and self.lifetimer < 20000 then
2594
2595 --print ("REMOVED " .. self.name)
2596
2597 self.object:remove()
2598
2599 return ""-- nil
2600 end
2601
2602 self.remove_ok = true
2603 self.attack = nil
2604 self.following = nil
2605 self.state = "stand"
2606
2607 -- used to rotate older mobs
2608 if self.drawtype
2609 and self.drawtype == "side" then
2610 self.rotate = math.rad(90)
2611 end
2612
2613 if use_cmi then
2614 self.serialized_cmi_components = cmi.serialize_components(self._cmi_components)
2615 end
2616
2617 local tmp = {}
2618
2619 for _,stat in pairs(self) do
2620
2621 local t = type(stat)
2622
2623 if t ~= "function"
2624 and t ~= "nil"
2625 and t ~= "userdata"
2626 and _ ~= "_cmi_components" then
2627 tmp[_] = self[_]
2628 end
2629 end
2630
2631 --print('===== '..self.name..'\n'.. dump(tmp)..'\n=====\n')
2632 return minetest.serialize(tmp)
2633 end
2634
2635
2636 -- activate mob and reload settings
2637 local mob_activate = function(self, staticdata, def, dtime)
2638
2639 -- remove monsters in peaceful mode
2640 if self.type == "monster"
2641 and peaceful_only then
2642
2643 self.object:remove()
2644
2645 return
2646 end
2647
2648 -- load entity variables
2649 local tmp = minetest.deserialize(staticdata)
2650
2651 if tmp then
2652 for _,stat in pairs(tmp) do
2653 self[_] = stat
2654 end
2655 end
2656
2657 -- select random texture, set model and size
2658 if not self.base_texture then
2659
2660 -- compatiblity with old simple mobs textures
2661 if def.textures and type(def.textures[1]) == "string" then
2662 def.textures = {def.textures}
2663 end
2664
2665 self.base_texture = def.textures and def.textures[random(1, #def.textures)]
2666 self.base_mesh = def.mesh
2667 self.base_size = self.visual_size
2668 self.base_colbox = self.collisionbox
2669 self.base_selbox = self.selectionbox
2670 end
2671
2672 -- for current mobs that dont have this set
2673 if not self.base_selbox then
2674 self.base_selbox = self.selectionbox or self.base_colbox
2675 end
2676
2677 -- set texture, model and size
2678 local textures = self.base_texture
2679 local mesh = self.base_mesh
2680 local vis_size = self.base_size
2681 local colbox = self.base_colbox
2682 local selbox = self.base_selbox
2683
2684 -- specific texture if gotten
2685 if self.gotten == true
2686 and def.gotten_texture then
2687 textures = def.gotten_texture
2688 end
2689
2690 -- specific mesh if gotten
2691 if self.gotten == true
2692 and def.gotten_mesh then
2693 mesh = def.gotten_mesh
2694 end
2695
2696 -- set child objects to half size
2697 if self.child == true then
2698
2699 vis_size = {
2700 x = self.base_size.x * .5,
2701 y = self.base_size.y * .5,
2702 }
2703
2704 if def.child_texture then
2705 textures = def.child_texture[1]
2706 end
2707
2708 colbox = {
2709 self.base_colbox[1] * .5,
2710 self.base_colbox[2] * .5,
2711 self.base_colbox[3] * .5,
2712 self.base_colbox[4] * .5,
2713 self.base_colbox[5] * .5,
2714 self.base_colbox[6] * .5
2715 }
2716 selbox = {
2717 self.base_selbox[1] * .5,
2718 self.base_selbox[2] * .5,
2719 self.base_selbox[3] * .5,
2720 self.base_selbox[4] * .5,
2721 self.base_selbox[5] * .5,
2722 self.base_selbox[6] * .5
2723 }
2724 end
2725
2726 if self.health == 0 then
2727 self.health = random (self.hp_min, self.hp_max)
2728 end
2729
2730 -- pathfinding init
2731 self.path = {}
2732 self.path.way = {} -- path to follow, table of positions
2733 self.path.lastpos = {x = 0, y = 0, z = 0}
2734 self.path.stuck = false
2735 self.path.following = false -- currently following path?
2736 self.path.stuck_timer = 0 -- if stuck for too long search for path
2737
2738 -- mob defaults
2739 self.object:set_armor_groups({immortal = 1, fleshy = self.armor})
2740 self.old_y = self.object:get_pos().y
2741 self.old_health = self.health
2742 self.sounds.distance = self.sounds.distance or 10
2743 self.textures = textures
2744 self.mesh = mesh
2745 self.collisionbox = colbox
2746 self.selectionbox = selbox
2747 self.visual_size = vis_size
2748 self.standing_in = "air"
2749
2750 -- check existing nametag
2751 if not self.nametag then
2752 self.nametag = def.nametag
2753 end
2754
2755 -- set anything changed above
2756 self.object:set_properties(self)
2757 set_yaw(self, (random(0, 360) - 180) / 180 * pi, 6)
2758 update_tag(self)
2759 set_animation(self, "stand")
2760
2761 -- run on_spawn function if found
2762 if self.on_spawn and not self.on_spawn_run then
2763 if self.on_spawn(self) then
2764 self.on_spawn_run = true -- if true, set flag to run once only
2765 end
2766 end
2767
2768 -- run after_activate
2769 if def.after_activate then
2770 def.after_activate(self, staticdata, def, dtime)
2771 end
2772
2773 if use_cmi then
2774 self._cmi_components = cmi.activate_components(self.serialized_cmi_components)
2775 cmi.notify_activate(self.object, dtime)
2776 end
2777 end
2778
2779
2780 -- handle mob lifetimer and expiration
2781 local mob_expire = function(self, pos, dtime)
2782
2783 -- when lifetimer expires remove mob (except npc and tamed)
2784 if self.type ~= "npc"
2785 and not self.tamed
2786 and self.state ~= "attack"
2787 and remove_far ~= true
2788 and self.lifetimer < 20000 then
2789
2790 self.lifetimer = self.lifetimer - dtime
2791
2792 if self.lifetimer <= 0 then
2793
2794 -- only despawn away from player
2795 local objs = minetest.get_objects_inside_radius(pos, 15)
2796
2797 for n = 1, #objs do
2798
2799 if objs[n]:is_player() then
2800
2801 self.lifetimer = 20
2802
2803 return
2804 end
2805 end
2806
2807 -- minetest.log("action",
2808 -- S("lifetimer expired, removed @1", self.name))
2809
2810 effect(pos, 15, "tnt_smoke.png", 2, 4, 2, 0)
2811
2812 self.object:remove()
2813
2814 return
2815 end
2816 end
2817 end
2818
2819
2820 -- main mob function
2821 local mob_step = function(self, dtime)
2822
2823 if use_cmi then
2824 cmi.notify_step(self.object, dtime)
2825 end
2826
2827 local pos = self.object:get_pos()
2828 local yaw = 0
2829
2830 -- get node at foot level every quarter second
2831 self.node_timer = (self.node_timer or 0) + dtime
2832
2833 if self.node_timer > 0.25 then
2834
2835 self.node_timer = 0
2836
2837 local y_level = self.collisionbox[2]
2838
2839 if self.child then
2840 y_level = self.collisionbox[2] * 0.5
2841 end
2842
2843 -- what is mob standing in?
2844 self.standing_in = node_ok({
2845 x = pos.x, y = pos.y + y_level + 0.25, z = pos.z}, "air").name
2846 -- print ("standing in " .. self.standing_in)
2847
2848 -- check for mob expiration (0.25 instead of dtime since were in a timer)
2849 mob_expire(self, pos, 0.25)
2850 end
2851
2852 -- check if falling, flying, floating
2853 falling(self, pos)
2854
2855 -- smooth rotation by ThomasMonroe314
2856
2857 if self.delay and self.delay > 0 then
2858
2859 local yaw = self.object:get_yaw()
2860
2861 if self.delay == 1 then
2862 yaw = self.target_yaw
2863 else
2864 local dif = abs(yaw - self.target_yaw)
2865
2866 if yaw > self.target_yaw then
2867
2868 if dif > pi then
2869 dif = 2 * pi - dif -- need to add
2870 yaw = yaw + dif / self.delay
2871 else
2872 yaw = yaw - dif / self.delay -- need to subtract
2873 end
2874
2875 elseif yaw < self.target_yaw then
2876
2877 if dif > pi then
2878 dif = 2 * pi - dif
2879 yaw = yaw - dif / self.delay -- need to subtract
2880 else
2881 yaw = yaw + dif / self.delay -- need to add
2882 end
2883 end
2884
2885 if yaw > (pi * 2) then yaw = yaw - (pi * 2) end
2886 if yaw < 0 then yaw = yaw + (pi * 2) end
2887 end
2888
2889 self.delay = self.delay - 1
2890 self.object:set_yaw(yaw)
2891 end
2892
2893 -- end rotation
2894
2895 -- knockback timer
2896 if self.pause_timer > 0 then
2897
2898 self.pause_timer = self.pause_timer - dtime
2899
2900 return
2901 end
2902
2903 -- run custom function (defined in mob lua file)
2904 if self.do_custom then
2905
2906 -- when false skip going any further
2907 if self.do_custom(self, dtime) == false then
2908 return
2909 end
2910 end
2911
2912 -- attack timer
2913 self.timer = self.timer + dtime
2914
2915 if self.state ~= "attack" then
2916
2917 if self.timer < 1 then
2918 return
2919 end
2920
2921 self.timer = 0
2922 end
2923
2924 -- never go over 100
2925 if self.timer > 100 then
2926 self.timer = 1
2927 end
2928
2929 -- mob plays random sound at times
2930 if random(1, 100) == 1 then
2931 mob_sound(self, self.sounds.random)
2932 end
2933
2934 -- environmental damage timer (every 1 second)
2935 self.env_damage_timer = self.env_damage_timer + dtime
2936
2937 if (self.state == "attack" and self.env_damage_timer > 1)
2938 or self.state ~= "attack" then
2939
2940 self.env_damage_timer = 0
2941
2942 -- check for environmental damage (water, fire, lava etc.)
2943 do_env_damage(self)
2944
2945 -- node replace check (cow eats grass etc.)
2946 replace(self, pos)
2947 end
2948
2949 general_attack(self)
2950
2951 breed(self)
2952
2953 follow_flop(self)
2954
2955 do_states(self, dtime)
2956
2957 do_jump(self)
2958
2959 runaway_from(self)
2960
2961 end
2962
2963
2964 -- default function when mobs are blown up with TNT
2965 local do_tnt = function(obj, damage)
2966
2967 --print ("----- Damage", damage)
2968
2969 obj.object:punch(obj.object, 1.0, {
2970 full_punch_interval = 1.0,
2971 damage_groups = {fleshy = damage},
2972 }, nil)
2973
2974 return false, true, {}
2975 end
2976
2977
2978 mobs.spawning_mobs = {}
2979
2980 -- register mob entity
2981 function mobs:register_mob(name, def)
2982
2983 mobs.spawning_mobs[name] = true
2984
2985 minetest.register_entity(name, {
2986
2987 stepheight = def.stepheight or 1.1, -- was 0.6
2988 name = name,
2989 type = def.type,
2990 attack_type = def.attack_type,
2991 fly = def.fly,
2992 fly_in = def.fly_in or "air",
2993 owner = def.owner or "",
2994 order = def.order or "",
2995 on_die = def.on_die,
2996 do_custom = def.do_custom,
2997 jump_height = def.jump_height or 4, -- was 6
2998 drawtype = def.drawtype, -- DEPRECATED, use rotate instead
2999 rotate = math.rad(def.rotate or 0), -- 0=front, 90=side, 180=back, 270=side2
3000 lifetimer = def.lifetimer or 180, -- 3 minutes
3001 hp_min = max(1, (def.hp_min or 5) * difficulty),
3002 hp_max = max(1, (def.hp_max or 10) * difficulty),
3003 physical = true,
3004 collisionbox = def.collisionbox or {-0.25, -0.25, -0.25, 0.25, 0.25, 0.25},
3005 selectionbox = def.selectionbox or def.collisionbox,
3006 visual = def.visual,
3007 visual_size = def.visual_size or {x = 1, y = 1},
3008 mesh = def.mesh,
3009 makes_footstep_sound = def.makes_footstep_sound or false,
3010 view_range = def.view_range or 5,
3011 walk_velocity = def.walk_velocity or 1,
3012 run_velocity = def.run_velocity or 2,
3013 damage = max(0, (def.damage or 0) * difficulty),
3014 light_damage = def.light_damage or 0,
3015 light_damage_min = def.light_damage_min or 14,
3016 light_damage_max = def.light_damage_max or 15,
3017 water_damage = def.water_damage or 0,
3018 lava_damage = def.lava_damage or 0,
3019 suffocation = def.suffocation or 2,
3020 fall_damage = def.fall_damage or 1,
3021 fall_speed = def.fall_speed or -10, -- must be lower than -2 (default: -10)
3022 drops = def.drops or {},
3023 armor = def.armor or 100,
3024 on_rightclick = def.on_rightclick,
3025 arrow = def.arrow,
3026 shoot_interval = def.shoot_interval,
3027 sounds = def.sounds or {},
3028 animation = def.animation,
3029 follow = def.follow,
3030 jump = def.jump ~= false,
3031 walk_chance = def.walk_chance or 50,
3032 attack_chance = def.attack_chance or 5,
3033 passive = def.passive or false,
3034 knock_back = def.knock_back ~= false,
3035 blood_amount = def.blood_amount or 5,
3036 blood_texture = def.blood_texture or "mobs_blood.png",
3037 shoot_offset = def.shoot_offset or 0,
3038 floats = def.floats or 1, -- floats in water by default
3039 replace_rate = def.replace_rate,
3040 replace_what = def.replace_what,
3041 replace_with = def.replace_with,
3042 replace_offset = def.replace_offset or 0,
3043 on_replace = def.on_replace,
3044 timer = 0,
3045 env_damage_timer = 0, -- only used when state = "attack"
3046 tamed = false,
3047 pause_timer = 0,
3048 horny = false,
3049 hornytimer = 0,
3050 child = false,
3051 gotten = false,
3052 health = 0,
3053 reach = def.reach or 3,
3054 htimer = 0,
3055 texture_list = def.textures,
3056 child_texture = def.child_texture,
3057 docile_by_day = def.docile_by_day or false,
3058 time_of_day = 0.5,
3059 fear_height = def.fear_height or 0,
3060 runaway = def.runaway,
3061 runaway_timer = 0,
3062 pathfinding = def.pathfinding,
3063 immune_to = def.immune_to or {},
3064 explosion_radius = def.explosion_radius,
3065 explosion_damage_radius = def.explosion_damage_radius,
3066 explosion_timer = def.explosion_timer or 3,
3067 allow_fuse_reset = def.allow_fuse_reset ~= false,
3068 stop_to_explode = def.stop_to_explode ~= false,
3069 custom_attack = def.custom_attack,
3070 double_melee_attack = def.double_melee_attack,
3071 dogshoot_switch = def.dogshoot_switch,
3072 dogshoot_count = 0,
3073 dogshoot_count_max = def.dogshoot_count_max or 5,
3074 dogshoot_count2_max = def.dogshoot_count2_max or (def.dogshoot_count_max or 5),
3075 group_attack = def.group_attack or false,
3076 attack_monsters = def.attacks_monsters or def.attack_monsters or false,
3077 attack_animals = def.attack_animals or false,
3078 attack_players = def.attack_players ~= false,
3079 attack_npcs = def.attack_npcs ~= false,
3080 specific_attack = def.specific_attack,
3081 runaway_from = def.runaway_from,
3082 owner_loyal = def.owner_loyal,
3083 facing_fence = false,
3084 pushable = def.pushable,
3085 _cmi_is_mob = true,
3086
3087 on_spawn = def.on_spawn,
3088
3089 on_blast = def.on_blast or do_tnt,
3090
3091 on_step = mob_step,
3092
3093 do_punch = def.do_punch,
3094
3095 on_punch = mob_punch,
3096
3097 on_breed = def.on_breed,
3098
3099 on_grown = def.on_grown,
3100
3101 on_activate = function(self, staticdata, dtime)
3102 return mob_activate(self, staticdata, def, dtime)
3103 end,
3104
3105 get_staticdata = function(self)
3106 return mob_staticdata(self)
3107 end,
3108
3109 })
3110
3111 end -- END mobs:register_mob function
3112
3113
3114 -- count how many mobs of one type are inside an area
3115 local count_mobs = function(pos, type)
3116
3117 local total = 0
3118 local objs = minetest.get_objects_inside_radius(pos, aoc_range * 2)
3119 local ent
3120
3121 for n = 1, #objs do
3122
3123 if not objs[n]:is_player() then
3124
3125 ent = objs[n]:get_luaentity()
3126
3127 -- count mob type and add to total also
3128 if ent and ent.name and ent.name == type then
3129 total = total + 1
3130 end
3131 end
3132 end
3133
3134 return total
3135 end
3136
3137
3138 -- global functions
3139
3140 function mobs:spawn_abm_check(pos, node, name)
3141 -- global function to add additional spawn checks
3142 -- return true to stop spawning mob
3143 end
3144
3145
3146 function mobs:spawn_specific(name, nodes, neighbors, min_light, max_light,
3147 interval, chance, aoc, min_height, max_height, day_toggle, on_spawn)
3148
3149 -- Do mobs spawn at all?
3150 if not mobs_spawn then
3151 return
3152 end
3153
3154 -- chance/spawn number override in minetest.conf for registered mob
3155 local numbers = minetest.settings:get(name)
3156
3157 if numbers then
3158 numbers = numbers:split(",")
3159 chance = tonumber(numbers[1]) or chance
3160 aoc = tonumber(numbers[2]) or aoc
3161
3162 if chance == 0 then
3163 minetest.log("warning", string.format("[mobs] %s has spawning disabled", name))
3164 return
3165 end
3166
3167 minetest.log("action",
3168 string.format("[mobs] Chance setting for %s changed to %s (total: %s)", name, chance, aoc))
3169
3170 end
3171
3172 minetest.register_abm({
3173
3174 label = name .. " spawning",
3175 nodenames = nodes,
3176 neighbors = neighbors,
3177 interval = interval,
3178 chance = max(1, (chance * mob_chance_multiplier)),
3179 catch_up = false,
3180
3181 action = function(pos, node, active_object_count, active_object_count_wider)
3182
3183 -- is mob actually registered?
3184 if not mobs.spawning_mobs[name]
3185 or not minetest.registered_entities[name] then
3186 --print ("--- mob doesn't exist", name)
3187 return
3188 end
3189
3190 -- additional custom checks for spawning mob
3191 if mobs:spawn_abm_check(pos, node, name) == true then
3192 return
3193 end
3194
3195 -- do not spawn if too many entities in area
3196 if active_object_count_wider >= max_per_block then
3197 --print("--- too many entities in area", active_object_count_wider)
3198 return
3199 end
3200
3201 -- get total number of this mob in area
3202 local num_mob = count_mobs(pos, name)
3203
3204 if num_mob >= aoc then
3205 --print ("--- too many " .. name .. " in area", num_mob .. "/" .. aoc)
3206 return
3207 end
3208
3209 -- if toggle set to nil then ignore day/night check
3210 if day_toggle ~= nil then
3211