115 lines
4.9 KiB
Lua
Executable File
115 lines
4.9 KiB
Lua
Executable File
-- Ephemeral third-person visuals only. Main loads this once before runtime.
|
|
-- No ItemStacks, inventories, metadata, static data, attacks or file paths from input.
|
|
local C, H = core, hearth
|
|
local meshes = {
|
|
["hearth:knife"] = "hearth_knife.obj",
|
|
["hearth:axe"] = "hearth_axe.obj",
|
|
["hearth:pick"] = "hearth_pick.obj",
|
|
["hearth:spear"] = "hearth_spear.obj",
|
|
}
|
|
local records, pending = {}, nil
|
|
local entity_name = "hearth:held_visual"
|
|
|
|
local function live(record)
|
|
local p, name = record.player, record.name
|
|
return C.get_player_by_name(name) == p and H.sessions[name] == record.session
|
|
and H.players[name] ~= nil and not record.session.waiting
|
|
and p:get_pos() ~= nil and p:get_hp() > 0
|
|
and p:get_wielded_item():get_name() == record.item
|
|
end
|
|
local function remove(record)
|
|
-- Clear ownership BEFORE remove/on_deactivate; an old callback cannot clear
|
|
-- a replacement belonging to a same-name reconnect or a different weapon.
|
|
if records[record.name] == record then records[record.name] = nil end
|
|
local object = record.object
|
|
record.object = nil
|
|
if object and object:get_pos() then object:remove() end
|
|
end
|
|
local function attached(record)
|
|
local object = record.object
|
|
if not object or not object:get_pos() then return false end
|
|
local parent, bone, pos, rotation, forced = object:get_attach()
|
|
return parent == record.player and bone == "Hand_Right" and pos
|
|
and pos.x == 0 and pos.y == 0 and pos.z == 0 and rotation
|
|
and rotation.x == 90 and rotation.y == 0 and rotation.z == 0 and forced == false
|
|
end
|
|
|
|
C.register_entity(entity_name, {
|
|
initial_properties = {
|
|
visual = "mesh", mesh = "hearth_knife.obj", textures = {"hearth_person.png"},
|
|
-- The generated B3D and held OBJ both use model units (10 per node).
|
|
visual_size = {x = 1, y = 1, z = 1}, is_visible = false,
|
|
physical = false, collide_with_objects = false, pointable = false,
|
|
collisionbox = {0, 0, 0, 0, 0, 0}, selectionbox = {0, 0, 0, 0, 0, 0},
|
|
static_save = false, hp_max = 1, makes_footstep_sound = false,
|
|
},
|
|
on_activate = function(self, staticdata)
|
|
-- Only synchronous creation by presentation_step may bind an entity.
|
|
-- Restored/foreign/unowned entities disappear rather than becoming drops.
|
|
local record = pending
|
|
if staticdata ~= "" or not record or record.object or not live(record) then
|
|
self.object:remove(); return
|
|
end
|
|
record.object, self._held_record = self.object, record
|
|
self.object:set_armor_groups({immortal = 1})
|
|
end,
|
|
get_staticdata = function() return "" end,
|
|
on_step = function(self)
|
|
local record = self._held_record
|
|
if not record then self.object:remove(); return end
|
|
if records[record.name] ~= record or not live(record) or not attached(record) then remove(record) end
|
|
end,
|
|
on_deactivate = function(self)
|
|
local record = self._held_record
|
|
self._held_record = nil
|
|
if record then
|
|
if records[record.name] == record then records[record.name] = nil end
|
|
record.object = nil
|
|
end
|
|
end,
|
|
-- Cosmetic, non-pointable, immortal; even a direct punch has no gameplay effect.
|
|
on_punch = function() return true end,
|
|
on_rightclick = function() end,
|
|
})
|
|
|
|
function H.presentation_step(player)
|
|
local name = player:get_player_name()
|
|
local record, session = records[name], H.sessions[name]
|
|
local item = player:get_wielded_item():get_name()
|
|
local mesh = meshes[item]
|
|
if record then
|
|
if record.player == player and live(record) and attached(record) then return end
|
|
-- A deferred call on an old PlayerRef must not destroy the live replacement.
|
|
if record.player ~= player and C.get_player_by_name(name) ~= player then return end
|
|
remove(record)
|
|
end
|
|
if not mesh or not session then return end
|
|
record = {name = name, player = player, session = session, item = item}
|
|
if not live(record) then return end
|
|
records[name], pending = record, record
|
|
local ok, object = pcall(C.add_entity, player:get_pos(), entity_name)
|
|
pending = nil
|
|
if not ok or not object or object ~= record.object or not object:get_pos() then
|
|
remove(record); return
|
|
end
|
|
object:set_properties({mesh = mesh})
|
|
-- forced_visible is the FIFTH argument: false hides this from its owner in
|
|
-- first person, but permits third-person rendering. Never toggle is_visible
|
|
-- from a guessed server-side camera mode (it would hide it for observers).
|
|
object:set_attach(player, "Hand_Right", {x = 0, y = 0, z = 0}, {x = 90, y = 0, z = 0}, false)
|
|
if not attached(record) then remove(record); return end
|
|
object:set_properties({is_visible = true})
|
|
end
|
|
function H.presentation_cleanup(player)
|
|
local record = records[player:get_player_name()]
|
|
if record and record.player == player then remove(record) end
|
|
end
|
|
C.register_on_leaveplayer(H.presentation_cleanup)
|
|
C.register_on_dieplayer(H.presentation_cleanup)
|
|
C.register_on_shutdown(function()
|
|
local snapshot = {}; for _, record in pairs(records) do snapshot[#snapshot + 1] = record end
|
|
for _, record in ipairs(snapshot) do remove(record) end
|
|
end)
|
|
|
|
return H.presentation_step
|