Sidequest Hero Documentation
Game infrastructure toolkit for indie developers and small studios.
How-To Guides
Practical guides generated from tested scenarios — if the docs say it works this way, there’s a test proving it does.
Concepts
Explanations of core concepts: how the system works and why it’s designed this way.
Unity API Reference
C# scripting API reference for Unity developers. (Coming soon)
How field mutability works
Definition-level fields are immutable on instances. Instance-level fields are mutable per-instance.
Create instance of unregistered type fails
If you try to create an item instance of a type that hasn’t been registered — such as “nonexistent” — the operation will fail with a type not found error. You must register a type definition before you can create instances of it.
Read definition-level field from instance
When you create an item instance of type “sword”, you can read its definition-level fields directly from the instance. For example, accessing the “damage” field on a sword instance returns the value 10.0 defined by the item type. Because this field is defined at the definition level, it remains immutable on the instance — every sword shares the same base damage value.
Set definition-level field fails
If you have an item instance of type “sword” and attempt to set the “damage” field to 20.0 directly on that instance, the operation will fail with a field immutable error. Definition-level fields like damage are set on the item definition and cannot be changed on individual instances — this protects shared properties from accidental modification.
Set field with wrong type fails
When you have an item instance — say, a “sword” — and you attempt to set a mutable field like “durability” to a value of the wrong type (for example, the string “high” instead of a number), the operation fails with a type mismatch error. Sidequest Hero enforces field types at runtime, so instance fields must always match the type defined in their item definition.
Set instance-level field succeeds
Instance-level fields can be freely modified on individual item instances. When you set the “durability” field to 80 on a “sword” instance, the instance stores that value directly — reading it back returns 80. This is how per-instance state like durability, ammo count, or charge level works: each instance tracks its own value independently.
Set nonexistent field fails
If you try to set a field that doesn’t exist on an item — for example, setting “magic” to 5 on a “sword” instance that has no such field defined — the operation fails with a field not found error. You can only set fields that have been declared in the item’s type definition.
Generated from core/tests/features/field_mutability.feature
How item identity works
Each item type in a container has a unique instance identity. Adding the same item type again stacks (increases quantity).
Adding the same item type stacks quantity
When you add the same item type to a container more than once, the system stacks them into a single entry rather than creating duplicates. For example, adding 1 “iron_sword” to an unlimited-capacity inventory and then adding another 1 “iron_sword” results in just 1 item in the container with a quantity of 2.
Different item types are separate instances
When you add different item types to the same container, each one becomes a separate instance with its own unique identity. For example, adding 1 “iron_sword” and 1 “steel_sword” to an unlimited-capacity inventory results in 2 distinct items, each with a different identity. This ensures that different item types are never merged together, even though they live in the same container.
Removed item can be identified by its instance
When you remove a specific item instance from a container, the operation succeeds and the removed item still carries a reference back to its original definition. For example, if you add 1 “iron_sword” to an unlimited-capacity “inventory” and then remove that specific instance, you can inspect the returned item and confirm it references the “iron_sword” definition. This lets you track and identify items even after they leave a container.
Generated from core/tests/features/item_identity.feature
How item stacking works
Items stack up to their type’s max_stack value. Default max_stack is 1 (unstackable). When a stack is full, a new stack is created (if capacity allows).
Exceed max_stack creates a new stack
When a stack reaches its maximum size, adding more of the same item automatically creates a new stack. If you have a container “bag” with unlimited capacity and already hold 20 “potion” items, adding 1 more brings the total quantity to 21 but splits the items across 2 separate stacks — the original full stack and a new one for the overflow.
Stack items up to limit
When you add items of the same type to a container in multiple operations, they accumulate into a single quantity. For example, adding 15 “potion” to a bag with unlimited capacity and then adding another 5 results in a total quantity of 20 potions in the bag.
Unstackable items create separate stacks
Since items have a default max_stack of 1, each item occupies its own slot. When you add two swords to a bag with unlimited capacity, both are stored successfully — the total quantity of swords is 2 — but they occupy 2 separate item slots rather than combining into a single stack.
Generated from core/tests/features/stacking.feature
How to create containers and add items
Containers are the universal abstraction for where items live.
Add an item to an empty container
Like in Elden Ring where players can carry anything, when you create a container with unlimited capacity, it accepts items without restriction. Adding one “iron_sword” to an empty “inventory” container results in the inventory holding exactly one item.
Add an unregistered item fails
If you try to add an item that hasn’t been registered with the system, the operation will fail. For example, attempting to add 1 “unknown_item” to an unlimited-capacity container called “inventory” produces an unregistered item error. You must register item types before they can be placed into any container.
Add multiple different items
When you create a container with unlimited capacity, it tracks each distinct item type as a single entry regardless of stack size. Adding 1 “iron_sword”, 3 “health_potion”, and 5 “golden_rune” to your inventory results in 3 items — one entry per item type, not one per unit.
Multiple containers are independent
Each container operates independently. If you create two unlimited containers — say “item_box” and “hunt_pouch” — adding an item to one has no effect on the other. The container you added to will hold that item, while the other remains empty.
Query an empty container
When you create a container with unlimited capacity and query it before adding anything, the result is empty. This is the baseline state — a freshly created container like “inventory” holds no items until you explicitly add them.
Query item quantity
When you add items of the same type to a container multiple times, their quantities stack. If you add a first batch and then a second batch of the same item to your inventory, querying the quantity of that item returns the combined total.
Query items in a container
Much like checking your inventory in Skyrim after picking up loot, you can query a container to see what’s inside. If you create an “inventory” container with unlimited capacity and add 1 “iron_sword” and 3 “health_potion” to it, querying “inventory” confirms that both “iron_sword” and “health_potion” are present, while items you never picked up — like “golden_rune” — are not.
Query quantity of an item not in the container
When you query the quantity of an item that hasn’t been added to a container, the result is 0 rather than an error. For example, asking for the quantity of “iron_sword” in an empty unlimited-capacity “inventory” container simply returns 0, making it safe to check quantities without first verifying whether an item exists.
Remove an item from a container
When you remove an item from a container, it’s no longer tracked. If you add an “iron_sword” to an unlimited-capacity “inventory” container and then remove it, the container will be empty with 0 items.
Remove an item that is not in the container
If you try to remove an item that doesn’t exist in the container — for example, removing “iron_sword” from an unlimited-capacity “inventory” that doesn’t contain it — the operation fails with a not found error rather than silently succeeding.
Generated from core/tests/features/container.feature
How to define item types
Type definitions describe item types with named, typed fields. Fields have types, defaults, and mutability (definition or instance).
Duplicate type registration fails
If you attempt to register a type with a name that already exists in the registry, the operation will fail with a duplicate type error. For example, after registering a “sword” type, trying to register another type also called “sword” — even with different field defaults — will be rejected. Each type name must be unique within the registry.
Look up unregistered type fails
If you look up a type that hasn’t been registered — such as “nonexistent” — the registry returns a not-found result rather than crashing or returning a default. This lets you safely check whether a type exists before trying to use it.
Register a type with fields
When you register a new type in an empty type registry, you define it with a name and a set of typed fields. For example, registering a “sword” type with four fields — a string “name” defaulting to “Sword”, an f32 “damage” defaulting to 10.0, an i32 “width” defaulting to 1, and an i32 “height” defaulting to 3, all with definition-level mutability — adds that type to the registry with all four fields intact.
Register a type with instance-mutable fields
When you register a type in the registry, each field can be marked as either definition-mutable or instance-mutable. For example, registering an “armor” type with a “name” field (string, default “Armor”, definition mutability) and a “durability” field (i32, default 100, instance mutability) means that “durability” can be changed on individual item instances, while “name” is fixed at the type definition level and cannot be modified per-instance.
Generated from core/tests/features/type_registry.feature
How to change capacity at runtime
Decreasing capacity triggers a threshold
When you reduce a container’s capacity at runtime, existing items may suddenly push past thresholds that weren’t previously exceeded. For example, if your “backpack” has a soft weight constraint of 100.0 with thresholds at 0.7 (“encumbered”) and 1.0 (“overloaded”), and it already holds 6 iron ingots, dropping the capacity to 50.0 causes both thresholds to be exceeded immediately. The capacity change result tells you exactly which thresholds were newly crossed, so you can react accordingly — such as slowing the player or blocking further pickups.
Increasing capacity clears a threshold
When you increase a container’s capacity at runtime, any thresholds that were previously exceeded are automatically re-evaluated. For example, if your “backpack” has a soft weight constraint of 100.0 with an “encumbered” threshold at 70% and contains 8 iron ingots pushing it past that threshold, raising the weight capacity to 150.0 drops the fill ratio below 0.7 — clearing the “encumbered” state. The capacity change result will report that the “encumbered” threshold was newly cleared, so you can react accordingly (e.g., restoring the player’s movement speed).
Reducing slot capacity displaces excess stacks
When you reduce a container’s slot capacity at runtime, any items that no longer fit are displaced rather than silently destroyed. For example, if your “belt” container holds 3 coins across 5 slots and you shrink its capacity to 2, the system reports 1 displaced item — a single coin — and the belt retains only 2 items. This lets you safely resize containers (e.g., when a buff expires or equipment changes) while giving you full control over what happens to the overflow.
Generated from core/tests/features/dynamic_capacity.feature
How to combine multiple constraints
Containers can have multiple constraints evaluated with AND logic.
Grid + hard weight — best-effort mode, weight bypassed
When a container has multiple constraints, best-effort mode can bypass hard limits that would normally block an operation. If you set up a 4×4 grid container “rig” with a hard weight limit of 14.0 and place ammo boxes at positions (0, 0) and (2, 0), then attempt to place a third ammo box at (0, 1) using best-effort mode, the item is successfully added even though it would exceed the weight constraint. The operation reports 1 added out of 1 requested, and the container holds all 3 items. Best-effort mode treats hard constraints as soft, allowing placement to proceed where it otherwise wouldn’t.
Grid + hard weight — exact mode, weight blocks
As in Escape from Tarkov where rigs enforce both grid space and weight limits simultaneously, a container can combine multiple constraints using AND logic. If you configure a “rig” container with a 4×4 grid and a hard weight limit of 14.0, you can place ammo boxes into open grid cells as long as the total weight stays under the cap. After placing one ammo box at position (0, 0) and another at (2, 0), attempting to place a third at (0, 1) will fail with a capacity exceeded error — even though the grid has open slots, the combined weight of three ammo boxes would breach the weight constraint, and both constraints must be satisfied for a placement to succeed.
Grid + hard weight — grid blocks (position occupied)
When a container combines multiple constraints, each one is evaluated independently. If you configure a “rig” container with both a 4×4 grid and a hard weight limit of 100.0, and you’ve already placed an ammo box at position (0, 0), attempting to place another ammo box at that same position will fail with a position occupied error. The grid constraint rejects the operation before the weight constraint is even considered — whichever constraint fails first blocks the action.
Grid + soft weight — grid allows, weight reports threshold
When you combine a grid constraint with a soft weight constraint, both are evaluated together. If you set up a container called “rig” with a 4×4 grid and a soft weight limit of 12.0 that flags an “overloaded” threshold at 100% capacity, placing items will succeed as long as the grid has space — but the weight constraint still tracks totals independently. After placing three ammo boxes into valid grid positions, the operation succeeds because the grid allows it, but the result reports that the “overloaded” threshold has been exceeded. This is the key difference between hard and soft constraints: a soft weight limit warns you without blocking the action.
Grid + soft weight — grid blocks even when weight is soft
When a container has multiple constraints, all of them are evaluated independently using AND logic. If you set up a container “rig” with a 2×1 grid constraint and a soft weight limit of 100.0, placing an “ammo_box” at position (0, 0) occupies that grid cell. Attempting to place a “grenade” at the same position (0, 0) fails with a position occupied error — even though the weight constraint is soft and permissive, the grid constraint still enforces spatial rules. A soft constraint on one axis doesn’t relax a hard constraint on another.
Slots + weight — slots block new type addition
When a container has multiple constraints, all of them must be satisfied for an operation to succeed. If you configure a “belt” container with both a hard SlotCount(1) and a hard Weight(100.0) constraint, adding a “grenade” fills the single available slot. Attempting to then add an “ammo_box” in exact mode fails with a capacity exceeded error — even if the belt still has weight allowance remaining, the slot limit alone is enough to block the addition.
Slots + weight — weight blocks when adding to existing stack
When a container enforces multiple constraints together, each one must pass independently. If you configure a “belt” container with both a hard SlotCount(5) and a hard Weight(8.0) constraint, adding 2 grenades succeeds as long as both limits are satisfied. However, attempting to add another grenade in exact mode fails with a capacity exceeded error when the weight limit would be breached — even though slot capacity remains available. The belt still holds its original single item stack, since the rejected operation leaves the container unchanged.
Split with weight + slots — slot constraint blocks
When a container enforces multiple constraints together, every constraint must be satisfied for an operation to succeed. If you configure a “belt” container with both a hard SlotCount(2) and a hard Weight(100.0) constraint, then add 5 “grenade” items and 1 “ammo_box” (filling both slots), attempting to split 3 from the “grenade” stack will fail with a capacity exceeded error — even if the weight limit would allow it. Splitting would create a third stack, violating the slot constraint. The belt still contains its original 2 items, unchanged.
Generated from core/tests/features/composable.feature
How to use enforcement modes
Containers support hard, soft, and report-only enforcement.
Best-effort add at exact slot capacity reports 0 added
When a container has already reached its hard slot limit, attempting to add more items in best-effort mode gracefully reports the shortfall instead of failing. If you configure a container “rack” with a hard SlotCount(3) constraint and fill it with three weapons, a best-effort attempt to add one more weapon will report 0 added out of 1 requested, and the container remains at 3 items. This lets you attempt bulk additions without risking errors, while still knowing exactly how many items couldn’t fit.
Best-effort add at exact weight capacity reports 0 added
When a container is already at its exact weight limit, a best-effort add correctly reports that nothing was added. If your “chest” has a hard weight constraint of 100.0 and already contains 10 “iron_ingot” totalling that limit, attempting to add 5 more in best-effort mode results in 0 added out of 5 requested, and the chest remains at a total weight of 100.0.
Exact add landing at exact slot capacity succeeds
When a container has a hard SlotCount(3) constraint and already holds 2 items, adding 1 more weapon in exact mode succeeds — the operation reports 1 added out of 1 requested, bringing the container to its full capacity of 3 items. Exact mode requires that the entire requested quantity can be accommodated, and since the third slot is available, the add completes without issue.
Exact add landing at exact weight capacity succeeds
When you add items to a container using exact mode, the operation succeeds as long as the total weight doesn’t exceed the constraint. If you have a “chest” with a hard weight limit of 100.0 and it already contains 9 iron ingots, adding 1 more iron ingot in exact mode succeeds — the operation reports 1 added out of 1 requested, and the chest sits at exactly 100.0 total weight, right at its capacity.
Hard + best-effort — add as many as fit
When a container uses hard enforcement, you can still attempt to add items using best-effort mode, which fits as many items as possible rather than rejecting the entire operation. If you set up a “chest” with a hard weight limit of 100.0 and it already contains 8 iron ingots, attempting to add 5 more in best-effort mode will add only the 2 that fit within the weight budget. The operation reports back that 2 out of 5 requested items were added, and the chest sits exactly at its 100.0 weight limit.
Hard + exact — reject add that would exceed capacity
When a container uses hard enforcement, it strictly prevents any operation that would violate its constraints. If you have a “chest” with a hard weight constraint of 100.0 and it already contains 8 iron ingots (weighing 80.0 total), attempting to add 5 more iron ingots in exact mode will fail with a capacity exceeded error. The chest remains unchanged at 80.0 weight — hard enforcement ensures the invalid addition is fully rejected rather than partially applied.
Hard slot + best-effort — report 0 added when full
When a container has a hard SlotCount constraint, it strictly enforces its capacity limit. If you create a “belt” container with a hard SlotCount of 2 and fill both slots with weapons, any further additions in best-effort mode won’t error out — instead, the operation completes gracefully and reports that 0 items were added out of the 1 requested, letting you handle the overflow in your game logic.
Hard slot + exact — reject when all slots full
When a container uses hard enforcement with a SlotCount constraint, it strictly prevents overfilling. If you create a “belt” container with a hard SlotCount(2) constraint and add two weapons to fill both slots, any further attempt to add another weapon in exact mode will fail with a capacity exceeded error. The belt remains unchanged at 2 items — hard enforcement guarantees the constraint is never violated.
Report-only — always succeeds, reports state
When you configure a container with report-only enforcement, additions always succeed regardless of whether constraints are violated. Instead of blocking the operation, the system tells you which thresholds have been crossed. For example, if you set up an “equipment” container with a report-only Weight constraint of 70.0 and define thresholds at 0.3 (“light_roll”), 0.7 (“medium_roll”), and 1.0 (“heavy_roll”), adding 8 “iron_ingot” items will go through successfully. The result then reports that all three thresholds — “heavy_roll”, “medium_roll”, and “light_roll” — have been exceeded, giving you full visibility into the container’s state without preventing the action.
Report-only slot — always succeeds
When a container uses report-only enforcement, constraint violations are logged but never prevent operations from succeeding. If you configure a “belt” container with a report-only SlotCount(2) constraint and then add four weapons to it, all four additions succeed and the belt contains all four items — even though the slot count of 2 was exceeded after the second weapon. This mode is useful for monitoring how often constraints would be hit without actually blocking gameplay.
Soft + best-effort — same as soft exact
When a container uses soft enforcement with a best-effort add, the behavior is the same as a regular soft add. If your “backpack” has a soft weight constraint of 100.0 and already holds 8 iron ingots, adding 5 more in best-effort mode succeeds fully — the operation reports all 5 added out of 5 requested, bringing the total weight to 130.0. Since soft constraints allow overflow, best-effort mode has no partial-fill behavior to trigger; every requested item is accepted.
Soft + exact — allow add beyond capacity, report thresholds
When a container uses soft enforcement, items are never rejected — but you still get threshold reports so your game can react. If you configure a “backpack” with a soft weight constraint of 100.0 and define thresholds at 0.7 (“encumbered”) and 1.0 (“overloaded”), adding 11 iron ingots that push the total weight to 110.0 will succeed fully. The operation confirms all 5 requested items were added, but the result flags that both the “encumbered” and “overloaded” thresholds have been exceeded. This lets you allow the action while still triggering gameplay consequences like movement penalties or warning UI.
Soft slot + exact — allow beyond slot capacity
When a container uses a soft SlotCount constraint, items can exceed the slot limit rather than being rejected. If your “belt” container has a soft SlotCount(2) constraint and already holds 2 weapons, adding a third weapon in exact mode still succeeds — the operation reports 1 added out of 1 requested, and the belt now contains 3 items. Soft constraints track violations without blocking the operation, letting you decide how to handle over-capacity situations in your game logic.
Generated from core/tests/features/enforcement.feature
How to use slot constraints
Containers with SlotCount constraint limit the number of distinct item stacks.
Add items exceeding slot limit
When your container has a hard SlotCount(2) constraint, it can hold at most two distinct item stacks. If you add a “weapon” to the “loadout” container twice, filling both slots, any further attempt to add another “weapon” will fail with a capacity exceeded error. Each addition occupies one slot, and the hard constraint strictly prevents exceeding the limit.
Add items within slot limit
When your container has a hard SlotCount constraint, it limits how many distinct item stacks the container can hold. If you create a container called “loadout” with a SlotCount of 3, adding a single weapon succeeds without issue — the container now holds 1 item, well within its 3-slot limit.
Remove item frees a slot
When you remove an item from a slot-limited container, that slot becomes available again. If your “loadout” container has a hard SlotCount(2) constraint and you add two “weapon” items, removing one frees up a slot. You can then add another “weapon” successfully, leaving the container with 2 items total. Slot capacity tracks current occupancy, not historical usage.
Swap item in full container
When a container is full, you can still swap items by removing one first. If your “loadout” container has a hard SlotCount(2) constraint and already holds a “weapon” and a “shield,” removing the “weapon” frees a slot, letting you add a new “weapon” back in. The container ends up with 2 items, never exceeding its slot limit at any point during the swap.
Generated from core/tests/features/slot_capacity.feature
How to use threshold transitions
Multiple thresholds crossed in one operation
When a single operation pushes a container past multiple thresholds at once, the result reports all of them. For example, if your “backpack” has a soft weight constraint of 100.0 with thresholds at 70% (“encumbered”) and 100% (“overloaded”), adding 11 “iron_ingot” items in exact mode will report both “encumbered” and “overloaded” as newly crossed in the same result.
Query slot threshold state for UI display
When you need to drive UI changes based on how full a container is, you can query its slot threshold state. For example, given a container “rack” with a soft slot count of 10 and two thresholds defined — “crowded” at 80% and “full” at 100% — adding 8 tokens and then querying the threshold state returns the current slot count (8), capacity (10), and ratio (0.8). At that point, the “crowded” threshold is exceeded while “full” is not, giving your UI the information it needs to show visual feedback like a color change or warning icon as the container approaches its limit.
Query threshold state for UI display
When you query the weight threshold state of a container, you get back everything needed to drive your UI: the current weight value, the total capacity, and the fill ratio, along with which named thresholds have been exceeded. For example, a “backpack” with a soft weight constraint of 100.0 and thresholds defined at 0.7 (“encumbered”) and 1.0 (“overloaded”) that holds 8 iron ingots will report a current weight of 80.0, a capacity of 100.0, and a ratio of 0.8 — meaning “encumbered” is exceeded but “overloaded” is not. This lets you update indicators like weight bars, warning icons, or movement penalties based on exactly where the container sits relative to each threshold.
Slot threshold cleared on remove
When a container has a threshold defined, removing items can clear a previously active threshold. If you set up a “rack” container with a soft slot count of 10 and a “crowded” threshold at 80% capacity, adding 8 tokens brings you to exactly that 80% mark, triggering the threshold. Removing one token drops the count to 7, which falls below the 0.8 ratio, and the result reports that the “crowded” threshold is cleared. This lets you react to state changes in both directions — knowing when a container becomes crowded and when it stops being crowded.
Slot threshold newly crossed on add
When a container has threshold ratios defined against a slot count constraint, adding items can trigger threshold crossing notifications. If you configure a container “rack” with a soft slot count of 10 and thresholds at 0.8 (“crowded”) and 1.0 (“full”), then add tokens one at a time, the eighth token pushes occupancy to 80% — exactly the “crowded” ratio. The result from that addition reports that the “crowded” threshold was newly crossed, while confirming the “full” threshold has not yet been exceeded. This lets you react to capacity milestones as they happen, such as warning the player that a container is getting full before it actually fills up.
Threshold newly crossed on add
When you configure a container with weight thresholds, the system tracks which thresholds are newly crossed after each operation. For example, if your “backpack” has a soft weight constraint of 100.0 with thresholds at 0.7 (“encumbered”) and 1.0 (“overloaded”), and it already contains 6 iron ingots, adding 2 more in exact mode will report that the “encumbered” threshold was newly crossed — meaning it wasn’t exceeded before but now is. At the same time, the result confirms that the “overloaded” threshold has not yet been exceeded. This lets you react precisely to state changes, such as showing a UI warning only at the moment a character becomes encumbered, rather than on every subsequent addition.
Generated from core/tests/features/thresholds.feature
How to use weight constraints
Containers with Weight constraint limit total weight of contained items. Items must have a “weight” field.
Add items within weight limit
When you configure a container with a hard Weight constraint — for example, a “pouch” with a maximum weight of 25.0 — the system enforces that limit as items are added. If you attempt to add 2 “heavy_item” entries to the pouch but their combined weight would exceed the capacity, only 1 item is actually stored. The hard constraint silently prevents the second item from being added rather than allowing the container to exceed its weight limit.
Item type missing weight field
If you try to add an item to a weight-constrained container but the item’s type doesn’t have a “weight” field, the operation will fail with a missing field error. For example, a “weightless” type that only defines a “name” field cannot be placed into a “pouch” with a hard Weight(100.0) constraint, because the system has no way to calculate how much weight the item would contribute.
Query current total weight
You can query the current total weight of a container at any time. If you create a container “pouch” with a hard weight capacity of 100.0 and add 2 “heavy_item” entries to it, you can check that the pouch reports a total weight of 20.0, reflecting the combined weight of its contents.
Reject item that exceeds weight limit
When your container enforces a hard weight capacity, adding items that would exceed the limit is rejected. If you configure a “pouch” with a hard Weight constraint of 15.0 and it already contains one “heavy_item,” attempting to add a second “heavy_item” will fail with a capacity exceeded error. This prevents containers from ever holding more weight than their defined maximum.
Generated from core/tests/features/weight_capacity.feature
How to use grid inventories
Containers with Grid constraint use a 2D grid for item placement. Items must have “width” and “height” fields. The core validates placement and prevents overlaps.
Adjacent multi-cell items succeed
When your container uses a grid constraint, items can be placed side by side as long as they don’t overlap. If you have a “backpack” with a 10×6 grid and place a “shield” at position (0, 0), you can place a second “shield” at (2, 0) without conflict. Both items coexist in the grid, and the backpack correctly reports 2 items.
Can place at checks without placing
You can check whether an item would fit at a specific position in a grid container without actually placing it. Given a “backpack” with a 10×6 grid, querying position (0, 0) for a “sword” confirms it can be placed there, while querying position (10, 0) correctly reports that placement is impossible since that coordinate falls outside the grid boundary.
Find free spot for item
When you need to find available space for an item in a grid container, the system can automatically locate a free spot. If you have a 10×6 “backpack” grid with a “sword” already placed at position (0, 0), asking the system to find a free spot for another “sword” will return position (1, 0) — the next available location that avoids overlapping with the existing item.
Get item at position
You can look up which item occupies any cell in the grid. If you place a “sword” at position (3, 2) in a 10×6 “backpack” grid, querying the cell at (3, 3) returns that “sword” — because the item spans multiple cells based on its dimensions, any cell it covers will resolve back to it.
Item missing grid fields
If you try to place an item into a grid container but the item’s type definition lacks the required “width” and “height” fields, the operation will fail with a missing field error. For example, a type “no_size” that only defines a “name” field cannot be placed at any position in a 10×6 grid container — the core requires every item placed in a grid to declare its dimensions.
Multi-cell item checks all cells
When you place a multi-cell item like a “shield” at position (0, 0) in a grid container, it occupies all the cells its width and height cover. If you then try to place a “potion” at position (1, 1) in the same “backpack” (a 10×6 grid), the operation fails with a position occupied error because the shield already occupies that cell. The core checks every cell a new item would cover, not just its origin point, ensuring no overlaps occur.
Multi-cell item partially out of bounds fails
When you have a grid-based container like a “backpack” with a 10×6 grid, the system prevents items from being placed partially outside the grid boundaries. If you try to place a “shield” at position (9, 0) and the item’s width would extend beyond column 10, the operation fails with an out of bounds error. This ensures every cell an item occupies must fall within the grid dimensions.
Multiple item types in same grid
A grid container can hold different item types simultaneously. If you define a “backpack” with a 10×6 grid, you can place a “sword” at position (0, 0), a “shield” at (2, 0), and a “potion” at (5, 0) — as long as each item fits within the grid bounds and doesn’t overlap another, the placement succeeds and the backpack will contain all 3 items.
Out of bounds placement fails
If you attempt to place an item at a position that falls outside the grid boundaries, the operation will fail. For example, given a container “backpack” with a 10×6 grid, trying to place a “sword” at position (10, 0) results in an out-of-bounds error, since valid column indices run from 0 to 9.
Overlapping placement fails
When you place a “sword” at position (0, 0) in a “backpack” container using a grid(10, 6) constraint, attempting to place a “shield” at the same (0, 0) position will fail with a position occupied error. The core prevents items from overlapping on the grid — each cell can only be claimed by one item at a time.
Partial overlap of multi-cell items fails
When two multi-cell items partially overlap on a grid, placement is rejected. If you place a shield at position (0, 0) in a 10×6 backpack grid, then attempt to place another shield at (1, 0), the operation fails with a position occupied error. The grid enforces that every cell an item would occupy must be free — even a single cell of overlap is enough to block placement.
Place item in grid
When you set up a container with a grid constraint, you can place items at specific coordinates within that grid. For example, given a “backpack” container with a 10×6 grid, placing a “sword” at position (0, 0) succeeds, and the backpack then contains one item. This confirms that the grid accepts valid placements within its bounds.
Query grid state after placing items
You can query the grid state of a container to inspect its current layout. After creating a “chest” with a 10×6 grid and placing a “sword” at position (0, 0) and a “potion” at (5, 5), querying the grid state returns the full dimensions (10×6), the number of occupied cells (5), and the number of free cells (55). This gives you a snapshot of how much space remains and how densely packed the container is, which is useful for UI display or placement logic.
Remove item frees grid space
When you remove an item from a grid container, the space it occupied becomes available again. If you place a “sword” at position (0, 0) in a 10×6 “backpack” grid and then remove it, the operation succeeds and you can immediately place a different item, such as a “shield,” at that same (0, 0) position.
Stack potions at same grid position
When items occupy a specific grid cell, you can stack additional items of the same type into that position. If you place 3 “potion” items at position (5, 5) in a 10×6 grid backpack and then add 2 more “potion” to the same cell, the stack grows to a total quantity of 5.
Generated from core/tests/features/grid_capacity.feature
How to move and swap items
Grid resize — grow adds empty cells
When you resize a grid container to larger dimensions, the grid expands and fills the new space with empty cells. For example, if you have a container “stash” configured as a 4×4 grid and set its dimensions to 6×6, the grid grows to 6×6, preserving existing contents while making the additional cells available for use.
Grid resize — shrink displaces items in removed cells
When you shrink a grid-based container, any items occupying cells that no longer exist are displaced rather than silently destroyed. For example, if your “stash” is a 6×6 grid and you’ve placed an iron ingot at position (5, 5), resizing the grid down to 4×4 will remove that item from the container and report it as displaced. The result includes the full details of each displaced item — in this case, one iron ingot with a quantity of 1 — so you can decide what to do with them, whether that’s returning them to the player, dropping them in the world, or moving them to another container.
Move by index in a non-grid container
When you move an item to a different index within a non-grid container, the other items shift to fill the gap. For example, if your “backpack” has an “iron_ingot” at index 0 and a “health_potion” at index 1, moving the item at index 0 to index 1 causes the “health_potion” to become the first item in the container.
Move item to occupied cell fails
When you try to move an item to a cell that’s already occupied by another item, the operation fails. For example, if you place an “iron_ingot” at position (0, 0) and a “health_potion” at (2, 2) in a 5×5 grid container, attempting to move the iron ingot to (2, 2) will return a position occupied error. To relocate an item, the target cell must be free — or you can use a swap operation instead.
Move item within a grid container
You can move an item from one position to another within the same grid container. For example, if you place an “iron_ingot” at position (0, 0) in a 5×5 grid container called “grid_chest”, you can then move it to position (3, 3). The operation succeeds and the container still contains exactly one item — the item is relocated, not duplicated.
Swap by index in a non-grid container
You can swap two items in a container by their index positions. If you add an “iron_ingot” and then a “health_potion” to a “backpack” with unlimited capacity, swapping the items at index 0 and index 1 will reverse their order, placing the “health_potion” first. This works in any non-grid container.
Swap two items in a grid
When two items occupy different positions in a grid container, you can swap them directly. If you place an iron ingot at position (0, 0) and a health potion at (2, 2) in a 5×5 grid, swapping those two positions moves the health potion to (0, 0) and the iron ingot to (2, 2). Both items exchange places in a single operation.
Generated from core/tests/features/move_swap.feature
How to remove items
Best-effort remove — remove what’s available
When you attempt to remove more items than a container actually holds, best-effort mode removes as many as it can rather than failing. For example, if your “backpack” contains 5 “iron_ingot” and you request a best-effort removal of 8, the operation succeeds by removing all 5 available and reports that 5 out of 8 requested were removed, leaving the quantity of “iron_ingot” at 0.
Exact remove — fail if insufficient quantity
When you attempt to remove more items than a container holds using exact mode, the operation fails safely. For example, if your “backpack” (with a soft weight constraint of 100.0) contains 5 “iron_ingot” and you try to remove 8 in exact mode, the removal is rejected with an insufficient quantity error and the container retains all 5 ingots unchanged.
Remove clears a previously exceeded threshold
When your container has weight-based thresholds configured, removing items can clear previously exceeded thresholds. For example, if your “backpack” has a soft weight limit of 100.0 with an “encumbered” threshold at 70% capacity, and it contains 8 iron ingots, removing 3 of them in exact mode successfully removes all 3 requested items. This brings the total weight down to 50.0, which falls below the 70% threshold, so the operation reports that the “encumbered” threshold has been cleared.
Generated from core/tests/features/remove.feature
How to split and merge stacks
Best-effort merge — merge up to max stack, leave remainder
When you merge two stacks in best-effort mode, the system combines as many items as possible into one stack without exceeding its maximum size, and any excess remains in the original stack. For example, if your “quiver” container holds a stack of 20 arrows and a stack of 15 arrows, a best-effort merge will fill one stack to its max and report that 5 items were merged, leaving 15 remaining in the other stack.
Exact merge — reject if combined exceeds max stack
When you attempt an exact merge of two stacks in a container, the operation enforces the item’s maximum stack size. If you have a “quiver” containing one stack of 20 arrows and another of 15 arrows, trying to merge them in exact mode will fail with a max stack exceeded error, since the combined 35 arrows would surpass the allowed limit. Exact mode is strict — it either merges the full quantities or rejects the operation entirely.
Merge clears a slot threshold
When you merge stacks, Sidequest Hero tracks whether the merge causes the container to cross any configured thresholds. For example, if your “pouch” container has a soft slot count limit of 5 and a “crowded” threshold at 75% capacity, splitting a stack of 20 gems repeatedly can push you over that threshold by creating 4 occupied slots. Merging two of those gem stacks back together reduces the container to 3 items, dropping below the 75% mark — and the merge result explicitly reports that the “crowded” threshold was newly cleared. This lets you react to capacity changes in your UI or game logic whenever a merge frees up enough space to cross a threshold boundary.
Split a stack
When you split a stack, the operation creates a new stack with the requested quantity while reducing the original. For example, if you add 10 “arrow” to a “quiver” with unlimited capacity and then split 4 from that stack, the result reports an original stack of 6 and a new stack of 4. The total quantity of “arrow” in the quiver remains 10, but the container now holds 2 separate item stacks.
Split in a full slot container — hard enforcement rejects
When you try to split a stack inside a container that’s already full, the operation is rejected if the container uses hard enforcement. For example, a “quiver” with a hard SlotCount(2) constraint that already holds 10 “arrow” in one slot and 1 “iron_sword” in another has no room for the new stack that a split would create. Attempting to split 4 from the “arrow” stack fails with a capacity exceeded error, because the split would require a third slot that the constraint doesn’t allow.
Split in a full slot container — soft enforcement allows
When a container uses a soft slot constraint, splitting is still allowed even if the container is already at capacity. If your “quiver” has a soft SlotCount(2) constraint and already holds a stack of 10 “arrow” and 1 “iron_sword” (filling both slots), splitting 4 from the “arrow” stack succeeds — the quiver now contains 3 items (two arrow stacks and the sword), and the total arrow quantity remains 10. Soft constraints permit temporary violations, so the extra slot created by the split is not blocked.
Split triggers a slot threshold
When you split a stack inside a container that has capacity thresholds, the split result tells you if any threshold was newly crossed. For example, if your “pouch” container has a soft slot count of 5 and a “crowded” threshold at 80%, and it currently holds three full stacks of 10 gems each (occupying 3 of 5 slots), splitting 5 gems off one of those stacks creates a fourth stack — pushing occupancy to 4 out of 5 slots. The split result reports that the “crowded” threshold was newly crossed, letting you react accordingly (e.g., warning the player their pouch is getting full).
Generated from core/tests/features/split_merge.feature
How to transfer items between containers
Transfer best-effort — moves what fits
When you transfer items between containers in best-effort mode, the system moves as many as it can without violating hard constraints. For example, if you have 10 iron ingots in a backpack (which has a soft weight limit of 200.0) and attempt to transfer 8 of them to a chest with a hard weight limit of 50.0, only 5 will actually move — the most the chest can accept. The operation reports that 5 out of 8 requested items were transferred, leaving 5 iron ingots in each container. This lets you fill a container to capacity in a single call without needing to calculate available space yourself.
Transfer exact — succeeds when destination has room
When you transfer items between containers in exact mode, the system moves precisely the quantity you request — as long as the destination has room. For example, if your “backpack” (with unlimited capacity) holds 5 “iron_ingot” and you transfer 3 to a “chest” constrained to a hard weight limit of 100.0, the operation reports 3 transferred out of 3 requested. The backpack is left with 2 iron ingots and the chest now contains 3.
Transfer to a hard container that can’t fit — nothing moves
When you attempt to transfer items into a container with a hard constraint and the transfer would exceed that limit, nothing moves. For example, if you try to transfer 8 iron ingots from a backpack (soft weight limit of 200.0) into a chest with a hard weight limit of 50.0 using exact mode, the operation fails with a capacity exceeded error. The backpack retains all 10 iron ingots and the chest remains empty — hard constraints are strict, so partial transfers don’t happen in exact mode.
Transfer triggers threshold on destination
When you transfer items between containers, the destination container’s thresholds are evaluated against the incoming weight. If you set up a container like “chest” with a soft weight constraint of 50.0 and an “encumbered” threshold at 70%, then transfer 4 “iron_ingot” from your “backpack” to the “chest” in exact mode, all 4 items transfer successfully. The result also reports that the “encumbered” threshold was newly crossed, letting you react to the destination container reaching a significant capacity milestone as a direct consequence of the transfer.
Generated from core/tests/features/transfer.feature
Composable Constraints
Sidequest Hero gives you three constraint types that control how a container limits its contents. You can use one, combine several, or use none at all. When multiple constraints are on the same container, they compose with AND logic — every constraint must be satisfied for an operation to succeed.
The Three Constraint Types
Weight
A weight constraint sets a total weight budget for the container. Every item must have a weight field. When you add an item, its weight is added to the running total. If the total would exceed capacity, the constraint blocks the operation (or reports it, depending on enforcement mode — see Enforcement Modes).
The constraint doesn’t care how many items you have or where they go — only the sum of their weights. This is the classic carry weight system — Skyrim’s 300 carry weight, Dark Souls’ equip load.
Slots
A slot constraint limits the number of distinct stacks in a container. Each unique item type occupies one slot. If two stacks of the same type exist (because you split a stack), each stack occupies its own slot.
You get a fixed number of slots, and each slot holds one stack. The constraint doesn’t care how heavy those stacks are or how large the items are — only how many slots are occupied. Think of a hotbar or item pouch — Diablo’s skill bar, Monster Hunter’s hunt pouch.
Grid
A grid constraint defines a 2D spatial layout with a fixed width and height. Every item has a width and height that determines how many cells it occupies. You place items at specific coordinates, and the grid tracks which cells are taken.
Items are spatial objects that must physically fit in the grid without overlapping — the attaché case from Resident Evil 4, or Escape from Tarkov’s stash.
AND Logic: How Constraints Combine
When you put multiple constraints on a container, all of them must agree before an operation succeeds. There is no priority, no override — it’s a flat AND.
Grid + Weight means the item needs both a free grid position AND available weight budget. Your rig has a spatial grid, but items also have weight and the rig has a weight limit — like Escape from Tarkov.
Scenario: Grid + hard weight — exact mode, weight blocks Given a container “rig” with grid(4, 4) and hard Weight(14.0) constraints And I place 1 “ammo_box” at (0, 0) in “rig” And I place 1 “ammo_box” at (2, 0) in “rig” When I try to place 1 “ammo_box” at (0, 1) in “rig” Then the operation fails with a capacity exceeded error
Two ammo boxes weigh 10.0 total. The third would push weight to 15.0, exceeding the 14.0 budget. Even though there’s grid space available, the weight constraint blocks the operation.
Slots + Weight means both a free slot AND weight room. This is a utility belt with limited pouches and a weight limit.
Scenario: Slots + weight — weight blocks when adding to existing stack Given a container “belt” with hard SlotCount(5) and hard Weight(8.0) constraints And I add 2 “grenade” to “belt” When I try to add 1 “grenade” to “belt” in exact mode Then the operation fails with a capacity exceeded error And “belt” contains 1 item
Two grenades weigh 6.0. Adding a third would push weight to 9.0, exceeding the 8.0 budget. The belt has plenty of slots, but the weight constraint says no.
Grid Is Always Structural
Spatial conflicts are absolute. Even if your weight constraint uses soft enforcement (allowing overflow with consequences), you still can’t place two items in the same cell. There’s no “soft overlap.”
This makes intuitive sense — soft weight models a character slowing down under heavy load, which is a gameplay mechanic. Two items occupying the same physical space is a data integrity problem, not a gameplay choice.
No Constraints = Unlimited
If you create a container with no constraints at all, it has unlimited capacity. Items go in, items come out, nothing is ever rejected for capacity reasons.
This is perfectly valid. If your game design doesn’t need inventory limits, don’t add constraints you don’t need — Elden Ring’s main inventory works exactly this way.
Quick Reference
| Game example | Constraint combination |
|---|---|
| Elden Ring (main inventory) | None — unlimited capacity |
| Skyrim (carry weight) | Weight only |
| Diablo (hotbar) | Slots only |
| Monster Hunter (item pouch) | Slots only |
| Resident Evil 4 (attaché case) | Grid only |
| Escape from Tarkov (rig) | Grid + Weight |
| RPG utility belt | Slots + Weight |
Enforcement Modes
Every constraint on a container has an enforcement mode that controls what happens when an operation would exceed the constraint’s capacity.
The Three Modes
Hard (default) — the operation is rejected. The container is unchanged. This is the safe default for chests, shops, NPC inventories — anywhere the game design says “this container has a limit.”
Soft — the operation succeeds even if it exceeds capacity. The result reports which thresholds were crossed. This is for player carry weight — the inventory accepts the items, but the game imposes consequences. In Skyrim, you can pick up items beyond your carry weight limit, but you can’t run or fast travel.
Report-only — there is no capacity limit at all. The container just tracks the current value and reports which thresholds are exceeded. This is for equip load displays — the equipment menu never blocks you, but the game reads the current ratio to apply effects. Elden Ring’s equip load works this way: roll penalties change based on ratio, but equipping gear always succeeds.
How Thresholds Work
Thresholds are named ratios of capacity, defined when the container is created. Here’s an example of soft enforcement with thresholds:
Scenario: Soft + exact — allow add beyond capacity, report thresholds Given a container “backpack” with soft Weight(100.0) constraint and thresholds: | ratio | name | | 0.7 | encumbered | | 1.0 | overloaded | And I add 6 “iron_ingot” to “backpack” When I add 5 “iron_ingot” to “backpack” in exact mode Then the operation reports 5 added out of 5 requested And “backpack” has total weight 110.0 And the result reports threshold “encumbered” is exceeded And the result reports threshold “overloaded” is exceeded
At 110 weight in a capacity-100 container, the ratio is 1.1. Both the “encumbered” (0.7) and “overloaded” (1.0) thresholds are exceeded. The game receives this information in the operation result and decides what to do — slow the player, show a warning, apply a debuff.
Thresholds are relative to capacity. If the player levels up and carry weight increases via set_weight_capacity, the same 0.7 ratio now triggers at a higher absolute weight. The threshold definitions never change — capacity is the dynamic knob.
Choosing an Enforcement Mode
| Your game needs… | Use |
|---|---|
| A container that rejects items when full | Hard (default) |
| A container that allows overflow with gameplay consequences | Soft + thresholds |
| A display showing current load without blocking anything | Report-only + thresholds |
Most containers use hard enforcement. You only need soft or report-only for the player-facing inventory systems where exceeding limits is a game mechanic, not an error.
Operation Modes: Exact vs Best-Effort
Every quantity-based operation — add, remove, split, merge, transfer — takes a mode parameter that controls how partial success is handled. Should the operation succeed only if the full quantity can be fulfilled, or should it do as much as it can?
Exact Mode (Default)
Exact mode is all-or-nothing. If you request 5 items and only 3 fit, the entire operation fails. Nothing changes. The container is left exactly as it was before the call.
This is the right choice when partial fulfillment would be a bug. A shop transaction should charge the full price or not go through at all. A crafting recipe that requires 5 iron ingots should not consume 3 and silently produce nothing. A quest that checks “does the player have 10 dragon scales?” should not remove 7 and call it done.
Exact mode is the default because it is the safer assumption. If you forget to specify a mode, your operations will never leave the game in a half-completed state.
Best-Effort Mode
Best-effort mode does as much as it can. If you request 5 items and only 3 fit, 3 are added. The operation succeeds — it just tells you what actually happened.
This is the right choice when partial is fine. A “Take All” button scoops up everything from a container until carry weight is full — like Skyrim’s loot interface. A stash transfer fills available slots and leaves the rest in your inventory — like Diablo’s stash. An auto-sort routine that redistributes items across containers should move what fits and report the remainder.
Here is a best-effort add that partially fills a weight-constrained container:
Scenario: Hard + best-effort — add as many as fit
Given a container "chest" with hard Weight(100.0) constraint
And I add 8 "iron_ingot" to "chest"
When I add 5 "iron_ingot" to "chest" in best-effort mode
Then the operation reports 2 added out of 5 requested
And "chest" has total weight 100.0
The chest already held 8 iron ingots (80 weight). You requested 5 more, but only 2 fit within the 100-weight capacity. Best-effort added those 2 and reported what happened.
The Result Struct
Every operation returns a result containing quantity_requested and quantity_affected.
In exact mode, these two values are either equal (the operation succeeded in full) or the operation failed entirely — you never get a partial result.
In best-effort mode, quantity_affected may be less than quantity_requested. The difference tells you how many items could not be fulfilled. Your game code can use this to display “inventory full” messages, return leftover items to a source container, or trigger overflow logic.
Modes and Enforcement Together
Operation modes interact with enforcement modes. The combination determines the strictest or most permissive behavior your container allows.
| Enforcement | Mode | Behaviour |
|---|---|---|
| Hard + Exact | Strictest. Rejects the entire operation if the full quantity won’t fit. | |
| Hard + Best-effort | Fits as many as capacity allows. Reports how many were added. | |
| Soft + Exact | Always succeeds. Soft enforcement never rejects, so the full quantity is added even beyond capacity. Threshold crossings are reported. | |
| Soft + Best-effort | Same as soft + exact. Since soft never rejects, there is nothing to partially fulfill — the full quantity always goes through. |
Hard + exact is the most common combination: a chest that holds 100 weight and rejects anything that won’t fit. Hard + best-effort is for bulk operations where you want to fill up to the limit. Soft enforcement makes the mode irrelevant — both exact and best-effort behave identically because the container never says no.
Choosing a Mode
| Your game needs… | Use |
|---|---|
| Shop purchases — pay full price or nothing | Exact |
| Crafting — consume all ingredients or abort | Exact |
| Quest requirements — check full quantity before removing | Exact |
| “Take All” from a loot pile | Best-effort |
| Auto-sort / rebalance across containers | Best-effort |
| Bulk transfer to a limited stash | Best-effort |
When in doubt, use exact. You can always relax to best-effort later, but catching unintended partial operations early saves debugging time.
Thresholds
A threshold is a named ratio on a constraint’s capacity. You define thresholds when you create a container, and they tell you when the container reaches specific fill levels — 70% full, completely full, over capacity.
Thresholds are how your game knows when to slow the player down, change a health bar color, show a “bag is almost full” warning, or trigger an encumbrance penalty.
Defining Thresholds
A threshold is a name and a ratio between 0.0 and 1.0 (or above 1.0 for over-capacity states). You attach thresholds to a constraint when you create the container:
0.7 encumbered— triggers at 70% of capacity1.0 overloaded— triggers at 100% of capacity
The ratio is relative to the constraint’s capacity. On a weight constraint with capacity 100, the “encumbered” threshold triggers when total weight reaches 70. On a slot constraint with capacity 10, a “crowded” threshold at 0.8 triggers when 8 slots are occupied.
Newly Crossed vs Exceeded
Every time the container changes — an item is added, removed, split, merged, transferred — all thresholds are re-evaluated. The result tells you two different things about each threshold:
Exceeded means the threshold is currently above its ratio right now. This is the ongoing state. Use it for persistent effects: movement speed penalties, UI color changes, stat debuffs.
Newly crossed means the threshold was below its ratio before the operation and is above it after. This is the transition event. Use it for one-time triggers: play a sound effect, show a toast notification, fire an animation.
The distinction matters for your game logic. The “You are carrying too much” message appears once when you cross the carry weight limit — that’s a newly crossed event. The movement speed penalty stays active the entire time you’re over the limit — that’s an exceeded state. Skyrim’s encumbrance system uses both.
| Your game needs… | Use |
|---|---|
| A one-time notification when a threshold is reached | Newly crossed |
| A persistent gameplay effect while above a threshold | Exceeded |
| A one-time notification when a threshold is cleared | Newly cleared |
Thresholds can also be newly cleared — the container was above the ratio before the operation and is below it after. Use this for the reverse trigger: “You can fast travel again”, removing a debuff, changing the UI back to normal.
Querying Threshold State
You can query threshold state at any time, not just after an operation. This is how you drive your UI — health bars, weight displays, inventory fullness indicators.
Scenario: Query threshold state for UI display Given a container “backpack” with soft Weight(100.0) constraint and thresholds: | ratio | name | | 0.7 | encumbered | | 1.0 | overloaded | And I add 8 “iron_ingot” to “backpack” When I query the weight threshold state of “backpack” Then the current weight value is 80.0 And the current weight capacity is 100.0 And the current weight ratio is 0.8 And “encumbered” is exceeded And “overloaded” is not exceeded
The query returns the current value, the capacity, the ratio (value / capacity), and which thresholds are currently exceeded. Your UI reads this every frame or on demand to render bars, numbers, and color changes.
Dynamic Capacity Interaction
Thresholds are ratios, not absolute values. When the capacity changes, the absolute trigger point moves with it.
Say your player has a “0.7 encumbered” threshold on a carry weight of 100. That triggers at 70 weight. The player levels up and carry weight increases to 150. Now “encumbered” triggers at 105 weight. The player’s current 80 weight was above the old trigger point but below the new one — the threshold clears automatically.
Scenario: Increasing capacity clears a threshold Given a container “backpack” with soft Weight(100.0) constraint and thresholds: | ratio | name | | 0.7 | encumbered | And I add 8 “iron_ingot” to “backpack” When I set the weight capacity of “backpack” to 150.0 Then the threshold state reports “encumbered” is not exceeded And the capacity change result reports threshold “encumbered” was newly cleared
This works the other way too. If a debuff reduces carry weight from 100 to 50, a player carrying 40 weight suddenly crosses the 0.7 threshold (now 35 weight) and becomes encumbered — without picking up a single item.
You never need to recalculate threshold trigger points manually. Define the ratios once, adjust capacity as the game demands, and thresholds stay correct.
Game Examples
Carry weight with overflow. Soft enforcement with a threshold at 1.0. The inventory accepts items beyond the limit, but crossing the threshold triggers the “over-encumbered” state — no running, no fast travel. The threshold is newly crossed once (message appears), then exceeded continuously (movement penalty persists). Skyrim’s carry weight works this way.
Equip load with tiered effects. Report-only enforcement with thresholds at 0.3 (light), 0.7 (medium), and 1.0 (heavy). The equipment menu never blocks you from equipping gear. The current ratio determines your dodge animation — fast roll, medium roll, fat roll. The UI shows the exact ratio as a bar. Elden Ring’s equip load is this pattern.
Resource bar with depletion events. Thresholds at 0.25 (“exhausted” warning) and 0.0 (collapse). Crossing 0.25 shows a one-time message. Reaching 0.0 triggers a collapse cutscene. The bar uses the current ratio to render its fill level and color — like Stardew Valley’s stamina system.
Choosing Your Thresholds
| Pattern | Thresholds | Enforcement |
|---|---|---|
| Hard limit with no overflow | None needed | Hard |
| Player carry weight with penalty | 0.7 warning, 1.0 penalty | Soft |
| Equip load with tiered effects | 0.3 / 0.7 / 1.0 tiers | Report-only |
| Stamina or resource bar | 0.25 low warning, 0.0 empty | Report-only |
| Storage chest with “almost full” hint | 0.9 warning | Hard |
Most containers don’t need thresholds at all. A shop inventory, a treasure chest, an NPC’s equipment — these just use hard enforcement and reject items when full. Thresholds are for the systems where how full matters to gameplay.