Weapons · Updated Apr 16, 2026

Weapon Swap & N-Slot Inventory

Weapon Swap & N-Slot Inventory

Scalable weapon inventory with cycle-based slot swap and automatic weapon replacement on pickup. Defaults to 2 slots (Halo style) — bump MaxCarriedWeapons for a Doom Eternal loadout with no code changes.


#Design Rules

  • Player carries at most MaxCarriedWeapons weapons (default 2)
  • CarriedWeapons[i] is slot i — the array index IS the slot number
  • One slot is always the active (equipped) weapon
  • Cycle — press next/previous input → equip the adjacent slot, wrapping around. Skips empty slots.
  • Pickup when a slot is empty — weapon fills the next empty slot automatically, no prompt
  • Pickup when all slots are full — current equipped weapon is dropped at the player's feet, new weapon takes its slot and auto-equips (Halo behavior, no choice)
  • Cycle re-uses the existing equip flow (RequestEquip) — no new ability needed

#Current State

ThingState
CarriedWeapons — replicated TArray<ASLWeaponActor*> on ASLCharacterBaseExists, no slot cap enforced
RequestEquip(WeaponActor*) — Server RPC, full equip flowExists
EquippedWeapon on ASLPlayerState — replicatedExists
Server_DropWeapon(WeaponActor*) on ASLCharacterBaseExists

#Part 1 — ASLCharacterBase Changes

#MaxCarriedWeapons

Configurable slot cap. Defaults to 2. Increase to support larger loadouts with no code changes.


UPROPERTY(EditDefaultsOnly, Category="SystemLink|Weapons", meta=(ClampMin="1"))

int32 MaxCarriedWeapons = 2;

#GetEquippedWeaponSlot()

Returns the index of EquippedWeapon in CarriedWeapons. Returns INDEX_NONE if not found.


UFUNCTION(BlueprintPure, Category="SystemLink|Weapons")

int32 GetEquippedWeaponSlot() const;

#Server_CycleWeapon(bool bForward)

New Server Reliable RPC. Finds the next occupied slot relative to the current equipped slot, wrapping around the array. Calls RequestEquip_Implementation on the found weapon directly (avoids double-RPC). No-ops if only one weapon is carried or IsEquipping() is true.


UFUNCTION(Server, Reliable, Category="SystemLink|Weapons")

void Server_CycleWeapon(bool bForward);

Cycle logic:


currentSlot = GetEquippedWeaponSlot()

step        = bForward ? +1 : -1

next        = (currentSlot + step + CarriedWeapons.Num()) % CarriedWeapons.Num()

while next != currentSlot:

    if CarriedWeapons[next] is valid → RequestEquip(CarriedWeapons[next]) and return

    next = (next + step + CarriedWeapons.Num()) % CarriedWeapons.Num()

// no valid slot found — no-op

#RemoveWeaponFromInventory(WeaponActor*)

Authority-guarded. Removes the weapon from CarriedWeapons. Called before AddWeaponToInventory in the full-inventory replacement path.


void RemoveWeaponFromInventory(ASLWeaponActor* WeaponActor);

#Slot cap in AddWeaponToInventory

Add early-out: if CarriedWeapons.Num() >= MaxCarriedWeapons return without adding. The pickup handles the replacement path before calling this.


#Part 2 — ASLPlayerCharacter Changes

#Input bindings

Two new input actions (Digital, Started trigger):

ActionDefault bindBehaviour
IA_SL_CycleWeaponNextMouse wheel up / QServer_CycleWeapon(true)
IA_SL_CycleWeaponPrevMouse wheel down / EServer_CycleWeapon(false)

Both bound in SetupPlayerInputComponent alongside existing weapon inputs. Dead-checks apply — cycle is blocked if IsCharacterDead().


#Part 3 — Pickup Changes (ASLWeaponPickup)

Current: Server_AcceptPickupAcceptPickupAddWeaponToInventory if not already carrying that type.

New behavior when inventory is full:


