Skip to content

Items (ItemDefs-lite)

Static, data-driven item content: ItemDefinition (stable id, LocString name/description, tags, rarity, base value, stack rule, a deferred-semantics legality class, and a typed custom-property bag) plus ItemDatabase, the flat content asset holding every item and rarity in the game. Doc 07 scopes this module explicitly as "ItemDefs-lite" — definitions only, no runtime item instances (quality, freshness, durability, individual stack counts are all T2/T3; see "Deliberately deferred" below).

What it is

  • ItemDefinition (ScriptableObject) — one item's content: id, display name/description LocStrings, rarity reference, tags, base value, max stack size, a legality-class string, and a typed custom-property bag (ItemProperty entries — bool/int/float/string, keyed by string).
  • RarityDefinition (ScriptableObject) — one rarity tier: id, display name LocString, UI sort order, tint color. Data-driven — there is no hardcoded rarity enum anywhere in this module.
  • ItemDatabase (ScriptableObject) — the flat content asset: every ItemDefinition and RarityDefinition in the game, loaded from a Resources folder by the exact name "ItemDatabase" (same convention CalendarConfig uses).
  • ItemIndex (pure C#) — the O(1) id/tag/rarity lookup index over an ItemDatabase's item list. Built from plain data (an IEnumerable<ItemDefinition>), so it works identically whether those definitions came from Resources.Load or from ScriptableObject.CreateInstance in an edit-mode test with nothing on disk. See "Headless discipline" below for the one place this differs from WorldFlags'/Calendar's headless cores.
  • Service layer — IItemDatabaseService / ItemDatabaseService: thin glue that wraps an ItemDatabase and its ItemIndex. Read-only — see "Why no save domain / events" below.
  • Editor — Tools/LoLGameplayKit/Validate Item Database: a menu item that finds every ItemDatabase asset in the project and logs Validate()'s issues to the Console.

Quickstart

  1. Create an ItemDatabase asset (Create → LoLGameplayKit → Items → Item Database), populate its Items/Rarities lists with ItemDefinition/RarityDefinition assets, and place it in a Resources folder under the exact name ItemDatabase.
  2. Leave enableItems on in your GameplayKitConfig asset (also in a Resources folder, named GameplayKitConfig).
  3. That's it — registration is automatic. The module ships ItemsServiceRegistrar, an IServiceRegistrar the engine's initializer discovers by assembly scan and constructs parameterlessly after all engine services are up. The registrar loads both config assets from Resources, checks the toggle, constructs ItemDatabaseService, calls Initialize() itself (the engine does not call Initialize/LateInitialize/Shutdown on externally registered services), and registers IItemDatabaseService.
  4. Resolve and use it from any game system:
var items = ServiceLocator.Instance.Get<IItemDatabaseService>();

if (items.TryGetItem("iron_sword", out var sword))
{
    string name = ServiceLocator.Instance.Get<ILocalizationService>().Format(sword.DisplayName);
    int damage = sword.GetIntProperty("damage", defaultValue: 0); // never throws
}

var weapons = items.GetItemsByTag("weapon");
var commonItems = items.GetItemsByRarity("common");

See Samples/Scripts/ItemsSample.cs for the full pattern, including reading typed custom properties and running validation on demand.

API overview

ItemDefinition

Member Notes
Id Stable data key. Unique across the owning ItemDatabase (enforced by Validate(), not by the type system).
DisplayName, Description LocStrings assembled from raw table/key strings held on the asset — same pattern as CalendarConfig.PhaseDefinition. Resolve via ILocalizationService.Format.
Rarity A RarityDefinition reference, or null if the item has no rarity concept.
Tags Free-form strings. No separate tag registry — any non-empty string is a valid tag.
BaseValue int, must not be negative.
MaxStack, IsStackable MaxStack of 1 means unstackable; IsStackable is MaxStack > 1.
LegalityClass Free-form string. See "Legality class" below — this module does not interpret it.
Properties The typed custom-property bag, in Inspector order.
GetBoolProperty/GetIntProperty/GetFloatProperty/GetStringProperty(key, default) Never throws — returns default for a missing key or a key held under a different kind. Mirrors WorldFlagStore's Get* shape.
TryGetBoolProperty/TryGetIntProperty/TryGetFloatProperty/TryGetStringProperty(key, out value) false for missing/wrong-kind.
HasProperty(key), TryGetPropertyKind(key, out kind) Existence/kind checks, mirroring WorldFlagStore.Has/TryGetKind.
Validate() Pure — returns issue strings, no Unity logging. OnValidate calls this and logs each issue.

RarityDefinition

Member Notes
Id Stable data key, referenced by ItemDefinition.Rarity and used as the key for IItemDatabaseService.GetItemsByRarity.
DisplayName LocString, same raw table/key pattern as ItemDefinition.
SortOrder UI ordering hint (lower sorts first) — no gameplay meaning.
Tint Color a UI may use for borders/text/icons.
Validate() Same pure-issues-list pattern as ItemDefinition.Validate().

ItemDatabase

Member Notes
Items, Rarities The flat content lists, in Inspector order.
Index The ItemIndex over Items, built lazily on first access and cached for the asset instance's lifetime.
Validate() Aggregates every item's/rarity's own Validate() (prefixed with which asset it came from) plus database-level checks a single asset can't see alone: duplicate item ids, duplicate rarity ids, null list entries, and items whose Rarity reference isn't in this database's Rarities list ("dangling" — a valid asset, just not registered here).

IItemDatabaseService

Mirrors ItemIndex's query surface (AllItems, TryGetItem/GetItem, GetItemsByTag, GetItemsByRarity) plus rarity lookup (AllRarities, TryGetRarity/GetRarity) and Validate() (delegates to the wrapped ItemDatabase). Game code almost never touches ItemIndex directly — it's an implementation detail of the service and of ItemDatabase.Index, exposed publicly for testability.

Key design decisions

  • Definitions, not instances. ItemDefinition is content, not runtime state. There is no "item instance" here — no per-pickup quality/freshness/durability, no individual stack counts. A future inventory/instance module builds on ItemDefinition as read-only reference data; this module's entire job is to be the thing that module points at.
  • Typed custom-property bag mirrors Flags, but isn't shared with it. ItemProperty/ ItemPropertyKind are the same shape as Flags' WorldFlagRecord/FlagKind (a bool/int/float/ string tagged union keyed by string) — deliberately, since it's the same underlying idea. They are not shared types: Items may not reference the Flags assembly (the Kit's module topology is a DAG with no sideways edges between Tier-1 siblings — see doc 07's dependency diagram). Consolidating a shared typed-value type into LoLEngine-GameplayKit-Core is a reasonable candidate once a third consumer needs the same shape (the Kit's rule of two).
  • Rarity is data, not an enum. RarityDefinition is itself content — a project defines as many or as few tiers as it wants, reorders them via SortOrder, and reskins them via Tint, all without touching code.
  • Tags have no registry. Any non-empty string is a valid tag. Validate() flags empty and duplicate tags within one item, but there is no project-wide tag vocabulary to validate against — that's explicitly out of scope for "lite."
  • Legality class: field now, semantics later. LegalityClass is a free-form string this module stores and never interprets — no enum, no allowed-value set, no enforcement. Doc 07 calls this out explicitly: the field exists so downstream content doesn't need a schema migration when a Tier-3 legality/contraband system arrives; what "legal" even means is that system's decision, not this one's.
  • Reads never throw. GetBoolProperty/GetIntProperty/etc. return the supplied default for both "key never set" and "key set under a different kind" — same contract as WorldFlagStore.Get*. Use TryGetPropertyKind/TryGet*Property when the distinction matters.
  • Validation is pure data, logged at two altitudes. Every validatable type has a pure Validate() returning issue strings (no Debug.Log inside it) — this is what makes validation testable without an Editor. OnValidate() on each SO calls its own Validate() and logs each issue with the asset's name; ItemDatabase.Validate() additionally aggregates every item's and rarity's issues (prefixed with which asset they came from) plus the database-level checks no single asset can see on its own (duplicate ids, dangling rarity references). The Tools/LoLGameplayKit/Validate Item Database menu item runs this pass on every ItemDatabase in the project on demand.
  • Headless discipline, with one honest caveat. Unlike WorldFlagStore/CalendarModel (zero UnityEngine references), ItemIndex cannot avoid referencing ItemDefinition itself, which is necessarily a ScriptableObject — the task's own required shape, since content authors need Inspector editing. What ItemIndex does keep headless: no ServiceLocator, no Resources, no save/event dependencies, no AssetDatabase. It is constructed from a plain IEnumerable<ItemDefinition> and behaves identically whether those instances came from Resources.Load or from ScriptableObject.CreateInstance with nothing on disk — which is what makes ItemsTests.cs fully edit-mode-testable without any asset files.

