Skip to content

Tag System

Stack Inventory includes a powerful hierarchical tag system for categorizing and filtering items.

Tags are labels you attach to items to group them by category, type, or behavior. They enable:

  • Categorization - Group items (Weapon, Consumable, Quest Item)
  • Filtering - Find items by tag (all weapons, all consumables)
  • Behavior - Apply logic based on tags (equippable items, stackable items)
  • Hierarchy - Organize tags in parent/child relationships
using StackInventory.Core.Items;
// Simple tag
var weaponTag = new ItemTag("Weapon");
var consumableTag = new ItemTag("Consumable");
var questTag = new ItemTag("Quest");

Tags support hierarchy using dot notation:

// Weapon hierarchy
var meleeTag = new ItemTag("Weapon.Melee");
var rangedTag = new ItemTag("Weapon.Ranged");
// Weapon.Melee hierarchy
var swordTag = new ItemTag("Weapon.Melee.Sword");
var axeTag = new ItemTag("Weapon.Melee.Axe");
// Weapon.Ranged hierarchy
var bowTag = new ItemTag("Weapon.Ranged.Bow");
var gunTag = new ItemTag("Weapon.Ranged.Gun");

Hierarchy structure:

Weapon
├── Melee
│ ├── Sword
│ ├── Axe
│ └── Hammer
└── Ranged
├── Bow
├── Gun
└── Crossbow

In the Godot Inspector:

  1. Create/open an ItemDefinition resource
  2. Find the Tags array property
  3. Click Add Element
  4. Set the Id field (e.g., “Weapon.Melee.Sword”)
using StackInventory.Core.Items;
// Create tags
var tags = new List<ItemTag>
{
new ItemTag("Weapon"),
new ItemTag("Weapon.Melee"),
new ItemTag("Weapon.Melee.Sword"),
new ItemTag("Equippable"),
new ItemTag("Rare")
};
// Create item with tags
var item = new Item(
id: Guid.NewGuid(),
name: "Iron Sword",
value: 100f,
tags: tags,
stackMaximum: 1
);
// Check exact tag
if (item.HasTag("Weapon"))
{
GD.Print("This is a weapon!");
}
// Check hierarchical tag
if (item.HasTag("Weapon.Melee"))
{
GD.Print("This is a melee weapon!");
}
// Check specific tag
if (item.HasTag("Weapon.Melee.Sword"))
{
GD.Print("This is specifically a sword!");
}
var weaponTag = new ItemTag("Weapon");
if (item.HasTag(weaponTag))
{
GD.Print("Item has weapon tag");
}

The tag system supports hierarchical matching:

var sword = new Item(
Guid.NewGuid(),
"Iron Sword",
100f,
tags: new List<ItemTag> { new("Weapon.Melee.Sword") },
stackMaximum: 1
);
// All of these return TRUE:
sword.HasTag("Weapon"); // ✓ Parent tag
sword.HasTag("Weapon.Melee"); // ✓ Parent tag
sword.HasTag("Weapon.Melee.Sword"); // ✓ Exact tag
// This returns FALSE:
sword.HasTag("Weapon.Ranged"); // ✗ Different branch

Rule: An item with tag A.B.C automatically matches A and A.B.

