Skip to content

Migrating from v1.x to v2.0.0

Welcome to Modular Inventory v2.0.0! This major release introduces an Equipment Manager to automate item logic, a UI State Manager for handling menus, and dynamic slot stacking limits.

Breaking Changes

Upgrading will require updates to your Player Controller, UI toggling logic, and custom Slot Rules.


1. The New EquipmentManager (Automated Item Logic)

In v1.x, you had to manually detect input in your player controller and call on_primary_use() on your item logic. This is no longer required.

v2.0.0 introduces the EquipmentManager node. It automatically listens for input, handles charging/releasing, instantiates 3D weapon models, and updates the active item's logic every frame.

How to migrate:

  1. Add an EquipmentManager node to your Player scene.
  2. In the Inspector, assign your Player node, InventoryComponent, and ModularHotbar.
  3. Delete all custom item-use code (like if Input.is_action_just_pressed("attack"): logic.on_primary_use()) from your player controller script. The EquipmentManager handles it automatically!

2. UIStateManager Replaces Manual UI Toggling

In v1.x, opening a chest required manually toggling visible, calling UICoordinator, and calling InputMode.ui().

v2.0.0 introduces the UIStateManager Autoload. It manages a stack of UI panels, automatically adds a dark background overlay, and can optionally pause the game.

Old Code (v1.x)

func open_chest():
    player_ui.visible = true
    chest_ui.visible = true
    UICoordinator.position_inventory(player_ui, "player")
    UICoordinator.position_inventory(chest_ui, "container", player_ui)
    InputMode.ui()

New Code (v2.0.0)

Instead of placing the chest UI in your scene and toggling it, save your chest UI as a .tscn (PackedScene) and spawn it via the manager:

@export var chest_ui_scene: PackedScene
@export var chest_inventory: InventoryComponent

func open_chest():
    # Ensure player inventory is open first
    UIStateManager.open_panel(player_ui_scene, player_inventory.inventory, "Inventory", "player")

    # Open the chest. The manager handles positioning, background, and mouse mode!
    UIStateManager.open_panel(chest_ui_scene, chest_inventory.inventory, "Chest", "container")
To close all UIs (e.g., pressing Escape), simply call UIStateManager.close_all().


3. ItemLogic Signature Changes

Because the EquipmentManager now handles context, the setup() method for ItemLogic has been expanded to provide more data to your items.

Old Code (v1.x)

func setup(item: ItemDefinition, player: Node) -> void:
    # ...

New Code (v2.0.0)

func setup(item: ItemDefinition, player: Node, slot_index: int = -1, weapon_model: Node3D = null, inventory: Inventory = null) -> void:
    # You now have direct access to the inventory and the 3D weapon model!

Additionally, a new cleanup() method was added. Override it to free any custom nodes or timers you created during setup().


4. Slot Rules & Dynamic Stacking

Slot Rules now support limiting how many items can be added to a slot, rather than just a simple "yes/no" acceptance.

If you wrote custom SlotRule scripts, you must update the signature of can_accept_item to include the amount parameter:

Old Code (v1.x)

func can_accept_item(item: ItemDefinition, slot_index: int, inventory: Inventory) -> bool:

New Code (v2.0.0)

func can_accept_item(item: ItemDefinition, slot_index: int, inventory: Inventory, amount: int = 1) -> bool:

New Feature: You can now override get_max_allowed_amount(item, current_count, inventory) -> int in your rules. For example, the built-in MaxOneRule now uses this to enforce a strict limit of 1 item, even if the item's max_stack_size is 64.


5. UI Component Refactoring (InventoryBinder & SlotGrid)

If you are building custom UI panels, the way slots are generated has been decoupled. - InventoryBinder: A new Control node that automatically finds all SlotUI children and binds them to an inventory. - SlotGrid: A new GridContainer that dynamically generates SlotUI nodes based on a slot_count export.

You no longer need to manually write loops to instantiate SlotUI nodes in your custom panels; just use SlotGrid!

Project Setup

This guide explains how to integrate the Modular Inventory System into your Godot 4.6 project.

Enabling the Plugin

  1. Copy the addons/modular_inventory_system/ folder into your project's res://addons/ directory.
  2. Go to Project > Project Settings > Plugins.
  3. Check the Enable box next to Modular Inventory.

Autoloads

When you enable the plugin, InventoryPlugin.gd automatically registers global nodes that are always available (Autoloads). You do not need to add them manually:

Autoload Description
DragDropSystem Handles global mouse input for dragging items, calculating drop targets, and dropping items into the 3D world.
InputMode Manages mouse capture states (MOUSE_MODE_CAPTURED vs VISIBLE) so your player controller and UI don't fight for mouse control.
UICoordinator Handles the positioning of multiple inventory panels (used automatically by UIStateManager).
UIStateManager (New in v2.0) Manages a stack of UI panels, handles background overlays, game pausing, and quick-move targeting.

Core Nodes

To give a Node (like your Player or a Chest) an inventory, you must attach the InventoryComponent node to it.

To handle item usage (attacks, consumables) automatically, add an EquipmentManager node to your Player.

Accessing the Inventory

You can access the underlying Inventory resource from any script via code:

var inv_comp = $InventoryComponent
var inventory = inv_comp.get_inventory()

Quick Start

Get up and running with the Modular Inventory System in just a few steps.

1. Add InventoryComponent & EquipmentManager

Give your Player an inventory and the ability to use items.

  1. Add an InventoryComponent to your Player. Set Capacity to 20.
  2. Add an EquipmentManager to your Player.
  3. In the EquipmentManager Inspector:
  4. Assign your Player node.
  5. Assign the Main Inventory (InventoryComponent).
  6. Assign your Hotbar (ModularHotbar).

No Manual Input Code Needed!

The EquipmentManager automatically listens for mouse clicks and triggers on_primary_use() on your items. You do not need to write input handling code in your player controller!


2. Create an Item Definition

Items are data-driven using the ItemDefinition resource.

  1. In the FileSystem dock, right-click and select Create New > Resource.
  2. Search for and select ItemDefinition.
  3. Name it apple.tres.
  4. In the Inspector, fill out the properties:
  5. ID: apple
  6. Display Name: Apple
  7. Max Stack Size: 64
  8. Icon: Drag and drop your 2D Apple icon here.

3. Add Items to Inventory

extends CharacterBody3D

@export var starting_apple: ItemDefinition
@onready var inv_comp: InventoryComponent = $InventoryComponent

func _ready():
    inv_comp.inventory_ready.connect(_on_inventory_ready)

func _on_inventory_ready(inv: Inventory):
    if starting_apple:
        inv.add_item(starting_apple, 5)

4. Setup the UI

Inventory Panel

  1. Add a CanvasLayer to your player scene.
  2. Add a ModularInventoryPanel node.
  3. In the Inspector, assign the Grid Container (create a GridContainer child) and the Source Component (your InventoryComponent).
  4. Save this entire UI setup as a PackedScene (e.g., player_inventory.tscn).

Hotbar

  1. Add a ModularHotbar node to your CanvasLayer.
  2. Assign the Slots Container (HBoxContainer) and Source Component.

5. Toggle the UI with UIStateManager

In v2.0, we use the global UIStateManager to open and close inventories. It handles mouse capture and background dimming automatically!

extends CharacterBody3D

@export var player_ui_scene: PackedScene
@export var toggle_action: StringName = &"toggle_inventory"

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed(toggle_action):
        if UIStateManager.has_open_ui():
            UIStateManager.close_all()
        else:
            # Open the player inventory
            var inv = $InventoryComponent.get_inventory()
            UIStateManager.open_panel(player_ui_scene, inv, "Inventory", "player")

        get_viewport().set_input_as_handled()

Next Steps

Item Logic

ItemLogic allows you to attach custom, scriptable behaviors to items. Instead of writing massive match statements in your player controller, you assign a Logic script to an ItemDefinition.

Automatic Execution via EquipmentManager

You do not need to write code in your player controller to trigger item logic! Simply add an EquipmentManager node to your player, assign your ModularHotbar, and it will automatically detect input and call on_primary_use(), on_release(), and update() on the active item.

Base Class: ItemLogic (RefCounted)

All custom logic scripts must extend ItemLogic.

Setup & Teardown

# Called when the logic is initialized. v2.0 provides full context!
func setup(item: ItemDefinition, player: Node, slot_index: int = -1, weapon_model: Node3D = null, inventory: Inventory = null) -> void

# Called when the item is unequipped or the slot changes. Free custom nodes here!
func cleanup() -> void

Execution Methods

