animals/init.lua

593 lines
19 KiB
Lua

local dbg
if moddebug then dbg=moddebug.dbg("animals") else dbg={v1=function() end,v2=function() end,v3=function() end} end
animals = {}
local animals_modpath = minetest.get_modpath("animals")
dofile(animals_modpath .. "/commands.lua")
dofile(animals_modpath .. "/behaviours.lua")
-- Contains all animal species. Each animal, which is defined in an individual
-- file in the 'species' directory, adds an entry to this table, with the key
-- being the animal species (e.g. "cow") and the value being a table that
-- defines the characteristics of it.
animals.species = {}
dofile(animals_modpath .. "/species/zombie.lua")
dofile(animals_modpath .. "/species/rat.lua")
dofile(animals_modpath .. "/species/wolf.lua")
dofile(animals_modpath .. "/species/cow.lua")
dofile(animals_modpath .. "/species/bull.lua")
dofile(animals_modpath .. "/species/sheep.lua")
dofile(animals_modpath .. "/species/donkey.lua")
dofile(animals_modpath .. "/species/horse.lua")
dofile(animals_modpath .. "/species/pig.lua")
dofile(animals_modpath .. "/species/goat.lua")
-- Default model animation frames if a species doesn't
-- define its own.
local animations = {
stand = { x= 0, y= 79, },
lay = { x=162, y=166, },
walk = { x=168, y=187, },
mine = { x=189, y=198, },
walk_mine = { x=200, y=219, },
sit = { x= 81, y=160, },
}
-- HUD tracking stuff...
animals.hud_show_by_player = {}
animals.savedata = function()
local f = io.open(minetest.get_worldpath().."/animals_data", "w+")
if f then
local data = {nn=animals._nn}
f:write(minetest.serialize(data))
f:close()
end
end
animals._nn = 0
local f = io.open(minetest.get_worldpath().."/animals_data", "r")
if f then
local loaddata = minetest.deserialize(f:read("*all"))
animals._nn = loaddata.nn
f:close()
end
animals.nextnum = function()
local rv = animals._nn
animals._nn = rv + 1
animals.savedata()
return rv
end
animals.create = function(pos, species)
local spinfo = animals.species[species]
pos.y = pos.y + spinfo.yoffset
local obj = minetest.add_entity(pos, "animals:animal")
if not obj then
return
end
local ent = obj:get_luaentity()
ent.hp_regen = 30
ent.species = species
ent.name = species .. animals.nextnum()
ent.props["mesh"] = spinfo["mesh"]
ent.props["textures"] = spinfo["textures"]
local box = {spinfo.collisionbox[1],
spinfo.collisionbox[2] - spinfo.yoffset,
spinfo.collisionbox[3],
spinfo.collisionbox[4],
spinfo.collisionbox[5] - spinfo.yoffset,
spinfo.collisionbox[6]}
ent.props["collisionbox"] = box
if spinfo["visual_size"] then
ent.props["visual_size"] = spinfo["visual_size"]
end
local hp_max = spinfo.hp_max
if not hp_max then hp_max = 20 end
ent.props['hp_max'] = hp_max
obj:set_hp(hp_max)
animals.species[ent.species].on_activate(ent)
obj:set_properties(ent.props)
end
minetest.register_entity("animals:animal", {
hp_max = 20,
physical = true,
collide_with_objects = true,
armor_groups = {immortal=1},
visual = "mesh",
visual_size = {x=1, y=1},
makes_footstep_sound = false,
-- Current action. A table, as defined by the people mod.
-- Is nil when it's time to select a new action.
action = nil,
-- Time (game time) to wait until before doing anything else
wait = 0,
stepheight = 1.1,
species = "unknown",
on_activate = function(self, staticdata)
dbg.v3("on_activate, staticdata = "..dump(staticdata))
self.mem = {}
self.props = {}
self.anim = nil
self.second = 1
self.hp_regen = 1
local restored
if staticdata and staticdata ~= "" then
restored = minetest.deserialize(staticdata)
if restored then
if restored.name then
self.name = restored.name
end
if restored.species then
self.species = restored.species
end
if restored.action then
self.action = restored.action
end
if restored.hp_regen then
self.hp_regen = restored.hp_regen
end
if restored.wait then
self.wait = restored.wait
end
if restored.props then
self.props = restored.props
end
if restored.mem then
self.mem = restored.mem
end
end
if not self.species or not animals.species[self.species] then
self.object:remove()
dbg.v1("Removing unknown species at activation time")
return
end
animals.species[self.species].on_activate(self)
self.object:set_properties(self.props)
end
if not self.mem.hungry then
self.mem.hungry = 1
end
if not self.mem.hungertime then
self.mem.hungertime = 120
end
if not self.mem.drown then
self.mem.drown = 0
end
if not self.mem.drowntime then
self.mem.drowntime = 5
end
end,
get_staticdata = function(self)
local data = {
name = self.name,
species = self.species,
action = self.action,
hp_regen = self.hp_regen,
wait = self.wait,
props = self.props,
mem = self.mem,
}
return minetest.serialize(data)
end,
on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir)
local spinfo = animals.species[self.species]
local hp = self.object:get_hp()
-- Can we harvest from this animal?
if spinfo.on_harvest then
if spinfo.on_harvest(self, puncher) then
if hp <= 0 then
self.object:set_hp(1)
end
return
end
end
-- Can we catch it?
if spinfo.catch_with then
local tool = puncher:get_wielded_item()
if tool and tool:get_name() == spinfo.catch_with then
local inv = puncher:get_inventory()
inv:remove_item("main", spinfo.catch_with.." 1")
inv:add_item("main", "animals:"..self.species.." 1")
self.object:remove()
return
end
end
-- Seems like it's a normal punch then...
local playername = puncher:get_player_name()
if hp <= 0 then
if puncher:is_player() then
dbg.v1(self.species.." killed by "..playername)
if spinfo.dead_drops then
for _, item in pairs(spinfo.dead_drops) do
if puncher:get_inventory():room_for_item("main", item) then
puncher:get_inventory():add_item("main", item)
end
end
end
else
dbg.v1(self.species.." killed by something")
end
return
end
if playername then
animals.species[self.species].on_hit(self, playername)
end
end,
on_done_attack = function(self)
local spinfo = animals.species[self.species]
local rsound = spinfo.sounds.attack
if not rsound then return end
if type(rsound) == "table" then
rsound = rsound[(math.random(#rsound))]
end
minetest.sound_play(rsound, {object=self.object})
end,
on_step = function(self, dtime)
if not self.species or not animals.species[self.species] then
self.object:remove()
dbg.v1("Removing unknown species "..(self.species or '<nil>').." at step time")
return
end
local spinfo = animals.species[self.species]
local pos = self.object:getpos()
-- Slowly regenerate health
self.hp_regen = self.hp_regen - dtime
if self.hp_regen < 0 then
self.hp_regen = 45
local hp = self.object:get_hp()
if hp < spinfo.hp_max then
hp = hp + 1
self.object:set_hp(hp)
end
end
self.second = self.second - dtime
if self.second <= 0 then
self.second = self.second + 1
if spinfo.on_second then
spinfo.on_second(self)
end
-- Handle hunger...
-- hunger is on a scale of 0 to 10
self.mem.hungertime = self.mem.hungertime - 1
if self.mem.hungertime <= 0 then
self.mem.hungertime = 240
if self.mem.hungry < 10 then
self.mem.hungry = self.mem.hungry + 1
end
end
-- Handle drowning...
self.mem.drowntime = self.mem.drowntime - 1
if self.mem.drowntime <= 0 then
self.mem.drowntime = 10
local np = minetest.get_node(pos)
local drowns_in
if self.drowns_in then
drowns_in = drowns_in
else
drowns_in = {"default:water_source", "default:water_flowing"}
end
local drowning = false
for _, nn in pairs(drowns_in) do
if np.name == nn then
drowning = true
break
end
end
if drowning then
self.mem.drown = self.mem.drown + 1
if self.mem.drown > 3 then
dbg.v1(self.name.." drowned")
self.object:remove()
return
end
else
self.mem.drown = 0
end
end
end
if spinfo.sounds then
if spinfo.sounds.random and math.random() < 0.001 then
local rsound = spinfo.sounds.random
if type(rsound) == "table" then
rsound = rsound[(math.random(#rsound))]
end
minetest.sound_play(rsound, {object=self.object})
elseif spinfo.sounds.hungry and math.random() < 0.005 and self.mem.hungry > 9 then
minetest.sound_play(spinfo.sounds.hungry, {object=self.object})
end
end
if self.wait ~= 0 then
if minetest.get_gametime() < self.wait then return end
dbg.v3(self.name.." finished wait at "..minetest.get_gametime())
self.wait = 0
end
-- If we don't have a current action, we let the animal code choose
-- one...
if not self.action then
dbg.v2(self.name.." choosing new action")
self.action = animals.species[self.species].on_act(self)
if not self.action then
self.action = {"wait", time=120}
dbg.v2(self.name.." failed to choose action - setting wait")
else
dbg.v2(self.name.." set new action: "..dump(self.action))
end
end
-- Prepare state table for action handler
local state = {
-- In - current yaw, Out - used as new yaw
yaw = self.object:getyaw() - spinfo.yawoffset,
-- In - current position
pos = pos,
-- In - 0, Out - new speed, in yaw direction
speed = 0,
-- Out - if set, a new position to set
setpos = nil,
-- In - current action, Out - modified or replaced action
action = self.action,
-- In - current gather settings, Out - modified or replaced gather settings
gather = self.gather,
-- In - 0, Out - non-zero sets a wait of that many seconds - doing this
-- will cause a wait and then the current action will
-- continue - different to setting a "wait" action!
wait = 0,
-- In - nil, Out - if set, use that animation - otherwise attempt to
-- select automatically
anim = nil,
-- In - 0, Out - damage to be applied.
damage = 0,
-- In - the entity, in case anything needs to be called on it (avoid
-- that if possible, but for example, getting the last collision
-- result is currently done this way)
ent = self,
-- In - dtime
dtime = dtime,
}
-- Run the appropriate handler for the current action...
local action_done = false
local action_success = false
local handler = people.actions[state.action[1]]
if handler then
action_done, action_success = handler(state)
-- Because state.action can be replaced...
if state.action ~= self.action then
dbg.v3(self.name.." action handler replaced action with "..dump(state.action))
self.action = state.action
end
if state.gather ~= self.gather then
self.gather = state.gather
end
else
dbg.v1(self.name.." has unknown action: "..dump(self.action))
self.action = nil
end
if action_done then
if state.action.prevaction then
dbg.v3(self.name.." completed action - setting previous action")
self.action = state.action.prevaction
else
if action_success then
dbg.v3(self.name.." completed action")
else
dbg.v3(self.name.." failed action")
end
self.action = nil
end
end
if state.damage > 0 then
local hp = self.object:get_hp()
hp = hp - state.damage
if hp <= 0 then
dbg.v1(state.ent.name.."died")
self.object:remove()
end
self.object:set_hp(hp)
end
if state.wait ~= 0 then
self.wait = minetest.get_gametime() + state.wait
dbg.v3(self.name.." will wait until "..self.wait)
end
-- Set appropriate animation
local wantanim = state.anim
if not wantanim then
wantanim = "stand"
if state.speed > 0 then
wantanim = "walk"
end
end
if wantanim ~= self.anim then
local anims = animations
if spinfo.animations then
anims = spinfo.animations
end
self.object:set_animation(anims[wantanim], 15, 0)
self.anim = wantanim
end
if state.setpos then
self.object:setpos(state.setpos)
end
local setvel = vector.from_speed_yaw(state.speed, state.yaw)
setvel.y = -10
self.object:setvelocity(setvel)
self.object:setyaw(state.yaw + spinfo.yawoffset)
end,
})
animals.get_animal = function(name)
for _, ent in pairs(minetest.luaentities) do
if ent and ent.species and ent.name == name then
dbg.v3("get_animal found " .. name)
return ent
end
end
dbg.v3("get_animal did not find " .. name)
return nil
end
local tick_hud = 0
local rate_hud = 1.5
minetest.register_globalstep(function(dtime)
tick_hud = tick_hud + dtime
if tick_hud > rate_hud then
tick_hud = 0
dbg.v3("HUD TICK")
for _, player in pairs(minetest.get_connected_players()) do
local playername = player:get_player_name()
local pshow = animals.hud_show_by_player[playername]
if pshow then
local ent = animals.get_animal(pshow.name)
if not ent then
-- Could be inactive or dead, so keep it but remove
-- any existing element from the HUD
-- TODO - put the element in the middle of the HUD
-- instead, to make it clear the tracked animal is
-- not being found!
if pshow.hudid then
player:hud_remove(pshow.hudid)
pshow.hudid = nil
end
else
local pos, col
pos = ent.object:getpos()
col = 0xFF00FF
-- Label the waypoint on the HUD with the name, along
-- with some minimal info on what the animal is currently
-- supposed to be doing.
local dispname = pshow.name
if ent.wait > 0 then
dispname = dispname .. " (wait " .. math.floor(ent.wait - minetest.get_gametime()) .. "s)"
elseif ent.action and ent.action[0] then
dispname = dispname .. " (" .. ent.action[0] .. ")"
end
if not pshow.hudid then
pshow.hudid = player:hud_add({
hud_elem_type = "waypoint",
name = dispname,
text = "m",
number = col,
world_pos = pos
})
pshow.pos = pos
pshow.col = col
pshow.dispname = dispname
else
dbg.v3("HUD DIST ".. vector.distance(pos, pshow.pos))
if pshow.pos and vector.distance(pos, pshow.pos) > 1 then
pshow.pos = pos
player:hud_change(pshow.hudid, "world_pos", pos)
end
if pshow.col and col ~= pshow.col then
pshow.col = col
player:hud_change(pshow.hudid, "number", col)
end
if pshow.dispname and dispname ~= pshow.dispname then
pshow.dispname = dispname
player:hud_change(pshow.hudid, "name", dispname)
end
end
end
end
end
end
end)
-- Register items for captured animals...
for species, spinfo in pairs(animals.species) do
minetest.register_craftitem("animals:"..species, {
description = spinfo.shortdesc,
inventory_image = 'animals_'..species..'_inv.png',
on_place = function(item, placer, pointed_thing)
if pointed_thing.type == "node" then
local pos = pointed_thing.above
animals.create(pos, species)
item:take_item()
return item
end
end
})
end