From 6e74815ab7b1a7cc7961db74694d20d8fbcdbc81 Mon Sep 17 00:00:00 2001 From: FatalErr42O <58855799+FatalError42O@users.noreply.github.com> Date: Thu, 29 Feb 2024 21:18:37 -0800 Subject: [PATCH] fixed some issues with reloading. Fixed an issue where magazines broke all recipes in game except the most recently registered magazine's reload --- .vscode/settings.json | 3 +- TODO.txt | 76 ++++++- ammo_api.lua | 69 ++++--- classes/Ammo_handler.lua | 92 +++++++-- classes/Control_handler.lua | 71 +++++-- classes/Gun.lua | 84 +++++--- classes/Player_handler.lua | 4 +- default_controls.lua | 297 +++++++++++++++------------ default_reload_actions.lua | 0 infinite_ammo.lua | 1 + init.lua | 3 + item_entities.lua | 124 +++++++++++ play_sound.lua | 43 +++- sounds/LICENSE ar_charge.ogg.txt | 3 + sounds/LICENSE ar_firing.ogg.txt | 2 +- sounds/LICENSE ar_firing_far.ogg.txt | 2 +- sounds/LICENSE ar_mag_load.ogg.txt | 1 + sounds/LICENSE ar_mag_store.ogg.txt | 5 +- sounds/LICENSE ar_mag_unload.ogg.txt | 1 + sounds/ar_charge.ogg | Bin 0 -> 9067 bytes 20 files changed, 627 insertions(+), 254 deletions(-) create mode 100644 default_reload_actions.lua create mode 100644 item_entities.lua create mode 100644 sounds/LICENSE ar_charge.ogg.txt create mode 100644 sounds/LICENSE ar_mag_load.ogg.txt create mode 100644 sounds/ar_charge.ogg diff --git a/.vscode/settings.json b/.vscode/settings.json index 45a66c9..006bd68 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "vector", "dump", "player_api", - "ItemStack" + "ItemStack", + "mtul" ] } \ No newline at end of file diff --git a/TODO.txt b/TODO.txt index 4f71a32..2ba2608 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,14 +1,58 @@ +( ) guns4d_pack_1 + ( ) find better name + -guns4d-WMA (western military arms) + ( ) finish guns + (~) m4 + ( ) add reload anim hand movement + (~) awm + (~) complete animations + ( ) load, unload + (~) complete sound effects + ( ) load, unload + ( ) mp5 + ( ) Vector + ( ) berreta (pistol) + ( ) glock + ( ) spas12 +( ) guns4d_pack_2 + ( ) find better name + -guns4d-EMA (eastern military arms) + ( ) ak74 + ( ) ak47? + ( ) SVD + ( ) PMM + ( ) Saiga 12k + ( ) Vitzyaz-SN +( ) guns4d_pack_3 + ( ) find better name + -guns4d-OP4 (opposing force's arms) + ( ) make guns + ( ) rugged ak47 + ( ) m1911 + ( ) RPG? + ( ) Mossberg + ( ) AR15 + ( ) Berreta m12 (smg) - -(~) add audio +( ) have breathing act as a multiplier for sway +( ) fix water/fluids +( ) add random deviation + ( ) randomness system + ( ) patterns? + ( ) bloom +(x) add audio + soundeffects system implemented! (x) sfx system (x) did signifficant work on it - (x) firing sound effects - ( ) reload sound effects - ( ) firemode sound effects + ( ) dry fire sound + (~) reload sound effects + (x) everything else + ( ) bolt-catch releasing click + (x) firemode sound effects + ( ) firemode switching sound (~) add config (x) add a table for config storage, some settings ( ) integrate with minetest settings @@ -16,18 +60,32 @@ (x) add infinite ammo privelage and quick command (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 third idle animation for loaded with magazine but chamber empty + ( ) use newly implemented bolt catch support ( ) add entity scopes (for holo sights etc) ( ) add attachments -( ) add other reloading types +(~) add other reloading types (x) magazine ( ) fractional ( ) flat (magless) - ( ) fractional clip + ( ) fractional with clip +(~) item visuals + ( ) make optional + (~) magazine + (x) unloads in correct position? (sort of, rotation is like impossible to do.) + ( ) realistic + (x) arcade/default + ( ) gun + ( ) realistic + ( ) arcade/default +(x) fix uncharge gun after incomplete reload, make special pull out animation only play for first charge +(x) make reloads "automatic" (press/hold z once, and again to cancel) + (x) add control system + (x) implement for reload (may not be needed if first step done correctly) ( ) (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 +(x) Fix HORRIBLE namespace violation in misc_helpers.lua. Also migrate features to MTUL libraries this won't be fun. documentation diff --git a/ammo_api.lua b/ammo_api.lua index df75ad4..4a56419 100644 --- a/ammo_api.lua +++ b/ammo_api.lua @@ -11,16 +11,10 @@ Default_mag = { craft_reload = true } Guns4d.ammo = { - default_empty_loaded_bullets = { - }, - registered_bullets = { - - }, - registered_magazines = { - - } + default_empty_loaded_bullets = {}, + registered_bullets = {}, + registered_magazines = {} } -local max_wear = 65535 function Guns4d.ammo.on_hit_player(bullet, force_mmRHA) end function Guns4d.ammo.register_bullet(def) @@ -39,14 +33,9 @@ function Guns4d.ammo.update_mag(def, itemstack, meta) meta = meta or itemstack:get_meta() local bullets = minetest.deserialize(meta:get_string("guns4d_loaded_bullets")) local count = 0 - local current_bullet = "empty" for i, v in pairs(bullets) do - current_bullet = i count = count + v end - local new_wear = max_wear-(max_wear*count/def.capacity) - --itemstack:set_wear(Guns4d.math.clamp(new_wear, 1, max_wear-1)) - meta:set_int("guns4d_total_bullets", count) if count > 0 then meta:set_string("count_meta", tostring(count).."/"..def.capacity) else @@ -54,7 +43,6 @@ function Guns4d.ammo.update_mag(def, itemstack, meta) end return itemstack end - function Guns4d.ammo.register_magazine(def) def = Guns4d.table.fill(Default_mag, def) assert(def.accepted_bullets, "missing property def.accepted_bullets. Need specified bullets to allow for loading") @@ -68,6 +56,8 @@ function Guns4d.ammo.register_magazine(def) Guns4d.ammo.registered_magazines[def.itemstring] = def --register craft prediction local old_on_use = minetest.registered_items[def.itemstring].on_use + + --the actual item. This will be changed. minetest.override_item(def.itemstring, { on_use = function(itemstack, user, pointed_thing) if old_on_use then @@ -85,6 +75,24 @@ function Guns4d.ammo.register_magazine(def) end end }) + --the magazine item entity + --print(dump(minetest.registered_entities)) + --[[local ent_def = minetest.registered_entities["__builtin:item"..def.itemstring] + if def.model then + ent_def.visual = "mesh" + ent_def.mesh = def.model + ent_def.collision_box = { + -0.5, 0, -0.5, + 0.5, 1, 0.5 + } + ent_def.on_step = function(self, dt, moveresult) + if moveresult.touching_ground then + self.object:set_rotation() + end + end + end]] + + --loading and unloading magazines if def.craft_reload then minetest.register_craft_predict(function(itemstack, player, old_craft_grid, craft_inv) --initialize all mags @@ -95,11 +103,12 @@ function Guns4d.ammo.register_magazine(def) Guns4d.ammo.initialize_mag_data(v) end end - if num_mags > 0 then + minetest.chat_send_all(num_mags) + if num_mags == 1 then if itemstack:get_name()=="" then for i, v in pairs(craft_inv:get_list("craft")) do - local name =v:get_name() - if name == def.itemstring then + local name = v:get_name() + if (name == def.itemstring) and (v:get_meta():get_string("guns4d_loaded_bullets")=="") then craft_inv:set_stack("craft", i, Guns4d.ammo.initialize_mag_data(v)) end if (name~=def.itemstring) and Guns4d.ammo.registered_magazines[name] then @@ -111,35 +120,41 @@ function Guns4d.ammo.register_magazine(def) end return def.itemstring end + elseif num_mags > 1 then + return "" end end) minetest.register_on_craft(function(itemstack, player, old_craft_grid, craft_inv) if craft_inv:contains_item("craft", def.itemstring) and craft_inv:contains_item("craftpreview", def.itemstring) then - local mag_stack_index local craft_list = craft_inv:get_list("craft") --there's basically no way to cleanly avoid two iterations, annoyingly. + --check for bullets and mags. + local mag_stack_index for i, v in pairs(craft_list) do local name = v:get_name() - if (name~=def.itemstring) then - if Guns4d.ammo.registered_magazines[name] then + if Guns4d.ammo.registered_magazines[name] then + --check if there is a magazine of a different type or multiple mags, also get our mag index + if (name==def.itemstring) then + mag_stack_index = i + else return end - else - mag_stack_index = i end - if not def.accepted_bullets_set[name] then - if (name ~= "") and (name~=def.itemstring) then - return - end + if (not def.accepted_bullets_set[name]) and (name ~= "") and (name~=def.itemstring) then + return end end + if not mag_stack_index then return end local bullets_unfilled = def.capacity local mag_stack = craft_inv:get_stack("craft", mag_stack_index) + --print(dump(mag_stack:get_name())) + --print(mag_stack_index) local new_ammo_table = minetest.deserialize(mag_stack:get_meta():get_string("guns4d_loaded_bullets")) for i, v in pairs(new_ammo_table) do bullets_unfilled = bullets_unfilled - v end local new_stack = ItemStack(def.itemstring) + --find the bullets, and fill the new_ammo_table up to any items with counts adding up to bullets_unfilled for i, v in pairs(craft_list) do local name = v:get_name() if def.accepted_bullets_set[name] then diff --git a/classes/Ammo_handler.lua b/classes/Ammo_handler.lua index 3cb2a4a..7e396aa 100644 --- a/classes/Ammo_handler.lua +++ b/classes/Ammo_handler.lua @@ -40,6 +40,23 @@ function Ammo_handler:update_meta(bullets) end self.handler.player:set_wielded_item(self.gun.itemstack) end +function Ammo_handler:get_magazine_bone_info() + local gun = self.gun + local handler = self.gun.handler + local node = mtul.b3d_nodes.get_node_by_name(gun.b3d_model, gun.properties.visuals.magazine, true) + --get trans of first frame + if not node then + minetest.log("error", "improperly set gun magazine bone name, could not properly calculate magazine transformation.") + return self.gun.pos, vector.new(), vector.new() + end + local pos1 = vector.new(mtul.b3d_nodes.get_node_global_position(nil, node, true, math.floor(gun.animation_data.current_frame))) + local pos2 = vector.new(mtul.b3d_nodes.get_node_global_position(nil, node, true, gun.animation_data.current_frame)) + local vel = (pos2-pos1)*((gun.animation_data.current_frame-math.floor(gun.animation_data.current_frame))/gun.animation_data.fps)+self.gun.player:get_velocity() + local pos = self.gun:get_pos(pos2/10)+self.gun.handler:get_pos() + --[[so I tried to get the rotation before, and it actually turns out that was... insanely difficult? Why? I don't know, the rotation behavior was beyond unexpected, I tried implementing it with quats and + matrices and neither worked. I'm done, I've spent countless hours on this, and its fuckin stupid to spend a SECOND more on this pointless shit. It wouldnt even look that much better!]] + return pos, vel +end --use a round, called when the gun is shot. Returns a bool indicating success. function Ammo_handler:spend_round() assert(self.instance, "attempt to call object method on a class") @@ -52,6 +69,9 @@ function Ammo_handler:spend_round() self.ammo.loaded_bullets[bullet_spent] = self.ammo.loaded_bullets[bullet_spent]-1 if self.ammo.loaded_bullets[bullet_spent] == 0 then self.ammo.loaded_bullets[bullet_spent] = nil end self.ammo.total_bullets = self.ammo.total_bullets - 1 + if (self.ammo.total_bullets == 0) and (self.gun.properties.charging.bolt_charge_mode == "catch") then + self.gun.bolt_charged = true + end end --set the new current bullet if next(self.ammo.loaded_bullets) then @@ -60,16 +80,35 @@ function Ammo_handler:spend_round() else self.ammo.next_bullet = "empty" meta:set_string("guns4d_next_bullet", "empty") + if self.gun.properties.charging.bolt_charge_mode == "catch" then + self.gun.bolt_charged = true + end end self:update_meta() return bullet_spent end end -function Ammo_handler:close_bolt() - self.ammo.next_bullet = Guns4d.math.weighted_randoms(self.ammo.loaded_bullets) or "empty" +function Ammo_handler:chamber_round() + self.ammo.next_bullet = Guns4d.math.weighted_randoms(self.ammo.loaded_bullets) or "empty" end -function Ammo_handler:load_magazine(charge) +local function tally_ammo_from_meta(meta) + local string = meta:get_string("guns4d_loaded_bullets") + if string=="" then return 0 end + local count = 0 + for i, v in pairs(minetest.deserialize(string)) do + count=count+v + end + return count +end +local function tally_ammo(ammo) + local total = 0 + for i, v in pairs(ammo) do + total = total + v + end + return total +end +function Ammo_handler:load_magazine() assert(self.instance, "attempt to call object method on a class") local inv = self.inventory local magstack_index @@ -82,17 +121,17 @@ function Ammo_handler:load_magazine(charge) end for i, v in pairs(inv:get_list("main")) do if gun_accepts[v:get_name()] then - local meta = v:get_meta() + local mag_meta = v:get_meta() --intiialize data if it doesn't exist so it doesnt kill itself - if meta:get_string("guns4d_loaded_bullets") == "" then + if mag_meta:get_string("guns4d_loaded_bullets") == "" then Guns4d.ammo.initialize_mag_data(v) inv:set_stack("main", i, v) end - local ammo = meta:get_int("guns4d_total_bullets") + local ammo = tally_ammo_from_meta(mag_meta) if ammo > highest_ammo then highest_ammo = ammo local has_unaccepted = false - for bullet, _ in pairs(minetest.deserialize(meta:get_string("guns4d_loaded_bullets"))) do + for bullet, _ in pairs(minetest.deserialize(mag_meta:get_string("guns4d_loaded_bullets"))) do if not gun.accepted_bullets[bullet] then has_unaccepted = true break @@ -106,16 +145,19 @@ function Ammo_handler:load_magazine(charge) local magstack = inv:get_stack("main", magstack_index) local magstack_meta = magstack:get_meta() --get the ammo stuff - local meta = self.gun.meta local bullet_string = magstack_meta:get_string("guns4d_loaded_bullets") - 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 = ((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))) + local ammo = self.ammo + ammo.loaded_mag = magstack:get_name() + ammo.loaded_bullets = minetest.deserialize(bullet_string) + ammo.total_bullets = tally_ammo(ammo.loaded_bullets) + if self.gun.bolt_charged or (self.gun.properties.charging.bolt_charge_mode == "none") then + ammo.next_bullet = Guns4d.math.weighted_randoms(ammo.loaded_bullets) or "empty" + self.gun.bolt_charged = false + else + ammo.next_bullet = "empty" + end self:update_meta() - inv:set_stack("main", magstack_index, "") return end @@ -124,10 +166,10 @@ function Ammo_handler:inventory_has_ammo() local inv = self.inventory local gun = self.gun for i, v in pairs(inv:get_list("main")) do - if gun.accepted_magazines[v:get_name()] and (v:get_meta():get_int("guns4d_total_bullets")>0) then + if gun.accepted_magazines[v:get_name()] and (tally_ammo_from_meta(v:get_meta())>0) then return true end - if (not gun.properties.magazine_only) and gun.accepted_bullets[v:get_name()] then + if (not gun.properties.ammo.magazine_only) and gun.accepted_bullets[v:get_name()] then return true end end @@ -159,8 +201,10 @@ function Ammo_handler:unload_magazine(to_ground) local gunmeta = self.gun.meta --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")) - magstack = Guns4d.ammo.update_mag(nil, magstack, magmeta) + --magmeta:set_string("guns4d_total_bullets", gunmeta:get_string("guns4d_total_bullets")) + if not Guns4d.ammo.registered_magazines[magstack:get_name()] then minetest.log("error", "player `"..self.gun.player:get_player_name().."` ejected an unregistered magazine: `"..magstack:get_name().." `from a gun") else + magstack = Guns4d.ammo.update_mag(nil, magstack, magmeta) + end --throw it on the ground if to_ground is true local remaining if to_ground then @@ -170,8 +214,14 @@ function Ammo_handler:unload_magazine(to_ground) end --eject leftover or full stack if remaining:get_count() > 0 then - local object = minetest.add_item(self.gun.pos, remaining) - object:add_velocity(vector.rotate({x=.6,y=-.3,z=.4}, {x=0,y=-self.handler.look_rotation.y*math.pi/180,z=0})) + local pos, vel = self:get_magazine_bone_info() + local object = minetest.add_item(pos, remaining) + object:set_velocity(vel) + vector.new(-1,-1,1) + --look I'm not going to pretend I understand how fucking broken minetest's coordinate systems are, and I'm so fucking done with this shit, so this is good enough + object:set_rotation(vector.multiply(vector.dir_to_rotation(self.gun:get_dir()), vector.new(-1,1,0))+vector.new(0,math.pi,0)) + --object:set_rotation(rot) + --object:add_velocity(vector.rotate({x=.6,y=-.3,z=.4}, {x=0,y=-self.handler.look_rotation.y*math.pi/180,z=0})) end self.ammo.loaded_mag = "empty" self.ammo.next_bullet = "empty" @@ -194,7 +244,6 @@ function Ammo_handler:unload_all(to_ground) end if leftover:get_count() > 0 then --I don't know itemstacks well enough to know if I need this (for leftover stack of add_item) local object = minetest.add_item(self.gun.pos, leftover) - object:add_velocity(vector.rotate({x=.6,y=-.3,z=.4}, {x=0,y=-self.handler.look_rotation.y*math.pi/180,z=0})) end end if self.ammo.loaded_mag ~= "empty" then @@ -206,7 +255,6 @@ function Ammo_handler:unload_all(to_ground) end if stack:get_count() > 0 then local object = minetest.add_item(self.gun.pos, stack) - object:add_velocity(vector.rotate({x=1,y=2,z=.4}, {x=0,y=-self.handler.look_rotation.y*math.pi/180,z=0})) end end self.ammo.loaded_mag = "empty" diff --git a/classes/Control_handler.lua b/classes/Control_handler.lua index a64a452..01c0a39 100644 --- a/classes/Control_handler.lua +++ b/classes/Control_handler.lua @@ -7,6 +7,7 @@ Guns4d.control_handler = { "zoom" }, timer = .3, + mode = "toggle", "hybrid", "hold" call_before_timer = false, loop = false, func=function(active, interrupted, data, busy_controls) @@ -18,8 +19,10 @@ Guns4d.control_handler = { --data table: --[[ { - held = bool + continue = bool timer = float + active_ticks = int + current_mode = "toggle", "hold" } ]] local controls = Guns4d.control_handler @@ -36,7 +39,7 @@ function controls:update(dt) 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 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 + for i, control in pairs(self.actions) do if not (i=="on_use") and not (i=="on_secondary_use") then local def = control local data = control.data @@ -46,18 +49,43 @@ function controls:update(dt) if not pressed[key] then conditions_met = false break end end if conditions_met then + if (def.mode == "toggle") or (def.mode == "hybrid") then + data.time_held = data.time_held + dt + if (not data.toggle_lock) and (data.toggled or (data.time_held > Guns4d.config.control_held_toggle_threshold)) then + data.toggled = not data.toggled + data.toggle_lock = true --so it can only be toggled once when conditions are met for a period of time + data.current_mode = "toggle" + end + if (data.current_mode ~= "hold") and (data.time_held > Guns4d.config.control_hybrid_toggle_threshold) and (def.mode=="hybrid") then + data.current_mode = "hold" + data.toggled = false + end + end + else + if def.mode=="hyrbid" then + data.current_mode = "toggle" + end + data.time_held = 0 + data.toggle_lock = false + end + --minetest.chat_send_all(data.current_mode) + --minetest.chat_send_all(tostring((conditions_met and not def.toggle) or data.toggled)) + if (conditions_met and data.current_mode == "hold") or (data.toggled and data.current_mode == "toggle") 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 + --when time is over, if it wasnt continue (or loop is active) then reset and call the function. + --continue indicates wether the function was called (as active) before last step. + if data.timer <= 0 and ((not data.continue) or def.loop) then + data.continue = 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. + if data.current_mode == "toggle" then + data.toggled = false + end + elseif def.call_before_timer and not data.continue 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 + data.continue = 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}) @@ -76,34 +104,37 @@ function controls:update(dt) end function controls:on_use(itemstack, pointed_thing) assert(self.instance, "attempt to call object method on a class") - if self.controls.on_use then - self.controls.on_use(itemstack, self.handler, pointed_thing) + if self.actions.on_use then + self.actions.on_use(itemstack, self.handler, pointed_thing) end end function controls:on_secondary_use(itemstack, pointed_thing) assert(self.instance, "attempt to call object method on a class") - if self.controls.on_secondary_use then - self.controls.on_secondary_use(itemstack, self.handler, pointed_thing) + if self.actions.on_secondary_use then + self.actions.on_secondary_use(itemstack, self.handler, pointed_thing) end end ---@diagnostic disable-next-line: duplicate-set-field function controls.construct(def) if def.instance then - assert(def.controls, "no controls provided") + assert(def.actions, "no actions provided") assert(def.player, "no player provided") - def.controls = Guns4d.table.deep_copy(def.controls) + def.actions = Guns4d.table.deep_copy(def.actions) def.busy_list = {} def.handler = Guns4d.players[def.player:get_player_name()] - for i, control in pairs(def.controls) do + def.mode = def.mode or "hold" + for i, action in pairs(def.actions) do if not (i=="on_use") and not (i=="on_secondary_use") then - control.timer = control.timer or 0 - control.data = { - timer = control.timer, - held = false + action.timer = action.timer or 0 + action.data = { + timer = action.timer, + continue = false, + time_held = 0, + current_mode = (def.mode=="hybrid" and "toggle") or def.mode } end end - table.sort(def.controls, function(a,b) + table.sort(def.actions, function(a,b) return #a.conditions > #b.conditions end) end diff --git a/classes/Gun.lua b/classes/Gun.lua index 92d1ac4..1b3d4ae 100644 --- a/classes/Gun.lua +++ b/classes/Gun.lua @@ -83,7 +83,7 @@ local gun_default = { player_axial = {x=1,y=1}, }, breathing_scale = .5, --the max angluler offset caused by breathing. - controls = { --used by control_handler + control_actions = { --used by control_handler __overfill=true, --this table will not be filled in. aim = Guns4d.default_controls.aim, auto = Guns4d.default_controls.auto, @@ -119,8 +119,22 @@ local gun_default = { }, }, sounds = { --this does not contain reload sound effects. + release_bolt = { + __overfill=true, + sound = "ar_release_bolt", + max_hear_distance = 5, + pitch = { + min = .95, + max = 1.05 + }, + gain = { + min = .9, + max = 1 + } + }, fire = { { + __overfill=true, sound = "ar_firing", max_hear_distance = 40, --far min_hear_distance is also this. pitch = { @@ -133,6 +147,7 @@ local gun_default = { } }, { + __overfill=true, sound = "ar_firing_far", min_hear_distance = 40, max_hear_distance = 600, @@ -245,7 +260,7 @@ function gun_default:charge() 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.ammo_handler:chamber_round() self.rechamber_time = props.charging.default_charge_time end --update gun, the main function. @@ -289,7 +304,6 @@ function gun_default:update(dt) self.local_paxial_dir = self:get_player_axial_dir(true) self.pos = self:get_pos()+self.handler:get_pos() - if self.properties.sprite_scope then self.sprite_scope:update() end @@ -302,7 +316,7 @@ function gun_default:update(dt) --[[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)) + --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 @@ -374,6 +388,12 @@ function gun_default:attempt_fire() if spent_bullet and spent_bullet ~= "empty" then local dir = self.dir local pos = self.pos + + if not Guns4d.ammo.registered_bullets[spent_bullet] then + minetest.log("error", "unregistered bullet itemstring"..tostring(spent_bullet)..", could not fire gun (player:"..self.player:get_player_name()..")"); + return false + end + local bullet_def = Guns4d.table.fill(Guns4d.ammo.registered_bullets[spent_bullet], { player = self.player, --we don't want it to be doing fuckshit and letting players shoot through walls. @@ -388,6 +408,7 @@ function gun_default:attempt_fire() self:recoil() self:muzzle_flash() + print(dump(self.properties.sounds.fire)) 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) @@ -452,33 +473,33 @@ function gun_default:get_player_axial_dir(rltv) return dir end function gun_default:get_dir(rltv) - assert(self.instance, "attempt to call object method on a class") - local rotation = self.total_offset_rotation - local handler = self.handler - --rotate x and then y. - local dir = Vec.new(Vec.rotate({x=0, y=0, z=1}, {y=0, x=((rotation.gun_axial.x+rotation.player_axial.x)*math.pi/180), z=0})) - dir = Vec.rotate(dir, {y=((rotation.gun_axial.y+rotation.player_axial.y)*math.pi/180), x=0, z=0}) - if not rltv then - if (self.properties.sprite_scope and handler.control_handler.ads) or (self.properties.crosshair and not handler.control_handler.ads) then - --we need the head rotation in either of these cases, as that's what they're showing. - dir = Vec.rotate(dir, {x=-handler.look_rotation.x*math.pi/180,y=-handler.look_rotation.y*math.pi/180,z=0}) - else - dir = Vec.rotate(dir, {x=self.player_rotation.x*math.pi/180,y=self.player_rotation.y*math.pi/180,z=0}) - end + assert(self.instance, "attempt to call object method on a class") + local rotation = self.total_offset_rotation + local handler = self.handler + --rotate x and then y. + local dir = Vec.new(Vec.rotate({x=0, y=0, z=1}, {y=0, x=((rotation.gun_axial.x+rotation.player_axial.x)*math.pi/180), z=0})) + dir = Vec.rotate(dir, {y=((rotation.gun_axial.y+rotation.player_axial.y)*math.pi/180), x=0, z=0}) + if not rltv then + if (self.properties.sprite_scope and handler.control_handler.ads) or (self.properties.crosshair and not handler.control_handler.ads) then + --we need the head rotation in either of these cases, as that's what they're showing. + dir = Vec.rotate(dir, {x=-handler.look_rotation.x*math.pi/180,y=-handler.look_rotation.y*math.pi/180,z=0}) + else + dir = Vec.rotate(dir, {x=self.player_rotation.x*math.pi/180,y=self.player_rotation.y*math.pi/180,z=0}) end + end - --local hud_pos = dir+player:get_pos()+{x=0,y=player:get_properties().eye_height,z=0}+vector.rotate(player:get_eye_offset()/10, {x=0,y=player_rotation.y*math.pi/180,z=0}) - --[[local hud = player:hud_add({ - hud_elem_type = "image_waypoint", - text = "muzzle_flash2.png", - world_pos = hud_pos, - scale = {x=10, y=10}, - alignment = {x=0,y=0}, - offset = {x=0,y=0}, - }) - minetest.after(0, function(hud) - player:hud_remove(hud) - end, hud)]] + --local hud_pos = dir+player:get_pos()+{x=0,y=player:get_properties().eye_height,z=0}+vector.rotate(player:get_eye_offset()/10, {x=0,y=player_rotation.y*math.pi/180,z=0}) + --[[local hud = player:hud_add({ + hud_elem_type = "image_waypoint", + text = "muzzle_flash2.png", + world_pos = hud_pos, + scale = {x=10, y=10}, + alignment = {x=0,y=0}, + offset = {x=0,y=0}, + }) + minetest.after(0, function(hud) + player:hud_remove(hud) + end, hud)]] return dir end @@ -872,8 +893,8 @@ gun_default.construct = function(def) local props = def.properties --validate controls, done before properties are filled to avoid duplication. - if props.controls then - for i, control in pairs(props.controls) do + if props.control_actions then + for i, control in pairs(props.control_actions) do if not (i=="on_use") and not (i=="on_secondary_use") then assert(control.conditions, "no conditions provided for control") for _, condition in pairs(control.conditions) do @@ -963,6 +984,7 @@ gun_default.construct = function(def) }) end + --TODO this may need to be renamed and put in constructor for instances (modifications could later change ammo types.) def.accepted_bullets = {} for _, v in pairs(def.properties.ammo.accepted_bullets) do def.accepted_bullets[v] = true diff --git a/classes/Player_handler.lua b/classes/Player_handler.lua index bd7138f..70c5df4 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, gun=self.gun}) + self.control_handler = Guns4d.control_handler:new({player=player, actions=self.gun.properties.control_actions, 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 = Guns4d.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), 360-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 diff --git a/default_controls.lua b/default_controls.lua index 1265fc7..6b4ca70 100644 --- a/default_controls.lua +++ b/default_controls.lua @@ -1,3 +1,5 @@ +--- a default control system for aiming, reloading, firing, reloading, and more. + Guns4d.default_controls = { controls = {} } @@ -24,10 +26,26 @@ Guns4d.default_controls.auto = { Guns4d.default_controls.firemode = { conditions = {"sneak", "zoom"}, loop = false, - timer = .5, + timer = 0, func = function(active, interrupted, data, busy_list, gun, handler) + if active then + if not (busy_list.on_use or busy_list.auto) then + gun:cycle_firemodes() + end + end + end +} +Guns4d.default_controls.toggle_safety = { + conditions = {"sneak", "zoom"}, + loop = false, + timer = 2, + func = function(active, interrupted, data, busy_list, gun, handler) + local safety = "a real variable here" + if safety and not data.timer_set then + + end if not (busy_list.on_use or busy_list.auto) then - gun:cycle_firemodes() + end end } @@ -42,10 +60,114 @@ Guns4d.default_controls.on_use = function(itemstack, handler, pointed_thing) end --handler.control_handler.busy_list.on_use = true end + + + + +local reload_actions = {} +function Guns4d.default_controls.register_reloading_state_type(name, def) + assert(type(def)=="table", "improper definition type") + assert(type(def.on_completion)=="function", "action has no completion function") + assert(type(def.validation_check)=="function") --return bool indicating wether it is valid. If nil it is assumed to be valid + reload_actions[name] = def +end +local reg_mstate = Guns4d.default_controls.register_reloading_state_type + +reg_mstate("unload_mag", { + on_completion = function(gun, ammo_handler, next_state) --what happens when the timer is completed. + if next_state and next_state.action == "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 in hotbar + else + ammo_handler:unload_magazine(true) --unload to ground if it's not going to be stored next state + end + return true --true indicates to move to the next action. If false it would replay the same state, if nil it would break out of the function and not continue until reset entirely. + end, + validation_check = function(gun, ammo_handler, next_state) + if ammo_handler.ammo.loaded_mag == "empty" then + return false --indicates that the state is not valid, this moves to the next state. If true then it is valid and it will start the reload action. Nil breaks out entirely. + end + return true + end +}) + +reg_mstate("store", { + on_completion = function(gun, ammo_handler, next_state) + local pause = false + --needs to happen before so we don't detect the ammo we just unloaded + if not ammo_handler:inventory_has_ammo() then + pause=true + end + if gun.properties.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 + end + return true + end, + validation_check = function(gun, ammo_handler, next_state) + if gun.properties.ammo.magazine_only and (ammo_handler.ammo.loaded_mag == "empty") then + return false + end + return true + end, + interrupt = function(gun, ammo_handler) + if gun.properties.ammo.magazine_only and (ammo_handler.ammo.loaded_mag ~= "empty") then + ammo_handler:unload_magazine(true) --"true" is for to_ground + else + ammo_handler:unload_all(true) + end + end +}) + +reg_mstate("load", { + on_completion = function(gun, ammo_handler, next_state) + if gun.properties.ammo.magazine_only then + ammo_handler:load_magazine() + else + ammo_handler:load_flat() + end + + if (not next_state) or (next_state.action ~= "charge") then + --chamber the round automatically. + ammo_handler:chamber_round() + end + return true + end, + validation_check = function(gun, ammo_handler, next_state) + if gun.properties.ammo.magazine_only then + if not ammo_handler:can_load_magazine() then + return false + end + else + if not ammo_handler:can_load_flat() then + return false + end + end + return true + end +}) + +reg_mstate("charge", { + on_completion = function(gun, ammo_handler) + ammo_handler:chamber_round() + return + end, + validation_check = function(gun, ammo_handler, next_state) + if (ammo_handler.ammo.next_bullet ~= "empty") or (ammo_handler.ammo.total_bullets == 0) then + return false + end + return true + end +}) Guns4d.default_controls.reload = { conditions = {"zoom"}, loop = false, - timer = 0, --1 so we have a call to initialize the timer. + mode = "hybrid", + timer = 0, --1 so we have a call to initialize the timer. This will also mean that data.toggled and data.continue will need to be set manually --remember that the data table allows us to store arbitrary data func = function(active, interrupted, data, busy_list, gun, handler) local ammo_handler = gun.ammo_handler @@ -62,76 +184,21 @@ Guns4d.default_controls.reload = { 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_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 - end - next_state_index = next_state_index + 1 - - --for these two we don't want to continue unless we're done unloading. - elseif this_state.type == "load" then - - if props.ammo.magazine_only then - ammo_handler:load_magazine() - else - ammo_handler:load_flat() - end - - 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 - end - end - --typically i'd return, that's not an option. - - --handle the animations. - local next_state = props.reload[next_state_index] + if this_state then + assert(reload_actions[this_state.action], "no reload action by the name: "..tostring(this_state.action)) + local result = reload_actions[this_state.action].on_completion(gun, ammo_handler, next_state) + if result==true then + next_state_index = next_state_index + 1 + elseif result == false then + --do something? + elseif result == nil then + return + else + error("invalid on_completion return for reload state: "..this_state.action) + end + end --check that the next states are actually valid, if not, skip them local valid_state = false @@ -139,58 +206,23 @@ Guns4d.default_controls.reload = { next_state = props.reload[next_state_index] if next_state 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 - 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 --UNIMPLEMENTED - - if not ammo_handler.ammo.total_bullets > 0 then - 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 - invalid_state = true - end - else - - if not ammo_handler:can_load_flat() then - invalid_state = true - end - end - end - if not invalid_state then + assert(reload_actions[next_state.action], "no reload action by the name: "..tostring(next_state.action)) + local result = reload_actions[next_state.action].validation_check(gun, ammo_handler, next_state) + if result==true then valid_state=true - else + elseif result==false then next_state_index = next_state_index + 1 next_state = props.reload[next_state_index] + elseif result==nil then + return + else + error("invalid validation_check return for reload state: "..this_state.action) 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. + --if the next state doesn't exist, we've reached the end (the gun is reloaded) and we should restart. "continue" so it doesn't continue unless the user lets go of the input button. data.state = 0 - data.timer = 0.5 - data.held = true + --data.timer = 0.5 + data.continue = true return end end @@ -198,40 +230,47 @@ Guns4d.default_controls.reload = { --[[ if next_state == nil then data.state = 0 data.timer = 0 - data.held = true + data.continue = true return else]] data.state = next_state_index data.timer = next_state.time - data.held = false + data.continue = false + if data.current_mode == "toggle" then --this control uses hybrid and therefor may be on either mode. + data.toggled = true + end + local anim = next_state.anim if type(next_state.anim) == "string" then anim = props.visuals.animations[next_state.anim] + if not anim then + minetest.log("error", "improperly set gun reload animation, animation not found `"..next_state.anim.."`, gun `"..gun.itemstring.."`") + end 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.."`") + minetest.log("error", "improperly set gun reload animation, reload state `"..next_state.action.."`, gun `"..gun.itemstring.."`") end end if next_state.sounds then - local sounds = Guns4d.table.deep_copy(props.reload[next_state_index].sounds) + local sounds + if type(next_state.sounds) == "table" then + sounds = Guns4d.table.deep_copy(props.reload[next_state_index].sounds) + elseif type(next_state.sounds) == "string" then + sounds = assert(props.sounds[next_state.sounds]) + end 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)) + --print(dump(next_state_index)) --end elseif interrupted then local this_state = props.reload[data.state] - 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) --"true" is for to_ground - else - ammo_handler:unload_all(true) - end + if this_state and reload_actions[this_state.action].interrupt then + reload_actions[this_state.action].interrupt(gun, ammo_handler) end if data.played_sounds then Guns4d.stop_sounds(data.played_sounds) diff --git a/default_reload_actions.lua b/default_reload_actions.lua new file mode 100644 index 0000000..e69de29 diff --git a/infinite_ammo.lua b/infinite_ammo.lua index ba4c0f1..2aae163 100644 --- a/infinite_ammo.lua +++ b/infinite_ammo.lua @@ -26,6 +26,7 @@ minetest.register_chatcommand("ammoinf", { description = "quick toggle infinite ammo", privs = {privs=true}, func = function(caller, arg) + local trgt local args = string.split(arg, " ") local set_arg if #args > 1 then diff --git a/init.lua b/init.lua index a404170..2eb3f66 100644 --- a/init.lua +++ b/init.lua @@ -9,12 +9,15 @@ Guns4d.config = { show_mag_inv_ammo_bar = true, show_mag_inv_ammo_count = true, show_gun_inv_ammo_count = true, + control_hybrid_toggle_threshold = .3, + control_held_toggle_threshold = 0, empty_symbol = "0e", infinite_ammo_priv = "guns4d_infinite_ammo" } local path = minetest.get_modpath("guns4d") dofile(path.."/infinite_ammo.lua") dofile(path.."/misc_helpers.lua") +dofile(path.."/item_entities.lua") dofile(path.."/play_sound.lua") dofile(path.."/visual_effects.lua") dofile(path.."/default_controls.lua") diff --git a/item_entities.lua b/item_entities.lua new file mode 100644 index 0000000..c7afa11 --- /dev/null +++ b/item_entities.lua @@ -0,0 +1,124 @@ +--- adds 3d items for guns and magazines +-- @script item_entities.lua + +Guns4d.registered_items = {} +local old_spawn_item = core.spawn_item --didnt know if I had to use core instead of minetest or if they are the same reference, not chancing it though. +core.spawn_item = function(pos, item, ...) + if item then --if it doesnt exist, let it handle itself... + local stack = ItemStack(item) + local name = stack:get_name() + local def = Guns4d.registered_items[name] + if def then + local obj = minetest.add_entity(pos, "guns4d:item") + if obj then + obj:get_luaentity():set_item(stack:to_string()) + end + return obj + end + end + return old_spawn_item(pos, item, ...) +end + +--- table defining the new 3d entity for a dropped item +-- @field light_source int, equivelant to minetest itemdef version +-- @field size int, the size of the collision box +-- @field mesh string, the mesh to use for the item +-- @field textures table, a list of textures (see minetest entity documentation) +-- @field collisionbox_size, the size of collisionbox in tenths of meters. +-- @field selectionbox vector, xyz scale of the selectionbox +-- @field offset vector, xyz offset of the visual object from the collision and selectionbox. (so that magazines's origin can match their bone.) +-- @table guns4d_itemdef + +local defaults = { + --light_source = 0, + collisionbox_size = 2, + visual_size = 1, + offset = {x=0,y=0,z=0} +} +--- replaces the item entity of the provided item with a 3d entity based on the definition +-- @param itemstring +-- @param def, a @{guns4d_itemdef} +-- @function Guns4d.register_item() +function Guns4d.register_item(itemstring, def) + assert(minetest.registered_items[itemstring], "item: `"..tostring(itemstring).."` not registered by minetest") + assert(type(def)=="table", "definition is not a table") + def = Guns4d.table.fill(defaults, def) + if not def.selectionbox then + def.selectionbox = vector.new(def.collisionbox_size, def.collisionbox_size, def.collisionbox_size) + end + def.offset = vector.new(def.offset) + Guns4d.registered_items[itemstring] = def +end + +local def = table.copy(minetest.registered_entities["__builtin:item"]) +def.visual = "mesh" +def.visual_size = {x=1,y=1,z=1} +def.set_item = function(self, item) + local stack = ItemStack(item or self.itemstring) + + + self.itemstring = stack:to_string() + if self.itemstring == "" then + return + end + + local item_def = Guns4d.registered_items[stack:get_name()] + local cbox + local sbox + local a = item_def.collisionbox_size + local b = item_def.selectionbox + if item_def.realistic == true then + cbox = {-a/20, 0, -a/20, a/20, (a*2)/20, a/20} --we want the collision_box to sit above it. + sbox = {-b.x/20, 0, -b.z/20, b.x/20, b.y/10, b.z/20, rotate=true} + else + cbox = {-a/20, -a/20, -a/20, a/20, a/20, a/20} + sbox = {-b.x/20, -b.y/20, -b.z/20, b.x/20, b.y/20, b.z/20} + end + self.object:set_properties({ + is_visible = true, + visual = "mesh", + mesh = item_def.mesh, + textures = item_def.textures, + collisionbox = cbox, + selectionbox = sbox, + glow = item_def and item_def.light_source and math.floor(def.light_source/2+0.5), + visual_size = {x=item_def.visual_size,y=item_def.visual_size,z=item_def.visual_size}, + automatic_rotate = (not item_def.realistic) and math.pi * 0.5 * 0.2 / a, + infotext = stack:get_description(), + }) + self._collisionbox = cbox +end +local old = def.on_step +print(dump(def)) +def.on_step = function(self, dt, mr, ...) + old(self, dt, mr, ...) + --icky nesting. + if mr and mr.touching_ground then + local item_def = Guns4d.registered_items[ItemStack(self.itemstring):get_name()] + if item_def and not self._rotated then + if item_def.realistic then + self.object:set_properties({ + automatic_rotate = (not item_def.realistic) and math.pi * 0.5 * 0.2 / item_def.visual_size, + }) + local rot = self.object:get_rotation() + self.object:set_rotation({y=rot.y, x=rot.x+(math.pi/2), z=0}) + self._rotated = true + else + self.object:set_properties({ + automatic_rotate = (not item_def.realistic) and math.pi * 0.5 * 0.2 / item_def.visual_size, + }) + local rot = self.object:get_rotation() + self.object:set_rotation({y=rot.y, x=0, z=0}) + self._rotated = true + end + end + else + if self._rotated then + self.object:set_properties({ + automatic_rotate = 0, + }) + self._rotated = false + end + end +end +minetest.register_entity("guns4d:item", def) \ No newline at end of file diff --git a/play_sound.lua b/play_sound.lua index 62ddf4c..8f7ed1a 100644 --- a/play_sound.lua +++ b/play_sound.lua @@ -33,6 +33,7 @@ local sqrt = math.sqrt -- @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) +-- @field delay delay the playing of the sound -- @table guns4d_soundspec local function handle_min_max(tbl) @@ -47,8 +48,20 @@ end -- soundspec_to_play1, -- soundspec_to_play2 -- } --- @return out a list of Minetest sound handles [insert link] (in the order they came) +-- @return out a Guns4d sound handle (an integer) -- @function Guns4d.play_sounds +local sound_handles = {} +local function play_sound(sound, soundspec, handle, i) + if soundspec.delay then + minetest.after(soundspec.delay, function() + if sound_handles[handle] ~= false then + sound_handles[handle][i] = minetest.sound_play(sound, soundspec, soundspec.ephemeral) + end + end) + else + sound_handles[handle][i] = minetest.sound_play(sound, soundspec) + end +end function Guns4d.play_sounds(soundspecs_list) --print(dump(soundspecs_list)) --support a list of sounds to play @@ -67,8 +80,9 @@ function Guns4d.play_sounds(soundspecs_list) soundspecs_list[field] = nil --so it isn't iterated end end - --print(dump(soundspecs_list)) - local out = {} + local handle = #sound_handles+1 --determine the sound handle before playing + sound_handles[handle] = {} + local handle_object = sound_handles[handle] 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 = soundspec.sound @@ -82,12 +96,17 @@ function Guns4d.play_sounds(soundspecs_list) sound = Guns4d.math.weighted_randoms(sound) end assert(sound, "no sound found") + if not mtul.paths.media_paths[sound..".ogg"] then + minetest.log("error", "no sound by the name `"..mtul.paths.media_paths[sound..".ogg"].."`") + end + --print(dump(soundspecs_list), i) if soundspec.to_player then soundspec.pos = nil end if soundspec.min_hear_distance then local exclude_player_ref if soundspec.exclude_player then exclude_player_ref = minetest.get_player_by_name(soundspec.exclude_player) end + --play sound for all players outside min hear distance for _, player in pairs(minetest.get_connected_players()) do soundspec.sound = nil local pos = player:get_pos() @@ -95,22 +114,30 @@ function Guns4d.play_sounds(soundspecs_list) 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.sound_play(sound, soundspec) + play_sound(sound, soundspec, handle, i) end end else soundspec.sound = nil - outval = minetest.sound_play(sound, soundspec) + play_sound(sound, soundspec, handle, i) end - out[i] = outval end - return out + return handle +end +-- @param handle a Guns4d sound handle +-- @function Guns4d.get_sounds gets a list of currently playing Minetest sound handles from the Guns4d sound handle. Modification not reccomended. +function Guns4d.get_sounds(handle) + return sound_handles[handle] 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) +function Guns4d.stop_sounds(handle) + local handle_list = (type(handle) == "table" and handle) or sound_handles[handle] + if not handle_list then return false end + sound_handles[handle] = false --indicate to not play any delayed noises. for i, v in pairs(handle_list) do minetest.sound_stop(v) end + return true end \ No newline at end of file diff --git a/sounds/LICENSE ar_charge.ogg.txt b/sounds/LICENSE ar_charge.ogg.txt new file mode 100644 index 0000000..efa14ad --- /dev/null +++ b/sounds/LICENSE ar_charge.ogg.txt @@ -0,0 +1,3 @@ +ar_charge.ogg: +by yzhamua on freesound.org +License: CC-BY 4.0 https://creativecommons.org/licenses/by/4.0/ diff --git a/sounds/LICENSE ar_firing.ogg.txt b/sounds/LICENSE ar_firing.ogg.txt index 7b8dec6..603c31e 100644 --- a/sounds/LICENSE ar_firing.ogg.txt +++ b/sounds/LICENSE ar_firing.ogg.txt @@ -1,3 +1,3 @@ ar_firing.ogg: by SuperPhat on freesound.org -License: Creative Commons 0 +License: Creative Commons (CC0) diff --git a/sounds/LICENSE ar_firing_far.ogg.txt b/sounds/LICENSE ar_firing_far.ogg.txt index 0efb658..ebbbbdb 100644 --- a/sounds/LICENSE ar_firing_far.ogg.txt +++ b/sounds/LICENSE ar_firing_far.ogg.txt @@ -1,3 +1,3 @@ ar_firing_far.ogg: by (unknown) on opengameart.org (https://opengameart.org/content/the-free-firearm-sound-library) -License: Creative Commons +License: Creative Commons (CC0) diff --git a/sounds/LICENSE ar_mag_load.ogg.txt b/sounds/LICENSE ar_mag_load.ogg.txt new file mode 100644 index 0000000..ddde885 --- /dev/null +++ b/sounds/LICENSE ar_mag_load.ogg.txt @@ -0,0 +1 @@ +CC0 by serøtōnin on freesound.org \ No newline at end of file diff --git a/sounds/LICENSE ar_mag_store.ogg.txt b/sounds/LICENSE ar_mag_store.ogg.txt index e4db745..d10a88a 100644 --- a/sounds/LICENSE ar_mag_store.ogg.txt +++ b/sounds/LICENSE ar_mag_store.ogg.txt @@ -1,3 +1,2 @@ -ar_mag_store.ogg: -by serøtōnin on freesound.org -License: Creative Commons +Copyright duckduckpony CC-BY 4.0 +(on freesound.org) \ No newline at end of file diff --git a/sounds/LICENSE ar_mag_unload.ogg.txt b/sounds/LICENSE ar_mag_unload.ogg.txt index e69de29..ddde885 100644 --- a/sounds/LICENSE ar_mag_unload.ogg.txt +++ b/sounds/LICENSE ar_mag_unload.ogg.txt @@ -0,0 +1 @@ +CC0 by serøtōnin on freesound.org \ No newline at end of file diff --git a/sounds/ar_charge.ogg b/sounds/ar_charge.ogg new file mode 100644 index 0000000000000000000000000000000000000000..277d8855456c3f725dc1ad71570620893f5cf254 GIT binary patch literal 9067 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 literal 0 HcmV?d00001