diff --git a/block_values.lua b/block_values.lua index c537554..f390dfa 100644 --- a/block_values.lua +++ b/block_values.lua @@ -23,14 +23,14 @@ minetest.register_on_mods_loaded(function() local random_deviation = 1 local behavior_type = "normal" if groups.wood then - RHA = RHA*groups.wood*.1 + RHA = RHA*.1 random_deviation = random_deviation/groups.wood end if groups.oddly_breakable_by_hand then RHA = RHA / groups.oddly_breakable_by_hand end if groups.choppy then - RHA = RHA*(1+(groups.choppy*.5)) + RHA = RHA*.5 end if groups.flora or groups.grass then RHA = 0 @@ -42,12 +42,12 @@ minetest.register_on_mods_loaded(function() random_deviation = .005 end if groups.stone then - RHA = groups.stone + RHA = 1/groups.stone random_deviation = .5 end if groups.cracky then - RHA = RHA*groups.cracky - random_deviation = random_deviation*(groups.cracky*.5) + RHA = RHA*(.5/groups.cracky) + random_deviation = random_deviation*(.5/groups.cracky) end if groups.crumbly then RHA = RHA/groups.crumbly diff --git a/classes/Ammo_handler.lua b/classes/Ammo_handler.lua index e2d4faa..54b2429 100644 --- a/classes/Ammo_handler.lua +++ b/classes/Ammo_handler.lua @@ -3,84 +3,86 @@ Ammo_handler = Instantiatable_class:inherit({ construct = function(def) if def.instance then assert(def.gun, "no gun") - def.itemstack = def.gun.itemstack def.handler = def.gun.handler def.inventory = def.handler.inventory local meta = def.gun.meta local gun = def.gun def.ammo = {} - if gun.properties.magazine then + if gun.properties.ammo then if meta:get_string("guns4d_loaded_bullets") == "" then - meta:set_string("guns4d_loaded_mag", gun.properties.magazine.comes_with or "empty") - meta:set_string("guns4d_next_bullet", "empty") - meta:set_int("guns4d_total_bullets", 0) - meta:set_string("guns4d_loaded_bullets", minetest.serialize({})) - def.ammo.loaded_mag = "empty" + def.ammo.loaded_mag = gun.properties.ammo.comes_with or "empty" def.ammo.next_bullet = "empty" def.ammo.total_bullets = 0 - def.ammo.bullets = {} + def.ammo.loaded_bullets = {} + def:update_meta() else def.ammo.loaded_mag = meta:get_string("guns4d_loaded_mag") - def.ammo.bullets = minetest.deserialize(meta:get_string("guns4d_loaded_bullets")) + def.ammo.loaded_bullets = minetest.deserialize(meta:get_string("guns4d_loaded_bullets")) def.ammo.total_bullets = meta:get_int("guns4d_total_bullets") def.ammo.next_bullet = meta:get_string("guns4d_next_bullet") - def:update_has_ammo() end end end end }) --spend the round, return false if impossible. -function Ammo_handler:update_has_ammo() +--updates all properties based on the ammo table, bullets string can be passed directly to avoid duplication (when needed) +function Ammo_handler:update_meta(bullets) assert(self.instance, "attempt to call object method on a class") - if next(self.ammo.bullets) then - self.has_ammo = true - else - self.has_ammo = true - end + local meta = self.gun.meta + meta:set_string("guns4d_loaded_mag", self.ammo.loaded_mag) + meta:set_string("guns4d_loaded_bullets", bullets or minetest.serialize(self.ammo.loaded_bullets)) + meta:set_int("guns4d_total_bullets", self.ammo.total_bullets) + meta:set_string("guns4d_next_bullet", self.ammo.next_bullet) + self.handler.player:set_wielded_item(self.gun.itemstack) 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") local bullet_spent = self.ammo.next_bullet local meta = self.gun.meta --subtract the bullet - print(bullet_spent) - print(self.ammo.total_bullets) if self.ammo.total_bullets > 0 then - self.ammo.bullets[bullet_spent] = self.ammo.bullets[bullet_spent]-1 - if self.ammo.bullets[bullet_spent] == 0 then self.ammo.bullets[bullet_spent] = nil end + 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 - meta:set_string("guns4d_loaded_bullets", minetest.serialize(self.ammo.bullets)) - meta:set_int("guns4d_total_bullets", self.ammo.total_bullets) --set the new current bullet - if next(self.ammo.bullets) then - self.ammo.next_bullet = math.weighted_randoms(self.ammo.bullets) + if next(self.ammo.loaded_bullets) then + self.ammo.next_bullet = math.weighted_randoms(self.ammo.loaded_bullets) meta:set_string("guns4d_next_bullet", self.ammo.next_bullet) else self.ammo.next_bullet = "empty" meta:set_string("guns4d_next_bullet", "empty") end - minetest.chat_send_all(self.ammo.total_bullets) - return true - else - return false + + self:update_meta() + return bullet_spent end end function Ammo_handler:load_magazine() assert(self.instance, "attempt to call object method on a class") local inv = self.inventory local magstack_index - local highest_ammo = 0 + local highest_ammo = -1 local gun = self.gun local gun_accepts = gun.accepted_magazines - print(dump(gun_accepts)) + if self.ammo.loaded_mag ~= "empty" then + --it's undefined, make assumptions. + self:unload_all() + end for i, v in pairs(inv:get_list("main")) do if gun_accepts[v:get_name()] then - print("success1") local meta = v:get_meta() - if meta:get_int("guns4d_total_bullets") > highest_ammo then - print("success2") + --intiialize data if it doesn't exist so it doesnt kill itself + if 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") + if ammo > highest_ammo then + highest_ammo = ammo local has_unaccepted = false + print(meta:get_string("guns4d_loaded_bullets")) for bullet, _ in pairs(minetest.deserialize(meta:get_string("guns4d_loaded_bullets"))) do if not gun.accepted_bullets[bullet] then has_unaccepted = true @@ -97,24 +99,108 @@ function Ammo_handler:load_magazine() --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.bullets = minetest.deserialize(magstack_meta:get_string("guns4d_loaded_bullets")) + self.ammo.loaded_bullets = minetest.deserialize(bullet_string) self.ammo.total_bullets = magstack_meta:get_int("guns4d_total_bullets") self.ammo.next_bullet = magstack_meta:get_string("guns4d_next_bullet") - -- - meta:set_string("guns4d_loaded_mag", self.ammo.loaded_mag) - meta:set_string("guns4d_loaded_bullets", magstack_meta:get_string("guns4d_loaded_bullets")) - meta:set_int("guns4d_total_bullets", self.ammo.total_bullets) - meta:set_string("guns4d_next_bullet", self.ammo.next_bullet) - + self:update_meta() inv:set_stack("main", magstack_index, "") return end end +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 + return true + end + if (not gun.properties.magazine_only) and gun.accepted_bullets[v:get_name()] then + return true + end + end + return false +end +function Ammo_handler:can_load_magazine() + local inv = self.inventory + local gun = self.gun + local gun_accepts = gun.accepted_magazines + for i, v in pairs(inv:get_list("main")) do + if gun_accepts[v:get_name()] then + return true + end + end + return false +end -function Ammo_handler:unload_mag() +function Ammo_handler:unload_magazine(to_ground) assert(self.instance, "attempt to call object method on a class") + if self.ammo.loaded_mag ~= "empty" then + minetest.chat_send_all("not empty") + local inv = self.handler.inventory + local magstack = ItemStack(self.ammo.loaded_mag) + local magmeta = magstack:get_meta() + 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")) + magmeta:set_string("guns4d_next_bullet", gunmeta:get_string("guns4d_next_bullet")) + magstack = Guns4d.ammo.update_mag(nil, magstack, magmeta) + --throw it on the ground if to_ground is true + local remaining + if to_ground then + remaining = magstack + else + remaining = inv:add_item("main", magstack) + 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})) + end + self.ammo.loaded_mag = "empty" + self.ammo.next_bullet = "empty" + self.ammo.total_bullets = 0 + self.ammo.loaded_bullets = {} + self:update_meta() + end +end +--this is used for unloading flat, or unloading as a "clip" aka a feed only magazine, you'd use this for something like an m1 garand. God that ping. +function Ammo_handler:unload_all(to_ground) + assert(self.instance, "attempt to call object method on a class") + local inv = self.handler.inventory + for i, v in pairs(self.ammo.loaded_bullets) do + local leftover + --if to_ground is true throw it to the ground + if to_ground then + leftover = ItemStack("main", i.." "..tostring(v)) + else + leftover = inv:add_item("main", i.." "..tostring(v)) + 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 + local stack + if to_ground or Guns4d.ammo.registered_magazines[self.ammo.loaded_mag].hot_eject then + stack = ItemStack(self.ammo.loaded_mag) + else + stack = inv:add_item("main", self.ammo.loaded_mag) + 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" + self.ammo.next_bullet = "empty" + self.ammo.total_bullets = 0 + self.ammo.loaded_bullets = {} + self:update_meta() end function Ammo_handler:load_magless() assert(self.instance, "attempt to call object method on a class") diff --git a/classes/Bullet_ray.lua b/classes/Bullet_ray.lua index 9eed78f..9c8a937 100644 --- a/classes/Bullet_ray.lua +++ b/classes/Bullet_ray.lua @@ -7,7 +7,7 @@ local ray = { --last_dir --exit_direction = dir, --range_left = def.bullet.range, - --force_mmRHA = def.bullet.penetration_RHA + --energy = def.bullet.penetration_RHA ITERATION_DISTANCE = .3, damage = 0 } @@ -19,7 +19,7 @@ function ray:record_state() }) end --find (valid) edge. Slabs or other nodeboxes that are not the last hit position are not considered (to account for holes) TODO: update to account for hollow nodes -function ray:transverse_end_point() +function ray:find_transverse_end_point() assert(self.instance, "attempt to call obj method on a class") local pointed local cast = minetest.raycast(self.pos+(self.dir*(self.ITERATION_DISTANCE+.01)), self.pos, false, false) @@ -31,44 +31,50 @@ function ray:transverse_end_point() end end if pointed and vector.distance(pointed.intersection_point, self.pos) < self.ITERATION_DISTANCE then - self.normal = pointed.intersection_normal - self.exit_direction = vector.direction(self.dir, vector.new()) --reverse dir is exit direction (for VFX) - return pointed.intersection_point + return pointed.intersection_point, pointed.intersection_normal end end -function ray:cast() +function ray:_cast() assert(self.instance, "attempt to call obj method on a class") - local end_pos = self.pos+(self.dir*self.range) - --if block ends early, then we set end position accordingly - local next_penetration_val + local next_state = self.state --next state of course the state of the next ray. + + local end_normal + + local end_pos local edge - local next_state = self.state + --if block ends early, then we find it and set end position of the ray accordingly. + --edge is where the section of solid blocks ends and becomes open air again. if self.state == "transverse" then - edge = self:transverse_end_point() + edge, end_normal = self:find_transverse_end_point() if edge then end_pos = edge next_state = "free" else end_pos = self.pos+(self.dir*self.ITERATION_DISTANCE) end + else + end_pos = self.pos+(self.dir*self.range) end - local continue = true + --do the main raycast. We don't account for mmRHA dropoff here. + local continue = true --indicates wether to :_iterate wether the Bullet_ray has ended local cast = minetest.raycast(self.pos, end_pos, true, true) local pointed for hit in cast do - if not continue then break end if vector.distance(hit.intersection_point, self.pos) > 0.0005 and vector.distance(hit.intersection_point, self.pos) < self.range then --if it's a node, check that it's note supposed to be ignored according to it's generated properties if hit.type == "node" then if self.state == "free" and Guns4d.node_properties[minetest.get_node(hit.under).name].behavior ~= "ignore" then next_state = "transverse" pointed = hit + end_normal = hit.intersection_normal + end_pos = pointed.intersection_point break end if self.state == "transverse" then --if it isn't the same name as the last node we intersected, then it's a different block with different stats for penetration if minetest.get_node(hit.under).name ~= self.last_node_name then pointed = hit + end_pos = pointed.intersection_point end --make sure it's set to transverse if the edge has a block infront of it if Guns4d.node_properties[minetest.get_node(hit.under).name].behavior == "ignore" then @@ -81,7 +87,7 @@ function ray:cast() end --if it's an object, make sure it's not the player object --note that while it may seem like this will create a infinite hit loop, it resolves itself as the intersection_point of the next ray will be close enough as to skip the pointed. See first line of iterator. - if hit.type == "object" and hit.ref ~= self.player then + if (hit.type == "object") and (hit.ref ~= self.player) and ((not self.last_pointed) or (hit.ref ~= self.last_pointed.ref)) then if self.over_penetrate then pointed = hit break @@ -90,62 +96,83 @@ function ray:cast() continue = false break end + end_pos = pointed.intersection_point end end end - if pointed then + --[[if pointed then end_pos = pointed.intersection_point if self.state == "transverse" then - next_penetration_val = self.force_mmRHA-(vector.distance(self.pos, end_pos)*Guns4d.node_properties[self.last_node_name].mmRHA) + next_penetration_val = self.energy-(vector.distance(self.pos, end_pos)*Guns4d.node_properties[self.last_node_name].mmRHA) else -- transverse - next_penetration_val = self.force_mmRHA-(vector.distance(self.pos, end_pos)*self.dropoff_mmRHA) + next_penetration_val = self.energy-(vector.distance(self.pos, end_pos)*self.dropoff_mmRHA) end else --if there is no pointed, and it's not transverse, then the ray has ended. if self.state == "transverse" then - next_penetration_val = self.force_mmRHA-(vector.distance(self.pos, end_pos)*Guns4d.node_properties[self.last_node_name].mmRHA) + next_penetration_val = self.energy-(vector.distance(self.pos, end_pos)*Guns4d.node_properties[self.last_node_name].mmRHA) else --free continue = false - next_penetration_val = self.force_mmRHA-(self.range*self.dropoff_mmRHA) + next_penetration_val = self.energy-(self.range*self.dropoff_mmRHA) end - end + end]] --set "last" values. - return pointed, next_penetration_val, next_state, end_pos, continue + return pointed, next_state, end_pos, end_normal, continue end -function ray:iterate(initialized) +--the main function. +function ray:_iterate(initialized) assert(self.instance, "attempt to call obj method on a class") - local pointed, penetration, next_state, end_pos, continue = self:cast() - self.range = self.range-vector.distance(self.pos, end_pos) - self.pos = end_pos - self.force_mmRHA = penetration + local pointed, next_state, end_pos, end_normal, continue = self:_cast() + + local distance = vector.distance(self.pos, end_pos) + if self.state == "free" then + self.energy = self.energy-(distance*self.energy_dropoff) + else + local penetration_loss = distance*Guns4d.node_properties[self.last_node_name].mmRHA + --calculate our energy loss based on the percentage of energy our penetration represents. + minetest.chat_send_all(penetration_loss/self.init_penetration) + minetest.chat_send_all(distance) + minetest.chat_send_all(Guns4d.node_properties[self.last_node_name].mmRHA) + --minetest.chat_send_all(penetration_loss) + self.energy = self.energy-((self.init_energy*self.energy_sharp_ratio)*(penetration_loss/self.init_penetration)) + end + --set values for next iteration. + self.range = self.range-distance + if self.range <= 0.0005 or self.energy < 0 then + continue = false + minetest.chat_send_all("range ended, dist:"); minetest.chat_send_all(tostring(distance)) + end ---@diagnostic disable-next-line: assign-type-mismatch self.state = next_state if pointed then self.last_pointed = pointed - end - if pointed then - if pointed.type == "node" then - self.last_node_name = minetest.get_node(pointed.under).name - elseif pointed.type == "object" then - ray:hit_entity(pointed.ref) + self.pos = pointed.intersection_point + if self.energy > 0 then + if pointed.type == "node" then + self.last_node_name = minetest.get_node(pointed.under).name + elseif pointed.type == "object" then + ray:hit_entity(pointed.ref) + end end + else + self.pos = end_pos end table.insert(self.history, { pos = self.pos, - force_mmRHA = self.force_mmRHA, + energy = self.energy, state = self.state, last_node = self.last_node_name, - normal = self.normal, + normal = end_normal, --end normal may be nil, as it's only for hit effects. }) - if continue and self.range > 0 and self.force_mmRHA > 0 then - self:iterate(true) + if continue and self.range > 0 and self.energy > 0 then + self:_iterate(true) end if not initialized then for i, v in pairs(self.history) do - --[[local hud = self.player:hud_add({ + local hud = self.player:hud_add({ hud_elem_type = "waypoint", - text = "mmRHA:"..tostring(math.floor(v.force_mmRHA or 0)).." ", + text = "mmRHA:"..tostring(v.energy).." ", number = 255255255, precision = 1, world_pos = v.pos, @@ -155,26 +182,49 @@ function ray:iterate(initialized) }) minetest.after(40, function(hud) self.player:hud_remove(hud) - end, hud)]] + end, hud) end end end +function ray:calculate_blunt_damage(bullet, armor, groups) +end +function ray:calculate_sharp_conversion(bullet, armor, groups) +end +function ray:calculate_sharp_damage(bullet, armor, groups) +end +function ray:apply_damage(object, blunt_pen, sharp_pen, blunt_dmg, sharp_dmg) + object:punch() +end function ray.construct(def) if def.instance then assert(def.player, "no player") assert(def.pos, "no position") assert(def.dir, "no direction") + assert(def.gun, "no Gun object") assert(def.range, "no range") - assert(def.force_mmRHA, "no force") - assert(def.dropoff_mmRHA, "no force dropoff") - --assert(def.on_node_hit, "no node hit behavior") - assert(def.hit_entity, "no entity hit behavior") - def.init_force_mmRHA = def.force_mmRHA + assert(def.energy, "no energy") + assert(def.energy_dropoff, "no energy dropoff") + assert(def.blunt_damage, "no blunt damage") + + def.sharp_damage = def.sharp_damage or 0 + def.sharp_penetration = def.sharp_penetration or 0 + if def.sharp_penetration==0 then + def.blunt_penetration = def.blunt_penetration or def.energy/2 + else + def.blunt_penetration = def.blunt_penetration or def.energy + end + + def.init_energy = def.energy + def.init_penetration = def.sharp_penetration + def.init_blunt = def.blunt_penetration + --blunt pen is in the same units (1 Joule/Area^3 = 1 MPa), so we use it to make the ratio by subtraction. + def.energy_sharp_ratio = (def.energy-def.blunt_penetration)/def.energy + def.dir = vector.new(def.dir) def.pos = vector.new(def.pos) def.history = {} - def:iterate() + def:_iterate() end end Guns4d.bullet_ray = Instantiatable_class:inherit(ray) \ No newline at end of file diff --git a/classes/Control_handler.lua b/classes/Control_handler.lua index 963a17f..bcd43d3 100644 --- a/classes/Control_handler.lua +++ b/classes/Control_handler.lua @@ -10,13 +10,17 @@ Guns4d.control_handler = { call_before_timer = false, loop = false, func=function(active, interrupted, data, busy_controls) - data = { - - } } } ]] } +--data table: +--[[ + { + held = bool + timer = float + } +]] local controls = Guns4d.control_handler --[[-modify controls (future implementation if needed) function controls.modify() @@ -46,7 +50,7 @@ function controls:update(dt) if data.timer <= 0 and ((not data.held) or def.loop) then data.held = true table.insert(call_queue, {control=def, active=true, interrupt=false, data=data}) - elseif def.call_before_timer then --this is useful for functionst that need to play animations for their progress. + elseif def.call_before_timer and not data.held then --this is useful for functions that need to play animations for their progress. table.insert(call_queue, {control=def, active=false, interrupt=false, data=data}) end else @@ -60,7 +64,6 @@ function controls:update(dt) end end end - local count = 0 --busy list is so we can tell if a function should be allowed or not if #busy_list == 0 then busy_list = nil end for i, tbl in pairs(call_queue) do diff --git a/classes/Gun.lua b/classes/Gun.lua index 9c2927a..d5d7a77 100644 --- a/classes/Gun.lua +++ b/classes/Gun.lua @@ -2,19 +2,21 @@ local Vec = vector local gun_default = { --itemstack = Itemstack --gun_entity = ObjRef - name = "__template__", + name = "__guns4d:default__", + itemstring = "", registered = {}, property_modifiers = {}, properties = { - hip = { + hip = { --used by gun entity (attached offset) offset = Vec.new(), }, - ads = { + ads = { --used by player_handler, animation handler (eye bone offset from horizontal_offset), gun entity (attached offset) offset = Vec.new(), horizontal_offset = 0, + aim_time = 1, }, - recoil = { - velocity_correction_factor = { + recoil = { --used by update_recoil() + velocity_correction_factor = { --velocity correction factor is currently very broken. gun_axial = 1, player_axial = 1, }, @@ -22,7 +24,7 @@ local gun_default = { gun_axial = 1, player_axial = 1, }, - angular_velocity_max = { + angular_velocity_max = { --max velocity, so your gun doesnt "spin me right round baby round round" gun_axial = 1, player_axial = 1, }, @@ -39,7 +41,7 @@ local gun_default = { player_axial = 1, }, }, - sway = { + sway = { --used by update_sway() max_angle = { gun_axial = 0, player_axial = 0, @@ -49,59 +51,34 @@ local gun_default = { player_axial = 0, }, }, - walking_offset = { - gun_axial = {x=.2, y=-.2}, + walking_offset = { --used by update_walking() (or something) + gun_axial = {x=1, y=-1}, player_axial = {x=1,y=1}, }, - controls = { - aim = { - conditions = {"RMB"}, - loop = false, - timer = 0, - func = function(active, interrupted, data, busy_list, handler) - if active then - handler.control_bools.ads = not handler.control_bools.ads - end - end - }, - fire = { - conditions = {"LMB"}, - loop = true, - timer = 0, - func = function(active, interrupted, data, busy_list, handler) - if not handler.control_handler.busy_list.on_use then - handler.gun:attempt_fire() - end - end - }, - reload = { - conditions = {"zoom"}, - loop = false, - timer = 0, - func = function(active, interrupted, data, busy_list, handler) - if not handler.control_handler.busy_list.on_use then - local props = handler.gun.properties - handler.gun.ammo_handler:load_magazine() - if not props.magazine.magazine_only then - --flat reload? - end - handler.player:set_wielded_item(handler.gun.itemstack) - end - end - }, - on_use = function(itemstack, handler, pointed_thing) - handler.gun:attempt_fire() - handler.control_handler.busy_list.on_use = true - end + controls = { --used by control_handler + __overfill=true, --if present, this table will not be filled in. + aim = Guns4d.default_controls.aim, + --fire = Guns4d.default_controls.fire, + reload = Guns4d.default_controls.reload, + on_use = Guns4d.default_controls.on_use }, - magazine = { + reload = { --used by defualt controls. Still provides usefulness elsewhere. + __overfill=true, --if present, this table will not be filled in. + {type="unload", time=1, anim="unload", interupt="to_ground", hold = true}, + {type="load", time=1, anim="load"} + }, + ammo = { --used by ammo_handler magazine_only = false, + accepted_bullets = {}, accepted_magazines = {} }, - accepted_bullets = {}, - flash_offset = Vec.new(), - aim_time = 1, - firerateRPM = 10000, + animations = { --used by animations handler for idle, and default controls + empty = {x=0,y=0}, + loaded = {x=1,y=1}, + }, + --used by ammo_handler + flash_offset = Vec.new(), --used by fire() (for fsx and ray start pos) [RENAME NEEDED] + firerateRPM = 600, --used by update() and by extent fire() + default controls ammo_handler = Ammo_handler }, offsets = { @@ -149,6 +126,15 @@ local gun_default = { HAS_SWAY = true, HAS_WAG = true, INFINITE_AMMO_IN_CREATIVE = true, + DEFAULT_FPS = 20, + LOOP_IDLE_ANIM = false + }, + animation_data = { --where animations data is stored. + anim_runtime = 0, + length = 0, + fps = 0, + animations_frames = {0,0}, + current_frame = 0, }, particle_spawners = {}, walking_tick = 0, @@ -158,32 +144,27 @@ local gun_default = { muzzle_flash = Guns4d.muzzle_flash } -function gun_default:spend_round() -end function gun_default:attempt_fire() assert(self.instance, "attempt to call object method on a class") if self.rechamber_time <= 0 then - if self.ammo_handler:spend_round() then + local spent_bullet = self.ammo_handler:spend_round() + if spent_bullet then local dir = self.dir local pos = self:get_pos() - Guns4d.bullet_ray:new({ + --[[print(dump(Guns4d.ammo.registered_bullets)) + print(self.ammo_handler.next_bullet) + print(Guns4d.ammo.registered_bullets[self.ammo_handler.next_bullet])]] + local bullet_def = table.fill(Guns4d.ammo.registered_bullets[spent_bullet], { player = self.player, pos = pos, dir = dir, - range = 100, - gun = self, - force_mmRHA = 1, - dropoff_mmRHA = 0, - hit_entity = function(pointed) - local damage = math.floor((self.damage*(self.force_mmRHA/self.init_force_mmRHA))+1) - pointed.ref:punch(self.player, nil, {damage_groups = {fleshy = damage, penetration_mmRHA=self.force_mmRHA}}, self.dir) - end + gun = self }) + Guns4d.bullet_ray:new(bullet_def) self:recoil() self:muzzle_flash() self.rechamber_time = 60/self.properties.firerateRPM end - self.player:set_wielded_item(self.itemstack) end end @@ -207,12 +188,12 @@ end function gun_default:get_player_axial_dir(rltv) assert(self.instance, "attempt to call object method on a class") local player = self.player - local player_rotation = 0 - if not rltv then player_rotation = self.offsets.player_rotation.x end local rotation = self.offsets.total_offset_rotation local dir = Vec.new(Vec.rotate({x=0, y=0, z=1}, {y=0, x=((rotation.player_axial.x)*math.pi/180), z=0})) dir = Vec.rotate(dir, {y=((rotation.player_axial.y)*math.pi/180), x=0, z=0}) - dir = Vec.rotate(dir, {x=player_rotation*(math.pi/180),y=0,z=0}) + if not rltv then + dir = Vec.rotate(dir, {x=self.offsets.player_rotation.x*(math.pi/180),y=0,z=0}) + end --[[local hud_pos = Vec.rotate(dir, {x=0,y=self.offsets.player_rotation.y*math.pi/180,z=0})+player:get_pos()+{x=0,y=player:get_properties().eye_height,z=0}+vector.rotate(player:get_eye_offset()/10, {x=0,y=self.offsets.player_rotation.y*math.pi/180,z=0}) local hud = player:hud_add({ hud_elem_type = "image_waypoint", @@ -229,18 +210,13 @@ function gun_default:get_player_axial_dir(rltv) end function gun_default:get_dir(rltv) assert(self.instance, "attempt to call object method on a class") - local player = self.player - local player_rotation - if rltv then - player_rotation = Vec.new() - else - player_rotation = Vec.new(self.offsets.player_rotation.x, self.offsets.player_rotation.y, 0) - end local rotation = self.offsets.total_offset_rotation 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}) --for it to be relative the the camera, rotation by player look occours post. - dir = Vec.rotate(dir, {x=player_rotation.x*math.pi/180,y=player_rotation.y*math.pi/180,z=0}) + if not rltv then + dir = Vec.rotate(dir, {x=self.offsets.player_rotation.x*math.pi/180,y=self.offsets.player_rotation.y*math.pi/180,z=0}) + 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", @@ -253,7 +229,6 @@ function gun_default:get_dir(rltv) minetest.after(0, function(hud) player:hud_remove(hud) end, hud)]] - return dir end @@ -321,14 +296,18 @@ function gun_default:update(dt) local look_rotation = {x=handler.look_rotation.x,y=handler.look_rotation.y} local total_rot = self.offsets.total_offset_rotation local player_rot = self.offsets.player_rotation - local constant = 1.4 + local constant = 6 - --player look rotation - local next_vert_aim = ((player_rot.x+look_rotation.x)/(1+((constant*10)*dt)))-look_rotation.x - if math.abs(look_rotation.x-next_vert_aim) > .005 then - player_rot.x = next_vert_aim + --player look rotation. I'm going to keep it real, I don't remember what this equation does. + if not self.sprite_scope then + local next_vert_aim = ((player_rot.x+look_rotation.x)/(1+constant*dt))-look_rotation.x + if math.abs(look_rotation.x-next_vert_aim) > .005 then + player_rot.x = next_vert_aim + else + player_rot.x = -look_rotation.x + end else - player_rot.x = look_rotation.x + player_rot.x = -look_rotation.x end --timers if self.rechamber_time > 0 then @@ -346,6 +325,8 @@ function gun_default:update(dt) if self.consts.HAS_WAG then self:update_wag(dt) end self.dir = self:get_dir() self.local_dir = self:get_dir(true) + self.paxial_dir = self:get_player_axial_dir() + self.local_paxial_dir = self:get_player_axial_dir(true) --sprite scope if self.properties.sprite_scope then @@ -427,7 +408,66 @@ function gun_default:update_recoil(dt) end end end - +local animation_data = { + anim_runtime = 0, + length = 0, + fps = 0, + animations_frames = {0,0}, + current_frame = 0, +} +function gun_default:animation_update(dt) + local ent = self.entity + local data = self.animation_data + local anim_range, frame_speed, frame_blend, frame_loop = ent:get_animation() + if (not data.animation_frames) or (anim_range.x ~= data.x) or (anim_range.y ~= data.y) then + data.runtime = 0 + data.animations_frames = false + elseif data.animation_frames then + data.runtime = data.runtime + dt + data.current_frame = math.clamp(data.runtime*data.fps, data.animation_frames.x, data.animation_frames.y) + end +end +function gun_default:set_animation(frames, length, fps, loop) + loop = loop or false --why the fuck default is loop? I DONT FUCKIN KNOW + assert(type(frames)=="table" and frames.x and frames.y, "frames invalid or nil in set_animation()!") + assert(length or fps, "need either length or FPS for animation") + assert(not (length and fps), "cannot play animation with both specified length and specified fps. Only one parameter can be used.") + local num_frames = math.abs(frames.x-frames.y) + local data = self.animation_data + if length then + fps = num_frames/length + elseif fps then + length = num_frames/fps + else + fps = self.consts.DEFAULT_FPS + length = num_frames/self.consts.DEFAULT_FPS + end + data.runtime = 0 + data.length = length + self.entity:set_animation(frames, fps, 0, loop) +end +function gun_default:clear_animation() + local loaded = false + for i, v in pairs(self.ammo_handler) do + print(i,v ) + end + if self.properties.ammo.magazine_only then + if self.ammo_handler.ammo.loaded_mag ~= "empty" then + loaded = true + end + elseif self.ammo_handler.ammo.total_bullets > 0 then + loaded = true + end + if loaded then + self.entity:set_animation({x=self.properties.animations.loaded.x, y=self.properties.animations.loaded.y}, 0, 0, self.consts.LOOP_IDLE_ANIM) + else + self.entity:set_animation({x=self.properties.animations.empty.x, y=self.properties.animations.empty.y}, 0, 0, self.consts.LOOP_IDLE_ANIM) + end + local data = self.animation_data + data.runtime = 0 + data.length = false + data.animations_frames = false +end function gun_default:update_breathing(dt) local breathing_info = {pause=1.4, rate=4.2} --we want X to be between 0 and 4.2. Since math.pi is a positive crest, we want X to be above it before it reaches our- @@ -469,6 +509,20 @@ function gun_default:prepare_deletion() if self.sprite_scope then self.sprite_scope:prepare_deletion() end end --construction for the base gun class +local valid_ctrls = { + up=true, + down=true, + left=true, + right=true, + jump=true, + aux1=true, + sneak=true, + dig=true, + place=true, + LMB=true, + RMB=true, + zoom=true, +} gun_default.construct = function(def) if def.instance then --make some quick checks. @@ -513,15 +567,6 @@ gun_default.construct = function(def) def.offsets[i] = Vec.new() end end - def.accepted_bullets = {} - for _, v in pairs(def.properties.accepted_bullets) do - def.accepted_bullets[v] = true - end - def.accepted_magazines = {} - print(dump(def.properties.magazine.accepted_magazines)) - for _, v in pairs(def.properties.magazine.accepted_magazines) do - def.accepted_magazines[v] = true - end --def.velocities = table.deep_copy(def.base_class.velocities) def.velocities = {} @@ -541,39 +586,73 @@ gun_default.construct = function(def) end end if def.properties.entity_scope then - if not def.sprite_scope then + if not def.entity_scope then end end def.ammo_handler = def.properties.ammo_handler:new({ gun = def }) - elseif def.name ~= "__template__" then + elseif def.name ~= "__guns4d:default__" then local props = def.properties - assert(def.name, "no name provided") - assert(def.itemstring, "no itemstring provided") - assert(minetest.registered_items[def.itemstring], "item is not registered, check dependencies.") - --override methods so control handler can do it's job - local old_on_use = minetest.registered_items[def.itemstring].on_use - local old_on_s_use = minetest.registered_items[def.itemstring].on_secondary_use - minetest.override_item(def.itemstring, { - on_use = function(itemstack, user, pointed_thing) - if old_on_use then - old_on_use(itemstack, user, pointed_thing) - end - Guns4d.players[user:get_player_name()].handler.control_handler:on_use(itemstack, pointed_thing) - end, - on_secondary_use = function(itemstack, user, pointed_thing) - if old_on_s_use then - old_on_s_use(itemstack, user, pointed_thing) - end - Guns4d.players[user:get_player_name()].handler.control_handler:on_secondary_use(itemstack, pointed_thing) - end - }) - def.properties = table.fill(def.parent_class.properties, def.properties or {}) - def.consts = table.fill(def.parent_class.consts, def.consts or {}) + --validate controls, done before properties are filled to avoid duplication. + if props.controls then + for i, control in pairs(props.controls) 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 + if not valid_ctrls[condition] then + assert(false, "invalid key: '"..condition.."'") + end + end + end + end + end + + --fill in the properties. + def.properties = table.fill(def.parent_class.properties, props or {}) + print(table.tostring(def.properties)) + def.consts = table.fill(def.parent_class.consts, def.consts or {}) + props = def.properties --have to reinitialize this as the reference is replaced. + + if def.name ~= "__template" then + assert(rawget(def, "name"), "no name provided in new class") + assert(rawget(def, "itemstring"), "no itemstring provided in new class") + assert(not((props.ammo.capacity) and (not props.ammo.magazine_only)), "gun does not accept magazines, but has no set capcity! Please define ammo.capacity") + assert(minetest.registered_items[def.itemstring], def.itemstring.." : item is not registered, check dependencies.") + + --override methods so control handler can do it's job + local old_on_use = minetest.registered_items[def.itemstring].on_use + local old_on_s_use = minetest.registered_items[def.itemstring].on_secondary_use + --override the item to hook in controls. (on_drop needed) + minetest.override_item(def.itemstring, { + on_use = function(itemstack, user, pointed_thing) + if old_on_use then + old_on_use(itemstack, user, pointed_thing) + end + Guns4d.players[user:get_player_name()].handler.control_handler:on_use(itemstack, pointed_thing) + end, + on_secondary_use = function(itemstack, user, pointed_thing) + if old_on_s_use then + old_on_s_use(itemstack, user, pointed_thing) + end + Guns4d.players[user:get_player_name()].handler.control_handler:on_secondary_use(itemstack, pointed_thing) + end + }) + end + + def.accepted_bullets = {} + for _, v in pairs(def.properties.ammo.accepted_bullets) do + def.accepted_bullets[v] = true + end + def.accepted_magazines = {} + for _, v in pairs(def.properties.ammo.accepted_magazines) do + def.accepted_magazines[v] = true + end + --add gun def to the registered table Guns4d.gun.registered[def.name] = def + --register the visual entity minetest.register_entity(def.name.."_visual", { initial_properties = { diff --git a/classes/Instantiatable_class.lua b/classes/Instantiatable_class.lua index f31e8cc..9370245 100644 --- a/classes/Instantiatable_class.lua +++ b/classes/Instantiatable_class.lua @@ -1,5 +1,6 @@ Instantiatable_class = { - instance = false + instance = false, + __no_copy = true } --not that construction change is NOT called for inheriting an object. function Instantiatable_class:inherit(def) @@ -7,6 +8,7 @@ function Instantiatable_class:inherit(def) --if not def then def = {} else def = table.shallow_copy(def) end def.parent_class = self def.instance = false + def.__no_copy = true def._construct_low = def.construct --this effectively creates a construction chain by overwriting .construct function def.construct(parameters) @@ -18,7 +20,6 @@ function Instantiatable_class:inherit(def) self.construct(parameters) end end - --print("CONSTRUCTED") def.construct(def) --iterate through table properties setmetatable(def, {__index = self}) @@ -28,6 +29,7 @@ function Instantiatable_class:new(def) --if not def then def = {} else def = table.shallow_copy(def) end def.base_class = self def.instance = true + def.__no_copy = true function def:inherit(def) assert(false, "cannot inherit instantiated object") end diff --git a/classes/Player_handler.lua b/classes/Player_handler.lua index 72c79bc..a7d1242 100644 --- a/classes/Player_handler.lua +++ b/classes/Player_handler.lua @@ -46,8 +46,11 @@ function player_handler:update(dt) self.model_handler = model_handler.get_handler(self:get_properties().mesh):new({player=self.player}) ----control handler---- self.control_handler = Guns4d.control_handler:new({player=player, controls=self.gun.properties.controls}) - --reinitialize some handler data and set set_hud_flags + + --this needs to be stored for when the gun is unset! self.horizontal_offset = self.gun.properties.ads.horizontal_offset + + --set_hud_flags player:hud_set_flags({wielditem = false, crosshair = false}) --for the gun's scopes to work properly we need predictable offsets. @@ -84,11 +87,11 @@ function player_handler:update(dt) --eye offsets and ads_location if self.control_bools.ads and (self.ads_location<1) then --if aiming, then increase ADS location - self.ads_location = math.clamp(self.ads_location + (dt/self.gun.properties.aim_time), 0, 1) + self.ads_location = math.clamp(self.ads_location + (dt/self.gun.properties.ads.aim_time), 0, 1) elseif (not self.control_bools.ads) and self.ads_location>0 then local divisor = .2 if self.gun then - divisor = self.gun.properties.aim_time/self.gun.consts.AIM_OUT_AIM_IN_SPEED_RATIO + divisor = self.gun.properties.ads.aim_time/self.gun.consts.AIM_OUT_AIM_IN_SPEED_RATIO end self.ads_location = math.clamp(self.ads_location - (dt/divisor), 0, 1) end diff --git a/classes/Player_model_handler.lua b/classes/Player_model_handler.lua index 1fd265c..e5ddcef 100644 --- a/classes/Player_model_handler.lua +++ b/classes/Player_model_handler.lua @@ -40,14 +40,14 @@ function player_model:update() local first, second = player:get_eye_offset() local eye_pos = vector.new(0, handler:get_properties().eye_height*10, 0)+first + if handler.control_bools.ads then + eye_pos.x = handler.horizontal_offset*10 + end player:set_bone_position("guns3d_hipfire_bone", self.offsets.arm.rltv_right, vector.new(-(pitch*gun.consts.HIP_PLAYER_GUN_ROT_RATIO), 180-player_axial_offset.y, 0)) player:set_bone_position("guns3d_reticle_bone", eye_pos, vector.new(combined.x, 180-combined.y, 0)) player:set_bone_position("guns3d_head", self.offsets.head, {x=pitch,z=0,y=0}) - local dir = gun:get_player_axial_dir() - dir = vector.normalize(dir) - local rot = vector.dir_to_rotation(dir)*180/math.pi - minetest.chat_send_all(dump(rot)) + local rot = vector.dir_to_rotation(gun.paxial_dir)*180/math.pi player:set_bone_position("guns3d_aiming_bone", eye_pos, {x=rot.x,y=-rot.y+180,z=0}) end function player_model:prepare_deletion() diff --git a/classes/Sprite_scope.lua b/classes/Sprite_scope.lua index 7b9c1e2..d22e5cd 100644 --- a/classes/Sprite_scope.lua +++ b/classes/Sprite_scope.lua @@ -2,17 +2,18 @@ Sprite_scope = Instantiatable_class:inherit({ images = { fore = { texture = "blank.png", - scale = {x=11,y=11}, + scale = {x=13,y=13}, movement_multiplier = 1, }, back = { texture = "blank.png", scale = {x=10,y=10}, movement_multiplier = -1, + opacity_delay = 2, }, reticle = { texture = "gun_mrkr.png", - scale = {x=1,y=1}, + scale = {x=.5,y=.5}, movement_multiplier = 1, misalignment_opacity_threshold_angle = 3, misalignment_opacity_maximum_angle = 8, @@ -30,12 +31,6 @@ Sprite_scope = Instantiatable_class:inherit({ if def.images then def.images = table.fill(new_images, def.images) end - def.elements.reticle = def.player:hud_add{ - hud_elem_type = "image", - position = {x=.5,y=.5}, - scale = def.images.reticle.scale, - text = "blank.png", - } def.elements.fore = def.player:hud_add{ hud_elem_type = "image", position = {x=.5,y=.5}, @@ -48,6 +43,12 @@ Sprite_scope = Instantiatable_class:inherit({ scale = def.images.back.scale, text = "blank.png", } + def.elements.reticle = def.player:hud_add{ + hud_elem_type = "image", + position = {x=.5,y=.5}, + scale = def.images.reticle.scale, + text = "blank.png", + } end end }) @@ -60,12 +61,15 @@ function Sprite_scope:update() if handler.ads_location ~= 1 then dir = dir + (self.gun.properties.ads.offset+vector.new(self.gun.properties.ads.horizontal_offset,0,0))*0 end - local v = Point_to_hud(dir, 80, ratio) - self.player:hud_change(self.elements.reticle, "position", {x=(v.x*self.images.reticle.movement_multiplier)+.5, y=(v.y*self.images.reticle.movement_multiplier)+.5}) - self.player:hud_change(self.elements.fore, "position", {x=(v.x*self.images.fore.movement_multiplier)+.5, y=(v.y*self.images.fore.movement_multiplier)+.5}) - self.player:hud_change(self.elements.back, "position", {x=(v.x*self.images.back.movement_multiplier)+.5, y=(v.y*self.images.back.movement_multiplier)+.5}) + local fov = self.player:get_fov() + local v1 = Point_to_hud(dir, fov, ratio) + local v2 = Point_to_hud(self.gun.local_paxial_dir, fov, ratio) + self.player:hud_change(self.elements.fore, "position", {x=(v1.x*self.images.fore.movement_multiplier)+.5, y=(v1.y*self.images.fore.movement_multiplier)+.5}) + self.player:hud_change(self.elements.back, "position", {x=(v2.x*self.images.back.movement_multiplier)+.5, y=(v2.y*self.images.back.movement_multiplier)+.5}) + self.player:hud_change(self.elements.reticle, "position", {x=(v1.x*self.images.reticle.movement_multiplier)+.5, y=(v1.y*self.images.reticle.movement_multiplier)+.5}) --update textures end + local angle =math.sqrt(self.gun.offsets.total_offset_rotation.gun_axial.x^2+self.gun.offsets.total_offset_rotation.gun_axial.y^2) for i, v in pairs(self.elements) do local def = self.images[i] local tex = def.texture @@ -73,12 +77,11 @@ function Sprite_scope:update() --25 possible images, instead of 255. local factor = 1 if def.misalignment_opacity_threshold_angle then - local angle = math.sqrt(self.gun.offsets.total_offset_rotation.gun_axial.x^2+self.gun.offsets.total_offset_rotation.gun_axial.y^2) if def.misalignment_opacity_threshold_angle < angle then factor = (factor - ((angle-def.misalignment_opacity_threshold_angle)/def.misalignment_opacity_maximum_angle)) end end - self.player:hud_change(v, "text", tex.."^[opacity:"..tostring(math.ceil((25.5*handler.ads_location*factor))*10)) + self.player:hud_change(v, "text", tex.."^[opacity:"..tostring(math.ceil((25.5*handler.ads_location))*10)) end end function Sprite_scope:prepare_deletion() diff --git a/default_controls.lua b/default_controls.lua new file mode 100644 index 0000000..39252f5 --- /dev/null +++ b/default_controls.lua @@ -0,0 +1,176 @@ +Guns4d.default_controls = { + controls = {} +} +Guns4d.default_controls.aim = { + conditions = {"RMB"}, + loop = false, + timer = 0, + func = function(active, interrupted, data, busy_list, handler) + if active then + handler.control_bools.ads = not handler.control_bools.ads + end + end +} +Guns4d.default_controls.fire = { + conditions = {"LMB"}, + loop = true, + timer = 0, + func = function(active, interrupted, data, busy_list, handler) + if not handler.control_handler.busy_list.on_use then + handler.gun:attempt_fire() + end + end +} +Guns4d.default_controls.reload = { + conditions = {"zoom"}, + loop = false, + timer = 0, --1 so we have a call to initialize the timer. + func = function(active, interrupted, data, busy_list, handler) + local gun = handler.gun + local ammo_handler = gun.ammo_handler + local props = gun.properties + if active then + if not data.state then + data.state = 0 + end + local this_state = props.reload[data.state] + local next_state_index = data.state + + if next_state_index == 0 then + + next_state_index = next_state_index + 1 + + elseif type(this_state.type) == "function" then + + this_state.type(true, handler, gun) + + elseif this_state.type == "unload" then + local pause = false + local next = props.reload[next_state_index+1] + if (next.type=="load_fractional" or next.type=="load") and (not ammo_handler:inventory_has_ammo()) then + pause=true + 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 + next_state_index = next_state_index +1 + + 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] + + --check that the next states are actually valid, if not, skip them + local valid_state = false + while not valid_state do + local state_changed = false + next_state = props.reload[next_state_index] + if next_state then + local state_changed = false + + if next_state.type == "unload" then + + if props.ammo.magazine_only and (ammo_handler.ammo.loaded_mag == "empty") then + state_changed = true + end + + elseif next_state.type == "unload_fractional" then + + if not ammo_handler.ammo.total_bullets > 0 then + state_changed = true + end + + elseif next_state.type == "load" then + if props.ammo.magazine_only then + if not ammo_handler:can_load_magazine() then + state_changed = true + end + else + + if not ammo_handler:can_load_flat() then + state_changed = true + end + end + end + if not state_changed then + valid_state=true + else + next_state_index = next_state_index + 1 + next_state = props.reload[next_state_index] + end + else + data.state = 0 + data.timer = 0.5 + data.held = true + return + end + end + --check if we're at cycle end + if next_state == nil then + data.state = 0 + data.timer = 0 + data.held = true + return + else + data.state = next_state_index + data.timer = next_state.time + data.held = false + local anim = next_state.anim + if type(next_state.anim) == "string" then + anim = props.animations[next_state.anim] + end + gun:set_animation(anim, next_state.time) + end + elseif interrupted then + local this_state = props.reload[data.state] + if this_state and (this_state.type == "unload") and (this_state.interupt == "to_ground") then + --true indicates to_ground (meaning they will be removed) + if 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 + gun:clear_animation() + data.state = 0 + end + end +} +Guns4d.default_controls.on_use = function(itemstack, handler, pointed_thing) + handler.gun:attempt_fire() + handler.control_handler.busy_list.on_use = true +end \ No newline at end of file diff --git a/docs/gun properties.txt b/docs/gun properties.txt index 2c51f12..49ddd57 100644 --- a/docs/gun properties.txt +++ b/docs/gun properties.txt @@ -12,12 +12,9 @@ ----------------------------------AMMUNITION-------------------------------------- - (praise the lord and pass the ammunition can't afford to be a politician) - --note: internally clips are the same as a magazine (sort of) - - --Ammo_handler derived class to handle ammunition, can be changed for modularity- not reccomended. + --Ammo_handler derived class to handle ammunition, can be changed for modularity- not reccomended, it should provide everything you need. ammo_handler = Ammo_handler --a table containing info about the magazines this gun may use. @@ -29,7 +26,7 @@ comes_with = "modname:itemstring", --a list of magazines that this gun takes. These must be registered via Guns4d.ammo.register_magazine() - --note that if the magazine contains unaccepted bullets (properties.accepted_bullets), the magazine won't be accepted. + --note that if the magazine contains unaccepted bullets (properties.ammo.accepted_bullets), the magazine won't be accepted. accepted_mags = { "modname:itemstring", "modname:itemstring" @@ -37,7 +34,7 @@ } --list of bullets this gun accepts. These must be registered via Guns4d.ammo.register_bullet() - accepted_bullets = { + ammo.accepted_bullets = { "modname:itemstring", "modname:itemstring" } diff --git a/docs/planned_and_potential_features.txt b/docs/planned_and_potential_features.txt index 235ee5e..4b43ee2 100644 --- a/docs/planned_and_potential_features.txt +++ b/docs/planned_and_potential_features.txt @@ -24,20 +24,24 @@ gun features fix shooting through walls be pressing against them correct player look-down gun leaning - cap aimed look angles <80 - >-80 (gun bugs out) add hip and aim bone offsets (this is for guns or modifiers that add stuff like "laser sights" or hud that simulates holographic sights, could be cool for futuristic type shooters) movement rotation + sprite scopes: fix player aim delay being present with sprite scopes bullets bullet class system add blunt force properties on_hitnode function callback +compatibility + add consts for player inv use in Ammo_handler + make monoid for player properties, player look offsets (especially needed for vehicle mods) + optimization - !!hardcore optimization of get_dir() type functions required- super inefficient. + hardcore optimization of get_dir() type functions required- super inefficient. + optimization of sprite scopes needed, buffering for relative dirs needed for when inactive. Potentially remove the global table for them (if possible.) auxillary (beta+/never) player hitboxes - server to client lag prediction possible user CSM for lag prediction bullet drop (maybe bullet wind?) @@ -45,4 +49,6 @@ auxillary (beta+/never) inverse kinematics stamina a better system for reloading magazine (something like a inventory that fractionally reloads magazines so it takes time.) - create a non-player gun API \ No newline at end of file + create a non-player gun API + bullets, mags: add meta data support? (probably not) + jamming \ No newline at end of file diff --git a/gun_api.lua b/gun_api.lua deleted file mode 100644 index a62db6e..0000000 --- a/gun_api.lua +++ /dev/null @@ -1,80 +0,0 @@ -local Vec = vector -local default_def = { - --name = - --itemstring = - --textures = {} - --mesh = (media) - hip = { - offset = Vec.new(0,0,.2), - }, - ads = { - offset = Vec.new(0,0,.1), - horizontal_offset = .1, - }, - recoil = { - velocity_correction_factor = { - gun_axial = 2, - player_axial = 2, - }, - target_correction_factor = { --angular correction rate per second: time_since_fire*target_correction_factor - gun_axial = 30, - player_axial = 1, - }, - target_correction_max_rate = { --the cap for time_since_fire*target_correction_factor - gun_axial = 100, - player_axial = 6, - }, - angular_velocity_max = { - gun_axial = 0, - player_axial = 0, - }, - angular_velocity = { - gun_axial = {x=.1, y=.1}, - player_axial = {x=.1, y=.1}, - }, - }, - firerateRPM = 600, - consts = { - HIP_PLAYER_GUN_ROT_RATIO = .6 - }, - aim_time = .5 -} -local valid_ctrls = { - up=true, - down=true, - left=true, - right=true, - jump=true, - aux1=true, - sneak=true, - dig=true, - place=true, - LMB=true, - RMB=true, - zoom=true, -} -function Guns4d.register_gun_default(def) - assert(def, "no definition table provided") - assert(def.name, "no name provided when registering gun") - assert(def.itemstring, "no itemstring provided when registering gun") - local new_def = {} - new_def.consts = def.consts - new_def.name = def.name; def.name = nil - new_def.itemstring = def.itemstring; def.itemstring = nil - new_def.properties = table.fill(default_def, def) - --validate controls - if new_def.properties.controls then - for i, control in pairs(new_def.properties.controls) 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 - if not valid_ctrls[condition] then - assert(false, "invalid key: '"..condition.."'") - end - end - end - end - end - --gun is registered within this function - Guns4d.gun:inherit(new_def) -end \ No newline at end of file diff --git a/init.lua b/init.lua index fbc585e..197072c 100644 --- a/init.lua +++ b/init.lua @@ -5,7 +5,7 @@ Guns4d = { local path = minetest.get_modpath("guns4d") dofile(path.."/misc_helpers.lua") dofile(path.."/visual_effects.lua") -dofile(path.."/gun_api.lua") +dofile(path.."/default_controls.lua") dofile(path.."/block_values.lua") dofile(path.."/register_ammo.lua") path = path .. "/classes" diff --git a/misc_helpers.lua b/misc_helpers.lua index b7d5bdf..0ce256e 100644 --- a/misc_helpers.lua +++ b/misc_helpers.lua @@ -152,7 +152,7 @@ function table.tostring(tbl, shallow, tables, depth) end ---replace elements in tbl with elements in replacement, but preserve the rest +--replace fields (and fill sub-tables) in `tbl` with elements in `replacement`. Recursively iterates all sub-tables. use property __overfill=true for subtables that don't want to be overfilled. function table.fill(tbl, replacement, preserve_reference, indexed_tables) if not indexed_tables then indexed_tables = {} end --store tables to prevent circular referencing local new_table = tbl @@ -161,13 +161,23 @@ function table.fill(tbl, replacement, preserve_reference, indexed_tables) end for i, v in pairs(replacement) do if new_table[i] then - if type(v) == "table" and type(replacement[i]) == "table" then - if not indexed_tables[v] then - indexed_tables[v] = true - new_table[i] = table.fill(tbl[i], replacement[i], false, indexed_tables) + local replacement_type = type(v) + if replacement_type == "table" then + if type(new_table[i]) == "table" then + if not indexed_tables[v] then + if not new_table[i].__overfill then + indexed_tables[v] = true + new_table[i] = table.fill(tbl[i], replacement[i], false, indexed_tables) + else --if overfill is present, we don't want to preserve the old table. + new_table[i] = table.deep_copy(replacement[i]) + end + end + elseif not replacement[i].__no_copy then + new_table[i] = table.deep_copy(replacement[i]) + else + new_table[i] = replacement[i] end - elseif type(replacement[i]) == "table" then - new_table[i] = table.deep_copy(replacement[i]) + new_table[i].__overfill = nil else new_table[i] = replacement[i] end diff --git a/register_ammo.lua b/register_ammo.lua index 430e142..c34d334 100644 --- a/register_ammo.lua +++ b/register_ammo.lua @@ -21,6 +21,8 @@ Guns4d.ammo = { } } local max_wear = 65535 +function Guns4d.ammo.on_hit_player(bullet, force_mmRHA) +end function Guns4d.ammo.register_bullet(def) assert(def.itemstring, "no itemstring") assert(minetest.registered_items[def.itemstring], "no item '"..def.itemstring.."' found. Must be a registered item (check dependencies?)") @@ -35,6 +37,7 @@ function Guns4d.ammo.initialize_mag_data(itemstack, meta) return itemstack end function Guns4d.ammo.update_mag(def, itemstack, meta) + def = def or Guns4d.ammo.registered_magazines[itemstack:get_name()] meta = meta or itemstack:get_meta() local bullets = minetest.deserialize(meta:get_string("guns4d_loaded_bullets")) local count = 0 @@ -43,7 +46,8 @@ function Guns4d.ammo.update_mag(def, itemstack, meta) current_bullet = i count = count + v end - itemstack:set_wear((max_wear-(max_wear*count/def.capacity))+1) + local new_wear = max_wear-(max_wear*count/def.capacity) + itemstack:set_wear(math.clamp(new_wear, 1, max_wear-1)) meta:set_int("guns4d_total_bullets", count) meta:set_string("guns4d_next_bullet", current_bullet) return itemstack @@ -88,7 +92,6 @@ function Guns4d.ammo.register_magazine(def) Guns4d.ammo.initialize_mag_data(v) end end - print(num_mags) if num_mags > 0 then if itemstack:get_name()=="" then for i, v in pairs(craft_inv:get_list("craft")) do @@ -100,7 +103,6 @@ function Guns4d.ammo.register_magazine(def) return end if (name~="") and (not (name == def.itemstring)) and (not def.accepted_bullets_set[name]) then - print("name:", dump(def.accepted_bullets_set)) return end end @@ -124,7 +126,6 @@ function Guns4d.ammo.register_magazine(def) end if not def.accepted_bullets_set[name] then if (name ~= "") and (name~=def.itemstring) then - print("return", "'"..name.."'") return end end @@ -161,7 +162,6 @@ function Guns4d.ammo.register_magazine(def) local meta = new_stack:get_meta() meta:set_string("guns4d_loaded_bullets", minetest.serialize(new_ammo_table)) new_stack = Guns4d.ammo.update_mag(def, new_stack, meta) - --print(new_stack:get_string()) return new_stack end end) diff --git a/register_bullet.lua b/register_bullet.lua deleted file mode 100644 index e69de29..0000000 diff --git a/textures/scope.png b/textures/scope.png new file mode 100644 index 0000000..a3546ff Binary files /dev/null and b/textures/scope.png differ diff --git a/visual_effects.lua b/visual_effects.lua index 873eddd..ccbec93 100644 --- a/visual_effects.lua +++ b/visual_effects.lua @@ -6,8 +6,8 @@ function Guns4d.muzzle_flash(self) end local dir, offset_pos = self.dir, self:get_pos(self.properties.flash_offset) offset_pos=offset_pos+self.player:get_pos() - local min = vector.rotate(vector.new(-2, -2, -.3), vector.dir_to_rotation(dir)) - local max = vector.rotate(vector.new(2, 2, .3), vector.dir_to_rotation(dir)) + local min = vector.rotate(vector.new(-1, -1, -.15), {x=0,y=self.offsets.player_rotation.y,z=0}) + local max = vector.rotate(vector.new(1, 1, .15), {x=0,y=self.offsets.player_rotation.y,z=0}) minetest.add_particlespawner({ exptime = .18, time = .1, @@ -16,7 +16,7 @@ function Guns4d.muzzle_flash(self) pos = self.properties.flash_offset, radius = .04, glow = 3.5, - vel = {min=vector.new(-1, -1, -.15), max=vector.new(1, 1, .15), bias=0}, + vel = {min=min, max=max, bias=0}, texpool = { { name = "smoke.png", alpha_tween = {.25, 0}, scale = 2, blend = "alpha", animation = {