-- Child system. -- This file adds functionality for child mobs as well as breeding. -- Hardcoded values: -- How long a mob remains horny (seconds) local HORNY_TIME = 40 -- How long a mob needs to wait before being able to become horny again (seconds) local HORNY_AGAIN_TIME = 240 -- How long it takes by default for a child mob to grow into an adult (seconds) local CHILD_GROW_TIME = 240 -- How long it take for a child to spawn once a mob turned pregnant local CHILD_BIRTH_TIME = 7 -- The size of child mobs is divided by this number local CHILD_SIZE_DIVISOR = 2 -- Range in within mobs can breed local DEFAULT_BREED_RANGE = 4 -- Interval in which the mob checks for partners to breed with (seconds) local BREED_CHECK_INTERVAL = 1 -- Duration that mobs need to breed (be close to each other) before pregnancy starts (seconds). -- Not a precise time, it is only as accurate as BREED_CHECK_INTERVAL. local BREED_TIME = 3 -- Entity variables to persist: rp_mobs.internal.add_persisted_entity_vars({ "_child", -- true if this is a child mob "_child_grow_timer", -- counts the time the mob has been a child (seconds) "_horny", -- true if mob is horny and ready to mate "_horny_timer", -- time the mob has been horny, or, if _horny_recover is true, time mob has been recovering from a previous horny phase "_horny_recover", -- true if mob is currently recovering from a previous horny phase "_breed_check_timer", -- timer for the breed check; if it reaches BREED_CHECK_INTERVAL, mob will attempt to mate, then the timer resets (in seconds) "_breed_timer", -- timer for checking how long 2 mobs have been breeding (mating) "_pregnant", -- true if mob is 'pregnant' and about to spawn a child "_pregnant_timer" -- timer the mob has been pregnant (seconds) }) --[[ NOT persisted variables: _last_breeder: Name of the player who last breeded the mob, used for achievement ]] -- Make mob horny, if possible rp_mobs.make_horny = function(mob, force) if mob._child or mob._horny_recover or mob._pregnant or not rp_mobs.is_alive(mob) then return end if not mob._horny_timer then mob._horny_timer = 0 end if (not mob._horny) and (force or (mob._horny_timer < HORNY_AGAIN_TIME)) then rp_mobs.default_mob_sound(mob, "horny") mob._horny = true mob._horny_recover = false mob._horny_timer = 0 -- Start spawning breed particles in next step mob._breed_check_timer = BREED_CHECK_INTERVAL end end -- Disable mob being horny again rp_mobs.make_unhorny = function(mob) if mob._horny then mob._horny_recover = true end mob._horny = false mob._horny_timer = 0 mob._breed_timer = 0 end -- Turn the mob into an adult rp_mobs.turn_into_adult = function(mob) if not mob._child or not rp_mobs.is_alive(mob) then -- No-op if already an adult or dying return end mob._child = false mob._child_grow_timer = nil local cpos = mob.object:get_pos() cpos.y = cpos.y + ((mob._base_colbox[5] - mob._base_colbox[2]) / CHILD_SIZE_DIVISOR) mob.object:set_pos(cpos) mob.object:set_properties({ visual_size = mob._base_size, collisionbox = mob._base_colbox, selectionbox = mob._base_selbox, textures = mob._textures_adult, }) end -- Change the mob's properties to child properties without -- setting the _child or _child_grow_timer variables. -- Meant for internal rp_mobs use only! rp_mobs.set_mob_child_properties = function(mob) if not rp_mobs.is_alive(mob) then return end mob.object:set_properties({ visual_size = { x = mob._base_size.x / CHILD_SIZE_DIVISOR, y = mob._base_size.y / CHILD_SIZE_DIVISOR, z = mob._base_size.z / CHILD_SIZE_DIVISOR, }, collisionbox = { mob._base_colbox[1] / CHILD_SIZE_DIVISOR, mob._base_colbox[2] / CHILD_SIZE_DIVISOR, mob._base_colbox[3] / CHILD_SIZE_DIVISOR, mob._base_colbox[4] / CHILD_SIZE_DIVISOR, mob._base_colbox[5] / CHILD_SIZE_DIVISOR, mob._base_colbox[6] / CHILD_SIZE_DIVISOR, }, selectionbox = { mob._base_selbox[1] / CHILD_SIZE_DIVISOR, mob._base_selbox[2] / CHILD_SIZE_DIVISOR, mob._base_selbox[3] / CHILD_SIZE_DIVISOR, mob._base_selbox[4] / CHILD_SIZE_DIVISOR, mob._base_selbox[5] / CHILD_SIZE_DIVISOR, mob._base_selbox[6] / CHILD_SIZE_DIVISOR, rotate = mob._base_selbox.rotate, }, textures = mob._textures_child, }) end -- Turn the mob into a child, if not already. rp_mobs.turn_into_child = function(mob) if not rp_mobs.is_alive(mob) then return end local mobl = mob:get_luaentity() if mobl._child then -- No-op if already a child return end rp_mobs.set_mob_child_properties(mobl) mobl._child = true mobl._child_grow_timer = 0 end -- Advance the child growth timer of the given mob by dtime (in seconds) -- and turn it into an adult once the time has passed. -- Recommended to be added into the on_step function of the mob -- if the mob supports a child version of it. rp_mobs.advance_child_growth = function(mob, dtime) if not rp_mobs.is_alive(mob) then return end -- If mob is child, take CHILD_GROW_TIME seconds before growing into adult if mob._child == true then mob._child_grow_timer = (mob._child_grow_timer or 0) + dtime if mob._child_grow_timer > CHILD_GROW_TIME then rp_mobs.turn_into_adult(mob) end end end -- Pregnancy check that needs to be called every step for mobs -- that can be bred. local pregnancy = function(mob, dtime) if not rp_mobs.is_alive(mob) then return end if mob._pregnant then mob._pregnant_timer = mob._pregnant_timer + dtime if mob._pregnant_timer >= CHILD_BIRTH_TIME then -- Spawn child local pos = mob.object:get_pos() local child = minetest.add_entity(pos, mob.name) if child then rp_mobs.default_mob_sound(mob, "give_birth") rp_mobs.turn_into_child(child) -- Award achievement to player who has breeded the mobs if mob._last_breeder then local breeder = minetest.get_player_by_name(mob._last_breeder) if breeder:is_player() then achievements.trigger_achievement(breeder, "wonder_of_life") end mob._last_breeder = nil end mob._pregnant = false mob._pregnant_timer = nil minetest.log("action", "[rp_mobs] Child mob of type '"..mob.name.."' was born at "..minetest.pos_to_string(pos, 1)) else minetest.log("error", "[rp_mobs] Child mob of type '"..mob.name.."' could not be born at "..minetest.pos_to_string(pos, 1)) end end end end rp_mobs.handle_breeding = function(mob, dtime) if mob._dying then return end if not mob._horny_timer then mob._horny_timer = 0 end if not mob._breed_timer then mob._breed_timer = 0 end if not mob._breed_check_timer then mob._breed_check_timer = 0 end -- Horny mob can mate for HORNY_TIME seconds, afterwards the mob cannot mate again for HORNY_AGAIN_TIME seconds if mob._horny then mob._horny_timer = mob._horny_timer + dtime if mob._horny_timer >= HORNY_TIME then rp_mobs.make_unhorny(mob) end -- Also, the horny recover time does not start while the mob is still pregnant elseif mob._horny_recover and not mob._pregnant then mob._horny_timer = mob._horny_timer + dtime if mob._horny_timer >= HORNY_AGAIN_TIME then mob._horny_recover = false mob._horny_timer = 0 mob._breed_timer = 0 end end -- Update pregnancy status, if pregnant pregnancy(mob, dtime) local breed_dtime = 0 -- If mob is horny, find another same mob who is horny, and mate if (not mob._pregnant) and mob._horny and mob._horny_timer <= HORNY_TIME then local pos = mob.object:get_pos() mob._breed_check_timer = mob._breed_check_timer + dtime if mob._breed_check_timer < BREED_CHECK_INTERVAL then return end breed_dtime = mob._breed_check_timer mob._breed_check_timer = 0 -- Particles show that the mob is horny local effect_pos = {x = pos.x, y = pos.y, z = pos.z} local props = mob.object:get_properties() effect_pos.y = effect_pos.y + props.collisionbox[5] minetest.add_particlespawner( { amount = 4, time = 0.25, minpos = effect_pos, maxpos = effect_pos, minvel = {x = -0, y = 2, z = -0}, maxvel = {x = 2, y = 6, z = 2}, minacc = {x = -4, y = -16, z = -4}, maxacc = {x = 4, y = -4, z = 4}, minexptime = 0.1, maxexptime = 1, minsize = 1, maxsize = 2, texture = { name = "mobs_breed.png", alpha_tween = { 1, 0, start = 0.75 }, }, }) -- Pick a partner to mate with local nearby_objects = minetest.get_objects_inside_radius(pos, mob._breed_range or DEFAULT_BREED_RANGE) local potential_partners = {} local num = 0 local partner = nil for _, obj in ipairs(nearby_objects) do local ent = obj:get_luaentity() -- Partner must be a horny non-pregnant mob if ent and mob ~= ent and ent.name == mob.name and ent._horny and ent._horny_timer <= HORNY_TIME and (not ent._pregnant) then -- There must be no solid node between the two partners local ray = minetest.raycast(obj:get_pos(), mob.object:get_pos(), false, false) local collision = false for pointed_thing in ray do if pointed_thing.type == "node" then local node = minetest.get_node(pointed_thing.under) local ndef = minetest.registered_nodes[node.name] -- Walkable nodes and unknown nodes count as "solid" nodes if (not ndef) or (ndef and ndef.walkable) then collision = true break end end end if not collision then table.insert(potential_partners, ent) end end end -- No partners found, abort if #potential_partners == 0 then mob._breed_timer = 0 return end -- Mob has have to had been close to a potential partner for BREED_TIME at least mob._breed_timer = mob._breed_timer + breed_dtime if mob._breed_timer < BREED_TIME then return end -- Of all eligible partners, pick a random one local r = math.random(1, #potential_partners) local partner = potential_partners[r] -- Of the two parents, a random mob will become pregnant and bear the child local child_bearer, child_giver r = math.random(1, 2) if r == 1 then child_bearer = mob child_giver = partner else child_bearer = partner child_giver = mob end rp_mobs.make_unhorny(child_bearer) rp_mobs.make_unhorny(child_giver) -- Record breeder name if player has fed both parents (for achievement) if child_bearer._last_feeder and child_bearer._last_feeder ~= "" and child_bearer._last_feeder == child_giver._last_feeder then child_bearer._last_breeder = child_bearer._last_feeder end -- Induce pregnancy; the actual child will happen in rp_mobs.pregnancy child_bearer._pregnant = true child_bearer._pregnant_timer = 0 child_bearer._breed_timer = 0 child_giver._breed_timer = 0 local ppos = child_bearer.object:get_pos() local effect2_pos = {x = ppos.x, y = ppos.y, z = ppos.z} props = child_bearer.object:get_properties() effect2_pos.y = effect2_pos.y + props.collisionbox[5] minetest.add_particlespawner({ amount = 1, time = 0.01, minpos = effect2_pos, maxpos = effect2_pos, minvel = {x = 0, y = 2, z = -0}, maxvel = {x = 0, y = 2, z = 0}, minexptime = 2, maxexptime = 2, minsize = 4, maxsize = 4, drag = { x=2, y=2, z=2 }, texture = { name = "mobs_pregnant.png", alpha_tween = { 1, 0, start = 0.75 }, }, }) minetest.log("action", "[rp_mobs] Mob of type '"..child_bearer.name.."' became pregnant at "..minetest.pos_to_string(child_bearer.object:get_pos(), 1)) end end