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
MaxCarriedWeaponsweapons (default 2)
CarriedWeapons[i]is sloti— 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
| Thing | State |
|---|---|
CarriedWeapons — replicated TArray<ASLWeaponActor*> on ASLCharacterBase | Exists, no slot cap enforced |
RequestEquip(WeaponActor*) — Server RPC, full equip flow | Exists |
EquippedWeapon on ASLPlayerState — replicated | Exists |
Server_DropWeapon(WeaponActor*) on ASLCharacterBase | Exists |
#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):
| Action | Default bind | Behaviour |
|---|---|---|
IA_SL_CycleWeaponNext | Mouse wheel up / Q | Server_CycleWeapon(true) |
IA_SL_CycleWeaponPrev | Mouse wheel down / E | Server_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_AcceptPickup → AcceptPickup → AddWeaponToInventory 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:
USLHUDWidgetadds per-slot weapon containers (scales withMaxCarriedWeapons)
- 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 fire — FSLWeaponFireMode 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 (inheritsUSLGameplayAbility_Fire)
GC_SL_Shotgun_PrimaryFire— muzzle flash + blast sound cue
#New Assets Summary
| Asset | Location | Type |
|---|---|---|
IA_SL_CycleWeaponNext | Content/SystemLink/Input/Actions/ | Input Action |
IA_SL_CycleWeaponPrev | Content/SystemLink/Input/Actions/ | Input Action |
MaxCarriedWeapons | ASLCharacterBase | C++ UPROPERTY |
GetEquippedWeaponSlot() | ASLCharacterBase | C++ BlueprintPure |
Server_CycleWeapon(bool) | ASLCharacterBase | C++ Server RPC |
RemoveWeaponFromInventory() | ASLCharacterBase | C++ helper |
#Build Order
C++ —✓MaxCarriedWeapons,GetEquippedWeaponSlot(),Server_CycleWeapon(), slot cap inAddWeaponToInventory. Compile.
Input — create✓IA_SL_CycleWeapon, add to input mapping context, bind inASLPlayerController.
Pickup — update✓ASLWeaponPickup::AcceptPickupfor full-inventory replacement behavior.
Test — placeholder second weapon, verify cycle and full-inventory replacement.✓
- HUD — per-slot weapon display.
- 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, notASLPlayerCharacter(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 toSetPendingEquipWeaponbeforeFinalizeEquip— 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