diff --git a/TODO.txt b/TODO.txt
index 4abcf04..4f71a32 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -3,11 +3,18 @@
-( ) add audio
-( ) add config
+(~) add audio
+ (x) sfx system
+ (x) did signifficant work on it
+ (x) firing sound effects
+ ( ) reload sound effects
+ ( ) firemode sound effects
+(~) add config
+ (x) add a table for config storage, some settings
+ ( ) integrate with minetest settings
(x) fix crash when switching from a gun into a gun with a sprite_scope
(x) add infinite ammo privelage and quick command
- ( ) privilege not directly tied to infinite ammo, fix without breaking performance?
+ (x) privilege not directly tied to infinite ammo, fix without breaking performance?
(x) fix animation rotation offset not displaying the correct frame
-was a problem with MTUL, changes push :D
( ) add entity scopes (for holo sights etc)
@@ -17,7 +24,7 @@
( ) fractional
( ) flat (magless)
( ) fractional clip
-( ) (for 5.9) make infinite ammo priv to rely on on_grant and on_revoke callback for runtime changes (broken as of 5.8)
+( ) (for 5.9) make infinite ammo priv to rely on on_grant and on_revoke callback for runtime changes (cannot be done as broken in 5.8)
( ) (5.9) POTENTIALLY make models use new PR that allows bone offsets to be disabled
-I'd probably have to modify models at loadtime to have an eye and hipfire bone? Probably easier then current system though.
( ) Fix HORRIBLE namespace violation in misc_helpers.lua. Also migrate features to MTUL libraries
@@ -42,5 +49,9 @@ documentation
( ) Bullet_ray
- ( ) play_sound.lua
- ( ) misc_helpers.lua
\ No newline at end of file
+ (x) play_sound.lua
+ (~) misc_helpers.lua
+ (x) weighted randoms
+ ( ) unique ID
+ ( ) table helpers
+ ( ) openGL/irrlicht relative dir projection
\ No newline at end of file
diff --git a/ammo_api.lua b/ammo_api.lua
index c2ccf84..df75ad4 100644
--- a/ammo_api.lua
+++ b/ammo_api.lua
@@ -26,7 +26,7 @@ end
function Guns4d.ammo.register_bullet(def)
assert(def.itemstring, "no itemstring")
assert(minetest.registered_items[def.itemstring], "no item '"..def.itemstring.."' found. Must be a registered item (check dependencies?)")
- Guns4d.ammo.registered_bullets[def.itemstring] = table.fill(Default_bullet, def)
+ Guns4d.ammo.registered_bullets[def.itemstring] = Guns4d.table.fill(Default_bullet, def)
end
function Guns4d.ammo.initialize_mag_data(itemstack, meta)
meta = meta or itemstack:get_meta()
@@ -45,9 +45,8 @@ function Guns4d.ammo.update_mag(def, itemstack, meta)
count = count + v
end
local new_wear = max_wear-(max_wear*count/def.capacity)
- --itemstack:set_wear(math.clamp(new_wear, 1, max_wear-1))
+ --itemstack:set_wear(Guns4d.math.clamp(new_wear, 1, max_wear-1))
meta:set_int("guns4d_total_bullets", count)
- meta:set_string("guns4d_next_bullet", current_bullet)
if count > 0 then
meta:set_string("count_meta", tostring(count).."/"..def.capacity)
else
@@ -57,7 +56,7 @@ function Guns4d.ammo.update_mag(def, itemstack, meta)
end
function Guns4d.ammo.register_magazine(def)
- def = table.fill(Default_mag, def)
+ def = Guns4d.table.fill(Default_mag, def)
assert(def.accepted_bullets, "missing property def.accepted_bullets. Need specified bullets to allow for loading")
assert(def.itemstring, "missing item name")
def.accepted_bullets_set = {} --this table is a "lookup" table, I didn't go to college so I have no idea
diff --git a/autogen_docs/index.html b/autogen_docs/index.html
new file mode 100644
index 0000000..64153e6
--- /dev/null
+++ b/autogen_docs/index.html
@@ -0,0 +1,77 @@
+
+
+
+
+ allows you to play one or more sounds with more complex features, so sounds can be easily coded for guns without the need for functions.
+ WARNING: this function modifies the tables passed to it, use Guns4d.table.shallow_copy() for guns4d_soundspecs
+
+
+
{
+ to_player = "singeplayer",
+ min_distance = 100, --soundspec_to_play1 & soundspec_to_play2 share this parameter (as well as the to_player)
+ soundspec_to_play1,
+ soundspec_to_play2
+ }
+
+
+
+
+
+
Returns:
+
+
+ out a list of Minetest sound handles [insert link] (in the order they came)
+
+
+
+
+
+
+
+
Tables
+
+
+
+
+ guns4d_soundspec
+
+
+ defines a sound.
+ This is passed to minetest.sound_play as a sound parameter table
+ however has the following changed or guns4d specific parameters.
+
+
+
Fields:
+
+
min_hear_distance
+ this is useful if you wish to play a sound which has a "far" sound, such as distant gunshots. incompatible with to_player
+
3dguns remastered. Currently a work in progress that is updating steadily (kind of).
+
+
+
+
+
+generated by LDoc 1.5.0
+Last updated 2024-01-19 14:08:01
+
+
+
+
diff --git a/classes/Ammo_handler.lua b/classes/Ammo_handler.lua
index fcb0974..3cb2a4a 100644
--- a/classes/Ammo_handler.lua
+++ b/classes/Ammo_handler.lua
@@ -34,6 +34,7 @@ function Ammo_handler:update_meta(bullets)
meta:set_string("guns4d_loaded_bullets", bullets or minetest.serialize(self.ammo.loaded_bullets))
meta:set_int("guns4d_total_bullets", self.ammo.total_bullets)
meta:set_string("guns4d_next_bullet", self.ammo.next_bullet)
+ self.ammo.magazine_psuedo_empty = false
if self.gun.ammo_handler then --if it's a first occourance it cannot work.
self.gun:update_image_and_text_meta(meta)
end
@@ -45,7 +46,7 @@ function Ammo_handler:spend_round()
local bullet_spent = self.ammo.next_bullet
local meta = self.gun.meta
--subtract the bullet
- if self.ammo.total_bullets > 0 then
+ if (self.ammo.total_bullets > 0) and (bullet_spent ~= "empty") then
--only actually subtract the round if infinite_ammo is false.
if not self.handler.infinite_ammo then
self.ammo.loaded_bullets[bullet_spent] = self.ammo.loaded_bullets[bullet_spent]-1
@@ -54,7 +55,7 @@ function Ammo_handler:spend_round()
end
--set the new current bullet
if next(self.ammo.loaded_bullets) then
- self.ammo.next_bullet = math.weighted_randoms(self.ammo.loaded_bullets)
+ self.ammo.next_bullet = Guns4d.math.weighted_randoms(self.ammo.loaded_bullets)
meta:set_string("guns4d_next_bullet", self.ammo.next_bullet)
else
self.ammo.next_bullet = "empty"
@@ -65,7 +66,10 @@ function Ammo_handler:spend_round()
return bullet_spent
end
end
-function Ammo_handler:load_magazine()
+function Ammo_handler:close_bolt()
+ self.ammo.next_bullet = Guns4d.math.weighted_randoms(self.ammo.loaded_bullets) or "empty"
+end
+function Ammo_handler:load_magazine(charge)
assert(self.instance, "attempt to call object method on a class")
local inv = self.inventory
local magstack_index
@@ -108,7 +112,8 @@ function Ammo_handler:load_magazine()
self.ammo.loaded_mag = magstack:get_name()
self.ammo.loaded_bullets = minetest.deserialize(bullet_string)
self.ammo.total_bullets = magstack_meta:get_int("guns4d_total_bullets")
- self.ammo.next_bullet = magstack_meta:get_string("guns4d_next_bullet")
+ self.ammo.next_bullet = ((not charge) and "empty") or Guns4d.math.weighted_randoms(self.ammo.loaded_bullets)
+ print(dump(self.ammo.next_bullet), dump(Guns4d.math.weighted_randoms(self.ammo.loaded_bullets)))
self:update_meta()
inv:set_stack("main", magstack_index, "")
@@ -139,7 +144,12 @@ function Ammo_handler:can_load_magazine()
end
return false
end
-
+--state will automatically set reset on update_meta()
+function Ammo_handler:set_unloading(bool)
+ self.ammo.magazine_psuedo_empty = bool
+ self.gun:update_image_and_text_meta()
+ self.gun.player:set_wielded_item(self.gun.itemstack)
+end
function Ammo_handler:unload_magazine(to_ground)
assert(self.instance, "attempt to call object method on a class")
if self.ammo.loaded_mag ~= "empty" then
@@ -150,7 +160,6 @@ function Ammo_handler:unload_magazine(to_ground)
--set the mag's meta before updating ours and adding the item.
magmeta:set_string("guns4d_loaded_bullets", gunmeta:get_string("guns4d_loaded_bullets"))
magmeta:set_string("guns4d_total_bullets", gunmeta:get_string("guns4d_total_bullets"))
- magmeta:set_string("guns4d_next_bullet", gunmeta:get_string("guns4d_next_bullet"))
magstack = Guns4d.ammo.update_mag(nil, magstack, magmeta)
--throw it on the ground if to_ground is true
local remaining
diff --git a/classes/Bullet_ray.lua b/classes/Bullet_ray.lua
index d06fd06..1a82765 100644
--- a/classes/Bullet_ray.lua
+++ b/classes/Bullet_ray.lua
@@ -115,22 +115,20 @@ function ray:_iterate(initialized)
local distance = vector.distance(self.pos, end_pos)
if self.state == "free" then
self.energy = self.energy-(distance*self.energy_dropoff)
- if distance ~= self.pos+(self.dir*self.range) then
+
+ if next_state == "transverse" then
+ print(vector.distance(self.pos, end_pos), vector.distance(self.pos, self.pos+(self.dir*self.range)))
self:bullet_hole(end_pos, end_normal)
end
else
- if self.history[#self.history].state == "free" then
- self:bullet_hole(self.pos, self.history[#self.history-1].normal)
- end
+ --add exit holes
if next_state == "free" then
self:bullet_hole(end_pos, end_normal)
end
+ --calc penetration loss from traveling through the block
local penetration_loss = distance*Guns4d.node_properties[self.last_node_name].mmRHA
--calculate our energy loss based on the percentage of energy our penetration represents.
self.energy = self.energy-((self.init_energy*self.energy_sharp_ratio)*(penetration_loss/self.sharp_penetration))
- end
- if self.state ~= self.next_state then
-
end
--set values for next iteration.
self.range = self.range-distance
@@ -187,11 +185,11 @@ function ray:hit_entity(object)
local resistance = object:get_armor_groups() -- support for different body parts is needed here, that's for... a later date, though.
local sharp_pen = self.sharp_penetration-(self.sharp_penetration*(self.energy/self.init_energy)*self.energy_sharp_ratio)
- sharp_pen = math.clamp(sharp_pen - (resistance.guns4d_mmRHA or 0), 0, 65535)
+ sharp_pen = Guns4d.math.clamp(sharp_pen - (resistance.guns4d_mmRHA or 0), 0, 65535)
local converted_Pa = (resistance.guns4d_mmRHA or 0) * self.mmRHA_to_Pa_energy_ratio
local blunt_pen = converted_Pa+(self.blunt_penetration-(self.blunt_penetration*(self.energy/self.init_energy)*(1-self.energy_sharp_ratio)))
- blunt_pen = math.clamp(blunt_pen - (resistance.guns4d_Pa or 0), 0, 65535)
+ blunt_pen = Guns4d.math.clamp(blunt_pen - (resistance.guns4d_Pa or 0), 0, 65535)
self:apply_damage(object, sharp_pen, blunt_pen)
--raw damage first
diff --git a/classes/Control_handler.lua b/classes/Control_handler.lua
index 40b144e..a64a452 100644
--- a/classes/Control_handler.lua
+++ b/classes/Control_handler.lua
@@ -35,40 +35,44 @@ function controls:update(dt)
local pressed = self.player_pressed
local call_queue = {} --so I need to have a "call" queue so I can tell the functions the names of other active controls (busy_list)
local busy_list = self.busy_list or {} --list of controls that have their conditions met. Has to be reset at END of update, so on_use and on_secondary_use can be marked
- for i, control in pairs(self.controls) do
- if not (i=="on_use") and not (i=="on_secondary_use") then
- local def = control
- local data = control.data
- local conditions_met = true
- --check no conditions are false
- for _, key in pairs(control.conditions) do
- if not pressed[key] then conditions_met = false break end
- end
- if conditions_met then
- busy_list[i] = true
- data.timer = data.timer - dt
- --when time is over, if it wasnt held (or loop is active) then reset and call the function.
- --held indicates wether the function was called (as active) before last step.
- if data.timer <= 0 and ((not data.held) or def.loop) then
- data.held = true
- table.insert(call_queue, {control=def, active=true, interrupt=false, data=data})
- elseif def.call_before_timer and not data.held then --this is useful for functions that need to play animations for their progress.
- table.insert(call_queue, {control=def, active=false, interrupt=false, data=data})
+ if not (self.gun.rechamber_time > 0 and self.gun.ammo_handler.ammo.next_bullet == "empty") then --check if the gun is being charged.
+ for i, control in pairs(self.controls) do
+ if not (i=="on_use") and not (i=="on_secondary_use") then
+ local def = control
+ local data = control.data
+ local conditions_met = true
+ --check no conditions are false
+ for _, key in pairs(control.conditions) do
+ if not pressed[key] then conditions_met = false break end
end
- else
- data.held = false
- --detect interrupts, check if the timer was in progress
- if data.timer ~= def.timer then
- table.insert(call_queue, {control=def, active=false, interrupt=true, data=data})
- data.timer = def.timer
+ if conditions_met then
+ busy_list[i] = true
+ data.timer = data.timer - dt
+ --when time is over, if it wasnt held (or loop is active) then reset and call the function.
+ --held indicates wether the function was called (as active) before last step.
+ if data.timer <= 0 and ((not data.held) or def.loop) then
+ data.held = true
+ table.insert(call_queue, {control=def, active=true, interrupt=false, data=data})
+ elseif def.call_before_timer and not data.held then --this is useful for functions that need to play animations for their progress.
+ table.insert(call_queue, {control=def, active=false, interrupt=false, data=data})
+ end
+ else
+ data.held = false
+ --detect interrupts, check if the timer was in progress
+ if data.timer ~= def.timer then
+ table.insert(call_queue, {control=def, active=false, interrupt=true, data=data})
+ data.timer = def.timer
+ end
end
end
end
+ for i, tbl in pairs(call_queue) do
+ tbl.control.func(tbl.active, tbl.interrupt, tbl.data, busy_list, self.handler.gun, self.handler)
+ end
+ self.busy_list = {}
+ elseif self.busy_list then
+ self.busy_list = nil
end
- for i, tbl in pairs(call_queue) do
- tbl.control.func(tbl.active, tbl.interrupt, tbl.data, busy_list, self.handler.gun, self.handler)
- end
- self.busy_list = {}
end
function controls:on_use(itemstack, pointed_thing)
assert(self.instance, "attempt to call object method on a class")
@@ -87,7 +91,7 @@ function controls.construct(def)
if def.instance then
assert(def.controls, "no controls provided")
assert(def.player, "no player provided")
- def.controls = table.deep_copy(def.controls)
+ def.controls = Guns4d.table.deep_copy(def.controls)
def.busy_list = {}
def.handler = Guns4d.players[def.player:get_player_name()]
for i, control in pairs(def.controls) do
diff --git a/classes/Dynamic_crosshair.lua b/classes/Dynamic_crosshair.lua
index fca98ea..752daa2 100644
--- a/classes/Dynamic_crosshair.lua
+++ b/classes/Dynamic_crosshair.lua
@@ -30,7 +30,7 @@ end
local function render_length(rotation, fov)
local dir = vector.rotate({x=0,y=0,z=1}, {x=rotation.x*math.pi/180,y=0,z=0})
vector.rotate(dir,{x=0,y=rotation.y*math.pi/180,z=0})
- local out = rltv_point_to_hud(dir, fov, 1)
+ local out = Guns4d.rltv_point_to_hud(dir, fov, 1)
return math.sqrt(out.x^2+out.y^2)
end
function Dynamic_crosshair:update(dt)
@@ -85,13 +85,13 @@ function Dynamic_crosshair:update(dt)
--now figure out what frame will be our correct spread
- local offset = rltv_point_to_hud(dir, fov, 1) --pretend it's a 1:1 ratio so we can do things correctly.
+ local offset = Guns4d.rltv_point_to_hud(dir, fov, 1) --pretend it's a 1:1 ratio so we can do things correctly.
local length = math.sqrt(offset.x^2+offset.y^2) --get the max length.
local img_perc = (self.scale*2*handler.wininfo.real_hud_scaling*self.width)/handler.wininfo.size.x --the percentage that the hud element takes up
local frame = length/img_perc --the percentage of the size the length takes up.
frame = math.floor(self.frames*frame)
- frame = math.clamp(frame, 0, self.frames-1)
+ frame = Guns4d.math.clamp(frame, 0, self.frames-1)
--"^[vertical_frame:"..self.frames..":"..frame
self.player:hud_change(self.hud, "text", self.image.."^[verticalframe:"..self.frames..":"..frame)
else
diff --git a/classes/Gun.lua b/classes/Gun.lua
index 801582d..92d1ac4 100644
--- a/classes/Gun.lua
+++ b/classes/Gun.lua
@@ -84,17 +84,22 @@ local gun_default = {
},
breathing_scale = .5, --the max angluler offset caused by breathing.
controls = { --used by control_handler
- __overfill=true, --if present, this table will not be filled in.
+ __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
},
+ charging = { --how the gun "cocks"
+ require_charge_on_swap = true,
+ 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.
+ default_charge_time = 1,
+ },
reload = { --used by defualt controls. Still provides usefulness elsewhere.
- __overfill=true, --if present, this table will not be filled in.
- {type="unload", time=1, anim="unload", interupt="to_ground", hold = true},
- {type="load", time=1, anim="load"}
+ __overfill=true,
+ --{type="unload_mag", time=1, anim="unload_mag", interupt="to_ground", hold = true, sound = {sound = "load magazine", pitch = {min=.9, max=1.1}}},
+ --{type="load", time=1, anim="load"}
},
ammo = { --used by ammo_handler
magazine_only = false,
@@ -105,6 +110,7 @@ local gun_default = {
--mesh
backface_culling = true,
root = "gun",
+ magazine = "magazine",
arm_right = "right_aimpoint",
arm_left = "left_aimpoint",
animations = { --used by animations handler for idle, and default controls
@@ -112,6 +118,35 @@ local gun_default = {
loaded = {x=1,y=1},
},
},
+ sounds = { --this does not contain reload sound effects.
+ fire = {
+ {
+ 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
+ }
+ },
+ {
+ sound = "ar_firing_far",
+ min_hear_distance = 40,
+ max_hear_distance = 600,
+ pitch = {
+ min = .95,
+ max = 1.05
+ },
+ gain = {
+ min = .9,
+ max = 1
+ }
+ }
+ },
+ },
--inventory_image
--inventory_image_empty
--used by ammo_handler
@@ -168,6 +203,7 @@ local gun_default = {
KEYFRAME_SAMPLE_PRECISION = .1, --[[what frequency to take precalcualted keyframe samples. The lower this is the higher the memory allocation it will need- though minimal.
This will fuck shit up if you change it after gun construction/inheritence (interpolation between precalculated vectors will not work right)]]
WAG_CYCLE_SPEED = 1.6,
+ DEFAULT_MAX_HEAR_DISTANCE = 10,
DEFAULT_FPS = 60,
WAG_DECAY = 1, --divisions per second
HAS_RECOIL = true,
@@ -189,6 +225,7 @@ local gun_default = {
}
]]
},
+ bolt_charged = false,
particle_spawners = {},
current_firemode = 1,
walking_tick = 0,
@@ -202,7 +239,16 @@ local gun_default = {
function gun_default.multiplier_coefficient(multiplier, ratio)
return 1+((multiplier*ratio)-ratio)
end
---update the gun, da meat and da potatoes
+function gun_default:charge()
+ assert(self.instance, "attempt to call object method on a class")
+ local props = self.properties
+ if props.visuals.animations.charge then
+ self:set_animation(props.visuals.animations.charge, props.charging.default_charge_time)
+ end
+ self.ammo_handler:close_bolt()
+ self.rechamber_time = props.charging.default_charge_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
@@ -251,6 +297,13 @@ function gun_default:update(dt)
self.crosshair:update()
end
+ --automatically cock if uncocked.
+ local ammo = self.ammo_handler.ammo
+ --[[if ammo.total_bullets and (ammo.total_bullets > 0 and ammo.next_bullet == "empty") then
+ self:charge()
+ end]]
+ print(dump(self.ammo_handler.ammo.next_bullet))
+
local offsets = self.offsets
--local player_axial = offsets.recoil.player_axial + offsets.walking.player_axial + offsets.sway.player_axial + offsets.breathing.player_axial
--local gun_axial = offsets.recoil.gun_axial + offsets.walking.gun_axial + offsets.sway.gun_axial
@@ -298,9 +351,9 @@ function gun_default:update_image_and_text_meta(meta)
end
--pick the image
local image = self.properties.inventory_image
- if ammo.total_bullets > 0 then
+ 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 == "") then
+ 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
@@ -321,9 +374,10 @@ function gun_default:attempt_fire()
if spent_bullet and spent_bullet ~= "empty" then
local dir = self.dir
local pos = self.pos
- local bullet_def = table.fill(Guns4d.ammo.registered_bullets[spent_bullet], {
+ local bullet_def = Guns4d.table.fill(Guns4d.ammo.registered_bullets[spent_bullet], {
player = self.player,
- pos = pos,
+ --we don't want it to be doing fuckshit and letting players shoot through walls.
+ pos = pos-((self.handler.control_handler.ads and dir*self.properties.ads.offset.z) or dir*self.properties.hip.offset.z),
dir = dir,
gun = self
})
@@ -333,19 +387,29 @@ function gun_default:attempt_fire()
end
self:recoil()
self:muzzle_flash()
+
+ local fire_sound = Guns4d.table.deep_copy(self.properties.sounds.fire) --important that we copy because play_sounds modifies it.
+ fire_sound.pos = self.pos
+ Guns4d.play_sounds(fire_sound)
+
self.rechamber_time = 60/self.properties.firerateRPM
return true
end
end
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]
- *math.rand_sign((rprops.bias[axis][i]/2)+.5))
+ *rand_sign((rprops.bias[axis][i]/2)+.5))
*self.multiplier_coefficient(rprops.hipfire_multiplier[axis], 1-self.handler.ads_location)
end
end
@@ -539,7 +603,7 @@ 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 = math.clamp(self.velocities.recoil[axis][i],-self.properties.recoil.angular_velocity_max[axis],self.properties.recoil.angular_velocity_max[axis])
+ 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
if math.abs(recoil_vel) > 0.01 then
@@ -560,7 +624,7 @@ function gun_default:update_recoil(dt)
if math.abs(recoil) > 0.001 then
local correction_multiplier = self.time_since_last_fire*self.properties.recoil.target_correction_factor[axis]
local correction_value = recoil*correction_multiplier
- correction_value = math.clamp(math.abs(correction_value), 0, self.properties.recoil.target_correction_max_rate[axis])
+ correction_value = Guns4d.math.clamp(math.abs(correction_value), 0, self.properties.recoil.target_correction_max_rate[axis])
recoil=recoil-(correction_value*dt*(math.abs(recoil)/recoil))
--prevent overcorrection
if math.abs(recoil) > math.abs(old_recoil) then
@@ -576,7 +640,7 @@ function gun_default:update_animation(dt)
local ent = self.entity
local data = self.animation_data
data.runtime = data.runtime + dt
- data.current_frame = math.clamp(data.current_frame+(dt*data.fps), data.frames.x, data.frames.y)
+ 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
@@ -743,7 +807,7 @@ gun_default.construct = function(def)
def.meta = meta
--create ID so we can track switches between weapons, also get some other data.
if meta:get_string("guns4d_id") == "" then
- local id = tostring(Unique_id.generate())
+ local id = tostring(Guns4d.unique_id.generate())
meta:set_string("guns4d_id", id)
def.player:set_wielded_item(def.itemstack)
def.id = id
@@ -756,10 +820,15 @@ gun_default.construct = function(def)
def.ammo_handler = def.properties.ammo_handler:new({ --initialize ammo handler from gun and gun metadata.
gun = def
})
+ local ammo = def.ammo_handler.ammo
+ if def.properties.require_charge_on_swap then
+ ammo.next_bullet = "empty"
+ end
+ minetest.after(0, function() if ammo.total_bullets > 0 then def:charge() end end)
def:update_image_and_text_meta() --has to be called manually in post as ammo_handler would not exist yet.
def.player:set_wielded_item(def.itemstack)
--unavoidable table instancing
- def.properties = table.fill(def.base_class.properties, def.properties)
+ def.properties = Guns4d.table.fill(def.base_class.properties, def.properties)
def.particle_spawners = {} --Instantiatable_class only shallow copies. So tables will not change, and thus some need to be initialized.
def.property_modifiers = {}
def.total_offset_rotation = {
@@ -768,7 +837,7 @@ gun_default.construct = function(def)
}
def.player_rotation = Vec.new()
--initialize all offsets
- --def.offsets = table.deep_copy(def.base_class.offsets)
+ --def.offsets = Guns4d.table.deep_copy(def.base_class.offsets)
def.offsets = {}
for offset, tbl in pairs(def.base_class.offsets) do
def.offsets[offset] = {}
@@ -781,7 +850,7 @@ gun_default.construct = function(def)
end
end
def.animation_rotation = vector.new()
- --def.velocities = table.deep_copy(def.base_class.velocities)
+ --def.velocities = Guns4d.table.deep_copy(def.base_class.velocities)
def.velocities = {}
for i, tbl in pairs(def.base_class.velocities) do
def.velocities[i] = {}
@@ -817,8 +886,8 @@ gun_default.construct = function(def)
end
--fill in the properties.
- def.properties = table.fill(def.parent_class.properties, props or {})
- def.consts = table.fill(def.parent_class.consts, def.consts or {})
+ def.properties = Guns4d.table.fill(def.parent_class.properties, props or {})
+ def.consts = Guns4d.table.fill(def.parent_class.consts, def.consts or {})
props = def.properties --have to reinitialize this as the reference is replaced.
--print(table.tostring(props))
@@ -856,13 +925,13 @@ gun_default.construct = function(def)
table.insert(def.b3d_model.global_frames.rotation, newvec)
end
end
- if main then
+ --[[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)))
end
for i, v in pairs(def.b3d_model.global_frames.rotation) do
print(i, dump(vector.new(v:to_euler_angles_unpack())*180/math.pi))
- end
+ end]]
--print()
-- if it's not a template, then create an item, override some props
if def.name ~= "__template" then
diff --git a/classes/Player_handler.lua b/classes/Player_handler.lua
index 6e0414d..bd7138f 100644
--- a/classes/Player_handler.lua
+++ b/classes/Player_handler.lua
@@ -39,7 +39,7 @@ function player_handler:update(dt)
self.player_model_handler = nil
end
self.player_model_handler = Guns4d.player_model_handler.get_handler(self:get_properties().mesh):new({player=self.player})
- self.control_handler = Guns4d.control_handler:new({player=player, controls=self.gun.properties.controls})
+ self.control_handler = Guns4d.control_handler:new({player=player, controls=self.gun.properties.controls, gun=self.gun})
--this needs to be stored for when the gun is unset!
self.horizontal_offset = self.gun.properties.ads.horizontal_offset
@@ -50,7 +50,7 @@ function player_handler:update(dt)
--for the gun's scopes to work properly we need predictable offsets.
end
--update some properties.
- self.look_rotation.x, self.look_rotation.y = math.clamp((player:get_look_vertical() or 0)*180/math.pi, -80, 80), -player:get_look_horizontal()*180/math.pi
+ self.look_rotation.x, self.look_rotation.y = Guns4d.math.clamp((player:get_look_vertical() or 0)*180/math.pi, -80, 80), -player:get_look_horizontal()*180/math.pi
if TICK % 10 == 0 then
self.wininfo = minetest.get_player_window_information(self.player:get_player_name())
end
@@ -80,13 +80,13 @@ function player_handler:update(dt)
--eye offsets and ads_location
if (self.control_handler and self.control_handler.ads) and (self.ads_location<1) then
--if aiming, then increase ADS location
- self.ads_location = math.clamp(self.ads_location + (dt/self.gun.properties.ads.aim_time), 0, 1)
+ self.ads_location = Guns4d.math.clamp(self.ads_location + (dt/self.gun.properties.ads.aim_time), 0, 1)
elseif ((not self.control_handler) or (not self.control_handler.ads)) and self.ads_location>0 then
local divisor = .2
if self.gun then
divisor = self.gun.properties.ads.aim_time/self.gun.consts.AIM_OUT_AIM_IN_SPEED_RATIO
end
- self.ads_location = math.clamp(self.ads_location - (dt/divisor), 0, 1)
+ self.ads_location = Guns4d.math.clamp(self.ads_location - (dt/divisor), 0, 1)
end
self.look_offset.x = self.horizontal_offset*self.ads_location
@@ -162,7 +162,7 @@ end
function player_handler:set_properties(properties)
assert(self.instance, "attempt to call object method on a class")
self.player:set_properties(properties)
- self.properties = table.fill(self.properties, properties)
+ self.properties = Guns4d.table.fill(self.properties, properties)
end
function player_handler:is_holding_gun()
assert(self.instance, "attempt to call object method on a class")
@@ -208,7 +208,7 @@ function player_handler.construct(def)
def[i] = v
end
end
- def.look_rotation = table.deep_copy(player_handler.look_rotation)
+ def.look_rotation = Guns4d.table.deep_copy(player_handler.look_rotation)
def.infinite_ammo = minetest.check_player_privs(def.player, Guns4d.config.infinite_ammo_priv)
end
end
diff --git a/classes/Sprite_scope.lua b/classes/Sprite_scope.lua
index a3a71c3..9e629a4 100644
--- a/classes/Sprite_scope.lua
+++ b/classes/Sprite_scope.lua
@@ -31,9 +31,9 @@ local Sprite_scope = Instantiatable_class:inherit({
def.player = def.gun.player
def.handler = def.gun.handler
def.elements = {}
- local new_images = table.deep_copy(def.images)
+ local new_images = Guns4d.table.deep_copy(def.images)
if def.images then
- def.images = table.fill(new_images, def.images)
+ def.images = Guns4d.table.fill(new_images, def.images)
end
for i, v in pairs(def.images) do
def.elements[i] = def.player:hud_add{
@@ -63,12 +63,12 @@ function Sprite_scope:update()
dir = dir + (self.gun.properties.ads.offset+vector.new(self.gun.properties.ads.horizontal_offset,0,0))*0
end
local fov = self.player:get_fov()
- local real_aim = rltv_point_to_hud(dir, fov, ratio)
- local anim_aim = rltv_point_to_hud(vector.rotate({x=0,y=0,z=1}, self.gun.animation_rotation*math.pi/180), fov, ratio)
+ local real_aim = Guns4d.rltv_point_to_hud(dir, fov, ratio)
+ local anim_aim = Guns4d.rltv_point_to_hud(vector.rotate({x=0,y=0,z=1}, self.gun.animation_rotation*math.pi/180), fov, ratio)
real_aim.x = real_aim.x+anim_aim.x; real_aim.y = real_aim.y+anim_aim.y
--print(dump(self.gun.animation_rotation))
- local paxial_aim = rltv_point_to_hud(self.gun.local_paxial_dir, fov, ratio)
+ local paxial_aim = Guns4d.rltv_point_to_hud(self.gun.local_paxial_dir, fov, ratio)
--so custom scopes can do their thing without doing more calcs
self.hud_projection_real = real_aim
self.hud_projection_paxial = paxial_aim
diff --git a/default_controls.lua b/default_controls.lua
index c0e486b..1265fc7 100644
--- a/default_controls.lua
+++ b/default_controls.lua
@@ -46,6 +46,7 @@ Guns4d.default_controls.reload = {
conditions = {"zoom"},
loop = false,
timer = 0, --1 so we have a call to initialize the timer.
+ --remember that the data table allows us to store arbitrary data
func = function(active, interrupted, data, busy_list, gun, handler)
local ammo_handler = gun.ammo_handler
local props = gun.properties
@@ -55,28 +56,37 @@ Guns4d.default_controls.reload = {
end
local this_state = props.reload[data.state]
local next_state_index = data.state
+ local next_state = props.reload[next_state_index+1]
+ --this elseif chain has gotten egregiously long, so I'll have to create a system for registering these reload states eventually- both for the sake of organization aswell as a modular API.
if next_state_index == 0 then
-
+ --nothing to do, let animations get set down the line.
next_state_index = next_state_index + 1
elseif type(this_state.type) == "function" then
-
this_state.type(true, handler, gun)
- elseif this_state.type == "unload" then
- local pause = false
- local next = props.reload[next_state_index+1]
- if (next.type=="load_fractional" or next.type=="load") and (not ammo_handler:inventory_has_ammo()) then
- pause=true
+ elseif this_state.type == "unload_mag" then
+
+ next_state_index = next_state_index + 1
+ if next_state and next_state.type == "store" then
+ ammo_handler:set_unloading(true) --if interrupted it will drop to ground, so just make it appear as if the gun is already unloaded.
+ else
+ ammo_handler:unload_magazine(true) --unload to ground
end
+ elseif this_state.type == "store" then
+
+ local pause = false
+ --needs to happen before so we don't detect the ammo we just unloaded
+ if next_state and (next_state.type=="load_fractional" or next_state.type=="load") and (not ammo_handler:inventory_has_ammo()) then
+ pause=true
+ end
if props.ammo.magazine_only and (ammo_handler.ammo.loaded_mag ~= "empty") then
ammo_handler:unload_magazine()
else
ammo_handler:unload_all()
end
-
--if there's no ammo make hold so you don't reload the same ammo you just unloaded.
if pause then
return
@@ -91,17 +101,26 @@ Guns4d.default_controls.reload = {
else
ammo_handler:load_flat()
end
- next_state_index = next_state_index +1
+
+ if not (next_state or (next_state.type ~= "charge")) then
+ --chamber the round automatically.
+ ammo_handler:close_bolt()
+ end
+ next_state_index = next_state_index + 1
+
+ elseif this_state.type == "charge" then
+
+ next_state_index = next_state_index + 1
+ ammo_handler:close_bolt()
+ --if not
elseif this_state.type == "unload_fractional" then
-
ammo_handler:unload_fractional()
if ammo_handler.ammo.total_bullets == 0 then
next_state_index = next_state_index + 1
end
elseif this_state.type == "load_fractional" then
-
ammo_handler:load_fractional()
if ammo_handler.ammo.total_bullets == props.ammo.capacity then
next_state_index = next_state_index + 1
@@ -117,76 +136,107 @@ Guns4d.default_controls.reload = {
--check that the next states are actually valid, if not, skip them
local valid_state = false
while not valid_state do
- local state_changed = false
next_state = props.reload[next_state_index]
if next_state then
- local state_changed = false
-
- if next_state.type == "unload" then
+ --determine wether the next_state is valid (can actually be completed)
+ local invalid_state = false
+ if next_state.type == "store" then
if props.ammo.magazine_only and (ammo_handler.ammo.loaded_mag == "empty") then
- state_changed = true
+ invalid_state = true
end
+ --need to check for inventory room, because otherwise we just want to drop it to the ground.
+ --[[
+ if ... then
+ if props.ammo.magazine_only and (ammo_handler.ammo.loaded_mag ~= "empty") then
+ ammo_handler:unload_magazine(true)
+ else
+ ammo_handler:unload_all(true)
+ end
+ end
+ ]]
- elseif next_state.type == "unload_fractional" then
+ --[[elseif next_state.type == "unload_fractional" then --UNIMPLEMENTED
if not ammo_handler.ammo.total_bullets > 0 then
- state_changed = true
+ invalid_state = true
+ end]]
+ elseif next_state.type == "unload_mag" then
+
+ if ammo_handler.ammo.loaded_mag == "empty" then
+ invalid_state = true
end
elseif next_state.type == "load" then
+ --check we have ammo
if props.ammo.magazine_only then
if not ammo_handler:can_load_magazine() then
- state_changed = true
+ invalid_state = true
end
else
if not ammo_handler:can_load_flat() then
- state_changed = true
+ invalid_state = true
end
end
end
- if not state_changed then
+ if not invalid_state then
valid_state=true
else
next_state_index = next_state_index + 1
next_state = props.reload[next_state_index]
end
else
+ --if the next state doesn't exist, we've reached the end (the gun is reloaded) and we should restart. "held" so it doesn't continue unless the user lets go of the input button.
data.state = 0
data.timer = 0.5
data.held = true
return
end
end
- --check if we're at cycle end
- if next_state == nil then
+ --I don't think this is needed given the above.
+ --[[ if next_state == nil then
data.state = 0
data.timer = 0
data.held = true
return
- else
- data.state = next_state_index
- data.timer = next_state.time
- data.held = false
- local anim = next_state.anim
- if type(next_state.anim) == "string" then
- anim = props.visuals.animations[next_state.anim]
- end
- if anim then
+ else]]
+ data.state = next_state_index
+ data.timer = next_state.time
+ data.held = false
+ local anim = next_state.anim
+ if type(next_state.anim) == "string" then
+ anim = props.visuals.animations[next_state.anim]
+ end
+ if anim then
+ if anim.x and anim.y then
gun:set_animation(anim, next_state.time)
+ else
+ minetest.log("error", "improperly set gun reload animation, reload state `"..next_state.type.."`, gun `"..gun.itemstring.."`")
end
end
+ if next_state.sounds then
+ local sounds = Guns4d.table.deep_copy(props.reload[next_state_index].sounds)
+ sounds.pos = gun.pos
+ sounds.max_hear_distance = sounds.max_hear_distance or gun.consts.DEFAULT_MAX_HEAR_DISTANCE
+ data.played_sounds = Guns4d.play_sounds(sounds)
+ end
+ print(dump(next_state_index))
+ --end
elseif interrupted then
local this_state = props.reload[data.state]
- if this_state and (this_state.type == "unload") and (this_state.interupt == "to_ground") then
- --true indicates to_ground (meaning they will be removed)
+ if this_state and (this_state.type == "store") then
+ --if the player was about to store the mag, eject it.
if props.ammo.magazine_only and (ammo_handler.ammo.loaded_mag ~= "empty") then
- ammo_handler:unload_magazine(true)
+ ammo_handler:unload_magazine(true) --"true" is for to_ground
else
ammo_handler:unload_all(true)
end
end
+ if data.played_sounds then
+ Guns4d.stop_sounds(data.played_sounds)
+ data.played_sounds = nil
+ end
gun:clear_animation()
data.state = 0
end
diff --git a/ldoc/config.ld b/ldoc/config.ld
new file mode 100644
index 0000000..839d8aa
--- /dev/null
+++ b/ldoc/config.ld
@@ -0,0 +1,11 @@
+project="4dguns"
+title="4dguns documentation"
+description="THEE ultimate 3d gun library."
+format="markdown"
+backtick_references=false
+file = {
+ "../",
+}
+dir='../autogen_docs'
+readme='../README.md'
+style='!new'
diff --git a/ldoc/install_and_build_docs b/ldoc/install_and_build_docs
new file mode 100644
index 0000000..cdf02a1
--- /dev/null
+++ b/ldoc/install_and_build_docs
@@ -0,0 +1,13 @@
+#! /bin/sh
+
+# on github, leafo/gh-actions-lua leafo/gh-actions-luarocks setup luarocks for us.
+#~ sudo apt-get install lua5.3 liblua5.3-dev luarocks
+
+# github ldoc is far ahead of the released version.
+echo ldoc version:
+git ls-remote https://github.com/lunarmodules/LDoc master
+luarocks --local install https://raw.githubusercontent.com/lunarmodules/LDoc/master/ldoc-scm-3.rockspec
+
+echo
+cd ./doc
+~/.luarocks/bin/ldoc .
diff --git a/ldoc/windows_quick_generate.bat b/ldoc/windows_quick_generate.bat
new file mode 100644
index 0000000..a025533
--- /dev/null
+++ b/ldoc/windows_quick_generate.bat
@@ -0,0 +1,3 @@
+# literally just so I dont have to open powershell every time.
+@echo off
+ldoc .
\ No newline at end of file
diff --git a/misc_helpers.lua b/misc_helpers.lua
index 4654a36..794c345 100644
--- a/misc_helpers.lua
+++ b/misc_helpers.lua
@@ -1,14 +1,15 @@
---can't be copyright claimed by myself, luckily... well actually knowing the legal system I probably could sue myself.
-Unique_id = {
+--- misc. common tools for 4dguns
+-- @script misc_helpers
+
+Guns4d.math = {}
+Guns4d.table = {}
+
+--store this so there arent duplicates
+Guns4d.unique_id = {
generated = {},
}
-function math.clamp(val, lower, upper)
- if lower > upper then lower, upper = upper, lower end
- return math.max(lower, math.min(upper, val))
-end
---I need to store this so there arent duplicates lol
-function Unique_id.generate()
- local genned_ids = Unique_id.generated
+function Guns4d.unique_id.generate()
+ local genned_ids = Guns4d.unique_id.generated
local id = string.sub(tostring(math.random()), 3)
while genned_ids[id] do
id = string.sub(tostring(math.random()), 3)
@@ -16,8 +17,24 @@ function Unique_id.generate()
genned_ids[id] = true
return id
end
---i probably should stop violating the math namespace, but I'll worry about that *later*
-function math.weighted_randoms(tbl)
+
+---math helpers.
+-- in guns4d.math
+--@section math
+
+--all of the following is disgusting and violates the namespace because I got used to love2d.
+function Guns4d.math.clamp(val, lower, upper)
+ if lower > upper then lower, upper = upper, lower end
+ return math.max(lower, math.min(upper, val))
+end
+--- picks a random index, with odds based on it's value. Returns the index of the selected.
+-- @param tbl a table containing weights, example
+-- {
+-- ["sound"] = 999, --999 in 1000 chance this is chosen
+-- ["rare_sound"] = 1 --1 in 1000 chance this is chosen
+-- }
+-- @function weighted_randoms
+function Guns4d.math.weighted_randoms(tbl)
local total_weight = 0
local new_tbl = {}
for i, v in pairs(tbl) do
@@ -39,38 +56,13 @@ function math.weighted_randoms(tbl)
scaled_weight = scaled_weight + v[2]
end
end
---[[function math.get_rotation(dir)
- local x = math.atan2(dir.y, dir.z)
- local y =-math.atan2(dir.x, dir.z)
- return vector.new(
- x,
- y,
- 0
- )
-end]]
---from luatic's old modlib, doesn't work to fix gimble lock, actually makes things worse (somehow)
-function math.get_rotation(dir)
- return vector.new(
- math.atan2(dir.y, math.sqrt(dir.x^2 + dir.z^2)),
- -math.atan2(dir.x, dir.z),
- 0
- )
-end
-
-function math.rand_sign(b)
- b = b or .5
- local int = 1
- if math.random() > b then int=-1 end
- return int
-end
---weighted randoms
+--[[
--for table vectors that aren't vector objects
----@diagnostic disable-next-line: lowercase-global
-function tolerance_check(a,b,tolerance)
+local function tolerance_check(a,b,tolerance)
return math.abs(a-b) > tolerance
end
-function vector.equals_tolerance(v, vb, tolerance)
+function Guns4d.math.vectors_in_tolerance(v, vb, tolerance)
tolerance = tolerance or 0
return (
tolerance_check(v.x, vb.x, tolerance) and
@@ -78,14 +70,20 @@ function vector.equals_tolerance(v, vb, tolerance)
tolerance_check(v.z, vb.z, tolerance)
)
end
+]]
+
+---table helpers.
+-- in guns4d.table
+--@section table
+
--copy everything
-function table.deep_copy(tbl, copy_metatable, indexed_tables)
+function Guns4d.table.deep_copy(tbl, copy_metatable, indexed_tables)
if not indexed_tables then indexed_tables = {} end
local new_table = {}
local metat = getmetatable(tbl)
if metat then
if copy_metatable then
- setmetatable(new_table, table.deep_copy(metat, true))
+ setmetatable(new_table, Guns4d.table.deep_copy(metat, true))
else
setmetatable(new_table, metat)
end
@@ -94,7 +92,7 @@ function table.deep_copy(tbl, copy_metatable, indexed_tables)
if type(v) == "table" then
if not indexed_tables[v] then
indexed_tables[v] = true
- new_table[i] = table.deep_copy(v, copy_metatable)
+ new_table[i] = Guns4d.table.deep_copy(v, copy_metatable)
end
else
new_table[i] = v
@@ -104,7 +102,7 @@ function table.deep_copy(tbl, copy_metatable, indexed_tables)
end
-function table.contains(tbl, value)
+function Guns4d.table.contains(tbl, value)
for i, v in pairs(tbl) do
if v == value then
return i
@@ -120,7 +118,8 @@ local function parse_index(i)
end
end
--dump() sucks.
-function table.tostring(tbl, shallow, list_length_lim, depth_limit, tables, depth)
+local table_contains = Guns4d.table.contains
+function Guns4d.table.tostring(tbl, shallow, list_length_lim, depth_limit, tables, depth)
--create a list of tables that have been tostringed in this chain
if not table then return "nil" end
if not tables then tables = {this_table = tbl} end
@@ -141,11 +140,11 @@ function table.tostring(tbl, shallow, list_length_lim, depth_limit, tables, dept
if val_type == "string" then
str = str..initial_string..parse_index(i).." = \""..v.."\","
elseif val_type == "table" and (not shallow) then
- local contains = table.contains(tables, v)
+ local contains = table_contains(tables, v)
--to avoid infinite loops, make sure that the table has not been tostringed yet
if not contains then
tables[i] = v
- str = str..initial_string..parse_index(i).." = "..table.tostring(v, shallow, list_length_lim, depth_limit, tables, depth)..","
+ str = str..initial_string..parse_index(i).." = "..Guns4d.table.tostring(v, shallow, list_length_lim, depth_limit, tables, depth)..","
else
str = str..initial_string..parse_index(i).." = "..tostring(v).." (index: '"..tostring(contains).."'),"
end
@@ -158,7 +157,7 @@ function table.tostring(tbl, shallow, list_length_lim, depth_limit, tables, dept
end
return str..string.sub(initial_string, 1, -5).."}"
end
-function table.tostring_structure_only(tbl, shallow, tables, depth)
+--[[function Guns4d.table.tostring_structure_only(tbl, shallow, tables, depth)
--create a list of tables that have been tostringed in this chain
if not table then return "nil" end
if not tables then tables = {this_table = tbl} end
@@ -183,11 +182,11 @@ function table.tostring_structure_only(tbl, shallow, tables, depth)
iterations = iterations + 1
local val_type = type(v)
if val_type == "table" then
- local contains = table.contains(tables, v)
+ local contains = table_contains(tables, v)
--to avoid infinite loops, make sure that the table has not been tostringed yet
if not contains then
tables[parse_index(i).." ["..tostring(v).."]"] = v
- str = str..initial_string..parse_index(i).."("..tostring(v)..") = "..table.tostring_structure_only(v, shallow, tables, depth)..","
+ str = str..initial_string..parse_index(i).."("..tostring(v)..") = "..Guns4d.table.tostring_structure_only(v, shallow, tables, depth)..","
elseif type(v) == "table" then
str = str..initial_string..parse_index(i).." = "..tostring(v)
else
@@ -201,14 +200,14 @@ function table.tostring_structure_only(tbl, shallow, tables, depth)
return "table too long"
end
return "{"..str..string.sub(initial_string, 1, -5).."}"
-end
+end]]
--replace fields (and fill sub-tables) in `tbl` with elements in `replacement`. Recursively iterates all sub-tables. use property __overfill=true for subtables that don't want to be overfilled.
-function table.fill(tbl, replacement, preserve_reference, indexed_tables)
+function Guns4d.table.fill(tbl, replacement, preserve_reference, indexed_tables)
if not indexed_tables then indexed_tables = {} end --store tables to prevent circular referencing
local new_table = tbl
if not preserve_reference then
- new_table = table.deep_copy(tbl)
+ new_table = Guns4d.table.deep_copy(tbl)
end
for i, v in pairs(replacement) do
if new_table[i] then
@@ -218,13 +217,13 @@ function table.fill(tbl, replacement, preserve_reference, indexed_tables)
if not indexed_tables[v] then
if not new_table[i].__overfill then
indexed_tables[v] = true
- new_table[i] = table.fill(tbl[i], replacement[i], false, indexed_tables)
+ new_table[i] = Guns4d.table.fill(tbl[i], replacement[i], false, indexed_tables)
else --if overfill is present, we don't want to preserve the old table.
- new_table[i] = table.deep_copy(replacement[i])
+ new_table[i] = Guns4d.table.deep_copy(replacement[i])
end
end
elseif not replacement[i].__no_copy then
- new_table[i] = table.deep_copy(replacement[i])
+ new_table[i] = Guns4d.table.deep_copy(replacement[i])
else
new_table[i] = replacement[i]
end
@@ -239,7 +238,7 @@ function table.fill(tbl, replacement, preserve_reference, indexed_tables)
return new_table
end
--for class based OOP, ensure values containing a table in btbl are tables in a_tbl- instantiate, but do not fill.
-function table.instantiate_struct(tbl, btbl, indexed_tables)
+--[[function table.instantiate_struct(tbl, btbl, indexed_tables)
if not indexed_tables then indexed_tables = {} end --store tables to prevent circular referencing
for i, v in pairs(btbl) do
if type(v) == "table" and not indexed_tables[v] then
@@ -252,8 +251,8 @@ function table.instantiate_struct(tbl, btbl, indexed_tables)
end
end
return tbl
-end
-function table.shallow_copy(t)
+end]]
+function Guns4d.table.shallow_copy(t)
local new_table = {}
for i, v in pairs(t) do
new_table[i] = v
@@ -261,14 +260,15 @@ function table.shallow_copy(t)
return new_table
end
+---other helpers
+--@section other
+
--for the following function only:
--for license see the link on the next line (direct permission was granted).
--https://github.com/3dreamengine/3DreamEngine
-function rltv_point_to_hud(pos, fov, aspect)
+function Guns4d.rltv_point_to_hud(pos, fov, aspect)
local n = .1 --near
local f = 1000 --far
- --wherever you are
- --I WILL FOLLOWWWW YOU
local scale = math.tan(fov * math.pi / 360)
local r = scale * n * aspect
local t = scale * n
diff --git a/mod.conf b/mod.conf
index 8fbe895..587e56a 100644
--- a/mod.conf
+++ b/mod.conf
@@ -2,4 +2,4 @@ name = guns4d
title = guns4d
description = Adds a library for 3d guns
author = FatalError42O
-depends = mtul_b3d
\ No newline at end of file
+depends = mtul_b3d, mtul_cpml, mtul_filesystem
\ No newline at end of file
diff --git a/play_sound.lua b/play_sound.lua
index 5876d2b..62ddf4c 100644
--- a/play_sound.lua
+++ b/play_sound.lua
@@ -1,3 +1,8 @@
+--- implements tools for quickly playing audio.
+-- @script play_sound
+
+local sqrt = math.sqrt
+
--simple specification for playing a sound in relation to an action, acts as a layer of minetest.play_sound
--"gsp" guns4d-sound-spec
--first person for the gun holder, third person for everyone else. If first not present, third will be used.
@@ -5,8 +10,6 @@
--example:
--[[
additional properties
- first_person = playername,
- second_person = playername
sounds = { --weighted randoms:
fire_fp = .5.
fire_fp_2 = .2.
@@ -19,29 +22,66 @@
gain = 1, --format for pitch and gain is interchangable.
min_hear_distance = 20, --this is for distant gunshots, for example. Entirely optional. Cannot be used with to_player
+ exclude_player
to_player
--when present it automatically plays positionless audio, as this is for first person effects.
]]
-local sqrt = math.sqrt
-function Guns4d.play_sounds(...)
- local args = {...}
+
+--- defines a sound.
+-- This is passed to `minetest.sound_play` as a [ sound parameter table](https://github.com/minetest/minetest/blob/master/doc/lua_api.md#sound-parameter-table)
+-- however has the following changed or guns4d specific parameters.
+-- @field min_hear_distance this is useful if you wish to play a sound which has a "far" sound, such as distant gunshots. incompatible `with to_player`
+-- @field sounds a @{misc_helpers.weighted_randoms| weighted_randoms table} for randomly selecting sounds. The output will overwrite the `sound` field.
+-- @field to_player 4dguns changes `to_player` so it only plays positionless audio (as it is only intended for first person audio)
+-- @table guns4d_soundspec
+
+local function handle_min_max(tbl)
+ return tbl.min+(math.random()*(tbl.max-tbl.min))
+end
+--- allows you to play one or more sounds with more complex features, so sounds can be easily coded for guns without the need for functions.
+-- WARNING: this function modifies the tables passed to it, use `Guns4d.table.shallow_copy()` for guns4d_soundspecs
+-- @param sound_specs a @{guns4d_soundspec} or a list of @{guns4d_soundspec}s indexed my number. Also allows for shared fields. Example:
+-- {
+-- to_player = "singeplayer",
+-- min_distance = 100, --soundspec_to_play1 & soundspec_to_play2 share this parameter (as well as the to_player)
+-- soundspec_to_play1,
+-- soundspec_to_play2
+-- }
+-- @return out a list of Minetest sound handles [insert link] (in the order they came)
+-- @function Guns4d.play_sounds
+function Guns4d.play_sounds(soundspecs_list)
+ --print(dump(soundspecs_list))
+ --support a list of sounds to play
+ if not soundspecs_list[1] then --turn into iteratable format.
+ soundspecs_list = {soundspecs_list}
+ end
+ local applied = {}
+ --all fields that aren't numbers will be copied over, allowing you to set fields across all sounds (i.e. pos, target player.), if already present it will remain the same.
+ for field, v in pairs(soundspecs_list) do
+ if type(field) ~= "number" then
+ for _, spec in ipairs(soundspecs_list) do
+ if not spec[field] then
+ spec[field] = v
+ end
+ end
+ soundspecs_list[field] = nil --so it isn't iterated
+ end
+ end
+ --print(dump(soundspecs_list))
local out = {}
- assert(args[1], "no arguments provided")
- for i, soundspec in pairs(args) do
+ for i, soundspec in pairs(soundspecs_list) do
assert(not (soundspec.to_player and soundspec.min_distance), "in argument '"..tostring(i).."' `min_distance` and `to_player` are incompatible parameters.")
- local sound
+ local sound = soundspec.sound
local outval
- if type(soundspec.pitch) == "table" then
- local pitch = soundspec.pitch
- soundspec.pitch = pitch.min+(math.random()*(pitch.max-pitch.min))
+ for i, v in pairs(soundspec) do
+ if type(v) == "table" and v.min then
+ soundspec[i]=handle_min_max(v)
+ end
end
- if type(soundspec.gain) == "table" then
- local gain = soundspec.gain
- soundspec.pitch = gain.min+(math.random()*(gain.max-gain.min))
- end
- if type(soundspec.sound) == "table" then
- sound = math.weighted_randoms(soundspec.sound)
+ if type(sound) == "table" then
+ sound = Guns4d.math.weighted_randoms(sound)
end
+ assert(sound, "no sound found")
if soundspec.to_player then soundspec.pos = nil end
if soundspec.min_hear_distance then
local exclude_player_ref
@@ -49,20 +89,28 @@ function Guns4d.play_sounds(...)
exclude_player_ref = minetest.get_player_by_name(soundspec.exclude_player)
end
for _, player in pairs(minetest.get_connected_players()) do
+ soundspec.sound = nil
local pos = player:get_pos()
- local dist = sqrt( sqrt(pos.x^2+pos.y^2)^2 +pos.z^2 )
- if (dist > soundspec.min_distance) and (player~=exclude_player_ref) then
+ local dist = sqrt( sqrt((pos.x-soundspec.pos.x)^2+(pos.y-soundspec.pos.y)^2)^2 + (pos.z-soundspec.pos.z)^2)
+ if (dist > soundspec.min_hear_distance) and (player~=exclude_player_ref) then
soundspec.exclude_player = nil --not needed anyway because we can just not play it for this player.
soundspec.to_player = player:get_player_name()
- outval = minetest.play_sound(sound, soundspec)
+ outval = minetest.sound_play(sound, soundspec)
end
end
else
- outval = minetest.play_sound(sound, soundspec)
+ soundspec.sound = nil
+ outval = minetest.sound_play(sound, soundspec)
end
out[i] = outval
end
return out
end
+--- stops a list of sounds
+-- @param handle_list a list of minetest sound handles to stop, this is the returned output of @{guns4d.play_sounds
+-- @function Guns4d.stop_sounds
function Guns4d.stop_sounds(handle_list)
+ for i, v in pairs(handle_list) do
+ minetest.sound_stop(v)
+ end
end
\ No newline at end of file
diff --git a/sounds/LICENSE ar_firing.ogg.txt b/sounds/LICENSE ar_firing.ogg.txt
new file mode 100644
index 0000000..7b8dec6
--- /dev/null
+++ b/sounds/LICENSE ar_firing.ogg.txt
@@ -0,0 +1,3 @@
+ar_firing.ogg:
+by SuperPhat on freesound.org
+License: Creative Commons 0
diff --git a/sounds/LICENSE ar_firing_far.ogg.txt b/sounds/LICENSE ar_firing_far.ogg.txt
new file mode 100644
index 0000000..0efb658
--- /dev/null
+++ b/sounds/LICENSE ar_firing_far.ogg.txt
@@ -0,0 +1,3 @@
+ar_firing_far.ogg:
+by (unknown) on opengameart.org (https://opengameart.org/content/the-free-firearm-sound-library)
+License: Creative Commons
diff --git a/sounds/LICENSE ar_mag_store.ogg.txt b/sounds/LICENSE ar_mag_store.ogg.txt
new file mode 100644
index 0000000..e4db745
--- /dev/null
+++ b/sounds/LICENSE ar_mag_store.ogg.txt
@@ -0,0 +1,3 @@
+ar_mag_store.ogg:
+by serøtōnin on freesound.org
+License: Creative Commons
diff --git a/sounds/LICENSE ar_mag_unload.ogg.txt b/sounds/LICENSE ar_mag_unload.ogg.txt
new file mode 100644
index 0000000..e69de29
diff --git a/sounds/ar_firing.ogg b/sounds/ar_firing.ogg
new file mode 100644
index 0000000..078a54f
Binary files /dev/null and b/sounds/ar_firing.ogg differ
diff --git a/sounds/ar_firing_far.ogg b/sounds/ar_firing_far.ogg
new file mode 100644
index 0000000..5fdb04e
Binary files /dev/null and b/sounds/ar_firing_far.ogg differ
diff --git a/sounds/ar_mag_load.ogg b/sounds/ar_mag_load.ogg
new file mode 100644
index 0000000..9a8f6ae
Binary files /dev/null and b/sounds/ar_mag_load.ogg differ
diff --git a/sounds/ar_mag_store.ogg b/sounds/ar_mag_store.ogg
new file mode 100644
index 0000000..7e8926a
Binary files /dev/null and b/sounds/ar_mag_store.ogg differ
diff --git a/sounds/ar_mag_unload.ogg b/sounds/ar_mag_unload.ogg
new file mode 100644
index 0000000..966a26e
Binary files /dev/null and b/sounds/ar_mag_unload.ogg differ
diff --git a/sounds/attribution and licensing.txt b/sounds/attribution and licensing.txt
new file mode 100644
index 0000000..67573c7
--- /dev/null
+++ b/sounds/attribution and licensing.txt
@@ -0,0 +1,5 @@
+Files contained within this folder each have a respective license, their licenses are named in the following format:
+"LICENSE [file].txt"
+example:
+for ar_firing.ogg the license would be named
+"LICENSE ar_firing.ogg.txt"
\ No newline at end of file