Pain Is Temporary
parent
88091c589c
commit
d6d9b0a112
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "mods/mobkit"]
|
||||||
|
path = mods/mobkit
|
||||||
|
url = https://github.com/TheTermos/mobkit.git
|
|
@ -4,7 +4,7 @@ function ikea.default_furniture_def()
|
||||||
paramtype2 = "facedir",
|
paramtype2 = "facedir",
|
||||||
drawtype = "mesh",
|
drawtype = "mesh",
|
||||||
wield_scale = {x = 2, y = 2, z = 2},
|
wield_scale = {x = 2, y = 2, z = 2},
|
||||||
groups = {carryable = 1, falling_node = 1, breakable = 10, pathable = 10, ikea_furniture = 1, size_x = 1, size_y = 1, size_z = 1},
|
groups = {carryable = 1, falling_node = 1, breakable = 10, pathable = 0, ikea_furniture = 1, size_x = 1, size_y = 1, size_z = 1},
|
||||||
box_contents = {},
|
box_contents = {},
|
||||||
after_dig_node = util.leave_behind,
|
after_dig_node = util.leave_behind,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,115 +0,0 @@
|
||||||
-- Staff Corpse Node --
|
|
||||||
-- Populates with items from items.lua --
|
|
||||||
|
|
||||||
local function randomtext(length)
|
|
||||||
math.randomseed(math.random(os.clock() * os.time()))
|
|
||||||
local chars = "`~.,!%^*-:;'\" "
|
|
||||||
local str = string.rep("%s", length):gsub("%%s", function()
|
|
||||||
local idx = math.random(1, chars:len())
|
|
||||||
return chars:sub(idx, idx)
|
|
||||||
end)
|
|
||||||
return str
|
|
||||||
end
|
|
||||||
|
|
||||||
local items = {
|
|
||||||
function() -- Stickynote
|
|
||||||
local stack = ItemStack("staff:stickynote")
|
|
||||||
local meta = stack:get_meta()
|
|
||||||
meta:set_int("palette_index", math.random(0, 3) * 64)
|
|
||||||
stack:set_count(math.random(1, 30))
|
|
||||||
return stack, 1
|
|
||||||
end,
|
|
||||||
function() -- Used stickynote
|
|
||||||
local stack = ItemStack("staff:stickynote_used")
|
|
||||||
local meta = stack:get_meta()
|
|
||||||
local message = randomtext(math.random(4, 20))
|
|
||||||
meta:set_string("description", "Sticky Note with Text:\n\"" .. message .. "\"")
|
|
||||||
meta:set_string("message", message)
|
|
||||||
meta:set_int("palette_index", math.random(0, 3) * 64)
|
|
||||||
return stack, 2
|
|
||||||
end,
|
|
||||||
function() -- Pen
|
|
||||||
local stack = ItemStack("staff:pen")
|
|
||||||
stack:set_wear(math.random(1, 65535))
|
|
||||||
return stack, 3
|
|
||||||
end,
|
|
||||||
function() -- ID
|
|
||||||
return "staff:id", 25
|
|
||||||
end,
|
|
||||||
function() -- Notepad
|
|
||||||
return "staff:notepad", 5
|
|
||||||
end,
|
|
||||||
function() -- Used notepad
|
|
||||||
local stack = ItemStack("staff:notepad_used")
|
|
||||||
local meta = stack:get_meta()
|
|
||||||
local pages = {}
|
|
||||||
local pcount = math.random(1, 20)
|
|
||||||
for i = 1, pcount do
|
|
||||||
local content = ""
|
|
||||||
if math.random(1, pcount) == 1 then
|
|
||||||
content = minetest.formspec_escape(randomtext(math.random(10, 200 / 3)))
|
|
||||||
end
|
|
||||||
pages[i] = content
|
|
||||||
end
|
|
||||||
meta:set_int("current_page", math.random(1, #pages))
|
|
||||||
meta:set_string("pages", minetest.serialize(pages))
|
|
||||||
return stack, 10
|
|
||||||
end,
|
|
||||||
function() -- Scanner
|
|
||||||
return "staff:scanner", 50
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
minetest.register_node(":staff:corpse", {
|
|
||||||
description = "SCP-3008-2 (Deceased)",
|
|
||||||
drawtype = "mesh",
|
|
||||||
mesh = "ikea_staff_member_dead.obj",
|
|
||||||
tiles = {"ikea_staff_member.png"},
|
|
||||||
selection_box = {type = "fixed", fixed = {-0.5, -0.5, 0, 0.5, 0, 3}},
|
|
||||||
collision_box = {type = "fixed", fixed = {-0.5, -0.5, 0, 0.5, 0, 3}},
|
|
||||||
walkable = false,
|
|
||||||
paramtype = "light",
|
|
||||||
paramtype2 = "facedir",
|
|
||||||
sunlight_propagates = true,
|
|
||||||
groups = {carryable = 1, falling_node = 1},
|
|
||||||
on_construct = function(pos)
|
|
||||||
-- Inventory randomizer
|
|
||||||
local meta = minetest.get_meta(pos)
|
|
||||||
local inv = meta:get_inventory()
|
|
||||||
inv:set_size("main", 5)
|
|
||||||
|
|
||||||
local contents = {}
|
|
||||||
|
|
||||||
for i = 1, inv:get_size("main") do
|
|
||||||
math.randomseed(math.random(os.clock() * os.time()))
|
|
||||||
if math.random(1, 10) == 1 then -- An item will go in this slot
|
|
||||||
while not contents[i] do
|
|
||||||
local selection, chance = items[math.random(1, #items)]()
|
|
||||||
if math.random(1, chance) == 1 then
|
|
||||||
contents[i] = selection
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
contents[i] = ""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
meta:from_table({
|
|
||||||
inventory = {
|
|
||||||
main = contents
|
|
||||||
},
|
|
||||||
fields = {
|
|
||||||
formspec = [[
|
|
||||||
size[8,7]
|
|
||||||
list[context;main;1.5,1;5,1]
|
|
||||||
list[current_player;main;0,3;8,4]
|
|
||||||
listring[context;main]
|
|
||||||
listring[current_player;main]
|
|
||||||
]],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
end,
|
|
||||||
allow_metadata_inventory_put = function()
|
|
||||||
return 0
|
|
||||||
end,
|
|
||||||
})
|
|
|
@ -7,8 +7,7 @@ function helpers.randompos(pos, radius)
|
||||||
end
|
end
|
||||||
|
|
||||||
function helpers.is_visible(pos, height, range)
|
function helpers.is_visible(pos, height, range)
|
||||||
height = height or 1
|
height = math.max(1, height or 0)
|
||||||
height = math.max(1, height)
|
|
||||||
for _, player in pairs(minetest.get_connected_players()) do
|
for _, player in pairs(minetest.get_connected_players()) do
|
||||||
local ppos = player:get_pos()
|
local ppos = player:get_pos()
|
||||||
-- Dont care about players way far away
|
-- Dont care about players way far away
|
||||||
|
@ -31,17 +30,4 @@ function helpers.is_visible(pos, height, range)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function helpers.get_best_player_sighting(Memory, pos)
|
|
||||||
local best_score = math.huge
|
|
||||||
local best_memory
|
|
||||||
|
|
||||||
for i, memory in Memory:memories() do
|
|
||||||
local score = (vector.distance(pos, memory.pos) / 10) + i -- Lower is Better
|
|
||||||
if score < best_score then
|
|
||||||
best_score, best_memory = score, memory
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return best_memory or false
|
|
||||||
end
|
|
||||||
|
|
||||||
return helpers
|
return helpers
|
||||||
|
|
|
@ -5,158 +5,175 @@
|
||||||
local PATH = minetest.get_modpath(minetest.get_current_modname()) .. "/"
|
local PATH = minetest.get_modpath(minetest.get_current_modname()) .. "/"
|
||||||
local helpers = dofile(PATH .. "helpers.lua")
|
local helpers = dofile(PATH .. "helpers.lua")
|
||||||
|
|
||||||
|
local RANGE = 100
|
||||||
|
|
||||||
--[[ Debug Entities ]]--
|
--[[ Debug Entities ]]--
|
||||||
minetest.register_entity("ikea_staff:debug", {
|
minetest.register_entity("ikea_staff:debug", {
|
||||||
visual = "sprite",
|
visual = "sprite",
|
||||||
textures = {"blank.png^[invert:a^[colorize:red"},
|
textures = {"blank.png^[invert:a^[colorize:red"},
|
||||||
visual_size = {x = 0.3, y = 0.3},
|
visual_size = {x = 0.3, y = 0.3},
|
||||||
on_activate = function(self, data)
|
on_activate = function(self, data)
|
||||||
self.object:set_nametag_attributes({text = data})
|
self.object:set_nametag_attributes({text = data})
|
||||||
minetest.after(5, function()
|
minetest.after(5, function()
|
||||||
if self.object then
|
if self.object then
|
||||||
self.object:remove()
|
self.object:remove()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end,
|
end,
|
||||||
on_punch = function(self)
|
on_punch = function(self)
|
||||||
self.object:remove()
|
self.object:remove()
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
--[[ Staff Entity ]]--
|
do --[[ Staff Entity ]]--
|
||||||
local StaffMember = soal.Mob:new()
|
-- Custom Functions --
|
||||||
|
local function die(self)
|
||||||
StaffMember.initial_properties.mesh = "ikea_staff_member.b3d"
|
local yaw = math.rad(90 * math.floor((math.deg(self.object:get_yaw()) / 90) + 0.5))
|
||||||
StaffMember.initial_properties.textures = {"ikea_staff_member.png"}
|
|
||||||
StaffMember.initial_properties.collisionbox = {-0.45, 0, -0.45, 0.45, 3.1, 0.45}
|
|
||||||
StaffMember.initial_properties.selectionbox = {-0.45, 0, -0.45, 0.45, 3.1, 0.45}
|
|
||||||
|
|
||||||
StaffMember:addCustomProp("range", 40, false) -- How large an area staff members cover
|
|
||||||
StaffMember:addCustomProp("animations", {
|
|
||||||
idle = {range = {x = 0, y = 140}, speed = 15},
|
|
||||||
moving = {range = {x = 141, y = 181}, speed = 45},
|
|
||||||
carry = {range = {x = 204, y = 244}, speed = 45},
|
|
||||||
dying = {range = {x = 182, y = 202}, speed = 10, loop = false},
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
-- Sight and Hive-Memory --
|
|
||||||
local player_sightings = {}
|
|
||||||
|
|
||||||
StaffMember:addCustomProp("worldstate", {
|
|
||||||
target = false, target_is_visible = false, target_is_close = false,
|
|
||||||
player_name = false,
|
|
||||||
wander = false,
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
StaffMember:addStep("sight", 1, function(self, dtime)
|
|
||||||
local pos = self.object:get_pos()
|
local pos = self.object:get_pos()
|
||||||
local range = self:getCustomProp("range")
|
|
||||||
local worldstate = self:getCustomProp("worldstate")
|
mobkit.turn2yaw(self, yaw, 1)
|
||||||
|
mobkit.animate(self, "die")
|
||||||
worldstate.target_is_visible = false
|
self.object:set_pos({x = math.floor(pos.x + 0.5), y = pos.y, z = math.floor(pos.z + 0.5)})
|
||||||
local distance_to_target
|
self.object:set_velocity({x = 0, y = 0, z = 0})
|
||||||
local target_player
|
|
||||||
|
minetest.after((self.animation.die.range.y - self.animation.die.range.x) / self.animation.die.speed, function()
|
||||||
-- Log Player Sightings --
|
minetest.set_node(pos, {name = "staff:corpse", param2 = minetest.dir_to_facedir(minetest.yaw_to_dir(yaw))})
|
||||||
for _, player in ipairs(minetest.get_connected_players()) do
|
self.object:remove()
|
||||||
local player_pos = player and player:get_pos()
|
end)
|
||||||
local player_name = player and player:get_player_name()
|
end
|
||||||
|
|
||||||
if player_pos and self:canSeePlayer(pos, player) then
|
-- Entity Registration --
|
||||||
player_sightings[player_name] = player_pos
|
minetest.register_entity("ikea_staff:member", {
|
||||||
|
initial_properties = {
|
||||||
if player_name == worldstate.player_name then
|
physical = true,
|
||||||
target_player = player
|
collide_with_objects = true,
|
||||||
distance_to_target = vector.distance(pos, player_pos)
|
visual = "mesh",
|
||||||
worldstate.target_is_visible = true
|
mesh = "ikea_staff_member.b3d",
|
||||||
|
textures = {"ikea_staff_member.png"},
|
||||||
|
collisionbox = {-0.45, 0, -0.45, 0.45, 3.1, 0.45},
|
||||||
|
selectionbox = {-0.45, 0, -0.45, 0.45, 3.1, 0.45},
|
||||||
|
},
|
||||||
|
|
||||||
|
animation = {
|
||||||
|
stand = {range = {x = 0, y = 140}, speed = 15},
|
||||||
|
walk = {range = {x = 141, y = 181}, speed = 80},
|
||||||
|
attack = {range = {x = 204, y = 244}, speed = 45},
|
||||||
|
die = {range = {x = 182, y = 202}, speed = 10, loop = false},
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Stats --
|
||||||
|
armor_groups = {fleshy = 100},
|
||||||
|
max_hp = 20,
|
||||||
|
timeout = 0,
|
||||||
|
buoyancy = 0,
|
||||||
|
max_speed = 4.5,
|
||||||
|
jump_height = 3,
|
||||||
|
view_range = RANGE,
|
||||||
|
attack = {
|
||||||
|
range = 2,
|
||||||
|
damage_groups = {fleshy = 5},
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Base Mobkit Functions --
|
||||||
|
on_step = mobkit.stepfunc,
|
||||||
|
on_activate = mobkit.actfunc,
|
||||||
|
get_staticdata = mobkit.statfunc,
|
||||||
|
|
||||||
|
-- Custom Logic --
|
||||||
|
logic = function(self)
|
||||||
|
if mobkit.timer(self, 1) then
|
||||||
|
local priority = mobkit.get_queue_priority(self)
|
||||||
|
mobkit.vitals(self)
|
||||||
|
|
||||||
|
if not mobkit.is_alive(self) then
|
||||||
|
mobkit.clear_queue_high(self)
|
||||||
|
mobkit.clear_queue_low(self)
|
||||||
|
die(self)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local player = mobkit.get_nearby_player(self)
|
||||||
|
|
||||||
|
if player then
|
||||||
|
mobkit.hq_hunt(self, 100, player)
|
||||||
|
end
|
||||||
|
|
||||||
|
if mobkit.is_queue_empty_high(self) then
|
||||||
|
mobkit.hq_roam(self, 0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end,
|
||||||
|
|
||||||
if worldstate.target == "player" then
|
on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
|
||||||
if (not target_player) or distance_to_target > range then
|
if mobkit.is_alive(self) then
|
||||||
worldstate.target = false
|
mobkit.hurt(self, tool_capabilities.damage_groups.fleshy or 0)
|
||||||
worldstate.player_name = false
|
|
||||||
worldstate.target_is_visible = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not worldstate.target then
|
|
||||||
local closest_player_dist = math.huge
|
|
||||||
for k, v in pairs(player_sightings) do
|
|
||||||
local distance = vector.distance(pos, v)
|
|
||||||
if distance <= range and distance < closest_player_dist then
|
|
||||||
distance_to_target = distance
|
|
||||||
worldstate.target = "player"
|
|
||||||
worldstate.player_name = k
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
worldstate.target_is_close = worldstate.target and distance_to_target < 5
|
|
||||||
self:setCustomProp("worldstate", worldstate)
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- GOAP --
|
|
||||||
local Planner = soag.Planner:new()
|
|
||||||
|
|
||||||
StaffMember:addCustomProp("wander_timer", 0, false)
|
|
||||||
Planner:addAction("wander", {
|
|
||||||
cost = 1,
|
|
||||||
requires = {},
|
|
||||||
provides = {wander = true},
|
|
||||||
func = function(self, dtime)
|
|
||||||
local timer = self:getCustomProp("wander_timer")
|
|
||||||
|
|
||||||
if timer == 0 then
|
|
||||||
local pos = self.object:get_pos()
|
|
||||||
self:setState("move", {
|
|
||||||
target = helpers.randompos(pos, 10),
|
|
||||||
speed = 1,
|
|
||||||
range = 0,
|
|
||||||
})
|
|
||||||
elseif timer >= 5 then
|
|
||||||
self:setCustomProp("wander_timer", 0)
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
self:setCustomProp("wander_timer", timer + dtime)
|
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
end
|
||||||
|
|
||||||
Planner:addAction("find_player", {
|
do --[[ Corpse ]]--
|
||||||
cost = 1,
|
local function randtime()
|
||||||
requires = {target = "player"},
|
return math.random(5, 120)
|
||||||
provides = {target_is_visible = true},
|
end
|
||||||
func = function(self, dtime)
|
|
||||||
local worldstate = self:getCustomProp("worldstate")
|
-- Staff Corpse Node --
|
||||||
if worldstate.target_is_visible then return true end
|
minetest.register_node(":staff:corpse", {
|
||||||
if not worldstate.target then return false end
|
description = "SCP-3008-2 (Deceased)",
|
||||||
|
drawtype = "mesh",
|
||||||
self:setState("move", {target = player_sightings[worldstate.player_name], speed = 2})
|
mesh = "ikea_staff_member_dead.obj",
|
||||||
end,
|
tiles = {"ikea_staff_member.png"},
|
||||||
})
|
selection_box = {type = "fixed", fixed = {-0.5, -0.5, 0, 0.5, 0, 3}},
|
||||||
|
collision_box = {type = "fixed", fixed = {-0.5, -0.5, 0, 0.5, 0, 3}},
|
||||||
StaffMember:addCustomProp("goap_plan", false, false)
|
walkable = false,
|
||||||
|
paramtype = "light",
|
||||||
StaffMember:addStep("ai", 1, function(self, dtime)
|
paramtype2 = "facedir",
|
||||||
local worldstate = self:getCustomProp("worldstate")
|
sunlight_propagates = true,
|
||||||
local plan = self:getCustomProp("goap_plan") or Planner:getPlan(worldstate, {wander = true})
|
groups = {carryable = 1, falling_node = 1},
|
||||||
|
|
||||||
if plan and plan[1] then
|
-- Corpse Mob Spawner --
|
||||||
local is_done = Planner.actions[plan[1]].func(self, dtime)
|
on_timer = function(pos, elapsed)
|
||||||
if is_done then
|
if not helpers.is_visible(pos, 3, RANGE) then
|
||||||
table.remove(plan, 1)
|
minetest.add_entity(helpers.randompos(pos, 5), "ikea_staff:member")
|
||||||
elseif is_done == false then
|
|
||||||
plan = false
|
|
||||||
end
|
end
|
||||||
end
|
minetest.get_node_timer(pos):start(randtime())
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
on_construct = function(pos)
|
||||||
|
minetest.get_node_timer(pos):start(randtime())
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
if plan and not plan[1] then
|
do --[[ Rare "Thin Air" Spawning ]]--
|
||||||
plan = false
|
local TRIES = 20 -- How many times to try finding a valid spawn point before giving up
|
||||||
|
local timer = 0
|
||||||
|
|
||||||
|
minetest.register_globalstep(function(dt)
|
||||||
|
timer = timer + dt
|
||||||
|
if timer >= (5 * 60) then
|
||||||
|
for _, player in pairs(minetest.get_connected_players()) do
|
||||||
|
local pos = player:get_pos()
|
||||||
|
|
||||||
|
local tries = 0
|
||||||
|
while tries <= TRIES do
|
||||||
|
local spawnat = helpers.randompos(pos, RANGE)
|
||||||
|
if not minetest.registered_nodes[minetest.get_node(spawnat).name].walkable then
|
||||||
|
if helpers.is_visible(spawnat, 3, RANGE) then
|
||||||
|
tries = tries + 1
|
||||||
|
else
|
||||||
|
spawnat.y = 2
|
||||||
|
minetest.add_entity(spawnat, "ikea_staff:member")
|
||||||
|
break
|
||||||
|
end
|
||||||
|
else
|
||||||
|
tries = tries + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
timer = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
self:setCustomProp("goap_plan", plan)
|
|
||||||
end)
|
end)
|
||||||
|
end
|
||||||
minetest.register_entity("ikea_staff:member", StaffMember)
|
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
name = ikea_staff
|
name = ikea_staff
|
||||||
depends = ikea, son_of_a_goap, son_of_a_luaentitysao
|
depends = ikea, mobkit
|
|
@ -1,43 +0,0 @@
|
||||||
-- Staff Spawning --
|
|
||||||
|
|
||||||
local TRIES = 20 -- How many times to try finding a valid spawn point before giving up
|
|
||||||
local rate = 15
|
|
||||||
local timer = 0
|
|
||||||
|
|
||||||
minetest.register_globalstep(function(dt)
|
|
||||||
timer = timer + dt
|
|
||||||
if timer >= rate then
|
|
||||||
for _, player in pairs(minetest.get_connected_players()) do
|
|
||||||
local pos = player:get_pos()
|
|
||||||
|
|
||||||
local count = 0
|
|
||||||
for _, entity in pairs(minetest.luaentities) do
|
|
||||||
if entity.name == "ikea_staff:member" then
|
|
||||||
if vector.distance(pos, entity.object:get_pos()) <= staff.RANGE then
|
|
||||||
count = count + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if count < 2 then -- math.floor(player:get_meta():get_float("age") / 7)
|
|
||||||
local tries = 0
|
|
||||||
while tries <= TRIES do
|
|
||||||
local spawnat = staff.randompos(pos, staff.RANGE)
|
|
||||||
if not minetest.registered_nodes[minetest.get_node(spawnat).name].walkable then
|
|
||||||
if staff.is_visible(spawnat, 3) then
|
|
||||||
tries = tries + 1
|
|
||||||
else
|
|
||||||
spawnat.y = 2
|
|
||||||
minetest.add_entity(spawnat, "ikea_staff:member")
|
|
||||||
break
|
|
||||||
end
|
|
||||||
else
|
|
||||||
tries = tries + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
timer = 0
|
|
||||||
end
|
|
||||||
end)
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit ddea141b081e087900a6acc5a2a90e8d4e564295
|
|
@ -18,6 +18,6 @@ minetest.register_item(":", {
|
||||||
|
|
||||||
carryable = {times = {[1] = 1.00}, uses = 0},
|
carryable = {times = {[1] = 1.00}, uses = 0},
|
||||||
},
|
},
|
||||||
damage_groups = {whacking = 2},
|
damage_groups = {fleshy = 2},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
local MODPATH = minetest.get_modpath(minetest.get_current_modname()) .. "/"
|
|
||||||
|
|
||||||
soag = {}
|
|
||||||
|
|
||||||
function soag.meets_requirements(a, b)
|
|
||||||
for k, v in pairs(a) do
|
|
||||||
if b[k] ~= v then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
soag.Planner = dofile(MODPATH .. "planner.lua")
|
|
||||||
soag.MemoryManager = dofile(MODPATH .. "memorymanager.lua")
|
|
||||||
|
|
||||||
-- [[ Tests ]] --
|
|
||||||
do
|
|
||||||
local none = function() return true end
|
|
||||||
local Planner = soag.Planner:new()
|
|
||||||
Planner:addAction("look", {
|
|
||||||
cost = 1,
|
|
||||||
requires = {},
|
|
||||||
provides = {sees_enemy = true},
|
|
||||||
prerequisites = none, func = none
|
|
||||||
})
|
|
||||||
|
|
||||||
Planner:addAction("approach", {
|
|
||||||
cost = 1,
|
|
||||||
requires = {sees_enemy = true},
|
|
||||||
provides = {near_enemy = true},
|
|
||||||
prerequisites = none, func = none
|
|
||||||
})
|
|
||||||
Planner:addAction("leap", {
|
|
||||||
cost = 5,
|
|
||||||
requires = {sees_enemy = true},
|
|
||||||
provides = {near_enemy = true, on_top_of_enemy = true},
|
|
||||||
prerequisites = none, func = none
|
|
||||||
})
|
|
||||||
Planner:addAction("chase", {
|
|
||||||
cost = 10,
|
|
||||||
requires = {sees_enemy = true, enemy_moving = true},
|
|
||||||
provides = {near_enemy = true},
|
|
||||||
prerequisites = none, func = none
|
|
||||||
})
|
|
||||||
|
|
||||||
Planner:addAction("grapple", {
|
|
||||||
cost = 1,
|
|
||||||
requires = {sees_enemy = true, near_enemy = true},
|
|
||||||
provides = {enemy_moving = false},
|
|
||||||
prerequisites = none, func = none
|
|
||||||
})
|
|
||||||
|
|
||||||
Planner:addAction("punch", {
|
|
||||||
cost = 1,
|
|
||||||
requires = {sees_enemy = true, near_enemy = true, on_top_of_enemy = false},
|
|
||||||
provides = {enemy_dead = true},
|
|
||||||
prerequisites = none, func = none
|
|
||||||
})
|
|
||||||
Planner:addAction("pound", {
|
|
||||||
cost = 1,
|
|
||||||
requires = {sees_enemy = true, near_enemy = true, on_top_of_enemy = true},
|
|
||||||
provides = {enemy_dead = true},
|
|
||||||
prerequisites = none, func = none
|
|
||||||
})
|
|
||||||
Planner:addAction("headbutt", {
|
|
||||||
cost = 5,
|
|
||||||
requires = {enemy_moving = false, near_enemy = true, near_enemy = true},
|
|
||||||
provides = {enemy_dead = true},
|
|
||||||
prerequisites = none, func = none
|
|
||||||
})
|
|
||||||
Planner:addAction("explode", {
|
|
||||||
cost = 10,
|
|
||||||
requires = {near_enemy = true},
|
|
||||||
provides = {enemy_dead = true, self_dead = true},
|
|
||||||
prerequisites = none, func = none
|
|
||||||
})
|
|
||||||
|
|
||||||
do
|
|
||||||
local plan = Planner:getPlan({self_dead = false, enemy_dead = false, on_top_of_enemy = false}, {enemy_dead = true, self_dead = false, on_top_of_enemy = false})
|
|
||||||
assert(plan, "Getting Plan Failed! " .. dump(plan))
|
|
||||||
|
|
||||||
local test_string = ""
|
|
||||||
while #plan ~= 0 do
|
|
||||||
test_string = test_string .. " " .. table.remove(plan, 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
assert(test_string == " look approach punch", "Plan Is Wrong\n" .. test_string)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,20 +0,0 @@
|
||||||
local MemoryManager = {}
|
|
||||||
function MemoryManager:new(o)
|
|
||||||
o = setmetatable(o or {}, {__index = self})
|
|
||||||
return o
|
|
||||||
end
|
|
||||||
|
|
||||||
function MemoryManager:addMemory(memory)
|
|
||||||
table.insert(self, 1, memory)
|
|
||||||
|
|
||||||
if self[101] then
|
|
||||||
table.remove(self)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function MemoryManager:memories()
|
|
||||||
return ipairs(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
return MemoryManager
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
depends = util
|
|
|
@ -1,82 +0,0 @@
|
||||||
local Planner = {}
|
|
||||||
function Planner:new(o)
|
|
||||||
o = setmetatable(o or {}, self)
|
|
||||||
self.__index = self
|
|
||||||
o.actions = {}
|
|
||||||
return o
|
|
||||||
end
|
|
||||||
|
|
||||||
function Planner:addAction(name, def)
|
|
||||||
assert(
|
|
||||||
type(name) == "string"
|
|
||||||
and type(def.cost) == "number" and def.cost > 0
|
|
||||||
and type(def.requires) == "table"
|
|
||||||
and type(def.provides) == "table"
|
|
||||||
and (def.prerequisites == nil or type(def.prerequisites) == "function")
|
|
||||||
and type(def.func) == "function",
|
|
||||||
|
|
||||||
"Incorrect Call To addAction()\n")
|
|
||||||
self.actions[name] = def
|
|
||||||
end
|
|
||||||
|
|
||||||
function Planner:getActionsCopy()
|
|
||||||
local actions = {}
|
|
||||||
for k, v in pairs(self.actions) do
|
|
||||||
actions[k] = v
|
|
||||||
end
|
|
||||||
return actions
|
|
||||||
end
|
|
||||||
|
|
||||||
local function reassemble_worldstate(path, nodes)
|
|
||||||
local worldstate = {}
|
|
||||||
for _, v in ipairs(path) do
|
|
||||||
for name, bool in pairs(nodes[v].provides) do
|
|
||||||
worldstate[name] = bool
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return worldstate
|
|
||||||
end
|
|
||||||
|
|
||||||
function Planner:getPlan(origin, target)
|
|
||||||
assert(origin and target, "Incorrect Call To getPlan")
|
|
||||||
|
|
||||||
local start_token, end_token = {}, {}
|
|
||||||
local nodes = self:getActionsCopy()
|
|
||||||
nodes[start_token] = {cost = 0, provides = origin, requires = {}}
|
|
||||||
nodes[end_token] = {cost = 0, provides = {}, requires = target}
|
|
||||||
|
|
||||||
local function get_neighbors(node, path)
|
|
||||||
local neighbors, worldstate = {}, reassemble_worldstate(path, nodes)
|
|
||||||
for k, v in pairs(nodes) do
|
|
||||||
if soag.meets_requirements(v.requires, worldstate) then
|
|
||||||
table.insert(neighbors, k)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return neighbors
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_good_node(n)
|
|
||||||
return nodes[n].prerequisites and nodes[n].prerequisites() or true
|
|
||||||
end
|
|
||||||
|
|
||||||
local function get_cost(a, b)
|
|
||||||
return nodes[b].cost or 1
|
|
||||||
end
|
|
||||||
|
|
||||||
local function get_heuristic(a, b, path)
|
|
||||||
local heuristic, worldstate = 0, reassemble_worldstate(path, nodes)
|
|
||||||
for k, v in pairs(target) do
|
|
||||||
heuristic = heuristic + (v ~= worldstate[k] and 1 or 0)
|
|
||||||
end
|
|
||||||
return heuristic
|
|
||||||
end
|
|
||||||
|
|
||||||
local path = util.astar(start_token, end_token, get_neighbors, is_good_node, get_cost, get_heuristic, true)
|
|
||||||
if path then
|
|
||||||
table.remove(path, 1)
|
|
||||||
table.remove(path)
|
|
||||||
end
|
|
||||||
return path
|
|
||||||
end
|
|
||||||
|
|
||||||
return Planner
|
|
|
@ -1,73 +0,0 @@
|
||||||
--[[ Entity Class ]]--
|
|
||||||
local Entity = {_initial_custom_props = {}, _steps = {}, initial_properties = {}}
|
|
||||||
|
|
||||||
function Entity:new(o)
|
|
||||||
o = setmetatable(o or {}, {__index = self})
|
|
||||||
o.initial_properties = table.copy(self.initial_properties)
|
|
||||||
o._initial_custom_props = table.copy(self._initial_custom_props)
|
|
||||||
o._custom_props = setmetatable({}, {__index = o._initial_custom_props})
|
|
||||||
o._steps = table.copy(self._steps)
|
|
||||||
return o
|
|
||||||
end
|
|
||||||
|
|
||||||
function Entity:addCustomProp(name, initial_value, persist)
|
|
||||||
assert(name and (initial_value ~= nil), "Incorrect call to Entity:addCustomProp")
|
|
||||||
self._initial_custom_props[name] = {value = initial_value, persist = persist}
|
|
||||||
end
|
|
||||||
|
|
||||||
function Entity:setCustomProp(name, value)
|
|
||||||
assert(self._custom_props, "Something has gone very wrong!")
|
|
||||||
assert(self._custom_props[name] ~= nil, "Custom Property Must Be Defined!")
|
|
||||||
self._custom_props[name] = {value = value}
|
|
||||||
end
|
|
||||||
|
|
||||||
function Entity:getCustomProp(name)
|
|
||||||
assert(self._custom_props, "Something has gone very wrong!")
|
|
||||||
assert(self._custom_props[name] ~= nil, "Custom Property Must Be Defined!")
|
|
||||||
return self._custom_props[name].value
|
|
||||||
end
|
|
||||||
|
|
||||||
function Entity:addStep(name, interval, func)
|
|
||||||
assert(type(name) == "string" and type(interval) == "number" and type(func) == "function", "Incorrect call to Entity.addStep")
|
|
||||||
table.insert(self._steps, {name = name, interval = interval, timer = 0, func = func})
|
|
||||||
end
|
|
||||||
|
|
||||||
function Entity:doSteps(dtime, moveresult)
|
|
||||||
for i=1, #self._steps do
|
|
||||||
self._steps[i].timer = self._steps[i].timer + dtime
|
|
||||||
if self._steps[i].timer >= self._steps[i].interval then
|
|
||||||
self._steps[i].func(self, self._steps[i].timer, moveresult)
|
|
||||||
self._steps[i].timer = 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Entity:getPersistingProps()
|
|
||||||
local props = {}
|
|
||||||
for name,value in pairs(self._custom_props) do
|
|
||||||
if self._initial_custom_props[name].persist then
|
|
||||||
props[name] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return props
|
|
||||||
end
|
|
||||||
|
|
||||||
function Entity:on_step(dtime, moveresult)
|
|
||||||
self:doSteps(dtime, moveresult)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Entity:get_staticdata()
|
|
||||||
local props = self.object:get_properties()
|
|
||||||
return minetest.serialize({props, self:getPersistingProps()})
|
|
||||||
end
|
|
||||||
|
|
||||||
function Entity:on_activate(staticdata)
|
|
||||||
local props, custom_props = unpack(minetest.deserialize(staticdata) or {{}, {}})
|
|
||||||
self.object:set_properties(props)
|
|
||||||
|
|
||||||
for name, prop in pairs(custom_props) do
|
|
||||||
self:setCustomProp(name, prop)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return Entity
|
|
|
@ -1,66 +0,0 @@
|
||||||
local PATH = minetest.get_modpath(minetest.get_current_modname()) .. "/"
|
|
||||||
|
|
||||||
soal = {}
|
|
||||||
soal.Entity = dofile(PATH .. "entity.lua")
|
|
||||||
|
|
||||||
do --[[ Entity Tests ]]--
|
|
||||||
local TestEntity = soal.Entity:new()
|
|
||||||
|
|
||||||
do -- Check Initial Props --
|
|
||||||
local error_message = "Initial Props Not Right!\n"
|
|
||||||
TestEntity:addCustomProp("a", 123, false)
|
|
||||||
TestEntity:addCustomProp("b", 456, false)
|
|
||||||
TestEntity:addCustomProp("c", 789, true)
|
|
||||||
local expected = {
|
|
||||||
a = {value = 123, persist = false},
|
|
||||||
b = {value = 456, persist = false},
|
|
||||||
c = {value = 789, persist = true},
|
|
||||||
}
|
|
||||||
local result = table.equals(TestEntity._initial_custom_props, expected)
|
|
||||||
assert(result, error_message..dump(TestEntity._initial_custom_props))
|
|
||||||
end
|
|
||||||
|
|
||||||
do -- Check Setting Props --
|
|
||||||
local error_message = "Custom Props Not Right!\n"
|
|
||||||
TestEntity:setCustomProp("a", 987)
|
|
||||||
TestEntity:setCustomProp("b", 654)
|
|
||||||
local expected = {
|
|
||||||
a = {value = 987},
|
|
||||||
b = {value = 654},
|
|
||||||
c = {value = 789, persist = true},
|
|
||||||
}
|
|
||||||
local result = table.equals(TestEntity._custom_props, expected)
|
|
||||||
assert(result, error_message..dump(TestEntity._custom_props))
|
|
||||||
end
|
|
||||||
|
|
||||||
do -- Check Getting Custom Props --
|
|
||||||
local error_message = "Custom Props Not Getting Right!\n"
|
|
||||||
assert(TestEntity:getCustomProp("a") == 987, error_message.."a, 987")
|
|
||||||
assert(TestEntity:getCustomProp("b") == 654, error_message.."b, 654")
|
|
||||||
assert(TestEntity:getCustomProp("c") == 789, error_message.."c, 789")
|
|
||||||
end
|
|
||||||
|
|
||||||
do -- Check addStep --
|
|
||||||
local error_message = "addStep Not Right!\n"
|
|
||||||
local name, interval, func = "test", 123, function()
|
|
||||||
return "foo"
|
|
||||||
end
|
|
||||||
|
|
||||||
TestEntity:addStep(name, interval, func)
|
|
||||||
assert(TestEntity._steps[1], error_message.."Not Defined")
|
|
||||||
assert(TestEntity._steps[1].name == name, error_message.."Name Incorrect")
|
|
||||||
assert(TestEntity._steps[1].interval == interval, error_message.."Interval Wrong!")
|
|
||||||
assert(TestEntity._steps[1].func() == "foo", error_message.."Func Wrong!")
|
|
||||||
end
|
|
||||||
|
|
||||||
do -- Check Persisting Props --
|
|
||||||
local error_message = "Persisting Props Not Right!\n"
|
|
||||||
TestEntity:setCustomProp("c", "test")
|
|
||||||
local expected = {c = {value = "test"}}
|
|
||||||
local result = table.equals(TestEntity:getPersistingProps(), expected)
|
|
||||||
assert(result, error_message..dump(TestEntity:getPersistingProps()))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
soal.Mob = dofile(PATH .. "mob.lua")
|
|
||||||
|
|
|
@ -1,205 +0,0 @@
|
||||||
local PATH = minetest.get_modpath(minetest.get_current_modname()) .. "/"
|
|
||||||
local pathfind = dofile(PATH .. "pathfind.lua")
|
|
||||||
|
|
||||||
--[[ Debug Entities ]]--
|
|
||||||
minetest.register_entity("son_of_a_luaentitysao:debug", {
|
|
||||||
visual = "sprite",
|
|
||||||
textures = {"blank.png^[invert:a^[colorize:red"},
|
|
||||||
visual_size = {x = 0.3, y = 0.3},
|
|
||||||
on_activate = function(self, data)
|
|
||||||
self.object:set_nametag_attributes({text = data})
|
|
||||||
minetest.after(1, function()
|
|
||||||
if self.object then
|
|
||||||
self.object:remove()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end,
|
|
||||||
on_punch = function(self)
|
|
||||||
self.object:remove()
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
--[[ Mob Class ]]--
|
|
||||||
local Mob = soal.Entity:new()
|
|
||||||
|
|
||||||
Mob.initial_properties.visual = "mesh"
|
|
||||||
Mob.initial_properties.mesh = "error.obj"
|
|
||||||
Mob.initial_properties.textures = {"unknown_node.png^[colorize:#ff0000:255"}
|
|
||||||
Mob.initial_properties.collide_with_objects = true
|
|
||||||
Mob.initial_properties.makes_footstep_sound = true
|
|
||||||
Mob.initial_properties.physical = true
|
|
||||||
Mob.initial_properties.max_hp = 100
|
|
||||||
|
|
||||||
Mob:addCustomProp("gravity", -9.8, true)
|
|
||||||
|
|
||||||
-- Velocity Handeling --
|
|
||||||
Mob:addCustomProp("velocity", {x=0,y=0,z=0}, false)
|
|
||||||
|
|
||||||
function Mob:setVelocity(velocity)
|
|
||||||
self.object:set_velocity(velocity)
|
|
||||||
self:setCustomProp("velocity", velocity)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Animation Handeling --
|
|
||||||
Mob:addCustomProp("animation", "idle", false)
|
|
||||||
Mob:addCustomProp("animations", {
|
|
||||||
idle = {range = {x = 0, y = 100}, speed = 15},
|
|
||||||
moving = {range = {x = 0, y = 100}, speed = 15, loop = true},
|
|
||||||
}, false)
|
|
||||||
|
|
||||||
function Mob:setAnimation(state, speed)
|
|
||||||
if self:getCustomProp("animation") ~= state then
|
|
||||||
local anim = self:getCustomProp("animations")[state] or {}
|
|
||||||
assert(anim.range, "Missing animation range for '" .. state .. "'.")
|
|
||||||
self.object:set_animation(anim.range, (anim.speed or 30) * (speed or 1), 0, anim.loop)
|
|
||||||
self:setCustomProp("animation", state)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Interpolated Turning --
|
|
||||||
Mob:addCustomProp("turn_speed", 180, true)
|
|
||||||
Mob:addCustomProp("turn_target", false, false)
|
|
||||||
|
|
||||||
function Mob:lookAt(pos, target, speed)
|
|
||||||
local lookat
|
|
||||||
if type(target) == "table" then
|
|
||||||
lookat = math.deg(minetest.dir_to_yaw(vector.direction(pos, target)))
|
|
||||||
-- Manual calculation: math.deg(math.atan2(target.z - pos.z, target.x - pos.x)) - 90
|
|
||||||
else
|
|
||||||
lookat = math.deg(target)
|
|
||||||
end
|
|
||||||
|
|
||||||
self:setCustomProp("turn_target", lookat)
|
|
||||||
end
|
|
||||||
|
|
||||||
Mob:addStep("turn_interpolate", 0, function(self, dtime)
|
|
||||||
local look = self:getCustomProp("turn_target")
|
|
||||||
local turnspeed = self:getCustomProp("turn_speed")
|
|
||||||
|
|
||||||
if look then
|
|
||||||
local yaw = math.deg(self.object:get_yaw())
|
|
||||||
if yaw ~= look then
|
|
||||||
local diff = look - yaw
|
|
||||||
|
|
||||||
while diff < -180 do diff = diff + 360 end
|
|
||||||
while diff > 180 do diff = diff - 360 end
|
|
||||||
|
|
||||||
if math.abs(diff) < turnspeed * dtime then
|
|
||||||
self.object:set_yaw(math.rad(look))
|
|
||||||
self:setCustomProp("turn_target", false)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.object:set_yaw(math.rad(yaw + math.abs(diff) / diff * turnspeed * dtime))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Misc --
|
|
||||||
function Mob:hasDirectPath(pos, target)
|
|
||||||
if Raycast(vector.add(pos, {x = 0, y = 0.5, z = 0}), vector.add(target, {x = 0, y = 0.5, z = 0}), false):next() then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
function Mob:canSeePlayer(pos, player)
|
|
||||||
local props = self.object:get_properties()
|
|
||||||
local eye = vector.offset(pos, 0, props.collisionbox[5] - 0.3, 0)
|
|
||||||
|
|
||||||
for i = 0, 2 do -- Check at feet, body, and head positions
|
|
||||||
for pointed in Raycast(eye, vector.offset(player:get_pos(), 0, i, 0)) do
|
|
||||||
if pointed.type == "object" and pointed.ref == player then
|
|
||||||
return true
|
|
||||||
elseif pointed.type == "object" and pointed.ref ~= self.object then
|
|
||||||
break
|
|
||||||
elseif pointed.type == "node" then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local parent_on_activate = Mob.on_activate
|
|
||||||
function Mob:on_activate(...)
|
|
||||||
parent_on_activate(self, ...)
|
|
||||||
self.object:set_acceleration(vector.new(0, self:getCustomProp("gravity"), 0))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- FSM --
|
|
||||||
Mob:addCustomProp("fsm_current_state", false, false)
|
|
||||||
Mob:addCustomProp("fsm_context", {}, false)
|
|
||||||
Mob:addCustomProp("fsm_states", {
|
|
||||||
move = function(self, context, dtime)
|
|
||||||
if not context.target then
|
|
||||||
self:setAnimation("idle")
|
|
||||||
return context
|
|
||||||
end
|
|
||||||
|
|
||||||
local pos = self.object:get_pos()
|
|
||||||
if vector.distance(pos, context.target) < 0.5 then
|
|
||||||
if context.path and #context.path > 0 then
|
|
||||||
context.target = table.remove(context.path, 1)
|
|
||||||
else
|
|
||||||
self:setAnimation("idle")
|
|
||||||
self:setVelocity(vector.new(0, 0, 0))
|
|
||||||
context.target = nil
|
|
||||||
return context
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not self:hasDirectPath(pos, context.target) then
|
|
||||||
local path = pathfind(pos, context.target, context.range or 20)
|
|
||||||
if path then
|
|
||||||
if context.path then
|
|
||||||
for i, v in ipairs(path) do
|
|
||||||
table.insert(context.path, i, v)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
context.path = path
|
|
||||||
end
|
|
||||||
else
|
|
||||||
return context
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self:setAnimation("moving")
|
|
||||||
self:lookAt(pos, context.target, context.speed * 10)
|
|
||||||
local velocity = vector.multiply(vector.direction(pos, context.target), context.speed)
|
|
||||||
velocity.y = 0
|
|
||||||
self:setVelocity(velocity)
|
|
||||||
return context
|
|
||||||
end,
|
|
||||||
|
|
||||||
emote = function(self, context, dtime)
|
|
||||||
if context.delta then
|
|
||||||
context.delta = context.delta + dtime
|
|
||||||
else
|
|
||||||
context.delta = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
if context.sequence and context.sequence[1] and context.sequence[1].time <= context.delta then
|
|
||||||
local keyframe = table.remove(context.sequence, 1)
|
|
||||||
if keyframe.sound then minetest.sound_play(unpack(keyframe.sound)) end
|
|
||||||
if keyframe.animation then self:setAnimation(keyframe.animation) end
|
|
||||||
if keyframe.callback then keyframe.callback(self, context.delta) end
|
|
||||||
end
|
|
||||||
|
|
||||||
return context
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
function Mob:setState(state, context)
|
|
||||||
self:setCustomProp("fsm_current_state", state)
|
|
||||||
self:setCustomProp("fsm_context", context)
|
|
||||||
end
|
|
||||||
|
|
||||||
Mob:addStep("fsm_state", 0, function(self, dtime)
|
|
||||||
local current_state = self:getCustomProp("fsm_current_state")
|
|
||||||
if current_state then
|
|
||||||
local context = self:getCustomProp("fsm_context")
|
|
||||||
context = self:getCustomProp("fsm_states")[current_state](self, context, dtime)
|
|
||||||
self:setCustomProp("fsm_context", context)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
return Mob
|
|
|
@ -1,2 +0,0 @@
|
||||||
name = son_of_a_luaentitysao
|
|
||||||
depends = util
|
|
|
@ -1,90 +0,0 @@
|
||||||
local walkable, cost = {}, {}
|
|
||||||
minetest.register_on_mods_loaded(function()
|
|
||||||
for k, v in pairs(minetest.registered_nodes) do
|
|
||||||
if v.walkable then
|
|
||||||
walkable[minetest.get_content_id(k)] = true
|
|
||||||
end
|
|
||||||
if v.groups.pathable and v.groups.pathable > 0 then
|
|
||||||
cost[minetest.get_content_id(k)] = v.groups.pathable
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
local function pathfind(origin, target, radius)
|
|
||||||
origin, target = vector.round(origin), vector.round(target)
|
|
||||||
target.y = 0 -- TODO: Jumping/Climbing
|
|
||||||
|
|
||||||
local padding = vector.new(radius, 1, radius)
|
|
||||||
local padded_min, padded_max = vector.sort(origin, target)
|
|
||||||
padded_min, padded_max = vector.subtract(padded_min, padding), vector.add(padded_max, padding)
|
|
||||||
|
|
||||||
local VM = minetest.get_voxel_manip()
|
|
||||||
local minp, maxp = VM:read_from_map(padded_min, padded_max)
|
|
||||||
local VA, node_data = VoxelArea:new{MinEdge = minp, MaxEdge = maxp}, VM:get_data()
|
|
||||||
|
|
||||||
local x_width, y_width, z_width = maxp.x - minp.x, maxp.y - minp.y, maxp.z - minp.z
|
|
||||||
|
|
||||||
local function get_neighbors(i)
|
|
||||||
-- Bounds check Y - 1
|
|
||||||
if math.floor(((i - 1 - VA.ystride) % VA.zstride) / VA.ystride) < 0 then
|
|
||||||
return {}
|
|
||||||
end
|
|
||||||
|
|
||||||
local north = math.floor((i - 1 + VA.zstride) / VA.zstride) <= z_width
|
|
||||||
local south = math.floor((i - 1 - VA.zstride) / VA.zstride) >= 0
|
|
||||||
local east = math.floor((i - 1 + 1) % VA.zstride % VA.ystride) <= x_width
|
|
||||||
local west = math.floor((i - 1 - 1) % VA.zstride % VA.ystride) >= 0
|
|
||||||
|
|
||||||
local neighbors = {}
|
|
||||||
if north then
|
|
||||||
neighbors[#neighbors + 1] = i + VA.zstride
|
|
||||||
if east then neighbors[#neighbors + 1] = i + VA.zstride + 1 end
|
|
||||||
if west then neighbors[#neighbors + 1] = i + VA.zstride - 1 end
|
|
||||||
end
|
|
||||||
if south then
|
|
||||||
neighbors[#neighbors + 1] = i - VA.zstride
|
|
||||||
if east then neighbors[#neighbors + 1] = i - VA.zstride + 1 end
|
|
||||||
if west then neighbors[#neighbors + 1] = i - VA.zstride - 1 end
|
|
||||||
end
|
|
||||||
if east then neighbors[#neighbors + 1] = i + 1 end
|
|
||||||
if west then neighbors[#neighbors + 1] = i - 1 end
|
|
||||||
|
|
||||||
return neighbors
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_good_node(i)
|
|
||||||
return (not walkable[node_data[i]]) and walkable[node_data[i - VA.ystride]]
|
|
||||||
end
|
|
||||||
|
|
||||||
local function get_cost(a, b)
|
|
||||||
return cost[node_data[b]] or 1
|
|
||||||
end
|
|
||||||
|
|
||||||
local function get_heuristic(a, b)
|
|
||||||
local a_pos, b_pos = VA:position(a), VA:position(b)
|
|
||||||
return vector.distance(a_pos, b_pos)
|
|
||||||
end
|
|
||||||
|
|
||||||
local raw_path = util.astar(VA:indexp(origin), VA:indexp(target), get_neighbors, is_good_node, get_cost, get_heuristic)
|
|
||||||
if not raw_path then return false end
|
|
||||||
|
|
||||||
table.remove(raw_path, 1) -- No need to walk to where we already are
|
|
||||||
local path = {}
|
|
||||||
for _, node_index in ipairs(raw_path) do
|
|
||||||
table.insert(path, VA:position(node_index))
|
|
||||||
end
|
|
||||||
|
|
||||||
local trimmed_path, old_direction, old_pos = {}, false, table.remove(path, 1)
|
|
||||||
for _, pos in ipairs(path) do
|
|
||||||
local direction = vector.direction(pos, old_pos)
|
|
||||||
if not old_direction or not vector.equals(direction, old_direction) then
|
|
||||||
table.insert(trimmed_path, old_pos)
|
|
||||||
old_pos, old_direction = pos, direction
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.insert(trimmed_path, table.remove(path))
|
|
||||||
|
|
||||||
return trimmed_path
|
|
||||||
end
|
|
||||||
|
|
||||||
return pathfind
|
|
Loading…
Reference in New Issue