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 (ItemPropertyentries — 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: everyItemDefinitionandRarityDefinitionin the game, loaded from aResourcesfolder by the exact name"ItemDatabase"(same conventionCalendarConfiguses).ItemIndex(pure C#) — the O(1) id/tag/rarity lookup index over anItemDatabase's item list. Built from plain data (anIEnumerable<ItemDefinition>), so it works identically whether those definitions came fromResources.Loador fromScriptableObject.CreateInstancein 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 anItemDatabaseand itsItemIndex. Read-only — see "Why no save domain / events" below. - Editor —
Tools/LoLGameplayKit/Validate Item Database: a menu item that finds everyItemDatabaseasset in the project and logsValidate()'s issues to the Console.
Quickstart¶
- Create an
ItemDatabaseasset (Create → LoLGameplayKit → Items → Item Database), populate itsItems/Raritieslists withItemDefinition/RarityDefinitionassets, and place it in aResourcesfolder under the exact nameItemDatabase. - Leave
enableItemson in yourGameplayKitConfigasset (also in aResourcesfolder, namedGameplayKitConfig). - That's it — registration is automatic. The module ships
ItemsServiceRegistrar, anIServiceRegistrarthe engine's initializer discovers by assembly scan and constructs parameterlessly after all engine services are up. The registrar loads both config assets fromResources, checks the toggle, constructsItemDatabaseService, callsInitialize()itself (the engine does not callInitialize/LateInitialize/Shutdownon externally registered services), and registersIItemDatabaseService. - 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.
ItemDefinitionis 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 onItemDefinitionas 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/ItemPropertyKindare 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 intoLoLEngine-GameplayKit-Coreis a reasonable candidate once a third consumer needs the same shape (the Kit's rule of two). - Rarity is data, not an enum.
RarityDefinitionis itself content — a project defines as many or as few tiers as it wants, reorders them viaSortOrder, and reskins them viaTint, 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.
LegalityClassis 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 asWorldFlagStore.Get*. UseTryGetPropertyKind/TryGet*Propertywhen the distinction matters. - Validation is pure data, logged at two altitudes. Every validatable type has a pure
Validate()returning issue strings (noDebug.Loginside it) — this is what makes validation testable without an Editor.OnValidate()on each SO calls its ownValidate()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). TheTools/LoLGameplayKit/Validate Item Databasemenu item runs this pass on everyItemDatabasein the project on demand. - Headless discipline, with one honest caveat. Unlike
WorldFlagStore/CalendarModel(zeroUnityEnginereferences),ItemIndexcannot avoid referencingItemDefinitionitself, which is necessarily aScriptableObject— the task's own required shape, since content authors need Inspector editing. WhatItemIndexdoes keep headless: noServiceLocator, noResources, no save/event dependencies, noAssetDatabase. It is constructed from a plainIEnumerable<ItemDefinition>and behaves identically whether those instances came fromResources.Loador fromScriptableObject.CreateInstancewith nothing on disk — which is what makesItemsTests.csfully 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.Registerthrows 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.ModelDbis global, static, mutable, process-wide state, cleared only viaNeverEverCallThisOutsideOfTests_Clear()(a method whose name is a deliberate warning sign). RoutingItemDatabase/ItemDefinitionthrough it would mean every edit-mode test that builds an in-memory database (seeItemsTests.cs, which creates multiple databases reusing ids like"sword"across test cases) would collide onModelDb'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.ModelIdis a two-partCategory/Entrystruct withAbstractModel's canonical/mutable-clone machinery layered on top (MutableClone,AssertCanonical/AssertMutable). None of that applies toItemDefinition— 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).ModelDboffers 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 throughModelDbdoesn't duplicate anythingModelDbalready provides beyond a bareDictionary<string, T>, whichItemIndexneeds 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).