From 35a60019a19bf73a9807263e65232f2aaf2da6b9 Mon Sep 17 00:00:00 2001 From: Izzy Date: Tue, 19 Jan 2016 19:21:22 -0800 Subject: [PATCH] initial commit --- README.md | 5 + api.lua | 2408 ++++++++++++++++++++++++++++++++++++ behavior.lua | 372 ++++++ depends.txt | 1 + giant.lua | 66 + init.lua | 20 + license.txt | 21 + models/character.b3d | Bin 0 -> 86880 bytes simple_api.lua | 521 ++++++++ sounds/default_punch.ogg | Bin 0 -> 5946 bytes spawner.lua | 127 ++ textures/mob_spawner.png | Bin 0 -> 108 bytes textures/mobs_blood.png | Bin 0 -> 267 bytes textures/mobs_npc.png | Bin 0 -> 901 bytes textures/mobs_npc2.png | Bin 0 -> 1018 bytes textures/mobs_npc_baby.png | Bin 0 -> 684 bytes 16 files changed, 3541 insertions(+) create mode 100644 api.lua create mode 100644 behavior.lua create mode 100644 depends.txt create mode 100644 giant.lua create mode 100644 init.lua create mode 100644 license.txt create mode 100644 models/character.b3d create mode 100644 simple_api.lua create mode 100644 sounds/default_punch.ogg create mode 100644 spawner.lua create mode 100644 textures/mob_spawner.png create mode 100644 textures/mobs_blood.png create mode 100644 textures/mobs_npc.png create mode 100644 textures/mobs_npc2.png create mode 100644 textures/mobs_npc_baby.png diff --git a/README.md b/README.md index caf5dd1..9a885ee 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ # minetest_giants A giant-themed mobs mod based on behavior trees + + +Based on Mobs (Redo) by PilzAdam, KrupnovPavel, Zeg9 and TenPlus1 +https://forum.minetest.net/viewtopic.php?f=9&t=9917 + diff --git a/api.lua b/api.lua new file mode 100644 index 0000000..510fe00 --- /dev/null +++ b/api.lua @@ -0,0 +1,2408 @@ +-- Mobs Api (16th January 2016) +mobs = {} +mobs.mod = "smart" + +-- Load settings +local damage_enabled = minetest.setting_getbool("enable_damage") +local peaceful_only = minetest.setting_getbool("only_peaceful_mobs") +local disable_blood = minetest.setting_getbool("mobs_disable_blood") +local creative = minetest.setting_getbool("creative_mode") + +mobs.protected = tonumber(minetest.setting_get("mobs_spawn_protected")) or 1 +mobs.remove = minetest.setting_getbool("remove_far_mobs") + +-- internal functions + +local pi = math.pi +local square = math.sqrt + +do_attack = function(self, player) + + if self.state ~= "attack" then + + if math.random(0,100) < 90 + and self.sounds.war_cry then + + minetest.sound_play(self.sounds.war_cry,{ + object = self.object, + max_hear_distance = self.sounds.distance + }) + end + + self.state = "attack" + self.attack = player + end +end + +set_velocity = function(self, v) + + local x = 0 + local z = 0 + + if v and v ~= 0 then + + local yaw = (self.object:getyaw() + self.rotate) or 0 + + x = math.sin(yaw) * -v + z = math.cos(yaw) * v + end + + self.object:setvelocity({ + x = x, + y = self.object:getvelocity().y, + z = z + }) +end + +get_velocity = function(self) + + local v = self.object:getvelocity() + + return (v.x * v.x + v.z * v.z) ^ 0.5 +end + +set_animation = function(self, type) + + if not self.animation then + return + end + + self.animation.current = self.animation.current or "" + + if type == "stand" + and self.animation.current ~= "stand" then + + if self.animation.stand_start + and self.animation.stand_end + and self.animation.speed_normal then + + self.object:set_animation({ + x = self.animation.stand_start, + y = self.animation.stand_end}, + + self.animation.speed_normal, 0) + self.animation.current = "stand" + end + + elseif type == "walk" + and self.animation.current ~= "walk" then + + if self.animation.walk_start + and self.animation.walk_end + and self.animation.speed_normal then + + self.object:set_animation({ + x = self.animation.walk_start, + y = self.animation.walk_end}, + self.animation.speed_normal, 0) + + self.animation.current = "walk" + end + + elseif type == "run" + and self.animation.current ~= "run" then + + if self.animation.run_start + and self.animation.run_end + and self.animation.speed_run then + + self.object:set_animation({ + x = self.animation.run_start, + y = self.animation.run_end}, + self.animation.speed_run, 0) + + self.animation.current = "run" + end + + elseif type == "punch" + and self.animation.current ~= "punch" then + + if self.animation.punch_start + and self.animation.punch_end + and self.animation.speed_normal then + + self.object:set_animation({ + x = self.animation.punch_start, + y = self.animation.punch_end}, + self.animation.speed_normal, 0) + + self.animation.current = "punch" + end + end +end + +-- particle effects +function effect(pos, amount, texture, max_size) + + minetest.add_particlespawner({ + amount = amount, + time = 0.25, + minpos = pos, + maxpos = pos, + minvel = {x = -0, y = -2, z = -0}, + maxvel = {x = 2, y = 2, z = 2}, + minacc = {x = -4, y = -4, z = -4}, + maxacc = {x = 4, y = 4, z = 4}, + minexptime = 0.1, + maxexptime = 1, + minsize = 0.5, + maxsize = (max_size or 1), + texture = texture, + }) +end + +-- update nametag and colour +function update_tag(self) + + local col = "#00FF00" + local qua = self.hp_max / 4 + + if self.health <= math.floor(qua * 3) then + col = "#FFFF00" + end + + if self.health <= math.floor(qua * 2) then + col = "#FF6600" + end + + if self.health <= math.floor(qua) then + col = "#FF0000" + end + + self.object:set_properties({ + nametag = self.nametag, + nametag_color = col + }) + +end + +-- check if mob is dead or only hurt +function check_for_death(self) + + -- return if no change + local hp = self.object:get_hp() + + if hp == self.health then + return false + end + + local pos = self.object:getpos() + + -- still got some health? play hurt sound + if hp > 0 then + + self.health = hp + + if self.sounds.damage then + + minetest.sound_play(self.sounds.damage,{ + pos = pos, + gain = 1.0, + max_hear_distance = self.sounds.distance + }) + end + + update_tag(self) + + return false + end + + -- drop items when dead + local obj + + for _,drop in pairs(self.drops) do + + if math.random(1, drop.chance) == 1 then + + obj = minetest.add_item(pos, + ItemStack(drop.name .. " " + .. math.random(drop.min, drop.max))) + + if obj then + + obj:setvelocity({ + x = math.random(-1, 1), + y = 6, + z = math.random(-1, 1) + }) + end + end + end + + -- play death sound + if self.sounds.death then + + minetest.sound_play(self.sounds.death,{ + pos = pos, + gain = 1.0, + max_hear_distance = self.sounds.distance + }) + end + + -- execute custom death function + if self.on_die then + self.on_die(self, pos) + end + + self.object:remove() + + return true +end + +-- check if within map limits (-30911 to 30927) +function within_limits(pos, radius) + + if (pos.x - radius) > -30913 + and (pos.x + radius) < 30928 + and (pos.y - radius) > -30913 + and (pos.y + radius) < 30928 + and (pos.z - radius) > -30913 + and (pos.z + radius) < 30928 then + return true -- within limits + end + + return false -- beyond limits +end + +-- is mob facing a cliff +local function is_at_cliff(self) + + if self.fear_height == 0 then -- if 0, no falling protection! + return false + end + + local yaw = self.object:getyaw() + local dir_x = -math.sin(yaw) * (self.collisionbox[4] + 0.5) + local dir_z = math.cos(yaw) * (self.collisionbox[4] + 0.5) + local pos = self.object:getpos() + local ypos = pos.y + self.collisionbox[2] -- just above floor + + if minetest.line_of_sight( + {x = pos.x + dir_x, y = ypos, z = pos.z + dir_z}, + {x = pos.x + dir_x, y = ypos - self.fear_height, z = pos.z + dir_z} + , 1) then + + return true + end + + return false +end + +-- environmental damage (water, lava, fire, light) +do_env_damage = function(self) + + -- feed/tame text timer (so mob 'full' messages dont spam chat) + if self.htimer > 0 then + self.htimer = self.htimer - 1 + end + + local pos = self.object:getpos() + + self.time_of_day = minetest.get_timeofday() + + -- remove mob if beyond map limits + if not within_limits(pos, 0) then + self.object:remove() + return + end + + -- daylight above ground + if self.light_damage ~= 0 + and pos.y > 0 + and self.time_of_day > 0.2 + and self.time_of_day < 0.8 + and (minetest.get_node_light(pos) or 0) > 12 then + + self.object:set_hp(self.object:get_hp() - self.light_damage) + + effect(pos, 5, "tnt_smoke.png") + end + + if self.water_damage ~= 0 + or self.lava_damage ~= 0 then + + pos.y = pos.y + self.collisionbox[2] + 0.1 -- foot level + + local nod = node_ok(pos, "air") ; --print ("standing in "..nod.name) + local nodef = minetest.registered_nodes[nod.name] + + pos.y = pos.y + 1 + + -- water + if self.water_damage ~= 0 + and nodef.groups.water then + + self.object:set_hp(self.object:get_hp() - self.water_damage) + + effect(pos, 5, "bubble.png") + end + + -- lava or fire + if self.lava_damage ~= 0 + and (nodef.groups.lava + or nod.name == "fire:basic_flame" + or nod.name == "fire:permanent_flame") then + + self.object:set_hp(self.object:get_hp() - self.lava_damage) + + effect(pos, 5, "fire_basic_flame.png") + end + end + + check_for_death(self) +end + +-- jump if facing a solid node (not fences) +do_jump = function(self) + + if self.fly + or self.child then + return + end + + local pos = self.object:getpos() + + -- what is mob standing on? + pos.y = pos.y + self.collisionbox[2] - 0.2 + + local nod = node_ok(pos) + +--print ("standing on:", nod.name, pos.y) + + if minetest.registered_nodes[nod.name].walkable == false then + return + end + + -- where is front + local yaw = self.object:getyaw() + local dir_x = -math.sin(yaw) * (self.collisionbox[4] + 0.5) + local dir_z = math.cos(yaw) * (self.collisionbox[4] + 0.5) + + -- what is in front of mob? + local nod = node_ok({ + x = pos.x + dir_x, + y = pos.y + 0.5, + z = pos.z + dir_z + }) + + -- thin blocks that do not need to be jumped + if nod.name == "default:snow" then + return + end + +--print ("in front:", nod.name, pos.y + 0.5) + + if minetest.registered_items[nod.name].walkable + and not nod.name:find("fence") + or self.walk_chance == 0 then + + local v = self.object:getvelocity() + + v.y = self.jump_height + 1 + v.x = v.x * 2.2 + v.z = v.z * 2.2 + + self.object:setvelocity(v) + + if self.sounds.jump then + + minetest.sound_play(self.sounds.jump, { + pos = pos, + gain = 1.0, + max_hear_distance = self.sounds.distance + }) + end + else + if self.state ~= "attack" then + self.state = "stand" + set_animation(self, "stand") + end + end +end + +-- this is a faster way to calculate distance +function get_distance(a, b) + + local x, y, z = a.x - b.x, a.y - b.y, a.z - b.z + + return square(x * x + y * y + z * z) +end + +-- blast damage to entities nearby (modified from TNT mod) +function entity_physics(pos, radius) + + radius = radius * 2 + + local objs = minetest.get_objects_inside_radius(pos, radius) + local obj_pos, dist + + for _, obj in pairs(objs) do + + obj_pos = obj:getpos() + + dist = math.max(1, get_distance(pos, obj_pos)) + + local damage = math.floor((4 / dist) * radius) + obj:set_hp(obj:get_hp() - damage) + end +end + +-- get node but use fallback for nil or unknown +function node_ok(pos, fallback) + + fallback = fallback or "default:dirt" + + local node = minetest.get_node_or_nil(pos) + + if not node then + return minetest.registered_nodes[fallback] + end + + local nodef = minetest.registered_nodes[node.name] + + if nodef then + return node + end + + return minetest.registered_nodes[fallback] +end + +-- should mob follow what I'm holding ? +function follow_holding(self, clicker) + + local item = clicker:get_wielded_item() + local t = type(self.follow) + + -- single item + if t == "string" + and item:get_name() == self.follow then + return true + + -- multiple items + elseif t == "table" then + + for no = 1, #self.follow do + + if self.follow[no] == item:get_name() then + return true + end + end + end + + return false +end + +local function breed(self) + + -- child take 240 seconds before growing into adult + if self.child == true then + + self.hornytimer = self.hornytimer + 1 + + if self.hornytimer > 240 then + + self.child = false + self.hornytimer = 0 + + self.object:set_properties({ + textures = self.base_texture, + mesh = self.base_mesh, + visual_size = self.base_size, + collisionbox = self.base_colbox, + }) + + -- jump when fully grown so not to fall into ground + self.object:setvelocity({ + x = 0, + y = self.jump_height, + z = 0 + }) + end + + return + end + + -- horny animal can mate for 40 seconds, + -- afterwards horny animal cannot mate again for 200 seconds + if self.horny == true + and self.hornytimer < 240 then + + self.hornytimer = self.hornytimer + 1 + + if self.hornytimer >= 240 then + self.hornytimer = 0 + self.horny = false + end + end + + -- find another same animal who is also horny and mate if close enough + if self.horny == true + and self.hornytimer <= 40 then + + local pos = self.object:getpos() + + effect({x = pos.x, y = pos.y + 1, z = pos.z}, 4, "heart.png") + + local ents = minetest.get_objects_inside_radius(pos, 3) + local num = 0 + local ent = nil + + for i, obj in pairs(ents) do + + ent = obj:get_luaentity() + + -- check for same animal with different colour + local canmate = false + + if ent then + + if ent.name == self.name then + canmate = true + else + local entname = string.split(ent.name,":") + local selfname = string.split(self.name,":") + + if entname[1] == selfname[1] then + entname = string.split(entname[2],"_") + selfname = string.split(selfname[2],"_") + + if entname[1] == selfname[1] then + canmate = true + end + end + end + end + + if ent + and canmate == true + and ent.horny == true + and ent.hornytimer <= 40 then + num = num + 1 + end + + -- found your mate? then have a baby + if num > 1 then + + self.hornytimer = 41 + ent.hornytimer = 41 + + -- spawn baby + minetest.after(5, function(dtime) + + local mob = minetest.add_entity(pos, self.name) + local ent2 = mob:get_luaentity() + local textures = self.base_texture + + if self.child_texture then + textures = self.child_texture[1] + end + + mob:set_properties({ + textures = textures, + visual_size = { + x = self.base_size.x / 2, + y = self.base_size.y / 2 + }, + collisionbox = { + self.base_colbox[1] / 2, + self.base_colbox[2] / 2, + self.base_colbox[3] / 2, + self.base_colbox[4] / 2, + self.base_colbox[5] / 2, + self.base_colbox[6] / 2 + }, + }) + ent2.child = true + ent2.tamed = true + ent2.owner = self.owner + end) + + num = 0 + + break + end + end + end +end + +function replace(self, pos) + + if self.replace_rate + and self.child == false + and math.random(1, self.replace_rate) == 1 then + + local pos = self.object:getpos() + + pos.y = pos.y + self.replace_offset + +-- print ("replace node = ".. minetest.get_node(pos).name, pos.y) + + if self.replace_what + and self.object:getvelocity().y == 0 + and #minetest.find_nodes_in_area(pos, pos, self.replace_what) > 0 then + + minetest.set_node(pos, {name = self.replace_with}) + + -- when cow/sheep eats grass, replace wool and milk + if self.gotten == true then + self.gotten = false + self.object:set_properties(self) + end + end + end +end + +-- check if daytime and also if mob is docile during daylight hours +function day_docile(self) + + if self.docile_by_day == false then + + return false + + elseif self.docile_by_day == true + and self.time_of_day > 0.2 + and self.time_of_day < 0.8 then + + return true + end +end + +-- register mob function +function mobs:register_mob(name, def) + +minetest.register_entity(name, { + + stepheight = def.stepheight or 0.6, + name = name, + type = def.type, + attack_type = def.attack_type, + fly = def.fly, + fly_in = def.fly_in or "air", + owner = def.owner or "", + order = def.order or "", + on_die = def.on_die, + do_custom = def.do_custom, + jump_height = def.jump_height or 6, + jump_chance = def.jump_chance or 0, + drawtype = def.drawtype, -- DEPRECATED, use rotate instead + rotate = math.rad(def.rotate or 0), -- 0=front, 90=side, 180=back, 270=side2 + lifetimer = def.lifetimer or 180, -- 3 minutes + hp_min = def.hp_min or 5, + hp_max = def.hp_max or 10, + physical = true, + collisionbox = def.collisionbox, + visual = def.visual, + visual_size = def.visual_size or {x = 1, y = 1}, + mesh = def.mesh, + makes_footstep_sound = def.makes_footstep_sound or false, + view_range = def.view_range or 5, + walk_velocity = def.walk_velocity or 1, + run_velocity = def.run_velocity or 2, + damage = def.damage or 0, + light_damage = def.light_damage or 0, + water_damage = def.water_damage or 0, + lava_damage = def.lava_damage or 0, + fall_damage = def.fall_damage or 1, + fall_speed = def.fall_speed or -10, -- must be lower than -2 (default: -10) + drops = def.drops or {}, + armor = def.armor, + on_rightclick = def.on_rightclick, + arrow = def.arrow, + shoot_interval = def.shoot_interval, + sounds = def.sounds or {}, + animation = def.animation, + follow = def.follow, + jump = def.jump or true, + walk_chance = def.walk_chance or 50, + attacks_monsters = def.attacks_monsters or false, + group_attack = def.group_attack or false, + --fov = def.fov or 120, + passive = def.passive or false, + recovery_time = def.recovery_time or 0.5, + knock_back = def.knock_back or 3, + blood_amount = def.blood_amount or 5, + blood_texture = def.blood_texture or "mobs_blood.png", + shoot_offset = def.shoot_offset or 0, + floats = def.floats or 1, -- floats in water by default + replace_rate = def.replace_rate, + replace_what = def.replace_what, + replace_with = def.replace_with, + replace_offset = def.replace_offset or 0, + timer = 0, + env_damage_timer = 0, -- only used when state = "attack" + tamed = false, + pause_timer = 0, + horny = false, + hornytimer = 0, + child = false, + gotten = false, + health = 0, + reach = def.reach or 3, + htimer = 0, + child_texture = def.child_texture, + docile_by_day = def.docile_by_day or false, + time_of_day = 0.5, + fear_height = def.fear_height or 0, +runaway = def.runaway, +runaway_timer = 0, + + on_step = function(self, dtime) + + local pos = self.object:getpos() + local yaw = self.object:getyaw() or 0 + + -- when lifetimer expires remove mob (except npc and tamed) + if self.type ~= "npc" + and not self.tamed + and self.state ~= "attack" then + + self.lifetimer = self.lifetimer - dtime + + if self.lifetimer <= 0 then + + minetest.log("action", + "lifetimer expired, removed " .. self.name) + + effect(pos, 15, "tnt_smoke.png") + + self.object:remove() + + return + end + end + + if not self.fly then + + -- floating in water (or falling) + local v = self.object:getvelocity() + + -- going up then apply gravity + if v.y > 0.1 then + + self.object:setacceleration({ + x = 0, + y = self.fall_speed, + z = 0 + }) + end + + -- in water then float up + if minetest.registered_nodes[node_ok(pos).name].groups.water then + + if self.floats == 1 then + + self.object:setacceleration({ + x = 0, + y = -self.fall_speed / (math.max(1, v.y) ^ 2), + z = 0 + }) + end + else + -- fall downwards + self.object:setacceleration({ + x = 0, + y = self.fall_speed, + z = 0 + }) + + -- fall damage + if self.fall_damage == 1 + and self.object:getvelocity().y == 0 then + + local d = self.old_y - self.object:getpos().y + + if d > 5 then + + self.object:set_hp(self.object:get_hp() - math.floor(d - 5)) + + effect(pos, 5, "tnt_smoke.png") + + if check_for_death(self) then + return + end + end + + self.old_y = self.object:getpos().y + end + end + end + + -- knockback timer + if self.pause_timer > 0 then + + self.pause_timer = self.pause_timer - dtime + + if self.pause_timer < 1 then + self.pause_timer = 0 + end + + return + end + + -- attack timer + self.timer = self.timer + dtime + + if self.state ~= "attack" then + + if self.timer < 1 then + return + end + + self.timer = 0 + end + + -- never go over 100 + if self.timer > 100 then + self.timer = 1 + end + + -- node replace check (cow eats grass etc.) + replace(self, pos) + + -- mob plays random sound at times + if self.sounds.random + and math.random(1, 100) == 1 then + + minetest.sound_play(self.sounds.random, { + object = self.object, + max_hear_distance = self.sounds.distance + }) + end + + -- environmental damage timer (every 1 second) + self.env_damage_timer = self.env_damage_timer + dtime + + if (self.state == "attack" and self.env_damage_timer > 1) + or self.state ~= "attack" then + + self.env_damage_timer = 0 + + do_env_damage(self) + + -- custom function (defined in mob lua file) + if self.do_custom then + self.do_custom(self) + end + end + + -- find someone to attack + if self.type == "monster" + and damage_enabled + and self.state ~= "attack" + and not day_docile(self) then + + local s = self.object:getpos() + local p, sp, dist + local player = nil + local type = nil + local obj = nil + local min_dist = self.view_range + 1 + local min_player = nil + + for _,oir in pairs(minetest.get_objects_inside_radius(s, self.view_range)) do + + if oir:is_player() then + + player = oir + type = "player" + else + obj = oir:get_luaentity() + + if obj then + player = obj.object + type = obj.type + end + end + + if type == "player" + or type == "npc" then + + s = self.object:getpos() + p = player:getpos() + sp = s + + -- aim higher to make looking up hills more realistic + p.y = p.y + 1 + sp.y = sp.y + 1 + + dist = get_distance(p, s) + + if dist < self.view_range then + -- field of view check goes here + + -- choose closest player to attack + if minetest.line_of_sight(sp, p, 2) == true + and dist < min_dist then + min_dist = dist + min_player = player + end + end + end + end + + -- attack player + if min_player then + do_attack(self, min_player) + end + end + + -- npc, find closest monster to attack + local min_dist = self.view_range + 1 + local min_player = nil + + if self.type == "npc" + and self.attacks_monsters + and self.state ~= "attack" then + + local s = self.object:getpos() + local obj = nil + + for _, oir in pairs(minetest.get_objects_inside_radius(s, self.view_range)) do + + obj = oir:get_luaentity() + + if obj + and obj.type == "monster" then + + -- attack monster + p = obj.object:getpos() + + dist = get_distance(p, s) + + if dist < min_dist then + min_dist = dist + min_player = obj.object + end + end + end + + if min_player then + do_attack(self, min_player) + end + end + + -- breed and grow children + breed(self) + + -- find player to follow + if (self.follow ~= "" + or self.order == "follow") + and not self.following + and self.state ~= "attack" +and self.state ~= "runaway" then + + local s, p, dist + + for _,player in pairs(minetest.get_connected_players()) do + + s = self.object:getpos() + p = player:getpos() + dist = get_distance(p, s) + + if dist < self.view_range then + self.following = player + break + end + end + end + + if self.type == "npc" + and self.order == "follow" + and self.state ~= "attack" + and self.owner ~= "" then + + -- npc stop following player if not owner + if self.following + and self.owner + and self.owner ~= self.following:get_player_name() then + self.following = nil + end + else + -- stop following player if not holding specific item + if self.following + and self.following:is_player() + and follow_holding(self, self.following) == false then + self.following = nil + end + + end + + -- follow that thing + if self.following then + + local s = self.object:getpos() + local p + + if self.following:is_player() then + + p = self.following:getpos() + + elseif self.following.object then + + p = self.following.object:getpos() + end + + if p then + + local dist = get_distance(p, s) + + -- dont follow if out of range + if dist > self.view_range then + self.following = nil + else + local vec = { + x = p.x - s.x, + y = p.y - s.y, + z = p.z - s.z + } + + if vec.x ~= 0 + or vec.z ~= 0 then + + yaw = (math.atan(vec.z / vec.x) + pi / 2) - self.rotate + + if p.x > s.x then + yaw = yaw + pi + end + + self.object:setyaw(yaw) + end + + -- anyone but standing npc's can move along + if dist > self.reach + and self.order ~= "stand" then + + if (self.jump + and get_velocity(self) <= 0.5 + and self.object:getvelocity().y == 0) + or (self.object:getvelocity().y == 0 + and self.jump_chance > 0) then + + do_jump(self) + end + + set_velocity(self, self.walk_velocity) + + if self.walk_chance ~= 0 then + set_animation(self, "walk") + end + else + set_velocity(self, 0) + set_animation(self, "stand") + end + + return + end + end + end + + if self.state == "stand" then + + if math.random(1, 4) == 1 then + + local lp = nil + local s = self.object:getpos() + + if self.type == "npc" then + + local o = minetest.get_objects_inside_radius(self.object:getpos(), 3) + + for _,o in pairs(o) do + + if o:is_player() then + lp = o:getpos() + break + end + end + end + + -- look at any players nearby, otherwise turn randomly + if lp then + + local vec = { + x = lp.x - s.x, + y = lp.y - s.y, + z = lp.z - s.z + } + + if vec.x ~= 0 + or vec.z ~= 0 then + + yaw = (math.atan(vec.z / vec.x) + pi / 2) - self.rotate + + if lp.x > s.x then + yaw = yaw + pi + end + end + else + yaw = self.object:getyaw() + ((math.random(0, 360) - 180) / 180 * pi) + end + + self.object:setyaw(yaw) + end + + set_velocity(self, 0) + set_animation(self, "stand") + + -- npc's ordered to stand stay standing + if self.type ~= "npc" + or self.order ~= "stand" then + + if self.walk_chance ~= 0 + and math.random(1, 100) <= self.walk_chance + and is_at_cliff(self) == false then + + set_velocity(self, self.walk_velocity) + self.state = "walk" + set_animation(self, "walk") + end + end + + elseif self.state == "walk" then + + local s = self.object:getpos() + local lp = minetest.find_node_near(s, 1, {"group:water"}) + + -- water swimmers cannot move out of water + if self.fly + and self.fly_in == "default:water_source" + and not lp then + + --print ("out of water") + + set_velocity(self, 0) + + -- change to undefined state so nothing more happens + self.state = "flop" + set_animation(self, "stand") + + return + end + + -- if water nearby then turn away + if lp then + + local vec = { + x = lp.x - s.x, + y = lp.y - s.y, + z = lp.z - s.z + } + + if vec.x ~= 0 + or vec.z ~= 0 then + + yaw = math.atan(vec.z / vec.x) + 3 * pi / 2 - self.rotate + + if lp.x > s.x then + yaw = yaw + pi + end + + self.object:setyaw(yaw) + end + + -- otherwise randomly turn + elseif math.random(1, 100) <= 30 then + + yaw = self.object:getyaw() + ((math.random(0, 360) - 180) / 180 * pi) + + self.object:setyaw(yaw) + end + + -- stand for great fall in front + local temp_is_cliff = is_at_cliff(self) + + -- jump when walking comes to a halt + if temp_is_cliff == false + and self.jump + and get_velocity(self) <= 0.5 + and self.object:getvelocity().y == 0 then + + do_jump(self) + end + + if temp_is_cliff + or math.random(1, 100) <= 30 then + + set_velocity(self, 0) + self.state = "stand" + set_animation(self, "stand") + else + set_velocity(self, self.walk_velocity) + set_animation(self, "walk") + end + + -- runaway when punched + elseif self.state == "runaway" then + + self.runaway_timer = self.runaway_timer + 1 + + -- stop after 3 seconds or when at cliff + if self.runaway_timer > 3 + or is_at_cliff(self) then + self.runaway_timer = 0 + set_velocity(self, 0) + self.state = "stand" + set_animation(self, "stand") + else + set_velocity(self, self.run_velocity) + set_animation(self, "walk") + end + + -- jump when walking comes to a halt + if self.jump + and get_velocity(self) <= 0.5 + and self.object:getvelocity().y == 0 then + + do_jump(self) + end + + -- attack routines (explode, dogfight, shoot, dogshoot) + elseif self.state == "attack" then + + -- calculate distance from mob and enemy + local s = self.object:getpos() + local p = self.attack:getpos() or s + local dist = get_distance(p, s) + + -- stop attacking if player or out of range + if dist > self.view_range + or not self.attack + or not self.attack:getpos() + or self.attack:get_hp() <= 0 then + + --print(" ** stop attacking **", dist, self.view_range) + self.state = "stand" + set_velocity(self, 0) + set_animation(self, "stand") + self.attack = nil + self.v_start = false + self.timer = 0 + self.blinktimer = 0 + + return + end + + if self.attack_type == "explode" then + + local vec = { + x = p.x - s.x, + y = p.y - s.y, + z = p.z - s.z + } + + if vec.x ~= 0 + or vec.z ~= 0 then + + yaw = math.atan(vec.z / vec.x) + pi / 2 - self.rotate + + if p.x > s.x then + yaw = yaw + pi + end + + self.object:setyaw(yaw) + end + + if dist > self.reach then + + if not self.v_start then + + self.v_start = true + set_velocity(self, self.run_velocity) + self.timer = 0 + self.blinktimer = 0 + else + self.timer = 0 + self.blinktimer = 0 + + if get_velocity(self) <= 0.5 + and self.object:getvelocity().y == 0 then + + local v = self.object:getvelocity() + v.y = 5 + self.object:setvelocity(v) + end + + set_velocity(self, self.run_velocity) + end + + set_animation(self, "run") + else + set_velocity(self, 0) + self.timer = self.timer + dtime + self.blinktimer = (self.blinktimer or 0) + dtime + + if self.blinktimer > 0.2 then + + self.blinktimer = 0 + + if self.blinkstatus then + self.object:settexturemod("") + else + self.object:settexturemod("^[brighten") + end + + self.blinkstatus = not self.blinkstatus + end + + if self.timer > 3 then + + local pos = self.object:getpos() + + -- hurt player/mobs caught in blast area + entity_physics(pos, 3) + + -- dont damage anything if area protected or next to water + if minetest.find_node_near(pos, 1, {"group:water"}) + or minetest.is_protected(pos, "") then + + if self.sounds.explode then + + minetest.sound_play(self.sounds.explode, { + pos = pos, + gain = 1.0, + max_hear_distance = 16 + }) + end + + self.object:remove() + + effect(pos, 15, "tnt_smoke.png", 5) + + return + end + + pos.y = pos.y - 1 + + mobs:explosion(pos, 2, 0, 1, self.sounds.explode) + + self.object:remove() + + return + end + end + + elseif self.attack_type == "dogfight" + or (self.attack_type == "dogshoot" and dist <= self.reach) then + + if self.fly + and dist > self.reach then + + local nod = node_ok(s) + local p1 = s + local me_y = math.floor(p1.y) + local p2 = p + local p_y = math.floor(p2.y + 1) + local v = self.object:getvelocity() + + if nod.name == self.fly_in then + + if me_y < p_y then + + self.object:setvelocity({ + x = v.x, + y = 1 * self.walk_velocity, + z = v.z + }) + + elseif me_y > p_y then + + self.object:setvelocity({ + x = v.x, + y = -1 * self.walk_velocity, + z = v.z + }) + end + else + if me_y < p_y then + + self.object:setvelocity({ + x = v.x, + y = 0.01, + z = v.z + }) + + elseif me_y > p_y then + + self.object:setvelocity({ + x = v.x, + y = -0.01, + z = v.z + }) + end + end + + end + + local vec = { + x = p.x - s.x, + y = p.y - s.y, + z = p.z - s.z + } + + if vec.x ~= 0 + or vec.z ~= 0 then + + yaw = (math.atan(vec.z / vec.x) + pi / 2) - self.rotate + + if p.x > s.x then + yaw = yaw + pi + end + + self.object:setyaw(yaw) + end + + -- move towards enemy if beyond mob reach + if dist > self.reach then + + -- jump attack + if (self.jump + and get_velocity(self) <= 0.5 + and self.object:getvelocity().y == 0) + or (self.object:getvelocity().y == 0 + and self.jump_chance > 0) then + + do_jump(self) + end + + if is_at_cliff(self) then + + set_velocity(self, 0) + set_animation(self, "stand") + else + set_velocity(self, self.run_velocity) + set_animation(self, "run") + end + + else + + set_velocity(self, 0) + set_animation(self, "punch") + + if self.timer > 1 then + + self.timer = 0 + + local p2 = p + local s2 = s + + p2.y = p2.y + 1.5 + s2.y = s2.y + 1.5 + + if minetest.line_of_sight(p2, s2) == true then + + -- play attack sound + if self.sounds.attack then + + minetest.sound_play(self.sounds.attack, { + object = self.object, + max_hear_distance = self.sounds.distance + }) + end + + -- punch player + self.attack:punch(self.object, 1.0, { + full_punch_interval = 1.0, + damage_groups = {fleshy = self.damage} + }, nil) + end + end + end + + elseif self.attack_type == "shoot" + or (self.attack_type == "dogshoot" and dist > self.reach) then + + p.y = p.y - .5 + s.y = s.y + .5 + + local dist = get_distance(p, s) + local vec = { + x = p.x - s.x, + y = p.y - s.y, + z = p.z - s.z + } + + if vec.x ~= 0 + or vec.z ~= 0 then + + yaw = (math.atan(vec.z / vec.x) + pi / 2) - self.rotate + + if p.x > s.x then + yaw = yaw + pi + end + + self.object:setyaw(yaw) + end + + set_velocity(self, 0) + + if self.shoot_interval + and self.timer > self.shoot_interval + and math.random(1, 100) <= 60 then + + self.timer = 0 + set_animation(self, "punch") + + -- play shoot attack sound + if self.sounds.shoot_attack then + + minetest.sound_play(self.sounds.shoot_attack, { + object = self.object, + max_hear_distance = self.sounds.distance + }) + end + + local p = self.object:getpos() + + p.y = p.y + (self.collisionbox[2] + self.collisionbox[5]) / 2 + + local obj = minetest.add_entity(p, self.arrow) + local ent = obj:get_luaentity() + + local amount = (vec.x ^ 2 + vec.y ^ 2 + vec.z ^ 2) ^ 0.5 + local v = ent.velocity + ent.switch = 1 + + -- offset makes shoot aim accurate + vec.y = vec.y + self.shoot_offset + vec.x = vec.x * v / amount + vec.y = vec.y * v / amount + vec.z = vec.z * v / amount + + obj:setvelocity(vec) + end + end + + end -- END if self.state == "attack" + end, + + on_punch = function(self, hitter, tflp, tool_capabilities, dir) + + -- weapon wear + local weapon = hitter:get_wielded_item() + local punch_interval = 1.4 + + if tool_capabilities then + punch_interval = tool_capabilities.full_punch_interval or 1.4 + end + + if weapon:get_definition() + and weapon:get_definition().tool_capabilities then + + weapon:add_wear(math.floor((punch_interval / 75) * 9000)) + hitter:set_wielded_item(weapon) + end + + -- weapon sounds + if weapon:get_definition().sounds ~= nil then + + local s = math.random(0, #weapon:get_definition().sounds) + + minetest.sound_play(weapon:get_definition().sounds[s], { + object = hitter, + max_hear_distance = 8 + }) + else + minetest.sound_play("default_punch", { + object = hitter, + max_hear_distance = 5 + }) + end + + -- exit here if dead + if check_for_death(self) then + return + end + + -- blood_particles + if self.blood_amount > 0 + and not disable_blood then + + local pos = self.object:getpos() + + pos.y = pos.y + (-self.collisionbox[2] + self.collisionbox[5]) / 2 + + effect(pos, self.blood_amount, self.blood_texture) + end + + -- knock back effect + if self.knock_back > 0 then + + local v = self.object:getvelocity() + local r = 1.4 - math.min(punch_interval, 1.4) + local kb = r * 5 + + self.object:setvelocity({ + x = (dir.x or 0) * kb, + y = 2, + z = (dir.z or 0) * kb + }) + + self.pause_timer = r + end + + -- if skittish then run away + if self.runaway == true then + + local lp = hitter:getpos() + local s = self.object:getpos() + + local vec = { + x = lp.x - s.x, + y = lp.y - s.y, + z = lp.z - s.z + } + + if vec.x ~= 0 + or vec.z ~= 0 then + + local yaw = math.atan(vec.z / vec.x) + 3 * pi / 2 - self.rotate + + if lp.x > s.x then + yaw = yaw + pi + end + + self.object:setyaw(yaw) + end + + self.state = "runaway" + self.runaway_timer = 0 + self.following = nil + end + + + -- attack puncher and call other mobs for help + if self.passive == false + and self.child == false + and hitter:get_player_name() ~= self.owner then + + if self.state ~= "attack" then + do_attack(self, hitter) + end + + -- alert others to the attack + local obj = nil + + for _, oir in pairs(minetest.get_objects_inside_radius(hitter:getpos(), 5)) do + + obj = oir:get_luaentity() + + if obj then + + if obj.group_attack == true + and obj.state ~= "attack" then + do_attack(obj, hitter) + end + end + end + end + end, + + on_activate = function(self, staticdata, dtime_s) + + -- remove monsters if playing on peaceful + if self.type == "monster" + and peaceful_only then + + self.object:remove() + + return + end + + -- load entity variables + if staticdata then + + local tmp = minetest.deserialize(staticdata) + + if tmp then + + for _,stat in pairs(tmp) do + self[_] = stat + end + end + else + self.object:remove() + + return + end + + -- select random texture, set model and size + if not self.base_texture then + + self.base_texture = def.textures[math.random(1, #def.textures)] + self.base_mesh = def.mesh + self.base_size = self.visual_size + self.base_colbox = self.collisionbox + end + + -- set texture, model and size + local textures = self.base_texture + local mesh = self.base_mesh + local vis_size = self.base_size + local colbox = self.base_colbox + + -- specific texture if gotten + if self.gotten == true + and def.gotten_texture then + textures = def.gotten_texture + end + + -- specific mesh if gotten + if self.gotten == true + and def.gotten_mesh then + mesh = def.gotten_mesh + end + + -- set child objects to half size + if self.child == true then + + vis_size = { + x = self.base_size.x / 2, + y = self.base_size.y / 2 + } + + if def.child_texture then + textures = def.child_texture[1] + end + + colbox = { + self.base_colbox[1] / 2, + self.base_colbox[2] / 2, + self.base_colbox[3] / 2, + self.base_colbox[4] / 2, + self.base_colbox[5] / 2, + self.base_colbox[6] / 2 + } + end + + if self.health == 0 then + self.health = math.random (self.hp_min, self.hp_max) + end + + self.object:set_hp(self.health) + self.object:set_armor_groups({fleshy = self.armor}) + self.old_y = self.object:getpos().y + self.object:setyaw(math.random(1, 360) / 180 * pi) + self.sounds.distance = (self.sounds.distance or 10) + self.textures = textures + self.mesh = mesh + self.collisionbox = colbox + self.visual_size = vis_size + + -- set anything changed above + self.object:set_properties(self) + update_tag(self) + end, + + get_staticdata = function(self) + + -- remove mob when out of range unless tamed + if mobs.remove + and self.remove_ok + and not self.tamed then + + --print ("REMOVED", self.remove_ok, self.name) + + self.object:remove() + + return nil + end + + self.remove_ok = true + self.attack = nil + self.following = nil + self.state = "stand" + + -- used to rotate older mobs + if self.drawtype + and self.drawtype == "side" then + self.rotate = math.rad(90) + end + + local tmp = {} + + for _,stat in pairs(self) do + + local t = type(stat) + + if t ~= 'function' + and t ~= 'nil' + and t ~= 'userdata' then + tmp[_] = self[_] + end + end + + -- print('===== '..self.name..'\n'.. dump(tmp)..'\n=====\n') + return minetest.serialize(tmp) + end, + +}) + +end -- END mobs:register_mob function + +-- global functions + +mobs.spawning_mobs = {} + +function mobs:spawn_specific(name, nodes, neighbors, min_light, max_light, + interval, chance, active_object_count, min_height, max_height) + + mobs.spawning_mobs[name] = true + + -- chance override in minetest.conf for registered mob + local new_chance = tonumber(minetest.setting_get(name .. "_chance")) + + if new_chance ~= nil then + + if chance == 0 then + print("[Mobs Redo] " .. name .. " has spawning disabled") + return + end + + chance = new_chance + + print ("[Mobs Redo] Chance setting for " .. name .. " is now " .. chance) + + end + + minetest.register_abm({ + + nodenames = nodes, + neighbors = neighbors, + interval = interval, + chance = chance, + + action = function(pos, node, _, active_object_count_wider) + + -- do not spawn if too many active entities in area + if active_object_count_wider > active_object_count + or not mobs.spawning_mobs[name] then + return + end + + -- spawn above node + pos.y = pos.y + 1 + + -- mobs cannot spawn in protected areas when enabled + if mobs.protected == 1 + and minetest.is_protected(pos, "") then + return + end + + -- check if light and height levels are ok to spawn + local light = minetest.get_node_light(pos) + if not light + or light > max_light + or light < min_light + or pos.y > max_height + or pos.y < min_height then + return + end + + -- are we spawning inside solid nodes? + if minetest.registered_nodes[node_ok(pos).name].walkable == true then + return + end + + pos.y = pos.y + 1 + + if minetest.registered_nodes[node_ok(pos).name].walkable == true then + return + end + + -- spawn mob half block higher than ground + pos.y = pos.y - 0.5 + + local mob = minetest.add_entity(pos, name) + + if mob and mob:get_luaentity() then +-- print ("[mobs] Spawned " .. name .. " at " +-- .. minetest.pos_to_string(pos) .. " on " +-- .. node.name .. " near " .. neighbors[1]) + else + print ("[mobs]" .. name .. " failed to spawn at " + .. minetest.pos_to_string(pos)) + end + + end + }) +end + +-- compatibility with older mob registration +function mobs:register_spawn(name, nodes, max_light, min_light, chance, active_object_count, max_height) + + mobs:spawn_specific(name, nodes, {"air"}, min_light, max_light, 30, + chance, active_object_count, -31000, max_height) +end + +-- set content id's +local c_air = minetest.get_content_id("air") +local c_ignore = minetest.get_content_id("ignore") +local c_obsidian = minetest.get_content_id("default:obsidian") +local c_brick = minetest.get_content_id("default:obsidianbrick") +local c_chest = minetest.get_content_id("default:chest_locked") + +-- explosion (cannot break protected or unbreakable nodes) +function mobs:explosion(pos, radius, fire, smoke, sound) + + radius = radius or 0 + fire = fire or 0 + smoke = smoke or 0 + + -- if area protected or near map limits then no blast damage + if minetest.is_protected(pos, "") + or not within_limits(pos, radius) then + return + end + + -- explosion sound + if sound + and sound ~= "" then + + minetest.sound_play(sound, { + pos = pos, + gain = 1.0, + max_hear_distance = 16 + }) + end + + pos = vector.round(pos) -- voxelmanip doesn't work properly unless pos is rounded ?!?! + + local vm = VoxelManip() + local minp, maxp = vm:read_from_map(vector.subtract(pos, radius), vector.add(pos, radius)) + local a = VoxelArea:new({MinEdge = minp, MaxEdge = maxp}) + local data = vm:get_data() + local p = {} + + for z = -radius, radius do + for y = -radius, radius do + local vi = a:index(pos.x + (-radius), pos.y + y, pos.z + z) + for x = -radius, radius do + + p.x = pos.x + x + p.y = pos.y + y + p.z = pos.z + z + + if data[vi] ~= c_air + and data[vi] ~= c_ignore + and data[vi] ~= c_obsidian + and data[vi] ~= c_brick + and data[vi] ~= c_chest then + + local n = node_ok(p).name + + if minetest.get_item_group(n, "unbreakable") ~= 1 then + + -- if chest then drop items inside + if n == "default:chest" + or n == "3dchest:chest" + or n == "bones:bones" then + + local meta = minetest.get_meta(p) + local inv = meta:get_inventory() + + for i = 1, inv:get_size("main") do + + local m_stack = inv:get_stack("main", i) + local obj = minetest.add_item(p, m_stack) + + if obj then + + obj:setvelocity({ + x = math.random(-2, 2), + y = 7, + z = math.random(-2, 2) + }) + end + end + end + + -- after effects + if fire > 0 + and (minetest.registered_nodes[n].groups.flammable + or math.random(1, 100) <= 30) then + + minetest.set_node(p, {name = "fire:basic_flame"}) + else + minetest.set_node(p, {name = "air"}) + + if smoke > 0 then + effect(p, 2, "tnt_smoke.png", 5) + end + end + end + end + + vi = vi + 1 + + end + end + end +end + +-- register arrow for shoot attack +function mobs:register_arrow(name, def) + + if not name or not def then return end -- errorcheck + + minetest.register_entity(name, { + + physical = false, + visual = def.visual, + visual_size = def.visual_size, + textures = def.textures, + velocity = def.velocity, + hit_player = def.hit_player, + hit_node = def.hit_node, + hit_mob = def.hit_mob, + drop = def.drop or false, + collisionbox = {0, 0, 0, 0, 0, 0}, -- remove box around arrows + timer = 0, + switch = 0, + + on_step = function(self, dtime) + + self.timer = self.timer + 1 + + local pos = self.object:getpos() + + if self.switch == 0 + or self.timer > 150 + or not within_limits(pos, 0) then + + self.object:remove() ; -- print ("removed arrow") + + return + end + + if self.hit_node then + + local node = node_ok(pos).name + + if minetest.registered_nodes[node].walkable then + + self.hit_node(self, pos, node) + + if self.drop == true then + + pos.y = pos.y + 1 + + self.lastpos = (self.lastpos or pos) + + minetest.add_item(self.lastpos, self.object:get_luaentity().name) + end + + self.object:remove() ; -- print ("hit node") + + return + end + end + + if (self.hit_player or self.hit_mob) + -- clear mob entity before arrow becomes active + and self.timer > (10 - (self.velocity / 2)) then + + for _,player in pairs(minetest.get_objects_inside_radius(pos, 1.0)) do + + if self.hit_player + and player:is_player() then + + self.hit_player(self, player) + self.object:remove() ; -- print ("hit player") + return + end + + if self.hit_mob + and player:get_luaentity() + and player:get_luaentity().name ~= self.object:get_luaentity().name + and player:get_luaentity().name ~= "__builtin:item" + and player:get_luaentity().name ~= "gauges:hp_bar" + and player:get_luaentity().name ~= "signs:text" then + + self.hit_mob(self, player) + + self.object:remove() ; -- print ("hit mob") + + return + end + end + end + + self.lastpos = pos + end + }) +end + +-- Spawn Egg +function mobs:register_egg(mob, desc, background, addegg) + + local invimg = background + + if addegg == 1 then + invimg = invimg .. "^mobs_chicken_egg.png" + end + + minetest.register_craftitem(mob, { + + description = desc, + inventory_image = invimg, + + on_place = function(itemstack, placer, pointed_thing) + + local pos = pointed_thing.above + + if pos + and within_limits(pos, 0) + and not minetest.is_protected(pos, placer:get_player_name()) then + + pos.y = pos.y + 1 + + local mob = minetest.add_entity(pos, mob) + local ent = mob:get_luaentity() + + if ent.type ~= "monster" then + -- set owner and tame if not monster + ent.owner = placer:get_player_name() + ent.tamed = true + end + + -- if not in creative then take item + if not creative then + itemstack:take_item() + end + end + + return itemstack + end, + }) +end + +-- capture critter (thanks to blert2112 for idea) +function mobs:capture_mob(self, clicker, chance_hand, chance_net, chance_lasso, force_take, replacewith) + + if not self.child + and clicker:is_player() + and clicker:get_inventory() then + + -- get name of clicked mob + local mobname = self.name + + -- if not nil change what will be added to inventory + if replacewith then + mobname = replacewith + end + + local name = clicker:get_player_name() + + -- is mob tamed? + if self.tamed == false + and force_take == false then + + minetest.chat_send_player(name, "Not tamed!") + + return + end + + -- cannot pick up if not owner + if self.owner ~= name + and force_take == false then + + minetest.chat_send_player(name, self.owner.." is owner!") + + return + end + + if clicker:get_inventory():room_for_item("main", mobname) then + + -- was mob clicked with hand, net, or lasso? + local tool = clicker:get_wielded_item() + local chance = 0 + + if tool:is_empty() then + chance = chance_hand + + elseif tool:get_name() == "mobs:net" then + + chance = chance_net + + tool:add_wear(4000) -- 17 uses + + clicker:set_wielded_item(tool) + + elseif tool:get_name() == "mobs:magic_lasso" then + + chance = chance_lasso + + tool:add_wear(650) -- 100 uses + + clicker:set_wielded_item(tool) + end + + -- return if no chance + if chance == 0 then return end + + -- calculate chance.. add to inventory if successful? + if math.random(1, 100) <= chance then + + clicker:get_inventory():add_item("main", mobname) + + self.object:remove() + else + minetest.chat_send_player(name, "Missed!") + end + end + end +end + +local mob_obj = {} +local mob_sta = {} + +-- feeding, taming and breeding (thanks blert2112) +function mobs:feed_tame(self, clicker, feed_count, breed, tame) + + if not self.follow then + return false + end + + -- can eat/tame with item in hand + if follow_holding(self, clicker) then + + -- if not in creative then take item + if not creative then + + local item = clicker:get_wielded_item() + + item:take_item() + + clicker:set_wielded_item(item) + end + + -- increase health + self.health = self.health + 4 + + if self.health >= self.hp_max then + + self.health = self.hp_max + + if self.htimer < 1 then + + minetest.chat_send_player(clicker:get_player_name(), + self.name:split(":")[2] + .. " at full health (" .. tostring(self.health) .. ")") + + self.htimer = 5 + end + end + + self.object:set_hp(self.health) + + update_tag(self) + + -- make children grow quicker + if self.child == true then + + self.hornytimer = self.hornytimer + 20 + + return true + end + + -- feed and tame + self.food = (self.food or 0) + 1 + if self.food >= feed_count then + + self.food = 0 + + if breed and self.hornytimer == 0 then + self.horny = true + end + + self.gotten = false + + if tame then + + if self.tamed == false then + minetest.chat_send_player(clicker:get_player_name(), + self.name:split(":")[2] + .. " has been tamed!") + end + + self.tamed = true + + if not self.owner or self.owner == "" then + self.owner = clicker:get_player_name() + end + end + + -- make sound when fed so many times + if self.sounds.random then + + minetest.sound_play(self.sounds.random, { + object = self.object, + max_hear_distance = self.sounds.distance + }) + end + end + + return true + end + + local item = clicker:get_wielded_item() + + -- if mob has been tamed you can name it with a nametag + if item:get_name() == "mobs:nametag" + and clicker:get_player_name() == self.owner then + + local name = clicker:get_player_name() + + -- store mob and nametag stack in external variables + mob_obj[name] = self + mob_sta[name] = item + + local tag = self.nametag or "" + + local formspec = "size[8,4]" + .. default.gui_bg + .. default.gui_bg_img + .. "field[0.5,1;7.5,0;name;Enter name and press button:;" .. tag .. "]" + .. "button_exit[2.5,3.5;3,1;mob_rename;Rename]" + minetest.show_formspec(name, "mobs_nametag", formspec) + end + + return false + +end + +-- inspired by blockmen's nametag mod +minetest.register_on_player_receive_fields(function(player, formname, fields) + + -- right-clicked with nametag, name entered and button pressed? + if formname == "mobs_nametag" + and fields.mob_rename + and fields.name ~= "" then + + local name = player:get_player_name() + local ent = mob_obj[name] + + if not ent + or not ent.object then + return + end + + -- update nametag + ent.nametag = fields.name + update_tag(ent) + + -- if not in creative then take item + if not creative then + + local itemstack = mob_sta[name] + + itemstack:take_item() + player:set_wielded_item(itemstack) + end + + -- reset external variables + mob_obj[name] = nil + mob_sta[name] = nil + + end +end) diff --git a/behavior.lua b/behavior.lua new file mode 100644 index 0000000..56b54a7 --- /dev/null +++ b/behavior.lua @@ -0,0 +1,372 @@ + + +bt = { + reset={}, + tick={}, +} + + + + + +function tprint (tbl, indent) + local formatting = "" + if not indent then indent = 0 end + if tbl == nil then + print(formatting .. "nil") + return + end + for k, v in pairs(tbl) do + formatting = string.rep(" ", indent) .. k .. ": " + if type(v) == "table" then + print(formatting) + tprint(v, indent+1) + elseif type(v) == 'boolean' then + print(formatting .. tostring(v)) + elseif type(v) == 'function' then + print(formatting .. "[function]") + elseif type(v) == 'userdata' then + print(formatting .. "[userdata]") + else + print(formatting .. v) + end + end +end + + +function mkSelector(name, list) + + return { + name=name, + kind="selector", + current_kid=-1, + kids=list, + + } +end + + +function mkSequence(name, list) + + return { + name=name, + kind="sequence", + current_kid=-1, + kids=list, + + } + + +end + +function mkRepeat(name, what) + + return { + name=name, + kind="repeat", + kids = what, + + } + + +end + + +bt.reset.selector = function(node, data) + node.current_kid = -1 +end +bt.reset.sequence = function(node, data) + node.current_kid = -1 +end + +bt.reset["repeat"] = function(node, data) + +end + +bt.tick["repeat"] = function(node, data) + --tprint(node) + local ret = bt.tick[node.kids[1].kind](node.kids[1], data) + if ret ~= "running" then + print("repeat resetting") + + bt.reset[node.kids[1].kind](node.kids[1], data) + end + + print("repeat ending\n&&&&&") + + return "success" +end + + +-- nodes never call :reset() on themselves + + +bt.tick.selector = function(node, data) + + local ret + + if node.current_kid == -1 then + node.current_kid = 1 + ret = "failed" -- trick reset into being run + end + + while node.current_kid <= table.getn(node.kids) do + + local cn = node.kids[node.current_kid] + + -- reset fresh nodes + if ret == "failed" then + bt.reset[cn.kind](cn, data) + end + + -- tick the current node + ret = bt.tick[cn.kind](cn, data) + print(" selector got status ["..ret.."] from kid "..node.current_kid) + if ret == "running" or ret == "success" then + return ret + end + + node.current_kid = node.current_kid + 1 + end + + + return "failed" +end + + + + +bt.tick.sequence = function(node, data) + + local ret + + if node.current_kid == -1 then + node.current_kid = 1 + ret = "failed" -- trick reset into being run + end + + while node.current_kid <= table.getn(node.kids) do + + local cn = node.kids[node.current_kid] + + -- reset fresh nodes + if ret == "failed" then + bt.reset[cn.kind](cn, data) + end + + -- tick the current node + ret = bt.tick[cn.kind](cn, data) + print(" selector got status ["..ret.."] from kid "..node.current_kid) + if ret == "running" or ret == "failed" then + return ret + end + + node.current_kid = node.current_kid + 1 + end + + return "success" +end + + + +-- distance on x-z plane +function distance(a,b) + local x = a.x - b.x + local z = a.z - b.z + + return math.abs(math.sqrt(x*x + z*z)) +end + + +bt.reset.find_node_near = function(node, data) + + local targetpos = minetest.find_node_near(data.pos, node.dist, node.sel) + data.targetPos = targetpos +end + +bt.tick.find_node_near = function(node, data) + if data.targetPos == nil then + print("could not find node near") + return "failed" + end + + return "success" +end + +function mkFindNodeNear(sel, dist) + + return { + name="find node near", + kind="find_node_near", + dist = dist, + sel = sel, + } + +end + + + +bt.reset.approach = function(node, data) + + if data.targetPos ~= nil then + print("Approaching target ("..data.targetPos.x..","..data.targetPos.y..","..data.targetPos.z..")") + data.mob.destination = data.targetPos + else + print("Approach: targetPos is nil") + end +end + +bt.tick.approach = function(node, data) + if data.targetPos == nil then + return "failed" + end + + local d = distance(data.pos, data.targetPos) + + print("dist: "..d) + + if d <= node.dist then + print("arrived at target") + return "success" + end + + return "running" +end + + +function mkApproach(dist) + + return { + name="go to", + kind="approach", + dist=dist, + } + +end + +function mkTryApproach(dist) + + return { + name="try to go to", + kind="try_approach", + dist=dist, + } + +end + +bt.reset.try_approach = function(node, data) + + node.last_d = nil + + if data.targetPos ~= nil then + print("Approaching target ("..data.targetPos.x..","..data.targetPos.y..","..data.targetPos.z..")") + data.mob.destination = data.targetPos + else + print("Approach: targetPos is nil") + end +end + +bt.tick.try_approach = function(node, data) + if data.targetPos == nil then + return "failed" + end + + local d = distance(data.pos, data.targetPos) + + if d <= node.dist then + print("arrived at target") + node.last_d = nil + return "success" + end + + + if node.last_d == nil then + node.last_d = d + else + local dd = math.abs(node.last_d - d) + print("dist: ".. dd) + if dd < .02 then + -- we're stuck + node.last_d = nil + return "failed" + end + end + + print("dist: ".. math.abs(node.last_d - d)) + + + + return "running" +end + + +bt.reset.destroy = function(node, data) + +end + +bt.tick.destroy = function(node, data) + print("Destroying target") + if data.targetPos == nil then + return "failed" + end + + minetest.set_node(data.targetPos, {name="air"}) + + return "success" +end + + +function mkDestroy() + + return { + name="destroy", + kind="destroy", + + } + +end + + + + + +bt.reset.bash_walls = function(node, data) + +end + +bt.tick.bash_walls = function(node, data) + + local pos = minetest.find_node_near(data.pos, 2, {"default:wood"}) + if pos == nil then + return "failed" + end + + minetest.set_node(pos, {name="air"}) + + return "success" +end + + +function mkBashWalls() + + return { + name="destroy", + kind="bash_walls", + + } + +end + + + + + + + + + + + + + + diff --git a/depends.txt b/depends.txt new file mode 100644 index 0000000..4ad96d5 --- /dev/null +++ b/depends.txt @@ -0,0 +1 @@ +default diff --git a/giant.lua b/giant.lua new file mode 100644 index 0000000..fd5231e --- /dev/null +++ b/giant.lua @@ -0,0 +1,66 @@ + + + + +mobs:register_simple_mob("giants:giant", { + type = "monster", + passive = false, + attack_type = "dogfight", + reach = 2, + damage = 1, + hp_min = 4, + hp_max = 20, + armor = 100, + collisionbox = {-0.35,-1.0,-0.35, 0.35,0.8,0.35}, + visual = "mesh", + mesh = "character.b3d", + drawtype = "front", + textures = { + {"mobs_npc.png"}, + }, + makes_footstep_sound = true, + walk_velocity = 1.5, + run_velocity = 4, + view_range = 15, + jump = true, + floats = 0, + drops = { + {name = "default:iron_lump", + chance = 1, min = 3, max = 5}, + }, + water_damage = 0, + lava_damage = 4, + light_damage = 0, + fear_height = 3, + animation = { + speed_normal = 30, + speed_run = 30, + stand_start = 0, + stand_end = 79, + walk_start = 168, + walk_end = 187, + run_start = 168, + run_end = 187, + punch_start = 200, + punch_end = 219, + }, + + pre_activate = function(self, s,d) + self.bt = mkRepeat("root", { + mkSequence("snuff torches", { + mkFindNodeNear({"default:torch"}, 20), + mkSelector("seek", { + mkTryApproach(.8), + mkBashWalls(), + mkTryApproach(.8), + }), + mkDestroy(), + + }) + }) + end +}) + +mobs:register_spawn("giants:giant", {"default:desert_sand"}, 20, 0, 7000, 2, 31000) + +mobs:register_egg("giants:giant", "Giant", "default_desert_sand.png", 1) \ No newline at end of file diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..a7ab2b6 --- /dev/null +++ b/init.lua @@ -0,0 +1,20 @@ +local path = minetest.get_modpath("giants") + +-- Mob Api + +dofile(path.."/api.lua") +dofile(path.."/behavior.lua") +dofile(path.."/simple_api.lua") + + + +dofile(path.."/giant.lua") + + +-- Mob Items +--dofile(path.."/crafts.lua") + +-- Spawner +--dofile(path.."/spawner.lua") + +print ("[MOD] Giants loaded") \ No newline at end of file diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..7a0960e --- /dev/null +++ b/license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Krupnov Pavel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/models/character.b3d b/models/character.b3d new file mode 100644 index 0000000000000000000000000000000000000000..5ea45e0a79cbd972026ae23534bf584e1716c2eb GIT binary patch literal 86880 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 1 then + + print("<<< start >>>") + bt.tick[self.bt.kind](self.bt, btdata) + print("<<< end >>>") + + self.bt_timer = 0 + end + + btdata.lastpos = pos + + + if not self.fly then + + -- floating in water (or falling) + local v = self.object:getvelocity() + + -- going up then apply gravity + if v.y > 0.1 then + + self.object:setacceleration({ + x = 0, + y = self.fall_speed, + z = 0 + }) + end + + -- in water then float up + if minetest.registered_nodes[node_ok(pos).name].groups.water then + + if self.floats == 1 then + + self.object:setacceleration({ + x = 0, + y = -self.fall_speed / (math.max(1, v.y) ^ 2), + z = 0 + }) + end + else + -- fall downwards + self.object:setacceleration({ + x = 0, + y = self.fall_speed, + z = 0 + }) + + -- fall damage + if self.fall_damage == 1 + and self.object:getvelocity().y == 0 then + + local d = self.old_y - self.object:getpos().y + + if d > 5 then + + self.object:set_hp(self.object:get_hp() - math.floor(d - 5)) + + effect(pos, 5, "tnt_smoke.png") + + if check_for_death(self) then + return + end + end + + self.old_y = self.object:getpos().y + end + end + end + +-- -- knockback timer +-- if self.pause_timer > 0 then +-- +-- self.pause_timer = self.pause_timer - dtime +-- +-- if self.pause_timer < 1 then +-- self.pause_timer = 0 +-- end +-- +-- return +-- end + + -- attack timer +-- self.timer = self.timer + dtime + +-- if self.state ~= "attack" then +-- +-- if self.timer < 1 then +-- return +-- end +-- +-- self.timer = 0 +-- end + + -- never go over 100 + if self.timer > 100 then + self.timer = 1 + end + + -- node replace check (cow eats grass etc.) + replace(self, pos) + + -- mob plays random sound at times + if self.sounds.random + and math.random(1, 100) == 1 then + + minetest.sound_play(self.sounds.random, { + object = self.object, + max_hear_distance = self.sounds.distance + }) + end + + -- environmental damage timer (every 1 second) + self.env_damage_timer = self.env_damage_timer + dtime + + if (self.state == "attack" and self.env_damage_timer > 1) + or self.state ~= "attack" then + + self.env_damage_timer = 0 + + do_env_damage(self) + + -- custom function (defined in mob lua file) + if self.do_custom then + self.do_custom(self) + end + end + + + if self.destination ~= nil then + + --print("destination ") + + local dist = distance(pos, self.destination) + -- print("walk dist ".. dist) + local s = self.destination + local vec = { + x = pos.x - s.x, + y = pos.y - s.y, + z = pos.z - s.z + } + + -- tprint(vec) + + if vec.x ~= 0 + or vec.z ~= 0 then + + yaw = (math.atan(vec.z / vec.x) + math.pi / 2) - self.rotate + + if s.x > pos.x then + yaw = yaw + math.pi + end + + -- print("yaw " .. yaw) + + self.object:setyaw(yaw) + end + + -- anyone but standing npc's can move along + if dist > .1 then + + if (self.jump + and get_velocity(self) <= 0.5 + and self.object:getvelocity().y == 0) + or (self.object:getvelocity().y == 0 + and self.jump_chance > 0) then + do_jump(self) + end + + + + set_velocity(self, self.walk_velocity) + set_animation(self, "walk") + else + -- we have arrived + self.destination = nil + + set_velocity(self, 0) + set_animation(self, "stand") + end + + end + + + + end, + + on_punch = function(self, hitter, tflp, tool_capabilities, dir) + + -- weapon wear + local weapon = hitter:get_wielded_item() + local punch_interval = 1.4 + + if tool_capabilities then + punch_interval = tool_capabilities.full_punch_interval or 1.4 + end + + if weapon:get_definition() + and weapon:get_definition().tool_capabilities then + + weapon:add_wear(math.floor((punch_interval / 75) * 9000)) + hitter:set_wielded_item(weapon) + end + + -- weapon sounds + if weapon:get_definition().sounds ~= nil then + + local s = math.random(0, #weapon:get_definition().sounds) + + minetest.sound_play(weapon:get_definition().sounds[s], { + object = hitter, + max_hear_distance = 8 + }) + else + minetest.sound_play("default_punch", { + object = hitter, + max_hear_distance = 5 + }) + end + + -- exit here if dead + if check_for_death(self) then + return + end + + -- blood_particles + if self.blood_amount > 0 + and not disable_blood then + + local pos = self.object:getpos() + + pos.y = pos.y + (-self.collisionbox[2] + self.collisionbox[5]) / 2 + + effect(pos, self.blood_amount, self.blood_texture) + end + + -- knock back effect + if self.knock_back > 0 then + + local v = self.object:getvelocity() + local r = 1.4 - math.min(punch_interval, 1.4) + local kb = r * 5 + + self.object:setvelocity({ + x = (dir.x or 0) * kb, + y = 2, + z = (dir.z or 0) * kb + }) + + self.pause_timer = r + end + + + end, + + on_activate = function(self, staticdata, dtime_s) + + + btdata.lastpos = self.object:getpos() + + if type(def.pre_activate) == "function" then + def.pre_activate(self, static_data, dtime_s) + end + + -- load entity variables + if staticdata then + + local tmp = minetest.deserialize(staticdata) + + if tmp then + + for _,stat in pairs(tmp) do + self[_] = stat + end + end + else + self.object:remove() + + return + end + + -- select random texture, set model and size + if not self.base_texture then + + self.base_texture = def.textures[math.random(1, #def.textures)] + self.base_mesh = def.mesh + self.base_size = self.visual_size + self.base_colbox = self.collisionbox + end + + -- set texture, model and size + local textures = self.base_texture + local mesh = self.base_mesh + local vis_size = self.base_size + local colbox = self.base_colbox + + -- specific texture if gotten + if self.gotten == true + and def.gotten_texture then + textures = def.gotten_texture + end + + -- specific mesh if gotten + if self.gotten == true + and def.gotten_mesh then + mesh = def.gotten_mesh + end + + -- set child objects to half size + if self.child == true then + + vis_size = { + x = self.base_size.x / 2, + y = self.base_size.y / 2 + } + + if def.child_texture then + textures = def.child_texture[1] + end + + colbox = { + self.base_colbox[1] / 2, + self.base_colbox[2] / 2, + self.base_colbox[3] / 2, + self.base_colbox[4] / 2, + self.base_colbox[5] / 2, + self.base_colbox[6] / 2 + } + end + + if self.health == 0 then + self.health = math.random (self.hp_min, self.hp_max) + end + + self.object:set_hp(self.health) + self.object:set_armor_groups({fleshy = self.armor}) + self.old_y = self.object:getpos().y + self.object:setyaw(math.random(1, 360) / 180 * math.pi) + self.sounds.distance = (self.sounds.distance or 10) + self.textures = textures + self.mesh = mesh + self.collisionbox = colbox + self.visual_size = vis_size + + -- set anything changed above + self.object:set_properties(self) + update_tag(self) + + if type(def.post_activate) == "function" then + def.post_activate(self, static_data, dtime_s) + end + end, + + get_staticdata = function(self) + + -- remove mob when out of range unless tamed + if mobs.remove + and self.remove_ok + and not self.tamed then + + --print ("REMOVED", self.remove_ok, self.name) + + self.object:remove() + + return nil + end + + self.remove_ok = true + self.attack = nil + self.following = nil + self.state = "stand" + + -- used to rotate older mobs + if self.drawtype + and self.drawtype == "side" then + self.rotate = math.rad(90) + end + + local tmp = {} + + for _,stat in pairs(self) do + + local t = type(stat) + + if t ~= 'function' + and t ~= 'nil' + and t ~= 'userdata' then + tmp[_] = self[_] + end + end + + -- print('===== '..self.name..'\n'.. dump(tmp)..'\n=====\n') + return minetest.serialize(tmp) + end, + +}) + +end -- END mobs:register_mob function + + + + +-- set content id's +local c_air = minetest.get_content_id("air") +local c_ignore = minetest.get_content_id("ignore") +local c_obsidian = minetest.get_content_id("default:obsidian") +local c_brick = minetest.get_content_id("default:obsidianbrick") +local c_chest = minetest.get_content_id("default:chest_locked") + + diff --git a/sounds/default_punch.ogg b/sounds/default_punch.ogg new file mode 100644 index 0000000000000000000000000000000000000000..28a500bf5759c8bb16ea780d100c8bbf1a7a2609 GIT binary patch literal 5946 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 0 and num <= 10 + and mlig and mlig >= 0 and mlig <= 15 + and xlig and xlig >= 0 and xlig <= 15 then + + meta:set_string("command", fields.text) + meta:set_string("infotext", "Spawner Active (" .. mob .. ")") + + else + minetest.chat_send_player(name, "Mob Spawner settings failed!") + end + end, +}) + +-- spawner abm +minetest.register_abm({ + nodenames = {"mobs:spawner"}, + interval = 10, + chance = 4, + catch_up = false, + + action = function(pos, node, active_object_count, active_object_count_wider) + + -- check objects inside 9x9 area around spawner + local objs = minetest.get_objects_inside_radius(pos, 9) + + -- get meta and command + local meta = minetest.get_meta(pos) + local comm = meta:get_string("command"):split(" ") + + -- get settings from command + local mob = comm[1] + local mlig = tonumber(comm[2]) + local xlig = tonumber(comm[3]) + local num = tonumber(comm[4]) + + -- if amount is 0 then do nothing + if num == 0 then + return + end + + local count = 0 + local ent = nil + + -- count objects of same type in area + for k, obj in pairs(objs) do + + ent = obj:get_luaentity() + + if ent and ent.name == mob then + count = count + 1 + end + end + + -- is there too many of same type? + if count >= num then + return + end + + -- find air blocks within 5 nodes of spawner + local air = minetest.find_nodes_in_area( + {x = pos.x - 5, y = pos.y, z = pos.z - 5}, + {x = pos.x + 5, y = pos.y, z = pos.z + 5}, + {"air"}) + + -- spawn in random air block + if air and #air > 0 then + + local pos2 = air[math.random(#air)] + local lig = minetest.get_node_light(pos2) + + pos2.y = pos2.y + 0.5 + + -- only if light levels are within range + if lig and lig >= mlig and lig <= xlig then + minetest.add_entity(pos2, mob) + end + end + + end +}) diff --git a/textures/mob_spawner.png b/textures/mob_spawner.png new file mode 100644 index 0000000000000000000000000000000000000000..8f0ac39b711f0b8919b945696cc73b4015f8329f GIT binary patch literal 108 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!D3?x-;bCrM;TYyi9E09+5%h?i;vJ=Q+ED7=p zW^j0RBMrzA@pN$v;kcgszopr0BYSC AegFUf literal 0 HcmV?d00001 diff --git a/textures/mobs_blood.png b/textures/mobs_blood.png new file mode 100644 index 0000000000000000000000000000000000000000..77cfbdaa665922472018ffd016e7a47c974e6bf5 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4&HECyrsn$JLrv%n*= zn1O*?7=#%aX3dcR3bL1Y`ns~;V-*l#XF68-IUFb?S>hT|;+&tGo0?a`;9QiNSdyBe zP@Y+mq2TW68xY>eCk|9p?CIhdVsU!wWN*F(0}fVpzOeIp;)i?nt~kXd=NwvPBT@I^ z`)$SD_5YRF3_|ya?$bSJv37BxYPI+g?T<4qoM6#^erD3SrIE87pG`RweterrYcywC zi% literal 0 HcmV?d00001 diff --git a/textures/mobs_npc.png b/textures/mobs_npc.png new file mode 100644 index 0000000000000000000000000000000000000000..93563989580324581cac10eaa5a91c427eaac724 GIT binary patch literal 901 zcmV;01A6?4P)067o8=&K2l_nO4V-n|G)B{ptiksc002fl!)YT zauTX)vt?PX*QHpQtAo{27_Eh=elm_*5!U(mUnpU$)#7J>>-Fo4Be(e#Kx-v*{UZS4 zZnrN!eVvd0rIA(%q5ce@l(y3Rroo#OoDxn5BmOL6EuA(($zK3|FCzprQV97r@HlH{ zm2^;0NbQW`OsrJ87FH>1p~;c@aW={tYmmRPsHJmCYKeYwwKZ^^coFY78zHgLn5_Y% z6lh6w75dUBA?ru^zCUiakMHZToP2Q2ue+M6=C)FxuZ46k^*_!UmA9fcdTU7Z5?r*U zF-`&DZ3-66XRHDOk|_hdP@5Y3Q5dj!Ct{&5(1d2Y5f;59Yb~?@s?cxC$Y+8wD`%8( zj&FlTLs;vK+pl+w!DG;_Q-Jw+4!rL1-q&;a4*sQ?!Z3^tZ2yiShVvQkVVtIMxC6{U zIL8!T06aFP_&^Sk^AFJjcpQd%K*pwcer)$c=(GkTimIdcl3tfpK& zXw3khAQqpO6XPc$EUbPE1OzG!Fk(EHee)e4Gjtl6LBe`win(Ke)$gM(oPeEn2L3+= zc}8vni@h?yhLrohFo55qa)GEnu~FMJZQJ;kP7e)I#{+yPo5U=plpo0i&Dq={Jj)i3ekvLUQ?c>wU)9kyd?HXbb9NVJ!%#CueOw`|tk zd!{O}eeXG#_`^eTABK@hu@5RJ2caCG0)&nq>K_0l&=VHHggs&gr&G5=l?rYQFuIL# zcuJv*MaTxoDRqT-Ov8|9GZY$^s6#NuvK$$NZWpWJB82jl;y9wA%V`~VM~Y0Hy#3R!0{9|`}$00000NkvXXu0mjfYEy{n literal 0 HcmV?d00001 diff --git a/textures/mobs_npc2.png b/textures/mobs_npc2.png new file mode 100644 index 0000000000000000000000000000000000000000..a9d1a2c216d55d943556282f27e5ddb2ab381523 GIT binary patch literal 1018 zcmV3 zK#@>8fmK9ZUtE+^K5Jk(mQ+BWS45y#Kc-wnh-pWvUq_i~QiyL$P0n{DQ$O7iM;P?!Sh$-0Mo&h*6K#c>X^|Gk_H9=))R7*7$S0MuMH2Vzw7v0~l z@8vtV=w=Q=Fb>?0wNey;?{|sdQ%}V!1jLyV@x7)LY!@LnucZ;5O-NkOjbR8Cn+$C5 zT;H`!ZC2MyeI4pmJXQvP4ksu(oswQOGn2dnjA1ZWVP*{}1BaPJ@EuzBAiX|R*{0G=d81nB2UR32#$nBpZY@(MhWla$sQN)`#` zBNiY@qP!I#M|k4X8-^I07*qoM6N<$f=z4BN&o-= literal 0 HcmV?d00001 diff --git a/textures/mobs_npc_baby.png b/textures/mobs_npc_baby.png new file mode 100644 index 0000000000000000000000000000000000000000..e26e450112be808a83cd019069f2f94bef5db144 GIT binary patch literal 684 zcmV;d0#p5oP)X<)d}xq;TP>eC)Q1^|poezLfvNng7I*is0b>$eR7gq5jdR{{Q^x;Au_( z0004WQchCNq+tro zfL~wBw9!;+<+s+acMaYV3xK~L0K^zTfPV(^K7bEc03cKO1OO4Zx6c8ZyZ3Ijewu=| z-g`3eSrK*dNDAxa4FKhv8>w470wmX1cHI9B0Qw?SPi>%z?ZiO35+TBm0N}OBf5Ztk zc$4ck9~=Q7|IR>8-`^JiaM^Y49|15bs9EFr3jjPKHfu{*_9sAiY#vQ?0+@B*wrDez z5psZk)ZRoaW4{RQphs?XTuqL!02)oIrBR4IyRrzDa!e2Pd_M1Iz!a6*b=l83T6rO| z{{WJ~&}V?u_z{39foLxQ>X8T(t%)37q)^H;!1c*wnPt9$^P*OzE-U$P55hLmyFXlD z>}b0I_9}z5@3QJxf&CP7%nv4gmA;9{_CM0el|AHvQSC`xKY~13(U=lcU+n4G|!P zdjxn)0Hm-7U~-&_SsSzw-db`vfQ52v(&+T*ZTPI=0|01HQ!QqNRDtQz=7dHOCI9JI z;x;%V$fSYN(H?|UwM@hD0`ZJxQ%LOU)DFK=r928L9YxB`uD{soqf}zqb>u(qr!3KW SyUJSt0000