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 @@ + + + + + 4dguns documentation + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ + +

THEE ultimate 3d gun library.

+ +

Scripts

+ + + + + + + + + +
misc_helpersmisc.
play_soundimplements tools for quickly playing audio.
+

Topics

+ + + + + +
readme.md
+ +
+
+
+generated by LDoc 1.5.0 +Last updated 2024-01-19 14:08:01 +
+
+ + diff --git a/autogen_docs/ldoc_new.css b/autogen_docs/ldoc_new.css new file mode 100644 index 0000000..13fef16 --- /dev/null +++ b/autogen_docs/ldoc_new.css @@ -0,0 +1,290 @@ +body { + color: #47555c; + font-size: 16px; + font-family: "Open Sans", sans-serif; + margin: 0; + background: #eff4ff; +} + +a:link { color: #008fee; } +a:visited { color: #008fee; } +a:hover { color: #22a7ff; } + +h1 { font-size:26px; font-weight: normal; } +h2 { font-size:22px; font-weight: normal; } +h3 { font-size:18px; font-weight: normal; } +h4 { font-size:16px; font-weight: bold; } + +hr { + height: 1px; + background: #c1cce4; + border: 0px; + margin: 15px 0; +} + +code, tt { + font-family: monospace; +} +span.parameter { + font-family: monospace; + font-weight: bold; + color: rgb(99, 115, 131); +} +span.parameter:after { + content:":"; +} +span.types:before { + content:"("; +} +span.types:after { + content:")"; +} +.type { + font-weight: bold; font-style:italic +} + +p.name { + font-family: "Andale Mono", monospace; +} + +#navigation { + float: left; + background-color: white; + border-right: 1px solid #d3dbec; + border-bottom: 1px solid #d3dbec; + + width: 14em; + vertical-align: top; + overflow: visible; +} + +#navigation br { + display: none; +} + +#navigation h1 { + background-color: white; + border-bottom: 1px solid #d3dbec; + padding: 15px; + margin-top: 0px; + margin-bottom: 0px; +} + +#navigation h2 { + font-size: 18px; + background-color: white; + border-bottom: 1px solid #d3dbec; + padding-left: 15px; + padding-right: 15px; + padding-top: 10px; + padding-bottom: 10px; + margin-top: 30px; + margin-bottom: 0px; +} + +#content h1 { + background-color: #2c3e67; + color: white; + padding: 15px; + margin: 0px; +} + +#content h2 { + background-color: #6c7ea7; + color: white; + padding: 15px; + padding-top: 15px; + padding-bottom: 15px; + margin-top: 0px; +} + +#content h2 a { + background-color: #6c7ea7; + color: white; + text-decoration: none; +} + +#content h2 a:hover { + text-decoration: underline; +} + +#content h3 { + font-style: italic; + padding-top: 15px; + padding-bottom: 4px; + margin-right: 15px; + margin-left: 15px; + margin-bottom: 5px; + border-bottom: solid 1px #bcd; +} + +#content h4 { + margin-right: 15px; + margin-left: 15px; + border-bottom: solid 1px #bcd; +} + +#content pre { + margin: 15px; +} + +pre { + background-color: rgb(50, 55, 68); + color: white; + border-radius: 3px; + /* border: 1px solid #C0C0C0; /* silver */ + padding: 15px; + overflow: auto; + font-family: "Andale Mono", monospace; +} + +#content ul pre.example { + margin-left: 0px; +} + +table.index { +/* border: 1px #00007f; */ +} +table.index td { text-align: left; vertical-align: top; } + +#navigation ul +{ + font-size:1em; + list-style-type: none; + margin: 1px 1px 10px 1px; +} + +#navigation li { + text-indent: -1em; + display: block; + margin: 3px 0px 0px 22px; +} + +#navigation li li a { + margin: 0px 3px 0px -1em; +} + +#content { + margin-left: 14em; +} + +#content p { + padding-left: 15px; + padding-right: 15px; +} + +#content table { + padding-left: 15px; + padding-right: 15px; + background-color: white; +} + +#content p, #content table, #content ol, #content ul, #content dl { + max-width: 900px; +} + +#about { + padding: 15px; + padding-left: 16em; + background-color: white; + border-top: 1px solid #d3dbec; + border-bottom: 1px solid #d3dbec; +} + +table.module_list, table.function_list { + border-width: 1px; + border-style: solid; + border-color: #cccccc; + border-collapse: collapse; + margin: 15px; +} +table.module_list td, table.function_list td { + border-width: 1px; + padding-left: 10px; + padding-right: 10px; + padding-top: 5px; + padding-bottom: 5px; + border: solid 1px rgb(193, 204, 228); +} +table.module_list td.name, table.function_list td.name { + background-color: white; min-width: 200px; border-right-width: 0px; +} +table.module_list td.summary, table.function_list td.summary { + background-color: white; width: 100%; border-left-width: 0px; +} + +dl.function { + margin-right: 15px; + margin-left: 15px; + border-bottom: solid 1px rgb(193, 204, 228); + border-left: solid 1px rgb(193, 204, 228); + border-right: solid 1px rgb(193, 204, 228); + background-color: white; +} + +dl.function dt { + color: rgb(99, 123, 188); + font-family: monospace; + border-top: solid 1px rgb(193, 204, 228); + padding: 15px; +} + +dl.function dd { + margin-left: 15px; + margin-right: 15px; + margin-top: 5px; + margin-bottom: 15px; +} + +#content dl.function dd h3 { + margin-top: 0px; + margin-left: 0px; + padding-left: 0px; + font-size: 16px; + color: rgb(128, 128, 128); + border-bottom: solid 1px #def; +} + +#content dl.function dd ul, #content dl.function dd ol { + padding: 0px; + padding-left: 15px; + list-style-type: none; +} + +ul.nowrap { + overflow:auto; + white-space:nowrap; +} + +.section-description { + padding-left: 15px; + padding-right: 15px; +} + +/* stop sublists from having initial vertical space */ +ul ul { margin-top: 0px; } +ol ul { margin-top: 0px; } +ol ol { margin-top: 0px; } +ul ol { margin-top: 0px; } + +/* make the target distinct; helps when we're navigating to a function */ +a:target + * { + background-color: #FF9; +} + + +/* styles for prettification of source */ +pre .comment { color: #bbccaa; } +pre .constant { color: #a8660d; } +pre .escape { color: #844631; } +pre .keyword { color: #ffc090; font-weight: bold; } +pre .library { color: #0e7c6b; } +pre .marker { color: #512b1e; background: #fedc56; font-weight: bold; } +pre .string { color: #8080ff; } +pre .number { color: #f8660d; } +pre .operator { color: #2239a8; font-weight: bold; } +pre .preprocessor, pre .prepro { color: #a33243; } +pre .global { color: #c040c0; } +pre .user-keyword { color: #800080; } +pre .prompt { color: #558817; } +pre .url { color: #272fc2; text-decoration: underline; } diff --git a/autogen_docs/modules/misc_helpers.html b/autogen_docs/modules/misc_helpers.html new file mode 100644 index 0000000..c966d04 --- /dev/null +++ b/autogen_docs/modules/misc_helpers.html @@ -0,0 +1,73 @@ + + + + + 4dguns documentation + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Module misc_helpers

+

picks a random index, with odds based on it's value.

+

Returns the index of the selected.

+ + + +
+
+ + + + +
+
+
+generated by LDoc 1.5.0 +Last updated 2024-01-16 17:00:50 +
+
+ + diff --git a/autogen_docs/scripts/misc_helper.html b/autogen_docs/scripts/misc_helper.html new file mode 100644 index 0000000..ffc1d99 --- /dev/null +++ b/autogen_docs/scripts/misc_helper.html @@ -0,0 +1,112 @@ + + + + + 4dguns documentation + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Script misc_helper

+

misc.

+

common tools for 4dguns

+ + +

Functions

+ + + + + +
Guns4d.math.weighted_randoms (tbl)picks a random index, with odds based on it's value.
+ +
+
+ + +

Functions

+ +
+
+ + Guns4d.math.weighted_randoms (tbl) +
+
+ picks a random index, with odds based on it's value. Returns the index of the selected. + + +

Parameters:

+
    +
  • tbl + +

    a table containing weights, example

    +
      {
    +      ["sound"] = 999, --999 in 1000 chance this plays
    +      ["rare_sound"] = 1 --1 in 1000 chance this plays
    +  }
    +
    + +
  • +
+ + + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2024-01-16 17:04:01 +
+
+ + diff --git a/autogen_docs/scripts/misc_helpers.html b/autogen_docs/scripts/misc_helpers.html new file mode 100644 index 0000000..d317e81 --- /dev/null +++ b/autogen_docs/scripts/misc_helpers.html @@ -0,0 +1,115 @@ + + + + + 4dguns documentation + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Script misc_helpers

+

misc.

+

common tools for 4dguns

+ + +

math helpers

+ + + + + +
weighted_randoms (tbl)picks a random index, with odds based on it's value.
+ +
+
+ + +

math helpers

+ +
+ in guns4d.math +
+
+
+ + weighted_randoms (tbl) +
+
+ picks a random index, with odds based on it's value. Returns the index of the selected. + + +

Parameters:

+
    +
  • 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
    +  }
    +
    + +
  • +
+ + + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2024-01-19 14:08:01 +
+
+ + diff --git a/autogen_docs/scripts/play_sound.html b/autogen_docs/scripts/play_sound.html new file mode 100644 index 0000000..bab65e1 --- /dev/null +++ b/autogen_docs/scripts/play_sound.html @@ -0,0 +1,162 @@ + + + + + 4dguns documentation + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ +

Script play_sound

+

implements tools for quickly playing audio.

+

+ +

+ + +

Functions

+ + + + + +
Guns4d.play_sound (sound_specs)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.
+

Tables

+ + + + + +
guns4d_soundspecdefines a sound.
+ +
+
+ + +

Functions

+ +
+
+ + Guns4d.play_sound (sound_specs) +
+
+ 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 + + +

Parameters:

+
    +
  • sound_specs + +

    a guns4d_soundspec or a list of guns4d_soundspecs 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
    +  }
    +
    + +
  • +
+ +

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 +
  • +
  • sounds + a weighted_randoms table the output will overwrite the sound field. +
  • +
  • to_player + 4dguns changes to_player so it only plays positionless audio (as it is only intended for first person audio) +
  • +
+ + + + + +
+
+ + +
+
+
+generated by LDoc 1.5.0 +Last updated 2024-01-19 14:08:01 +
+
+ + diff --git a/autogen_docs/topics/readme.md.html b/autogen_docs/topics/readme.md.html new file mode 100644 index 0000000..3ece7c6 --- /dev/null +++ b/autogen_docs/topics/readme.md.html @@ -0,0 +1,63 @@ + + + + + 4dguns documentation + + + + +
+ +
+ +
+
+
+ + +
+ + + + + + +
+ + +

4dguns

+

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