593 lines
19 KiB
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
|
|
|
|
|