
How to Design an Inventory and Equipment System in ECS
Inventory systems seem simple until they start interacting with the rest of the game.
At first, all we need is:
- pick up an item
- store it in inventory
- show it in the UI
Then we add:
- equipping weapons
- armor slots
- consumables
- item quantities
- quickbar shortcuts
- stat modifiers
- loot drops
- item details
- cooldowns
- vendors
- crafting
Before long, inventory touches almost every major gameplay system.
This is exactly where clear ECS boundaries become useful.
Instead of building one giant InventorySystem that owns everything, we can separate:
- runtime inventory state
- equipment state
- static item definitions
- pickup logic
- item-use requests
- item-use resolution
- stat calculation
- quickbar state
- UI presentation
In this guide, we'll build an inventory and equipment architecture that keeps those responsibilities separate while still allowing them to work together.
The Main Pieces
A practical ECS inventory feature might contain:
InventoryEquipmentQuickbarPickupItemDatabaseInventorySystemEquipmentSystemItemUseRequestSystemItemUseSystemStatsSystem- inventory UI systems
- gameplay events
Each piece has a different purpose.
The first important distinction is between runtime state and item definitions.
Inventory Is Runtime State
An Inventory component describes what a particular entity currently owns.
For example:
1export class Inventory {2 constructor() {3 this.items = new Map()4 }5}The map might contain:
healthPotion→ 3bronzeSword→ 1coin→ 27
This is runtime state.
Different entities can have different inventories.
The player's inventory may contain twenty items.
A chest may contain three.
An NPC might have none.
Keep Behaviour Out of Inventory
It is tempting to write:
1export class Inventory {2 constructor() {3 this.items = new Map()4 }5
6 addItem(itemId, amount) {7 // ...8 }9
10 useItem(itemId) {11 // ...12 }13
14 equipItem(itemId) {15 // ...16 }17
18 dropItem(itemId) {19 // ...20 }21}That begins moving gameplay behaviour into the component.
Soon Inventory needs to understand:
- item definitions
- equipment
- health
- stats
- quickbars
- events
- UI
- crafting
A cleaner ECS approach keeps Inventory focused on state while systems own the rules.
Item Definitions Are Static Data
The inventory should not contain the full definition of every item.
Instead, store reusable item information separately.
For example:
1export const ItemDatabase = {2 healthPotion: {3 name: 'Health Potion',4 type: 'heal',5 amount: 25,6 },7
8 bronzeSword: {9 name: 'Bronze Sword',10 type: 'equipment',11 slot: 'weapon',12
13 modifiers: {14 attack: 5,15 },16 },17
18 leatherArmor: {19 name: 'Leather Armor',20 type: 'equipment',21 slot: 'body',22
23 modifiers: {24 defense: 3,25 },26 },27}The database answers:
What kind of item is bronzeSword?The inventory answers:
How many Bronze Swords does this entity currently own?
Those are different responsibilities.
Store IDs Instead of Duplicating Definitions
Suppose the player owns a Bronze Sword.
The inventory does not need to copy:
- its name
- attack bonus
- equipment slot
- description
It can simply store the ID:
1inventory.items.set(2 'bronzeSword',3 1,4)Whenever a system needs the definition:
1const item =2 ItemDatabase['bronzeSword']This keeps static configuration in one place.
If the sword changes from +5 Attack to +6 Attack, we change the item definition rather than every inventory record.
A Simple Inventory Quantity Model
A useful inventory representation is:
1Map<itemId, amount>For example:
1inventory.items.set(2 'healthPotion',3 3,4)5
6inventory.items.set(7 'coin',8 27,9)To increase an existing quantity:
1const current =2 inventory.items.get(3 itemId,4 ) ?? 05
6inventory.items.set(7 itemId,8 current + amount,9)This works well for stackable items.
Not Every Game Uses the Same Inventory Model
Some games need individual item instances rather than simple quantities.
For example, two swords may have different:
- durability
- enchantments
- random rolls
- upgrades
- ownership IDs
In that case, each item instance may need its own runtime identity.
But for items where every instance is identical, storing an item ID and amount is much simpler.
Choose the model based on the game you are building.
Pickups Can Be Components Too
An item lying in the world can be represented with a Pickup component.
For example:
1export class Pickup {2 constructor(3 itemId,4 amount = 1,5 ) {6 this.itemId = itemId7 this.amount = amount8 this.collected = false9 }10}The entity itself may also have:
TransformMesh- collision data
Interactable
The Pickup component tells gameplay systems what the entity represents as loot.
Picking Up an Item
A pickup flow might look like:
- player interacts with a pickup
- interaction resolves
- pickup event is created
- InventorySystem receives the event
- inventory quantity increases
- pickup entity is removed or marked collected
- UI reflects the new inventory
The important part is that the pickup does not modify the inventory itself.
The pickup stores data.
The inventory system owns the inventory change.
InventorySystem Can Own Inventory Changes
A simple InventorySystem might consume pickup events.
For example:
1const pickups =2 this.world.events.consume(3 EVT_PICKUP,4 )5
6for (const event of pickups) {7 const inventory =8 this.world.getComponent(9 event.entity,10 Inventory,11 )12
13 if (!inventory) {14 continue15 }16
17 const current =18 inventory.items.get(19 event.itemId,20 ) ?? 021
22 inventory.items.set(23 event.itemId,24 current + event.amount,25 )26}This gives us one place responsible for adding picked-up items.
Why Use a Pickup Event?
Without an event, the interaction system might directly modify inventory.
That would mean the interaction system needs to understand inventory rules.
Instead, it can communicate:
This entity picked up this item.
Then InventorySystem handles the inventory-specific behaviour.
That keeps interaction and inventory responsibilities separate.
Equipment Is Different From Inventory
Owning an item and equipping an item are not the same thing.
The player might own:
- three swords
- two helmets
- five potions
but only one sword can be equipped in the weapon slot.
That is why equipment deserves its own component.
For example:
1export class Equipment {2 constructor() {3 this.weapon = null4 this.head = null5 this.body = null6 this.accessory = null7 }8}The inventory answers:
What do I own?
Equipment answers:
What am I currently using?
Equipment Slots Are Data
A simple RPG might use:
1export const EQUIP_SLOTS = [2 'weapon',3 'head',4 'body',5 'accessory',6]Later, the game could add:
- gloves
- boots
- necklace
- ring
- offhand
The exact slots are game-specific.
The architectural idea remains the same.
Item Definitions Can Declare Their Slot
A weapon definition might contain:
1bronzeSword: {2 name: 'Bronze Sword',3 type: 'equipment',4 slot: 'weapon',5
6 modifiers: {7 attack: 5,8 },9}Armor might contain:
1leatherArmor: {2 name: 'Leather Armor',3 type: 'equipment',4 slot: 'body',5
6 modifiers: {7 defense: 3,8 },9}Now the equipment logic can be data-driven.
It does not need a hard-coded condition for every item.
Equipping Is a Gameplay Rule
Do not make the UI directly write:
1equipment.weapon =2 'bronzeSword'just because the user clicked Equip.
The UI should request the action.
A gameplay system should validate and apply it.
Why?
Because equipping may eventually require checking:
- does the player own the item?
- is it actually equipment?
- which slot does it use?
- is the character allowed to equip it?
- are level requirements satisfied?
- is the entity dead?
- is equipment locked during combat?
- should an old item be returned somewhere?
Those are gameplay rules.
They do not belong in presentation code.
Request, Validate, Resolve
A useful equipment flow is:
UI or input
requests an equip action.
EquipmentSystem
validates the request.
Equipment component
stores the successful result.
StatsSystem
recalculates effective stats.
UI
reads the updated state.
This follows the same request-and-resolution pattern we use elsewhere in ECS.
An Equip Request Event
For example:
1this.world.events.emit(2 EVT_EQUIP_ITEM_REQUEST,3 {4 entity: player,5 itemId: 'bronzeSword',6 },7)An EquipmentSystem can consume it.
The source of the request does not matter.
It could come from:
- inventory UI
- keyboard input
- controller input
- a script
- eventually a network message
The gameplay system receives the same intent.
Validating Equipment
A simplified equipment system might do:
1const inventory =2 this.world.getComponent(3 entity,4 Inventory,5 )6
7const equipment =8 this.world.getComponent(9 entity,10 Equipment,11 )12
13const item =14 ItemDatabase[itemId]15
16if (!inventory) {17 continue18}19
20if (!equipment) {21 continue22}23
24if (!item) {25 continue26}27
28if (29 item.type !== 'equipment'30) {31 continue32}33
34if (35 !inventory.items.has(36 itemId,37 )38) {39 continue40}41
42equipment[item.slot] =43 itemIdThis is intentionally simple.
The system is the authority deciding whether the equip action is valid.
Equipment Does Not Need to Remove the Item From Inventory
There are several valid inventory models.
One approach is:
Equipped items remain represented in inventory.
The equipment component simply references the item ID.
Another approach removes equipped items from normal inventory slots.
Both can work.
The important thing is to choose a clear rule and keep it consistent.
For a simple ECS RPG, retaining ownership in Inventory and storing the equipped reference separately is often easy to reason about.
Equipment Feeds Into Stats
Now our inventory architecture connects directly to the stats system.
Suppose the player has:
1equipment.weapon =2 'bronzeSword'StatsSystem can look up:
1ItemDatabase[2 equipment.weapon3]and find:
1modifiers: {2 attack: 5,3}Then calculate:
- Base Attack: 5
- Bronze Sword: +5
- Final Attack: 10
Equipment stores what is equipped.
StatsSystem decides what those items do to effective stats.
Don't Let Equipment Directly Modify FinalStats
Avoid:
1equipment.equip(2 'bronzeSword',3)4
5finalStats.attack += 5inside the same component or UI action.
That reintroduces the problems of manually adding and subtracting modifiers.
Instead:
- update Equipment
- recalculate FinalStats from BaseStats
- apply current equipment modifiers
This keeps the stat pipeline deterministic.
Unequipping Becomes Straightforward
To unequip:
1equipment.weapon = nullThen StatsSystem recalculates.
Because final stats begin from base values, the sword bonus disappears naturally.
We do not need to remember:
1finalStats.attack -= 5This is a major advantage of separating equipment state from derived stats.
Item Use Is Different From Equipment
Not every item is equipped.
A health potion might be consumed.
For example:
1healthPotion: {2 name: 'Health Potion',3 type: 'heal',4 amount: 25,5}Using it should not go through the same logic as equipping a sword.
That is why an ItemUseSystem can own consumable behaviour.
Separate Item Use Requests From Item Use Resolution
A useful pipeline is:
Input / Quickbar / Inventory UI
→ item-use request
→ ItemUseSystem
→ validate inventory
→ consume item
→ emit gameplay effect
This keeps input and UI separate from gameplay.
For example:
1this.world.events.emit(2 EVT_USE_ITEM_REQUEST,3 {4 entity: player,5 itemId: 'healthPotion',6 },7)Then ItemUseSystem decides what happens.
A Simple ItemUseSystem
A simplified implementation might contain:
1const requests =2 this.world.events.consume(3 EVT_USE_ITEM_REQUEST,4 )5
6for (const request of requests) {7 const inventory =8 this.world.getComponent(9 request.entity,10 Inventory,11 )12
13 if (!inventory) {14 continue15 }16
17 const amount =18 inventory.items.get(19 request.itemId,20 ) ?? 021
22 if (amount <= 0) {23 continue24 }25
26 const item =27 ItemDatabase[28 request.itemId29 ]30
31 if (!item) {32 continue33 }34
35 if (item.type === 'heal') {36 inventory.items.set(37 request.itemId,38 amount - 1,39 )40
41 this.world.events.emit(42 EVT_HEAL,43 {44 entity:45 request.entity,46 amount:47 item.amount,48 },49 )50 }51}The exact implementation can vary, but the responsibilities are clear.
Healing Belongs in HealSystem
Even after ItemUseSystem determines that a potion should heal, it does not necessarily need to modify Health directly.
It can emit a heal event.
Then:
1export class HealSystem2 extends System {3 update() {4 const events =5 this.world.events.consume(6 EVT_HEAL,7 )8
9 for (const event of events) {10 const health =11 this.world.getComponent(12 event.entity,13 Health,14 )15
16 if (!health) {17 continue18 }19
20 health.current =21 Math.min(22 health.max,23 health.current24 + event.amount,25 )26 }27 }28}Now healing from:
- potions
- spells
- regeneration
- checkpoints
- food
can potentially reuse the same healing pipeline.
Why Not Make the Potion Heal Directly?
Because the item definition should describe the item.
It should not execute gameplay behaviour.
Avoid something like:
1healthPotion: {2 use(player) {3 player.health += 254 },5}That mixes:
- static configuration
- gameplay behaviour
- entity assumptions
A data-driven definition is easier to:
- serialize
- inspect
- balance
- load from JSON
- edit later through tools
Quickbar Is Its Own State
A quickbar is not the same thing as inventory.
The inventory answers:
What items do I own?
The quickbar answers:
What actions have I assigned to these shortcut slots?
A component might look like:
1export class Quickbar {2 constructor(size = 8) {3 this.slots =4 new Array(size).fill(null)5 }6}A slot might reference:
- a consumable item
- an ability
- another usable action
This keeps quickbar configuration separate from item ownership.
Quickbar Slots Can Support Different Action Types
A useful quickbar slot could contain:
1{2 type: 'item',3 id: 'healthPotion',4}or:
1{2 type: 'ability',3 id: 'heavyStrike',4}Now the same bar can support both consumables and abilities.
Input determines which slot was pressed.
Gameplay systems determine what that assigned action means.
Quickbar Input Should Produce Intent
If the player presses key 1, the quickbar system should not necessarily heal the player directly.
Instead, it can inspect the assigned slot.
For an item:
1this.world.events.emit(2 EVT_USE_ITEM_REQUEST,3 {4 entity: player,5 itemId:6 slot.id,7 },8)For an ability, it might produce an ability request.
The quickbar becomes an input mapping layer rather than the owner of item or ability gameplay.
Inventory UI Should Display State
The inventory UI has important responsibilities:
- display items
- show quantities
- show selected item details
- show item modifiers
- show equipped state
- create user requests
But it should not become the authority on gameplay.
The UI reads:
- Inventory
- Equipment
- ItemDatabase
Then presents that information.
If the user clicks Equip, it requests an equip action.
Keep Inventory Toggle and Inventory Rendering Separate
Even inside UI, responsibilities can be separated.
For example:
InventoryToggleSystem
answers:
Should the panel be open?
InventoryUISystem
answers:
What should the panel display?
Those are different responsibilities.
This becomes useful as the UI grows to include:
- sorting
- filters
- comparisons
- tooltips
- crafting
- vendors
- controller navigation
Show Equipped State From Equipment
Do not maintain a second independent UI boolean such as:
1item.isEquipped = trueif Equipment already contains the source of truth.
The UI can check:
1equipment.weapon ===2 itemIdor inspect the other slots.
That prevents gameplay state and UI state from disagreeing.
Item Details Can Read the Definition
When an item is selected, the inventory panel can show information from ItemDatabase.
For example:
Bronze Sword
- Weapon
- Attack +5
The UI does not need to understand how the modifier affects final combat calculations.
It only displays the modifier definition.
StatsSystem remains responsible for applying it.
Inventory Quantities Need Clear Rules
When using a consumable, decide what happens when its quantity reaches zero.
For example:
1const nextAmount =2 amount - 13
4if (nextAmount <= 0) {5 inventory.items.delete(6 itemId,7 )8} else {9 inventory.items.set(10 itemId,11 nextAmount,12 )13}This keeps the inventory clean.
The same helper logic can be centralized inside the relevant inventory system if many features need it.
Don't Let Every System Edit Inventory Arbitrarily
As the game grows, many systems may want to affect inventory:
- pickups
- item use
- crafting
- vendors
- quests
- loot
- rewards
If all of them directly mutate inventory.items, debugging becomes harder.
A stronger architecture may centralize inventory changes through:
InventorySystem- inventory commands
- request events
That gives inventory mutation a clear owner.
Item Removal Can Be a Request Too
For example:
- consume item
- sell item
- craft with item
- drop item
all require inventory quantities to change.
Eventually you may want requests such as:
- add item
- remove item
- move item
- use item
Whether all of these need events depends on the project.
Do not create events merely for the sake of it.
Use them when they help establish a meaningful responsibility boundary.
Loot Drops Fit Into the Same Architecture
Suppose an enemy dies and drops a coin.
The death or loot system can create an entity with:
TransformMeshPickup
For example:
1world.addComponent(2 pickup,3 new Pickup(4 'coin',5 7,6 ),7)When the player collects it, the existing pickup and inventory pipeline handles the result.
The loot system does not need special code for how inventory stores coins.
Random Loot Amounts Belong Before Inventory Storage
Suppose a drop gives between 3 and 8 coins.
The loot system can determine:
1const amount =2 randomInt(3 3,4 8,5 )Then create:
1new Pickup(2 'coin',3 amount,4)InventorySystem receives the final amount and adds it.
This keeps random loot generation separate from inventory storage rules.
Equipment and Inventory Can Share Item Definitions
One benefit of ItemDatabase is that several systems can use the same data.
Inventory UI
reads names and descriptions.
EquipmentSystem
reads equipment type and slot.
StatsSystem
reads modifiers.
ItemUseSystem
reads consumable type and amount.
Loot UI
reads names and icons.
One definition becomes the shared description of the item.
Don't Put UI-Specific State in ItemDatabase
Static item definitions should describe the game item.
Avoid fields such as:
1selected: true2inventoryRow: 43isHovered: falseThose belong to UI state.
Likewise, runtime quantity does not belong in the definition.
ItemDatabase should not say:
1healthPotion: {2 quantity: 33}because every inventory can own a different amount.
Static Definition vs Runtime State
A useful distinction is:
ItemDatabase
- name
- type
- icon
- description
- modifier values
- equipment slot
Inventory
- item ID
- quantity
Equipment
- currently equipped item IDs
Quickbar
- assigned action IDs
Pickup
- item ID
- amount
- collected state
Each type of data has a clear home.
Events Can Connect the Entire Flow
A larger item flow might look like:
World Pickup
→ EVT_PICKUP
→ InventorySystem
→ Inventory changes
Then later:
Inventory UI
→ EVT_EQUIP_ITEM_REQUEST
→ EquipmentSystem
→ Equipment changes
→ Stats recalculate
→ EVT_STATS_CHANGED
→ UI refreshes
Or:
Quickbar Input
→ EVT_USE_ITEM_REQUEST
→ ItemUseSystem
→ item consumed
→ EVT_HEAL
→ HealSystem
→ Health changes
No single system needs to own the entire chain.
System Order Still Matters
If equipment affects combat stats during the same frame, the order should be predictable.
For example:
- resolve equipment changes
- recalculate stats
- resolve combat
- update UI
If combat runs before stats recalculate, it could use the previous equipment values.
This is where inventory architecture connects directly to ECS system order.
Same-Frame vs Next-Frame Requests
Not every inventory action needs to complete immediately.
For example, an equip request might be processed later in the same frame.
Or some structural changes may be deferred until the next update.
What matters is consistency.
If input creates a request during one phase, the resolving systems should run in an order that makes the outcome predictable.
Saving Inventory Becomes Straightforward
Data-focused components are easy to serialize.
For example, Inventory can become:
1{2 healthPotion: 3,3 coin: 27,4 bronzeSword: 1,5}Equipment:
1{2 weapon: 'bronzeSword',3 head: null,4 body: 'leatherArmor',5 accessory: null,6}Quickbar:
1[2 {3 type: 'item',4 id: 'healthPotion',5 },6
7 {8 type: 'ability',9 id: 'heavyStrike',10 },11]These are clean pieces of state.
Derived Stats Do Not Need to Be Saved
Because equipment and BaseStats can regenerate FinalStats, a save system may not need to persist the calculated result.
When loading:
- restore BaseStats
- restore Inventory
- restore Equipment
- restore relevant modifier sources
- recalculate FinalStats
That reduces the chance of stale derived values being saved.
Multiplayer Benefits From Clear Ownership
Inventory is a sensitive gameplay system in multiplayer.
A client should not be able to simply say:
I now have 999 swords.
Instead, an authoritative server can process requests.
For example:
Client
Use potion.
Server
- does the player own it?
- can it be used?
- remove one
- apply effect
- replicate result
The request-and-resolution architecture we've already built fits naturally with that model.
Equipment Requests Work the Same Way
A multiplayer equip request might eventually contain:
1{2 itemId: 'bronzeSword',3}The server validates:
- ownership
- slot
- requirements
- current state
Then updates Equipment.
The client receives the authoritative result.
The UI does not become the authority simply because the player clicked the button.
Item Instances Can Be Added Later
If the game eventually needs unique equipment, the architecture can evolve.
Instead of inventory storing:
1'bronzeSword' -> 1it may store an item instance ID:
1'item_10482'That item instance could contain:
- definition ID:
bronzeSword - durability: 71
- rarity: rare
- bonus attack: 3
Equipment then references the instance rather than only the static definition.
The higher-level separation remains the same.
Stackable and Unique Items Can Coexist
A mature RPG may use:
Stackable item
Health Potion × 8
Unique item
Bronze Sword
Durability 72
Critical +3%
The inventory representation becomes more advanced, but the architecture still separates:
- definitions
- instances
- inventory ownership
- equipment state
- gameplay systems
You do not need that complexity before the game requires it.
Crafting Can Build on Inventory
Crafting may eventually request:
- remove 3 Iron Ore
- remove 1 Wood
- add 1 Iron Sword
A CraftingSystem does not need to become the owner of inventory storage.
It can validate recipes and then request inventory changes.
The existing inventory architecture remains useful.
Vendors Can Build on Inventory Too
A vendor transaction may involve:
- player inventory
- currency
- vendor inventory
- prices
A VendorSystem owns trade rules.
Inventory components continue storing ownership.
This is a good sign that the architecture is composable.
New gameplay features can work with existing state instead of replacing it.
Keep Responsibilities Clear
A useful division is:
Inventory Component
What does this entity own?
Equipment Component
What is currently equipped?
Quickbar Component
What shortcuts are assigned?
Pickup Component
What item exists in the world to collect?
ItemDatabase
What is this item type?
InventorySystem
How does inventory state change?
EquipmentSystem
Can an item be equipped, and where?
ItemUseSystem
Can this item be used, and what gameplay effect should begin?
StatsSystem
How does equipment affect effective stats?
UI Systems
How should all of this state be presented?
That separation keeps one feature from becoming one enormous class.
A Useful Inventory Checklist
When adding an inventory-related feature, ask:
Is this reusable item configuration?
Put it in the item definition.
Is this something an entity currently owns?
Store it in Inventory.
Is this currently equipped?
Store it in Equipment.
Is this assigned to a shortcut?
Store it in Quickbar.
Is this an item in the world?
Use something like Pickup.
Is the player requesting an action?
Create gameplay intent.
Does the action require validation?
Resolve it inside a gameplay system.
Does equipment change stats?
Let StatsSystem recalculate them.
Is this only presentation?
Keep it in the UI layer.
Common Inventory Architecture Mistakes
Letting the UI directly change equipment
The UI should request the action.
Putting item quantities in ItemDatabase
Quantities are runtime inventory state.
Putting gameplay methods inside Inventory
Inventory should remain data-focused.
Modifying FinalStats directly when equipping
Let StatsSystem recalculate from BaseStats.
Hard-coding every item ID inside systems
Prefer item definitions and data-driven rules.
Giving the quickbar its own item quantities
Inventory should remain the ownership source of truth.
Making Pickup modify Inventory directly
Let gameplay systems coordinate the change.
The Bigger Picture
A complete item flow can span several ECS responsibilities.
A sword pickup might move through:
Pickup entity
→ interaction
→ inventory
→ equipment request
→ equipment state
→ StatsSystem
→ FinalStats
→ combat
A potion might move through:
Inventory
→ quickbar
→ use request
→ ItemUseSystem
→ inventory quantity decreases
→ HealSystem
→ Health changes
The systems are separate, but the data flows through a clear pipeline.
Why This Architecture Scales
The biggest benefit is that future features can build on existing boundaries.
We can add:
- loot rarity
- durability
- vendors
- crafting
- item comparison
- equipment requirements
- two-handed weapons
- armor sets
- consumable cooldowns
- drag and drop
- controller navigation
- item instances
- multiplayer validation
without turning Inventory into a giant object that owns every item-related rule.
Each new responsibility can attach to the existing architecture where it belongs.
Conclusion
A good ECS inventory system is not one system.
It is a collection of focused responsibilities working together.
Inventory stores ownership.
Equipment stores equipped items.
Quickbar stores shortcut assignments.
Pickup describes loot in the world.
ItemDatabase describes reusable item definitions.
InventorySystem handles inventory changes.
EquipmentSystem validates equipment actions.
ItemUseSystem resolves consumables.
StatsSystem converts equipment modifiers into effective stats.
UI systems display the state and produce player requests without becoming gameplay authorities.
That gives us a clean flow from world loot all the way to combat:
Pickup
→ Inventory
→ Equipment
→ Stats
→ Gameplay
and another flow for consumables:
Quickbar / Inventory UI
→ Use Request
→ ItemUseSystem
→ Gameplay Effect
The key principle is the same one that appears throughout ECS architecture:
Keep state, configuration, gameplay rules, input intent, and presentation in clearly defined places.
Once those boundaries are established, inventory stops being an isolated feature and becomes a reusable foundation for equipment, loot, consumables, crafting, vendors, abilities, saving, and eventually multiplayer.

Build an RPG stats system in ECS using BaseStats, FinalStats, equipment modifiers, and a StatsSystem that keeps derived combat values predictable.

Learn how to use an Event Bus in ECS to decouple systems, model gameplay requests and outcomes, and avoid tightly coupled system-to-system calls.

Learn why ECS components should stay data-focused, how logic-heavy components create coupling, and why systems are a better place for gameplay behaviour.