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