Upload files to 'mods/gunslinger'
This commit is contained in:
parent
98a0868e64
commit
5b876085bb
|
@ -0,0 +1,11 @@
|
|||
unused_args = false
|
||||
allow_defined_top = true
|
||||
|
||||
globals = {
|
||||
"gunslinger",
|
||||
"table",
|
||||
"vector",
|
||||
"minetest",
|
||||
"ItemStack",
|
||||
"PcgRandom"
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
language: generic
|
||||
sudo: false
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- luarocks
|
||||
before_install:
|
||||
- luarocks install --local luacheck
|
||||
script:
|
||||
- $HOME/.luarocks/bin/luacheck --no-color .
|
||||
notifications:
|
||||
email: false
|
|
@ -0,0 +1,203 @@
|
|||
# `gunslinger` API documentation
|
||||
|
||||
This file aims to thoroughly document the `gunslinger` code-base and API.
|
||||
|
||||
## Data structures
|
||||
|
||||
### Ammo Definition Table
|
||||
|
||||
- `itemdef` [table]: Item definition table passed to `minetest.register_item`.
|
||||
- Note that `on_use`, `on_place`, and `on_secondary_use` will be overridden by gunslinger.
|
||||
|
||||
### Gun Definition Table (GDT)
|
||||
|
||||
- `itemdef` [table]: Item definition table passed to `minetest.register_item`.
|
||||
- Note that `on_use`, `on_place`, and `on_secondary_use` will be overridden by gunslinger.
|
||||
- `type` [string]: Type to inherit initial weapon properties from.
|
||||
- `clip_size` [number]: Number of rounds per-clip.
|
||||
- `fire_rate` [number]: Number of rounds per-second.
|
||||
- `range` [number]: Range of fire in number of nodes.
|
||||
- `mode` [string]: Firing mode.
|
||||
- `"manual"`: One round per-click, but requires manual loading for every round; aka bolt-action rifles.
|
||||
- `"semi-automatic"`: One round per-click. e.g. a typical 9mm pistol.
|
||||
- `"burst"`: Multiple rounds per-click. Can be set by defining `burst` field. Defaults to 3. e.g. M16A4
|
||||
- `"automatic"`: Fully automatic; shoots as long as primary button is held down. e.g. AKM, M416.
|
||||
- `"hybrid"`: Same as `"automatic"`, but switches to `"burst"` mode when scope view is toggled.
|
||||
|
||||
- `ammo` [string]: Name of valid registered ammo to be associated with the weapon. Defaults to `gunslinger:ammo`.
|
||||
- `dmg_mult` [number]: Damage multiplier. Multiplied with `base_dmg` to obtain initial/rated damage value. Defaults to 1.
|
||||
- `spread_mult` [number]: Spread multiplier. Multiplied with `base_spread` to obtain spread threshold for projectile. Defaults to 0.
|
||||
- `recoil_mult` [number]: Recoil multiplier. Multiplied with `base_recoil` to obtain final recoil per-round. Defaults to 0.
|
||||
- `reload_time` [number]: Reload time in seconds. Defaults to 3 to match default reload sound.
|
||||
- `pellets` [number]: Number of pellets per-round. Used for firing multiple pellets shotgun-style. Defaults to 1, meaning only one "pellet" is fired each round.
|
||||
|
||||
- `textures` [table]: Textures for various requirements.
|
||||
`projectile` [string]: Texture used for projectiles.
|
||||
- Note that assymmetric textures aren't supported. A texture modelled after a bullet-like shape won't point in the direction of flight.
|
||||
|
||||
- `sounds` [table]: Sounds for various events.
|
||||
- `fire` [string]: Sound played on fire. Defaults to `gunslinger_fire.ogg`.
|
||||
- `reload` [string]: Sound played on reload. Defaults to `gunslinger_reload.ogg`.
|
||||
- `ooa` [string]: Sound played when the gun is out of ammo and ammo isn't available in the player's inventory. Defaults to `gunslinger_ooa.ogg`.
|
||||
- `load` [string]: Sound played when the gun is manually loaded. Only used if `mode` is set to `manual`.
|
||||
|
||||
- `zoom` [number]: Zoom multiplier to be applied on player's default FOV.
|
||||
- `scope` [string]: Name of scope overlay texture.
|
||||
- Overlay texture would be stretched across the screen, and center of texture will be positioned on top of crosshair.
|
||||
- Only required if `zoom` is defined.
|
||||
- `scope_scale` [table]: Passed to `ObjectRef:hud_add` for the field `scale`.
|
||||
- Needs to have two numerical values, indexed by `x` and `y`.
|
||||
- Either of the values can be negative, and would be taken as the percentage of that direction to scale to.
|
||||
- Only required if `scope` is defined.
|
||||
|
||||
## `gunslinger` namespace
|
||||
|
||||
The `gunslinger` namespace has the following members:
|
||||
|
||||
### "Private" members
|
||||
|
||||
(**Note**: _It's not recommended to directly access the private members of the `gunslinger` namespace_)
|
||||
|
||||
- `__guns` [table]: Table of registered guns.
|
||||
- `__types` [table]: Table of registered types.
|
||||
- `__automatic` [table]: Table of players wielding automatic guns.
|
||||
- `__scopes` [table]: Table of HUD IDs of scope overlays.
|
||||
- `__interval` [table]: Table storing time from last fire; used to regulate fire-rate.
|
||||
|
||||
### `gunslinger.register_ammo(name, def)`
|
||||
|
||||
- Registers ammo.
|
||||
- `def` [ADT]: Ammo properties. ADTs currently only support the `itemdef` field, but
|
||||
|
||||
### `gunslinger.register_type(name, def)`
|
||||
|
||||
- Registers a type for `name`.
|
||||
- `def` [GDT]: Type defaults.
|
||||
|
||||
### `gunslinger.register_gun(name, def)`
|
||||
|
||||
- Registers a gun with the name `name`.
|
||||
- `def` [GDT]: Gun properties.
|
||||
|
||||
### `gunslinger.get_def(name)`
|
||||
|
||||
- Retrieves the [GDT] of the given itemname. Returns `nil` if no registered gun matches `name`.
|
||||
|
||||
### `gunslinger.get_config(name)`
|
||||
|
||||
- Returns the gunslinger's internal config table (read-only). This table contains, among others, configuration settings and default values used by the API internally. e.g `debug` [bool].
|
||||
- TODO: Improve this section.
|
||||
|
||||
## Misc. helpers
|
||||
|
||||
### `rangelim(min, val, max, default)`
|
||||
|
||||
- Convenience function used for validating gun definition fields. Returns a range-limited value if `val` exists, or returns `default`.
|
||||
- `min`, `max` [number]: Allowed minimum and maximum bounds for the value.
|
||||
- `val` [number]: Value to be validated and returned.
|
||||
- `default` [number]: Value to be returned if `val` is `nil`.
|
||||
|
||||
### `get_eye_pos(player)`
|
||||
|
||||
- Returns position of player eye in `v3f` format.
|
||||
- Equivalent to
|
||||
|
||||
```lua
|
||||
local pos = player:get_pos()
|
||||
pos.y = pos.y + player:get_properties().eye_height
|
||||
```
|
||||
|
||||
- `player` [ObjectRef]: Player whose eye position is to be calculated.
|
||||
|
||||
### `get_pointed_thing(pos, dir, range)`
|
||||
|
||||
- Helper function that performs a raycast from player in the direction of player's look dir, and up to the distance defined by `range`.
|
||||
- `pos` [table]: Initial position of raycast.
|
||||
- `dir` [table]: Direction of raycast.
|
||||
- `range` [number]: Range of raycast from `pos` in nodes/meters.
|
||||
|
||||
### `play_sound(sound, obj)`
|
||||
|
||||
- Helper function to play object-centric sound.
|
||||
- `sound` [SimpleSoundSpec]: Sound to be played.
|
||||
- `obj` [ObjectRef]: ObjectRef which is the origin of the played sound.
|
||||
|
||||
## Internal API methods
|
||||
|
||||
### `add_auto(name, def, stack)`
|
||||
|
||||
- Helper function to add player entry to `automatic` table.
|
||||
- `def` and `stack` are cached locally for improved performance.
|
||||
- `name` [string]: Player name.
|
||||
- `def` [GDT]: Wielded gun's GDT.
|
||||
- `stack` [itemstack]: Itemstack of wielded item.
|
||||
|
||||
### `sanitize_def(def)`
|
||||
|
||||
- Helper function to check for and correct erroneous fields and to add default values for missing fields in a GDT.
|
||||
- Returns the sanitized version of `def`.
|
||||
- `def` [GDT]: GDT to be sanitized.
|
||||
|
||||
### `show_scope(player, scope, zoom)`
|
||||
|
||||
- Activates gun scope, handles placement of HUD scope element.
|
||||
- `player` [ObjectRef]: Player used for HUD element creation.
|
||||
- `scope` [string]: Name of scope overlay texture.
|
||||
- `zoom` [number]: FOV that will override player's default FOV.
|
||||
|
||||
### `hide_scope(player)`
|
||||
|
||||
- De-activates gun scope, removes HUD element.
|
||||
- `player` [ObjectRef]: Player to remove HUD element from.
|
||||
|
||||
### `on_lclick(stack, player)`
|
||||
|
||||
- `on_use` callback for all registered guns. This is where most of the firing logic happens.
|
||||
- Handles gun firing depending on their `mode`.
|
||||
- [`reload`] is called when the gun's magazine is empty.
|
||||
- If `mode` is `"automatic"`, an entry is added to the `automatic` table which is parsed by `on_step`.
|
||||
- `stack` [ItemStack]: ItemStack of wielditem.
|
||||
- `player` [ObjectRef]: ObjectRef of user.
|
||||
|
||||
### `on_rclick(stack, player)`
|
||||
|
||||
- `on_place`/`on_secondary_use` callback for all registered guns. Toggles scope view.
|
||||
- `stack` [ItemStack]: ItemStack of wielditem.
|
||||
- `player` [ObjectRef]: Right-clicker.
|
||||
|
||||
### `reload(stack, player)`
|
||||
|
||||
- Reloads stack if ammo exists and plays `def.sounds.reload`. Otherwise, just plays `def.sounds.ooa`.
|
||||
- Takes the same arguments as `on_lclick`.
|
||||
|
||||
### `fire(stack, player)`
|
||||
|
||||
- Responsible for firing one single round and dealing damage if target was hit. Updates wear by `def.unit_wear`.
|
||||
- If gun is worn out, `reload` is called.
|
||||
- Takes the same arguments as `on_lclick`.
|
||||
|
||||
### `burst_fire(stack, player)`
|
||||
|
||||
- Helper method to fire in burst mode.
|
||||
- Takes the same arguments as `on_lclick`.
|
||||
|
||||
### `handle_hit_target(shooter, pthing, stack)`
|
||||
|
||||
- Processes target hits. Currently only damages the target by `config.base_dmg * def.dmg_mult` HP.
|
||||
- `shooter` [ObjectRef]: Player firing the projectile that intersected the target.
|
||||
- `pthing` [pointed_thing]: Pointed thing corresponding to the intersected target.
|
||||
- `stack` [ItemStack]: Item used to fire the projectile that intersected the target.
|
||||
|
||||
### `auto_fire(dtime)`
|
||||
|
||||
- Updates player's time from last shot (`gunslinger.__interval`).
|
||||
- Calls `fire` for all guns in the `automatic` table if player's LMB is pressed.
|
||||
- If LMB is released, the respective entry is removed from the table.
|
||||
- `dtime` [number]: Delta-time (in seconds) passed to all globalsteps.
|
||||
|
||||
### `process_progressive_raycast(dtime)`
|
||||
|
||||
- Processes all progressive raycast rounds each server step.
|
||||
- If the projectile intersects a target, `handle_hit_target` is invoked with the appropriate params.
|
||||
- If no targets are intersected, the projectile continues on its path.
|
||||
- `dtime` [number]: Delta-time (in seconds) passed to all globalsteps.
|
|
@ -0,0 +1,552 @@
|
|||
gunslinger = {
|
||||
__guns = {},
|
||||
__ammo = {},
|
||||
__types = {},
|
||||
__stack = {},
|
||||
__rounds = {},
|
||||
__reloading = {},
|
||||
__automatic = {},
|
||||
__scopes = {},
|
||||
__interval = {}
|
||||
}
|
||||
|
||||
local config = {
|
||||
debug = minetest.settings:get_bool("gunslinger.debug", false),
|
||||
max_wear = 65534,
|
||||
base_dmg = 1,
|
||||
projectile_speed = 500,
|
||||
base_spread = 0.001,
|
||||
base_recoil = 0.001,
|
||||
lite = minetest.settings:get_bool("gunslinger.lite"),
|
||||
fov_transition_time = 0.1
|
||||
}
|
||||
|
||||
local random = PcgRandom(os.time())
|
||||
local vec = table.copy(vector)
|
||||
|
||||
--
|
||||
-- Internal API functions
|
||||
--
|
||||
|
||||
local function rangelim(low, val, high, default)
|
||||
if not val and default then
|
||||
return default
|
||||
elseif low and val and high then
|
||||
return math.max(low, math.min(val, high))
|
||||
else
|
||||
error("gunslinger: Invalid rangelim invocation!", 2)
|
||||
end
|
||||
end
|
||||
|
||||
local function get_eye_pos(player)
|
||||
if not player then
|
||||
return
|
||||
end
|
||||
|
||||
local pos = player:get_pos()
|
||||
pos.y = pos.y + player:get_properties().eye_height
|
||||
return pos
|
||||
end
|
||||
|
||||
local function get_pointed_thing(pos, dir, range, avoid_self)
|
||||
if not pos or not dir or not range then
|
||||
error("gunslinger: Invalid get_pointed_thing invocation" ..
|
||||
" (missing params)", 2)
|
||||
end
|
||||
|
||||
local pos2 = vector.add(pos, vector.multiply(dir, range))
|
||||
local ray = minetest.raycast(pos, pos2)
|
||||
local pthing = ray:next()
|
||||
|
||||
-- pointer.intersection_normal is a zero vector
|
||||
-- if ray originates from inside pointed_thing
|
||||
if avoid_self and pthing and
|
||||
vector.equals(pthing.intersection_normal, vector.new(0, 0, 0)) then
|
||||
pthing = ray:next()
|
||||
end
|
||||
return pthing
|
||||
end
|
||||
|
||||
local function play_sound(sound, player)
|
||||
minetest.sound_play(sound, {
|
||||
object = player,
|
||||
loop = false
|
||||
})
|
||||
end
|
||||
|
||||
local function add_auto(name, def, stack)
|
||||
gunslinger.__automatic[name] = {
|
||||
def = def,
|
||||
stack = stack
|
||||
}
|
||||
end
|
||||
|
||||
local function sanitize_def(def)
|
||||
if type(def) ~= "table" then
|
||||
error("gunslinger: Gun definition has to be a table!", 2)
|
||||
end
|
||||
|
||||
if (def.mode == "automatic" or def.mode == "hybrid") and config.lite then
|
||||
error("gunslinger: Attempting to register gun of type '" ..
|
||||
def.mode .. "' when lite mode is enabled", 2)
|
||||
end
|
||||
|
||||
-- Check for ammo
|
||||
if def.ammo then
|
||||
assert(gunslinger.__ammo[def.ammo], "gunslinger.register_gun: Invalid ammo!")
|
||||
else
|
||||
def.ammo = "gunslinger:default_ammo"
|
||||
end
|
||||
|
||||
if def.mode == "burst" then
|
||||
def.burst = rangelim(2, def.burst, 5, 3)
|
||||
end
|
||||
|
||||
def.dmg_mult = rangelim(1, def.dmg_mult, 100, 1)
|
||||
def.reload_time = rangelim(1, def.reload_time, 10, 2.5)
|
||||
def.spread_mult = rangelim(0, def.spread_mult, 500, 0)
|
||||
def.recoil_mult = rangelim(0, def.recoil_mult, 500, 0)
|
||||
def.pellets = rangelim(1, def.pellets, 20, 1)
|
||||
|
||||
def.sounds = def.sounds or {}
|
||||
def.sounds.fire = def.sounds.fire or "gunslinger_fire"
|
||||
def.sounds.reload = def.sounds.reload or "gunslinger_reload"
|
||||
def.sounds.ooa = def.sounds.ooa or "gunslinger_ooa"
|
||||
|
||||
def.textures = def.textures or {}
|
||||
def.textures.projectile = def.textures.projectile or "gunslinger_projectile.png"
|
||||
|
||||
-- Limit zoom to 8x; default to no zoom
|
||||
def.zoom = def.zoom and rangelim(1, def.zoom, 8)
|
||||
|
||||
local scale = def.scope_scale
|
||||
if def.scope and (not scale or type(scale) ~= "table" or not scale.x or not scale.y or
|
||||
type(scale.x) ~= "number" or type(scale.y) ~= "number") then
|
||||
error("gunslinger: Invalid `scope_scale` definition!", 2)
|
||||
end
|
||||
|
||||
return def
|
||||
end
|
||||
|
||||
--------------------------------
|
||||
|
||||
local function show_scope(player, zoom, scope, scale)
|
||||
if not player then
|
||||
return
|
||||
end
|
||||
|
||||
local scope_spec = { fov = zoom }
|
||||
|
||||
-- Set FOV multiplier to 1 / def.zoom
|
||||
-- e.g. if def.zoom == 4, FOV multiplier would be 1/4
|
||||
player:set_fov(1 / zoom, true, config.fov_transition_time)
|
||||
|
||||
-- Scope HUD element; disable wielditem and crosshair HUD elements if scope exists
|
||||
if scope then
|
||||
scope_spec.hud = player:hud_add({
|
||||
hud_elem_type = "image",
|
||||
position = {x = 0.5, y = 0.5},
|
||||
alignment = {x = 0, y = 0},
|
||||
scale = scale,
|
||||
text = scope
|
||||
})
|
||||
player:hud_set_flags({
|
||||
wielditem = false,
|
||||
crosshair = false
|
||||
})
|
||||
end
|
||||
|
||||
gunslinger.__scopes[player:get_player_name()] = scope_spec
|
||||
end
|
||||
|
||||
local function hide_scope(player)
|
||||
if not player then
|
||||
return
|
||||
end
|
||||
|
||||
local name = player:get_player_name()
|
||||
local scope_spec = gunslinger.__scopes[name]
|
||||
|
||||
player:set_fov(0, false, config.fov_transition_time)
|
||||
|
||||
-- Remove scope HUD element; revert visibility changes to default HUD elements
|
||||
if scope_spec.hud then
|
||||
player:hud_remove(scope_spec.hud)
|
||||
player:hud_set_flags({
|
||||
wielditem = true,
|
||||
crosshair = true
|
||||
})
|
||||
end
|
||||
|
||||
gunslinger.__scopes[name] = nil
|
||||
end
|
||||
|
||||
--------------------------------
|
||||
|
||||
local function reload(stack, player)
|
||||
-- Check for ammo
|
||||
local inv = player:get_inventory()
|
||||
local def = gunslinger.__guns[stack:get_name()]
|
||||
local meta = stack:get_meta()
|
||||
|
||||
if meta:contains("reloading") then
|
||||
return
|
||||
end
|
||||
|
||||
local taken = inv:remove_item("main", def.ammo .. " " .. def.clip_size)
|
||||
if taken:is_empty() then
|
||||
play_sound(def.sounds.ooa, player)
|
||||
else
|
||||
local name = player:get_player_name()
|
||||
gunslinger.__interval[name] = gunslinger.__interval[name] - def.reload_time
|
||||
play_sound(def.sounds.reload, player)
|
||||
meta:set_string("reloading")
|
||||
local wear = math.floor(config.max_wear -
|
||||
(taken:get_count() / def.clip_size) * config.max_wear)
|
||||
minetest.after(def.reload_time, function()
|
||||
stack:set_wear(wear)
|
||||
meta:set_string("reloading", "")
|
||||
player:set_wielded_item(stack)
|
||||
end)
|
||||
end
|
||||
|
||||
return stack
|
||||
end
|
||||
|
||||
local function fire(stack, player)
|
||||
if not stack then
|
||||
return
|
||||
end
|
||||
|
||||
local name = player:get_player_name()
|
||||
local def = gunslinger.__guns[stack:get_name()]
|
||||
if not def then
|
||||
return stack
|
||||
end
|
||||
|
||||
local wear = stack:get_wear()
|
||||
if wear == config.max_wear then
|
||||
gunslinger.__automatic[name] = nil
|
||||
return reload(stack, player)
|
||||
end
|
||||
|
||||
-- Play gunshot sound
|
||||
play_sound(def.sounds.fire, player)
|
||||
|
||||
local pos = get_eye_pos(player)
|
||||
local dir = player:get_look_dir()
|
||||
|
||||
-- Apply projectile engine to each pellet
|
||||
for i = 1, def.pellets do
|
||||
-- Mimic inaccuracy by applying randomized miniscule deviations
|
||||
-- Reduce inaccuracy by half if player is using scope
|
||||
if def.spread_mult ~= 0 then
|
||||
-- TODO: Unhardcode scoping factor by taking scope FOVs into consideration
|
||||
local scoping_factor = gunslinger.__scopes[name] and 0.5 or 1
|
||||
dir = vector.apply(dir, function(n)
|
||||
return n +
|
||||
random:next(-def.spread_mult, def.spread_mult) *
|
||||
config.base_spread * scoping_factor
|
||||
end)
|
||||
end
|
||||
|
||||
--
|
||||
-- Progressive Raycasting
|
||||
--
|
||||
-- Tracks and simulates individual projectiles until they hit a target
|
||||
--
|
||||
|
||||
-- Insert round_spec
|
||||
gunslinger.__rounds[#gunslinger.__rounds + 1] = {
|
||||
shooter = name,
|
||||
stack = player:get_wielded_item(),
|
||||
|
||||
initial_pos = pos,
|
||||
pos = pos,
|
||||
dir = dir,
|
||||
range = def.range,
|
||||
speed = config.projectile_speed
|
||||
}
|
||||
|
||||
-- Projectile particle
|
||||
minetest.add_particle({
|
||||
pos = pos,
|
||||
velocity = vector.multiply(dir, config.projectile_speed),
|
||||
expirationtime = def.range / config.projectile_speed,
|
||||
size = 3,
|
||||
texture = def.textures.projectile,
|
||||
collisiondetection = true,
|
||||
collision_removal = true,
|
||||
object_collision = true,
|
||||
glow = 10
|
||||
})
|
||||
end
|
||||
|
||||
-- Simulate recoil
|
||||
local offset = config.base_recoil * def.recoil_mult
|
||||
local look_vertical = player:get_look_vertical() - offset
|
||||
look_vertical = rangelim(-math.pi / 2, look_vertical, math.pi / 2)
|
||||
player:set_look_vertical(look_vertical)
|
||||
|
||||
-- Update wear
|
||||
wear = stack:get_wear() + def.unit_wear
|
||||
if wear > config.max_wear then
|
||||
wear = config.max_wear
|
||||
end
|
||||
stack:set_wear(wear)
|
||||
|
||||
return stack
|
||||
end
|
||||
|
||||
local function burst_fire(stack, player)
|
||||
local def = gunslinger.__guns[stack:get_name()]
|
||||
for i = 1, def.burst do
|
||||
minetest.after(i / def.fire_rate, function(...)
|
||||
-- Use global var to store stack, because the stack
|
||||
-- can't be directly accessed outside minetest.after
|
||||
gunslinger.__stack[arg[2]:get_player_name()] = fire(arg[1], arg[2])
|
||||
end, stack, player)
|
||||
end
|
||||
|
||||
return gunslinger.__stack[player:get_player_name()]
|
||||
end
|
||||
|
||||
--------------------------------
|
||||
|
||||
local function handle_hit_target(shooter, pthing, stack)
|
||||
-- TODO: Run on_hit callbacks here
|
||||
|
||||
if config.debug then
|
||||
local pthing_str
|
||||
if pthing.type == "object" then
|
||||
local obj = pthing.ref
|
||||
if obj:is_player() then
|
||||
pthing_str = "[Player] " .. obj:get_player_name()
|
||||
else
|
||||
pthing_str = "[Entity] " .. obj:get_luaentity()
|
||||
end
|
||||
else
|
||||
pthing_str = minetest.get_node(pthing.under).name
|
||||
end
|
||||
|
||||
minetest.chat_send_all("handle_hit_target\n-----------------" ..
|
||||
"\n\tstack=" .. stack:to_string() .. "\n\tpthing=" .. pthing_str)
|
||||
end
|
||||
|
||||
if pthing.type == "object" then
|
||||
pthing.ref:punch(shooter, nil, {damage_groups = {
|
||||
fleshy = config.base_dmg * gunslinger.__guns[stack:get_name()].dmg_mult
|
||||
}})
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------
|
||||
|
||||
-- Progressive Raycasting
|
||||
local function process_progressive_raycast(dtime)
|
||||
for i, round_spec in pairs(gunslinger.__rounds) do
|
||||
-- Calculate distance projectile can travel until next iteration
|
||||
local delta_range = round_spec.speed * dtime
|
||||
local pointed = get_pointed_thing(round_spec.pos,
|
||||
round_spec.dir, delta_range, true)
|
||||
|
||||
-- We've hit something!
|
||||
if pointed then
|
||||
gunslinger.__rounds[i] = nil
|
||||
|
||||
-- Invoke handle_hit_target, pass the required data
|
||||
handle_hit_target(minetest.get_player_by_name(round_spec.shooter),
|
||||
pointed, round_spec.stack)
|
||||
end
|
||||
|
||||
-- We've hit nothing; continue tracking projectile
|
||||
local prev_pos = round_spec.pos
|
||||
round_spec.pos = vec.add(round_spec.pos,
|
||||
vec.multiply(round_spec.dir, delta_range))
|
||||
round_spec.dir = vec.direction(prev_pos, round_spec.pos)
|
||||
|
||||
-- Spawn particles
|
||||
if config.debug then
|
||||
minetest.add_particle({
|
||||
pos = prev_pos,
|
||||
expirationtime = 10,
|
||||
size = 10,
|
||||
glow = 10
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
minetest.register_globalstep(process_progressive_raycast)
|
||||
|
||||
--------------------------------
|
||||
|
||||
local function on_lclick(stack, player)
|
||||
if not stack or not player then
|
||||
return
|
||||
end
|
||||
|
||||
local def = gunslinger.__guns[stack:get_name()]
|
||||
if not def then
|
||||
return
|
||||
end
|
||||
|
||||
local name = player:get_player_name()
|
||||
if gunslinger.__interval[name] and gunslinger.__interval[name] < def.unit_time then
|
||||
return
|
||||
end
|
||||
gunslinger.__interval[name] = 0
|
||||
|
||||
if def.mode == "automatic" and not gunslinger.__automatic[name] then
|
||||
stack = fire(stack, player)
|
||||
add_auto(name, def, stack)
|
||||
elseif def.mode == "hybrid"
|
||||
and not gunslinger.__automatic[name] then
|
||||
if gunslinger.__scopes[name] then
|
||||
stack = burst_fire(stack, player)
|
||||
else
|
||||
add_auto(name, def)
|
||||
end
|
||||
elseif def.mode == "burst" then
|
||||
stack = burst_fire(stack, player)
|
||||
elseif def.mode == "semi-automatic" then
|
||||
stack = fire(stack, player)
|
||||
elseif def.mode == "manual" then
|
||||
local meta = stack:get_meta()
|
||||
local loaded = meta:get("loaded")
|
||||
if not loaded then
|
||||
if def.sounds.load then
|
||||
play_sound(def.sounds.load, player)
|
||||
end
|
||||
|
||||
meta:set_string("loaded", "true")
|
||||
stack = reload(stack, player)
|
||||
else
|
||||
meta:set_string("loaded", "")
|
||||
stack = fire(stack, player)
|
||||
end
|
||||
end
|
||||
|
||||
return stack
|
||||
end
|
||||
|
||||
local function on_rclick(stack, player)
|
||||
local def = gunslinger.__guns[stack:get_name()]
|
||||
if gunslinger.__scopes[player:get_player_name()] then
|
||||
hide_scope(player)
|
||||
else
|
||||
if def.zoom then
|
||||
show_scope(player, def.zoom, def.scope, def.scope_scale)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------
|
||||
|
||||
-- Process automatic fire
|
||||
local function auto_fire(dtime)
|
||||
for name in pairs(gunslinger.__interval) do
|
||||
gunslinger.__interval[name] = gunslinger.__interval[name] + dtime
|
||||
end
|
||||
if not config.lite then
|
||||
for name, info in pairs(gunslinger.__automatic) do
|
||||
local player = minetest.get_player_by_name(name)
|
||||
if not player or player:get_hp() <= 0 then
|
||||
gunslinger.__automatic[name] = nil
|
||||
elseif gunslinger.__interval[name] > info.def.unit_time then
|
||||
if player:get_player_control().LMB and
|
||||
player:get_wielded_item():get_name() ==
|
||||
info.stack:get_name() then
|
||||
-- If LMB pressed, fire
|
||||
info.stack = fire(info.stack, player)
|
||||
player:set_wielded_item(info.stack)
|
||||
if gunslinger.__automatic[name] then
|
||||
gunslinger.__automatic[name].stack = info.stack
|
||||
end
|
||||
gunslinger.__interval[name] = 0
|
||||
else
|
||||
gunslinger.__automatic[name] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
minetest.register_globalstep(auto_fire)
|
||||
|
||||
--
|
||||
-- Public API functions
|
||||
--
|
||||
|
||||
function gunslinger.get_config()
|
||||
return table.copy(config)
|
||||
end
|
||||
|
||||
function gunslinger.get_def(name)
|
||||
return gunslinger.__guns[name]
|
||||
end
|
||||
|
||||
function gunslinger.register_ammo(name, def)
|
||||
assert(type(name) == "string" and type(def) == "table",
|
||||
"gunslinger.register_ammo: Invalid params!")
|
||||
assert(not gunslinger.__ammo[name], "gunslinger.register_ammo:" ..
|
||||
" Attempt to register new ammo with an existing name!")
|
||||
|
||||
-- TODO: Generalize sanitize_def to work with all definition tables
|
||||
assert(def.itemdef and type(def.itemdef) == "table",
|
||||
"gunslinger.register_ammo: Invalid Ammo Definition Table!")
|
||||
|
||||
gunslinger.__ammo[name] = def
|
||||
minetest.register_craftitem(name, def.itemdef)
|
||||
end
|
||||
|
||||
function gunslinger.register_type(name, def)
|
||||
assert(type(name) == "string" and type(def) == "table",
|
||||
"gunslinger.register_type: Invalid params!")
|
||||
assert(not gunslinger.__types[name], "gunslinger.register_type:" ..
|
||||
" Attempt to register new type with an existing name!")
|
||||
|
||||
gunslinger.__types[name] = def
|
||||
end
|
||||
|
||||
function gunslinger.register_gun(name, def)
|
||||
assert(type(name) == "string" and type(def) == "table",
|
||||
"gunslinger.register_gun: Invalid params!")
|
||||
assert(not gunslinger.__guns[name], "gunslinger.register_gun: " ..
|
||||
"Attempt to register new gun with an existing name!")
|
||||
|
||||
-- Import type defaults if def.type specified
|
||||
-- This should be the first field to be parsed for the types system to work properly
|
||||
if def.type then
|
||||
assert(gunslinger.__types[def.type], "gunslinger.register_gun: Invalid type!")
|
||||
|
||||
for attr, val in pairs(gunslinger.__types[def.type]) do
|
||||
def[attr] = val
|
||||
end
|
||||
end
|
||||
|
||||
def = sanitize_def(def)
|
||||
|
||||
-- Add additional helper fields for internal use
|
||||
def.unit_wear = math.ceil(config.max_wear / def.clip_size)
|
||||
def.unit_time = 1 / def.fire_rate
|
||||
|
||||
-- Register gun
|
||||
gunslinger.__guns[name] = def
|
||||
|
||||
def.itemdef.on_use = on_lclick
|
||||
def.itemdef.on_secondary_use = on_rclick
|
||||
def.itemdef.on_place = function(stack, player, pointed)
|
||||
if pointed.type == "node" then
|
||||
local node = minetest.get_node_or_nil(pointed.under)
|
||||
local nodedef = minetest.registered_items[node.name]
|
||||
return nodedef.on_rightclick or on_rclick(stack, player)
|
||||
elseif pointed.type == "object" then
|
||||
local entity = pointed.ref:get_luaentity()
|
||||
return entity:on_rightclick(player) or on_rclick(stack, player)
|
||||
end
|
||||
end
|
||||
|
||||
-- Register tool
|
||||
minetest.register_tool(name, def.itemdef)
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
gunslinger.register_gun("gunslinger:cheetah", {
|
||||
itemdef = {
|
||||
description = "Cheetah (Assault Rifle)",
|
||||
inventory_image = "gunslinger_cheetah.png",
|
||||
wield_image = "gunslinger_cheetah.png^[transformFXR300",
|
||||
wield_scale = {x = 3, y = 3, z = 1}
|
||||
},
|
||||
|
||||
mode = "automatic",
|
||||
dmg_mult = 2,
|
||||
recoil_mult = 5,
|
||||
fire_rate = 8,
|
||||
clip_size = 30,
|
||||
range = 80
|
||||
})
|
||||
|
||||
minetest.register_alias("cheetah", "gunslinger:cheetah")
|
Loading…
Reference in New Issue