Bestiary System - UQcsse3200/2026-studio-3 GitHub Wiki
Team 5, Sprints 2–3 | Enemy definitions, discovery progress, Library UI and Save/Load integration | Core integration merged through PR #405; discovery-only Library behaviour merged through PR #409
The Bestiary is the game's progress-aware enemy encyclopedia. Team 1's enemy roster is the single source of truth for each enemy's combat facts and backstory description. Team 5 owns the discovery rules, Bestiary UI and persistence. For the player-facing roster and artwork, see Enemy Bestiary.
| Source | Owner | Purpose |
|---|---|---|
configs/enemies.json |
Enemy System (Team 1) | Stable ID, name, tier, base stats, behaviour, sprite path and description. |
EnemyConfig |
Enemy System (Team 1) | Loaded representation of one enemy definition, including its description. |
BestiaryService |
Bestiary (Team 5) | Builds read-only Bestiary entries from EnemyConfig and owns unlock progress for the active game. |
The Bestiary never identifies an enemy from UI text or artwork. enemyId is the contract shared by enemy creation, tracking, display and saves.
| State | Visible information |
|---|---|
LOCKED |
Internal progress state only. It is deliberately excluded from the player-visible list: no row, silhouette, ???, or detail panel is shown. |
ENCOUNTERED |
Name and artwork; combat stats and description remain hidden. |
DEFEATED |
Full description, health, attack, armour and behaviour. |
Progress only moves forward. Recording an encounter or defeat again is a no-op, and a direct defeat also implies the enemy has been encountered.
flowchart LR
Factory[EnemyFactory] -->|stable enemyId| Tracker[BestiaryTrackingComponent]
Tracker -->|entity created| Encountered[recordEncountered]
Damage[CombatStatsComponent.takeDamage] --> EnemyStats[EnemyStatsComponent]
EnemyStats -->|enemyDefeated event| Tracker
Tracker --> Defeated[recordDefeated]
Encountered --> Service[BestiaryService]
Defeated --> Service
Service -->|ENTRY_UPDATED_EVENT| Display[BestiaryDisplay]
EnemyFactory attaches BestiaryTrackingComponent only when the current BestiaryService contains the configured ID. The component records ENCOUNTERED when the entity is created and listens for the existing enemyDefeated lifecycle event. The UI subscribes to ENTRY_UPDATED_EVENT, refreshes the active tier immediately and unregisters when disposed.
The class diagram below shows the complete Bestiary boundary: Team 1 combat configuration enters on the left, unlock progress is managed in the centre, and the UI and Save/Load adapters consume the service on the right.
classDiagram
direction TB
class GdxGame {
-BestiaryService bestiaryService
+getBestiaryService() BestiaryService
+setScreen(ScreenType screenType) void
}
class ServiceLocator {
+registerBestiaryService(BestiaryService service) void
+getBestiaryService() BestiaryService
}
class EnemyConfig {
<<external>>
+String id
+String name
+EnemyTier tier
+int health
+int baseAttack
+int armour
+String behaviour
+String sprite
+String description
}
class EnemyTier {
<<enumeration>>
NORMAL
ELITE
BOSS
}
class BestiaryEntry {
<<record>>
+enemyId() String
+name() String
+tier() EnemyTier
+health() int
+baseAttack() int
+armour() int
+behaviour() String
+sprite() String
+description() String
+fromEnemyConfig(EnemyConfig config) BestiaryEntry
}
class BestiaryUnlockState {
<<enumeration>>
LOCKED
ENCOUNTERED
DEFEATED
+isAtLeast(BestiaryUnlockState required) boolean
}
class BestiaryEntryView {
<<record>>
+enemyId() String
+tier() EnemyTier
+unlockState() BestiaryUnlockState
+displayName() String
+sprite() Optional
+description() Optional
+health() OptionalInt
+baseAttack() OptionalInt
+armour() OptionalInt
+behaviour() Optional
+hasFullDetails() boolean
}
class EventHandler {
+addListener(String eventName, EventListener listener) void
+removeListener(String eventName, EventListener listener) void
+trigger(String eventName, Object payload) void
}
class BestiaryService {
+String ENTRY_UPDATED_EVENT
-Map entries
-Map progress
-EventHandler events
+loadDefault() BestiaryService
+contains(String enemyId) boolean
+getEntries() List
+getEntriesByTier(EnemyTier tier) List
+getEntry(String enemyId) Optional
+getProgressSnapshot() Map
+replaceProgress(Map restoredProgress) void
+recordEncountered(String enemyId) boolean
+recordDefeated(String enemyId) boolean
-advance(String enemyId, BestiaryUnlockState target) boolean
-viewFor(String enemyId) BestiaryEntryView
}
class EnemyFactory {
+create(String id) Entity
+create(String id, int floor) Entity
+create(EnemyConfig config) Entity
}
class EnemyStatsComponent {
+create() void
-onHealthUpdated(int health, int maxHealth) void
}
class BestiaryTrackingComponent {
-String enemyId
-BestiaryService bestiary
+create() void
+getEnemyId() String
-onEnemyDefeated() void
}
class BestiaryScreen {
-GdxGame game
-BestiaryService bestiary
-Renderer renderer
-String[] atlasPaths
+render(float delta) void
+resize(int width, int height) void
+dispose() void
-createUI() void
}
class BestiaryDisplay {
-BestiaryService bestiary
-EnemyTier activeTier
-Runnable returnAction
+create() void
-applyFilter(EnemyTier tier) void
-rebuildEnemyList() void
-showDetails(BestiaryEntryView entry) void
-onEntryUpdated(BestiaryEntryView entry) void
+dispose() void
}
class SaveGameData {
+ProgressSaveData progress
}
class ProgressSaveData {
+String pendingRewardId
+String resumeScreen
+List bestiary
}
class BestiaryProgressSaveData {
+String enemyId
+String unlockState
}
class GameStateSnapshotProvider {
-BestiaryService bestiaryService
+capture() SaveGameData
-captureProgress() ProgressSaveData
}
class SaveGameRestoreService {
-BestiaryService bestiaryService
+restore(SaveGameData data) RestoreResult
-validateBestiaryProgress(ProgressSaveData progress) RestoreResult
-restoreBestiaryProgress(ProgressSaveData progress) void
}
EnemyConfig --> EnemyTier : classifies
BestiaryEntry ..> EnemyConfig : copies combat data
BestiaryEntry --> EnemyTier : belongs to
BestiaryService "1" *-- "0..*" BestiaryEntry : definitions by enemyId
BestiaryService --> BestiaryUnlockState : stores monotonic progress
BestiaryService *-- EventHandler : publishes entryUpdated
BestiaryService ..> BestiaryEntryView : creates redacted views
BestiaryEntryView --> BestiaryUnlockState : exposes state
BestiaryEntryView --> EnemyTier : exposes tier
GdxGame "1" o-- "1" BestiaryService : game-session lifetime
GdxGame ..> ServiceLocator : registers service per screen
ServiceLocator o-- BestiaryService : shared reference
EnemyFactory ..> ServiceLocator : resolves service
EnemyFactory ..> BestiaryTrackingComponent : attaches when ID is known
EnemyFactory ..> EnemyStatsComponent : attaches combat lifecycle
BestiaryTrackingComponent "0..*" --> "1" BestiaryService : recordEncountered / recordDefeated
EnemyStatsComponent ..> BestiaryTrackingComponent : enemyDefeated event
BestiaryScreen --> GdxGame : navigation
BestiaryScreen --> BestiaryService : obtains shared progress
BestiaryScreen "1" *-- "1" BestiaryDisplay : creates UI component
BestiaryDisplay --> BestiaryService : queries and subscribes
BestiaryDisplay ..> BestiaryEntryView : renders safe fields
BestiaryDisplay --> EventHandler : add/remove entryUpdated listener
GameStateSnapshotProvider --> BestiaryService : reads progress snapshot
GameStateSnapshotProvider ..> SaveGameData : creates
SaveGameData "1" *-- "1" ProgressSaveData : contains
ProgressSaveData "1" *-- "0..*" BestiaryProgressSaveData : serialises non-locked entries
SaveGameRestoreService --> SaveGameData : validates and restores
SaveGameRestoreService --> BestiaryService : atomic replaceProgress
Relationship notation:
- A filled diamond (
*--) means the source owns the contained data or component. - A hollow diamond (
o--) means a shared object is retained for the game session. - A dashed arrow (
..>) means creation, transformation or event delivery rather than ownership. - A solid arrow (
-->) means a stored reference or direct runtime call.
The important architectural constraints are:
-
enemyIdis the only cross-system identity; display names and atlas paths are never used as keys. -
BestiaryEntryViewis the presentation boundary and removes fields that the current unlock state must not reveal. -
BestiaryServiceis the sole writer of unlock progress and only permitsLOCKED -> ENCOUNTERED -> DEFEATEDadvancement during play. - Restore validates the complete Bestiary payload before calling
replaceProgress(), preventing a malformed save from applying a partial state.
The main menu opens LibraryScreen. Its Enemy Library action opens BestiaryScreen, which builds BestiaryDisplay from the game's shared service. The Back action returns to the Library.
The display provides:
- Normal, Elite and Boss filters.
- A scrollable enemy list and detail panel.
- Only encountered and defeated enemies appear in the list. A category with no discovered enemies displays
No enemies discovered in this category yet.and has no selected detail record. - Progress-aware names, descriptions, stats and behaviour.
- Enemy artwork loaded from the roster's atlas path, with the default enemy atlas as fallback.
GameStateSnapshotProvider stores every non-locked entry as BestiaryProgressSaveData(enemyId, unlockState) inside ProgressSaveData.bestiary. SaveGameRestoreService validates IDs, duplicate entries and enum values before applying any loaded state, then restores progress through BestiaryService.replaceProgress().
Old saves without a bestiary field are treated as an empty list, so all entries remain locked. IDs removed from a later roster are safely ignored during restore. See Save/Load System for the complete save format.
"progress": {
"pendingRewardId": "",
"resumeScreen": "MAP",
"bestiary": [
{ "enemyId": "lesser_shade", "unlockState": "DEFEATED" },
{ "enemyId": "boss_knight", "unlockState": "ENCOUNTERED" }
]
}- Team 1 adds the combat definition and atlas using a stable ID in
configs/enemies.json. - Team 1 adds the player-facing
descriptionto that same enemy definition. There is no separatebestiary.jsonorBestiaryConfigto keep in sync. - Confirm the ID appears in the expected tier and that the atlas contains
defaultoridleartwork. - Test that the fresh record is absent from the player-visible list, then test encountered, defeated and save/restore behaviour.
The complete rationale, case matrix, manual steps and evidence placeholders are maintained in Bestiary System Test Plan.
-
BestiaryServiceTest: default roster andEnemyConfig.description, unlock rules, discovered-only tier queries, events and progress replacement. -
BestiaryTrackingComponentTest: real damage throughenemyDefeatedtoDEFEATED. -
EnemyFactoryTest: production component wiring and the complete defeat path. -
BestiaryDisplayTest: empty discovered-category state, revealed content, live event refresh and listener cleanup. -
GameStateSnapshotEndToEndTest: Bestiary progress across save, relaunch and restore.