Server_AcceptPickup fires

  → CarriedWeapons.Num() >= MaxCarriedWeapons?

      Yes → Server_DropWeapon(EquippedWeapon)          // drop current at feet

             RemoveWeaponFromInventory(EquippedWeapon)  // remove from array

             AddWeaponToInventory(NewWeapon)            // fill the now-empty slot

             RequestEquip(NewWeapon)                    // auto-equip the pickup

      No  → existing path (fill empty slot, no auto-equip)


#Part 4 — HUD (deferred)

Deferred until cycle mechanic is verified working. During testing use debug prints to confirm slot state.

When implemented:

  • USLHUDWidget adds per-slot weapon containers (scales with MaxCarriedWeapons)
  • Each slot shows weapon icon + ammo count
  • Active slot is highlighted; inactive slots are dimmed
  • Driven by OnEquippedWeaponChanged — HUD re-evaluates all slot states on each equip change

#Part 5 — Shotgun (deferred)

Pump-action shotgun like Halo. Requires one fire system extension not yet built:

Multi-pellet spread fireFSLWeaponFireMode needs two new fields:

  • PelletCount (int, default 1 — preserves all existing weapon behaviour)
  • PelletSpreadAngle (float, degrees half-angle cone)

USLGameplayAbility_Fire fire step: if PelletCount > 1, fire N traces in randomised directions within PelletSpreadAngle cone. Each pellet is an independent trace with its own damage application.

Pump mechanic: PelletCount = 8–12, RPM set to pump cycle rate (~60–80 RPM), semi-auto only.

Planned assets:

  • DA_SL_Shotgun — weapon data asset
  • BP_GA_SL_Equip_Shotgun_FP / _TP — equip abilities
  • BP_GA_SL_Fire_Shotgun — fire ability (inherits USLGameplayAbility_Fire)
  • GC_SL_Shotgun_PrimaryFire — muzzle flash + blast sound cue

#New Assets Summary

AssetLocationType
IA_SL_CycleWeaponNextContent/SystemLink/Input/Actions/Input Action
IA_SL_CycleWeaponPrevContent/SystemLink/Input/Actions/Input Action
MaxCarriedWeaponsASLCharacterBaseC++ UPROPERTY
GetEquippedWeaponSlot()ASLCharacterBaseC++ BlueprintPure
Server_CycleWeapon(bool)ASLCharacterBaseC++ Server RPC
RemoveWeaponFromInventory()ASLCharacterBaseC++ helper

#Build Order

  1. C++MaxCarriedWeapons, GetEquippedWeaponSlot(), Server_CycleWeapon(), slot cap in AddWeaponToInventory. Compile.
  1. Input — create IA_SL_CycleWeapon, add to input mapping context, bind in ASLPlayerController.
  1. Pickup — update ASLWeaponPickup::AcceptPickup for full-inventory replacement behavior.
  1. Test — placeholder second weapon, verify cycle and full-inventory replacement.
  1. HUD — per-slot weapon display.
  1. Shotgun — multi-pellet fire system extension + shotgun weapon asset.

#Implementation Notes (deviations from original plan)

  • Single IA_SL_CycleWeapon (forward only) instead of Next/Prev pair — can add reverse later
  • Input lives on ASLPlayerController, not ASLPlayerCharacter (all input is controller-side)
  • Cycle while unarmed (after dropping) equips first valid weapon in inventory
  • Every pickup auto-equips regardless of whether a weapon is already held
  • WriteBackAmmoToWeaponActor() added to SetPendingEquipWeapon before FinalizeEquip — fixes ammo resetting to max on weapon swap

#Test Checklist

  • [x] Pick up first weapon → fills slot 0, equips
  • [x] Pick up second weapon → fills slot 1, auto-equips
  • [x] Cycle → equips other slot, wraps around
  • [x] Cycle while unarmed → equips first valid weapon
  • [x] Pick up third weapon when full → current dropped, new weapon equips
  • [x] Ammo preserved per-weapon across swaps
  • [ ] Die with two weapons → both drop correctly
  • [ ] Respawn → default loadout equips correctly
  • [ ] Set MaxCarriedWeapons = 3 → pick up three weapons, cycle through all three