Inventory and Item Persistence - UQcsse3200/2026-studio-3 GitHub Wiki
This feature introduces a durable owned-item collection in PlayerRunState for storing the items held by the player during a run.
Previously, reward items were applied as temporary effects through a single pending reward value. This meant that item effects could be lost when the current player entity or battle screen was disposed. The new implementation stores all owned items in the shared run state. Persistent passive effects are replayed whenever a new player entity is created, while active combat items remain in the inventory until the player chooses to use them.
The feature currently supports:
- Lucky Coin
- Energy Crystal
- Merchant's Favor
- Iron Aegis
- Warrior's Crest
- Persistent item effects between battles
- Multiple copies of the same item
- Gold reward bonuses
- Shop discounts
- Maximum energy bonuses
- An inventory popup displaying owned items
The main goals of this Sprint 3 work are:
- Introduce a reusable inventory data structure.
- Persist owned items for the duration of a run.
- Support multiple copies of the same item.
- Reapply persistent passive effects whenever the player entity is recreated.
- Connect Lucky Coin to the gold reward system.
- Connect Merchant's Favor to the shop system.
- Display the player's items through the battle inventory UI.
- Provide a foundation for expanding the reward item pool.
The inventory system is divided into four main responsibilities:
Reward selection
|
v
PlayerRunState
|
v
PlayerRunState.ownedItems
|
v
ItemEffectApplier
|
v
Player components
When the player claims an item reward, the selected ItemType is added to the player's persistent inventory.
PlayerRunState owns the inventory for the current run. It provides the integration point between rewards, battle screens, shops and the player entity.
PlayerRunState owns an ordered List<ItemType> named ownedItems. It is the run-scoped source of truth for item ownership and supports duplicate items in acquisition order. There is no separate production ItemInventory class in the merged implementation.
ItemEffectApplier maps each ItemType to its gameplay effect. Persistent passive items are applied during player creation, while battle consumables call the same effect mapping only after a valid player-turn use action.
PlayerRunState.ownedItems is the shared collection used to represent items held by the player during a run.
It supports:
- Adding an item with
addOwnedItem() - Counting copies with
getOwnedItemCount() - Retrieving an immutable snapshot with
getOwnedItems() - Replacing a validated restored list with
replaceOwnedItems() - Consuming one owned battle item with
useBattleItem()only after its effect succeeds - Preserving acquisition order and duplicate copies for stacking effects
Example:
PlayerRunState playerState = new PlayerRunState(100, 100, 0);
playerState.addOwnedItem(ItemType.LUCKY_COIN);
playerState.addOwnedItem(ItemType.LUCKY_COIN);
playerState.addOwnedItem(ItemType.ENERGY_CRYSTAL);
int luckyCoins = playerState.getOwnedItemCount(ItemType.LUCKY_COIN);
// luckyCoins == 2The returned item list is immutable so external code cannot accidentally modify the persistent run state.
PlayerRunState owns the ownedItems list and exposes the item operations used by rewards, saves and battle UI.
Important operations include:
addOwnedItem(ItemType item);
getOwnedItemCount(ItemType item);
getOwnedItems();
useBattleItem(ItemType item, Entity player);
replaceOwnedItems(List<ItemType> items);When a gameplay screen creates a new player entity, the persisted state is applied using:
playerState.applyTo(player);This restores:
- Current health
- Maximum health
- Gold
- Persistent passive item effects
Each owned item is passed through ItemEffectApplier:
for (ItemType item : playerState.getOwnedItems()) {
if (!item.isBattleConsumable()) {
ItemEffectApplier.applyItemEffect(item, player);
}
}This replay is required because a new player entity and new components are created for each battle.
The player state must only be applied once during player creation. Applying it again in another game area or screen would cause stackable item effects to be counted twice.
Rewards can be either gold or items.
When a gold reward is claimed:
-
RewardGeneratorchooses a base reward from 20–30 gold. - Every owned Lucky Coin contributes a 10% multiplier to that base reward.
- The generated final amount is rounded and displayed before selection.
- Choosing the reward stores that final amount in
PlayerRunState. - Lucky Coin remains owned; item rewards also leave it unchanged.
finalGold = round(baseReward × (1 + 0.10 × luckyCoinCount))
When an item reward is claimed:
- The selected
ItemTypeis added toPlayerRunState.ownedItems. - Passive items remain owned for the run; consumable items remain owned until they are used.
- Persistent passive effects are replayed whenever a new player entity is created.
- Active battle items remain stored until the player uses them during a valid player turn.
- The inventory popup displays the updated item count and action state.
The persistent inventory replaces the earlier single-item pendingReward approach for durable item ownership.
Gold changes are communicated to the player UI through the following event:
updateMoney
InventoryComponent triggers the event whenever its gold value changes:
entity.getEvents().trigger("updateMoney", gold);PlayerStatsTopDisplay listens for the same event and updates the visible gold amount.
Using one shared event name prevents the stored gold value and the displayed value from becoming inconsistent.
Current merged behaviour: a persistent reward modifier. Each copy adds 10% of the generated gold reward amount, and copies stack.
- Selecting a gold reward does not consume Lucky Coin.
- Selecting an item reward does not consume Lucky Coin.
- Multiple copies stack on each generated gold reward.
- The calculation is
round(baseReward × (1 + 0.10 × copyCount)).
int finalGold = Math.round(baseReward * (1f + 0.10f * luckyCoinCount));The reward card displays the final calculated gain before the player selects it.
Known design discrepancy: earlier planning copy described one Lucky Coin being consumed per gold claim and a +20 cap based on current gold plus the reward. That is not the behaviour in the merged code or its current tests. Keep this page and the Sprint 3 Test Plan aligned with the implementation until the team agrees on, implements and tests a revised rule.
Effect: Increases the player's maximum energy by 1 per copy.
1 Energy Crystal = +1 maximum energy
2 Energy Crystals = +2 maximum energy
The effect is reapplied each time the player entity is created.
Energy Crystal affects energy rather than health.
Effect: Applies a 10% shop discount per copy.
1 Merchant's Favor = 10% discount
2 Merchant's Favors = 20% discount
3 Merchant's Favors = 30% discount
4 Merchant's Favors = 40% discount
5+ Merchant's Favors = 50% discount
The maximum discount is 50%.
discount = Math.min(itemCount * 0.10f, 0.50f);The map shop reads the discount through the shared player-state gateway. This ensures the shop uses the same persistent run state as the reward and battle systems.
Effect: A player-turn combat consumable that grants +5 Armour when used.
- It remains in the inventory until the player presses USE.
- It cannot be used during the enemy turn.
- One successful use removes one copy from the inventory.
- The popup refreshes after a successful use; it does not currently create a separate toast notification.
Effect: A player-turn combat consumable that grants +1 Strength for the current battle when used.
- It remains in the inventory until the player presses USE.
- It cannot be used during the enemy turn.
- One successful use removes one copy from the inventory.
- The effect adds to existing Strength rather than replacing it.
- The popup refreshes after a successful use; it does not currently create a separate toast notification.
The inventory popup is available from the battle interface and displays the items currently owned by the player.
Each item entry contains:
- Item icon
- Formatted item name
- Short effect description
- For passive items, a right-side
x<count>quantity - For battle consumables, a right-side
USEbutton that is disabled outside the player's turn
The popup also displays:
- A title and close (
X) button - A scrollable item list
- A player-turn hint for battle consumables
Example:
INVENTORY
Lucky Coin x2
+10% generated Gold per copy
Energy Crystal x1
+1 Max Energy
Merchant's Favor x2
+10% Shop Discount | Max 50%
Iron Aegis [USE]
Use in battle: +5 Armour
Warrior's Crest [USE]
Use in battle: +1 Strength
The UI reads directly from PlayerRunState whenever it opens, ensuring it does not display stale inventory information.
The inventory popup uses a dark pixel-art style consistent with the battle screen.
Its visual elements include:
- A dark navy panel and antique-copper borders generated from the active skin
- Dedicated item icons
- Muted cream text
- A centred title, scrollable rows and player-turn hint
The UI artwork is stored under:
core/assets/images/ui/
Current inventory assets include:
lucky-coin.png
energy-crystal.png
merchants-favor.png
iron-aegis.png
warriors-crest.png
The inventory popup is created in BattleScreen.
The required inventory textures are loaded before the UI is created. The popup receives the current RunState, the live battle player, and the controller's player-turn predicate.
PopupDisplay itemInventory = new PopupDisplay("Item Inventory", CARD_INVENTORY_STYLE);
itemInventory.setMinSize(ITEM_INVENTORY_MIN_WIDTH, ITEM_INVENTORY_MIN_HEIGHT);
InventoryPopupComponent inventoryPopup =
new InventoryPopupComponent(
game.getRunState(), itemInventory, gameArea.getPlayer(), controller::isPlayerTurn);The popup uses its own entity so that its component lifecycle does not interfere with another popup or battle UI entity.
The popup currently closes through its on-screen close button. An ESC-key close action is not wired for this popup in the merged implementation.
Health, maximum health, gold and ownedItems are stored in PlayerRunState. Owned items survive screen and player-entity recreation. Maximum-energy and shop-discount effects are rebuilt from passive items. Lucky Coin is evaluated when a gold option is generated, while Iron Aegis and Warrior's Crest apply only when actively used.
Direct run values:
- Health
- Maximum health
- Gold
- Owned items
Derived item values:
- Lucky Coin claim bonus
- Shop discount
- Maximum energy
- Armour granted by an active Iron Aegis use
- Strength granted by an active Warrior's Crest use
GameStateSnapshotProvider serialises PlayerRunState.getOwnedItems() as stable ItemType.name() values in PlayerSaveData.ownedItems. SaveGameRestoreService validates every saved item name before mutating live state, then restores the complete ordered list through replaceOwnedItems().
- Duplicate copies and acquisition order are retained.
- A legacy save with no
ownedItemsfield restores an empty list safely. - An unknown, null or invalid item name rejects the restore before the live run is changed.
- Unused Iron Aegis and Warrior's Crest copies persist; an item already used in battle has been removed and is not restored.
PlayerItemEffectsSaveTest verifies the JSON round trip and derived-effect rebuild after a simulated relaunch. See Save/Load System Test Plan for the manual evidence checklist.
To add another reward item:
- Add the item to
ItemType.
public enum ItemType {
LUCKY_COIN,
ENERGY_CRYSTAL,
MERCHANTS_FAVOR,
NEW_ITEM
}- Implement its item effect.
public final class NewItemEffect {
public static void apply(Entity player) {
// Apply the item effect.
}
}- Register the effect in
ItemEffectApplier.
case NEW_ITEM -> NewItemEffect.apply(player);-
Add the item to the reward item pool.
-
Add its formatted display name.
-
Add its effect description to the inventory UI.
-
Add its icon to:
core/assets/images/ui/
-
Load the icon before the inventory popup is created.
-
Add tests covering:
- First application
- Multiple copies
- Persistence across battles
- Interaction with related systems
- Any stacking limit
The implementation is covered by tests for:
-
PlayerRunStateTest: immutable owned-item snapshots, duplicate counts, passive replay, battle-item use and invalid player state. -
ItemEffectTestandItemEffectApplierTest: individual effects and dispatch for everyItemType. -
RewardGeneratorTestandRewardIntegrationTest: deterministic generation, item-reward ownership and gold/discount integration. -
ItemLibraryScreenTest: all item artwork is preloaded and every item has a distinct player-facing name. -
PlayerSaveDataTestandPlayerItemEffectsSaveTest: JSON serialisation, legacy saves withoutownedItems, a full relaunch round trip and persistence of unconsumed battle items. - Manual checks: Battle-only inventory-popup interaction, player-turn restriction, icon/text readability and item Library navigation.
Tests can be run with:
./gradlew testFormatting can be applied with:
./gradlew spotlessApplyThis work integrates several Team 7 systems:
- Reward generation
- Reward claiming
- Player run state
- Item persistence
- Battle player creation
- Gold UI updates
- Shop discounts
- Inventory display
The inventory UI and initial shop gateway integration were developed on:
Feature7-Inventory
The reusable inventory structure, reward integration, Lucky Coin persistence and inventory UI improvements were completed on:
feature/reward-item-pool-expansion
Animation&item&cardupgrade
Changes to shared event names, player-state gateways or item-effect values should be coordinated with the owners of the battle UI and shop systems before merging.
The following work was completed as part of this Sprint 3 task:
- Fixed gold UI updates by standardising the
updateMoneyevent. - Connected gold rewards and item ownership to
PlayerRunState. - Added the
PlayerRunState.ownedItemscollection with add, count, immutable query, replacement and battle-item consumption operations. - Added support for duplicate and stackable items.
- Reapplied persistent passive effects once when a new player entity is created.
- Implemented Lucky Coin reward preview using the current persistent +10%-per-copy multiplier.
- Integrated Energy Crystal and Merchant's Favor as persistent passive items.
- Connected Merchant's Favor to the shop system with a 10% per-copy discount and 50% cap.
- Added Iron Aegis and Warrior's Crest to the reward pool.
- Added player-turn
USEactions and one-copy consumption for both combat items. - Added +5 Armour and +1 current-battle Strength effects with pixel-style feedback.
- Added a themed battle inventory popup with icons, quantities, descriptions and action states.
- Redesigned the victory reward screen with item-specific presentation and real Lucky Coin totals.
- Added the popup close button and live refresh after a successful consumable use.
- Added
ownedItemscapture, JSON serialisation, validation and restore with legacy-save compatibility. - Added and updated unit and integration tests.
- Ran the complete test suite, Spotless check and desktop compilation successfully.
- Updated Wiki documentation.
- Builds on the Team 7 inventory UI and shop integration work from
Feature7-Inventory. - Coordinates with the shop system through the existing player-state gateway.
- Uses
PlayerRunStateas the shared state between rewards, battles and shops. - Uses
ItemEffectApplierto apply passive item effects. - Uses the existing battle UI skin and resource-loading system.
- Iron Aegis and Warrior's Crest can be consumed during the player turn; other items cannot be manually discarded.
- Item order is based on acquisition order rather than category or rarity.
- Energy Crystal, Merchant's Favor and Lucky Coin are persistent modifiers; only Iron Aegis and Warrior's Crest are active battle consumables.
- New item types still require explicit registration in
ItemEffectApplier. - Lucky Coin's implemented persistent, stacking reward modifier differs from earlier consumable-and-cap planning copy; see Lucky Coin.
Planned follow-up work includes:
- Adding item rarity or reward weighting
- Adding tooltips or longer item descriptions
- Adding categories or sorting if the item pool becomes larger
- Extending the item pool in later sprints
- Updating tests and documentation as new items are added
- Team 7 feature issue: #267
- Gold reward and Lucky Coin integration: #271
- Inventory UI and item-effect persistence: #281
- Generic item inventory and reward item pool: #311
- Persistent item inventory integration: #316
Feature7-Inventory
feature/reward-item-pool-expansion
Animation&item&cardupgrade
- My code follows the style guidelines of this project.
- I have performed a self-review of my code.
- My changes generate no new warnings.
- I have added tests that prove the inventory and item effects work.
- New and existing unit tests pass locally.
- Wiki documentation has been updated.