Deliberately deferred (per doc 07)

  • Item instances: quality, freshness, durability, per-stack individual state (T2).
  • Weight/volume capacity math for inventories (T2).
  • Legality class semantics — the field exists now; what it means is T3.
  • CSV importer — game-side first per the Vertical Slice plan, extracted to the Kit at the rule of two (a second game needing the identical importer).

Why no save domain / no events

Every other Tier-1 module obeys the Kit's four contracts, including save and events — Items is the one exception, and deliberately so: ItemDefinition/RarityDefinition/ItemDatabase are static content assets. Nothing about them changes at runtime, so there is nothing to capture into a save domain and nothing to notify listeners about. (Contrast WorldFlagsSaveData/ WorldFlagChangedEvent, which exist because flag values change during play.) A future module that models item instances/inventories is where save participation and change events for item state will live; this module only needs to exist reliably underneath it.

ModelDb integration verdict

Doc 07 describes ItemDatabase as built "over the core's ModelDb." Having read LoLEngine.Runtime.Models.ModelDb/AbstractModel/ModelId in the engine, this module does not route item storage through ModelDb, for concrete reasons rather than convenience:

  • ModelDb.Register throws on a duplicate id (InvalidOperationException). That's the right call for its actual job — a canonical bootstrap registry where a duplicate is a hard programmer error — but it's hostile to this module's authoring-time discipline, where a duplicate item id is a logged validation issue (Validate()/OnValidate), not a crash, so content authors can see and fix it without a domain reload.
  • ModelDb is global, static, mutable, process-wide state, cleared only via NeverEverCallThisOutsideOfTests_Clear() (a method whose name is a deliberate warning sign). Routing ItemDatabase/ItemDefinition through it would mean every edit-mode test that builds an in-memory database (see ItemsTests.cs, which creates multiple databases reusing ids like "sword" across test cases) would collide on ModelDb's duplicate-id throw unless every test remembered to clear the global registry first — a footgun this module's actual tests (fully isolated, ScriptableObject.CreateInstance-based, no assets on disk) are built to avoid.
  • ModelId is a two-part Category/Entry struct with AbstractModel's canonical/mutable-clone machinery layered on top (MutableClone, AssertCanonical/AssertMutable). None of that applies to ItemDefinition — there is no mutable runtime clone of an item definition in this module's scope (that's the instance layer, explicitly T2/T3, see above).
  • ModelDb offers id lookup only — no tag or rarity indexing. The actual "runtime lookup by id (O(1)), by tag, by rarity" requirement this module needs is net-new either way; not routing through ModelDb doesn't duplicate anything ModelDb already provides beyond a bare Dictionary<string, T>, which ItemIndex needs regardless as the foundation its tag/rarity indexes are built next to.

ItemIndex's internal Dictionary<string, ItemDefinition> is therefore a small, purpose-built store for this module's actual query needs, not a duplication of ModelDb's mission (a canonical content registry with mutable-clone runtime instances). If the Kit later adopts ModelDb as a general content-registration substrate Kit-wide — e.g. so any system can ModelDb.GetAll<T>() across item/contract/deduction content uniformly — that's a decision above a single module's scope, and this module's ItemDatabase/ItemIndex would need to change together with that decision, not ahead of it.

Seam ledger

Per the Kit's portability discipline (doc 07, "Portability discipline"), every module's docs page lists exactly which engine surface it consumes.

Engine surface Namespace Used for
ILoLEngineService LoLEngine.Core.ServiceManagement.Interfaces Service lifecycle (Initialize/Shutdown) on IItemDatabaseService.
LocString, LocTable LoLEngine.Core.Localization.LocString ItemDefinition.DisplayName/Description, RarityDefinition.DisplayName.
UnityEngine.ScriptableObject, SerializeField, CreateAssetMenu UnityEngine ItemDefinition/RarityDefinition/ItemDatabase are all content assets.
UnityEngine.Color UnityEngine RarityDefinition.Tint.
UnityEngine.Debug UnityEngine OnValidate issue logging.
UnityEngine.Resources UnityEngine ItemsServiceRegistrar loading GameplayKitConfig/ItemDatabase.

Unlike WorldFlagService/CalendarService, ItemDatabaseService consumes no IEventManager/ISaveSystem — see "Why no save domain / no events" above.

ItemDatabaseValidation (editor) additionally consumes UnityEditor.AssetDatabase/MenuItem — expected for an editor-only tool, out of scope for the portability discipline (which concerns runtime code only).