Pain Is Temporary

master
benrob0329 2021-05-30 23:40:43 -04:00
parent 88091c589c
commit d6d9b0a112
18 changed files with 165 additions and 945 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "mods/mobkit"]
path = mods/mobkit
url = https://github.com/TheTermos/mobkit.git

View File

@ -4,7 +4,7 @@ function ikea.default_furniture_def()
paramtype2 = "facedir",
drawtype = "mesh",
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 = {},
after_dig_node = util.leave_behind,
}

View File

@ -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,
})

View File

@ -7,8 +7,7 @@ function helpers.randompos(pos, radius)
end
function helpers.is_visible(pos, height, range)
height = height or 1
height = math.max(1, height)
height = math.max(1, height or 0)
for _, player in pairs(minetest.get_connected_players()) do
local ppos = player:get_pos()
-- Dont care about players way far away
@ -31,17 +30,4 @@ function helpers.is_visible(pos, height, range)
return false
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

View File

@ -5,158 +5,175 @@
local PATH = minetest.get_modpath(minetest.get_current_modname()) .. "/"
local helpers = dofile(PATH .. "helpers.lua")
local RANGE = 100
--[[ Debug Entities ]]--
minetest.register_entity("ikea_staff: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(5, function()
if self.object then
self.object:remove()
end
end)
end,
on_punch = function(self)
self.object:remove()
end,
})
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(5, function()
if self.object then
self.object:remove()
end
end)
end,
on_punch = function(self)
self.object:remove()
end,
})
--[[ Staff Entity ]]--
local StaffMember = soal.Mob:new()
StaffMember.initial_properties.mesh = "ikea_staff_member.b3d"
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)
do --[[ Staff Entity ]]--
-- Custom Functions --
local function die(self)
local yaw = math.rad(90 * math.floor((math.deg(self.object:get_yaw()) / 90) + 0.5))
local pos = self.object:get_pos()
local range = self:getCustomProp("range")
local worldstate = self:getCustomProp("worldstate")
worldstate.target_is_visible = false
local distance_to_target
local target_player
-- Log Player Sightings --
for _, player in ipairs(minetest.get_connected_players()) do
local player_pos = player and player:get_pos()
local player_name = player and player:get_player_name()
if player_pos and self:canSeePlayer(pos, player) then
player_sightings[player_name] = player_pos
if player_name == worldstate.player_name then
target_player = player
distance_to_target = vector.distance(pos, player_pos)
worldstate.target_is_visible = true
mobkit.turn2yaw(self, yaw, 1)
mobkit.animate(self, "die")
self.object:set_pos({x = math.floor(pos.x + 0.5), y = pos.y, z = math.floor(pos.z + 0.5)})
self.object:set_velocity({x = 0, y = 0, z = 0})
minetest.after((self.animation.die.range.y - self.animation.die.range.x) / self.animation.die.speed, function()
minetest.set_node(pos, {name = "staff:corpse", param2 = minetest.dir_to_facedir(minetest.yaw_to_dir(yaw))})
self.object:remove()
end)
end
-- Entity Registration --
minetest.register_entity("ikea_staff:member", {
initial_properties = {
physical = true,
collide_with_objects = true,
visual = "mesh",
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
if worldstate.target == "player" then
if (not target_player) or distance_to_target > range then
worldstate.target = false
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)
end,
on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
if mobkit.is_alive(self) then
mobkit.hurt(self, tool_capabilities.damage_groups.fleshy or 0)
return true
end
self:setCustomProp("wander_timer", timer + dtime)
end,
})
end
Planner:addAction("find_player", {
cost = 1,
requires = {target = "player"},
provides = {target_is_visible = true},
func = function(self, dtime)
local worldstate = self:getCustomProp("worldstate")
if worldstate.target_is_visible then return true end
if not worldstate.target then return false end
self:setState("move", {target = player_sightings[worldstate.player_name], speed = 2})
end,
})
StaffMember:addCustomProp("goap_plan", false, false)
StaffMember:addStep("ai", 1, function(self, dtime)
local worldstate = self:getCustomProp("worldstate")
local plan = self:getCustomProp("goap_plan") or Planner:getPlan(worldstate, {wander = true})
if plan and plan[1] then
local is_done = Planner.actions[plan[1]].func(self, dtime)
if is_done then
table.remove(plan, 1)
elseif is_done == false then
plan = false
do --[[ Corpse ]]--
local function randtime()
return math.random(5, 120)
end
-- Staff Corpse Node --
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},
-- Corpse Mob Spawner --
on_timer = function(pos, elapsed)
if not helpers.is_visible(pos, 3, RANGE) then
minetest.add_entity(helpers.randompos(pos, 5), "ikea_staff:member")
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
plan = false
do --[[ Rare "Thin Air" Spawning ]]--
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
self:setCustomProp("goap_plan", plan)
end)
minetest.register_entity("ikea_staff:member", StaffMember)
end

View File

@ -1,2 +1,2 @@
name = ikea_staff
depends = ikea, son_of_a_goap, son_of_a_luaentitysao
depends = ikea, mobkit

View File

@ -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)

1
mods/mobkit Submodule

@ -0,0 +1 @@
Subproject commit ddea141b081e087900a6acc5a2a90e8d4e564295

View File

@ -18,6 +18,6 @@ minetest.register_item(":", {
carryable = {times = {[1] = 1.00}, uses = 0},
},
damage_groups = {whacking = 2},
damage_groups = {fleshy = 2},
},
})

View File

@ -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

View File

@ -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

View File

@ -1 +0,0 @@
depends = util

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -1,2 +0,0 @@
name = son_of_a_luaentitysao
depends = util

View File

@ -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