added framework for a gun menu and inventories, created attachment manager

This commit is contained in:
FatalErr42O 2024-08-19 23:35:08 -07:00
parent 0a6c07cce1
commit e5906fee9e
15 changed files with 1266 additions and 868 deletions

13
attachments.lua Normal file
View File

@ -0,0 +1,13 @@
local attachment = {
attached_bone = "gun",
}
function attachment:construct()
if self.instance then
assert(self.gun, "attachment has no gun")
end
end
function attachment:update_entity()
self.entity =
end
Guns4d.gun_attachment = mtul.class.new_class:inherit(attachment)

View File

@ -1,4 +1,4 @@
Ammo_handler = mtul.class.new_class:inherit({
local Ammo_handler = mtul.class.new_class:inherit({
name = "Ammo_handler",
construct = function(def)
if def.instance then
@ -9,8 +9,9 @@ Ammo_handler = mtul.class.new_class:inherit({
local gun = def.gun
def.ammo = {}
if gun.properties.ammo then
--this is pretty inefficient it's been built on. Refactor later maybe.
local spawn_with = meta:get_int("guns4d_spawn_with_ammo")
if (meta:get_string("guns4d_loaded_bullets") == "") and ((spawn_with > 0) or (Guns4d.config.interpret_initial_wear_as_ammo))then
if (meta:get_string("guns4d_loaded_bullets") == "") and ((spawn_with > 0) or (Guns4d.config.interpret_initial_wear_as_ammo)) then
local bullets = (spawn_with > 0 and spawn_with) or (1-(def.gun.itemstack:get_wear()/65535))
if gun.properties.ammo.magazine_only then
local magname = gun.properties.ammo.accepted_magazines[1]
@ -47,6 +48,7 @@ Ammo_handler = mtul.class.new_class:inherit({
end
end
})
Guns4d.ammo_handler = Ammo_handler
--spend the round, return false if impossible.
--updates all properties based on the ammo table, bullets string can be passed directly to avoid duplication (when needed)
function Ammo_handler:update_meta(bullets)
@ -74,7 +76,7 @@ function Ammo_handler:get_magazine_bone_info()
local pos1 = vector.new(mtul.b3d_nodes.get_node_global_position(nil, node, true, math.floor(gun.animation_data.current_frame)))
local pos2 = vector.new(mtul.b3d_nodes.get_node_global_position(nil, node, true, gun.animation_data.current_frame))
local vel = (pos2-pos1)*((gun.animation_data.current_frame-math.floor(gun.animation_data.current_frame))/gun.animation_data.fps)+self.gun.player:get_velocity()
local pos = self.gun:get_pos(pos2/10)+self.gun.handler:get_pos()
local pos = self.gun:get_pos(pos2*gun.properties.visual_scale)+self.gun.handler:get_pos()
--[[so I tried to get the rotation before, and it actually turns out that was... insanely difficult? Why? I don't know, the rotation behavior was beyond unexpected, I tried implementing it with quats and
matrices and neither worked. I'm done, I've spent countless hours on this, and its fuckin stupid to spend a SECOND more on this pointless shit. It wouldnt even look that much better!]]
return pos, vel
@ -314,18 +316,3 @@ function Ammo_handler:unload_all(to_ground)
self.ammo.loaded_bullets = {}
self:update_meta()
end
function Ammo_handler:load_magless()
assert(self.instance, "attempt to call object method on a class")
end
function Ammo_handler:unload_magless()
assert(self.instance, "attempt to call object method on a class")
end
function Ammo_handler:load_fractional()
assert(self.instance, "attempt to call object method on a class")
end
function Ammo_handler:unload_fractional()
assert(self.instance, "attempt to call object method on a class")
end
function Ammo_handler:unload_chamber()
assert(self.instance, "attempt to call object method on a class")
end

View File

@ -0,0 +1,94 @@
--will have to merge with ammo_handler eventually for coherency.
local attachment_handler = mtul.class.new_class:inherit({})
Guns4d.attachment_handler = attachment_handler
function attachment_handler:construct()
assert(self.gun, "no gun object provided")
local meta = self.gun.meta
if self.instance then
self.modifier = {}
self.gun.property_modifiers = self.modifier
self.handler = self.gun.handler
if meta:get_string("guns4d_attachments") == "" then
self.attachments = {}
for i, v in pairs(self.gun.properties.inventory.attachment_slots) do
self.attachments[i] = {}
if type(v.default)=="string" then
self:add_attachment(v.default)
end
end
meta:set_string("guns4d_attachments", minetest.serialize(self.attachments))
else
self.attachments = minetest.deserialize(meta:get_string("guns4d_attachments"))
--self:update_meta()
end
end
end
Guns4d.registered_attachments = {}
function attachment_handler.register_attachment(def)
assert(def.itemstring, "itemstring field required")
--assert(def.modifier)
Guns4d.registered_attachments[def.itemstring] = def
end
function attachment_handler:rebuild_modifiers()
--rebuild the modifier
local new_mods = self.modifier
local index = 1
--replace indices with modifiers
for _, v in pairs(self.attachments) do
for name, _ in pairs(v) do
if Guns4d.registered_attachments[name].modifier then
new_mods[index]=Guns4d.registered_attachments[name].modifier
index = index + 1
end
end
end
--remove any remaining modifiers
if index < #new_mods then
for i=index, #new_mods do
new_mods[i]=nil
end
end
self.gun.property_modifiers["attachment_handler"] = self.modifier
end
--returns bool indicating success.
function attachment_handler:add_attachment(itemstack, slot)
assert(self.instance)
itemstack = ItemStack(itemstack)
local stackname = itemstack:get_name()
if self:can_add(itemstack, slot) then
self.attachments[slot][stackname] = itemstack
self:rebuild_modifiers()
return true
else
return false
end
end
function attachment_handler:can_add(itemstack, slot)
assert(self.instance)
local name = itemstack:get_name()
local props = self.gun.properties
print(slot, dump(self.attachments))
if Guns4d.registered_attachments[name] and (not self.attachments[slot][name]) and (props.inventory.attachment_slots[slot].allowed) then
--check if it's allowed, group check required
for i, v in pairs(props.inventory.attachment_slots[slot].allowed) do
print(v, name)
if v==name then
return true
end
end
else
return false
end
end
--returns bool indicating success.
function attachment_handler:remove_attachment(itemstack, slot)
assert(self.instance)
local stackname = itemstack:get_name()
if (self.attachments[slot][stackname]) then
self.attachments[slot][stackname] = nil
self:rebuild_modifiers()
else
return false
end
end

View File

@ -50,7 +50,7 @@ function controls:update(dt)
local gun = self.gun
if not (gun.rechamber_time > 0 and gun.ammo_handler.ammo.next_bullet == "empty") then --check if the gun is being charged.
for i, control in pairs(self:get_actions()) do
if not (i=="on_use") and not (i=="on_secondary_use") then
if (i~="on_use") and (i~="on_secondary_use") then
local def = control
local data = control.data
local conditions_met = true
@ -105,7 +105,7 @@ function controls:update(dt)
end
end
for i, tbl in pairs(call_queue) do
tbl.control.func(tbl.active, tbl.interrupt, tbl.data, busy_list, gun, self.handler)
tbl.control.func(self, tbl.active, tbl.interrupt, tbl.data, busy_list, gun, self.handler)
end
self.busy_list = {}
elseif self.busy_list then
@ -116,42 +116,47 @@ function controls:update(dt)
--if aiming, then increase ADS location
self.ads_location = Guns4d.math.clamp(self.ads_location + (dt/gun.properties.ads.aim_time), 0, 1)
elseif (not self.ads) and (self.ads_location>0) then
local divisor = gun.properties.ads.aim_time/gun.consts.AIM_OUT_AIM_IN_SPEED_RATIO
local divisor = gun.properties.ads.aim_time/Guns4d.config.aim_out_multiplier
self.ads_location = Guns4d.math.clamp(self.ads_location - (dt/divisor), 0, 1)
end
end
--builtin overrides for the item
function controls:on_use(itemstack, pointed_thing)
assert(self.instance, "attempt to call object method on a class")
local actions = self:get_actions()
if actions.on_use then
actions.on_use(itemstack, self.handler, pointed_thing)
actions.on_use(self, itemstack, self.handler, pointed_thing, self.busy_list)
end
end
function controls:on_drop(itemstack, pointed_thing, pos)
local actions = self:get_actions()
if actions.on_drop then
return actions.on_use(itemstack, self.handler, pos)
return actions.on_use(self, itemstack, self.handler, pos, self.busy_list)
end
end
function controls:on_secondary_use(itemstack, pointed_thing)
assert(self.instance, "attempt to call object method on a class")
local actions = self:get_actions()
if actions.on_secondary_use then
actions.on_secondary_use(itemstack, self.handler, pointed_thing)
actions.on_secondary_use(self, itemstack, self.handler, pointed_thing, self.busy_list)
end
end
--touchscreen mode, work in progress.
---@diagnostic disable-next-line: duplicate-set-field
function controls:toggle_touchscreen_mode(active)
if active~=nil then self.touchscreen=active else self.touchscreen = not self.touchscreen end
self.handler.touchscreen = self.touchscreen
for i, action in pairs((self.touchscreen and self.actions_pc) or self.actions_touch) do
if not (i=="on_use") and not (i=="on_secondary_use") then
if (i~="on_use") and (i~="on_secondary_use") then
action.timer = action.timer or 0
action.data = nil --no need to store excess data
end
end
for i, action in pairs((self.touchscreen and self.actions_touch) or self.actions_pc) do
if not (i=="on_use") and not (i=="on_secondary_use") then
if(i~="on_use") and (i~="on_secondary_use") then
action.timer = action.timer or 0
action.data = {
timer = action.timer,

View File

@ -92,13 +92,18 @@ function gun_default:construct_instance()
--unavoidable table instancing
self.properties = Guns4d.table.fill(self.base_class.properties, self.properties)
self.particle_spawners = {} --mtul.class.new_class only shallow copies. So tables will not change, and thus some need to be initialized.
self.property_modifiers = {}
self.particle_spawners = {}
self.property_modifiers = {}
initialize_animation(self)
initialize_physics(self)
--properties have been assigned, create necessary objects TODO: completely change this system for selfining them.
if self.properties.inventory.attachment_slots then
self.attachment_handler = self.properties.attachment_handler:new({
gun = self
})
end
if self.properties.sprite_scope then
self.sprite_scope = self.properties.sprite_scope:new({
gun = self
@ -152,7 +157,7 @@ local function validate_controls(props)
end
end
local function initialize_b3d_animation_data(self, props)
self.b3d_model = mtul.b3d_reader.read_model(props.visuals.mesh, true)
self.b3d_model = mtul.b3d_reader.read_model(props.visuals.mesh)
self.b3d_model.global_frames = {
arm_right = {}, --the aim position of the right arm
arm_left = {}, --the aim position of the left arm
@ -167,18 +172,19 @@ local function initialize_b3d_animation_data(self, props)
for target_frame = 0, self.b3d_model.node.animation.frames+1, self.consts.KEYFRAME_SAMPLE_PRECISION do
--we need to check that the bone exists first.
if left then
table.insert(self.b3d_model.global_frames.arm_left, vector.new(mtul.b3d_nodes.get_node_global_position(self.b3d_model, left, nil, target_frame))/10)
table.insert(self.b3d_model.global_frames.arm_left, vector.new(mtul.b3d_nodes.get_node_global_position(self.b3d_model, left, nil, target_frame))*props.visuals.scale)
else
self.b3d_model.global_frames.arm_left = nil
end
if right then
table.insert(self.b3d_model.global_frames.arm_right, vector.new(mtul.b3d_nodes.get_node_global_position(self.b3d_model, right, nil, target_frame))/10)
table.insert(self.b3d_model.global_frames.arm_right, vector.new(mtul.b3d_nodes.get_node_global_position(self.b3d_model, right, nil, target_frame))*props.visuals.scale)
else
self.b3d_model.global_frames.arm_right = nil
end
if main then
--ATTENTION: this is broken, roll is somehow translating to yaw. How? fuck if I know, but I will have to fix this eventually.
--use -1 as it does not exist and thus will always go to the default resting pose
--we compose it by the inverse because we need to get the global CHANGE in rotation for the animation rotation offset. I really need to comment more often
local newvec = (mtul.b3d_nodes.get_node_rotation(self.b3d_model, main, nil, -1):inverse())*mtul.b3d_nodes.get_node_rotation(self.b3d_model, main, nil, target_frame)
@ -187,14 +193,38 @@ local function initialize_b3d_animation_data(self, props)
end
end
--[[if main then
local quat = mtul.math.quat.new(main.keys[1].rotation)
print(dump(main.keys[1]), vector.new(quat:to_euler_angles_unpack(quat)))
local verts = {}
self.bones = {}
--iterate all nodes, check for meshes.
for i, v in pairs(self.b3d_model.node_paths) do
if v.mesh then
--if there's a mesh present transform it's verts into global coordinate system, add add them to them to a big list.
local transform, _ = mtul.b3d_nodes.get_node_global_transform(v, self.properties.visuals.animations.loaded.x, "transform")
for _, vert in ipairs(v.mesh.vertices) do
vert.pos[4]=1
table.insert(verts, transform*vert.pos)
end
end
end
for i, v in pairs(self.b3d_model.global_frames.rotation) do
print(i, dump(vector.new(v:to_euler_angles_unpack())*180/math.pi))
end]]
--print()
local high_points = {0,0,0,0,0,0}
for _, v in pairs(verts) do
for i = 1,3 do
if high_points[i+3] > v[i] then
high_points[i+3]=v[i]
end
if high_points[i] < v[i] then
high_points[i]=v[i]
end
end
end
for i=1,6 do
high_points[i]=high_points[i]*self.properties.visuals.scale
end
self.model_bounding_box = high_points
self.properties.item = {
collisionbox = {.2, high_points[2], .2, -.2, high_points[5], -.2},
selectionbox = {high_points[1]*3, high_points[2], high_points[3], high_points[4]*3, high_points[5], high_points[6]}
}
end
local function reregister_item(self, props)
assert(self.itemstring, "no itemstring provided. Cannot create a gun without an associated itemstring.")
@ -224,7 +254,10 @@ local function reregister_item(self, props)
end
end,
on_drop = function(itemstack, user, pos)
local cancel_drop = Guns4d.players[user:get_player_name()].control_handler:on_drop(itemstack)
local cancel_drop
if Guns4d.players[user:get_player_name()].control_handler then
cancel_drop = Guns4d.players[user:get_player_name()].control_handler:on_drop(itemstack)
end
if (not cancel_drop) and old_on_drop then
return old_on_drop(itemstack, user, pos)
end
@ -233,27 +266,12 @@ local function reregister_item(self, props)
Guns4d.register_item(self.itemstring, {
collisionbox = self.properties.item.collisionbox,
selectionbox = self.properties.item.selectionbox,
visual_size = 10*self.properties.visuals.scale,
mesh = self.properties.visuals.mesh,
textures = self.properties.visuals.textures,
animation = self.properties.visuals.animations.loaded
})
end
local function register_visual_entity(def, props)
minetest.register_entity(def.name.."_visual", {
initial_properties = {
visual = "mesh",
mesh = props.visuals.mesh,
textures = props.visuals.textures,
glow = 0,
pointable = false,
static_save = false,
backface_culling = props.visuals.backface_culling
},
on_step = function(self)
if not self.object:get_attach() then self.object:remove() end
end
})
end
--========================== MAIN CLASS CONSTRUCTOR ===============================
function gun_default:construct_base_class()
@ -284,5 +302,4 @@ function gun_default:construct_base_class()
self.properties = mtul.class.proxy_table:new(self.properties)
Guns4d.gun._registered[self.name] = self --add gun self to the registered table
register_visual_entity(self, props) --register the visual entity
end

667
classes/Gun-methods.lua Normal file
View File

@ -0,0 +1,667 @@
local gun_default = Guns4d.gun
--I dont remember why I made this, used it though lmao
function gun_default.multiplier_coefficient(multiplier, ratio)
return 1+((multiplier*ratio)-ratio)
end
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
--update gun, the main function.
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
local handler = self.handler
--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
--timers
if self.rechamber_time > 0 then
self.rechamber_time = self.rechamber_time - dt
else
self.rechamber_time = 0
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)
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()
self.local_dir = self:get_dir(true)
self.paxial_dir = self:get_player_axial_dir()
self.local_paxial_dir = self:get_player_axial_dir(true)
self.pos = self:get_pos()+self.handler:get_pos()
self:update_entity()
if self.properties.sprite_scope then
self.sprite_scope:update()
end
if self.properties.crosshair then
self.crosshair:update()
end
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
end
--update modifiers
--manage burstfire
function gun_default:update_burstfire()
if self.rechamber_time <= 0 then
local success = self:attempt_fire()
if not success then
self.burst_queue = 0
else
self.burst_queue = self.burst_queue - 1
end
end
end
--cycle firemodes, typically activated by default_controls.lua.
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
--remember to set_wielded_item to self.itemstack! otherwise these changes will not apply!
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_image
if (ammo.total_bullets > 0) and not ammo.magazine_psuedo_empty then
image = self.properties.inventory_image
elseif self.properties.inventory_image_magless and ( (ammo.loaded_mag == "empty") or (ammo.loaded_mag == "") or ammo.magazine_psuedo_empty) then
image = self.properties.inventory_image_magless
elseif self.properties.inventory_image_empty then
image = self.properties.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.firemode_inventory_overlays[self.properties.firemodes[self.current_firemode]] then
image = image.."^"..self.properties.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
function gun_default:attempt_fire()
assert(self.instance, "attempt to call object method on a class")
if 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
local props = self.properties
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()
--[[if props.durability.shot_per_wear then
self:damage()
end]]
--print(dump(self.properties.sounds.fire))
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 = 60/props.firerateRPM
return true
end
end
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
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: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.sprite_scope and handler.control_handler.ads) or (self.properties.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
--this needs to be optimized because it may be called frequently...
function gun_default:get_dir(rltv, offset_x, offset_y)
assert(self.instance, "attempt to call object method on a class")
local rotation = self.total_offsets
local handler = self.handler
--rotate x and then y.
--used symbolab.com to precalculate the rotation matrices to save on performance since spread for pellets has to run this.
local p = -(rotation.gun_axial.x+rotation.player_axial.x+(offset_x or 0))*math.pi/180
local y = -(rotation.gun_axial.y+rotation.player_axial.y+(offset_y or 0))*math.pi/180
local Cy = math.cos(y)
local Sy = math.sin(y)
local Cp = math.cos(p)
local Sp = math.sin(p)
local dir = {
x=Sy*Cy,
y=-Sp,
z=Cy*Cp
}
if not rltv then
p = -self.player_rotation.x*math.pi/180
y = -self.player_rotation.y*math.pi/180
Cy = math.cos(y)
Sy = math.sin(y)
Cp = math.cos(p)
Sp = math.sin(p)
dir = vector.new(
(Cy*dir.x)+(Sy*Sp*dir.y)+(Sy*Cp*dir.z),
(dir.y*Cp)-(dir.z*Sp),
(-dir.x*Sy)+(dir.y*Sp*Cy)+(dir.z*Cy*Cp)
)
else
dir = vector.new(dir)
end
return dir
end
--Should probably optimize this at some point.
local zero = vector.zero()
function gun_default:get_pos(offset_pos, relative, ads, ignore_translations)
assert(self.instance, "attempt to call object method on a class")
local player = self.player
local handler = self.handler
local bone_location = handler.player_model_handler.gun_bone_location
local gun_translation = self.gun_translation
if offset_pos then
gun_translation = gun_translation+offset_pos
end
if gun_translation==self.gun_translation then gun_translation = vector.new(gun_translation) end
--dir needs to be rotated twice seperately to avoid weirdness
local pos
if not relative then
pos = vector.rotate(bone_location, {x=0, y=-handler.look_rotation.y*math.pi/180, z=0})
pos = pos+vector.rotate(gun_translation, vector.dir_to_rotation(self.paxial_dir))
else
pos = vector.rotate(gun_translation, vector.dir_to_rotation(self.local_paxial_dir)+{x=self.player_rotation.x*math.pi/180,y=0,z=0})+bone_location
end
--[[local hud_pos
if relative then
hud_pos = vector.rotate(pos, {x=0,y=player:get_look_horizontal(),z=0})+handler:get_pos()
else
hud_pos = pos+handler:get_pos()
end]]
if minetest.get_player_by_name("fatal2") then
--[[local hud = minetest.get_player_by_name("fatal2"):hud_add({
hud_elem_type = "image_waypoint",
text = "muzzle_flash2.png",
world_pos = hud_pos,
scale = {x=10, y=10},
alignment = {x=0,y=0},
offset = {x=0,y=0},
})
minetest.after(0, function(hud)
minetest.get_player_by_name("fatal2"):hud_remove(hud)
end, hud)]]
end
--world pos, position of bone, offset of gun from bone (with added_pos)
return pos
end
--=============================================== 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
self.entity:set_properties({
mesh = props.visuals.mesh,
textures = 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}
})
Guns4d.gun_by_ObjRef[self.entity] = self
--obj:on_step()
--self:update_entity()
end
function gun_default:update_entity()
local obj = self.entity
local player = self.player
local axial_rot = self.total_offsets.gun_axial
local handler = self.handler
local props = self.properties
--attach to the correct bone, and rotate
local visibility = true
if self.sprite_scope and self.sprite_scope.hide_gun and (not (self.control_handler.ads_location == 0)) then
visibility = false
end
--Irrlicht uses counterclockwise but we use clockwise.
local pos = self.gun_translation
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
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
self.gun_translation = pos
obj:set_attach(player, handler.player_model_handler.bone_aliases.gun, {x=pos.x*10, y=pos.y*10, z=pos.z*10}, -axial_rot, visibility)
end
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:get_pos() then return false end
return true
end
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...
function gun_default:update_recoil(dt)
for axis, _ in pairs(self.offsets.recoil) do
for _, i in pairs({"x","y"}) do
local recoil = self.offsets.recoil[axis][i]
local recoil_vel = Guns4d.math.clamp(self.velocities.recoil[axis][i],-self.properties.recoil.angular_velocity_max[axis],self.properties.recoil.angular_velocity_max[axis])
local old_recoil_vel = recoil_vel
recoil = recoil + recoil_vel
--this is modelled off a geometric sequence where the Y incercept of the sequence is set to recoil_vel.
if math.abs(recoil_vel) > 0.001 then
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
else
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
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
--track rotations and applies to aim.
if self.consts.ANIMATIONS_OFFSET_AIM then self:update_animation_rotation() end
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.
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
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
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_rotation()
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 out
if self.b3d_model.global_frames.rotation then
if self.b3d_model.global_frames.rotation[frame1] then
if (not self.b3d_model.global_frames.rotation[frame2]) or (current_frame==frame1) then
out = vector.new(self.b3d_model.global_frames.rotation[frame1]:to_euler_angles_unpack())*180/math.pi
--print("rawsent")
else --to stop nan
local ip_ratio = (current_frame-frame1)/(frame2-frame1)
local vec1 = self.b3d_model.global_frames.rotation[frame1]
local vec2 = self.b3d_model.global_frames.rotation[frame2]
out = vector.new(vec1:slerp(vec2, ip_ratio):to_euler_angles_unpack())*180/math.pi
end
else
out = vector.copy(self.b3d_model.global_frames.rotation[1])
end
--print(frame1, frame2, current_frame, dump(out))
else
out = vector.new()
end
self.animation_rotation = out
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+1
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]
--print(current_frame, frame1, frame2, ip_ratio)
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
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
if self.sprite_scope then self.sprite_scope:prepare_deletion() end
if self.crosshair then self.crosshair:prepare_deletion() end
end

View File

@ -28,10 +28,18 @@ local Vec = vector
local gun_default = {
--- `string` the name of the gun. Set to __template for guns which have no instances.
name = "__guns4d:default__",
--- `ItemStack` itemstack held by the player
--- `ItemStack` the gun itemstack. Remember to player:set_wielded_item(self.itemstack) when making meta or itemstack changes.
itemstack = nil,
--- `MetaDataRef` itemstack meta
meta = nil,
--- `string` the ID of the gun used for tracking of it's inventory
id = nil,
--- `ObjRef` the gun entity
gun_entity = nil,
--- `string` inventory image for when the gun has no magazine
inventory_image_magless = nil,
--- `string` inventory image for when the gun is loaded. This is added automatically during construction.
inventory_image = nil,
--- `string` the itemstring of the gun- i.e. "guns4d_pack_1:m4". Set to "" for __template guns.
itemstring = "",
--- list of registered guns, **DO NOT MODIFY** I really need a metatable for this class...
@ -56,6 +64,8 @@ local gun_default = {
muzzle_flash = Guns4d.effects.muzzle_flash,
--- `vec3` translation of the gun relative to the "gun" bone or the player axial rotation.
gun_translation = vector.new(),
--- `table` indexed list of modifiers not set by the gun but to be applied to the gun. After changing, gun:update_modifiers() must be called to update it. Also may contain lists of modifiers.
property_modifiers = nil,
--- properties
--
@ -80,11 +90,11 @@ local gun_default = {
flash_offset = Vec.new(),
--- `int`=600 The number of rounds (cartidges) this gun can throw per minute. Used by update(), fire() and default controls
firerateRPM = 600,
--- the item entity's attributes. This will later include held item definition...
item = {
--- the item entity's attributes. [DOCUMENTATION NEEDED]
--[[item = {
collisionbox = ((not Guns4d.config.realistic_items) and {-.1,-.1,-.1, .1,.1,.1}) or {-.1,-.05,-.1, .1,.15,.1},
selectionbox = {-.1,-.1,-.1, .1,.1,.1}
},
},]]
--- properties.hip
-- @table gun.properties.hip
-- @compact
@ -252,6 +262,31 @@ local gun_default = {
on_secondary_use = Guns4d.default_touch_controls.on_secondary_use,
firemode = Guns4d.default_touch_controls.firemode
},
--[[ parts framework coming soon. example for m4
parts = {
barrel = {
operable_without = false,
group = "guns4d_m4_barrel"
default = "guns4d:m4_15in"
}
}
]]
inventory = {
--[[attachment_slots = {
underbarrel = {
formspec_inventory_location = {x=0, y=1}
slots = 2,
rail = "picatinny" --only attachments fit for this type will be usable.
allowed = {
"group:guns4d_underbarrel"
},
bone = "" --the bone both to attach to and to display at on the menu.
}
},]]
render_size = 2, --length (in meters) which is visible accross the z/forward axis at y/up=0, x=0. For orthographic this will be the scale of the orthographic camera.
render_image = "m4_ortho.png", --expects an image of the right side of the gun, where the gun is facing the right.
--rendered_from_model = true --if true the rendering is automatically moved to the center of the screen
},
--- properties.charging
--
-- @table gun.properties.charging
@ -300,6 +335,12 @@ local gun_default = {
-- @table gun.properties.visuals
-- @compact
visuals = {
--- name of mesh to display
mesh = nil,
--- list of textures to use
textures = {},
--- scale multiplier
scale = 1,
--- toggles backface culling
backface_culling = true,
--- a table of animations in the format {x=int, y=float}. Indexes define the name of the animation to be refrenced by other functions of the gun.
@ -308,6 +349,8 @@ local gun_default = {
loaded = {x=1,y=1},
},
},
--- a table {x1,y1,z1,x2,y2,z2} specifying the bounding box of the model. The first 3 (x1,y1,z1) are the lower of their counterparts
model_bounding_box = nil,
--- a table of @{guns4d_soundspec|soundspecs} to be referenced by other functions
sounds = { --this does not contain reload sound effects.
fire = {
@ -340,7 +383,8 @@ local gun_default = {
}
},
},
ammo_handler = Ammo_handler,
ammo_handler = Guns4d.ammo_handler,
attachment_handler = Guns4d.attachment_handler,
sprite_scope = nil,
crosshair = nil,
initial_vertical_rotation = -60,
@ -423,8 +467,6 @@ local gun_default = {
-- @table lvl1_fields.consts
-- @compact
consts = {
---
AIM_OUT_AIM_IN_SPEED_RATIO = 2.5,
--- frequency of keyframe samples for animation offsets and
KEYFRAME_SAMPLE_PRECISION = .1,
--- default max hear distance when not specified
@ -458,666 +500,25 @@ local gun_default = {
},
}
--I dont remember why I made this, used it though lmao
function gun_default.multiplier_coefficient(multiplier, ratio)
return 1+((multiplier*ratio)-ratio)
end
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)
minetest.register_entity("guns4d:gun_entity", {
initial_properties = {
visual = "mesh",
mesh = "",
textures = {},
glow = 0,
pointable = false,
static_save = false,
visual_size = {x=10,y=10,z=10},
backface_culling = false
},
on_step = function(self)
if not self.object:get_attach() then self.object:remove() end
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
--update gun, the main function.
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
local handler = self.handler
--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
--timers
if self.rechamber_time > 0 then
self.rechamber_time = self.rechamber_time - dt
else
self.rechamber_time = 0
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)
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()
self.local_dir = self:get_dir(true)
self.paxial_dir = self:get_player_axial_dir()
self.local_paxial_dir = self:get_player_axial_dir(true)
self.pos = self:get_pos()+self.handler:get_pos()
self:update_entity()
if self.properties.sprite_scope then
self.sprite_scope:update()
end
if self.properties.crosshair then
self.crosshair:update()
end
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
end
function gun_default:update_burstfire()
if self.rechamber_time <= 0 then
local success = self:attempt_fire()
if not success then
self.burst_queue = 0
else
self.burst_queue = self.burst_queue - 1
end
end
end
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
--remember to set_wielded_item to self.itemstack! otherwise these changes will not apply!
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_image
if (ammo.total_bullets > 0) and not ammo.magazine_psuedo_empty then
image = self.properties.inventory_image
elseif self.properties.inventory_image_magless and ( (ammo.loaded_mag == "empty") or (ammo.loaded_mag == "") or ammo.magazine_psuedo_empty) then
image = self.properties.inventory_image_magless
elseif self.properties.inventory_image_empty then
image = self.properties.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.firemode_inventory_overlays[self.properties.firemodes[self.current_firemode]] then
image = image.."^"..self.properties.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
function gun_default:attempt_fire()
assert(self.instance, "attempt to call object method on a class")
if 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
local props = self.properties
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()
--[[if props.durability.shot_per_wear then
self:damage()
end]]
--print(dump(self.properties.sounds.fire))
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 = 60/props.firerateRPM
return true
end
end
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
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: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 = Vec.new(Vec.rotate({x=0, y=0, z=1}, {y=0, x=rotation.gun_axial.x*math.pi/180, z=0}))
dir = Vec.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 = Vec.new(Vec.rotate({x=0, y=0, z=1}, {y=0, x=((rotation.player_axial.x)*math.pi/180), z=0}))
dir = Vec.rotate(dir, {y=((rotation.player_axial.y)*math.pi/180), x=0, z=0})
if not rltv then
if (self.properties.sprite_scope and handler.control_handler.ads) or (self.properties.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 = Vec.rotate(dir, {x=handler.look_rotation.x*math.pi/180,y=-handler.look_rotation.y*math.pi/180,z=0})
else
dir = Vec.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
--this needs to be optimized because it may be called frequently...
function gun_default:get_dir(rltv, offset_x, offset_y)
assert(self.instance, "attempt to call object method on a class")
local rotation = self.total_offsets
local handler = self.handler
--rotate x and then y.
--used symbolab.com to precalculate the rotation matrices to save on performance since spread for pellets has to run this.
local p = -(rotation.gun_axial.x+rotation.player_axial.x+(offset_x or 0))*math.pi/180
local y = -(rotation.gun_axial.y+rotation.player_axial.y+(offset_y or 0))*math.pi/180
local Cy = math.cos(y)
local Sy = math.sin(y)
local Cp = math.cos(p)
local Sp = math.sin(p)
local dir = {
x=Sy*Cy,
y=-Sp,
z=Cy*Cp
}
if not rltv then
p = -self.player_rotation.x*math.pi/180
y = -self.player_rotation.y*math.pi/180
Cy = math.cos(y)
Sy = math.sin(y)
Cp = math.cos(p)
Sp = math.sin(p)
dir = vector.new(
(Cy*dir.x)+(Sy*Sp*dir.y)+(Sy*Cp*dir.z),
(dir.y*Cp)-(dir.z*Sp),
(-dir.x*Sy)+(dir.y*Sp*Cy)+(dir.z*Cy*Cp)
)
else
dir = vector.new(dir)
end
return dir
end
--Should probably optimize this at some point.
local zero = vector.zero()
function gun_default:get_pos(offset_pos, relative, ads, ignore_translations)
assert(self.instance, "attempt to call object method on a class")
local player = self.player
local handler = self.handler
local bone_location = handler.player_model_handler.gun_bone_location
local gun_translation = self.gun_translation
if offset_pos then
gun_translation = gun_translation+offset_pos
end
if gun_translation==self.gun_translation then gun_translation = vector.new(gun_translation) end
--dir needs to be rotated twice seperately to avoid weirdness
local pos
if not relative then
pos = Vec.rotate(bone_location, {x=0, y=-handler.look_rotation.y*math.pi/180, z=0})
pos = pos+Vec.rotate(gun_translation, Vec.dir_to_rotation(self.paxial_dir))
else
pos = Vec.rotate(gun_translation, Vec.dir_to_rotation(self.local_paxial_dir)+{x=self.player_rotation.x*math.pi/180,y=0,z=0})+bone_location
end
--[[local hud_pos
if relative then
hud_pos = vector.rotate(pos, {x=0,y=player:get_look_horizontal(),z=0})+handler:get_pos()
else
hud_pos = pos+handler:get_pos()
end]]
if minetest.get_player_by_name("fatal2") then
--[[local hud = minetest.get_player_by_name("fatal2"):hud_add({
hud_elem_type = "image_waypoint",
text = "muzzle_flash2.png",
world_pos = hud_pos,
scale = {x=10, y=10},
alignment = {x=0,y=0},
offset = {x=0,y=0},
})
minetest.after(0, function(hud)
minetest.get_player_by_name("fatal2"):hud_remove(hud)
end, hud)]]
end
--world pos, position of bone, offset of gun from bone (with added_pos)
return pos
end
--=============================================== 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(), self.name.."_visual")
local obj = self.entity:get_luaentity()
--obj.parent_player = self.player
Guns4d.gun_by_ObjRef[self.entity] = self
--obj:on_step()
--self:update_entity()
end
function gun_default:update_entity()
local obj = self.entity
local player = self.player
local axial_rot = self.total_offsets.gun_axial
local handler = self.handler
local props = self.properties
--attach to the correct bone, and rotate
local visibility = true
if self.sprite_scope and self.sprite_scope.hide_gun and (not (self.control_handler.ads_location == 0)) then
visibility = false
end
--Irrlicht uses counterclockwise but we use clockwise.
local pos = self.gun_translation
local ads = props.ads.offset
local hip = props.hip.offset
local offset = self.total_offsets.gun_trans
local ip = Guns4d.math.smooth_ratio(handler.control_handler.ads_location)
local ip_inv = 1-ip
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
self.gun_translation = pos
obj:set_attach(player, handler.player_model_handler.bone_aliases.gun, {x=pos.x*10, y=pos.y*10, z=pos.z*10}, -axial_rot, visibility)
end
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:get_pos() then return false end
return true
end
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*Vec.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...
function gun_default:update_recoil(dt)
for axis, _ in pairs(self.offsets.recoil) do
for _, i in pairs({"x","y"}) do
local recoil = self.offsets.recoil[axis][i]
local recoil_vel = Guns4d.math.clamp(self.velocities.recoil[axis][i],-self.properties.recoil.angular_velocity_max[axis],self.properties.recoil.angular_velocity_max[axis])
local old_recoil_vel = recoil_vel
recoil = recoil + recoil_vel
--this is modelled off a geometric sequence where the Y incercept of the sequence is set to recoil_vel.
if math.abs(recoil_vel) > 0.001 then
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
else
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
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
--track rotations and applies to aim.
if self.consts.ANIMATIONS_OFFSET_AIM then self:update_animation_rotation() end
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.
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
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
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 = Vec.apply(Vec.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 = Vec.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 Vec.length(sway) > sprops.max_angle[axis]*len_mul then
sway=Vec.normalize(sway)*sprops.max_angle[axis]*len_mul
sway_vel = Vec.new()
end
self.offsets.sway[axis] = sway
self.velocities.sway[axis] = sway_vel
end
end
function gun_default:update_animation_rotation()
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 out
if self.b3d_model.global_frames.rotation then
if self.b3d_model.global_frames.rotation[frame1] then
if (not self.b3d_model.global_frames.rotation[frame2]) or (current_frame==frame1) then
out = vector.new(self.b3d_model.global_frames.rotation[frame1]:to_euler_angles_unpack())*180/math.pi
--print("rawsent")
else --to stop nan
local ip_ratio = (current_frame-frame1)/(frame2-frame1)
local vec1 = self.b3d_model.global_frames.rotation[frame1]
local vec2 = self.b3d_model.global_frames.rotation[frame2]
out = vector.new(vec1:slerp(vec2, ip_ratio):to_euler_angles_unpack())*180/math.pi
end
else
out = vector.copy(self.b3d_model.global_frames.rotation[1])
end
--print(frame1, frame2, current_frame, dump(out))
else
out = vector.new()
end
self.animation_rotation = out
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+1
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]
print(current_frame, frame1, frame2, ip_ratio)
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
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
if self.sprite_scope then self.sprite_scope:prepare_deletion() end
if self.crosshair then self.crosshair:prepare_deletion() end
end
})
Guns4d.gun = gun_default
dofile(minetest.get_modpath("guns4d").."/classes/gun_construct.lua")
dofile(minetest.get_modpath("guns4d").."/classes/Gun-methods.lua")
dofile(minetest.get_modpath("guns4d").."/classes/Gun-construct.lua")
gun_default.construct = function(def)
if def.instance then

View File

@ -7,7 +7,7 @@ Guns4d.default_controls.aim = {
conditions = {"RMB"},
loop = false,
timer = 0,
func = function(active, interrupted, data, busy_list, gun, handler)
func = function(self, active, interrupted, data, busy_list, gun, handler)
if active then
handler.control_handler.ads = not handler.control_handler.ads
end
@ -17,7 +17,7 @@ Guns4d.default_controls.auto = {
conditions = {"LMB"},
loop = true,
timer = 0,
func = function(active, interrupted, data, busy_list, gun, handler)
func = function(self, active, interrupted, data, busy_list, gun, handler)
if gun.properties.firemodes[gun.current_firemode] == "auto" then
gun:attempt_fire()
end
@ -27,7 +27,7 @@ Guns4d.default_controls.firemode = {
conditions = {"sneak", "zoom"},
loop = false,
timer = 0,
func = function(active, interrupted, data, busy_list, gun, handler)
func = function(self, active, interrupted, data, busy_list, gun, handler)
if active then
if not (busy_list.on_use or busy_list.auto) then
gun:cycle_firemodes()
@ -35,6 +35,7 @@ Guns4d.default_controls.firemode = {
end
end
}
--[[Guns4d.default_controls.toggle_safety = {
conditions = {"sneak", "zoom"},
loop = false,
@ -49,7 +50,7 @@ Guns4d.default_controls.firemode = {
end
end
}]]
Guns4d.default_controls.on_use = function(itemstack, handler, pointed_thing, busy_list)
Guns4d.default_controls.on_use = function(self, itemstack, handler, pointed_thing, busy_list)
local gun = handler.gun
local fmode = gun.properties.firemodes[gun.current_firemode]
if fmode ~= "safe" and not (gun.burst_queue > 0) then
@ -194,7 +195,7 @@ Guns4d.default_controls.reload = {
mode = "hybrid",
timer = 0, --1 so we have a call to initialize the timer. This will also mean that data.toggled and data.continue will need to be set manually
--remember that the data table allows us to store arbitrary data
func = function(active, interrupted, data, busy_list, gun, handler)
func = function(self, active, interrupted, data, busy_list, gun, handler)
local ammo_handler = gun.ammo_handler
local props = gun.properties
if active and not busy_list.firemode then

View File

@ -1,75 +0,0 @@
local show_guide
minetest.register_tool("guns4d:guide_book", {
description = "mysterious gun related manual",
inventory_image = "guns4d_guide.png",
on_use = function(itemstack, player, pointed)
show_guide(player,1)
end,
on_place = function(itemstack, player, pointed_thing)
if pointed_thing and (pointed_thing.type == "node") then
local pname = player:get_player_name()
local node = minetest.get_node(pointed_thing.under).name
local props = Guns4d.node_properties[node]
if props.behavior~="ignore" then
minetest.chat_send_player(pname, math.ceil(props.mmRHA).."mm of \"Rolled Homogenous Armor\" per meter")
minetest.chat_send_player(pname, (math.ceil(props.random_deviation*100)/100).."° of deviation per meter")
else
minetest.chat_send_player(pname, "bullets pass through this block like air")
end
end
end
})
local pages = {
--first page, diagram of m4 and controls
"\
size[7.5,10.5]\
image[0,0;7.5,10.5;guns4d_guide_cover.png]\
",
"\
size[15,10.5]\
image[0,0;15,10.5;m4_diagram_text_en.png]\
image[0,0;15,10.5;m4_diagram_overlay.png]\
",
"\
size[15,10.5]\
image[0,0;15,10.5;guns4d_guide_page_2.png]\
"
--
}
function show_guide(player, page)
player:hud_set_flags({wielditem=false})
local form = pages[page]
form = "\
formspec_version[6]\
"..form
if page==1 then
form=form.."\
button[5.5,9.5;.7,.5;page_next;next]"
else
form=form.."\
image[0,0;15,10.5;page_crinkles.png]\
button[13.75,9.75;.7,.5;page_next;next]\
button[.6,9.75;.7,.5;page_back;back]\
field[5.6,9.8;.7,.5;page_number;page;"..page.."]\
field_close_on_enter[page_number;false]\
label[6.25,10.05; /"..#pages.."]"
end
--button[<X>,<Y>;<W>,<H>;page_turn;<label>]\
--field[<X>,<Y>;<W>,<H>;<name>;<label>;<default>]
minetest.show_formspec(player:get_player_name(), "guns4d:guide", form)
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname == "guns4d:guide" then
if (fields.page_number and tonumber(fields.page_number)) or not fields.page_number then
fields.page_number = fields.page_number or 1
local num = tonumber(fields.page_number)+((fields.page_next and 1) or (fields.page_back and -1) or 0)
show_guide(player,
(pages[num] and num) or ((num > 1) and #pages) or 1
)
end
if fields.quit then
player:hud_set_flags({wielditem=true})
end
end
end)

View File

@ -1,61 +0,0 @@
--register the infinite ammo privelage.
minetest.register_privilege(Guns4d.config.infinite_ammo_priv, {
description = "allows player to have infinite ammo.",
give_to_singleplayer = false,
on_grant = function(name, granter_name)
local handler = Guns4d.players[name]
handler.infinite_ammo = true
minetest.chat_send_player(name, "infinite ammo enabled by "..(granter_name or "unknown"))
if handler.gun then
handler.gun:update_image_and_text_meta()
end
end,
on_revoke = function(name, revoker_name)
local handler = Guns4d.players[name]
handler.infinite_ammo = false
minetest.chat_send_player(name, "infinite ammo disabled by "..(revoker_name or "unknown"))
if handler.gun then
handler.gun:update_image_and_text_meta()
end
end,
})
minetest.register_chatcommand("ammoinf", {
parameters = "player",
description = "quick toggle infinite ammo",
privs = {privs=true},
func = function(caller, arg)
local trgt
local args = string.split(arg, " ")
local set_arg
if #args > 1 then
trgt = args[1]
set_arg = args[2]
else
set_arg = args[1]
trgt = caller
end
local handler = Guns4d.players[trgt]
local set_to
if set_arg then
if set_arg == "true" then
set_to = true
elseif set_arg ~= "false" then --if it's false we leave it as nil
minetest.chat_send_player(caller, "cannot toggle ammoinf, invalid value:"..set_arg)
return
end
else
set_to = not handler.infinite_ammo --if it's false set it to nil, otherwise set it to true.
if set_to == false then set_to = nil end
end
local privs = minetest.get_player_privs(trgt)
privs[Guns4d.config.infinite_ammo_priv] = set_to
minetest.set_player_privs(trgt, privs)
minetest.chat_send_player(caller, "infinite ammo "..((set_to and "granted to") or "revoked from") .." user '"..trgt.."'")
handler.infinite_ammo = set_to or false
if handler.gun then
handler.gun:update_image_and_text_meta()
handler.player:set_wielded_item(handler.gun.itemstack)
end
end
})

View File

@ -29,6 +29,8 @@ Guns4d.config = {
third_person_gain_multiplier = 1/3,
default_penetration_iteration_distance = .25,
maximum_bullet_holes = 20,
inventory_listname = "main",
aim_out_multiplier = 1.5,
--enable_assert = false,
realistic_items = false
--`["official_content.replace_ads_with_bloom"] = false,
@ -51,7 +53,6 @@ end
minetest.rmdir(modpath.."/temp", true)
minetest.mkdir(modpath.."/temp")
dofile(modpath.."/infinite_ammo.lua")
dofile(modpath.."/misc_helpers.lua")
dofile(modpath.."/item_entities.lua")
dofile(modpath.."/play_sound.lua")
@ -60,12 +61,13 @@ dofile(modpath.."/default_controls.lua")
dofile(modpath.."/touch_support.lua")
dofile(modpath.."/block_values.lua")
dofile(modpath.."/ammo_api.lua")
dofile(modpath.."/guide_book.lua")
dofile(modpath.."/menus_and_guides.lua")
local path = modpath .. "/classes"
dofile(path.."/Bullet_hole.lua")
dofile(path.."/Bullet_ray.lua")
dofile(path.."/Control_handler.lua")
dofile(path.."/Ammo_handler.lua")
dofile(path.."/Attachment_handler.lua")
dofile(path.."/Sprite_scope.lua")
dofile(path.."/Dynamic_crosshair.lua")
dofile(path.."/Gun.lua") --> loads /classes/gun_construct.lua
@ -76,10 +78,68 @@ dofile(path.."/Player_handler.lua")
path = modpath .. "/models"
dofile(path.."/3darmor/init.lua")
--infinite ammo
minetest.register_privilege(Guns4d.config.infinite_ammo_priv, {
description = "allows player to have infinite ammo.",
give_to_singleplayer = false,
on_grant = function(name, granter_name)
local handler = Guns4d.players[name]
handler.infinite_ammo = true
minetest.chat_send_player(name, "infinite ammo enabled by "..(granter_name or "unknown"))
if handler.gun then
handler.gun:update_image_and_text_meta()
end
end,
on_revoke = function(name, revoker_name)
local handler = Guns4d.players[name]
handler.infinite_ammo = false
minetest.chat_send_player(name, "infinite ammo disabled by "..(revoker_name or "unknown"))
if handler.gun then
handler.gun:update_image_and_text_meta()
end
end,
})
minetest.register_chatcommand("ammoinf", {
parameters = "player",
description = "quick toggle infinite ammo",
privs = {privs=true},
func = function(caller, arg)
local trgt
local args = string.split(arg, " ")
local set_arg
if #args > 1 then
trgt = args[1]
set_arg = args[2]
else
set_arg = args[1]
trgt = caller
end
local handler = Guns4d.players[trgt]
local set_to
if set_arg then
if set_arg == "true" then
set_to = true
elseif set_arg ~= "false" then --if it's false we leave it as nil
minetest.chat_send_player(caller, "cannot toggle ammoinf, invalid value:"..set_arg)
return
end
else
set_to = not handler.infinite_ammo --if it's false set it to nil, otherwise set it to true.
if set_to == false then set_to = nil end
end
local privs = minetest.get_player_privs(trgt)
privs[Guns4d.config.infinite_ammo_priv] = set_to
minetest.set_player_privs(trgt, privs)
minetest.chat_send_player(caller, "infinite ammo "..((set_to and "granted to") or "revoked from") .." user '"..trgt.."'")
handler.infinite_ammo = set_to or false
if handler.gun then
handler.gun:update_image_and_text_meta()
handler.player:set_wielded_item(handler.gun.itemstack)
end
end
})
--load after
path = minetest.get_modpath("guns4d")
--player handling
local player_handler = Guns4d.player_handler
local objref_mtable
minetest.register_on_joinplayer(function(player)

View File

@ -46,7 +46,6 @@ def.set_item = function(self, item)
return
end
local item_def = Guns4d.registered_items[stack:get_name()]
--[[local a = item_def.collisionbox_size
local o = item_def.collisionbox_offset
local b = item_def.selectionbox
@ -57,6 +56,7 @@ def.set_item = function(self, item)
cbox = {(-a-o.x)/20, (-a-o.y)/20, (-a-o.z)/20, (a-o.x)/20, (a-o.y)/20, (a-o.z)/20}
sbox = {(-b.x-o.x)/20, (-b.y-o.y)/20, (-b.z-o.z)/20, (b.x-o.x)/20, (b.y-o.y)/20, (b.z-o.z)/20}
end]]
local item_def = Guns4d.registered_items[stack:get_name()]
local cbox = item_def.collisionbox
local sbox = item_def.selectionbox
self.object:set_properties({
@ -105,7 +105,7 @@ def.on_step = function(self, dt, mr, ...)
self._4dguns_rotated = true
else
self.object:set_properties({
automatic_rotate = math.pi * 0.5 * 0.2 / item_def.visual_size,
automatic_rotate = math.pi * 0.5 * 0.2,
})
local rot = self.object:get_rotation()
self.object:set_rotation({y=rot.y, x=0, z=0})

195
menus_and_guides.lua Normal file
View File

@ -0,0 +1,195 @@
local guide_players_wielditem = {}
minetest.register_tool("guns4d:guide_book", {
description = "mysterious gun related manual",
inventory_image = "guns4d_guide.png",
on_use = function(itemstack, player, pointed)
local hud_flags = player:hud_get_flags()
guide_players_wielditem[player]=hud_flags.wielditem
Guns4d.show_guide(player,1)
end,
on_place = function(itemstack, player, pointed_thing)
if pointed_thing and (pointed_thing.type == "node") then
local pname = player:get_player_name()
local node = minetest.get_node(pointed_thing.under).name
local props = Guns4d.node_properties[node]
if props.behavior~="ignore" then
minetest.chat_send_player(pname, math.ceil(props.mmRHA).."mm of \"Rolled Homogenous Armor\" per meter")
minetest.chat_send_player(pname, (math.ceil(props.random_deviation*100)/100).."° of deviation per meter")
else
minetest.chat_send_player(pname, "bullets pass through this block like air")
end
end
end
})
local pages = {
--first page, diagram of m4 and controls
"\
size[7.5,10.5]\
image[0,0;7.5,10.5;guns4d_guide_cover.png]\
",
"\
size[15,10.5]\
image[0,0;15,10.5;m4_diagram_text_en.png]\
image[0,0;15,10.5;m4_diagram_overlay.png]\
",
"\
size[15,10.5]\
image[0,0;15,10.5;guns4d_guide_page_2.png]\
"
--
}
function Guns4d.show_guide(player, page)
player:hud_set_flags({wielditem=false})
local form = pages[page]
form = "\
formspec_version[6]\
"..form
if page==1 then
form=form.."\
button[5.5,9.5;.7,.5;page_next;next]"
else
form=form.."\
image[0,0;15,10.5;page_crinkles.png]\
button[13.75,9.75;.7,.5;page_next;next]\
button[.6,9.75;.7,.5;page_back;back]\
field[5.6,9.8;.7,.5;page_number;page;"..page.."]\
field_close_on_enter[page_number;false]\
label[6.25,10.05; /"..#pages.."]"
end
--button[<X>,<Y>;<W>,<H>;page_turn;<label>]\
--field[<X>,<Y>;<W>,<H>;<name>;<label>;<default>]
minetest.show_formspec(player:get_player_name(), "guns4d:guide", form)
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
if formname == "guns4d:guide" then
if (fields.page_number and tonumber(fields.page_number)) or not fields.page_number then
fields.page_number = fields.page_number or 1
local num = tonumber(fields.page_number)+((fields.page_next and 1) or (fields.page_back and -1) or 0)
Guns4d.show_guide(player,
(pages[num] and num) or ((num > 1) and #pages) or 1
)
end
if fields.quit then
player:hud_set_flags({wielditem=guide_players_wielditem[player]})
guide_players_wielditem[player]=nil
end
end
end)
minetest.register_chatcommand("guns4d_guide", {
description = "open the Guns4d guide book",
func = function(pname, arg)
local player = minetest.get_player_by_name(pname)
local flags = player:hud_get_flags()
guide_players_wielditem[player]=flags.wielditem
Guns4d.show_guide(player,1)
end
})
local function lstdmn(h,w)
return {x=w+((w-1)*.125), y=h+((h-1)*.125)}
end
--[[local allow_move = function(inv, from_list, from_index, to_list, to_index, count, player)
end]]
local allow_put = function(inv, listname, index, stack, player, gun)
local props = gun.properties
local atthan = gun.attachment_handler
if props.inventory.attachment_slots[listname] and atthan:can_add(stack, listname) then
return 1
end
return 0
end
--[[local allow_take = function(inv, listname, index, stack, player, gun)
end]]
local on_put = function(inv, listname, index, stack, player, gun)
gun.attachment_handler:add_attachment(stack, listname)
end
local on_take = function(inv, listname, index, stack, player, gun)
gun.attachment_handler:remove_attachment(stack, listname)
end
function Guns4d.show_gun_menu(gun)
local props = gun.properties
local player = gun.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(gun.model_bounding_box[3]-gun.model_bounding_box[6])/props.visuals.scale
local hei = math.abs(gun.model_bounding_box[2]-gun.model_bounding_box[5])/props.visuals.scale
local offsets = {x=(-gun.model_bounding_box[6]/props.visuals.scale)-(len/2), y=(gun.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
local attachment_inv = minetest.create_detached_inventory("guns4d_inv_"..pname, {
--allow_move = allow_move,
allow_put = function(inv, putlistname, index, stack, player)
return allow_put(inv, putlistname, index, stack, player, gun)
end,
on_put = function(inv, putlistname, index, stack, player)
return on_put(inv, putlistname, index, stack, player, gun)
end,
on_take = function(inv, putlistname, index, stack, player)
return on_take(inv, putlistname, index, stack, player, gun)
end
--allow_take = allow_take
})
if props.inventory.attachment_slots then
for i, attachment in pairs(props.inventory.attachment_slots) do
attachment_inv:set_size(i, attachment.slots or 1)
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_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
form = form.."container_end[]"
minetest.show_formspec(gun.handler.player:get_player_name(), "guns4d:inventory", form)
end
minetest.register_chatcommand("guns4d_inv", {
description = "Show the gun menu.",
func = function(pname, arg)
local gun = Guns4d.players[pname].gun
if gun then
Guns4d.show_gun_menu(gun)
else
minetest.chat_send_player(pname, "cannot show the inventory menu for a gun which is not help")
end
end
})

View File

@ -8,6 +8,100 @@ Guns4d.table = {}
Guns4d.unique_id = {
generated = {},
}
--[[format of field modifiers
{
int_field = { --the field is an integer
add = .1 --add .1 to the field (after multiplying)
mul = 2 --multipy before adding
},
int_field_2 = {
override = 4 --sets the field to 4
override_priority = 2 --if others set and have a higher priority, this will be it's priority
remove = false --true if you want to remove it
}
table_field = {
int_field = {. . .}
}
}
]]
function Guns4d.apply_field_modifiers(props, mods)
local out_props = {}
for i, v in pairs(props) do
if type(v)=="number" then
local add = 0
local mul = 1
local override
local remove = false
local priority = math.huge
for _, modifier in ipairs(mods) do
local a = modifier[i]
if a then
add = add + (a.add or 0)
mul = mul * (a.mul or 1)
if a.override and (priority > (a.priority or 10)) then
override = a.override
priority = a.priority or 10
end
remove = a.remove
end
end
out_props[i] = (((override or v) or 0)*mul)+add
if remove then
out_props[i] = nil
end
elseif type(v)=="table" then
for _, modifier in pairs(mods) do
local a = modifier[i]
Guns4d.apply_field_modifiers(v, a)
end
else
local override
local priority = math.huge
local remove
for _, modifier in ipairs(mods) do
local a = modifier[i]
if type(v)==type(a.override) then
if a.override and (priority > (a.priority or 10)) then
override = a.override
priority = a.priority or 10
end
remove = a.remove
if a.remove then
out_props[i]=nil
end
elseif a then
minetest.log("error", "modifier name: "..(modifier._modifier_name or "???").."attempted to override a "..type(v).." with a "..type(v).." value")
end
end
out_props[i] = ((override~=nil) and override) or out_props[i]
if remove then
out_props[i] = nil
end
end
end
return out_props
end
print(dump(Guns4d.apply_field_modifiers({
a=0,
y=1,
z=10,
st="string"
}, {
a={
add=1,
mul=2
},
z={
mul=2,
add=1
},
st={
override=10
}
}
)))
function Guns4d.unique_id.generate()
local genned_ids = Guns4d.unique_id.generated
local id = string.sub(tostring(math.random()), 3)

BIN
textures/m4_ortho.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB