ikea/mods/ikea_staff/init.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,
})