# Return false to prevent the item from being used (e.g., out of stamina).
func can_use() -> bool

# Called when the player presses the "Primary Use" button (e.g., Left Click).
func on_primary_use(slot_index: int = -1) -> void

# Called when the player presses the "Secondary Use" button (e.g., Right Click).
func on_secondary_use(slot_index: int = -1) -> void

# Called when the player releases the "Use" button (great for charged attacks).
func on_release() -> void

# Called every frame while the item is being held/used.
func update(delta: float) -> void

# Direct access to raw input events (handled by EquipmentManager).
func on_input(event: InputEvent) -> void

Signals

  • use_started: Emitted when use begins.
  • use_ended: Emitted when use finishes.
  • use_finished(item: ItemDefinition, success: bool): Emitted with the final result.

Built-in Helper

# Automatically reduces durability using the provided inventory context.
func _consume_item_durability(slot_index: int, amount: int = 1) -> bool

Slot Rules

Slot Rules allow you to restrict what items can be placed into specific inventory slots.

Base Class: SlotRule (Resource)

All rules extend SlotRule. In v2.0, rules now support dynamic stacking limits.

# Returns true if the item is allowed in this slot.
# 'amount' is the number of items being dropped/added.
func can_accept_item(item: ItemDefinition, slot_index: int, inventory: Inventory, amount: int = 1) -> bool

# Returns the maximum number of items allowed in this slot (overrides item.max_stack_size).
# Return -1 to use the item's default max_stack_size.
func get_max_allowed_amount(item: ItemDefinition, current_count: int, inventory: Inventory) -> int

# Returns a human-readable string explaining why the item was rejected.
func get_rejection_reason(item: ItemDefinition, slot_index: int) -> String

Built-in Rules

EquipmentTypeRule

Locks a slot to a specific integer equipment_type.

ItemTagRule

Requires items to have specific string tags.

MaxOneRule

Prevents items from stacking in a specific slot.

Dynamic Limits

MaxOneRule now implements get_max_allowed_amount() to return 1. This means even if you drag a stack of 64 swords onto an equipment slot, the system will only allow 1 to be placed, preventing illegal stacking.

Containers & Chests

Creating an interactive container (like a chest) in v2.0 is handled entirely by the UIStateManager. You no longer need to manually toggle visibility or manage mouse states.

Step-by-Step Setup

1. Physical Container & Inventory

  1. Create a StaticBody3D or RigidBody3D for your chest.
  2. Add an InventoryComponent as a child. Set the Capacity.

2. Create the Chest UI Scene

  1. Create a new Scene (e.g., chest_ui.tscn).
  2. Add a ModularInventoryPanel (or use SlotGrid + InventoryBinder).
  3. Do not assign the source_component in the Inspector. The UIStateManager will bind the inventory dynamically at runtime.
  4. Save this scene.

3. Interaction Logic

When the player presses "E", use the UIStateManager to open both the player and chest inventories.

@export var player_ui_scene: PackedScene
@export var chest_ui_scene: PackedScene
@export var player_inv: InventoryComponent
@export var chest_inv: InventoryComponent

func open_chest():
    # 1. Open Player Inventory (Role: "player" -> anchors to the right)
    UIStateManager.open_panel(
        player_ui_scene, 
        player_inv.inventory, 
        "Inventory", 
        "player"
    )

    # 2. Open Chest Inventory (Role: "container" -> anchors to the left)
    UIStateManager.open_panel(
        chest_ui_scene, 
        chest_inv.inventory, 
        "Chest", 
        "container"
    )

    # The UIStateManager automatically:
    # - Calls InputMode.ui() to show the mouse cursor.
    # - Adds a dark background overlay.
    # - Tells UICoordinator to position them side-by-side.

4. Closing the Container

To close the container, simply call close_all().

func close_chest():
    UIStateManager.close_all()
    # The UIStateManager automatically:
    # - Frees the UI panels.
    # - Calls InputMode.game() to capture the mouse.
    # - Removes the background overlay.

Quick Moving Items

Because both panels are registered with the UIStateManager, players can now Shift+Left-Click items to instantly transfer entire stacks between the chest and their inventory!

(Note: In v2.0, Right-clicking no longer quick-moves; instead, it starts a drag operation picking up exactly 1 item).

UI Coordinator

Managed by UIStateManager

