--- Gun class -- -- ## Defining a gun: -- -- **method documentation coming soon** (or never...) -- -- guns are defined by two table fields: their @{lvl1_fields.consts|consts} and their @{lvl1_fields.properties|properties}. -- @{lvl1_fields.properties|properties} define nearly everything, from how a gun handles to how it looks, what model it uses, -- while @{lvl1_fields.consts|consts} define attributes that should never change, like bones within the gun, framerates, -- wether the gun is allowed to have certain attributes at all. The rest is mainly for internal workings of the mod. -- -- Guns4d uses a class system for most moving parts- including the gun. New guns therefor are created with the :inherit(def) method, -- where def is the definition of your new gun- or rather the changed parts of it. So to make a new gun you can run Guns4d.gun:inherit() -- or you can do the same thing with a seperate class of weapons. Set name to "__template" for template classes of guns. -- -- @class Gun -- @compact local Vec = vector --- gun fields -- -- @table gun -- @field properties @{lvl1_fields.properties|properties} which define the vast majority of gun attributes and may change accross instances -- @field consts @{lvl1_fields.consts|constancts} which define gun attributes and should not be changed in an instance of the gun -- @field offsets @{lvl1_fields.offsets|offsets}. runtime storage of offsets generated by recoil sway wag or any other element. -- @compact 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 = nil, --- `ObjRef` the gun entity gun_entity = 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... _registered = {}, --- `bool` is the bolt charged bolt_charged = false, --- `table` list of particle spawner handles (generated by firing) particle_spawners = {}, --- `int` the active index of the firemode from @{lvl1_fields.properties.firemodes} current_firemode = 1, --- `float` walking time used to generate the figure 8 for wag walking_tick = 0, --- `float` time_since_last_fire = 0, --- `float` time_since_creation = 0, --- `float` time left for the chamber to cycle (for firerates) rechamber_time = 0, --- `int` number of rounds left that need to be fired after a burst fire burst_queue = 0, --- `function` muzzle_flash = Guns4d.effects.muzzle_flash, --- properties -- -- the table containing every attribute of the gun. -- @table lvl1_fields.properties -- @field hip `table` @{gun.properties.hip|hipfire properties} -- @field ads `table` @{gun.properties.ads|aiming ("aiming down sights") properties} -- @field firemodes `table` @{gun.properties.firemodes|list of firemodes} -- @field firemode_inventory_overlays `table` @{gun.properties.firemode_inventory_overlays|list of corresponding images for firemodes} -- @field recoil `table` @{gun.properties.recoil|defines the guns recoil} -- @field sway `table` @{gun.properties.sway|defines the guns idle sway} -- @field wag `table` @{gun.properties.wag|defines the movement of the gun while walking} -- @field charging `table` @{gun.properties.charging|defines how rounds are chambered into the gun} -- @field ammo @{gun.properties.ammo|defines what ammo the gun uses} -- @field visuals @{gun.properties.visuals|defines visual attributes of the gun} -- @compact properties = { --- `float`=.5 max angular deviation (vertical) from breathing breathing_scale = .5, --- `vector` the offset from the center of the muzzle flash. Used by fire() 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 = { 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 hip = {--#1 --- `vector` the offset of the gun (relative to the right arm's default position) at hip. offset = Vec.new(), --- the ratio that the look rotation is expressed through player_axial (rotated around the viewport) rotation as opposed to gun_axial (rotating the entity). axis_rotation_ratio = .75, --- sway speed multiplier while at hip sway_vel_mul = 5, --- sway angle multiplier while at hip+ sway_angle_mul = 1, }, --- properties.ads -- @table gun.properties.ads -- @compact ads = { --#2 --- `vector` the offset of the gun relative to the eye's position at hip. offset = Vec.new(), --- `float` the horizontal offset of the eye when aiming horizontal_offset = 0, --- the time it takes to go into full aim aim_time = 1, }, --- `int`=3 how many rounds in burst using when firemode is at "burst" burst = 3, --- properties.firemodes -- -- list containing the firemodes of the gun. Default only contains "single". Strings allowed by default: -- @table gun.properties.firemodes -- @compact -- @field "single" -- @field "burst" -- @field "auto" firemodes = { --#3 "single", --not limited to semi-automatic. }, --- `string` overlay on the item to use when infinite ammo is on infinite_inventory_overlay = "inventory_overlay_inf_ammo.png", --- properties.firemode_inventory_overlays -- -- defines the overlay on the gun item for each firemode. These are selected automatically by firemode string. Defaults are as follows: -- @table gun.properties.firemode_inventory_overlays -- @compact firemode_inventory_overlays = { --#4 --- "inventory_overlay_single.png" single = "inventory_overlay_single.png", --- "inventory_overlay_auto.png" auto = "inventory_overlay_auto.png", --- "inventory_overlay_burst.png" burst = "inventory_overlay_burst.png", --- "inventory_overlay_safe.png" (unimplemented firemode) safe = "inventory_overlay_safe.png" }, --- properties.recoil -- -- **IMPORTANT**: expects fields to be tables containing a "gun_axial" and "player_axial" field. -- @example -- property = { -- gun_axial = type -- player_axial = type -- } -- --using a vector... -- property = { -- gun_axial={x=float, y=float}, -- player_axial={x=float, y=float} -- }` -- @table gun.properties.recoil -- @compact recoil = { --#5 used by update_recoil() --- `float` TL:DR higher decreases recoil at expense of smoothness. 1/value is the deviation of a normalized bell curve, where x is the time since firing. -- this means that increasing it decreases the time it takes for the angular velocity to fully "decay". velocity_correction_factor = { --velocity correction factor is currently very broken. gun_axial = 1, player_axial = 1, }, --- `float` Correction of recoil offset per second is calculated as such: `target_correction_factor[axis]*time_since_fire*recoil[axis]` target_correction_factor = { --angular correction rate per second: time_since_fire*target_correction_factor gun_axial = 1, player_axial = 1, }, --- `float` The maximum rate per second of recoil offset as determined with @{target_correction_factor} target_correction_max_rate = { --the cap for target_correction_fire (i.e. this is the maximum amount it will ever correct per second.) gun_axial = math.huge, player_axial = math.huge, }, --- `float` caps the recoil velocity that can stack up from shots. angular_velocity_max = { --max velocity, so your gun doesnt "spin me right round baby round round" gun_axial = 5, player_axial = 5, }, --- `vector` {x=`float`, y=`float`}, defines the initial angular velocity produced by firing the gun angular_velocity = { --the velocity added per shot. This is the real "recoil" part of the recoil gun_axial = {x=0, y=0}, player_axial = {x=0, y=0}, }, --- `vector` {x=`float`, y=`float`}, ranges -1 to 1. Defines the probability of the recoil being positive or negative for any given axis. bias = { --dictates the distribution bias for the direction angular_velocity is in. I.e. if you want recoil to always go up you set x to 1, more often to the right? y to -.5 gun_axial = {x=1, y=0}, player_axial = {x=1, y=0}, }, --- `float` angular velocity multiplier when firing from the hip hipfire_multiplier = { --the mutliplier for recoil (angular_velocity) at hipfire (can be fractional) gun_axial = 1, player_axial = 1 }, }, --- properties.sway -- -- **IMPORTANT**: expects fields to be tables containing a "gun_axial" and "player_axial" field. In the same format as @{gun.properties.recoil} -- @table gun.properties.sway -- @compact sway = { --#6 --- `float` maximum angle of the sway max_angle = { gun_axial = 0, player_axial = 0, }, --- `float` angular velocity the sway angular_velocity = { gun_axial = 0, player_axial = 0, }, --- `float` maximum angle multiplier while the gun is at the hip hipfire_angle_multiplier = { --the mutliplier for sway max_angle at hipfire (can be fractional) gun_axial = 2, player_axial = 2 }, --- `float` velocity multiplier while the gun is at the hip hipfire_velocity_multiplier = { --same as above but for velocity. gun_axial = 2, player_axial = 2 } }, --- properties.wag -- -- @table gun.properties.wag -- @compact wag = { --- `float`=1.6 the cycle speed multiplier cycle_speed = 1.6, --- `float`=1 decay factor when walking has stopped and offset remains. decay_speed = 1, --- `table` containing angular deviation while walking in the same format as @{gun.properties.recoil|an offset vector}. Acts as a multiplier on the figure-8 generated while walking. offset = { --used by update_walking() (or something) gun_axial = {x=1, y=-1}, player_axial = {x=1,y=1}, }, }, --- `table` containing a list of actions for PC users passed to @{Control_handler} pc_control_actions = { --used by control_handler __overfill=true, --this table will not be filled in. aim = Guns4d.default_controls.aim, auto = Guns4d.default_controls.auto, reload = Guns4d.default_controls.reload, on_use = Guns4d.default_controls.on_use, firemode = Guns4d.default_controls.firemode }, --- `table` containing a list of actions for touch screen users passed to @{Control_handler} touch_control_actions = { __overfill=true, aim = Guns4d.default_touch_controls.aim, auto = Guns4d.default_touch_controls.auto, reload = Guns4d.default_touch_controls.reload, on_secondary_use = Guns4d.default_touch_controls.on_secondary_use, firemode = Guns4d.default_touch_controls.firemode }, --- properties.charging -- -- @table gun.properties.charging -- @compact charging = { --#7 --- `bool` defines wether the draw animation is played on swap (when loaded). Used in the instance construction method require_draw_on_swap = true, --- `string` "none" bolt will never need to be charged after reload, "catch" when fired to empty bolt will not need to be charged after reload, "no_catch" bolt will always need to be charged after reload. bolt_charge_mode = "none", --"none"-chamber is always full, "catch"-when fired to dry bolt will not need to be charged after reload, "no_catch" bolt will always need to be charged after reload. --- `float` the time it takes to swap to the gun draw_time = 1, --- `string` name of the animation to play from @{gun.properties.visuals.animations|visuals.animations} draw_animation = "draw", --- `string` name of the sound to play from @{gun.properties.sounds|sounds} draw_sound = "draw" --sound = soundspec }, --- and ordered list of reloading states used by @{default_controls}. -- -- the default reload states for a magazine operated weapon, copied from the m4. -- @example -- {action="charge", time=.5, anim="charge", sounds={sound="ar_charge", delay = .2}}, -- {action="unload_mag", time=.25, anim="unload", sounds = {sound="ar_mag_unload"}}, -- {action="store", time=.5, anim="store", sounds = {sound="ar_mag_store"}}, -- {action="load", time=.5, anim="load", sounds = {sound="ar_mag_load", delay = .25}}, -- {action="charge", time=.5, anim="charge", sounds={sound="ar_charge", delay = .2}} reload = { __overfill=true, }, --- properties.ammo -- -- @table gun.properties.ammo -- @compact ammo = { --#8 magazine_only = false, --capacity = 0, --this is only needed if magazine_only = false --- `table` a list of accepted bullet itemstrings accepted_bullets = {}, --- `table` a list of accepted magazine itemstrings accepted_magazines = {}, --- `string` the mag the gun starts with. Set to "empty" for no mag, otherwise it defaults to accepted_magazines[1] (if present) initial_mag = nil }, --- properties.visuals -- -- @table gun.properties.visuals -- @compact visuals = { --- 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. animations = { --used by animations handler for idle, and default controls empty = {x=0,y=0}, loaded = {x=1,y=1}, }, }, --- a table of @{guns4d_soundspec|soundspecs} to be referenced by other functions sounds = { --this does not contain reload sound effects. fire = { { __overfill=true, sound = "ar_firing", max_hear_distance = 40, --far min_hear_distance is also this. pitch = { min = .95, max = 1.05 }, gain = { min = .9, max = 1 } }, { __overfill=true, sound = "ar_firing_far", min_hear_distance = 40, max_hear_distance = 600, pitch = { min = .95, max = 1.05 }, gain = { min = .9, max = 1 } } }, }, ammo_handler = Ammo_handler, sprite_scope = nil, crosshair = nil, initial_vertical_rotation = -60, }, --- offsets -- -- a list of tables each containing two vectors, a gun_axial offset and a player_axial offset. These are required for automatic initialization of offsets. -- @example -- recoil = { -- gun_axial = {x=0, y=0} -- player_axial = {x=0, y=0} -- } -- @table lvl1_fields.offsets -- @compact offsets = { --- recoil = { gun_axial = Vec.new(), player_axial = Vec.new(), --move_dynamic_crosshair = false, this would make the dynamic crosshair move instead of get larger }, --- sway = { gun_axial = Vec.new(), player_axial = Vec.new(), }, --- walking = { gun_axial = Vec.new(), player_axial = Vec.new(), tick = 0, --velocity }, --- breathing = { gun_axial = Vec.new(), --gun axial unimplemented... player_axial = Vec.new(), }, --- look_snap = { gun_axial = Vec.new(), player_axial = Vec.new() }, }, --- `vector` containing the offset from animations, this will be generated if {@consts.ANIMATIONS_OFFSET_AIM}=true animation_rotation = vector.new(), --- total offsets of the gun in the same format as a @{offsets|an offset} total_offset_rotation = nil, --[[total_offset_rotation = { --can't be in offsets, as they're added automatically. gun_axial = Vec.new(), player_axial = Vec.new(), },]] --player_rotation = Vec.new(), velocities = { recoil = { gun_axial = Vec.new(), player_axial = Vec.new(), }, init_recoil = { gun_axial = Vec.new(), player_axial = Vec.new(), }, sway = { gun_axial = Vec.new(), player_axial = Vec.new(), }, }, --- consts -- -- These are variables that are constant across the class and should usually not ever be changed by instnaces -- @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 DEFAULT_MAX_HEAR_DISTANCE = 10, --- `fps`=60 animation fps i.e. during firing when no length is specified DEFAULT_FPS = 60, --- `bool` HAS_RECOIL = true, --- `bool` HAS_BREATHING = true, --- `bool` HAS_SWAY = true, --- `bool` HAS_WAG = true, --- `bool` wether the gun rotates on it's own axis instead of the player's view (i.e. ironsight misalignments) HAS_GUN_AXIAL_OFFSETS = true, --- wether animations create an offset ANIMATIONS_OFFSET_AIM = false, --- whether the idle animation changes or not LOOP_IDLE_ANIM = false, --- general gain multiplier for third persons when hearing sounds THIRD_PERSON_GAIN_MULTIPLIER = Guns4d.config.third_person_gain_multiplier, --- the root bone of the gun (for animation offsets) ROOT_BONE = "gun", --- `string`="magazine",the bone of the magazine in the gun (for dropping mags) MAG_BONE = "magazine", --- `string`="right_aimpoint", the bone which the right arm aims at to ARM_RIGHT_BONE = "right_aimpoint", --- `string`="left_aimpoint", the bone which the left arm aims at to ARM_LEFT_BONE = "left_aimpoint", }, } --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 local total_rot = self.total_offset_rotation --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_rotation(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 offsets = self.offsets total_rot.player_axial.x = 0; total_rot.player_axial.y = 0 total_rot.gun_axial.x = 0; total_rot.gun_axial.y = 0 for type, _ in pairs(total_rot) do for i, offset in pairs(offsets) do if self.consts.HAS_GUN_AXIAL_OFFSETS or type~="gun_axial" then total_rot[type] = total_rot[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() self.current_firemode = ((self.current_firemode)%(#self.properties.firemodes))+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 if #self.properties.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.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_rotation(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 if not handler.control_handler.ads then local pitch = self.total_offset_rotation.player_axial.x+player_rot.x local gun_axial = self.offsets.look_snap.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-self.properties.hip.axis_rotation_ratio)) self.offsets.look_snap.player_axial.x = -pitch*(1-self.properties.hip.axis_rotation_ratio) else self.offsets.look_snap.gun_axial.x = 0 self.offsets.look_snap.player_axial.x = 0 --quick little experiment... --[[local pitch = self.total_offset_rotation.player_axial.x+player_rot.x self.offsets.look_snap.gun_axial.x = handler.look_rotation.x-player_rot.x self.offsets.look_snap.player_axial.x = 0]] end 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_offset_rotation 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_offset_rotation 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_offset_rotation local handler = self.handler --rotate x and then y. --old code. I used a site (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. function gun_default:get_pos(added_pos, relative, debug) assert(self.instance, "attempt to call object method on a class") local player = self.player local handler = self.handler local bone_location local gun_offset local pprops = handler:get_properties() if handler.control_handler.ads then gun_offset = self.properties.ads.offset bone_location = player:get_eye_offset() or vector.zero() bone_location.y = bone_location.y + pprops.eye_height bone_location.x = handler.horizontal_offset else --minetest is really wacky. gun_offset = self.properties.hip.offset bone_location = vector.new(handler.player_model_handler.offsets.global.hipfire) bone_location.x = (bone_location.x / 10)*pprops.visual_size.x bone_location.y = (bone_location.y / 10)*pprops.visual_size.y bone_location.z = (bone_location.z / 10)*pprops.visual_size.z end if added_pos then gun_offset = gun_offset+added_pos 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_offset, Vec.dir_to_rotation(self.paxial_dir)) else pos = Vec.rotate(gun_offset, Vec.dir_to_rotation(self.local_paxial_dir)+{x=self.player_rotation.x*math.pi/180,y=0,z=0})+bone_location end if debug then 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 local hud = player: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) player: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_offset_rotation.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 (handler.ads_location == 0)) then visibility = false end if handler.control_handler.ads then local normal_pos = (props.ads.offset)*10 obj:set_attach(player, handler.player_model_handler.bone_names.aim, normal_pos, -axial_rot, visibility) else local normal_pos = vector.new(props.hip.offset)*10 obj:set_attach(player, handler.player_model_handler.bone_names.hipfire, normal_pos, -axial_rot, visibility) end 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.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.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 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 --out = vec1+((vec1-vec2)*ip_ratio) --they're euler angles... actually I wouldnt think this works, but it's good enough for my purposes. --print("interpolated") 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 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 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") gun_default.construct = function(def) if def.instance then gun_default.construct_instance(def) elseif def.name ~= "__guns4d:default__" then --print(dump(def)) gun_default.construct_base_class(def) end end Guns4d.gun = Instantiatable_class:inherit(gun_default)