Major refactor.
All step code is now part of the driver engine: - sound steps - factor accounting - factor-based switching - animation step handling. Moreover, there is no more local factor state subtable, and factors need to return or pass factordata to the driver method. Factordata is only passed to the .start() method as well, but that can then store the needed data into the entity_ai_state as needed by the driver itself. This still has an issue with entity animation sequence ends, as far as I can see, in the sheep eat driver. This will need further work. finders, drivers and factors are now all in separate files. The sheep script has been partially cleaned up to account for the differences Many asserts were added. We need to keep these until things become more stable, and possibly add a ton more of them to make sure we're not passing garbage. A luacheck file is added to keep things sane going forward.
This commit is contained in:
parent
ac2be48792
commit
8a62d34438
16
.luacheckrc
Normal file
16
.luacheckrc
Normal file
@ -0,0 +1,16 @@
|
||||
unused_args = false
|
||||
allow_defined_top = true
|
||||
|
||||
read_globals = {
|
||||
"DIR_DELIM",
|
||||
"minetest", "core",
|
||||
"dump",
|
||||
"vector", "nodeupdate",
|
||||
"VoxelManip", "VoxelArea",
|
||||
"PseudoRandom", "ItemStack",
|
||||
"intllib",
|
||||
"default",
|
||||
"unpack",
|
||||
table = { fields = { "copy", "getn" } }
|
||||
}
|
||||
|
127
driver.lua
127
driver.lua
@ -40,7 +40,25 @@ local function driver_setup(self, driver)
|
||||
end
|
||||
end
|
||||
|
||||
local function driver_start(self)
|
||||
|
||||
--- constructor
|
||||
function Driver.new(object, driver)
|
||||
local self = setmetatable({}, Driver)
|
||||
self.object = object
|
||||
driver_setup(self, driver)
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
-- public methods
|
||||
function Driver:switch(driver, factordata)
|
||||
self:stop()
|
||||
driver_setup(self, driver)
|
||||
self:start(factordata)
|
||||
end
|
||||
|
||||
function Driver:start(factordata)
|
||||
-- sounds
|
||||
local script = self.object.script
|
||||
local sounds = script[self.name].sounds
|
||||
if sounds and sounds.start then
|
||||
@ -55,38 +73,60 @@ local function driver_start(self)
|
||||
end
|
||||
end
|
||||
--print("Calling driver start for driver " .. self.name)
|
||||
self.driver.start(self.object)
|
||||
self.driver.start(self.object, factordata)
|
||||
end
|
||||
|
||||
|
||||
local function driver_stop(self)
|
||||
--print("Calling driver stop for driver " .. self.name)
|
||||
function Driver:stop()
|
||||
self.driver.stop(self.object)
|
||||
end
|
||||
|
||||
|
||||
--- constructor
|
||||
function Driver.new(object, driver)
|
||||
local self = setmetatable({}, Driver)
|
||||
self.object = object
|
||||
driver_setup(self, driver)
|
||||
return self
|
||||
function Driver:get_property(property)
|
||||
return self.properties[property]
|
||||
end
|
||||
|
||||
|
||||
-- public functions
|
||||
function Driver:switch(driver)
|
||||
driver_stop(self)
|
||||
driver_setup(self, driver)
|
||||
driver_start(self)
|
||||
end
|
||||
|
||||
function Driver:start()
|
||||
driver_start(self)
|
||||
function Driver:factor(name, data)
|
||||
-- valid factor for current driver?
|
||||
local script = self.object.script
|
||||
local driver = script[self.name].factors[name]
|
||||
if not driver then
|
||||
-- not valid for current driver!
|
||||
print("notice: invalid factor " .. name .. " for driver " .. self.name)
|
||||
return
|
||||
end
|
||||
self:switch(driver, {[name] = data})
|
||||
end
|
||||
|
||||
function Driver:step(dtime)
|
||||
-- factor handling
|
||||
local script = self.object.script
|
||||
for factor, factordriver in pairs(script[self.name].factors) do
|
||||
-- do we have a test we need to run?
|
||||
local factordata = nil
|
||||
if entity_ai.registered_factors[factor] and not factordata then
|
||||
factordata = entity_ai.registered_factors[factor](self.object, dtime)
|
||||
end
|
||||
-- check results
|
||||
if factordata then
|
||||
print("factor " .. factor .. " affects " .. self.name .. " driver changed to " .. factordriver)
|
||||
self:switch(factordriver, {[factor] = factordata})
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- animation handling
|
||||
local state = self.object.entity_ai_state
|
||||
|
||||
if state.animttl then
|
||||
state.animttl = state.animttl - dtime
|
||||
if state.animttl <= 0 then
|
||||
state.animttl = nil
|
||||
self:animation(state.animation, state.segment + 1)
|
||||
self:factor("anim_end", true)
|
||||
end
|
||||
end
|
||||
|
||||
-- sound handling
|
||||
local sounds = script[self.name].sounds
|
||||
if math.random(1, 200) == 1 and sounds and sounds.random then
|
||||
local sound = script.sounds[sounds.random]
|
||||
@ -99,13 +139,46 @@ function Driver:step(dtime)
|
||||
.. "' from " .. self.name .. " driver")
|
||||
end
|
||||
end
|
||||
|
||||
-- execute driver specific step code
|
||||
self.driver.step(self.object, dtime)
|
||||
end
|
||||
|
||||
function Driver:stop()
|
||||
driver_stop(self)
|
||||
end
|
||||
|
||||
function Driver:get_property(property)
|
||||
return self.properties[property]
|
||||
function Driver:animation(animation, segment)
|
||||
local state = self.object.entity_ai_state
|
||||
state.animation = animation
|
||||
--print(self.name .. ": driver = " .. self.driver.name .. ", animation = " ..
|
||||
-- animation .. ", segment = " .. (segment or 0))
|
||||
if not segment then
|
||||
local animations = self.object.script.animations[animation]
|
||||
if not animations then
|
||||
print(self.object.name .. ": no animations for " .. animation ..
|
||||
", segment = " .. (segment or 0))
|
||||
return
|
||||
end
|
||||
for i = 1, 3 do
|
||||
local animdef = animations[i]
|
||||
if animdef then
|
||||
state.segment = i
|
||||
-- calculate when to advance to next segment
|
||||
if not animdef.frame_loop then
|
||||
local animlen = (animdef[1].y - animdef[1].x) / animdef.frame_speed
|
||||
state.animttl = animlen
|
||||
else
|
||||
state.animttl = nil
|
||||
end
|
||||
self.object.object:set_animation(animdef[1], animdef.frame_speed, animdef.frame_loop)
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
local animdef = self.object.script.animations[animation][segment]
|
||||
if animdef then
|
||||
state.segment = segment
|
||||
self.object.object:set_animation(animdef[1], animdef.frame_speed, animdef.frame_loop)
|
||||
return
|
||||
end
|
||||
end
|
||||
print("animation_select: can't find animation " .. state.animation .. " for driver " ..
|
||||
state.driver .. " for entity " .. self.object.name)
|
||||
end
|
||||
|
226
drivers.lua
Normal file
226
drivers.lua
Normal file
@ -0,0 +1,226 @@
|
||||
|
||||
--[[
|
||||
|
||||
Copyright (c) 2016 - Auke Kok <sofar@foo-projects.org>
|
||||
|
||||
* entity_ai is licensed as follows:
|
||||
- All code is: GNU Affero General Public License, Version 3.0 (AGPL-3.0)
|
||||
- All artwork is: CC-BY-ND-4.0
|
||||
|
||||
A Contributor License Agreement exists, please read:
|
||||
- https://github.com/sofar/entity_ai/readme.md.
|
||||
|
||||
--]]
|
||||
|
||||
entity_ai.register_driver("roam", {
|
||||
start = function(self)
|
||||
-- start with idle animation unless we get a path
|
||||
self.driver:animation("idle")
|
||||
local state = self.entity_ai_state
|
||||
state.roam_ttl = math.random(3, 9)
|
||||
|
||||
self.path = Path(self)
|
||||
if not self.path:find() then
|
||||
--print("Unable to calculate path")
|
||||
self.driver:switch("idle")
|
||||
return
|
||||
end
|
||||
|
||||
-- done, roaming mode good!
|
||||
self.driver:animation("move")
|
||||
end,
|
||||
step = function(self, dtime)
|
||||
-- handle movement stuff
|
||||
local state = self.entity_ai_state
|
||||
if state.roam_ttl and state.roam_ttl <= 0 then
|
||||
self.driver:switch("idle")
|
||||
return
|
||||
end
|
||||
state.roam_ttl = state.roam_ttl - dtime
|
||||
|
||||
-- do path movement
|
||||
if not self.path or self.path:distance() < 0.7 or
|
||||
not self.path:step(dtime) then
|
||||
self.driver:switch("idle")
|
||||
return
|
||||
end
|
||||
end,
|
||||
stop = function(self)
|
||||
local state = self.entity_ai_state
|
||||
state.roam_ttl = nil
|
||||
end,
|
||||
})
|
||||
|
||||
entity_ai.register_driver("idle", {
|
||||
start = function(self)
|
||||
self.driver:animation("idle")
|
||||
self.object:setvelocity(vector.new())
|
||||
local state = self.entity_ai_state
|
||||
state.idle_ttl = math.random(2, 20)
|
||||
-- sanity checks
|
||||
check_trapped_and_escape(self)
|
||||
end,
|
||||
step = function(self, dtime)
|
||||
local state = self.entity_ai_state
|
||||
state.idle_ttl = state.idle_ttl - dtime
|
||||
if state.idle_ttl <= 0 then
|
||||
self.driver:switch("roam")
|
||||
return
|
||||
end
|
||||
end,
|
||||
stop = function(self)
|
||||
local state = self.entity_ai_state
|
||||
state.idle_ttl = nil
|
||||
end,
|
||||
})
|
||||
|
||||
entity_ai.register_driver("startle", {
|
||||
start = function(self, factordata)
|
||||
-- startle animation
|
||||
self.driver:animation("startle")
|
||||
self.object:setvelocity(vector.new())
|
||||
-- collect info we want to use in this driver
|
||||
local state = self.entity_ai_state
|
||||
if factordata and factordata["got_hit"] then
|
||||
state.attacker = factordata["got_hit"][1]
|
||||
state.attacked_at = factordata["got_hit"][5]
|
||||
end
|
||||
end,
|
||||
step = function(self, dtime)
|
||||
end,
|
||||
stop = function(self)
|
||||
-- play out remaining animations
|
||||
end,
|
||||
})
|
||||
|
||||
entity_ai.register_driver("eat", {
|
||||
start = function(self, factordata)
|
||||
self.driver:animation("eat")
|
||||
self.object:setvelocity(vector.new())
|
||||
-- collect info we want to use in this driver
|
||||
local state = self.entity_ai_state
|
||||
state.eat_ttl = math.random(30, 60)
|
||||
if factordata and factordata.near_foodnode then
|
||||
state.food = factordata.near_foodnode
|
||||
end
|
||||
end,
|
||||
step = function(self, dtime)
|
||||
local state = self.entity_ai_state
|
||||
if state.eat_ttl > 0 then
|
||||
state.eat_ttl = state.eat_ttl - dtime
|
||||
return
|
||||
end
|
||||
state.ate_enough = math.random(200, 300)
|
||||
self.driver:switch("eat_end")
|
||||
end,
|
||||
stop = function(self)
|
||||
local state = self.entity_ai_state
|
||||
state.eat_ttl = nil
|
||||
-- increase HP
|
||||
local hp = self.object:get_hp()
|
||||
if hp < self.driver:get_property("hp_max") then
|
||||
self.object:set_hp(hp + 1)
|
||||
end
|
||||
|
||||
-- eat foodnode
|
||||
local food = state.food
|
||||
if not food then
|
||||
return
|
||||
end
|
||||
|
||||
local node = minetest.get_node(food)
|
||||
minetest.sound_play(minetest.registered_nodes[node.name].sounds.dug, {pos = food, max_hear_distance = 18})
|
||||
if node.name == "default:dirt_with_grass" or node.name == "default:dirt_with_dry_grass" then
|
||||
minetest.set_node(food, {name = "default:dirt"})
|
||||
--elseif node.name == "default:grass_1" or node.name == "default:dry_grass_1" then
|
||||
-- minetest.remove_node(food)
|
||||
elseif node.name == "default:grass_2" then
|
||||
minetest.set_node(food, {name = "default:grass_1"})
|
||||
elseif node.name == "default:grass_3" then
|
||||
minetest.set_node(food, {name = "default:grass_2"})
|
||||
elseif node.name == "default:grass_4" then
|
||||
minetest.set_node(food, {name = "default:grass_3"})
|
||||
elseif node.name == "default:grass_5" then
|
||||
minetest.set_node(food, {name = "default:grass_4"})
|
||||
elseif node.name == "default:dry_grass_2" then
|
||||
minetest.set_node(food, {name = "default:dry_grass_1"})
|
||||
elseif node.name == "default:dry_grass_3" then
|
||||
minetest.set_node(food, {name = "default:dry_grass_2"})
|
||||
elseif node.name == "default:dry_grass_4" then
|
||||
minetest.set_node(food, {name = "default:dry_grass_3"})
|
||||
elseif node.name == "default:dry_grass_5" then
|
||||
minetest.set_node(food, {name = "default:dry_grass_4"})
|
||||
end
|
||||
|
||||
state.food = nil
|
||||
end,
|
||||
})
|
||||
|
||||
entity_ai.register_driver("eat_end", {
|
||||
start = function(self)
|
||||
self.driver:animation("eat")
|
||||
self.object:setvelocity(vector.new())
|
||||
end,
|
||||
step = function(self, dtime)
|
||||
end,
|
||||
stop = function(self)
|
||||
end,
|
||||
})
|
||||
|
||||
|
||||
entity_ai.register_driver("flee", {
|
||||
start = function(self)
|
||||
self.driver:animation("move")
|
||||
local state = self.entity_ai_state
|
||||
state.flee_start = minetest.get_us_time()
|
||||
end,
|
||||
step = function(self, dtime)
|
||||
-- check timer ourselves
|
||||
local state = self.entity_ai_state
|
||||
if (minetest.get_us_time() - state.flee_start) > (15 * 1000000) then
|
||||
state.flee_start = nil
|
||||
self.driver:switch("roam")
|
||||
return
|
||||
end
|
||||
|
||||
-- are we fleeing yet?
|
||||
if self.path and self.path.distance then
|
||||
-- stop fleeing if we're at a safe distance
|
||||
-- execute flee path
|
||||
if self.path:distance() < 2.0 then
|
||||
-- get a new flee path
|
||||
self.path = {}
|
||||
else
|
||||
-- follow path
|
||||
if not self.path:step() then
|
||||
self.path = {}
|
||||
end
|
||||
end
|
||||
else
|
||||
self.path = Path(self)
|
||||
if not self.path:find() then
|
||||
--print("Unable to calculate path")
|
||||
return
|
||||
end
|
||||
|
||||
-- done, flee path good!
|
||||
self.driver:animation("move")
|
||||
end
|
||||
end,
|
||||
stop = function(self)
|
||||
-- play out remaining animations
|
||||
end,
|
||||
})
|
||||
|
||||
entity_ai.register_driver("death", {
|
||||
start = function(self)
|
||||
-- start with moving animation
|
||||
self.driver:animation("idle")
|
||||
end,
|
||||
step = function(self, dtime)
|
||||
end,
|
||||
stop = function(self)
|
||||
-- play out remaining animations
|
||||
end,
|
||||
})
|
||||
|
60
factors.lua
Normal file
60
factors.lua
Normal file
@ -0,0 +1,60 @@
|
||||
|
||||
--[[
|
||||
|
||||
Copyright (c) 2016 - Auke Kok <sofar@foo-projects.org>
|
||||
|
||||
* entity_ai is licensed as follows:
|
||||
- All code is: GNU Affero General Public License, Version 3.0 (AGPL-3.0)
|
||||
- All artwork is: CC-BY-ND-4.0
|
||||
|
||||
A Contributor License Agreement exists, please read:
|
||||
- https://github.com/sofar/entity_ai/readme.md.
|
||||
|
||||
--]]
|
||||
|
||||
entity_ai.register_factor("near_foodnode", function(self, dtime)
|
||||
local state = self.entity_ai_state
|
||||
|
||||
-- still fed?
|
||||
if state.ate_enough and state.ate_enough > 0 then
|
||||
state.ate_enough = state.ate_enough - dtime
|
||||
return
|
||||
end
|
||||
state.ate_enough = nil
|
||||
|
||||
-- don't check too often
|
||||
if state.near_foodnode_ttl and state.near_foodnode_ttl > 0 then
|
||||
state.near_foodnode_ttl = state.near_foodnode_ttl - dtime
|
||||
return
|
||||
end
|
||||
state.near_foodnode_ttl = 2.0
|
||||
|
||||
local pos = vector.round(self.object:getpos())
|
||||
local yaw = self.object:getyaw()
|
||||
self.yaw = yaw
|
||||
local offset = minetest.yaw_to_dir(yaw)
|
||||
local maxp = vector.add(pos, offset)
|
||||
local minp = vector.subtract(maxp, {x = 0, y = 1, z = 0 })
|
||||
local nodes = minetest.find_nodes_in_area(minp, maxp, self.driver:get_property("foodnodes"))
|
||||
|
||||
if #nodes == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
--[[ minetest.add_particle({
|
||||
pos = maxp,
|
||||
velocity = vector.new(),
|
||||
acceleration = vector.new(),
|
||||
expirationtime = 3,
|
||||
size = 6,
|
||||
collisiondetection = false,
|
||||
vertical = false,
|
||||
texture = "wool_pink.png",
|
||||
playername = nil
|
||||
})
|
||||
--]]
|
||||
|
||||
-- store grass node in our factor result - take topmost in list
|
||||
return nodes[#nodes]
|
||||
end)
|
||||
|
127
finders.lua
Normal file
127
finders.lua
Normal file
@ -0,0 +1,127 @@
|
||||
|
||||
--[[
|
||||
|
||||
Copyright (c) 2016 - Auke Kok <sofar@foo-projects.org>
|
||||
|
||||
* entity_ai is licensed as follows:
|
||||
- All code is: GNU Affero General Public License, Version 3.0 (AGPL-3.0)
|
||||
- All artwork is: CC-BY-ND-4.0
|
||||
|
||||
A Contributor License Agreement exists, please read:
|
||||
- https://github.com/sofar/entity_ai/readme.md.
|
||||
|
||||
--]]
|
||||
|
||||
entity_ai.register_finder("find_habitat", function(self)
|
||||
local pos = self.object:getpos()
|
||||
local minp, maxp = vector.sort({
|
||||
x = math.random(pos.x - 10, pos.x + 10),
|
||||
y = pos.y - 5,
|
||||
z = math.random(pos.z - 10, pos.z + 10)
|
||||
}, {
|
||||
x = math.random(pos.x - 10, pos.x + 10),
|
||||
y = pos.y + 5,
|
||||
z = math.random(pos.z - 10, pos.z + 10)
|
||||
})
|
||||
local nodes = minetest.find_nodes_in_area_under_air(minp, maxp, self.driver:get_property("habitatnodes"))
|
||||
if #nodes == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local pick = nodes[math.random(1, #nodes)]
|
||||
-- find top walkable node
|
||||
while true do
|
||||
local node = minetest.get_node(pick)
|
||||
if not minetest.registered_nodes[node.name].walkable then
|
||||
pick.y = pick.y - 1
|
||||
else
|
||||
-- one up at the end
|
||||
pick.y = pick.y + 1
|
||||
break
|
||||
end
|
||||
end
|
||||
-- move to the top surface of pick
|
||||
if not pick then
|
||||
return nil
|
||||
end
|
||||
|
||||
--[[ minetest.add_particle({
|
||||
pos = {x = pick.x, y = pick.y - 0.1, z = pick.z},
|
||||
velocity = vector.new(),
|
||||
acceleration = vector.new(),
|
||||
expirationtime = 3,
|
||||
size = 6,
|
||||
collisiondetection = false,
|
||||
vertical = false,
|
||||
texture = "wool_red.png",
|
||||
playername = nil
|
||||
})
|
||||
--]]
|
||||
return pick
|
||||
end)
|
||||
|
||||
entity_ai.register_finder("flee_attacker", function(self)
|
||||
local state = self.entity_ai_state
|
||||
local from = state.attacked_at
|
||||
if state.attacker and state.attacker ~= "" then
|
||||
local player = minetest.get_player_by_name(state.attacker)
|
||||
if player then
|
||||
from = player:getpos()
|
||||
end
|
||||
end
|
||||
if not from then
|
||||
from = self.object:getpos()
|
||||
state.attacked_at = from
|
||||
end
|
||||
|
||||
from = vector.round(from)
|
||||
|
||||
local pos = self.object:getpos()
|
||||
local dir = vector.subtract(pos, from)
|
||||
dir = vector.normalize(dir)
|
||||
dir = vector.multiply(dir, 10)
|
||||
local to = vector.add(pos, dir)
|
||||
|
||||
local nodes = minetest.find_nodes_in_area_under_air(
|
||||
vector.subtract(to, 4),
|
||||
vector.add(to, 4),
|
||||
{"group:crumbly", "group:cracky", "group:stone"})
|
||||
|
||||
if #nodes == 0 then
|
||||
-- failed to get a target, just run away from attacker?!
|
||||
print("No target found, stopped")
|
||||
return
|
||||
end
|
||||
|
||||
-- find top walkable node
|
||||
local pick = nodes[math.random(1, #nodes)]
|
||||
while true do
|
||||
local node = minetest.get_node(pick)
|
||||
if not minetest.registered_nodes[node.name].walkable then
|
||||
pick.y = pick.y - 1
|
||||
else
|
||||
-- one up at the end
|
||||
pick.y = pick.y + 1
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- move to the top surface of pick
|
||||
if not pick then
|
||||
return false
|
||||
end
|
||||
--[[
|
||||
minetest.add_particle({
|
||||
pos = {x = pick.x, y = pick.y - 0.1, z = pick.z},
|
||||
velocity = vector.new(),
|
||||
acceleration = vector.new(),
|
||||
expirationtime = 3,
|
||||
size = 6,
|
||||
collisiondetection = false,
|
||||
vertical = false,
|
||||
texture = "wool_red.png",
|
||||
playername = nil
|
||||
})
|
||||
--]]
|
||||
return pick
|
||||
end)
|
512
init.lua
512
init.lua
@ -80,29 +80,6 @@ obj:factor_is_near_mate = ...
|
||||
-- misc functions
|
||||
--
|
||||
|
||||
-- misc helper functions
|
||||
|
||||
function dir_to_yaw(vec)
|
||||
if vec.z < 0 then
|
||||
return math.pi - math.atan(vec.x / vec.z)
|
||||
elseif vec.z > 0 then
|
||||
return -math.atan(vec.x / vec.z)
|
||||
elseif vec.x < 0 then
|
||||
return math.pi
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
function yaw_to_dir(yaw)
|
||||
local y = yaw + (math.pi / 2)
|
||||
return {x = math.cos(y), y = 0, z = math.sin(y)}
|
||||
end
|
||||
|
||||
function vector.sort(v1, v2)
|
||||
return {x = math.min(v1.x, v2.x), y = math.min(v1.y, v2.y), z = math.min(v1.z, v2.z)},
|
||||
{x = math.max(v1.x, v2.x), y = math.max(v1.y, v2.y), z = math.max(v1.z, v2.z)}
|
||||
end
|
||||
|
||||
function check_trapped_and_escape(self)
|
||||
local pos = vector.round(self.object:getpos())
|
||||
@ -127,16 +104,19 @@ entity_ai = {}
|
||||
|
||||
entity_ai.registered_drivers = {}
|
||||
function entity_ai.register_driver(name, def)
|
||||
assert(not entity_ai.registered_drivers[name])
|
||||
entity_ai.registered_drivers[name] = def
|
||||
end
|
||||
|
||||
entity_ai.registered_factors = {}
|
||||
function entity_ai.register_factor(name, func)
|
||||
assert(not entity_ai.registered_factors[name])
|
||||
entity_ai.registered_factors[name] = func
|
||||
end
|
||||
|
||||
entity_ai.registered_finders = {}
|
||||
function entity_ai.register_finder(name, func)
|
||||
assert(not entity_ai.registered_finders[name])
|
||||
entity_ai.registered_finders[name] = func
|
||||
end
|
||||
|
||||
@ -149,469 +129,20 @@ local modpath = minetest.get_modpath(minetest.get_current_modname())
|
||||
dofile(modpath .. "/path.lua")
|
||||
dofile(modpath .. "/driver.lua")
|
||||
|
||||
|
||||
--
|
||||
-- Animation functions
|
||||
-- standard entity methods
|
||||
--
|
||||
|
||||
local function animation_select(self, animation, segment)
|
||||
local state = self.entity_ai_state
|
||||
state.animation = animation
|
||||
--print(self.name .. ": driver = " .. self.driver.name .. ", animation = " .. animation .. ", segment = " .. (segment or 0))
|
||||
if not segment then
|
||||
local animations = self.script.animations[animation]
|
||||
if not animations then
|
||||
print(self.name .. ": no animations for " .. animation .. ", segment = " .. (segment or 0))
|
||||
return
|
||||
end
|
||||
for i = 1, 3 do
|
||||
local animdef = animations[i]
|
||||
if animdef then
|
||||
state.segment = i
|
||||
-- calculate when to advance to next segment
|
||||
if not animdef.frame_loop then
|
||||
local animlen = (animdef[1].y - animdef[1].x) / animdef.frame_speed
|
||||
state.animttl = animlen
|
||||
else
|
||||
state.animttl = nil
|
||||
end
|
||||
self.object:set_animation(animdef[1], animdef.frame_speed, animdef.frame_loop)
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
local animdef = self.script.animations[animation][segment]
|
||||
if animdef then
|
||||
state.segment = segment
|
||||
self.object:set_animation(animdef[1], animdef.frame_speed, animdef.frame_loop)
|
||||
return
|
||||
end
|
||||
end
|
||||
print("animation_select: can't find animation " .. state.animation .. " for driver " .. state.driver .. " for entity " .. self.name)
|
||||
end
|
||||
|
||||
local function animation_loop(self, dtime)
|
||||
local state = self.entity_ai_state
|
||||
|
||||
if state.animttl then
|
||||
state.animttl = state.animttl - dtime
|
||||
if state.animttl <= 0 then
|
||||
state.animttl = nil
|
||||
state.factors.anim_end = true
|
||||
animation_select(self, state.animation, state.segment + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function consider_factors(self, dtime)
|
||||
local state = self.entity_ai_state
|
||||
|
||||
for factor, factordriver in pairs(self.script[self.driver.name].factors) do
|
||||
-- do we have a test we need to run?
|
||||
if entity_ai.registered_factors[factor] then
|
||||
entity_ai.registered_factors[factor](self, dtime)
|
||||
end
|
||||
-- check results
|
||||
if state.factors[factor] then
|
||||
print("factor " .. factor .. " affects " .. self.name .. " driver changed to " .. factordriver)
|
||||
state.driver = factordriver
|
||||
self.driver:switch(factordriver)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
entity_ai.register_finder("find_habitat", function(self)
|
||||
local pos = self.object:getpos()
|
||||
local minp, maxp = vector.sort({
|
||||
x = math.random(pos.x - 10, pos.x + 10),
|
||||
y = pos.y - 5,
|
||||
z = math.random(pos.z - 10, pos.z + 10)
|
||||
}, {
|
||||
x = math.random(pos.x - 10, pos.x + 10),
|
||||
y = pos.y + 5,
|
||||
z = math.random(pos.z - 10, pos.z + 10)
|
||||
})
|
||||
minp, maxp = vector.sort(minp, maxp)
|
||||
local nodes = minetest.find_nodes_in_area_under_air(minp, maxp, self.driver:get_property("habitatnodes"))
|
||||
if #nodes == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local pick = nodes[math.random(1, #nodes)]
|
||||
-- find top walkable node
|
||||
while true do
|
||||
local node = minetest.get_node(pick)
|
||||
if not minetest.registered_nodes[node.name].walkable then
|
||||
pick.y = pick.y - 1
|
||||
else
|
||||
-- one up at the end
|
||||
pick.y = pick.y + 1
|
||||
break
|
||||
end
|
||||
end
|
||||
-- move to the top surface of pick
|
||||
if not pick then
|
||||
return nil
|
||||
end
|
||||
|
||||
--[[ minetest.add_particle({
|
||||
pos = {x = pick.x, y = pick.y - 0.1, z = pick.z},
|
||||
velocity = vector.new(),
|
||||
acceleration = vector.new(),
|
||||
expirationtime = 3,
|
||||
size = 6,
|
||||
collisiondetection = false,
|
||||
vertical = false,
|
||||
texture = "wool_red.png",
|
||||
playername = nil
|
||||
})
|
||||
--]]
|
||||
return pick
|
||||
end)
|
||||
|
||||
|
||||
entity_ai.register_driver("roam", {
|
||||
start = function(self)
|
||||
-- start with idle animation unless we get a path
|
||||
animation_select(self, "idle")
|
||||
local state = self.entity_ai_state
|
||||
state.roam_ttl = math.random(3, 9)
|
||||
|
||||
self.path = Path(self)
|
||||
if not self.path:find() then
|
||||
--print("Unable to calculate path")
|
||||
self.driver:switch("idle")
|
||||
return
|
||||
end
|
||||
|
||||
-- done, roaming mode good!
|
||||
animation_select(self, "move")
|
||||
end,
|
||||
step = function(self, dtime)
|
||||
-- handle movement stuff
|
||||
local state = self.entity_ai_state
|
||||
if state.roam_ttl > 0 then
|
||||
state.roam_ttl = state.roam_ttl - dtime
|
||||
-- do path movement
|
||||
if not self.path or
|
||||
self.path:distance() < 0.7 or
|
||||
not self.path:step(dtime) then
|
||||
self.driver:switch("idle")
|
||||
return
|
||||
end
|
||||
else
|
||||
self.driver:switch("idle")
|
||||
end
|
||||
end,
|
||||
stop = function(self)
|
||||
local state = self.entity_ai_state
|
||||
state.roam_ttl = nil
|
||||
end,
|
||||
})
|
||||
|
||||
entity_ai.register_driver("idle", {
|
||||
start = function(self)
|
||||
animation_select(self, "idle")
|
||||
self.object:setvelocity(vector.new())
|
||||
local state = self.entity_ai_state
|
||||
state.idle_ttl = math.random(2, 20)
|
||||
-- sanity checks
|
||||
check_trapped_and_escape(self)
|
||||
local pos = vector.round(self.object:getpos())
|
||||
local node = minetest.get_node(pos)
|
||||
if minetest.registered_nodes[node.name].walkable then
|
||||
-- stuck, can we go up?
|
||||
local p2 = {x = pos.x, y = pos.y + 1, z = pos.z}
|
||||
local n2 = minetest.get_node(pos)
|
||||
if not minetest.registered_nodes[n2.name].walkable then
|
||||
end
|
||||
end
|
||||
end,
|
||||
step = function(self, dtime)
|
||||
local state = self.entity_ai_state
|
||||
state.idle_ttl = state.idle_ttl - dtime
|
||||
if state.idle_ttl <= 0 then
|
||||
self.driver:switch("roam")
|
||||
end
|
||||
end,
|
||||
stop = function(self)
|
||||
local state = self.entity_ai_state
|
||||
state.idle_ttl = nil
|
||||
end,
|
||||
})
|
||||
|
||||
entity_ai.register_driver("startle", {
|
||||
start = function(self)
|
||||
-- startle animation
|
||||
animation_select(self, "startle")
|
||||
self.object:setvelocity(vector.new())
|
||||
-- collect info we want to use in this driver
|
||||
local state = self.entity_ai_state
|
||||
if state.factors.got_hit then
|
||||
state.attacker = state.factors.got_hit[1]
|
||||
state.attacked_at = state.factors.got_hit[5]
|
||||
end
|
||||
-- clear factors
|
||||
state.factors.got_hit = nil
|
||||
state.factors.anim_end = nil
|
||||
end,
|
||||
step = function(self, dtime)
|
||||
end,
|
||||
stop = function(self)
|
||||
-- play out remaining animations
|
||||
end,
|
||||
})
|
||||
|
||||
entity_ai.register_driver("eat", {
|
||||
start = function(self)
|
||||
animation_select(self, "eat")
|
||||
self.object:setvelocity(vector.new())
|
||||
-- collect info we want to use in this driver
|
||||
local state = self.entity_ai_state
|
||||
state.eat_ttl = math.random(30, 60)
|
||||
end,
|
||||
step = function(self, dtime)
|
||||
local state = self.entity_ai_state
|
||||
if state.eat_ttl > 0 then
|
||||
state.eat_ttl = state.eat_ttl - dtime
|
||||
return
|
||||
end
|
||||
state.factors.ate_enough = math.random(200, 00)
|
||||
self.driver:switch("eat_end")
|
||||
end,
|
||||
stop = function(self)
|
||||
local state = self.entity_ai_state
|
||||
state.eat_ttl = nil
|
||||
-- increase HP
|
||||
local hp = self.object:get_hp()
|
||||
if hp < self.driver:get_property("hp_max") then
|
||||
self.object:set_hp(hp + 1)
|
||||
end
|
||||
|
||||
-- eat foodnode
|
||||
local food = state.factors.near_foodnode
|
||||
if not food then
|
||||
return
|
||||
end
|
||||
-- FIXME can probably be removed.
|
||||
if type(food) == "number" then
|
||||
return
|
||||
end
|
||||
local node = minetest.get_node(food)
|
||||
minetest.sound_play(minetest.registered_nodes[node.name].sounds.dug, {pos = food, max_hear_distance = 18})
|
||||
if node.name == "default:dirt_with_grass" or node.name == "default:dirt_with_dry_grass" then
|
||||
minetest.set_node(food, {name = "default:dirt"})
|
||||
--elseif node.name == "default:grass_1" or node.name == "default:dry_grass_1" then
|
||||
-- minetest.remove_node(food)
|
||||
elseif node.name == "default:grass_2" then
|
||||
minetest.set_node(food, {name = "default:grass_1"})
|
||||
elseif node.name == "default:grass_3" then
|
||||
minetest.set_node(food, {name = "default:grass_2"})
|
||||
elseif node.name == "default:grass_4" then
|
||||
minetest.set_node(food, {name = "default:grass_3"})
|
||||
elseif node.name == "default:grass_5" then
|
||||
minetest.set_node(food, {name = "default:grass_4"})
|
||||
elseif node.name == "default:dry_grass_2" then
|
||||
minetest.set_node(food, {name = "default:dry_grass_1"})
|
||||
elseif node.name == "default:dry_grass_3" then
|
||||
minetest.set_node(food, {name = "default:dry_grass_2"})
|
||||
elseif node.name == "default:dry_grass_4" then
|
||||
minetest.set_node(food, {name = "default:dry_grass_3"})
|
||||
elseif node.name == "default:dry_grass_5" then
|
||||
minetest.set_node(food, {name = "default:dry_grass_4"})
|
||||
end
|
||||
|
||||
state.factors.near_foodnode = nil
|
||||
end,
|
||||
})
|
||||
|
||||
entity_ai.register_driver("eat_end", {
|
||||
start = function(self)
|
||||
animation_select(self, "eat")
|
||||
self.object:setvelocity(vector.new())
|
||||
end,
|
||||
step = function(self, dtime)
|
||||
end,
|
||||
stop = function(self)
|
||||
end,
|
||||
})
|
||||
|
||||
entity_ai.register_finder("flee_attacker", function(self)
|
||||
local state = self.entity_ai_state
|
||||
local from = state.attacked_at
|
||||
if state.attacker and state.attacker ~= "" then
|
||||
local player = minetest.get_player_by_name(state.attacker)
|
||||
if player then
|
||||
from = player:getpos()
|
||||
end
|
||||
end
|
||||
if not from then
|
||||
from = self.object:getpos()
|
||||
state.attacked_at = from
|
||||
end
|
||||
|
||||
from = vector.round(from)
|
||||
|
||||
local pos = self.object:getpos()
|
||||
local dir = vector.subtract(pos, from)
|
||||
dir = vector.normalize(dir)
|
||||
dir = vector.multiply(dir, 10)
|
||||
local to = vector.add(pos, dir)
|
||||
|
||||
local nodes = minetest.find_nodes_in_area_under_air(
|
||||
vector.subtract(to, 4),
|
||||
vector.add(to, 4),
|
||||
{"group:crumbly", "group:cracky", "group:stone"})
|
||||
|
||||
if #nodes == 0 then
|
||||
-- failed to get a target, just run away from attacker?!
|
||||
print("No target found, stopped")
|
||||
return
|
||||
end
|
||||
|
||||
-- find top walkable node
|
||||
local pick = nodes[math.random(1, #nodes)]
|
||||
while true do
|
||||
local node = minetest.get_node(pick)
|
||||
if not minetest.registered_nodes[node.name].walkable then
|
||||
pick.y = pick.y - 1
|
||||
else
|
||||
-- one up at the end
|
||||
pick.y = pick.y + 1
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- move to the top surface of pick
|
||||
if not pick then
|
||||
return false
|
||||
end
|
||||
--[[
|
||||
minetest.add_particle({
|
||||
pos = {x = pick.x, y = pick.y - 0.1, z = pick.z},
|
||||
velocity = vector.new(),
|
||||
acceleration = vector.new(),
|
||||
expirationtime = 3,
|
||||
size = 6,
|
||||
collisiondetection = false,
|
||||
vertical = false,
|
||||
texture = "wool_red.png",
|
||||
playername = nil
|
||||
})
|
||||
--]]
|
||||
return pick
|
||||
end)
|
||||
|
||||
entity_ai.register_driver("flee", {
|
||||
start = function(self)
|
||||
animation_select(self, "move")
|
||||
local state = self.entity_ai_state
|
||||
state.flee_start = minetest.get_us_time()
|
||||
state.factors.fleed_too_long = nil
|
||||
end,
|
||||
step = function(self, dtime)
|
||||
-- check timer ourselves
|
||||
local state = self.entity_ai_state
|
||||
if (minetest.get_us_time() - state.flee_start) > (15 * 1000000) then
|
||||
state.factors.got_hit = nil
|
||||
state.factors.fleed_too_long = true
|
||||
end
|
||||
|
||||
-- are we fleeing yet?
|
||||
if self.path and self.path.distance then
|
||||
-- stop fleeing if we're at a safe distance
|
||||
-- execute flee path
|
||||
if self.path:distance() < 2.0 then
|
||||
-- get a new flee path
|
||||
self.path = {}
|
||||
else
|
||||
-- follow path
|
||||
if not self.path:step() then
|
||||
self.path = {}
|
||||
end
|
||||
end
|
||||
else
|
||||
self.path = Path(self)
|
||||
if not self.path:find() then
|
||||
--print("Unable to calculate path")
|
||||
return
|
||||
end
|
||||
|
||||
-- done, flee path good!
|
||||
animation_select(self, "move")
|
||||
end
|
||||
end,
|
||||
stop = function(self)
|
||||
-- play out remaining animations
|
||||
end,
|
||||
})
|
||||
|
||||
entity_ai.register_driver("death", {
|
||||
start = function(self)
|
||||
-- start with moving animation
|
||||
animation_select(self, "idle")
|
||||
end,
|
||||
step = function(self, dtime)
|
||||
end,
|
||||
stop = function(self)
|
||||
-- play out remaining animations
|
||||
end,
|
||||
})
|
||||
|
||||
entity_ai.register_factor("near_foodnode", function(self, dtime)
|
||||
local state = self.entity_ai_state
|
||||
if state.factors.ate_enough and state.factors.ate_enough > 0 then
|
||||
state.factors.ate_enough = state.factors.ate_enough - dtime
|
||||
return
|
||||
else
|
||||
state.factors.ate_enough = nil
|
||||
end
|
||||
if self.near_foodnode_ttl and self.near_foodnode_ttl > 0 then
|
||||
self.near_foodnode_ttl = self.near_foodnode_ttl - dtime
|
||||
return
|
||||
end
|
||||
-- don't check too often
|
||||
self.near_foodnode_ttl = 2.0
|
||||
local pos = vector.round(self.object:getpos())
|
||||
local yaw = self.object:getyaw()
|
||||
self.yaw = yaw
|
||||
local offset = yaw_to_dir(yaw)
|
||||
local maxp = vector.add(pos, offset)
|
||||
local minp = vector.subtract(maxp, {x = 0, y = 1, z = 0 })
|
||||
local nodes = minetest.find_nodes_in_area(minp, maxp, self.driver:get_property("foodnodes"))
|
||||
|
||||
if #nodes == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
--[[ minetest.add_particle({
|
||||
pos = maxp,
|
||||
velocity = vector.new(),
|
||||
acceleration = vector.new(),
|
||||
expirationtime = 3,
|
||||
size = 6,
|
||||
collisiondetection = false,
|
||||
vertical = false,
|
||||
texture = "wool_pink.png",
|
||||
playername = nil
|
||||
})
|
||||
--]]
|
||||
|
||||
-- store grass node in our factor result - take topmost in list
|
||||
state.factors.near_foodnode = nodes[#nodes]
|
||||
end)
|
||||
|
||||
|
||||
local function entity_ai_on_activate(self, staticdata)
|
||||
self.entity_ai_state = {
|
||||
factors = {}
|
||||
}
|
||||
local driver = ""
|
||||
self.entity_ai_state = {}
|
||||
|
||||
local driver
|
||||
|
||||
if staticdata ~= "" then
|
||||
-- load staticdata
|
||||
self.entity_ai_state = minetest.deserialize(staticdata)
|
||||
if not self.entity_ai_state then
|
||||
print("entity_ai entity without saved state, removing")
|
||||
self.object:remove()
|
||||
return
|
||||
end
|
||||
@ -626,6 +157,7 @@ local function entity_ai_on_activate(self, staticdata)
|
||||
driver = self.script.driver
|
||||
end
|
||||
self.driver = Driver(self, driver)
|
||||
state.driver_save = nil
|
||||
|
||||
-- path class
|
||||
if self.script[driver].finders then
|
||||
@ -656,22 +188,29 @@ local function entity_ai_on_activate(self, staticdata)
|
||||
end
|
||||
|
||||
local function entity_ai_on_step(self, dtime)
|
||||
animation_loop(self, dtime)
|
||||
consider_factors(self, dtime)
|
||||
self.driver:step(dtime)
|
||||
end
|
||||
|
||||
local function entity_ai_on_punch(self, puncher, time_from_last_punch, tool_capabilities, dir)
|
||||
local state = self.entity_ai_state
|
||||
state.factors["got_hit"] = {puncher:get_player_name(), time_from_last_punch, tool_capabilities, dir, self.object:getpos()}
|
||||
-- sounds?
|
||||
minetest.sound_play("on_punch", {object = self.object})
|
||||
|
||||
-- hp dmg
|
||||
if self.object:get_hp() == 0 then
|
||||
--FIXME
|
||||
print("death")
|
||||
self.driver:switch("death")
|
||||
return
|
||||
end
|
||||
|
||||
-- factor
|
||||
self.driver:factor("got_hit", {
|
||||
puncher:get_player_name(),
|
||||
time_from_last_punch,
|
||||
tool_capabilities,
|
||||
dir,
|
||||
self.object:getpos()
|
||||
})
|
||||
end
|
||||
|
||||
local function entity_ai_on_rightclick(self, clicker)
|
||||
@ -707,12 +246,17 @@ function entity_ai.register_entity(name, def)
|
||||
minetest.register_entity(name, def)
|
||||
end
|
||||
|
||||
-- load builtin registrations
|
||||
dofile(modpath .. "/finders.lua")
|
||||
dofile(modpath .. "/factors.lua")
|
||||
dofile(modpath .. "/drivers.lua")
|
||||
|
||||
-- load entities
|
||||
dofile(modpath .. "/sheep.lua")
|
||||
dofile(modpath .. "/stone_giant.lua")
|
||||
|
||||
|
||||
-- misc.
|
||||
minetest.register_on_joinplayer(function(player)
|
||||
minetest.add_entity({x=31.0,y=2.0,z=96.0}, "entity_ai:stone_giant")
|
||||
end)
|
||||
--minetest.register_on_joinplayer(function(player)
|
||||
-- minetest.add_entity({x=31.0,y=2.0,z=96.0}, "entity_ai:stone_giant")
|
||||
--end)
|
||||
|
9
path.lua
9
path.lua
@ -118,15 +118,14 @@ function Path:step(dtime)
|
||||
if vector.distance(pos, v) < 0.3 then
|
||||
-- remove one
|
||||
--FIXME shouldn't return here
|
||||
local j = i
|
||||
local i, v = next(self.path, i)
|
||||
if not v then
|
||||
local _, v2 = next(self.path, i)
|
||||
if not v2 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
-- prune path more?
|
||||
local ii, vv = next(self.path, i)
|
||||
local iii, vvv = next(self.path, ii)
|
||||
local _, vvv = next(self.path, ii)
|
||||
if vv and vvv and vvv.y == v.y and vector.distance(vv,v) < 2 then
|
||||
-- prune one
|
||||
self.path[ii] = nil
|
||||
@ -164,7 +163,7 @@ function Path:step(dtime)
|
||||
else
|
||||
spd.y = self.object:getvelocity().y
|
||||
-- don't change yaw when jumping
|
||||
self.object:setyaw(dir_to_yaw(spd))
|
||||
self.object:setyaw(minetest.dir_to_yaw(spd))
|
||||
self.object:setvelocity(spd)
|
||||
end
|
||||
end
|
||||
|
@ -116,7 +116,6 @@ local sheep_script = {
|
||||
eat = {
|
||||
factors = {
|
||||
got_hit = "startle",
|
||||
ate_enough = "roam",
|
||||
became_fertile = "fertile",
|
||||
attractor_nearby = "attracted",
|
||||
},
|
||||
@ -146,7 +145,6 @@ local sheep_script = {
|
||||
},
|
||||
factors = {
|
||||
got_hit = "startle",
|
||||
fleed_too_long = "roam",
|
||||
},
|
||||
sounds = {
|
||||
random = "footsteps",
|
||||
|
Loading…
x
Reference in New Issue
Block a user