593 lines
18 KiB
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
|
|
)]] |