// Armor
"Armor"
"Armor.Head" // Helmet
"Armor.Chest" // Chestplate
"Armor.Legs" // Leggings
"Armor.Feet" // Boots
// Weapons
"Weapon"
"Weapon.Melee"
"Weapon.Ranged"
"Weapon.Magic"
// Accessories
"Accessory"
"Accessory.Ring"
"Accessory.Amulet"
// Consumables
"Consumable"
"Consumable.Potion"
"Consumable.Food"
"Consumable.Scroll"
// Resources
"Resource"
"Resource.Ore"
"Resource.Wood"
"Resource.Herb"
// Quest
"Quest"
"Quest.Key"
"Quest.Document"
// Functionality
"Equippable" // Can be equipped
"Consumable" // Can be consumed/used
"Stackable" // Can stack in inventory
"Tradeable" // Can be sold/traded
"Droppable" // Can be dropped in world
// Rarity
"Rarity.Common"
"Rarity.Uncommon"
"Rarity.Rare"
"Rarity.Epic"
"Rarity.Legendary"
// Binding
"BindOnPickup" // Cannot trade once picked up
"BindOnEquip" // Cannot trade once equipped
"Soulbound" // Cannot trade at all
public partial class Inventory : Node
{
private List<(IItem item, int amount)> _items = new();
public List<IItem> GetItemsWithTag(string tagId)
{
return _items
.Where(i => i.item.HasTag(tagId))
.Select(i => i.item)
.ToList();
}
public List<IItem> GetWeapons()
{
return GetItemsWithTag("Weapon");
}
public List<IItem> GetConsumables()
{
return GetItemsWithTag("Consumable");
}
}
public partial class InventoryUI : Control
{
private string _currentFilter = "";
public void ShowWeapons()
{
_currentFilter = "Weapon";
RefreshDisplay();
}
public void ShowArmor()
{
_currentFilter = "Armor";
RefreshDisplay();
}
public void ShowAll()
{
_currentFilter = "";
RefreshDisplay();
}
private void RefreshDisplay()
{
var items = string.IsNullOrEmpty(_currentFilter)
? _inventory.GetAllItems()
: _inventory.GetItemsWithTag(_currentFilter);
// Update UI to show filtered items
}
}
// Item must have ALL tags
public bool HasAllTags(IItem item, params string[] tagIds)
{
return tagIds.All(tagId => item.HasTag(tagId));
}
// Example: Find equippable weapons
if (HasAllTags(item, "Weapon", "Equippable"))
{
GD.Print("Can equip this weapon");
}
// Has one tag but not another
public bool HasTagButNotTag(IItem item, string hasTag, string notTag)
{
return item.HasTag(hasTag) && !item.HasTag(notTag);
}
// Example: Tradeable quest items (not soulbound)
if (HasTagButNotTag(item, "Quest", "Soulbound"))
{
GD.Print("Can trade this quest item");
}
// Add tag at runtime (if using Item POCS)
var item = new Item(...);
item.Tags.Add(new ItemTag("Enchanted"));
// Check for enchantments
if (item.HasTag("Enchanted"))
{
ApplyEnchantmentEffects(item);
}
  • Use hierarchy - Weapon.Melee.Sword instead of Sword
  • Be consistent - Decide on naming convention early
  • Use PascalCase - Weapon.Melee not weapon.melee
  • Group logically - Related items under same parent
  • Document tags - Keep a list of all tag patterns
  • Mix conventions - Don’t use both Weapon.Melee and MeleeWeapon
  • Over-tag - Don’t add every possible tag, be selective
  • Use too many levels - More than 3-4 levels gets confusing
  • Include spaces - Use Weapon.Melee not Weapon.Melee Sword
  • Hard-code - Store tag constants for reuse

Create a static class for tag constants:

public static class ItemTags
{
// Weapons
public const string Weapon = "Weapon";
public const string WeaponMelee = "Weapon.Melee";
public const string WeaponMeleeSword = "Weapon.Melee.Sword";
public const string WeaponMeleeAxe = "Weapon.Melee.Axe";
public const string WeaponRanged = "Weapon.Ranged";
public const string WeaponRangedBow = "Weapon.Ranged.Bow";
// Armor
public const string Armor = "Armor";
public const string ArmorHead = "Armor.Head";
public const string ArmorChest = "Armor.Chest";
// Consumables
public const string Consumable = "Consumable";
public const string ConsumablePotion = "Consumable.Potion";
public const string ConsumableFood = "Consumable.Food";
// Behavior
public const string Equippable = "Equippable";
public const string Stackable = "Stackable";
public const string Tradeable = "Tradeable";
}
// Usage
if (item.HasTag(ItemTags.WeaponMelee))
{
// Handle melee weapon
}
using StackInventory.Core.Items;
public class ItemFactory
{
public static Item CreateIronSword()
{
return new Item(
id: Guid.NewGuid(),
name: "Iron Sword",
value: 100f,
tags: new List<ItemTag>
{
new(ItemTags.Weapon),
new(ItemTags.WeaponMelee),
new(ItemTags.WeaponMeleeSword),
new(ItemTags.Equippable),
new("Rarity.Common")
},
stackMaximum: 1,
iconPath: "res://icons/weapons/iron_sword.png",
tooltip: "A sturdy iron sword. Reliable in combat."
);
}
public static Item CreateHealthPotion()
{
return new Item(
id: Guid.NewGuid(),
name: "Health Potion",
value: 50f,
tags: new List<ItemTag>
{
new(ItemTags.Consumable),
new(ItemTags.ConsumablePotion),
new(ItemTags.Stackable),
new("Rarity.Common")
},
stackMaximum: 99,
iconPath: "res://icons/consumables/health_potion.png",
tooltip: "Restores 50 HP when consumed."
);
}
}