Inventory and Item Persistence - UQcsse3200/2026-studio-3 GitHub Wiki

Team 7 — Inventory and Item Persistence

Overview

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

Goals

The main goals of this Sprint 3 work are:

  1. Introduce a reusable inventory data structure.
  2. Persist owned items for the duration of a run.
  3. Support multiple copies of the same item.
  4. Reapply persistent passive effects whenever the player entity is recreated.
  5. Connect Lucky Coin to the gold reward system.
  6. Connect Merchant's Favor to the shop system.
  7. Display the player's items through the battle inventory UI.
  8. Provide a foundation for expanding the reward item pool.

Architecture

The inventory system is divided into four main responsibilities:

Reward selection
      |
      v
PlayerRunState
      |
      v
PlayerRunState.ownedItems
      |
      v
ItemEffectApplier
      |
      v
Player components

Reward Selection

When the player claims an item reward, the selected ItemType is added to the player's persistent inventory.

PlayerRunState

PlayerRunState owns the inventory for the current run. It provides the integration point between rewards, battle screens, shops and the player entity.

Owned-item collection

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

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.


Owned items in PlayerRunState

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 == 2

The returned item list is immutable so external code cannot accidentally modify the persistent run state.


PlayerRunState Integration

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.


Reward Claiming

Rewards can be either gold or items.

Gold Rewards

When a gold reward is claimed:

  1. RewardGenerator chooses a base reward from 20–30 gold.
  2. Every owned Lucky Coin contributes a 10% multiplier to that base reward.
  3. The generated final amount is rounded and displayed before selection.
  4. Choosing the reward stores that final amount in PlayerRunState.
  5. Lucky Coin remains owned; item rewards also leave it unchanged.
finalGold = round(baseReward × (1 + 0.10 × luckyCoinCount))

Item Rewards

When an item reward is claimed:

  1. The selected ItemType is added to PlayerRunState.ownedItems.
  2. Passive items remain owned for the run; consumable items remain owned until they are used.
  3. Persistent passive effects are replayed whenever a new player entity is created.
  4. Active battle items remain stored until the player uses them during a valid player turn.
  5. 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 UI Event

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.


Supported Items

Lucky Coin

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.

Energy Crystal

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.

Merchant's Favor

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.

Iron Aegis

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.

Warrior's Crest

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.

Inventory UI

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 USE button 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.

Visual Design

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

Screen Integration

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.


Save and Load Considerations

Live-run persistence

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

Save-file persistence

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 ownedItems field 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.


Adding a New Item

To add another reward item:

  1. Add the item to ItemType.
public enum ItemType {
  LUCKY_COIN,
  ENERGY_CRYSTAL,
  MERCHANTS_FAVOR,
  NEW_ITEM
}
  1. Implement its item effect.
public final class NewItemEffect {
  public static void apply(Entity player) {
    // Apply the item effect.
  }
}
  1. Register the effect in ItemEffectApplier.
case NEW_ITEM -> NewItemEffect.apply(player);
  1. Add the item to the reward item pool.

  2. Add its formatted display name.

  3. Add its effect description to the inventory UI.

  4. Add its icon to:

core/assets/images/ui/
  1. Load the icon before the inventory popup is created.

  2. Add tests covering:

  • First application
  • Multiple copies
  • Persistence across battles
  • Interaction with related systems
  • Any stacking limit

Testing

The implementation is covered by tests for:

  • PlayerRunStateTest: immutable owned-item snapshots, duplicate counts, passive replay, battle-item use and invalid player state.
  • ItemEffectTest and ItemEffectApplierTest: individual effects and dispatch for every ItemType.
  • RewardGeneratorTest and RewardIntegrationTest: deterministic generation, item-reward ownership and gold/discount integration.
  • ItemLibraryScreenTest: all item artwork is preloaded and every item has a distinct player-facing name.
  • PlayerSaveDataTest and PlayerItemEffectsSaveTest: JSON serialisation, legacy saves without ownedItems, 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 test

Formatting can be applied with:

./gradlew spotlessApply

Team Coordination

This 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.


Sprint 3 Contributions

The following work was completed as part of this Sprint 3 task:

  • Fixed gold UI updates by standardising the updateMoney event.
  • Connected gold rewards and item ownership to PlayerRunState.
  • Added the PlayerRunState.ownedItems collection 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 USE actions 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 ownedItems capture, 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.

Dependencies

  • 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 PlayerRunState as the shared state between rewards, battles and shops.
  • Uses ItemEffectApplier to apply passive item effects.
  • Uses the existing battle UI skin and resource-loading system.

Current Limitations

  • 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.

Future Work

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

Related Issues

  • 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

Related Branches

Feature7-Inventory
feature/reward-item-pool-expansion
Animation&item&cardupgrade

Checklist

  • 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.
⚠️ **GitHub.com Fallback** ⚠️