Skip to content

Pickups & Inventory ​

Pickups are server-authoritative — the server decides who gets what and when. Your client only renders the world objects and shows the HUD prompts.

Inventory Model ​

Washiki Washoi uses a 3-slot COD-style inventory:

SlotDefaultSwappable?
1Blade (melee)No — always equipped
2(empty)Yes — picked up
3(empty)Yes — picked up

You can carry the Blade plus up to two other weapons. Picking up a fourth weapon prompts for a swap (see below). Ammo is tracked per-type (not per-weapon):

Ammo types
Shells, bullets, rockets, cells, grenades, bolts, instas and lasers
Blade uses no finite ammo pool

The total ammo pool is shared across weapons of the same ammo type. Ammo pickups add to your pool; capacity defaults to 200 (per type).

Pickup Types ​

TypeEffectExamples
WeaponAdds the weapon to your inventory if you have room; otherwise prompts you to swap.weapon_rocketlauncher, weapon_riotgun
AmmoAdds to your ammo pool. Auto-pickup on touch.ammo_rockets, ammo_shells
HealthRestores HP (see below).item_health_small/medium/large/mega/ultra
ArmorAdds armor plates (see below).item_armor_shard/ga/ya/ra

Armor — plate model ​

Armor is one 0–100 pool presented as four plates × 25 (Call-of-Duty style, masked behind the classic Quake armor names). Damage depletes the pool continuously (~66% of incoming damage is absorbed by armor); plates only matter at pickup time — inserting a plate tops your current partial plate up to its 25-boundary:

PickupClassnameEffect
Sharditem_armor_shard+5 flat
Green Armoritem_armor_ga+1 plate (round up to the next 25, then +25) — e.g. 58 → 75
Yellow Armoritem_armor_ya+2 plates
Red Armoritem_armor_rafill to 100

Health ​

PickupClassnameEffect
Smallitem_health_small+5, capped at 100 (does not overheal)
Mediumitem_health_medium (or bare item_health)+25, capped at 100
Largeitem_health_large+50, capped at 100
Mega / Ultraitem_health_mega / _ultra+100 overhealing to 200, then decaying 1 HP/s back toward 100

Walking Over a Weapon Pickup ​

The client checks proximity every frame (cylindrical region around the pickup: 96 unit radius, -24 to +88 vertical band). When you enter the region:

  • You don't have the weapon, and you have a free slot → server auto-gives you the weapon. No prompt.
  • You already have the weapon → server auto-gives you the weapon's default ammo. No prompt.
  • All three slots are full + you don't have this weapon + your equipped weapon is the Blade → server still auto-swaps. No prompt (the Blade is always free to drop).
  • All three slots full + you don't have this weapon + your equipped weapon is NOT the Blade → HUD prompt: PRESS [F] TO SWAP TO <WEAPON>. The server waits for your USE button (default F). On press, it drops your currently equipped weapon and gives you the new one.

Walking Over an Ammo Pickup ​

Server auto-gives the ammo on touch. The HUD ammo counter updates immediately. No prompt.

Dropped Weapons ​

When a player dies, their non-Blade weapons drop as physical entities with Havok dynamic-body tumble. They land via BSP collision, settle, and become regular weapon pickups that other players can grab. Dropped weapons carry the ammo their owner had at the moment of death.

Pickup Respawn ​

Map-spawned pickups respawn on a per-type timer, the same in every gametype:

PickupRespawn
Weapons15 s
Ammo15 s
Armor25 s
Health (small / medium / large)20 s
Mega / ultra health60 s
Quad Damage120 s

Armor and mega/ultra health are also absent for the first 15s of a match so nobody grabs a power item off spawn. Dropped weapons do not respawn — they're one-and-done.

Implementation note

The weapon timer is set by the server launcher (setPickupRespawnTime(15) in the dedicated-server CLI) and overrides the 30 s each gametype declares in getItemSpawnRules(). The other timers are fixed defaults in PickupSystem.ts; the per-gametype ammo, armor, health and mega-health values in getItemSpawnRules() are not read today.

A gametype can also change when items return (respawnMode):

  • timed — after the timer above. This is the default and is what every mode except Search & Destroy uses.
  • round — not until the next round. Search & Destroy works this way: one Red Armor per round, and whoever takes it keeps it.
  • alternating — not until the round after next. Experimental; no shipped mode uses it.

Search & Destroy also leaves the Quad Damage out of the map entirely.

Powerups ​

Timed pickups that modify you until they run out. Currently one is implemented:

PowerupEffectDurationRespawn
Quad Damage×4 damage and knockback on everything you deal30 s120 s

Things worth knowing:

  • Quad scales everything, including self-damage. A quad rocket jump throws you four times as far and usually kills you. That trade is the point.
  • Re-taking extends, it does not reset. Picking up a quad you already hold adds a full 30 seconds to what is left.
  • Every shipped map places exactly one item_quad, so the 120-second respawn is the whole map-control rhythm — long enough that holding it is worth fighting over.
  • Expiry is stored as an absolute server tick, not a countdown. That is what keeps it exact through rollback and demo re-simulation, and it means the HUD timer is a read of authoritative state rather than a local guess.

item_warshell and item_regen exist in the map data and in the pk3 icon and sound banks, but are deliberately not implemented.

Loadouts (Search & Destroy) ​

In Search & Destroy you do not start with just a blade — you spawn with a chosen kit of blade + one primary + one secondary, plus full spawn armor. Map items still spawn on top of that.

The choice applies from the next round. Pick it from the HUD's loadout picker during the freeze, or from the console:

loadout rlauncher machinegun

Everywhere else, the spawn loadout is blade-only and the map is the whole economy.

Server-Authoritative Active Set ​

Pickups are fully server-authoritative and convergent. Rather than one-shot "you took it" events, every snapshot carries a packed bitmask of which pickups are currently active (pickupActiveMask, bit index = pickup id). The client reconciles each pickup's visibility to the mask every frame.

Two properties fall out of this:

  • Self-healing. A dropped packet can't cause a "ghost weapon" (visible but ungrantable) — the next snapshot re-states the truth and the client converges.
  • Provably matching ids. Both server and client derive the pickup list and its ids from the same BSP entity table (extractPickups in src/shared/bsp/PickupTable.ts), so pickup id N means the same object on both sides with no handshake. Unresolved ids fail closed (no phantom pickup) rather than guessing by position.

Inventory API (for developers) ​

InventoryManager lives at src/games/fps/world/InventoryManager.ts and is exposed on FPSPlayer.inventory. Notable methods:

  • giveWeapon(weaponId) — returns { acquired, dropped }; if at capacity, drops the currently equipped weapon.
  • removeWeapon(weaponId) — manual remove.
  • setCurrentWeapon(weaponId) — switch equipped slot.
  • giveAmmo(type, amount) — adds to the type pool.
  • setAmmo(type, amount) — set absolute.
  • getAmmo(type) — current count for a type.
  • getSlotCount() — how many slots are filled.
  • hasWeapon(weaponId) — boolean.
  • MAX_WEAPONS (static) — 3 by design; see the comment in InventoryManager.ts.

See Also ​

  • Weapons — per-weapon stats and ammo types
  • HUD — reading the weapon slots + ammo counter
  • The engine's Pickup System Guide (docs/manual/guides/pickup-system.md in the engine repo) — the engine-generic Pickup base class that FPSPickup extends

Built on the SHAG Engine.