Card Discovery and Library Lore - UQcsse3200/2026-studio-3 GitHub Wiki
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.
| 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). |
- Bestiary System
- Card Data Model
- Card Design and Naming Standards
- Card Library API
- Save Load System
- Debug Terminal — Commands and Cheats
- The Fall of the Pantheon Story
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).
| 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.
| 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.
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.
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
- Location:
~/DECO2800Game/card_discovery.json, in the same folder assettings.jsonand thesavesfolder. (DECO2800Gameis 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,
GdxGameloads the file and merges it intoCardDiscoveryServicebefore registering the save listener, so startup does not trigger a write.
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.
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.
Delete ~/DECO2800Game/card_discovery.json and restart the game. Do not delete the whole folder, as it also holds settings and saves.
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. |
-
descriptionis rules text for the card face. Keep it short so it fits at the real hand size. -
loreis the background story for the Library. - Flavour sentences currently at the start of some descriptions should move into
lore(see [Known Issues](#known-issues)).
{
"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.
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
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
- If
card_discovery.jsonis 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.replaceProgresscan lower a card's state, but it has no production caller. Card restoration usesmergeProgress, and any future caller should do the same to keep the forward-only guarantee.
| 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.
Performed on feature/sprint3-cards-and-story after #350, starting from a deleted card_discovery.json.
Test 1 — discovery survives a restart
-
Open the Card Library from the main menu: every card is locked (
0 / 26 discovered).[Screenshot 1: Library with 0 / 26 discovered]
- Start a new run and return to the Library:
only the six starter cards are discovered (
6 / 26 discovered).
[Screenshot 2: Library with 6 / 26 discovered after starting a new run]
- Enter a shop. It offered Break the Seal (
unseal_the_breach), Poison Blade (poison_blade) and Expose. Quit without saving.card_discovery.jsonnow contains the six starter cards pluspoison_bladeandunseal_the_breach.
[Screenshot 3: Wayside Reliquary shop offers]
- 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.
[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:
- Start a new run and save before visiting any shop or battle.
- Visit a shop and see new cards.
- Return to the main menu, load the earlier save and open the Library: the cards seen after that save should remain discovered.
| 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) |
- [Bestiary System](Bestiary-System)
- [Card Data Model](Card-Data-Model)
- [Card Design and Naming Standards](Card-Design-and-Naming-Standards)
- [Card Library API](Card-Library-API)
- [Save Load System](Save-Load-System)
- [Debug Terminal — Commands and Cheats](Debug-Terminal-—-Commands-and-Cheats)
- [The Fall of the Pantheon Story](The-Fall-of-the-Pantheon-Story)