diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a2f3cb --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Gunslinger + +This mod provides an API to add a variety of realistic and enjoyable guns to Minetest. A variety of different guns are provided with the mod, and can be disabled if required. + +## License + +- **Code**: MIT +- **Media**: CC0 + +## Architecture + +Gunslinger makes use of gun _types_ in order to ease registration of similar guns. A `type` is made up of a name and a table of default values to be applied to all guns registered with that type. At least one type needs to be registered in order to register guns. + +`Raycast` is used to find target in line-of-sight, and all objects take damage. Damage is calculated as detailed in [Damage calculation](###Damage-calculation) + +## API + +### Damage calculation + +Weapon damage = `def.type.base_dmg * def.dmg_mult` + +If headshot, damage is increased by 50% + +If shooter was looking through scope, damage is increased by 20% + +### Methods + +#### `gunslinger.register_type(name, def)` + +- Registers a def table for the type `name`. +- The def table contains the default values for all the guns registered with this type. e.g. `style_of_fire`, `enable_scope`, etc. + +#### `gunslinger.register_gun(name, def)` + +- Registers a gun with the type `def.type`, and assigns the type defaults to the gun. +- The def table contains gun-specific values like `clip_size`, `wield_image`, `scope_image`, etc. +- Default type values can also be overridden per gun by just including that value in the def table. + +### Definition table fields used by API methods + +#### Fields passed to `gunslinger.register_type` + +- `type` [string]: Name of a valid type (i.e. type registered by `gunslinger.register_type`) +- `style_of_fire` [string]: Sets style of fire + - `"manual"`: One shot per-click. + - `"burst"`: Three shots per-click. + - `"splash"`: Shotgun-style pellets; one burst per-click. + - `"automatic"`: Fully automatic; shoots as long as primary button is held down. + - `"semi-automatic"`: Same as `"automatic"`, but switches to `"burst"` when scope view is toggled. +- `scope` [string]: Sets style of scope. + - `"none"`: Default. No scope functionality. + - `"ironsight"`: Ironsight, without zoom. Unrestricted peripheral vision + - `"scope"`: Proper scope, with zoom. Restricted peripheral vision. +- `base_dmg` [number]: Base amount of damage dealt in HP. + +#### Fields passed to `gunslinger.register_gun` + +- `itemdef` [table]: Item definition table passed to `minetest.register_item`. +- `scope_overlay` [string]: Name of scope overlay texture. Must be provided if `scope` is defined. Overlay texture would be stretched across the screen. +- `clip_size` [number]: Number of bullets per-clip. +- `fire_rate` [number]: Number of shots per-second. +- `dmg_mult` [number]: Damage multiplier value. Final damage is calculated by multiplying `dmg_mult` with the type's `base_dmg`. diff --git a/api.lua b/api.lua new file mode 100644 index 0000000..186e5ee --- /dev/null +++ b/api.lua @@ -0,0 +1,191 @@ +gunslinger = {} +local guns = {} +local types = {} +local automatic = {} +local scope_overlay = {} + +-- +-- Internal API functions +-- + +local function get_def(name) + return guns[name] +end + +local function play_sound(sound, player) + minetest.sound_play(sound, { + object = player, + loop = false + }) +end + +local function show_scope(player, def) + if not player then + return + end + + -- Create HUD image + scope_overlay[player:get_player_name()] = player:hud_add({ + hud_elem_type = "image", + position = {x = 0.5, y = 0.5}, + alignment = {x = 0, y = 0}, + text = def.scope_overlay + }) +end + +local function hide_scope(player) + if not player then + return + end + + local name = player:get_player_name() + player:hud_remove(scope_overlay[name]) + scope_overlay[name] = nil +end + +local function fire(stack, player) + -- Take aim + local def = get_def(stack:get_name()) + local eye_offset = player:get_eye_offset().offset_first + local p1 = vector.add(player:get_pos(), eye_offset) + p1 = vector.add(p1, player:get_look_dir()) + local p2 = vector.add(p1, vector.multiply(player:get_look_dir(), max_dist)) + local ray = minetest.raycast(p1, p2) + local pointed = ray:next() + + -- Fire! + if pointed and pointed.type == "object" then + local target = pointed.ref + local point = pointed.intersection_point + local dmg = base_dmg * def.dmg_mult + + -- Add 50% damage if headshot + if point.y > target:get_pos().y + 1.5 then + dmg = dmg * 1.5 + end + + -- Add 20% more damage if player using scope + if scope_overlay[player:get_player_name()] then + dmg = dmg * 1.2 + end + + target:set_hp(target:get_hp() - dmg) + end + + -- Update wear + wear = wear + def.wear_step + stack:set_wear(wear) + + return stack +end + +local function reload(stack, player) + -- Check for ammo + local inv = player:get_inventory() + if inv:contains_item("main", "gunslinger:ammo") then + -- Ammo exists, reload and reset wear + inv:remove_item("main", "gunslinger:ammo") + stack:set_wear(0) + else + -- No ammo, play click sound + play_sound("gunslinger_ooa", player) + end + + return stack +end + +local function on_lclick(stack, player) + local wear = stack:get_wear() + local def = get_def(stack:get_name()) + if wear >= 65535 then + --Reload + stack = reload(stack, player) + else + if def.style_of_fire == "automatic" then + automatic[player:get_player_name()] = { + stack = stack, + def = def + } + else + stack = fire(stack, player) + end + end + + return stack +end + +local function on_rclick(stack, player) + local def = get_def(stack:get_name()) + local hud = scope_overlay[player:get_player_name()] + if hud then + hide_scope(player) + else + show_scope(player, def) + end + + return stack +end + +local function verify_def(def, is_gun) + +end + +-- Globalstep to handle firing of automatic guns +minetest.register_globalstep(function(dtime) + for name, info in pairs(automatic) do + local player = minetest.get_player_by_name(name) + if player:get_player_control().LMB then + -- If LMB pressed, fire + info.stack = fire(info.stack, player) + else + -- If LMB not pressed, remove player from list + automatic[name] = nil + end + end +end) + +-- +-- Ammo +-- + + + +-- +-- Gun registration +-- + +function gunslinger.register_type(name, def) + assert(type(name) == "string" and type(def) == "table", + "gunslinger.register_type: Invalid params!") + assert(not types[name], "gunslinger.register_type:" + .. " Attempt to register a type with an existing name!") + + types[name] = def +end + +function gunslinger.register_gun(name, def) + assert(type(name) == "string" and type(def) == "table", + "gunslinger.register_type: Invalid params!") + assert(not guns[name], "gunslinger.register_gun:" + .. " Attempt to register a gun with an existing name!") + assert(types[def.type], "gunslinger.register_gun: Attempt to" + .. " register gun of non-existent type (" .. def.type .. ")!") + + 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() + end + end + + def.style_of_fire = def.style_of_fire or def.type.style_of_fire + + def.wear = math.ceil(65534 / def.clip_size) + guns[name] = def + minetest.register_tool(name, def.itemdef) +end diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..0999f52 --- /dev/null +++ b/init.lua @@ -0,0 +1,11 @@ +local modpath = minetest.get_modpath("gunslinger") .. "/" + +-- Import API +dofile(modpath .. "api.lua") + +if not minetest.settings:get_bool("gunslinger.disable_builtin") then + dofile(modpath .. "assault_rifle.lua") + dofile(modpath .. "shotgun.lua") + dofile(modpath .. "sniper_rifle.lua") + dofile(modpath .. "handgun.lua") +end diff --git a/sounds/gunslinger_fire1.ogg b/sounds/gunslinger_fire1.ogg new file mode 100644 index 0000000..79872e4 Binary files /dev/null and b/sounds/gunslinger_fire1.ogg differ diff --git a/sounds/gunslinger_fire2.ogg b/sounds/gunslinger_fire2.ogg new file mode 100644 index 0000000..04eda6c Binary files /dev/null and b/sounds/gunslinger_fire2.ogg differ diff --git a/textures/gunslinger_cheetah.png b/textures/gunslinger_cheetah.png new file mode 100644 index 0000000..74da21e Binary files /dev/null and b/textures/gunslinger_cheetah.png differ