commit 5bd53dc6a2c73ed4a90dd44606bbf06107fe5827 Author: FatalErr42O <58855799+FatalError42O@users.noreply.github.com> Date: Sun Jun 11 23:34:28 2023 -0700 first commit diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..45a66c9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "Lua.diagnostics.globals": [ + "minetest", + "vector", + "dump", + "player_api", + "ItemStack" + ] +} \ No newline at end of file diff --git a/block_values.lua b/block_values.lua new file mode 100644 index 0000000..c537554 --- /dev/null +++ b/block_values.lua @@ -0,0 +1,75 @@ +Guns4d.node_properties = {} +--{["default:gravel"] = {rha=2, random_deviation=1, behavior="normal"}, . . . } +--behavior types: +--normal, bullets hit and penetrate +--breaks, bullets break it but still applies RHA/randomness values (etc) +--ignore, bullets pass through + +--unimplemented + +--liquid, bullets hit and penetrate, but effects are different +--damage, bullets hit and penetrate, but replace with "replace = _" + +--mmRHA of wood .05 (mostly arbitrary) +--{choppy = 2, oddly_breakable_by_hand = 2, flammable = 2, wood = 1} + +--this is really the best way I could think of to do this +--in a perfect world you could perfectly balance each node, but a aproximation will have to do +--luckily its still an option, if you are literally out of your fucking mind. +minetest.register_on_mods_loaded(function() + for i, v in pairs(minetest.registered_nodes) do + local groups = v.groups + local RHA = 1 + local random_deviation = 1 + local behavior_type = "normal" + if groups.wood then + RHA = RHA*groups.wood*.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)) + end + if groups.flora or groups.grass then + RHA = 0 + random_deviation = 0 + behavior_type = "ignore" + end + if groups.leaves then + RHA = .0001 + random_deviation = .005 + end + if groups.stone then + RHA = groups.stone + random_deviation = .5 + end + if groups.cracky then + RHA = RHA*groups.cracky + random_deviation = random_deviation*(groups.cracky*.5) + end + if groups.crumbly then + RHA = RHA/groups.crumbly + end + if groups.soil then + RHA = RHA*(groups.soil*2) + end + if groups.sand then + RHA = RHA*(groups.sand*2) + end + if groups.liquid then + --behavior type here + RHA = .5 + random_deviation = .1 + end + Guns4d.node_properties[i] = {mmRHA=RHA*1000, random_deviation=random_deviation, behavior=behavior_type} + end +end) +function Guns4d.override_node_propertoes(node, table) + --TODO: check if node is valid + assert(type(table.mmRHA)=="number", "no mmRHA value provided in override") + assert(type(table.behavior)=="string", "no behavior type provided in override") + assert(type(table.behavior)=="number", "no random_deviation value provided in override") + Guns4d.node_properties[node] = table +end \ No newline at end of file diff --git a/classes/Bullet_ray.lua b/classes/Bullet_ray.lua new file mode 100644 index 0000000..b9f858b --- /dev/null +++ b/classes/Bullet_ray.lua @@ -0,0 +1,184 @@ +local ray = { + history = {}, + state = "free", + --pos = pos, + last_node = "", + normal = vector.new(), + --last_dir + --exit_direction = dir, + --range_left = def.bullet.range, + --force_mmRHA = def.bullet.penetration_RHA + ITERATION_DISTANCE = .3, + damage = 0 +} +function ray:validate_location() +end + +function ray:record_state() + table.insert(self.history, { + state = self.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() + 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) + for hit in cast do + --we can't solidly predict all nodes, so ignore them as the distance will be solved regardless. If node name is different then + if hit.type == "node" and (vector.equals(hit.under, self.last_pointed.under) or not minetest.registered_nodes[self.last_node_name].node_box) then + pointed = hit + break + 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 + end +end +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 edge + local next_state = self.state + if self.state == "transverse" then + edge = self:transverse_end_point() + if edge then + end_pos = edge + next_state = "free" + else + end_pos = self.pos+(self.dir*self.ITERATION_DISTANCE) + end + end + local continue = true + 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 + 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 + --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 + next_state = "free" + else + next_state = "transverse" + end + break + end + 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 self.over_penetrate then + pointed = hit + break + else + pointed = hit + continue = false + break + end + end + end + end + 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) + else -- transverse + next_penetration_val = self.force_mmRHA-(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) + else --free + continue = false + next_penetration_val = self.force_mmRHA-(self.range*self.dropoff_mmRHA) + end + end + + --set "last" values. + return pointed, next_penetration_val, next_state, end_pos, continue +end +function ray:apply_damage(obj) + local damage = math.floor((self.damage*(self.force_mmRHA/self.init_force_mmRHA))+1) + obj:punch(self.player, nil, {damage_groups = {fleshy = damage, penetration_mmRHA=self.force_mmRHA}}, self.dir) +end +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 +---@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:apply_damage(pointed.ref) + end + end + table.insert(self.history, { + pos = self.pos, + force_mmRHA = self.force_mmRHA, + state = self.state, + last_node = self.last_node_name, + normal = self.normal, + }) + if continue and self.range > 0 and self.force_mmRHA > 0 then + self:iterate(true) + end + if not initialized then + for i, v in pairs(self.history) do + --[[local hud = self.player:hud_add({ + hud_elem_type = "waypoint", + text = "mmRHA:"..tostring(math.floor(v.force_mmRHA or 0)).." ", + number = 255255255, + precision = 1, + world_pos = v.pos, + scale = {x=1, y=1}, + alignment = {x=0,y=0}, + offset = {x=0,y=0}, + }) + minetest.after(40, function(hud) + self.player:hud_remove(hud) + end, hud)]] + end + end +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") + def.init_force_mmRHA = def.force_mmRHA + def.dir = vector.new(def.dir) + def.pos = vector.new(def.pos) + def.history = {} + 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 new file mode 100644 index 0000000..94e0543 --- /dev/null +++ b/classes/Control_handler.lua @@ -0,0 +1,82 @@ +Guns4d.control_handler = { + --[[example: + controls = { + reload = { + conditions = { --the list of controls (see lua_api.txt) to call + "shift", + "zoom" + }, + timer = .3, + call_before_timer = false, + loop = false, + func=function(active, interrupted, data, busy_controls) + data = { + + } + } + } + ]] +} +local controls = Guns4d.control_handler +--[[-modify controls (future implementation if needed) +function controls.modify() +end]] +function controls:update(dt) + self.player_pressed = self.player:get_player_control() + local pressed = self.player_pressed + local call_queue = {} --so I need to have a "call" queue so I can tell the functions the names of other active controls (busy_list) + local busy_list = {} --list of controls that have their conditions met + for i, control in pairs(self.controls) do + local def = control + local data = control.data + local conditions_met = true + for _, key in pairs(control.conditions) do + if not pressed[key] then conditions_met = false break end + end + if not conditions_met then + busy_list[i] = true + data.held = false + --detect interrupts + if data.timer ~= def.timer then + table.insert(call_queue, {control=def, active=false, interrupt=true, data=data}) + data.timer = def.timer + end + else + data.timer = data.timer - dt + --when time is over, if it wasnt held (or loop is active) then reset and call the function. + 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 + table.insert(call_queue, {control=def, active=false, interrupt=false, data=data}) + end + end + end + local count = 0 + for i, v in pairs(busy_list) do + count = count + 1 + end + if count == 0 then busy_list = nil end --so funcs can quickly deduce if they can call + for i, tbl in pairs(call_queue) do + tbl.control.func(tbl.active, tbl.interrupt, tbl.data, busy_list, Guns4d.players[self.player:get_player_name()].handler) + 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.player, "no player provided") + def.controls = table.deep_copy(def.controls) + for i, control in pairs(def.controls) do + control.timer = control.timer or 0 + control.data = { + timer = control.timer, + held = false + } + end + table.sort(def.controls, function(a,b) + return #a.conditions > #b.conditions + end) + end +end +Guns4d.control_handler = Instantiatable_class:inherit(Guns4d.control_handler) \ No newline at end of file diff --git a/classes/Gun.lua b/classes/Gun.lua new file mode 100644 index 0000000..038c754 --- /dev/null +++ b/classes/Gun.lua @@ -0,0 +1,473 @@ +local Vec = vector +local gun = { + --itemstack = Itemstack + --gun_entity = ObjRef + name = "__template__", + registered = {}, + properties = { + hip = { + offset = Vec.new(), + }, + ads = { + offset = Vec.new(), + horizontal_offset = 0, + }, + recoil = { + velocity_correction_factor = { + gun_axial = 1, + player_axial = 1, + }, + target_correction_factor = { --angular correction rate per second: time_since_fire*target_correction_factor + gun_axial = 1, + player_axial = 1, + }, + angular_velocity_max = { + gun_axial = 2, + player_axial = 2, + }, + angular_velocity = { + gun_axial = {x=0, y=0}, + player_axial = {x=0, y=0}, + }, + angular_velocity_bias = { + gun_axial = {x=1, y=0}, + player_axial = {x=1, y=0}, + }, + target_correction_max_rate = { --the cap for time_since_fire*target_correction_factor + gun_axial = 10000, + player_axial = 10000, + }, + }, + sway = { + max_angle = { + gun_axial = 0, + player_axial = 0, + }, + angular_velocity = { + gun_axial = 0, + player_axial = 0, + }, + }, + walking_offset = { + gun_axial = {x=.2, y=-.2}, + player_axial = {x=1,y=1}, + }, + flash_offset = Vec.new(), + aim_time = 1, + firerateRPM = 10000, + controls = {} + }, + transforms = { + pos = Vec.new(), + player_rotation = Vec.new(), + dir = Vec.new(), + --I'll need all three of them, do some precalculation. + total_offset_rotation = { + gun_axial = Vec.new(), + player_axial = Vec.new(), + }, + recoil = { + gun_axial = Vec.new(), + player_axial = Vec.new(), + }, + sway = { + gun_axial = Vec.new(), + player_axial = Vec.new(), + }, + walking = { + gun_axial = Vec.new(), + player_axial = Vec.new(), + }, + breathing = { + gun_axial = 1, + player_axial = 1, + } + }, + velocities = { + recoil = { + gun_axial = Vec.new(), + player_axial = Vec.new(), + }, + sway = { + gun_axial = Vec.new(), + player_axial = Vec.new(), + }, + }, + particle_spawners = { + --muzzle_smoke + }, + --magic number BEGONE + consts = { + HIP_PLAYER_GUN_ROT_RATIO = .75, + HIPFIRE_BONE = "guns3d_hipfire_bone", + AIMING_BONE = "guns3d_aiming_bone", + HAS_RECOIL = true, + HAS_BREATHING = true, + HAS_SWAY = true, + HAS_WAG = true, + }, + walking_tick = 0, + time_since_last_fire = 0, + time_since_creation = 0, + rechamber_time = 0, + muzzle_flash = Guns4d.muzzle_flash +} +function gun:fire() + if self.rechamber_time <= 0 then + local dir = self:get_dir() + local pos = self:get_pos() + Guns4d.bullet_ray:new({ + player = self.player, + pos = pos, + dir = dir, + range = 100, + gun = self, + force_mmRHA = 1, + dropoff_mmRHA = 0 + }) + self:recoil() + self:muzzle_flash() + self.rechamber_time = 60/self.properties.firerateRPM + end +end +function gun:recoil() + for axis, recoil in pairs(self.velocities.recoil) do + for _, i in pairs({"x","y"}) do + print(i,self.properties.recoil.angular_velocity_bias[axis][i]) + recoil[i] = recoil[i] + (self.properties.recoil.angular_velocity[axis][i]*math.rand_sign((self.properties.recoil.angular_velocity_bias[axis][i]/2)+.5)) + end + end + self.time_since_last_fire = 0 +end +function gun:get_dir(added_pos) + local offset + if added_pos then + offset = true + end + added_pos = Vec.new(added_pos) + local player = self.player + local player_rotation = Vec.new(self.transforms.player_rotation.x, self.transforms.player_rotation.y, 0) + local rotation = self.transforms.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+player_rotation.x)*math.pi/180), z=0})) + dir = Vec.rotate(dir, {y=((rotation.gun_axial.y+rotation.player_axial.y+player_rotation.y)*math.pi/180), x=0, z=0}) + local hud_pos = dir+self:get_pos() + if not false then + 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) + end + return dir +end +function gun:get_pos(added_pos) + added_pos = Vec.new(added_pos) + local player = self.player + local handler = self.handler + local bone_location = Vec.new(handler.model_handler.offsets.arm.right)/10 + local gun_offset = Vec.new(self.properties.hip.offset) + local player_rotation = Vec.new(self.transforms.player_rotation.x, self.transforms.player_rotation.y, 0) + if handler.controls.ads then + gun_offset = self.properties.ads.offset + bone_location = Vec.new(0, handler:get_properties().eye_height, 0)+player:get_eye_offset()/10 + else + --minetest is really wacky. + bone_location = Vec.new(-bone_location.x, bone_location.y, bone_location.z) + player_rotation.x = self.transforms.player_rotation.x*self.consts.HIP_PLAYER_GUN_ROT_RATIO + end + gun_offset = gun_offset+added_pos + --dir needs to be rotated twice seperately to avoid weirdness + local rotation = self.transforms.total_offset_rotation + local bone_pos = Vec.rotate(bone_location, {x=0, y=player_rotation.y*math.pi/180, z=0}) + local gun_offset = Vec.rotate(Vec.rotate(gun_offset, {x=(rotation.player_axial.x+player_rotation.x)*math.pi/180,y=0,z=0}), {x=0,y=(rotation.player_axial.y+player_rotation.y)*math.pi/180,z=0}) + --[[local hud_pos = bone_pos+gun_offset+handler:get_pos() + if not false then + 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) + end]] + --world pos, position of bone, offset of gun from bone (with added_pos) + return bone_pos+gun_offset+handler:get_pos(), bone_pos, gun_offset +end +function gun:add_entity() + assert(self.instance, "attempt to call object method on a class") + self.entity = minetest.add_entity(self.player:get_pos(), self.name.."_visual") + local obj = self.entity:get_luaentity() + obj.parent_player = self.player + obj:on_step() +end +function gun:has_entity() + assert(self.instance, "attempt to call object method on a class") + if not self.entity then return false end + if not self.entity:get_pos() then return false end + return true +end +function gun:update(dt) + assert(self.instance, "attempt to call object method on a class") + if not self:has_entity() then self:add_entity() end + self.dir = self:get_dir() + self.pos = self:get_pos() + local handler = self.handler + local look_rotation = {x=handler.look_rotation.x,y=handler.look_rotation.y} + local total_rot = self.transforms.total_offset_rotation + local player_rot = self.transforms.player_rotation + local constant = 1.4 + + --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 + else + player_rot.x = look_rotation.x + end + if self.rechamber_time > 0 then + self.rechamber_time = self.rechamber_time - dt + else + self.rechamber_time = 0 + end + self.time_since_creation = self.time_since_creation + dt + self.time_since_last_fire = self.time_since_last_fire + dt + if self.consts.HAS_SWAY then self:update_sway(dt) end + if self.consts.HAS_RECOIL then self:update_recoil(dt) end + if self.consts.HAS_BREATHING then self:update_breathing(dt) end + if self.consts.HAS_WAG then self:update_wag(dt) end + player_rot.y = -handler.look_rotation.y + local offsets = self.transforms + total_rot.player_axial = offsets.recoil.player_axial + offsets.walking.player_axial + offsets.sway.player_axial + {x=offsets.breathing.player_axial,y=0,z=0} + {x=0,y=0,z=0} + total_rot.gun_axial = offsets.recoil.gun_axial + offsets.walking.gun_axial + offsets.sway.gun_axial + if self.handler.controls.ads then + if not self.useless_hud then + self.useless_hud = {} + self.useless_hud.reticle = self.player:hud_add{ + hud_elem_type = "image", + position = {x=.5,y=.5}, + scale = {x=1.5,y=1.5}, + text = "gun_mrkr.png", + } + self.useless_hud.fore = self.player:hud_add{ + hud_elem_type = "image", + position = {x=.5,y=.5}, + scale = {x=10,y=10}, + text = "scope_fore.png", + } + self.useless_hud.back = self.player:hud_add{ + hud_elem_type = "image", + position = {x=.5,y=.5}, + scale = {x=10,y=10}, + text = "scope_back.png", + } + end + local wininfo = minetest.get_player_window_information(self.player:get_player_name()) + if wininfo then + local rot = total_rot.player_axial+total_rot.gun_axial + local ratio = wininfo.size.x/wininfo.size.y + local offset_y = ((-rot.y/(80*2))+.5) + local offset_x = (((-rot.x*ratio)/(80*2))+.5) + self.player:hud_change(self.useless_hud.reticle, "position", {x=offset_y, y=offset_x}) + self.player:hud_change(self.useless_hud.fore, "position", {x=offset_y, y=offset_x}) + self.player:hud_change(self.useless_hud.back, "position", {x=((4*total_rot.player_axial.y/(80*2))+.5), y=(((4*total_rot.player_axial.x*ratio)/(80*2))+.5)}) + end + elseif self.useless_hud then + for i, v in pairs(self.useless_hud) do + self.player:hud_remove(v) + end + self.useless_hud = nil + end +end +function gun:update_wag(dt) + local handler = self.handler + if handler.walking then + self.walking_tick = self.walking_tick + (dt*Vec.length(self.player:get_velocity())) + else + self.walking_tick = 0 + end + local walking_offset = self.transforms.walking + for _, i in pairs({"x","y"}) do + for axis, _ in pairs(walking_offset) do + if handler.walking then + local time = self.walking_tick + local multiplier = 1 + if i == "x" then + multiplier = 2 + end + print(dump(self.properties.walking_offset[axis])) + walking_offset[axis][i] = math.sin((time/1.6)*math.pi*multiplier)*self.properties.walking_offset[axis][i] + else + local old_value = walking_offset[axis][i] + if (math.abs(walking_offset[axis][i]) > .5 and axis=="player_axial") or (math.abs(walking_offset[axis][i]) > .6 and axis=="gun_axial") then + local multiplier = (walking_offset[axis][i]/math.abs(walking_offset[axis][i])) + walking_offset[axis][i] = walking_offset[axis][i]-(dt*2*multiplier) + elseif axis == "gun_axial" then + walking_offset[axis][i] = 0 + end + if math.abs(walking_offset[axis][i]) > math.abs(old_value) then + walking_offset[axis][i] = 0 + end + end + end + end +end +function gun:update_recoil(dt) + for axis, _ in pairs(self.transforms.recoil) do + for _, i in pairs({"x","y"}) do + local recoil = self.transforms.recoil[axis][i] + local recoil_vel = math.clamp(self.velocities.recoil[axis][i],-self.properties.recoil.angular_velocity_max[axis],self.properties.recoil.angular_velocity_max[axis]) + local old_recoil_vel = recoil_vel + recoil = recoil + recoil_vel + if math.abs(recoil_vel) > 0.01 then + --look, I know this doesn't really make sense, but this is the best I can do atm. I've looked for better and mroe intuitive methods, I cannot find them. + --8-(8*(1-(8/100)) + --recoil_vel = recoil_vel-((recoil_vel-(recoil_vel/(1+self.properties.recoil.velocity_correction_factor[axis])))*dt*10) + recoil_vel = recoil_vel * (recoil_vel/(recoil_vel/(self.properties.recoil.velocity_correction_factor[axis]*2))*dt) + else + recoil_vel = 0 + end + if math.abs(recoil_vel)>math.abs(old_recoil_vel) then + recoil_vel = 0 + end + --ax^2+bx+c + --recoil_velocity_correction_rate + --recoil_correction_rate + local old_recoil = recoil + if math.abs(recoil) > 0.001 then + local correction_multiplier = self.time_since_last_fire*self.properties.recoil.target_correction_factor[axis] + local correction_value = recoil*correction_multiplier + correction_value = math.clamp(math.abs(correction_value), 0, self.properties.recoil.target_correction_max_rate[axis]) + recoil=recoil-(correction_value*dt*(math.abs(recoil)/recoil)) + --prevent overcorrection + if math.abs(recoil) > math.abs(old_recoil) then + recoil = 0 + end + end + self.velocities.recoil[axis][i] = recoil_vel + self.transforms.recoil[axis][i] = recoil + end + end +end +function gun: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- + --"length" (aka rate-pause), thus it will pi/length or pi/(rate-pause) will represent out slope of our control. + local x = (self.time_since_creation%breathing_info.rate)*math.pi/(breathing_info.rate-breathing_info.pause) + local scale = 1 + --now if it's above math.pi we know it's in the pause half of the cycle. For smoothness, we cut the sine off early and decay the value linearly. + if x > math.pi*(8/9) then + self.transforms.breathing.player_axial=self.transforms.breathing.player_axial-(self.transforms.breathing.player_axial*2*dt) + else + self.transforms.breathing.player_axial = scale*(math.sin(x)) + end +end +function gun:update_sway(dt) + for axis, sway in pairs(self.transforms.sway) do + local sway_vel = self.velocities.sway[axis] + local ran + ran = Vec.apply(Vec.new(), function(i,v) + if i ~= "x" then + return (math.random()-.5)*2 + end + end) + ran.z = 0 + sway_vel = Vec.normalize(sway_vel+(ran*dt))*self.properties.sway.angular_velocity[axis] + sway=sway+(sway_vel*dt) + if Vec.length(sway) > self.properties.sway.max_angle[axis] then + sway=Vec.normalize(sway)*self.properties.sway.max_angle[axis] + sway_vel = Vec.new() + end + self.transforms.sway[axis] = sway + self.velocities.sway[axis] = sway_vel + end + print(self.transforms.sway) +end +function gun:prepare_deletion() + assert(self.instance, "attempt to call object method on a class") + if self:has_entity() then self.entity:remove() end +end +gun.construct = function(def) + if def.instance then + --remember to give gun an id + assert(def.itemstack, "no itemstack provided for initialized object") + assert(def.player, "no player provided") + local meta = def.itemstack:get_meta() + if meta:get_string("guns4d_id") == "" then + local id = tostring(Unique_id.generate()) + meta:set_string("guns4d_id", id) + def.player:set_wielded_item(def.itemstack) + def.id = id + else + def.id = meta:get_string("guns4d_id") + end + --make sure there's nothing missing + def.properties = table.fill(gun.properties, def.properties) + --Vecize + def.transforms = table.deep_copy(gun.transforms) + for i, tbl in pairs(def.transforms) do + if tbl.gun_axial and tbl.player_axial and (not i=="breathing") then + tbl.gun_axial = Vec.new(tbl.gun_axial) + tbl.player_axial = Vec.new(tbl.player_axial) + end + end + def.velocities = table.deep_copy(gun.velocities) + for i, tbl in pairs(def.velocities) do + if tbl.gun_axial and tbl.player_axial then + tbl.gun_axial = Vec.new(tbl.gun_axial) + tbl.player_axial = Vec.new(tbl.player_axial) + end + end + elseif def.name ~= "__template__" then + local props = def.properties + assert(def.name, "no name provided") + assert(def.itemstring, "no itemstring provided") + --(this tableref is ephermeral after constructor is called, see instantiatable_class) + Guns4d.gun.registered[def.name] = def + minetest.register_entity(def.name.."_visual", { + initial_properties = { + visual = "mesh", + mesh = props.mesh, + textures = props.textures, + glow = 0, + pointable = false, + static_save = false, + }, + on_step = function(self, dtime) + local name = string.gsub(self.name, "_visual", "") + local obj = self.object + if not self.parent_player then obj:remove() return end + local player = self.parent_player + local handler = Guns4d.players[player:get_player_name()].handler + local lua_object = handler.gun + if not lua_object then obj:remove() return end + --this is changing the point of rotation if not aiming, this is to make it look less shit. + local axial_modifier = Vec.new() + if not handler.controls.ads then + local pitch = lua_object.transforms.total_offset_rotation.player_axial.x+lua_object.transforms.player_rotation.x + axial_modifier = Vec.new(pitch*(1-lua_object.consts.HIP_PLAYER_GUN_ROT_RATIO),0,0) + end + local axial_rot = lua_object.transforms.total_offset_rotation.gun_axial+axial_modifier + --attach to the correct bone, and rotate + if handler.controls.ads == false then + local normal_pos = Vec.new(props.hip.offset)*10 + -- Vec.multiply({x=normal_pos.x, y=normal_pos.z, z=-normal_pos.y}, 10) + obj:set_attach(player, gun.consts.HIPFIRE_BONE, normal_pos, -axial_rot, true) + else + local normal_pos = (props.ads.offset+Vec.new(props.ads.horizontal_offset,0,0))*10 + obj:set_attach(player, gun.consts.AIMING_BONE, normal_pos, -axial_rot, true) + end + end + }) + end +end +Guns4d.gun = Instantiatable_class:inherit(gun) \ No newline at end of file diff --git a/classes/Instantiatable_class.lua b/classes/Instantiatable_class.lua new file mode 100644 index 0000000..65cb4b5 --- /dev/null +++ b/classes/Instantiatable_class.lua @@ -0,0 +1,35 @@ +Instantiatable_class = { + instance = false +} +--not that construction change is NOT called for inheriting an object. +function Instantiatable_class:inherit(def) + --construction chain for inheritance + if not def then def = {} else def = table.shallow_copy(def) end + def.instance = false + def._construct_low = def.construct + --this effectively creates a construction chain by overwriting .construct + function def.construct(parameters) + --rawget because in a instance it may only be present in a hierarchy but not the table itself + if rawget(def, "_construct_low") then + def._construct_low(parameters) + end + if self.construct then + self.construct(parameters) + end + end + def.construct(def) + --iterate through table properties + setmetatable(def, {__index = self}) + return def +end +function Instantiatable_class:new(def) + if not def then def = {} else def = table.shallow_copy(def) end + def.instance = true + function def:inherit(def) + assert(false, "cannot inherit instantiated object") + end + setmetatable(def, {__index = self}) + --call the construct chain for inherited objects, also important this is called after meta changes + self.construct(def) + return def +end \ No newline at end of file diff --git a/classes/Player_handler.lua b/classes/Player_handler.lua new file mode 100644 index 0000000..84b57d8 --- /dev/null +++ b/classes/Player_handler.lua @@ -0,0 +1,195 @@ +local Vec = vector +local default_active_controls = { + ads = false +} +local player_handler = { + --player = playerref + --name = playername + --wielded_item = ItemStack + --gun = Gun (class) + --wield_index = Int + --model_handler = player_model_handler + look_rotation = {x=0, y=0}, + look_offset = Vec.new(), + ads_location = 0, + controls = {}, + horizontal_offset = 0 +} +local model_handler = Guns4d.player_model_handler +function player_handler:update(dt) + assert(self.instance, "attempt to call object method on a class") + local player = self.player + self.wielded_item = self.player:get_wielded_item() + local held_gun = self:holding_gun() --get the gun class that is associated with the held gun + if held_gun then + --was there a gun last time? did the wield index change? + local old_index = self.wield_index + self.wield_index = player:get_wield_index() + --if gun has changed or was not held, then reset. + if (not self.gun) or (self.gun.id ~= self.wielded_item:get_meta():get_string("guns4d_id")) then + --initialize all handlers + ----gun handler---- + if self.gun then --delete gun object if present + self.gun:prepare_deletion() + self.gun = nil + end + self.gun = held_gun:new({itemstack=self.wielded_item, player=self.player, handler=self}) --this will set itemstack meta, and create the gun based off of meta and other data. + ----model handler---- + if self.model_handler then --if model_handler present, then delete + self.model_handler:prepare_deletion() + self.model_handler = nil + end + 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 + self.horizontal_offset = self.gun.properties.ads.horizontal_offset + player:hud_set_flags({wielditem = false, crosshair = false}) + end + self.look_rotation.x, self.look_rotation.y = player:get_look_vertical()*180/math.pi, -player:get_look_horizontal()*180/math.pi + --update handlers + self.control_handler:update(dt) + self.model_handler:update(dt) + self.gun:update(dt) + elseif self.gun then + self.control_handler = nil + --delete gun object + self.gun:prepare_deletion() + self.gun = nil + self:reset_controls_table() --return controls to default + --delete model handler object (this resets the player model) + self.model_handler:prepare_deletion() + self.model_handler = nil + player:hud_set_flags({wielditem = true, crosshair = true}) --reenable hud elements + end + --eye offsets + if self.controls.ads and (self.ads_location<1) then + self.ads_location = math.clamp(self.ads_location + (dt/self.gun.properties.aim_time), 0, 1) + elseif (not self.controls.ads) and self.ads_location>0 then + local divisor = .4 + if self.gun then + divisor = self.gun.properties.aim_time + end + self.ads_location = math.clamp(self.ads_location - (dt/divisor), 0, 1) + end + self.look_offset.x = self.horizontal_offset*self.ads_location + player:set_eye_offset(self.look_offset*10) + --some status stuff + if TICK % 2 == 0 then + self.touching_ground = self:get_is_on_ground() + end + self.walking = self:get_is_walking() + --stored properties and pos must be reset as they could be outdated. + self.properties = nil + self.pos = nil +end +function player_handler:get_is_on_ground() + assert(self.instance, "attempt to call object method on a class") + local touching_ground = false + local player = self.player + local player_properties = self:get_properties() + local ray = minetest.raycast(self:get_pos()+vector.new(0, self:get_properties().eye_height, 0), self:get_pos()-vector.new(0,.1,0), true, true) + for pointed_thing in ray do + if pointed_thing.type == "object" then + if pointed_thing.ref ~= player and pointed_thing.ref:get_properties().physical == true then + touching_ground = true + end + end + if pointed_thing.type == "node" then + touching_ground = true + end + end + return touching_ground +end +function player_handler:get_is_walking() + assert(self.instance, "attempt to call object method on a class") + local walking = false + local velocity = self.player:get_velocity() + local controls + if not self.control_handler then + controls = self.player:get_player_control() + else + controls = self.control_handler.player_pressed + end + if (vector.length(vector.new(velocity.x, 0, velocity.z)) > .1) and (controls.up or controls.down or controls.left or controls.right) and self.touching_ground then + walking = true + end + return walking +end +--resets the controls bools table for the player_handler +function player_handler:reset_controls_table() + assert(self.instance, "attempt to call object method on a class") + self.controls = table.deep_copy(default_active_controls) +end +--doubt I'll ever use this... but just in case I don't want to forget. +function player_handler:get_pos() + assert(self.instance, "attempt to call object method on a class") + if self.pos then return self.pos end + self.pos = self.player:get_pos() + return self.pos +end +function player_handler:set_pos(val) + assert(self.instance, "attempt to call object method on a class") + self.pos = vector.new(val) + self.player:set_pos(val) +end +function player_handler:get_properties() + assert(self.instance, "attempt to call object method on a class") + if self.properties then return self.properties end + self.properties = self.player:get_properties() + return self.properties +end +function player_handler:set_properties(properties) + assert(self.instance, "attempt to call object method on a class") + self.player:set_properties(properties) + self.properties = table.fill(self.properties, properties) +end +function player_handler:holding_gun() + assert(self.instance, "attempt to call object method on a class") + if self.wielded_item then + for name, obj in pairs(Guns4d.gun.registered) do + if obj.itemstring == self.wielded_item:get_name() then + return obj + end + end + end +end +function player_handler:update_wield_item(wield, meta) + assert(self.instance, "attempt to call object method on a class") + local stack = self.wielded_item + if wield then + stack = ItemStack(wield) + end + if meta then + local tbl = meta + if type(meta) ~= "table" then + tbl = meta:to_table() + end + stack:from_table(tbl) + end + self.player:set_wielded_item(stack) + return stack +end +function player_handler:prepare_deletion() + assert(self.instance, "attempt to call object method on a class") + if self.gun then + self.gun:prepare_deletion() + self.gun = nil + end +end +--note that construct is NOT called as a method +function player_handler.construct(def) + if def.instance then + def.old_mesh = def.player:get_properties().mesh + assert(def.player, "no player obj provided to player_handler on construction") + --this is important, as setting a value within a table would set it for all tables otherwise + for i, v in pairs(player_handler) do + if (type(v) == "table") and not def[i] then + def[i] = v + end + end + def.look_rotation = table.deep_copy(player_handler.look_rotation) + def.controls = table.deep_copy(default_active_controls) + end +end +Guns4d.player_handler = Instantiatable_class:inherit(player_handler) diff --git a/classes/Player_model_handler.lua b/classes/Player_model_handler.lua new file mode 100644 index 0000000..1a696a9 --- /dev/null +++ b/classes/Player_model_handler.lua @@ -0,0 +1,78 @@ +local Vec = vector +--[[offsets = { + head = vector.new(0,6.3,0), + arm_right = vector.new(-3.15, 5.5, 0), + arm_right_global = vector.new(-3.15, 11.55, 0), --can be low precision + arm_left = vector.new(3.15, 5.5, 0), + arm_left_global = vector.new(3.15, 11.55, 0), +}]] +Guns4d.player_model_handler = { + offsets = { + arm = { + right = Vec.new(-3.15, 11.55, 0), + rltv_right = Vec.new(-3.15, 5.5, 0), + left = Vec.new(3.15, 11.55, 0), + rltv_left = Vec.new(3.15, 5.5, 0) + }, + head = Vec.new(0,6.3,0) + }, + handlers = {}, + mesh = "guns3d_character.b3d" +} +local player_model = Guns4d.player_model_handler +function player_model:set_default_handler() + assert(not self.instance, "cannot set default handler to an instance of a handler") + player_model.default_handler = self +end +function player_model:get_handler(meshname) + local selected_handler = player_model.handlers[meshname] + if selected_handler then return selected_handler end + return player_model.default_handler +end +function player_model:update() + assert(self.instance, "attempt to call object method on a class") + local player = self.player + local handler = Guns4d.players[player:get_player_name()].handler + local gun = handler.gun + local player_axial_offset = gun.transforms.total_offset_rotation.player_axial + local pitch = player_axial_offset.x+gun.transforms.player_rotation.x + local combined = player_axial_offset+gun.transforms.total_offset_rotation.gun_axial+Vec.new(gun.transforms.player_rotation.x,0,0) + local eye_pos = vector.new(0, handler:get_properties().eye_height*10, 0) + 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_aiming_bone", eye_pos, vector.new(pitch, 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}) +end +function player_model:prepare_deletion() + assert(self.instance, "attempt to call object method on a class") + local handler = Guns4d.players[self.player:get_player_name()].handler + local properties = handler:get_properties() + if minetest.get_modpath("player_api") then + player_api.set_model(self.player, self.old) + end + properties.mesh = self.old + handler:set_properties(properties) +end +---@diagnostic disable-next-line: duplicate-set-field +function player_model.construct(def) + if def.instance then + assert(def.mesh, "model has no mesh") + assert(def.player, "no player provided") + local handler = Guns4d.players[def.player:get_player_name()].handler + local properties = handler:get_properties() + def.old = properties.mesh + --set the model + if minetest.get_modpath("player_api") then + player_api.set_model(def.player, def.mesh) + end + properties.mesh = def.mesh + handler:set_properties(properties) + else + if def.replace then + player_model.handlers[def.replace] = def + end + end +end +Guns4d.player_model_handler = Instantiatable_class:inherit(player_model) +Guns4d.player_model_handler:set_default_handler() + diff --git a/classes/raycast_defunked.lua b/classes/raycast_defunked.lua new file mode 100644 index 0000000..78ce11e --- /dev/null +++ b/classes/raycast_defunked.lua @@ -0,0 +1,159 @@ +function guns3d.ray(player, pos, dir, def, bullet_info) + --"transverse" just means in a node + --"free" means in open air + local playername = player:get_player_name() + local is_first_iter = false + local constant = .7 + local normal + ----------------------------------------------------------initialize------------------------------------------------------------------ + if not bullet_info then + is_first_iter = true + bullet_info = { + history = {}, + state = "free", + last_pos = pos, + last_node = "", + last_normal = vector.new(), + end_direction = dir, + range_left = def.bullet.range, + penetrating_force = def.bullet.penetration_RHA + --last_pointed + } + end + table.insert(bullet_info.history, {start_pos=pos, state=bullet_info.state, normal=bullet_info.last_normal, end_direction = bullet_info.end_direction}) + --set ray end + local pos2 = pos+(dir*bullet_info.range_left) + local block_ends_early = false + --if was last in a block, check where the "transverse" state should end. + --------------------------------------------------prepare for raycast -------------------------------------------------------------- + if bullet_info.state == "transverse" then + local pointed + local ray = minetest.raycast(pos+(dir*(constant+.01)), pos, false, false) + for p in ray do + if p.type == "node" and (table.compare(p.under, bullet_info.last_pointed.under) or not minetest.registered_nodes[minetest.get_node(bullet_info.last_pointed.under).name].node_box) then + pointed = p + break + end + end + --maybe remove check for pointed + if pointed and vector.distance(pointed.intersection_point, pos) < constant then + pos2 = pointed.intersection_point + block_ends_early = true + normal = pointed.intersection_normal + bullet_info.end_direction = vector.direction(dir, vector.new()) + else + pos2 = pos+(dir*constant) + end + end + -----------------------------------------------------------raycast-------------------------------------------------------------- + local ray = minetest.raycast(pos, pos2, true, true) + local pointed + local next_ray_pos = pos2 + for p in ray do + if vector.distance(p.intersection_point, bullet_info.last_pos) > 0.0005 and vector.distance(p.intersection_point, bullet_info.last_pos) < bullet_info.range_left then + local distance = vector.distance(pos, p.intersection_point) + --if it's a node, check that it's note supposed to be ignored according to it's generated properties + if p.type == "node" and guns3d.node_properties[minetest.get_node(p.under).name].behavior ~= "ignore" then + local next_penetration_val = bullet_info.penetrating_force-(distance*guns3d.node_properties[minetest.get_node(p.under).name].rha*1000) + if bullet_info.state ~= "transverse" then + pointed = p + --print(dump(p)) + bullet_info.state = "transverse" + next_ray_pos = p.intersection_point + else + pointed = p + if minetest.get_node(p.under).name ~= bullet_info.last_node and next_penetration_val > 0 and guns3d.node_properties[minetest.get_node(p.under).name].behavior ~= "ignore" then + next_ray_pos = p.intersection_point + end + end + break + 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 p.type == "object" and p.ref ~= player then + --apply force dropoff + local next_penetration_val = bullet_info.penetrating_force-def.bullet.penetration_dropoff_RHA*distance + if bullet_info.state == "transverse" then + next_penetration_val = bullet_info.penetrating_force-(distance*guns3d.node_properties[minetest.get_node(bullet_info.last_pointed.under).name].rha*1000) + end + --insure there's still penetrating force left to actually damage the player + if bullet_info.penetrating_force > 0 then + if (bullet_info.state == "transverse" and next_penetration_val > 0) or (bullet_info.state == "free" and bullet_info.penetrating_force-def.bullet.penetration_dropoff_RHA*distance > 0) then + local penetration_val = next_penetration_val + if bullet_info.state == "free" then + bullet_info.penetrating_force = next_penetration_val + penetration_val = bullet_info.penetrating_force + end + local damage = math.floor((def.bullet.damage*(next_penetration_val/def.bullet.penetration_RHA))+1) + p.ref:punch(player, nil, {damage_groups = {fleshy = damage}}, dir) + if p.ref:is_player() then + --TODO: finish + end + end + end + end + end + end + ---------------------prepare for recursion--------------------------------------------------------------------------------- + local penetration_loss = def.bullet.penetration_dropoff_RHA + local distance = vector.distance(pos, next_ray_pos) + local new_dir = dir + local node_properties + if pointed then + node_properties = guns3d.node_properties[minetest.get_node(pointed.under).name] + end + if pointed and (not normal) then + normal = pointed.intersection_normal + else + normal = vector.new() + end + if not bullet_info.end_direction then + bullet_info.end_direction = new_dir + end + --we know if the first raycast didn't find it ended early, or if there wasn't a hit, that it isn't in a block + if block_ends_early or not pointed then + bullet_info.state = "free" + end + --calculate penetration loss, and simulate loss of accuracy + if bullet_info.history[#bullet_info.history].state == "transverse" and pointed then + local rotation = vector.apply(vector.new(), function(a) + a=a+(((math.random()-.5)*2)*node_properties.random_deviation*def.bullet.penetration_deviation*distance) + return a + end) + new_dir = vector.rotate(new_dir, rotation*math.pi/180) + penetration_loss = node_properties.rha*1000 + end + --set the current bullet info. + bullet_info.penetrating_force=bullet_info.penetrating_force-(penetration_loss*distance) + bullet_info.range_left = bullet_info.range_left-distance + bullet_info.last_pointed = pointed + bullet_info.last_normal = normal + bullet_info.last_pos = pos + + --set the last node + if pointed then + bullet_info.last_node = minetest.get_node(pointed.under).name + end + --recurse. + if bullet_info.range_left > 0.001 and bullet_info.penetrating_force > 0 then + guns3d.ray(player, next_ray_pos, new_dir, def, bullet_info) + end + -------------------------- visual ------------------------------------------------------------------------------------- + if is_first_iter then + for i, val in pairs(bullet_info.history) do + if not table.compare(val.normal, vector.new()) then + guns3d.handle_node_hit_fx(val.normal, val.end_direction, val.start_pos) + end + end + end +end +local raycast = { + history = {}, + state = "free", + last_pos = pos, + last_node = "", + last_normal = vector.new(), + end_direction = dir, + range_left = def.bullet.range, + penetrating_force = def.bullet.penetration_RHA +} \ No newline at end of file diff --git a/gun_api.lua b/gun_api.lua new file mode 100644 index 0000000..c66ac20 --- /dev/null +++ b/gun_api.lua @@ -0,0 +1,98 @@ +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=.2, y=.25}, + player_axial = {x=.25, y=.4}, + }, + }, + firerateRPM = 600, + controls = { + aim = { + conditions = {"RMB"}, + loop = false, + timer = 0, + func = function(active, interrupted, data, busy_list, handler) + if active then + handler.controls.ads = not handler.controls.ads + end + end + }, + fire = { + conditions = {"LMB"}, + loop = true, + timer = 0, + func = function(active, interrupted, data, busy_list, handler) + handler.gun:fire() + end + } + }, + 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 + 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 + --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 new file mode 100644 index 0000000..c671907 --- /dev/null +++ b/init.lua @@ -0,0 +1,51 @@ +local Vec = vector +Guns4d = { + players = {} +} +local path = minetest.get_modpath("guns4d") +dofile(path.."/misc_helpers.lua") +dofile(path.."/visual_effects.lua") +dofile(path.."/gun_api.lua") +dofile(path.."/block_values.lua") +path = path .. "/classes" +dofile(path.."/Instantiatable_class.lua") +dofile(path.."/Bullet_ray.lua") +dofile(path.."/Control_handler.lua") +dofile(path.."/Gun.lua") +dofile(path.."/Player_model_handler.lua") +dofile(path.."/Player_handler.lua") + +--load after +path = minetest.get_modpath("guns4d") + + +local player_handler = Guns4d.player_handler +local gun = Guns4d.gun + + + +minetest.register_on_joinplayer(function(player) + local pname = player:get_player_name() + Guns4d.players[pname] = { + handler = player_handler:new({player=player}) + } + player:set_fov(80) +end) +minetest.register_on_leaveplayer(function(player) + local pname = player:get_player_name() + Guns4d.players[pname].handler:prepare_deletion() + Guns4d.players[pname] = nil +end) +TICK = 0 +minetest.register_globalstep(function(dt) + TICK = TICK + 1 + if TICK > 100000 then TICK = 0 end + for player, obj in pairs(Guns4d.players) do + if not obj.handler then + --spawn the player handler. The player handler handles the gun(s), + --the player's model, and controls + obj.handler = player_handler:new({player=player}) + end + obj.handler:update(dt) + end +end) \ No newline at end of file diff --git a/misc_helpers.lua b/misc_helpers.lua new file mode 100644 index 0000000..70912dd --- /dev/null +++ b/misc_helpers.lua @@ -0,0 +1,129 @@ +--can't be copyright claimed by myself, luckily... well actually knowing the legal system I probably could sue myself. +Unique_id = { + generated = {}, +} +function math.clamp(val, lower, upper) + if lower > upper then lower, upper = upper, lower end + return math.max(lower, math.min(upper, val)) +end +function Unique_id.generate() + local genned_ids = Unique_id.generated + local id = string.sub(tostring(math.random()), 3) + while genned_ids[id] do + id = string.sub(tostring(math.random()), 3) + end + genned_ids[id] = true + return id +end +function math.rand_sign(b) + b = b or .5 + local int = 1 + if math.random() > b then int=-1 end + return int +end +--for table vectors that aren't vector objects +---@diagnostic disable-next-line: lowercase-global +function tolerance_check(a,b,tolerance) + return math.abs(a-b) > tolerance +end +function vector.equals_tolerance(v, vb, tolerance) + tolerance = tolerance or 0 + return ( + tolerance_check(v.x, vb.x, tolerance) and + tolerance_check(v.y, vb.y, tolerance) and + tolerance_check(v.z, vb.z, tolerance) + ) +end +--copy everything +function table.deep_copy(tbl, copy_metatable, indexed_tables) + if not indexed_tables then indexed_tables = {} end + local new_table = {} + local metat = getmetatable(tbl) + if metat then + if copy_metatable then + setmetatable(new_table, table.deep_copy(metat, true)) + else + setmetatable(new_table, metat) + end + end + for i, v in pairs(tbl) do + if type(v) == "table" then + if not indexed_tables[v] then + indexed_tables[v] = true + new_table[i] = table.deep_copy(v, copy_metatable) + end + else + new_table[i] = v + end + end + return new_table +end +--replace elements in tbl with elements in replacement, but preserve the rest +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 + if not preserve_reference then + new_table = table.deep_copy(tbl) + 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) + end + elseif type(replacement[i]) == "table" then + new_table[i] = table.deep_copy(replacement[i]) + else + new_table[i] = replacement[i] + end + else + new_table[i] = replacement[i] + end + end + return new_table +end +--fill "holes" in the tables. +function table.fill_in(tbl, replacement, preserve_reference, indexed_tables) + if not indexed_tables then indexed_tables = {} end --store tables to prevent circular referencing + local new_table = tbl + if not preserve_reference then + new_table = table.deep_copy(tbl) + end + for i, v in pairs(replacement) do + if new_table[i]==nil then + if type(v)=="table" then + new_table[i] = table.deep_copy(v) + else + new_table[i] = v + end + else + if (type(new_table[i]) == "table") and (type(v) == "table") then + table.fill_in(new_table[i], v, true, indexed_tables) + end + end + end + return new_table +end +--for class based OOP, ensure values containing a table in btbl are tables in a_tbl- instantiate, but do not fill. +function table.instantiate_struct(tbl, btbl, indexed_tables) + if not indexed_tables then indexed_tables = {} end --store tables to prevent circular referencing + for i, v in pairs(btbl) do + if type(v) == "table" and not indexed_tables[v] then + indexed_tables[v] = true + if not tbl[i] then + tbl[i] = table.instantiate_struct({}, v, indexed_tables) + elseif type(tbl[i]) == "table" then + tbl[i] = table.instantiate_struct(tbl[i], v, indexed_tables) + end + end + end + return tbl +end +function table.shallow_copy(t) + local new_table = {} + for i, v in pairs(t) do + new_table[i] = v + end + return new_table +end \ No newline at end of file diff --git a/model_api.lua b/model_api.lua new file mode 100644 index 0000000..e69de29 diff --git a/models/guns3d_character.b3d b/models/guns3d_character.b3d new file mode 100644 index 0000000..73a3c0d Binary files /dev/null and b/models/guns3d_character.b3d differ diff --git a/models/guns3d_character.blend b/models/guns3d_character.blend new file mode 100644 index 0000000..c6290dc Binary files /dev/null and b/models/guns3d_character.blend differ diff --git a/textures/gun_mrkr.png b/textures/gun_mrkr.png new file mode 100644 index 0000000..9bad1e2 Binary files /dev/null and b/textures/gun_mrkr.png differ diff --git a/textures/scope_back.png b/textures/scope_back.png new file mode 100644 index 0000000..24a932e Binary files /dev/null and b/textures/scope_back.png differ diff --git a/textures/scope_fore.png b/textures/scope_fore.png new file mode 100644 index 0000000..85e3d19 Binary files /dev/null and b/textures/scope_fore.png differ diff --git a/textures/smoke.png b/textures/smoke.png new file mode 100644 index 0000000..62aba0e Binary files /dev/null and b/textures/smoke.png differ diff --git a/visual_effects.lua b/visual_effects.lua new file mode 100644 index 0000000..96771e7 --- /dev/null +++ b/visual_effects.lua @@ -0,0 +1,78 @@ +--designed for use with the gun class +function Guns4d.muzzle_flash(self) + local playername = self.player:get_player_name() + if self.particle_spawners.muzzle_smoke and self.particle_spawners.muzzle_smoke ~= -1 then + minetest.delete_particlespawner(self.particle_spawners.muzzle_smoke, self.player:get_player_name()) + end + local dir, offset_pos = self:get_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)) + minetest.add_particlespawner({ + exptime = .18, + time = .1, + amount = 15, + attached = self.entity, + 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}, + texpool = { + { name = "smoke.png", alpha_tween = {.25, 0}, scale = 2, blend = "alpha", + animation = { + type = "vertical_frames", aspect_w = 16, + aspect_h = 16, length = .1, + }, + }, + { name = "smoke.png", alpha_tween = {.25, 0}, scale = .8, blend = "alpha", + animation = { + type = "vertical_frames", aspect_w = 16, + aspect_h = 16, length = .1, + }, + }, + { name = "smoke.png^[multiply:#dedede", alpha_tween = {.25, 0}, scale = 2, + blend = "alpha", + animation = { + type = "vertical_frames", aspect_h = 16, + aspect_w = 16, length = .1, + }, + }, + { name = "smoke.png^[multiply:#b0b0b0", alpha_tween = {.2, 0}, scale = 2, blend = "alpha", + animation = { + type = "vertical_frames", + aspect_w = 16, + aspect_h = 16, + length = .25, + }, + } + } + }) + --muzzle smoke + self.particle_spawners.muzzle_smoke = minetest.add_particlespawner({ + exptime = .3, + time = 2, + amount = 50, + pos = self.properties.flash_offset, + glow = 2, + vel = {min=vector.new(-.1,.4,.2), max=vector.new(.1,.6,1), bias=0}, + attached = self.entity, + texpool = { + {name = "smoke.png", alpha_tween = {.12, 0}, scale = 1.4, blend = "alpha", + animation = { + type = "vertical_frames", + aspect_w = 16, + aspect_h = 16, + length = .35, + }, + }, + {name = "smoke.png^[multiply:#b0b0b0", alpha_tween = {.2, 0}, scale = 1.4, blend = "alpha", + animation = { + type = "vertical_frames", + aspect_w = 16, + aspect_h = 16, + length = .35, + }, + } + } + }) +end \ No newline at end of file