Card Discovery and Library Lore - UQcsse3200/2026-studio-3 GitHub Wiki

Card Discovery and Library Lore

Team 6, Sprint 3 | Card discovery progress, global persistence and Card Library lore | Issue #342 | PRs #350, #351 | Branch: feature/sprint3-cards-and-story

The Card Library only reveals a card once the player has actually seen it during play. Discovery is global: it survives restarting the game and is never rolled back by loading an older save. Once a card is discovered, the Library also shows its optional background lore.

For the enemy equivalent, see Bestiary System. For card fields in general, see Card Data Model and Card Design and Naming Standards.

Data Ownership

Source Owner Purpose
configs/cards.json Each card's author (Team 6) Card definitions, including the optional lore field.
CardDiscoveryService Team 6 (Member 1) Unlock state for every configured card during the game process.
CardDiscoveryStore Team 6 (Member 1) Reads and writes the global discovery file.
CardLibraryDisplay Card Library (Luqishu Wu), lore row added by Team 6 Shows locked and discovered entries.
Save slots (GameStateSnapshotProvider, SaveGameRestoreService) Save/Load Also carry card progress; restoring merges it (see Persistence).

Related Pages

# Card Discovery and Library Lore

Team 6, Sprint 3 | Card discovery progress, global persistence and Card Library lore | Issue [#342](https://github.com/UQcsse3200/2026-studio-3/issues/342) | PRs [#350](https://github.com/UQcsse3200/2026-studio-3/pull/350), [#351](https://github.com/UQcsse3200/2026-studio-3/pull/351) | Branch: feature/sprint3-cards-and-story

The Card Library only reveals a card once the player has actually seen it during play. Discovery is global: it survives restarting the game and is never rolled back by loading an older save. Once a card is discovered, the Library also shows its optional background lore.

For the enemy equivalent, see [Bestiary System](Bestiary-System). For card fields in general, see [Card Data Model](Card-Data-Model) and [Card Design and Naming Standards](Card-Design-and-Naming-Standards).

Data Ownership

Source Owner Purpose
configs/cards.json Each card's author (Team 6) Card definitions, including the optional lore field.
CardDiscoveryService Team 6 (Member 1) Unlock state for every configured card during the game process.
CardDiscoveryStore Team 6 (Member 1) Reads and writes the global discovery file.
CardLibraryDisplay Card Library (Luqishu Wu), lore row added by Team 6 Shows locked and discovered entries.
Save slots (GameStateSnapshotProvider, SaveGameRestoreService) Save/Load Also carry card progress; restoring merges it (see [Persistence](#persistence)).

Cards are always identified by their stable id, never by display name. Upgraded copies use their base card's ID, so discovering Strike+ is the same as discovering strike.

Unlock States

State What the Library shows
LOCKED A ??? entry. Name, cost, type, target, rarity, effects, artwork and lore are all hidden.
SEEN The full card: rendered card face, name, description, cost, type, target, rarity, effects and lore (if any).

Progress only moves forward. Recording a card that is already SEEN is a no-op and fires no event. The counter at the top of the Library shows discovered / total.

When a Card Counts as Seen

A card is marked SEEN only when it is actually shown to the player, not when an offer is merely generated.

Trigger Where it is recorded
Starting a new run (starter deck) RunState, when the starter deck is created
Shop offers displayed ShopDisplay, for every displayed card
Event (chance encounter) card outcome displayed or committed ChanceEncounterDisplay, ChanceOutcomeApplier
Post-battle card reward choices opened RewardDisplay, for every visible candidate
Debug only: discoverallcards DiscoverAllCardsCommand (see [Debug Terminal](Debug-Terminal-—-Commands-and-Cheats))

Choosing the Gold or Item reward does not reveal the hidden card candidates.

Persistence

Discovery is stored in two places, and they are always merged, never replaced.

flowchart TD
    Start[Game starts] --> Load[CardDiscoveryStore.load]
    Load --> Merge1[CardDiscoveryService.mergeProgress]
    Seen[Card shown in starter deck, shop, event or reward] --> Record[recordSeen]
    Record --> Event[ENTRY_UPDATED_EVENT]
    Event --> Save[CardDiscoveryStore.save]
    Save --> File[(card_discovery.json)]
    Load -.reads.-> File
    Restore[Player loads a save slot] --> Merge2[SaveGameRestoreService merges saved card progress]
    Merge2 --> Event
Loading

Global file

  • Location: ~/DECO2800Game/card_discovery.json, in the same folder as settings.json and the saves folder. (DECO2800Game is the engine's existing data folder name.)
  • Shape:
{
  "progress": [
    { "cardId": "strike", "unlockState": "SEEN" },
    { "cardId": "poison_blade", "unlockState": "SEEN" }
  ]
}
  • Only non-locked cards are written. The file is rewritten each time a card becomes SEEN (at most once per card).
  • At startup, GdxGame loads the file and merges it into CardDiscoveryService before registering the save listener, so startup does not trigger a write.

Failure handling

The game never crashes because of this file.

Situation Behaviour
File missing Start with every card locked.
Malformed JSON or invalid structure Log a warning and start with every card locked.
One entry with a blank cardId Skip that entry only; keep the others.
Unknown unlockState Skip that entry only.
Card ID no longer in cards.json Ignored when merging.
Write fails Log a warning; the game continues.

Known limitation: the file is overwritten in place, so a crash mid-write could leave it malformed. The game would then start with empty discovery rather than crash.

Save slots

Saves still include card progress (ProgressSaveData.cards, non-locked entries only). When a save is loaded, SaveGameRestoreService calls CardDiscoveryService.mergeProgress, which can only upgrade a card from LOCKED to SEEN. Loading an older save therefore never re-locks cards discovered later.

The Bestiary still uses per-slot replace semantics (BestiaryService.replaceProgress); that is owned by Team 5 and was intentionally left unchanged.

Resetting discovery for testing

Delete ~/DECO2800Game/card_discovery.json and restart the game. Do not delete the whole folder, as it also holds settings and saves.

Library Lore

CardConfig has an optional lore field: background text shown in the Card Library once a card is discovered.

Rule Detail
Optional Cards without lore load and display normally.
Not blank If present, lore must not be empty or whitespace; CardValidator rejects it.
Library only Lore is not shown on the card face. The card face (CardWidget) renders description through CardResolver.
Upgrades Upgraded cards reuse the base card's lore; upgrade has no lore field.
Hidden while locked CardEntryView.lore() is empty while a card is LOCKED.
Layout Shown below the description in the Library detail panel and wraps to the panel width. If there is no lore, the row collapses and takes no space.

description vs lore

  • description is rules text for the card face. Keep it short so it fits at the real hand size.
  • lore is the background story for the Library.
  • Flavour sentences currently at the start of some descriptions should move into lore (see [Known Issues](#known-issues)).

Adding lore to a card

{
  "id": "poison_blade",
  "name": "Poison Blade",
  "description": "Deal 10 piercing damage, ignoring Block and Armour. Apply 4 Poison for 2 turns.",
  "lore": "This blade was kept for the fallen who hid behind corrupted armour. Its purpose was to reach them, not to end them.",
  "...": "other fields unchanged"
}

CardConfigLoader rejects unknown JSON fields, so lore only works on branches that include the lore-field change ([#351](https://github.com/UQcsse3200/2026-studio-3/pull/351)). Lore content is coordinated by Ziqin (Member 4); each card's author owns their own entries.

Design Diagrams

Each card is either LOCKED or SEEN. Progress only moves forward.

stateDiagram-v2
    [*] --> LOCKED
    LOCKED --> SEEN: recordSeen, or merge of a higher saved state
    SEEN --> SEEN: repeated recordSeen or any merge
    note right of SEEN
        No production path returns a card to LOCKED
    end note
Loading

How a card is discovered, saved globally, restored at start-up, and merged when a save slot is loaded.

sequenceDiagram
    actor Player
    participant Source as Shop, reward, chance or new run
    participant Discovery as CardDiscoveryService
    participant Game as GdxGame
    participant Store as CardDiscoveryStore
    participant Restore as SaveGameRestoreService

    Note over Game,Store: Game start
    Game->>Store: load global progress
    Store-->>Game: valid entries only (empty if missing or unreadable)
    Game->>Discovery: mergeProgress(global progress)
    Game->>Discovery: listen for ENTRY_UPDATED_EVENT

    Note over Player,Store: During play
    Player->>Source: card is shown or received
    Source->>Discovery: recordSeen / recordSeenAll
    alt card is LOCKED
        Discovery->>Discovery: set SEEN
        Discovery-->>Game: ENTRY_UPDATED_EVENT
        Game->>Store: save non-locked snapshot
    else already SEEN or unknown ID
        Discovery->>Discovery: no change
    end

    Note over Restore,Store: Loading a save slot
    Restore->>Restore: validate the whole save first
    Restore->>Discovery: mergeProgress(saved card progress)
    Discovery->>Discovery: keep the higher state per card
    Discovery-->>Game: ENTRY_UPDATED_EVENT for advanced cards
    Game->>Store: save non-locked snapshot
Loading

Known Limitations

  • If card_discovery.json is deleted or unreadable, the game logs a warning and starts with every card LOCKED instead of crashing. Loading a save slot still merges that slot's card progress back in.
  • CardDiscoveryService.replaceProgress can lower a card's state, but it has no production caller. Card restoration uses mergeProgress, and any future caller should do the same to keep the forward-only guarantee.

Test Coverage

Automated

Test Covers
CardDiscoveryServiceTest Recording, replace, monotonic mergeProgress (no downgrade, unknown IDs ignored, invalid input rejected without mutation, events only for changed entries).
CardDiscoveryStoreTest Missing file, save/load round trip, locked entries not written, malformed file, invalid entries skipped, unknown states ignored. Uses @TempDir, never the real home folder.
SaveGameRestoreServiceTest restoresCardProgressAndSkipsUnknownIds, restoringSaveKeepsCardsDiscoveredOutsideTheSave (undiscovered cards stay locked).
GameStateSnapshotProviderTest capturesOnlySeenCardProgress.
RunStateTest creatingNewRunDeckMarksStarterCardIdsSeen.
ShopDisplayTest shouldRecordEveryDisplayedShopCardSeen.
ChanceEncounterDisplayTest shouldRecordEventCardSeenOnlyWhenSuccessfulOutcomeIsDisplayed.
RewardDisplayTest openingCardShowsBaseWidgetsAndMarksEveryVisibleCandidateSeen, goldAndItemClaimsDoNotRevealHiddenCardCandidates.
CardConfigLoaderTest shouldLoadCardsWithAndWithoutOptionalLore.
CardValidatorTest shouldAcceptNullLoreAndRejectBlankLore.
CardEntryViewTest Lore present when seen, hidden when locked, empty when absent.
CardLibraryDisplayTest shouldShowLoreOnlyForDiscoveredCardsThatHaveIt.

Run with cd source && ./gradlew :core:test.

Manual verification

Performed on feature/sprint3-cards-and-story after #350, starting from a deleted card_discovery.json.

Test 1 — discovery survives a restart

  1. Open the Card Library from the main menu: every card is locked (0 / 26 discovered).

    [Screenshot 1: Library with 0 / 26 discovered]

screenshot1
  1. Start a new run and return to the Library: only the six starter cards are discovered (6 / 26 discovered).
2

[Screenshot 2: Library with 6 / 26 discovered after starting a new run]

  1. Enter a shop. It offered Break the Seal (unseal_the_breach), Poison Blade (poison_blade) and Expose. Quit without saving. card_discovery.json now contains the six starter cards plus poison_blade and unseal_the_breach.
3

[Screenshot 3: Wayside Reliquary shop offers]

  1. Restart the game and open the Library without loading a save or starting a run: 8 / 26 discovered, with Poison Blade and Break the Seal still revealed.
4

[Screenshot 4: Library after restart showing 8 / 26 discovered]

Test 2 — loading an older save does not re-lock cards

Covered by the automated test SaveGameRestoreServiceTest.restoringSaveKeepsCardsDiscoveredOutsideTheSave. Manual in-game check pending:

  1. Start a new run and save before visiting any shop or battle.
  2. Visit a shop and see new cards.
  3. Return to the main menu, load the earlier save and open the Library: the cards seen after that save should remain discovered.

Known Issues

Issue Owner
In the shop, new cards show only their illustration (no name, cost or description); see Screenshot 3. ShopDisplay draws texturePath directly instead of the dynamic CardWidget. The six original cards look complete only because their old concept art has text baked in. Card face (Ray) / Shop (Zeyu) — to be confirmed
Long descriptions overflow the card face: on Poison Blade the last line is cut off (see Screenshot 4). Caused by flavour sentences at the start of description; fix by moving them into lore. Card authors; card face (Ray)
The Library detail panel shows the developer artwork path (Artwork: images/cards/...). Card Library (Luqishu Wu)

Related Pages

⚠️ **GitHub.com Fallback** ⚠️