473 lines
13 KiB
Lua
473 lines
13 KiB
Lua
--[[ IKEA Staff ]] --
|
|
-- By GreenXenith
|
|
-- Constants
|
|
local PATH = minetest.get_modpath("ikea_staff")
|
|
local SPEED = 1
|
|
local GRAVITY = -9.8
|
|
|
|
-- Animation map
|
|
local anims = {
|
|
idle = {range = {x = 0, y = 140}, speed = 15},
|
|
walk = {range = {x = 141, y = 181}, speed = 45},
|
|
die = {range = {x = 182, y = 202}, speed = 10, loop = false},
|
|
}
|
|
|
|
-- Libs and files
|
|
dofile(PATH .. "/items.lua")
|
|
local spawning = dofile(PATH .. "/spawning.lua") -- This doesnt _need_ to be a variable, but we might use it later
|
|
local pathfind = dofile(PATH .. "/pathfind.lua")
|
|
|
|
-- Helpers
|
|
local F = minetest.formspec_escape
|
|
local C = minetest.colorize
|
|
|
|
local function animation(object)
|
|
local get = object:get_animation()
|
|
if get.range then
|
|
get = get.range
|
|
end
|
|
for state, anim in pairs(anims) do
|
|
if table.equals(anim.range, get) then
|
|
return state
|
|
end
|
|
end
|
|
end
|
|
|
|
local function animate(object, state, loop)
|
|
loop = loop or false
|
|
if animation(object) ~= state then
|
|
local anim = anims[state]
|
|
assert(anim.range, "Missing animation range for '" .. state .. "'.")
|
|
object:set_animation(anim.range, anim.speed or 30, 0, anim.loop)
|
|
end
|
|
end
|
|
|
|
-- Interpolated rotation (ObjectRef, target rotation (position or yaw in radians), speed (degrees per step), step is internal only)
|
|
local function point_at(object, target, speed, step)
|
|
-- Make sure object exists
|
|
if not object or not object:get_luaentity() then
|
|
return
|
|
end
|
|
|
|
-- Make sure we dont run 2 turns at once
|
|
local ent = object:get_luaentity()
|
|
if not step then
|
|
if ent._step then
|
|
step = ent._step + 1
|
|
else
|
|
step = 1
|
|
end
|
|
ent._step = step
|
|
elseif ent._step ~= step then
|
|
return
|
|
end
|
|
|
|
local pos = object:get_pos()
|
|
local yaw = math.deg(object:get_yaw())
|
|
if not yaw or not pos then
|
|
return
|
|
end
|
|
|
|
-- Get target yaw
|
|
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
|
|
local diff = lookat - yaw
|
|
local speed = speed or 10
|
|
|
|
while diff < -180 do
|
|
diff = diff + 360
|
|
end
|
|
while diff > 180 do
|
|
diff = diff - 360
|
|
end
|
|
|
|
-- Increment or set directly if needed
|
|
if math.abs(diff) < speed then
|
|
object:set_yaw(math.rad(lookat))
|
|
ent._step = nil
|
|
else
|
|
if diff > 0 then
|
|
yaw = yaw + speed
|
|
elseif diff < 0 then
|
|
yaw = yaw - speed
|
|
end
|
|
object:set_yaw(math.rad(yaw))
|
|
minetest.after(0.01, point_at, object, target, speed, step)
|
|
end
|
|
end
|
|
|
|
local function go_to(object, target)
|
|
if not object then
|
|
return
|
|
end
|
|
local pos = object:get_pos()
|
|
point_at(object, target, 10)
|
|
local velocity = vector.multiply(vector.direction(pos, target), SPEED)
|
|
velocity.y = 0
|
|
object:set_velocity(velocity)
|
|
animate(object, "walk")
|
|
end
|
|
|
|
minetest.register_entity("ikea_staff:member", {
|
|
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},
|
|
physical = true,
|
|
collide_with_objects = false,
|
|
makes_footstep_sound = true,
|
|
hostile = false,
|
|
hp = 100,
|
|
on_activate = function(self, staticdata)
|
|
self.object:set_armor_groups({fleshy = 100})
|
|
self.object:set_acceleration({x = 0, y = GRAVITY, z = 0})
|
|
local scale = {x = 1, y = 1, z = 1}
|
|
local box = {-0.45, 0, -0.45, 0.45, 3.1, 0.45}
|
|
if staticdata and staticdata ~= "" then
|
|
local data = minetest.deserialize(staticdata)
|
|
scale = data.scale
|
|
box = data.box
|
|
self.hp = data.hp
|
|
else
|
|
if math.random(1, 4) == 1 then
|
|
scale = {x = 1, y = 0.6, z = 1}
|
|
box = {-0.45, 0, -0.45, 0.45, 1.9, 0.45}
|
|
end
|
|
end
|
|
self.object:set_hp(self.hp)
|
|
self.object:set_properties({visual_size = scale, collisionbox = box, selectionbox = box})
|
|
animate(self.object, "idle")
|
|
end,
|
|
get_staticdata = function(self)
|
|
local props = self.object:get_properties()
|
|
return minetest.serialize({scale = props.visual_size, box = props.collisionbox, hp = self.object:get_hp()})
|
|
end,
|
|
on_punch = function(self, puncher, time, caps, dir, damage)
|
|
if self.dead then
|
|
return
|
|
end
|
|
|
|
self.hp = self.hp - damage
|
|
self.object:set_hp(self.hp)
|
|
|
|
if not puncher or puncher == self.object then
|
|
return
|
|
end
|
|
|
|
local ppos = puncher:get_pos()
|
|
local eye = table.copy(ppos)
|
|
eye.y = eye.y + puncher:get_properties().eye_height
|
|
local ray = Raycast(eye, vector.add(eye, vector.multiply(puncher:get_look_dir(),
|
|
vector.distance(ppos, self.object:get_pos()))))
|
|
local intersect
|
|
for pointed in ray do
|
|
if pointed.type == "object" and not pointed.ref:is_player() then
|
|
intersect = pointed.intersection_point
|
|
break
|
|
end
|
|
end
|
|
if not intersect then
|
|
intersect = vector.add(self.object:get_pos(), {x = 0, y = 1, z = 0})
|
|
end
|
|
|
|
self.object:set_texture_mod("^[colorize:#780000:128")
|
|
local vel = self.object:get_velocity()
|
|
vel.y = 1
|
|
local size = 3 + (damage / 2)
|
|
if size > 10 then
|
|
size = 10
|
|
end
|
|
minetest.add_particle({
|
|
expiration_time = math.random(5, 10) * 0.1,
|
|
pos = intersect,
|
|
velocity = vector.multiply(vel, 2),
|
|
acceleration = {x = 0, y = GRAVITY, z = 0},
|
|
size = size,
|
|
texture = "ikea_staff_damage.png",
|
|
collisiondetection = true,
|
|
})
|
|
minetest.after(0.2, function()
|
|
self.object:set_texture_mod("")
|
|
end)
|
|
|
|
local knockback = math.ceil(damage / 3)
|
|
if knockback > 3 then
|
|
knockback = 3
|
|
end
|
|
if knockback <= 0 then
|
|
return
|
|
end
|
|
-- This is sort of broke
|
|
self.object:set_velocity(vector.multiply(puncher:get_look_dir(), knockback))
|
|
end,
|
|
on_death = function(self)
|
|
local corpse = minetest.add_entity(self.object:get_pos(), "ikea_staff:member")
|
|
local yaw = self.object:get_yaw()
|
|
local pos = self.object:get_pos()
|
|
|
|
self.object:remove()
|
|
|
|
corpse:set_velocity({x = 0, y = 0, z = 0})
|
|
corpse:set_yaw(yaw)
|
|
corpse:set_properties(self.object:get_properties())
|
|
corpse:get_luaentity().dead = true
|
|
|
|
yaw = math.rad(90 * math.floor((math.deg(yaw) / 90) + 0.5))
|
|
|
|
animate(corpse, "die")
|
|
point_at(corpse, yaw, 1)
|
|
corpse:move_to({x = math.floor(pos.x + 0.5), y = pos.y, z = math.floor(pos.z + 0.5)})
|
|
|
|
local anim = anims.die
|
|
minetest.after((anim.range.y - anim.range.x) / anim.speed, function()
|
|
minetest.set_node(pos, {name = "staff:corpse", param2 = minetest.dir_to_facedir(minetest.yaw_to_dir(yaw))})
|
|
corpse:remove()
|
|
end)
|
|
end,
|
|
timer = 0,
|
|
on_step = function(self, dtime)
|
|
self.timer = self.timer + dtime
|
|
if self.dead or self.timer < 0.4 then
|
|
return
|
|
else
|
|
self.timer = 0
|
|
end
|
|
|
|
if self.trapped then
|
|
if math.random(1, 50) == 1 then
|
|
self.dead = true
|
|
self:on_death()
|
|
end
|
|
end
|
|
|
|
self.hostile = ikea.is_open()
|
|
-- BUG: When it becomes day, if a staff member is following a player, they just walk in a line forever
|
|
|
|
local pos = self.object:get_pos()
|
|
local vel = self.object:get_velocity()
|
|
|
|
if self.target and vector.length(vel) < 0.5 then
|
|
local success, path = pathfind.tree(pos, self.target[#self.target])
|
|
if not success then
|
|
animate(self.object, "idle")
|
|
self.object:set_velocity({x = 0, y = 0, z = 0})
|
|
self.target = nil
|
|
end
|
|
end
|
|
|
|
-- Hunt
|
|
if self.hostile then
|
|
-- Check for visible players
|
|
for _, obj in pairs(minetest.get_objects_inside_radius(pos, 8)) do
|
|
if obj:is_player() then
|
|
local eye = table.copy(pos)
|
|
eye.y = eye.y + self.object:get_properties().collisionbox[5] - 0.3
|
|
local ppos = obj:get_pos()
|
|
-- Check feet, body, and head positions
|
|
for i = 0, 2 do
|
|
if not Raycast(eye, vector.add(ppos, {x = 0, y = i, z = 0}), false):next() then
|
|
-- Check if path is obstructed
|
|
if Raycast(vector.add(pos, {x = 0, y = 0.1, z = 0}), vector.add(ppos, {x = 0, y = 0.1, z = 0}), false):next() then
|
|
local success, path = pathfind.tree(pos, ppos)
|
|
if success then
|
|
self.target = pathfind.trim(path)
|
|
break
|
|
end
|
|
-- Continue looping if no path
|
|
else
|
|
go_to(self.object, ppos)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if not self.target then
|
|
-- Wander
|
|
if math.random(1, 10) == 0 then -- 1 in 10 chance of being idle
|
|
self.object:set_velocity({x = 0, y = 0, z = 0})
|
|
animate(self.object, "idle")
|
|
else
|
|
if animation(self.object) == "idle" then -- Once idle, 1 in 10 chance of being active again
|
|
if math.random(1, 10) ~= 1 then -- Still idle
|
|
if math.random(1, 10) == 1 then -- Point in random directions
|
|
local yaw = self.object:get_yaw()
|
|
point_at(self.object, yaw + math.rad(math.random(-45, 45)), 5)
|
|
end
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Find a place to go if we dont have one. This shouldn't ever really loop more than once, but it could
|
|
while not self.target do
|
|
local query = table.copy(pos)
|
|
local radius = 15
|
|
query.x = query.x + math.random(-radius, radius)
|
|
query.z = query.z + math.random(-radius, radius)
|
|
query = pathfind.find_open_near(query)
|
|
if query then
|
|
local success, path = pathfind.tree(pos, query)
|
|
if not success then
|
|
-- Trapped, die
|
|
if path == "NO_START" then
|
|
self.trapped = true
|
|
return
|
|
end
|
|
else
|
|
-- Only save useful positions (pivot points)
|
|
self.target = pathfind.trim(path)
|
|
-- Go there
|
|
go_to(self.object, self.target[1])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
else
|
|
-- Turn and move
|
|
self.target[1].y = pos.y
|
|
if vector.distance(pos, self.target[1]) <= 0.2 then
|
|
self.object:move_to(self.target[1])
|
|
table.remove(self.target, 1)
|
|
if next(self.target) then
|
|
go_to(self.object, self.target[1])
|
|
else -- Reached destination
|
|
self.target = nil
|
|
animate(self.object, "idle")
|
|
self.object:set_velocity({x = 0, y = 0, z = 0})
|
|
end
|
|
end
|
|
end
|
|
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()
|
|
local function randomtext(length)
|
|
math.randomseed(math.random(65535))
|
|
-- Include spaces in char set, sets of 3 to offset 3-byte chars
|
|
local charset = table.shuffle({
|
|
"═",
|
|
"║",
|
|
"╗",
|
|
"╚",
|
|
"╝",
|
|
"╠",
|
|
"╣",
|
|
"╦",
|
|
"╩",
|
|
"╬",
|
|
" ",
|
|
" ",
|
|
" ",
|
|
" ",
|
|
})
|
|
if length > 0 then
|
|
return randomtext(length - 1) .. charset[math.random(1, #charset)]
|
|
else
|
|
return ""
|
|
end
|
|
end
|
|
inv:set_size("main", 5)
|
|
local contents = {}
|
|
local choose = {
|
|
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 = 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,
|
|
}
|
|
for i = 1, 5 do
|
|
if math.random(1, 10) == 1 then
|
|
while not contents[i] do
|
|
math.randomseed(math.random(65535))
|
|
local selection, chance = choose[math.random(1, #choose)]()
|
|
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,
|
|
})
|