first commit

This commit is contained in:
FatalErr42O 2023-06-11 23:34:28 -07:00
commit 5bd53dc6a2
20 changed files with 1646 additions and 0 deletions

9
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"Lua.diagnostics.globals": [
"minetest",
"vector",
"dump",
"player_api",
"ItemStack"
]
}

75
block_values.lua Normal file
View File

@ -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

184
classes/Bullet_ray.lua Normal file
View File

@ -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)

View File

@ -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)

473
classes/Gun.lua Normal file
View File

@ -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)

View File

@ -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

195
classes/Player_handler.lua Normal file
View File

@ -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)

View File

@ -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()

View File

@ -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
}

98
gun_api.lua Normal file
View File

@ -0,0 +1,98 @@
local Vec = vector
local default_def = {
--name = <string>
--itemstring = <string>
--textures = {<textures>}
--mesh = <meshname> (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

51
init.lua Normal file
View File

@ -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)

129
misc_helpers.lua Normal file
View File

@ -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

0
model_api.lua Normal file
View File

BIN
models/guns3d_character.b3d Normal file

Binary file not shown.

Binary file not shown.

BIN
textures/gun_mrkr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

BIN
textures/scope_back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
textures/scope_fore.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
textures/smoke.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 B

78
visual_effects.lua Normal file
View File

@ -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