Bestiary System - UQcsse3200/2026-studio-3 GitHub Wiki

Bestiary System

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.

Data Ownership

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.

Unlock States

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.

Runtime Flow

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]
Loading

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.

Architecture UML

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
Loading

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:

  1. enemyId is the only cross-system identity; display names and atlas paths are never used as keys.
  2. BestiaryEntryView is the presentation boundary and removes fields that the current unlock state must not reveal.
  3. BestiaryService is the sole writer of unlock progress and only permits LOCKED -> ENCOUNTERED -> DEFEATED advancement during play.
  4. Restore validates the complete Bestiary payload before calling replaceProgress(), preventing a malformed save from applying a partial state.

Library UI

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.

Save/Load Integration

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" }
  ]
}

Adding an Enemy

  1. Team 1 adds the combat definition and atlas using a stable ID in configs/enemies.json.
  2. Team 1 adds the player-facing description to that same enemy definition. There is no separate bestiary.json or BestiaryConfig to keep in sync.
  3. Confirm the ID appears in the expected tier and that the atlas contains default or idle artwork.
  4. Test that the fresh record is absent from the player-visible list, then test encountered, defeated and save/restore behaviour.

Test Coverage

The complete rationale, case matrix, manual steps and evidence placeholders are maintained in Bestiary System Test Plan.

  • BestiaryServiceTest: default roster and EnemyConfig.description, unlock rules, discovered-only tier queries, events and progress replacement.
  • BestiaryTrackingComponentTest: real damage through enemyDefeated to DEFEATED.
  • 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.

Related Pages

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