fixed some issues with reloading. Fixed an issue where magazines broke all recipes in game except the most recently registered magazine's reload
This commit is contained in:
parent
8585180634
commit
6e74815ab7
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -4,6 +4,7 @@
|
||||
"vector",
|
||||
"dump",
|
||||
"player_api",
|
||||
"ItemStack"
|
||||
"ItemStack",
|
||||
"mtul"
|
||||
]
|
||||
}
|
76
TODO.txt
76
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
|
||||
|
69
ammo_api.lua
69
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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
0
default_reload_actions.lua
Normal file
0
default_reload_actions.lua
Normal file
@ -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
|
||||
|
3
init.lua
3
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")
|
||||
|
124
item_entities.lua
Normal file
124
item_entities.lua
Normal file
@ -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)
|
@ -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
|
3
sounds/LICENSE ar_charge.ogg.txt
Normal file
3
sounds/LICENSE ar_charge.ogg.txt
Normal file
@ -0,0 +1,3 @@
|
||||
ar_charge.ogg:
|
||||
by yzhamua on freesound.org
|
||||
License: CC-BY 4.0 https://creativecommons.org/licenses/by/4.0/
|
@ -1,3 +1,3 @@
|
||||
ar_firing.ogg:
|
||||
by SuperPhat on freesound.org
|
||||
License: Creative Commons 0
|
||||
License: Creative Commons (CC0)
|
||||
|
@ -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)
|
||||
|
1
sounds/LICENSE ar_mag_load.ogg.txt
Normal file
1
sounds/LICENSE ar_mag_load.ogg.txt
Normal file
@ -0,0 +1 @@
|
||||
CC0 by serøtōnin on freesound.org
|
@ -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)
|
@ -0,0 +1 @@
|
||||
CC0 by serøtōnin on freesound.org
|
BIN
sounds/ar_charge.ogg
Normal file
BIN
sounds/ar_charge.ogg
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user