In v2.0, you rarely need to call UICoordinator directly. The UIStateManager automatically calls it behind the scenes whenever you open or close panels to ensure they are arranged perfectly side-by-side.

How it Works

UICoordinator looks at the ui_role metadata assigned to panels by the UIStateManager and applies anchor presets.

Role Behavior
"player" Anchors the UI to the right side of the screen.
"container" Anchors to the left side if a player UI is open. Otherwise, centers itself.
"default" Places the UI dead center in the middle of the screen.

Manual API (For Custom UI Systems)

If you are building your own UI manager and need to position panels manually, you can use:

# Arrange an array of open panels automatically
UICoordinator.arrange_panels(panels_array)

# Position a specific panel manually
UICoordinator.position_panel(panel_control, "primary") # "primary", "secondary", "centered"

UI Components Overview

The UI system is highly decoupled. Visuals update based on signals from the Inventory resource.

Core Components

  1. ModularInventoryPanel: A window that shows all inventory slots in a grid.
  2. ModularHotbar: A horizontal row of slots for quick access.
  3. SlotUI: A single slot showing the item's picture, stack count, and durability bar.
  4. ItemTooltip: A popup window showing formatted BBCode text.

Advanced UI Building (v2.0)

If you want to build completely custom UI layouts (e.g., a character paperdoll screen), v2.0 introduces two helper nodes:

InventoryBinder

A Control node that automatically searches its children for SlotUI nodes and binds them to an Inventory. It also handles tooltip routing and quick-move logic.

SlotGrid

A GridContainer that dynamically instantiates SlotUI nodes based on a slot_count export.

Usage: 1. Add InventoryBinder to your custom UI. 2. Add SlotGrid as a child. 3. Call binder.bind_inventory(inventory_resource) via code, or let the UIStateManager handle it automatically when opening the panel.

API Reference

New Autoloads & Managers

UIStateManager

Extends: Node (Autoload singleton) Manages a stack of UI panels, background overlays, and game pausing.

Methods:

Method Description
open_panel(scene: PackedScene, inv: Inventory, title: String, role: String, type: UIType) -> Control Instantiates a UI panel, binds the inventory, and arranges it.
close_all() -> void Closes all open panels, restores mouse capture, and removes overlays.
close_top_ui() -> void Closes only the most recently opened panel.
get_other_inventory(current_inv: Inventory) -> Inventory Finds the second open inventory (used for Quick Move).
has_open_ui() -> bool Returns true if any panels are open.

EquipmentManager

Extends: Node
Automates item logic execution and 3D model equipping.

Properties:

Property Type Description
player Node3D The player node.
main_inventory InventoryComponent The player's inventory.
hotbar ModularHotbar The hotbar to track active items from.
item_socket Marker3D Optional 3D node to attach weapon models to.

Signals:

Signal Description
active_item_changed(item: ItemDefinition, slot_index: int) Emitted when the player scrolls to a new hotbar slot.

Updated Core Classes

Inventory

Updated Method:

Method Description
can_accept_at_slot(item: ItemDefinition, slot_index: int, amount: int = 1) -> bool Checks if slot accepts item, factoring in dynamic rule limits.

ItemLogic (base class)

Updated Setup Signature:

Method Description
setup(item: ItemDefinition, player: Node, slot_index: int = -1, weapon_model: Node3D = null, inventory: Inventory = null) -> void Called with full context by the EquipmentManager.
cleanup() -> void Called when the item is unequipped. Override to free custom nodes.
on_input(event: InputEvent) -> void Raw input event passed from EquipmentManager.

SlotRule (base class)

Updated Methods:

Method Description
can_accept_item(item: ItemDefinition, slot_index: int, inventory: Inventory, amount: int = 1) -> bool Now includes the amount being dropped.
get_max_allowed_amount(item: ItemDefinition, current_count: int, inventory: Inventory) -> int (New) Returns the max stack size allowed by this specific rule. Return -1 to defer to the item's default.

New UI Components

InventoryBinder

Extends: Control
Automatically binds child SlotUI nodes to an inventory and handles tooltips/quick-moves.

Method: bind_inventory(inv: Inventory) -> void

SlotGrid

Extends: GridContainer
Dynamically generates SlotUI nodes.

Properties: slot_scene, start_index, slot_count.
Method: bind_inventory(inv: Inventory) -> void