sm64coopdx/mods/gun-mod/c-weapon.lua

593 lines
18 KiB
Lua

if SM64COOPDX_VERSION == nil then return end
---------------------------------------
-- API and other important functions --
---------------------------------------
--- @class Weapon
--- @field public id integer
--- @field public name string
--- @field public dualWield boolean
--- @field public model ModelExtendedId
--- @field public armModel ModelExtendedId
--- @field public bulletModel ModelExtendedId
--- @field public primarySounds integer[]
--- @field public secondarySounds integer[]
--- @field public strong boolean
--- @field public rapidFire boolean
--- @field public spread number
--- @field public damage number
--- @field public bulletCount integer
--- @field public bulletScale number
--- @field public bulletSpeed number
--- @field public bulletSteps integer
--- @field public maxAmmo integer
--- @field public ammo integer
--- @field public deployTime integer
--- @field public cooldownTime integer
--- @field public reloadTime integer
--- @field public deployTimer integer
--- @field public cooldownTimer integer
--- @field public reloadTimer integer
--- @field public primaryFireFunc fun(weapon:Weapon)
--- @field public secondaryFireFunc fun(weapon:Weapon)
--- @field public reqCheck fun(m:MarioState)
--- @class WeaponId
--- @type Weapon[]
gWeaponTable = {}
--- @type Weapon[]
gInventory = {}
for i = 1, MAX_INVENTORY_SLOTS do gInventory[i] = nil end
inventorySlot = 1
useDualWieldWeapon = false
local weaponObtained = false
local weaponId = -1
local sMutableWeaponFields = {
["ammo"] = true,
["deployTimer"] = true,
["cooldownTimer"] = true,
["reloadTimer"] = true
}
local sReadonlyMetatable = {
__index = function(table, key)
return rawget(table, key)
end,
__newindex = function()
error("attempt to update a read-only table", 2)
end
}
local sWeaponMetatable = {
__index = function(table, key)
return rawget(table, key)
end,
__newindex = function(table, key, value)
if sMutableWeaponFields[key] then
rawset(table, key, value)
else
error("attempt to update an immutable weapon field", 2)
end
end
}
--- @param name string
--- @param dualWield boolean
--- @param model ModelExtendedId
--- @param armModel ModelExtendedId
--- @param bulletModel ModelExtendedId
--- @param primarySounds integer[]
--- @param secondarySounds integer[]
--- @param strong boolean
--- @param rapidFire boolean
--- @param spread number
--- @param damage number
--- @param bulletCount integer
--- @param bulletScale number
--- @param bulletSpeed number
--- @param bulletSteps integer
--- @param maxAmmo integer
--- @param deployTime integer
--- @param cooldownTime integer
--- @param reloadTime integer
--- @param primaryFireFunc fun(weapon:Weapon)
--- @param secondaryFireFunc fun(weapon:Weapon)
--- @param reqCheck function
--- @return integer
--- Registers a weapon metatable into existence
function weapon_register(name, dualWield, model, armModel, bulletModel, primarySounds, secondarySounds, strong, rapidFire, spread, damage, bulletCount, bulletScale, bulletSpeed, bulletSteps, maxAmmo, deployTime, cooldownTime, reloadTime, primaryFireFunc, secondaryFireFunc, reqCheck)
weaponId = weaponId + 1
gWeaponTable[weaponId] = {
id = weaponId,
name = name,
dualWield = dualWield,
model = model,
armModel = armModel,
bulletModel = bulletModel,
primarySounds = primarySounds,
secondarySounds = secondarySounds,
strong = strong,
rapidFire = rapidFire,
spread = spread,
damage = damage,
bulletCount = bulletCount,
bulletScale = bulletScale,
bulletSpeed = bulletSpeed,
bulletSteps = bulletSteps,
maxAmmo = maxAmmo,
ammo = maxAmmo,
deployTime = deployTime,
cooldownTime = cooldownTime,
reloadTime = reloadTime,
deployTimer = deployTime,
cooldownTimer = 0,
reloadTimer = 0,
primaryFireFunc = primaryFireFunc,
secondaryFireFunc = secondaryFireFunc,
reqCheck = reqCheck
}
setmetatable(gWeaponTable[weaponId], sReadonlyMetatable)
return weaponId
end
--- @param id integer
--- @return nil
--- Unregisters a weapon from existence by ID
function weapon_unregister(id)
if type(id) ~= "number" then return end
gWeaponTable[id] = nil
end
--- @return nil
--- Unregisters all weapons from existence
function weapon_unregister_all()
gWeaponTable = {}
end
--- @return string
--- Properly formatted weapon list string for commands
function get_weapon_list_string()
local string = "["
for id, weapon in pairs(gWeaponTable) do
string = string .. weapon.name:lower()
if id < weaponId then
string = string .. "|"
end
end
string = string .. "]"
return string
end
--- @param obj Object
--- @param owner integer
--- @param id integer
--- @param dualWield integer
--- @param extra integer
--- @return nil
--- Returns a 4 byte behavior parameter integer for weapon objects.
function obj_set_weapon_params(obj, owner, id, dualWield, extra)
if obj == nil then return end
if type(owner) ~= "number" or type(id) ~= "number" or type(dualWield) ~= "number" or type(extra) ~= "number" then return end
obj.oBehParams = owner | (id << 8) | (dualWield << 16) | (extra << 24)
obj.oBehParams2ndByte = id
end
--- @param obj Object
--- @return integer
--- Returns the owner parameter (first parameter) of a weapon object.
function obj_get_weapon_owner(obj)
if obj == nil then return 0 end
return obj.oBehParams & 0xFF
end
--- @param obj Object
--- @return integer
--- Returns the id parameter (second parameter) of a weapon object.
function obj_get_weapon_id(obj)
if obj == nil then return 0 end
return obj.oBehParams2ndByte
end
--- @param obj Object
--- @return integer
--- Returns the dual wield parameter (third parameter) of a weapon object.
function obj_get_weapon_dual_wield(obj)
if obj == nil then return 0 end
return (obj.oBehParams >> 16) & 0xFF
end
--- @param obj Object
--- @return integer
--- Returns the extra parameter (fourth parameter) of a weapon object.
function obj_get_weapon_extra(obj)
if obj == nil then return 0 end
return (obj.oBehParams >> 24) & 0xFF
end
--- @return Weapon|nil
--- Returns the table of the current weapon
function cur_weapon()
return gInventory[inventorySlot]
end
--- @return Weapon|nil
--- Returns the table of the current dual wielding weapon
function cur_dual_wield_weapon()
local weapon = cur_weapon()
if weapon == nil or not weapon.dualWield then return nil end
local weaponBefore = gInventory[inventorySlot - 1]
if weaponBefore ~= nil and weaponBefore.dualWield and weaponBefore.id == weapon.id then return weaponBefore end
local weaponAfter = gInventory[inventorySlot + 1]
if weaponAfter ~= nil and weaponAfter.dualWield and weaponAfter.id == weapon.id then return weaponAfter end
return nil
end
--- @return integer
--- Returns the current inventory slot
function get_inventory_slot()
return inventorySlot
end
--- @param slot integer
--- @return nil
--- Sets the current inventory slot
function set_inventory_slot(slot)
if type(slot) ~= "number" then return end
-- reset fields
gInventory[slot].deployTimer = gInventory[slot].deployTime
gInventory[slot].cooldownTimer = 0
gInventory[slot].reloadTimer = if_then_else(gInventory[slot].ammo <= 0 and gInventory[slot].maxAmmo > 0, gInventory[slot].reloadTime, 0)
inventorySlot = clamp(slot, 1, MAX_INVENTORY_SLOTS)
sync_current_weapons()
delete_held_weapon()
if get_first_person_enabled() then
delete_viewmodels()
spawn_viewmodels()
end
end
local function table_clone(table)
local cloned = {}
for k, v in pairs(table) do
if type(v) == "table" then
cloned[k] = table_clone(v)
else
cloned[k] = v
end
end
return cloned
end
--- @param weapon WeaponId
local function inventory_clone(weapon)
local table = table_clone(gWeaponTable[weapon])
setmetatable(table, sWeaponMetatable)
return table
end
local function get_inventory_slots_used()
local count = 0
for i = 1, MAX_INVENTORY_SLOTS do
if gInventory[i] ~= nil then
count = count + 1
end
end
return count
end
--- @param weapon WeaponId
--- @return boolean
--- Picks up a weapon by ID
function pickup_weapon(weapon)
if type(weapon) ~= "number" then return false end
for i = 1, MAX_INVENTORY_SLOTS do
if gInventory[i] == weapon then
return false
elseif gInventory[i] == nil or get_inventory_slots_used() == MAX_INVENTORY_SLOTS then
if not weaponObtained then
play_sound(SOUND_MENU_STAR_SOUND, gMarioStates[0].marioObj.header.gfx.cameraToObject)
weaponObtained = true
else
play_sound(SOUND_MENU_CLICK_CHANGE_VIEW, gMarioStates[0].marioObj.header.gfx.cameraToObject)
end
gInventory[i] = inventory_clone(weapon)
set_inventory_slot(i)
return true
end
end
return false
end
function sync_current_weapons()
gPlayerSyncTable[0].curWeapon = nil
gPlayerSyncTable[0].curWeapon2 = nil
local weapon = cur_weapon()
if weapon ~= nil then
gPlayerSyncTable[0].curWeapon = weapon.id
end
local weapon2 = cur_dual_wield_weapon()
if weapon2 ~= nil then
gPlayerSyncTable[0].curWeapon2 = weapon2.id
end
end
--- @param weapon Weapon
function handle_reloading(weapon)
if weapon == nil then return end
if weapon.reloadTimer > 0 then
weapon.reloadTimer = weapon.reloadTimer - 1
if weapon.name == "Shotgun" then
weapon.ammo = math.floor(lerp(weapon.maxAmmo, 0, weapon.reloadTimer / weapon.reloadTime))
end
if weapon.reloadTimer == 0 then
weapon.ammo = weapon.maxAmmo
end
end
end
function weapon_update()
--- @type MarioState
local m = gMarioStates[0]
-- update inventory slot
if (m.controller.buttonPressed & U_JPAD) ~= 0 then
inventorySlot = inventorySlot + 1
if inventorySlot > MAX_INVENTORY_SLOTS then
inventorySlot = 1
end
set_inventory_slot(inventorySlot)
elseif (m.controller.buttonPressed & D_JPAD) ~= 0 then
inventorySlot = inventorySlot - 1
if inventorySlot < 1 then
inventorySlot = MAX_INVENTORY_SLOTS
end
set_inventory_slot(inventorySlot)
end
local weapon1 = cur_weapon()
local weapon2 = cur_dual_wield_weapon()
--- @type Weapon
local weapon = if_then_else(weapon2 ~= nil and useDualWieldWeapon, weapon2, weapon1)
if weapon1 == nil then return end -- if the player isn't holding a weapon
if weapon.deployTimer <= 0 and weapon.cooldownTimer <= 0 then
local buttonFlags = if_then_else(weapon.rapidFire, m.controller.buttonDown, m.controller.buttonPressed)
if (buttonFlags & Y_BUTTON) ~= 0 and weapon.primaryFireFunc ~= nil then
weapon.cooldownTimer = weapon.cooldownTime
weapon.primaryFireFunc(weapon)
elseif (buttonFlags & X_BUTTON) ~= 0 and weapon.secondaryFireFunc ~= nil then
weapon.cooldownTimer = weapon.cooldownTime
weapon.secondaryFireFunc(weapon)
end
end
weapon1.deployTimer = handle_timer(weapon1.deployTimer)
weapon1.cooldownTimer = handle_timer(weapon1.cooldownTimer)
if weapon2 ~= nil then
weapon2.deployTimer = handle_timer(weapon2.deployTimer)
weapon2.cooldownTimer = handle_timer(weapon2.cooldownTimer)
end
handle_reloading(weapon1)
handle_reloading(weapon2)
end
function delete_held_weapon()
local held = obj_get_first_with_behavior_id(id_bhvHeldWeapon)
while held ~= nil do
if obj_get_weapon_owner(held) == gNetworkPlayers[0].globalIndex then
obj_mark_for_deletion(held)
end
held = obj_get_next_with_same_behavior_id(held)
end
end
-------------
-- objects --
-------------
--- @param o Object
local function bhv_held_weapon_init(o)
o.oFlags = OBJ_FLAG_UPDATE_GFX_POS_AND_ANGLE
o.oGraphYOffset = 10
cur_obj_scale(0.6)
cur_obj_hide()
end
--- @param o Object
local function bhv_held_weapon_loop(o)
local np = network_player_from_global_index(obj_get_weapon_owner(o))
if np == nil or not gGlobalSyncTable.gunModEnabled then
obj_mark_for_deletion(o)
return
end
local m = gMarioStates[np.localIndex]
local dualWield = obj_get_weapon_dual_wield(o)
--- @type Weapon|nil
local weapon = if_then_else(dualWield ~= 0, cur_dual_wield_weapon(), cur_weapon())
if active_player(m) == 0 or weapon == nil then
obj_mark_for_deletion(o)
return
end
local index = if_then_else(dualWield ~= 0, 1, 0)
if m.marioBodyState.updateTorsoTime == gMarioStates[0].marioBodyState.updateTorsoTime and m.marioBodyState.handState == MARIO_HAND_FISTS and weapon.reqCheck(m) then
cur_obj_unhide()
o.oPosX = get_hand_foot_pos_x(m, index) + m.vel.x - (sins(m.faceAngle.y) * 30)
o.oPosY = get_hand_foot_pos_y(m, index) + m.vel.y + if_then_else(m.action == ACT_JUMP or m.action == ACT_DOUBLE_JUMP, 20, 0)
o.oPosZ = get_hand_foot_pos_z(m, index) + m.vel.z - (coss(m.faceAngle.y) * 30)
o.oFaceAngleYaw = m.faceAngle.y - 0x4000
o.oFaceAnglePitch = 0
o.oFaceAngleRoll = 0
else
cur_obj_hide()
o.oPosX = m.pos.x
o.oPosY = m.pos.y + 60
o.oPosZ = m.pos.z
end
if m.playerIndex == 0 and get_first_person_enabled() then
cur_obj_hide()
end
obj_set_model_extended(o, weapon.model)
end
id_bhvHeldWeapon = hook_behavior(nil, OBJ_LIST_GENACTOR, true, bhv_held_weapon_init, bhv_held_weapon_loop, "bhvGmHeldWeapon")
--- @param o Object
local function bhv_pickup_weapon_init(o)
o.oFlags = OBJ_FLAG_UPDATE_GFX_POS_AND_ANGLE
o.oIntangibleTimer = 0
obj_scale(o, 0.75)
o.hitboxRadius = 90
o.hitboxHeight = 90
network_init_object(o, true, {})
end
--- @param o Object
local function bhv_pickup_weapon_loop(o)
cur_obj_update_floor_height()
o.oPosY = o.oFloorHeight + 130
o.oFaceAngleYaw = o.oFaceAngleYaw + 0x800
local nearest = nearest_mario_state_to_object(o)
if obj_check_hitbox_overlap(o, nearest.marioObj) and o.oIntangibleTimer == 0 and nearest.playerIndex == 0 then
o.oIntangibleTimer = 450
pickup_weapon(o.oBehParams)
end
if o.oIntangibleTimer ~= 0 then
cur_obj_hide()
else
cur_obj_unhide()
o.oGraphYOffset = math.sin(o.oTimer * 0.5) * 5
end
end
id_bhvWeaponPickup = hook_behavior(nil, OBJ_LIST_LEVEL, true, bhv_pickup_weapon_init, bhv_pickup_weapon_loop, "bhvGmWeaponPickup")
-------------
-- weapons --
-------------
WEAPON_PISTOL = weapon_register(
"Pistol", -- name
true, -- dual wieldable
E_MODEL_PISTOL, -- weapon model
E_MODEL_SINGLE_ARM, -- arm model
E_MODEL_YELLOW_COIN, -- bullet model
{ sync_audio_sample_load("pistol_shoot.mp3") }, -- primary sounds
{ sync_audio_sample_load("pistol_reload.mp3") }, -- secondary sounds
false, -- strong weapon
false, -- rapid fire
10, -- spread
15, -- damage
1, -- bullet count
0.2, -- bullet scale
500, -- bullet speed
10, -- bullet steps
18, -- max ammo
15, -- deploy time
5, -- cooldown time
60, -- reload time
common_shoot, -- primary fire
common_reload, -- secondary fire
check_common_gun_requirements -- requirement checks function
)
WEAPON_MAGNUM = weapon_register(
"Magnum", -- name
true, -- dual wieldable
E_MODEL_MAGNUM, -- weapon model
E_MODEL_SINGLE_ARM, -- arm model
E_MODEL_METALLIC_BALL, -- bullet model
{ sync_audio_sample_load("magnum_shoot.mp3") }, -- primary sounds
{ sync_audio_sample_load("magnum_reload.mp3") }, -- secondary sounds
false, -- strong weapon
false, -- rapid fire
0, -- spread
50, -- damage
1, -- bullet count
0.2, -- bullet scale
2000, -- bullet speed
40, -- bullet steps
6, -- max ammo
15, -- deploy time
30, -- cooldown time
90, -- reload time
common_shoot, -- primary fire
common_reload, -- secondary fire
check_common_gun_requirements -- requirement checks function
)
WEAPON_AK47 = weapon_register(
"AK47", -- name
false, -- dual wieldable
E_MODEL_AK47, -- weapon model
E_MODEL_SINGLE_ARM, -- arm model
E_MODEL_YELLOW_COIN, -- bullet model
{ sync_audio_sample_load("ak47_shoot.mp3") }, -- primary sounds
{ sync_audio_sample_load("pistol_reload.mp3") }, -- secondary sounds
false, -- strong weapon
true, -- rapid fire
30, -- spread
10, -- damage
1, -- bullet count
0.2, -- bullet scale
500, -- bullet speed
10, -- bullet steps
40, -- max ammo
30, -- deploy time
4, -- cooldown time
70, -- reload time
common_shoot, -- primary fire
common_reload, -- secondary fire
check_common_gun_requirements -- requirement checks function
)
--[[WEAPON_SHOTGUN = weapon_register(
"Shotgun", -- name
false, -- dual wieldable
E_MODEL_SHOTGUN, -- weapon model
E_MODEL_SINGLE_ARM, -- arm model
E_MODEL_RED_COIN, -- bullet model
{ sync_audio_sample_load("shotgun_shoot.mp3") }, -- primary sounds
{}, -- secondary sounds
true, -- strong weapon
false, -- rapid fire
50, -- spread
6, -- damage
5, -- bullet count
0.2, -- bullet scale
1000, -- bullet speed
20, -- bullet steps
7, -- max ammo
15, -- deploy time
30, -- cooldown time
120, -- reload time
common_shoot, -- primary fire
common_reload, -- secondary fire
check_common_gun_requirements -- requirement checks function
)]]