guns4d-cd2025/classes/Gun-methods.lua
2025-01-03 16:14:00 -08:00

924 lines
42 KiB
Lua

-- @within Gun.gun
-- @compact
local gun_default = Guns4d.gun
local mat4 = leef.math.mat4
--I dont remember why I made this, used it though lmao
function gun_default.multiplier_coefficient(multiplier, ratio)
return 1+((multiplier*ratio)-ratio)
end
--- The entry method for the update of the gun
--
-- calls virtually all functions that begin with `update` once. Also updates subclass
--
-- @tparam float dt
function gun_default:update(dt)
assert(self.instance, "attempt to call object method on a class")
if not self:has_entity() then self:add_entity() self:clear_animation() end
--player look rotation. I'm going to keep it real, I don't remember what this math does. Player handler just stores the player's rotation from MT in degrees, which is for some reason inverted
--it's set up like this so that if the gun is fired on auto and the RPM is very fast (faster then globalstep) we know how many rounds to let off.
if self.rechamber_time > 0 then
self.rechamber_time = self.rechamber_time - dt
end
self.time_since_creation = self.time_since_creation + dt
self.time_since_last_fire = self.time_since_last_fire + dt
if self.burst_queue > 0 then self:update_burstfire() end
--update some vectors
self:update_look_offsets(dt)
--I should make this into a list
if self.consts.HAS_SWAY then self:update_sway(dt) end
if self.consts.HAS_RECOIL then self:update_recoil(dt) end
if self.consts.HAS_BREATHING then self:update_breathing(dt) end
if self.consts.HAS_WAG then self:update_wag(dt) end
self:update_animation(dt)
self.dir = self:get_dir(nil,nil,nil,self.consts.ANIMATIONS_OFFSET_AIM)
self.pos = self:get_pos()+self.handler:get_pos()
--update subclasses
self:update_entity()
--this should really be a list of subclasses so its more easily expansible
for i, instance in pairs(self.subclass_instances) do
if instance.update then instance:update(dt) end
if not self.properties.subclasses[i] then
instance:prepare_deletion()
self.subclass_instances[i] = nil
end
end
--finalize transforms
self:update_transforms()
end
function gun_default:regenerate_properties()
self._properties_unsafe = Guns4d.table.deep_copy(self.base_class.properties)
self.properties = self._properties_unsafe
for i, func in pairs(self.property_modifiers) do
func(self)
end
self.properties = leef.class.proxy_table.new(self.properties)
self:update_visuals()
end
--- not typically called every step, updates the gun object's visuals
function gun_default:update_visuals()
local props = self.properties
self.entity:set_properties({
mesh = props.visuals.mesh,
textures = table.copy(props.visuals.textures),
backface_culling = props.visuals.backface_culling,
visual_size = {x=10*props.visuals.scale,y=10*props.visuals.scale,z=10*props.visuals.scale}
})
for i, ent in pairs(self.attached_objects) do
if not self.properties.visuals.attached_objects[i] then
ent:remove()
end
end
for i, attached in pairs(self.properties.visuals.attached_objects) do
if attached.mesh then
assert(type(attached)=="table", self.name..": `attached.objects` expects a list of tables, incorrect type given.")
local obj
if (not self.attached_objects[i]) or (not self.attached_objects[i]:is_valid()) then
obj = minetest.add_entity(self.handler:get_pos(), "guns4d:gun_entity")
self.attached_objects[i] = obj
else
obj = self.attached_objects[i]
end
obj:set_properties({
mesh = attached.mesh,
textures = table.copy(attached.textures or self.properties.visuals.textures),
backface_culling = attached.backface_culling,
visual_size = {x=attached.scale or 1, y=attached.scale or 1, z=attached.scale or 1}
})
local offset
if attached.offset then
offset = attached.offset
offset = mat4.mul_vec4({}, self.b3d_model.root_orientation_rest_inverse, {offset.x, offset.y, offset.z, 0})
offset = {x=offset[1], y=offset[2], z=offset[3]}
end
local rotation
if attached.rotation then
rotation = attached.rotation
local rotm4 = mat4.set_rot_luanti_entity(mat4.identity(), rotation.x*math.pi/180, rotation.y*math.pi/180, rotation.z*math.pi/180)
rotm4 = self.b3d_model.root_orientation_rest_inverse*rotm4
rotation = {rotm4:get_rot_luanti_entity()}
rotation = {x=rotation[1]*180/math.pi, y=rotation[2]*180/math.pi, z=rotation[3]*180/math.pi}
else
rotation = {(self.b3d_model.root_orientation_rest_inverse):get_rot_luanti_entity()}
rotation = {x=rotation[1]*180/math.pi, y=rotation[2]*180/math.pi, z=rotation[3]*180/math.pi}
end
obj:set_attach(
self.entity,
self.consts.ROOT_BONE,
offset,
rotation
--true
)
else
minetest.log("error", "Guns4d: attached object had no mesh")
end
end
end
--- updates self.total_offsets which stores offsets for bones
function gun_default:update_transforms()
local total_offset = self.total_offsets
--axis rotations
total_offset.player_axial.x = 0; total_offset.player_axial.y = 0
total_offset.gun_axial.x = 0; total_offset.gun_axial.y = 0
--translations
total_offset.player_trans.x = 0; total_offset.player_trans.y = 0; total_offset.player_trans.z = 0
total_offset.gun_trans.x = 0; total_offset.gun_trans.y = 0; total_offset.gun_trans.z = 0
total_offset.look_trans.x = 0; total_offset.look_trans.y = 0; total_offset.look_trans.z = 0
--this doesnt work.
for type, _ in pairs(total_offset) do
for i, offset in pairs(self.offsets) do
if offset[type] and (self.consts.HAS_GUN_AXIAL_OFFSETS or type~="gun_axial") then
total_offset[type] = total_offset[type]+offset[type]
end
end
end
--total_offset.gun_axial.x = 0; total_offset.gun_axial.y = 0
end
--- Update and fire the queued weapon burst
function gun_default:update_burstfire()
if self.rechamber_time <= 0 then
local iter = 1
while true do
local success = self:attempt_fire()
if success then
self.burst_queue = self.burst_queue - 1
else
if not self.ammo_handler:can_spend_round() then
self.burst_queue = 0
end
break
end
iter = iter + 1
end
end
end
--- cycles to the next firemode of the weapon
function gun_default:cycle_firemodes()
--cannot get length using length operator because it's a proxy table
local length = 0
for i, v in ipairs(self.properties.firemodes) do
length = length+1
end
self.current_firemode = ((self.current_firemode)%(length))+1
self.meta:set_int("guns4d_firemode", self.current_firemode)
self:update_image_and_text_meta()
self.player:set_wielded_item(self.itemstack)
end
--- update the inventory information of the gun
function gun_default:update_image_and_text_meta(meta)
meta = meta or self.meta
local ammo = self.ammo_handler.ammo
--set the counter
if ammo.total_bullets == 0 then
meta:set_string("count_meta", Guns4d.config.empty_symbol)
else
if Guns4d.config.show_gun_inv_ammo_count then
meta:set_string("count_meta", tostring(ammo.total_bullets))
else
meta:set_string("count_meta", "F")
end
end
--pick the image
local image = self.properties.inventory.inventory_image
if (ammo.total_bullets > 0) and not ammo.magazine_psuedo_empty then
image = self.properties.inventory.inventory_image
elseif self.properties.inventory.inventory_image_magless and ( (ammo.loaded_mag == "empty") or (ammo.loaded_mag == "") or ammo.magazine_psuedo_empty) then
image = self.properties.inventory.inventory_image_magless
elseif self.properties.inventory.inventory_image_empty then
image = self.properties.inventory.inventory_image_empty
end
--add the firemode overlay to the image
local firemodes = 0
for i, v in pairs(self.properties.firemodes) do
firemodes = firemodes+1
end
if firemodes > 1 and self.properties.inventory.firemode_inventory_overlays[self.properties.firemodes[self.current_firemode]] then
image = image.."^"..self.properties.inventory.firemode_inventory_overlays[self.properties.firemodes[self.current_firemode]]
end
if self.handler.infinite_ammo then
image = image.."^"..self.properties.infinite_inventory_overlay
end
meta:set_string("inventory_image", image)
end
--- plays the draw animation and sound for the gun, delays usage.
function gun_default:draw()
assert(self.instance, "attempt to call object method on a class")
local props = self.properties
if props.visuals.animations[props.charging.draw_animation] then
self:set_animation(props.visuals.animations[props.charging.draw_animation], props.charging.draw_time)
end
if props.sounds[props.charging.draw_sound] then
local sounds = Guns4d.table.deep_copy(props.sounds[props.charging.draw_sound])
self:play_sounds(sounds)
end
self.ammo_handler:chamber_round()
self.rechamber_time = props.charging.draw_time
end
--- attempt to fire the gun
function gun_default:attempt_fire()
assert(self.instance, "attempt to call object method on a class")
local props = self.properties
--check if there could have been another round fired between steps.
if ( (self.rechamber_time + (60/props.firerateRPM) < 0) or (self.rechamber_time <= 0) ) and (not self.ammo_handler.ammo.magazine_psuedo_empty) then
local spent_bullet = self.ammo_handler:spend_round()
if spent_bullet and spent_bullet ~= "empty" then
local dir = self.dir
local pos = self.pos
if not Guns4d.ammo.registered_bullets[spent_bullet] then
minetest.log("error", "unregistered bullet itemstring"..tostring(spent_bullet)..", could not fire gun (player:"..self.player:get_player_name()..")");
return false
end
--begin subtasks
local bullet_def = Guns4d.table.fill(Guns4d.ammo.registered_bullets[spent_bullet], {
player = self.player,
--we don't want it to be doing fuckshit and letting players shoot through walls.
pos = pos-((self.handler.control_handler.ads and dir*props.ads.offset.z) or dir*props.hip.offset.z),
--dir = dir, this is now collected directly by calling get_dir so pellets and spread can be handled by the bullet_ray instance.
gun = self
})
Guns4d.bullet_ray:new(bullet_def)
if props.visuals.animations.fire then
self:set_animation(props.visuals.animations.fire, nil, false)
end
self:recoil()
self:muzzle_flash()
--this is gonna have to be optimized eventually because this system is complete ass.
local fire_sound = Guns4d.table.deep_copy(props.sounds.fire) --important that we copy because play_sounds modifies it.
fire_sound.pos = self.pos
self:play_sounds(fire_sound)
self.rechamber_time = self.rechamber_time + (60/props.firerateRPM)
--acount for animation rotation in same update firing
if (self.rechamber_time<(60/props.firerateRPM)) and props.firemodes[self.current_firemode]~="single" then
self.animation_data.runtime = self.animation_data.runtime + (60/props.firerateRPM)
self:update_animation_transforms()
end
return true
end
end
return false
end
--[[function gun_default:damage()
assert(self.instance, "attempt to call object method on a class")
self.itemstack:set_wear(self.itemstack:get_wear()-self.properties.durability.shot_per_wear)
self.player:set_wielded_item(self.itemstack)
end]]
local function rand_sign(b)
b = b or .5
local int = 1
if math.random() > b then int=-1 end
return int
end
--- simulate recoil by adding to the recoil velocity (called by attempt_fire)
function gun_default:recoil()
assert(self.instance, "attempt to call object method on a class")
local rprops = self.properties.recoil
for axis, recoil in pairs(self.velocities.recoil) do
for _, i in pairs({"x","y"}) do
recoil[i] = recoil[i] + (rprops.angular_velocity[axis][i]
*rand_sign((rprops.bias[axis][i]/2)+.5))
*self.multiplier_coefficient(rprops.hipfire_multiplier[axis], 1-self.control_handler.ads_location)
--set original velocity
self.velocities.init_recoil[axis][i] = recoil[i]
end
local length = math.sqrt(recoil.x^2+recoil.y^2)
if length > rprops.angular_velocity_max[axis] then
local co = rprops.angular_velocity_max[axis]/length
recoil.x = recoil.x*co
recoil.y = recoil.y*co
end
end
self.time_since_last_fire = 0
end
function gun_default:open_inventory_menu()
local props = self.properties
local player = self.player
local pname = player:get_player_name()
local inv = minetest.get_inventory({type="player", name=pname})
local window = minetest.get_player_window_information(pname)
local listname = Guns4d.config.inventory_listname
local form_dimensions = {x=20,y=15}
local inv_height=4+((4-1)*.125)
local hotbar_length = player:hud_get_hotbar_itemcount()
local form = "\
formspec_version[7]\
size[".. form_dimensions.x ..",".. form_dimensions.y .."]"
local hotbar_height = math.ceil(hotbar_length/8)
form = form.."\
scroll_container[.25,"..(form_dimensions.y)-inv_height-1.25 ..";10,5;player_inventory;vertical;.05]\
list[current_player;"..listname..";0,0;"..hotbar_length..","..hotbar_height..";]\
list[current_player;"..listname..";0,1.5;8,3;"..hotbar_length.."]\
scroll_container_end[]\
"
if math.ceil(inv:get_size("main")/8) > 4 then
local h = math.ceil(inv:get_size("main")/8)
form=form.."\
scrollbaroptions[max="..h+((h-1)*.125).."]\
scrollbar[10.25,"..(form_dimensions.y)-inv_height-1.25 ..";.5,5;vertical;player_inventory;0]\
"
end
--display gun preview
local len = math.abs(self.model_bounding_box[3]-self.model_bounding_box[6])/props.visuals.scale
local hei = math.abs(self.model_bounding_box[2]-self.model_bounding_box[5])/props.visuals.scale
local offsets = {x=(-self.model_bounding_box[6]/props.visuals.scale)-(len/2), y=(self.model_bounding_box[5]/props.visuals.scale)+(hei/2)}
local meter_scale = 15
local image_scale = meter_scale*(props.inventory.render_size or 1)
local gun_gui_offset = {x=0,y=-2.5}
form = form.."container["..((form_dimensions.x-image_scale)/2)+gun_gui_offset.x.. ","..((form_dimensions.y-image_scale)/2)+gun_gui_offset.y.."]"
if props.inventory.render_image then
form = form.."image["
..(offsets.x*meter_scale) ..","
..(offsets.y*meter_scale) ..";"
..image_scale..","
..image_scale..";"
..props.inventory.render_image.."]"
end
if self.part_handler then
--local attachment_inv = self.part_handler.virtual_inventory
if props.inventory.part_slots and self.part_handler then
for i, attachment in pairs(props.inventory.part_slots) do
form = form.."label["..(image_scale/2)+(attachment.formspec_offset.x or 0)-.75 ..","..(image_scale/2)+(-attachment.formspec_offset.y or 0)-.2 ..";"..(attachment.description or i).."]"
--list[<inventory location>;<list name>;<X>,<Y>;<W>,<H>;<starting item index>]
local width = attachment.slots or 1
width = width+((width-1)*.125)
form = form.."list[detached:guns4d_attachment_inv_"..pname..";"..i..";"..(image_scale/2)+(attachment.formspec_offset.x or 0)-(width/2)..","..(image_scale/2)+(-attachment.formspec_offset.y or 0)..";3,5;]"
end
end
end
form = form.."container_end[]"
minetest.show_formspec(self.handler.player:get_player_name(), "guns4d:inventory", form)
end
core.register_on_player_receive_fields(function(player, formname, fields)
if formname=="guns4d:inventory" and fields.quit then
local gun = Guns4d.players[player:get_player_name()].gun
gun:regenerate_properties()
end
end)
--- update the offsets of the player's look created by the gun
function gun_default:update_look_offsets(dt)
assert(self.instance, "attempt to call object method on a class")
local handler = self.handler
local look_rotation = handler.look_rotation --remember that this is in counterclock-wise rotation. For 4dguns we use clockwise so it makes a bit more sense for recoil. So it needs to be inverted.
local player_rot = self.player_rotation
player_rot.y = -handler.look_rotation.y
local rot_factor = Guns4d.config.vertical_rotation_factor*dt
rot_factor = rot_factor
local next_vert_aim = ((player_rot.x-look_rotation.x)/(1+rot_factor))+look_rotation.x --difference divided by a value and then added back to the original
if math.abs(look_rotation.x-next_vert_aim) > .005 then
player_rot.x = next_vert_aim
else
player_rot.x = look_rotation.x
end
local props = self.properties
local hip = props.hip
local ads = props.ads
if not handler.control_handler.ads then
--hipfire rotation offsets
local pitch = self.total_offsets.player_axial.x+player_rot.x
local gun_axial = self.offsets.look.gun_axial
local offset = handler.look_rotation.x-player_rot.x
gun_axial.x = Guns4d.math.clamp(offset, 0, 15*(offset/math.abs(offset)))
gun_axial.x = gun_axial.x+(pitch*(1-hip.axis_rotation_ratio))
self.offsets.look.player_axial.x = -pitch*(1-hip.axis_rotation_ratio)
self.offsets.look.look_trans.x = 0
else
self.offsets.look.gun_axial.x = 0
self.offsets.look.player_axial.x = 0
end
local location = Guns4d.math.clamp(Guns4d.math.smooth_ratio(self.control_handler.ads_location)*2, 0, 1)
self.offsets.look.look_trans.x = ads.horizontal_offset*location
local fwd_offset = 0
if look_rotation.x < 0 then --minetest's pitch is inverted, checking here if it's above horizon.
fwd_offset = math.abs(math.sin(look_rotation.x*math.pi/180))*props.ads.offset.z*location
end
self.offsets.look.player_trans.z = fwd_offset
self.offsets.look.look_trans.z = fwd_offset
end
--============================================== positional info =====================================
--all of this dir shit needs to be optimized HARD
--[[function gun_default:get_gun_axial_dir()
assert(self.instance, "attempt to call object method on a class")
local rotation = self.total_offsets
local dir = vector.new(vector.rotate({x=0, y=0, z=1}, {y=0, x=rotation.gun_axial.x*math.pi/180, z=0}))
dir = vector.rotate(dir, {y=rotation.gun_axial.y*math.pi/180, x=0, z=0})
return dir
end]]
--[[function gun_default:get_player_axial_dir(rltv)
assert(self.instance, "attempt to call object method on a class")
local handler = self.handler
local rotation = self.total_offsets
local dir = vector.new(vector.rotate({x=0, y=0, z=1}, {y=0, x=((rotation.player_axial.x)*math.pi/180), z=0}))
dir = vector.rotate(dir, {y=((rotation.player_axial.y)*math.pi/180), x=0, z=0})
if not rltv then
if (self.properties.subclasses.sprite_scope and handler.control_handler.ads) or (self.properties.subclasses.crosshair and not handler.control_handler.ads) then
--we need the head rotation in either of these cases, as that's what they're showing.
dir = vector.rotate(dir, {x=handler.look_rotation.x*math.pi/180,y=-handler.look_rotation.y*math.pi/180,z=0})
else
dir = vector.rotate(dir, {x=self.player_rotation.x*math.pi/180,y=self.player_rotation.y*math.pi/180,z=0})
end
end
return dir
end]]
local tmv3_rot = vector.new()
local tmv4_in = {0,0,0,1}
local tmv4_pivot_inv = {0,0,0,0}
local tmv4_offset = {0,0,0,1}
local tmv4_gun = {0,0,0,1}
local empty_vec = {x=0,y=0,z=0}
local ttransform = mat4.identity()
local out = vector.new() --reserve the memory, we still want to create new vectors each time though.
--gets the gun's position relative to the player. Relative indicates wether it's relative to the player's horizontal look
--offset is relative to the's rotation
--- get the global position of the gun. This is customized to rely on the assumption that there are 3-4 main rotations and 2-3 translations. If the behavior of the bones are changed this method may not work.
-- the point of this is to allow the user to find the gun's object origin as well as calculate where a given point should be offset given the parameters.
-- @tparam vec3 offset_pos
-- @tparam bool relative_y wether the y axis is relative to the player's look
-- @tparam bool relative_x wether the x axis is relative to the player's look
-- @tparam bool with_animation wether rotational and translational offsets from the animation are applied
-- @treturn vec3 position of gun (in global or local orientation) relative to the player's position
function gun_default:get_pos(offset, relative_y, relative_x, with_animation)
assert(self.instance, "attempt to call object method on a class")
--local player = self.player
local px = (relative_x and 0) or nil
local py = (relative_y and 0) or nil
local ax = ((not with_animation) and 0) or nil
local ay = ((not with_animation) and 0) or nil
local az = ((not with_animation) and 0) or nil
offset = offset or empty_vec
local gun_translation = self.gun_translation --needs a refactor
local root_transform = self.b3d_model.root_orientation_rest
--dir needs to be rotated twice seperately to avoid weirdness
local gun_scale = self.properties.visuals.scale
--generate rotation values based on our output
ttransform=self:get_rotation_transform(ttransform,nil,nil,nil,nil,nil,px,py,ax,ay,az)
--change the pivot of `offset` to the root bone by making our vector relative to it (basically setting it to origin)
tmv4_in[1], tmv4_in[2], tmv4_in[3] = offset.x-root_transform[13]*gun_scale, offset.y-root_transform[14]*gun_scale, offset.z-root_transform[15]*gun_scale
tmv4_offset = ttransform.mul_vec4(tmv4_offset, ttransform, tmv4_in) --rotate by our rotation transform
--to bring it back to global space we need to find what we offset it by in `ttransform`'s local space, so we apply the transform to it
tmv4_in[1], tmv4_in[2], tmv4_in[3] = root_transform[13]*gun_scale, root_transform[14]*gun_scale, root_transform[15]*gun_scale
tmv4_pivot_inv = ttransform.mul_vec4(tmv4_pivot_inv, ttransform, tmv4_in)
--quickly add together tmv4_offset+tmv4_pivot_inv to get the global position of the offset relative to the entity
tmv4_offset[1],tmv4_offset[2],tmv4_offset[3] = tmv4_offset[1]+tmv4_pivot_inv[1], tmv4_offset[2]+tmv4_pivot_inv[2], tmv4_offset[3]+tmv4_pivot_inv[3]
--get the position of the gun entity in global space relative to the bone which it is attached to.
ttransform=self:get_rotation_transform(ttransform, 0,0,0,nil,nil,px,py,ax,ay,az)
tmv4_in[1], tmv4_in[2], tmv4_in[3] = gun_translation.x, gun_translation.y, gun_translation.z
tmv4_gun = ttransform.mul_vec4(tmv4_gun, ttransform, tmv4_in)
--get the position of the bone globally
local bone_location = self.handler.player_model_handler.gun_bone_location
if relative_y then
out = vector.new(bone_location)
else
tmv3_rot.y = -self.handler.look_rotation.y*math.pi/180
out = vector.rotate(bone_location, tmv3_rot)
end
--add our global translations together
--bonepos + gunentity + gunoffset + animation offset
local anim = (with_animation and self.animation_translation) or empty_vec
out.x, out.y, out.z = out.x+anim.x+tmv4_gun[1]+tmv4_offset[1], out.y+anim.y+tmv4_gun[2]+tmv4_offset[2], out.z+anim.z+tmv4_gun[3]+tmv4_offset[3]
return out
end
local roll = mat4.identity() --roll offset (future implementation )
local lrot = mat4.identity() --local rotation offset
local grot = mat4.identity() --global rotation offset
local prot = mat4.identity() --global player rotation
local trad = math.pi/180
function gun_default:get_rotation_transform(out, lx,ly,lz,gx,gy,px,py,ax,ay,az)
--local pitch, global pitch etc.
local rotations = self.total_offsets
local arotation = self.animation_rotation
local protation = self.player_rotation
--eventually we want to INTERNALLY use radians, for now we have to do this though.
lz = lz or 0 --roll is currently unused.
ax, ay, az = ax or -arotation.x*trad, ay or -arotation.y*trad, az or -arotation.z*trad
lx, ly = lx or -rotations.gun_axial.x*trad, ly or -rotations.gun_axial.y*trad
gx, gy = gx or -rotations.player_axial.x*trad, gy or -rotations.player_axial.y*trad
px, py = px or -protation.x*trad, py or -protation.y*trad
--this doesnt account for the actual rotation of the player
--reset roll matrix
roll[1] = 1
roll[2] = 0
roll[5] = 0
roll[6] = 1
roll = mat4.rotate_Z(roll, lz+az)
--we use bone rotation because it uses the XYZ order. Overall order is "PGLA", player (ZXY)<-global_offset (XYZ)<-local_offset (XYZ)<-roll (Z)\
out = mat4.multiply(out, {prot:set_rot_luanti_entity(px, py, 0), grot:set_rot_irrlicht_bone(gx, gy, 0), lrot:set_rot_irrlicht_bone(lx+ax, ly+ay, 0), roll})
return out
end
local forward = {0,0,1,0}
local tmv4_out = {0,0,0,0}
-- get the direction for firing
function gun_default:get_dir(rltv, offx, offy, suppress_anim)
local rotations = self.total_offsets
local anim_x = (suppress_anim and 0) or nil
local anim_y = (suppress_anim and 0) or nil
local anim_z = (suppress_anim and 0) or nil
if rltv then
ttransform = self:get_rotation_transform(ttransform, (-rotations.gun_axial.x-(offx or 0) )*trad, (-rotations.gun_axial.y-(offy or 0))*trad, nil, nil, nil, 0, 0, anim_x,anim_y,anim_z)
else
local player_aim
if (self.properties.subclasses.sprite_scope and self.handler.control_handler.ads) or (self.properties.subclasses.crosshair and not self.handler.control_handler.ads) then
player_aim=self.player:get_look_vertical()
end
ttransform = self:get_rotation_transform(ttransform, (-rotations.gun_axial.x-(offx or 0))*trad, (-rotations.gun_axial.y-(offy or 0))*trad, nil, nil, nil, player_aim, nil, anim_x,anim_y,anim_z)
end
local tmv4 = ttransform.mul_vec4(tmv4_out, ttransform, forward)
local pos = vector.new(tmv4[1], tmv4[2], tmv4[3])
return pos
end
--=============================================== ENTITY ======================================================
--- adds the gun entity
function gun_default:add_entity()
assert(self.instance, "attempt to call object method on a class")
self.entity = minetest.add_entity(self.player:get_pos(), "guns4d:gun_entity")
local props = self.properties
Guns4d.gun_by_ObjRef[self.entity] = self
self:update_visuals()
end
local tmp_mat4_rot = mat4.identity()
local ip_time = Guns4d.config.gun_axial_interpolation_time
local ip_time2 = Guns4d.config.translation_interpolation_time
--- updates the gun's entity
function gun_default:update_entity()
local obj = self.entity
local player = self.player
local handler = self.handler
local props = self.properties
--attach to the correct bone, and rotate
local visibility = true
if self.subclass_instances.sprite_scope and self.subclass_instances.sprite_scope.hide_gun and (not (self.control_handler.ads_location == 0)) then
visibility = false
end
--Irrlicht uses counterclockwise but we use clockwise.
local ads = props.ads.offset
local hip = props.hip.offset
local offset = self.total_offsets.gun_trans
local ip = Guns4d.math.smooth_ratio(Guns4d.math.clamp(handler.control_handler.ads_location*2,0,1))
local ip_inv = 1-ip
local pos = self.gun_translation --entity directly dictates the translation of the gun
pos.x = (ads.x*ip)+(hip.x*ip_inv)+offset.x
pos.y = (ads.y*ip)+(hip.y*ip_inv)+offset.y
pos.z = (ads.z*ip)+(hip.z*ip_inv)+offset.z
local scale = self.properties.visuals.scale
--some complicated math to get client interpolation to work. It doesn't really account for the root bone having an (oriented) parent bone currently... hopefully that's not an issue.
local b3d = self.b3d_model
local rot = self:get_rotation_transform(tmp_mat4_rot, nil,nil,nil, 0,0, 0,0, 0,0,0)
tmp_mat4_rot = mat4.mul(tmp_mat4_rot, {b3d.root_orientation_rest_inverse, rot, b3d.root_orientation_rest})
local xr,yr,zr = tmp_mat4_rot:get_rot_irrlicht_bone()
obj:set_attach(player, handler.player_model_handler.bone_aliases.gun, nil, nil, visibility)
obj:set_bone_override(self.consts.ROOT_BONE, {
position = {
vec = {x=pos.x/scale, y=pos.y/scale, z=pos.z/scale},
interpolation = ip_time2,
},
rotation = {
vec = {x=xr,y=yr,z=zr},
interpolation = ip_time,
}
})
end
--- checks if the gun entity exists...
-- @treturn bool
function gun_default:has_entity()
assert(self.instance, "attempt to call object method on a class")
if not self.entity then return false end
if not self.entity:is_valid() then return false end
return true
end
--- updates the gun's wag offset for walking
-- @tparam float dt
function gun_default:update_wag(dt)
local handler = self.handler
local wag = self.offsets.walking
local velocity = wag.velocity
local props = self.properties
local old_tick
if handler.walking then
velocity = self.player:get_velocity()
wag.velocity = velocity
end
old_tick = old_tick or wag.tick
if velocity then
if handler.walking then
wag.tick = wag.tick + (dt*vector.length(velocity))
else
wag.tick = wag.tick + (dt*4)
end
end
local walking_offset = self.offsets.walking
if velocity and (not handler.walking) and (math.ceil(old_tick/props.wag.cycle_speed)+.5 < (math.ceil(wag.tick/props.wag.cycle_speed))+.5) and (wag.tick > old_tick) then
wag.velocity = nil
return
end
for _, i in ipairs({"x","y"}) do
for _, axis in ipairs({"player_axial", "gun_axial"}) do
if velocity then
local multiplier = 1
if i == "x" then
multiplier = 2
end
--if the result is negative we know that it's flipped, and thus can be ended.
local inp = (wag.tick/props.wag.cycle_speed)*math.pi*multiplier
--this is a mess, I think that 1.6 is the frequency of human steps or something
walking_offset[axis][i] = math.sin(inp)*self.properties.wag.offset[axis][i]
else
local old_value = walking_offset[axis][i]
if math.abs(walking_offset[axis][i]) > .005 then
local multiplier = 1/props.wag.decay_speed
walking_offset[axis][i] = walking_offset[axis][i]-(walking_offset[axis][i]*multiplier*dt)
else
walking_offset[axis][i] = 0
end
if math.abs(walking_offset[axis][i]) > math.abs(old_value) then
walking_offset[axis][i] = 0
end
end
end
end
end
local e = 2.7182818284590452353602874713527 --I don't know how to find it otherwise...
--- updates the gun's recoil simulation
-- @tparam float dt
function gun_default:update_recoil(dt)
for axis, _ in pairs(self.offsets.recoil) do
for _, i in pairs({"x","y"}) do
local recoil_vel = self.velocities.recoil[axis][i]
local recoil = self.offsets.recoil[axis][i]
recoil = recoil + recoil_vel
--this is modelled off a geometric sequence where the Y incercept of the sequence is set to recoil_vel.
local r = (10*self.properties.recoil.velocity_correction_factor[axis])^-1
local vel_co = e^-( (self.time_since_last_fire^2)/(2*r^2) )
recoil_vel = self.velocities.init_recoil[axis][i]*vel_co
if math.abs(recoil_vel) < 0.0001 then
recoil_vel = 0
end
self.velocities.recoil[axis][i] = recoil_vel
--ax^2+bx+c
--recoil_velocity_correction_rate
--recoil_correction_rate
local old_recoil = recoil
local abs = math.abs(recoil)
local sign = old_recoil/abs
if abs > 0.001 then
local correction_value = abs*self.time_since_last_fire*self.properties.recoil.target_correction_factor[axis]
correction_value = Guns4d.math.clamp(correction_value, 0, self.properties.recoil.target_correction_max_rate[axis])
abs=abs-(correction_value*dt)
--prevent overcorrection
if abs < 0 then
abs = 0
end
end
if sign~=sign then
sign = 1
end
self.offsets.recoil[axis][i] = abs*sign
end
end
end
--- updates the gun's animation data
-- @tparam dt
function gun_default:update_animation(dt)
--local ent = self.entity
local data = self.animation_data
data.runtime = data.runtime + dt
data.current_frame = Guns4d.math.clamp(data.current_frame+(dt*data.fps), data.frames.x, data.frames.y)
if data.loop and (data.current_frame > data.frames.y) then
data.current_frame = data.frames.x
end
self:update_animation_transforms()
end
--IMPORTANT!!! this does not directly modify the animation_data table anymore, it's all hooked through ObjRef:set_animation() (init.lua) so if animation is set elsewhere it doesnt break.
--this may be deprecated in the future- as it is no longer really needed now that I hook ObjRef functions.
--- sets the gun's animation in the same format as ObjRef:set_animation() (future deprecation?)
-- @tparam table frames `{x=int, y=int}`
-- @tparam float|nil length length of the animation in seconds
-- @tparam int fps frames per second of the animation
-- @tparam bool loop wether to loop
function gun_default:set_animation(frames, length, fps, loop)
loop = loop or false --why the fuck default is true? I DONT FUCKIN KNOW (this undoes this)
assert(type(frames)=="table" and frames.x and frames.y, "frames invalid or nil in set_animation()!")
assert(not (length and fps), "cannot play animation with both specified length and specified fps. Only one parameter can be used.")
local num_frames = math.abs(frames.x-frames.y)
if length then
fps = num_frames/length
elseif not fps then
fps = self.consts.DEFAULT_FPS
end
self.entity:set_animation(frames, fps, 0, loop) --see init.lua for modified ObjRef stuff.
end
--- clears the animation to the rest state
function gun_default:clear_animation()
local loaded = false
if self.properties.ammo.magazine_only then
if self.ammo_handler.ammo.loaded_mag ~= "empty" then
loaded = true
end
elseif self.ammo_handler.ammo.total_bullets > 0 then
loaded = true
end
if loaded then
self.entity:set_animation({x=self.properties.visuals.animations.loaded.x, y=self.properties.visuals.animations.loaded.y}, 0, 0, self.consts.LOOP_IDLE_ANIM)
else
self.entity:set_animation({x=self.properties.visuals.animations.empty.x, y=self.properties.visuals.animations.empty.y}, 0, 0, self.consts.LOOP_IDLE_ANIM)
end
end
local function adjust_gain(tbl, v)
v = tbl.third_person_gain_multiplier or v
for i = 1, #tbl do
adjust_gain(tbl[i], v)
end
if tbl.gain and (tbl.split_audio_by_perspective~=false) then
if type(tbl.gain) == "number" then
tbl.gain = tbl.gain*v
else
tbl.gain.min = tbl.gain.min*v
tbl.gain.max = tbl.gain.max*v
end
end
end
--- plays a list of sounds for the gun's user and thirdpersons
-- @tparam soundspec_list sound parameters following the format of @{Guns4d.play_sounds}
-- @treturn integer thirdperson sound's guns4d sound handle
-- @treturn integer firstperson sound's guns4d sound handle
function gun_default:play_sounds(sound)
local thpson_sound = Guns4d.table.deep_copy(sound)
local fsprsn_sound = Guns4d.table.deep_copy(sound)
thpson_sound.pos = self.pos
thpson_sound.player = self.player
thpson_sound.exclude_player = self.player
adjust_gain(thpson_sound, self.consts.THIRD_PERSON_GAIN_MULTIPLIER)
fsprsn_sound.player = self.player
fsprsn_sound.to_player = "from_player"
return Guns4d.play_sounds(thpson_sound), Guns4d.play_sounds(fsprsn_sound)
end
function gun_default:update_breathing(dt)
assert(self.instance)
local breathing_info = {pause=1.4, rate=4.2}
--we want X to be between 0 and 4.2. Since math.pi is a positive crest, we want X to be above it before it reaches our-
--"length" (aka rate-pause), thus it will pi/length or pi/(rate-pause) will represent out slope of our control.
local x = (self.time_since_creation%breathing_info.rate)*math.pi/(breathing_info.rate-breathing_info.pause)
local scale = self.properties.breathing_scale
--now if it's above math.pi we know it's in the pause half of the cycle. For smoothness, we cut the sine off early and decay the value non-linearly.
--not sure why 8/9 is a constant here... I assume it's if it's 8/9 of the way through the cycle. Not going to worry about it.
if x > math.pi*(8/9) then
self.offsets.breathing.player_axial.x=self.offsets.breathing.player_axial.x-(self.offsets.breathing.player_axial.x*2*dt)
else
self.offsets.breathing.player_axial.x = scale*(math.sin(x))
end
end
function gun_default:update_sway(dt)
assert(self.instance, "attempt to call object method from a base class")
local sprops = self.properties.sway
for axis, sway in pairs(self.offsets.sway) do
local sway_vel = self.velocities.sway[axis]
local ran
ran = vector.apply(vector.new(), function(i,v)
if i ~= "x" then
return (math.random()-.5)*2
end
end)
ran.z = 0
local vel_mul = self.multiplier_coefficient(sprops.hipfire_velocity_multiplier[axis], 1-self.control_handler.ads_location)
sway_vel = vector.normalize(sway_vel+(ran*dt))*sprops.angular_velocity[axis]*vel_mul
sway=sway+(sway_vel*dt)
local len_mul = self.multiplier_coefficient(sprops.hipfire_angle_multiplier[axis], 1-self.control_handler.ads_location)
if vector.length(sway) > sprops.max_angle[axis]*len_mul then
sway=vector.normalize(sway)*sprops.max_angle[axis]*len_mul
sway_vel = vector.new()
end
self.offsets.sway[axis] = sway
self.velocities.sway[axis] = sway_vel
end
end
--should merge these functions eventually...
function gun_default:update_animation_transforms()
local current_frame = self.animation_data.current_frame+self.consts.KEYFRAME_SAMPLE_PRECISION
local frame1 = math.floor(current_frame/self.consts.KEYFRAME_SAMPLE_PRECISION)
local frame2 = math.floor(current_frame/self.consts.KEYFRAME_SAMPLE_PRECISION)+1
current_frame = current_frame/self.consts.KEYFRAME_SAMPLE_PRECISION
local rotations = self.b3d_model.global_frames.root_rotation
local positions = self.b3d_model.global_frames.root_translation
local euler_rot
local trans
if not rotations[frame1] then --note that we are inverting the rotations, this is because b3d turns the wrong way or something? It might be an issue with LEEF idk.
euler_rot = vector.new(rotations[1]:get_euler_irrlicht_bone())*-1
else
local ip_ratio = (frame2 and (current_frame-frame1)/(frame2-frame1)) or 1
local vec1 = rotations[frame1]
local vec2 = rotations[frame2] or rotations[frame1]
euler_rot = vector.new(vec1:slerp(vec2, ip_ratio):get_euler_irrlicht_bone())*-180/math.pi
end
if not positions[frame1] then --note that we are inverting the rotations, this is because b3d turns the wrong way or something? It might be an issue with LEEF idk.
trans = positions[1]*-1
else
local ip_ratio = (frame2 and (current_frame-frame1)/(frame2-frame1)) or 1
local vec1 = positions[frame1]
local vec2 = positions[frame2] or positions[frame1]
trans = (vec1*(1-ip_ratio))+(vec2*ip_ratio)
end
self.animation_rotation = euler_rot
self.animation_translation = trans
end
--relative to the gun's entity. Returns left, right vectors.
local out = {arm_left=vector.new(), arm_right=vector.new()}
function gun_default:get_arm_aim_pos()
local current_frame = self.animation_data.current_frame
local frame1 = math.floor(current_frame/self.consts.KEYFRAME_SAMPLE_PRECISION)
local frame2 = math.floor(current_frame/self.consts.KEYFRAME_SAMPLE_PRECISION)+1
current_frame = current_frame/self.consts.KEYFRAME_SAMPLE_PRECISION
for i, v in pairs(out) do
if self.b3d_model.global_frames[i] then
if self.b3d_model.global_frames[i][frame1] then
if (not self.b3d_model.global_frames[i][frame2]) or (current_frame==frame1) then
out[i] = vector.copy(self.b3d_model.global_frames[i][frame1])
else --to stop nan
local ip_ratio = (current_frame-frame1)/(frame2-frame1)
local vec1 = self.b3d_model.global_frames[i][frame1]
local vec2 = self.b3d_model.global_frames[i][frame2]
out[i] = vec1+((vec1-vec2)*ip_ratio)
end
else
out[i]=vector.copy(self.b3d_model.global_frames[i][1])
end
else
out[i] = vector.new()
end
end
return out.arm_left, out.arm_right
--return vector.copy(self.b3d_model.global_frames.arm_left[1]), vector.copy(self.b3d_model.global_frames.arm_right[1])
end
--- ready the gun to be deleted
function gun_default:prepare_deletion()
self.released = true
assert(self.instance, "attempt to call object method on a class")
if self:has_entity() then self.entity:remove() end
for i, instance in pairs(self.subclass_instances) do
if instance.prepare_deletion then instance:prepare_deletion() end
end
end