363 lines
16 KiB
Lua
363 lines
16 KiB
Lua
|
|
--- player_model_handler
|
|
--
|
|
-- ## defining the player model when holding a gun
|
|
--
|
|
-- each player model should have a "gun holding equivelant". There are numerous reasons for this
|
|
-- first and foremost is that because Minetest is a [redacted mindless insults].
|
|
-- because of this you cannot unset bone offsets and return to normal animations.
|
|
-- Bone offsets are needed for the arms to aim at the gun there's no simple way around this fact.
|
|
-- Since every model is different custom behavior has to be defined for most.
|
|
--
|
|
-- @class player_model_handler
|
|
-- @compact
|
|
|
|
--- player_model_handler fields
|
|
-- @table fields
|
|
-- @field offsets @{fields.offsets}
|
|
Guns4d.player_model_handler = {
|
|
handlers = {}, --not for children, this stores a global list of handlers by meshname.
|
|
-- @table fields.offsets
|
|
offsets = {
|
|
-- a list of offsets relative to the whole model
|
|
global = {
|
|
--right arm (for hipfire bone)
|
|
},
|
|
-- a list of offsets relative to their parents (at rest position)
|
|
relative = { --none of these are specifically needed...
|
|
--left arm
|
|
--right arm
|
|
--head
|
|
},
|
|
},
|
|
|
|
--model generation attributes
|
|
override_bones = { --a list of bones to be read and or generated
|
|
Arm_Right = "guns4d_arm_right",
|
|
Arm_Left = "guns4d_arm_left",
|
|
Head = "guns4d_head"
|
|
},
|
|
new_bones = { --currently only supports empty bones. Sets at identity rotation, position 0, and parentless
|
|
"guns4d_gun_bone",
|
|
"guns4d_reflector_bone"
|
|
},
|
|
bone_aliases = { --names of bones used by the model handler and other parts of guns4d.
|
|
arm_right = "guns4d_arm_right", --this is a needed alias for hipfire position
|
|
arm_left = "guns4d_arm_left",
|
|
head = "guns4d_head",
|
|
|
|
gun = "guns4d_gun_bone", --another needed alias
|
|
reflector = "guns4d_reflector_bone"
|
|
},
|
|
still_frame = 0, --the frame to take bone offsets from. This system has to be improved in the future (to allow better animation support)- it works for now though.
|
|
auto_generate = true,
|
|
scale = 1, --this is important for autogen
|
|
output_path = minetest.get_modpath("guns4d").."/temp/",
|
|
compatible_meshes = { --list of meshes and their corresponding partner meshes for this handler. Must have the same bones used by guns4d. The first on this list will be read and have it's bone offsets logged for use.
|
|
--["character.b3d"] = "guns4d_character.b3d", this would tell the handler to use guns4d_character.b3d instead of generating a new one based on the override parameters.
|
|
["character.b3d"] = (leef.paths.media_paths["character.b3d"] and true) or nil, --it is compatible but it has no predefined model, one will be therefore generated using the override_bone_aliases parameters.
|
|
},
|
|
gun_bone_location = vector.new(),
|
|
fallback_mesh = "character.b3d", --if no meshes are found in "compatible_meshes" it chooses this index in "compatible_meshes"
|
|
is_new_default = true --this will set the this to be the default handler.
|
|
}
|
|
local player_model = Guns4d.player_model_handler
|
|
function player_model.set_default_handler(class_or_name)
|
|
assert(class_or_name, "class or mesh name (string) needed. Example: 'character.b3d' sets the default handler to whatever handler is used for character.b3d.")
|
|
local handler = assert(((type(class_or_name) == "class") and class_or_name) or player_model.get_handler(class_or_name), "no handler by the name '"..tostring(class_or_name).."' found.")
|
|
assert(not handler.instance, "cannot set instance of a handler as the default player_model_handler")
|
|
player_model.default_handler = handler
|
|
end
|
|
|
|
function player_model.get_handler(meshname)
|
|
local selected_handler = player_model.handlers[meshname] or player_model.main
|
|
if selected_handler then return selected_handler end
|
|
return player_model.default_handler
|
|
end
|
|
|
|
--[[function player_model:add_compatible_mesh(original, replacement)
|
|
assert(not self.instance, "attempt to call class method on an object. Cannot modify original class from an instance.")
|
|
assert(original and replacement, "one or more parameters missing")
|
|
self.compatible_meshes[original] = replacement
|
|
player_model.handlers[original] = self
|
|
end]]
|
|
|
|
--we store the read file so it can be reused in the constructor if needed.
|
|
|
|
-- main update function
|
|
function player_model:update(dt)
|
|
--assert(dt, "delta time (dt) not provided.")
|
|
--assert(self.instance, "attempt to call object method on a class")
|
|
self:update_aiming(dt)
|
|
self:update_head(dt)
|
|
self:update_arm_bones(dt)
|
|
self:update_look_offset(dt)
|
|
end
|
|
|
|
local ip_time = Guns4d.config.player_axial_interpolation_time
|
|
local ip_time2 = Guns4d.config.translation_interpolation_time
|
|
local ttransform = leef.math.mat4.identity()
|
|
function player_model:update_aiming(dt)
|
|
--gun bones:
|
|
local player = self.player
|
|
local handler = self.handler
|
|
local gun = handler.gun
|
|
local pprops = handler:get_properties()
|
|
local vs = pprops.visual_size
|
|
|
|
local player_trans = gun.total_offsets.player_trans
|
|
local hip_pos = self.offsets.global.arm_right
|
|
|
|
local ip = Guns4d.math.smooth_ratio(handler.control_handler.ads_location or 0)
|
|
local ip_inv = 1-ip
|
|
local pos = self.gun_bone_location --reuse allocated table
|
|
local xr,yr,zr = gun:get_rotation_transform(ttransform, nil, nil, nil, nil, nil, nil, 0, nil,nil,nil):get_rot_irrlicht_bone()
|
|
pos.x = ( (player_trans.x*10) + ((gun and gun.properties.ads.horizontal_offset*10) or 0 ))/vs.x
|
|
pos.y = ( (player_trans.y*10) + (pprops.eye_height*10) )/vs.y
|
|
pos.z = ( (player_trans.z*10) )/vs.z
|
|
player:set_bone_override(self.bone_aliases.reflector,
|
|
{
|
|
position = {
|
|
vec={x=pos.x, y=pos.y, z=pos.z},
|
|
interpolation=ip_time2,
|
|
absolute = true
|
|
},
|
|
rotation = {
|
|
vec={x=xr,y=yr,z=zr},
|
|
interpolation=ip_time,
|
|
absolute = true
|
|
}
|
|
})
|
|
--hip pos is already relative to local scale
|
|
pos.x = (hip_pos.x*10*ip_inv)+( (player_trans.x*10) + ((gun and gun.properties.ads.horizontal_offset*10*ip) or 0 ))/vs.x
|
|
pos.y = (hip_pos.y*10*ip_inv)+( (player_trans.y*10) + (pprops.eye_height*10*ip) )/vs.y
|
|
pos.z = (hip_pos.z*10*ip_inv)+( (player_trans.z*10) )/vs.z
|
|
|
|
xr,yr,zr = gun:get_rotation_transform(ttransform, 0,0,0, nil,nil, nil,0, 0,0,0):get_rot_irrlicht_bone()
|
|
player:set_bone_override(self.bone_aliases.gun,
|
|
{
|
|
position = {
|
|
vec={x=pos.x, y=pos.y, z=pos.z},
|
|
interpolation=ip_time2,
|
|
absolute = true
|
|
},
|
|
rotation = {
|
|
vec={x=xr,y=yr,z=zr},
|
|
interpolation=ip_time,
|
|
absolute = true
|
|
}
|
|
})
|
|
pos.x = (pos.x/10)*vs.x
|
|
pos.y = (pos.y/10)*vs.y
|
|
pos.z = (pos.z/10)*vs.z
|
|
-- minetest.chat_send_all(dump(pos))
|
|
end
|
|
|
|
function player_model:update_look_offset(dt)
|
|
local gun = self.handler.gun
|
|
self.player:set_eye_offset(gun.total_offsets.look_trans*10)
|
|
end
|
|
function player_model:unset_look_offset()
|
|
self.player:set_eye_offset()
|
|
end
|
|
--default arm code, compatible with MTG model.
|
|
function player_model:update_arm_bones(dt)
|
|
local player = self.player
|
|
local handler = self.handler
|
|
local gun = handler.gun
|
|
|
|
local pprops = handler:get_properties()
|
|
local left_bone, right_bone = vector.multiply(self.offsets.global.arm_left, pprops.visual_size), vector.multiply(self.offsets.global.arm_right, pprops.visual_size)
|
|
local left_trgt, right_trgt = gun:get_arm_aim_pos() --this gives us our offsets relative to the gun.
|
|
--get the real position of the gun's bones relative to the player (2nd param true)
|
|
left_trgt = gun:get_pos(left_trgt, true)
|
|
right_trgt = gun:get_pos(right_trgt, true)
|
|
local left_rotation = vector.dir_to_rotation(vector.direction(left_bone, left_trgt))
|
|
local right_rotation = vector.dir_to_rotation(vector.direction(right_bone, right_trgt))
|
|
--all of this is pure insanity. There's no logic, or rhyme or reason. Trial and error is the only way to write this garbo.
|
|
left_rotation.x = -left_rotation.x
|
|
right_rotation.x = -right_rotation.x
|
|
player:set_bone_override(self.bone_aliases.arm_right, {
|
|
rotation = {
|
|
vec={x=math.pi/2, y=0, z=0}-right_rotation,
|
|
absolute = true,
|
|
interpolation = .1
|
|
}
|
|
})
|
|
player:set_bone_override(self.bone_aliases.arm_left, {
|
|
rotation = {
|
|
vec={x=math.pi/2, y=0, z=0}-left_rotation,
|
|
absolute = true,
|
|
interpolation = .1
|
|
}
|
|
})
|
|
end
|
|
--updates the rotation of the head to match the gun.
|
|
function player_model:update_head(dt)
|
|
local player = self.player
|
|
local handler = self.handler
|
|
--player:set_bone_position(self.bone_aliases.head, self.offsets.relative.head, {x=handler.look_rotation.x,z=0,y=0})
|
|
player:set_bone_override(self.bone_aliases.head, {
|
|
rotation = {
|
|
vec={x=handler.look_rotation.x*math.pi/180,z=0,y=0},
|
|
absolute = true,
|
|
interpolation = .5
|
|
}
|
|
})
|
|
end
|
|
--should be renamed to "release" but, whatever.
|
|
function player_model:prepare_deletion()
|
|
assert(self.instance, "attempt to call object method on a class")
|
|
local handler = Guns4d.players[self.player:get_player_name()]
|
|
local properties = handler:get_properties()
|
|
--[[if minetest.get_modpath("player_api") then
|
|
player_api.set_model(self.player, self.old)
|
|
end]]
|
|
self:unset_look_offset()
|
|
local player = self.player
|
|
player:set_bone_override(self.bone_aliases.arm_left, {})
|
|
player:set_bone_override(self.bone_aliases.arm_right, {})
|
|
player:set_bone_override(self.bone_aliases.head, {})
|
|
properties.mesh = self.old
|
|
handler:set_properties(properties)
|
|
end
|
|
--todo: add value for "still_frame" (the frame to take samples from in case 0, 0 is not still.)
|
|
|
|
|
|
local model_buffer
|
|
local modpath = minetest.get_modpath("guns4d")
|
|
function player_model:custom_b3d_generation_parameters(b3d)
|
|
--empty for now, this is for model customizations.
|
|
return b3d
|
|
end
|
|
|
|
--generates a new guns4d model bases off of the `new_bones` and `bone_overrides` parameters if one does not already exist.
|
|
function player_model:generate_b3d_model(name)
|
|
assert(self and name, "error while generating a b3d model. Name not provided or not called as a method.")
|
|
--generate a new model
|
|
local filename = string.sub(name, 1, -5).."_guns4d_temp.b3d"
|
|
local new_path = self.output_path..filename
|
|
|
|
--buffer and modify the model
|
|
model_buffer = leef.b3d_reader.read_model(name)
|
|
local b3d = model_buffer
|
|
local replaced = {}
|
|
--add bone... forgot i made this so simple by adding node_paths
|
|
for _, node in pairs(b3d.node_paths) do
|
|
if self.override_bones[node.name] then
|
|
replaced[node.name] = true
|
|
--change the name
|
|
node.name = self.override_bones[node.name]
|
|
--unset rotation because it breaks shit
|
|
local rot = node.rotation
|
|
for i, v in pairs(node.keys) do
|
|
v.rotation = rot
|
|
end
|
|
--node.rotation = {0,0,0,1}
|
|
end
|
|
end
|
|
--check bones were replaced to avoid errors.
|
|
for i, v in pairs(self.override_bones) do
|
|
if (not replaced[i]) and i~="__replace_old_table" then
|
|
error("bone '"..i.."' not replaced with it's guns4d counterpart, bone was not found. Check bone name")
|
|
end
|
|
end
|
|
|
|
for i, v in pairs(self.new_bones) do
|
|
table.insert(b3d.node.children, {
|
|
name = v,
|
|
position = {0,0,0},
|
|
scale = {1/self.scale,1/self.scale,1/self.scale},
|
|
rotation = {0,0,0,1},
|
|
children = {},
|
|
bone = {} --empty works?
|
|
})
|
|
end
|
|
--call custom generation parameters...
|
|
b3d=self:custom_b3d_generation_parameters(b3d)
|
|
--write temp model
|
|
local writefile = io.open(new_path, "w+b")
|
|
leef.b3d_writer.write_model_to_file(b3d, writefile)
|
|
writefile:close()
|
|
|
|
--send to player media paths
|
|
minetest.after(0, function()
|
|
assert(
|
|
minetest.dynamic_add_media({filepath = new_path}, function()end),
|
|
"failed sending media"
|
|
)
|
|
end)
|
|
leef.paths.media_paths[filename] = new_path
|
|
leef.paths.modname_by_media[filename] = "guns4d"
|
|
return filename
|
|
end
|
|
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function player_model.construct(def)
|
|
if def.instance then
|
|
assert(def.player, "no player provided")
|
|
def.handler = Guns4d.players[def.player:get_player_name()]
|
|
--set the mesh
|
|
local properties = def.handler:get_properties()
|
|
def.old = properties.mesh
|
|
properties.mesh = def.compatible_meshes[properties.mesh]
|
|
def.gun_bone_location = vector.new()
|
|
if not properties.mesh then
|
|
local fallback = def.compatible_meshes[def.fallback_mesh]
|
|
minetest.log("error", "Player model handler error: no equivelant mesh found for '"..def.old.."'. Using fallback mesh ("..fallback..")")
|
|
properties.mesh = fallback
|
|
end
|
|
def.handler:set_properties(properties)
|
|
--no further aciton required, it e
|
|
-- character.b3d (from player_api) not present, ignore generation.
|
|
elseif (def~=player_model) or (minetest.get_modpath("player_api")) then
|
|
for og_mesh, replacement_mesh in pairs(def.compatible_meshes) do
|
|
assert(type(og_mesh)=="string", "mesh index to be replaced in compatible_meshes must be a string!")
|
|
if player_model.handlers[og_mesh] then minetest.log("warning", "Guns4d: mesh '"..og_mesh.."' overridden by a handler class, this will replace the old handler. Is this a mistake?") end
|
|
player_model.handlers[replacement_mesh] = def
|
|
end
|
|
|
|
--find a valid model to read.
|
|
if rawget(def, "auto_generate") then
|
|
--blame mod security, this is dumb.
|
|
assert(rawget(def, "output_path"), "a output path contained within the mod's source files is required to automatically generate models")
|
|
end
|
|
local read_model
|
|
for i, v in pairs(def.compatible_meshes) do
|
|
if type(i)=="string" then
|
|
if (v==true) then
|
|
if def.auto_generate then
|
|
def.compatible_meshes[i] = def:generate_b3d_model(i)
|
|
elseif i~=def.fallback_mesh then
|
|
def.compatible_meshes[i] = def.compatible_meshes[def.fallback_mesh]
|
|
else
|
|
error("improperly set list of compatible_meshes in a player_model_handler inherited class. Fallback mesh "..def.fallback_mesh.." has no assigned mesh and auto_generate is off")
|
|
end
|
|
end
|
|
read_model=def.compatible_meshes[i]
|
|
end
|
|
end
|
|
assert(read_model, "at least one compatible mesh required by default for autogeneration of offsets")
|
|
local b3d_table = leef.b3d_reader.read_model(read_model, true)
|
|
--[[all of the compatible_meshes should be identical in terms of guns4d specific bones and offsets (arms, head).
|
|
Otherwise a new handler should be different. With new compatibilities]]
|
|
for i, v in pairs(def.bone_aliases) do
|
|
local node = leef.b3d_nodes.get_node_by_name(b3d_table, v, true)
|
|
assert(node, "player model handler: no node found by the name of \""..v.."\" check that it is the correct value, or that it has been correctly overriden to use that name.")
|
|
local transform, _ = leef.b3d_nodes.get_node_global_transform(node, def.still_frame)
|
|
|
|
def.offsets.relative[i] = vector.new(node.position[1], node.position[2], node.position[3])
|
|
def.offsets.global[i] = vector.new(transform[13], transform[14], transform[15])/10 --4th column first 3 rows give us our global transform.
|
|
--print(i, leef.b3d_nodes.get_node_rotation(b3d_table, node, true, def.still_frame))
|
|
end
|
|
def.offsets.global.hipfire = vector.new(leef.b3d_nodes.get_node_global_position(b3d_table, def.bone_aliases.arm_right, true, def.still_frame))
|
|
|
|
if def.is_new_default then
|
|
player_model.set_default_handler(def)
|
|
end
|
|
end
|
|
end
|
|
Guns4d.player_model_handler = leef.class.new_class:inherit(player_model)
|
|
Guns4d.player_model_handler:set_default_handler()
|
|
|