Player HUD - UQcsse3200/2026-studio-2 GitHub Wiki

Player HUD

Introduction

The Player HUD handles all components and features visually displayed on the game screen relating to the player. It gives the player constant feedback on their health, equipment, resources and level context without interrupting gameplay. It is drawn as a screen-space overlay, independent of the world camera, so it stays fixed while the level scrolls.

Player's Health

The Player HUD displays the player's health as a row of heart icons in the top-left corner of the screen, and the player's hotbar as a row of item slots along the bottom edge. Hearts disappear (with a flicker animation) as the player takes damage, and the row grows automatically if the player's maximum health increases (e.g. an earned extra life); the hotbar redraws itself whenever the inventory or selection changes.

Architecture

classDiagram
    class UIComponent {
        <<abstract>>
    }

    class PlayerStatsDisplay {
        -Table root
        -Group heartGroup
        -List~Image~ heartImages
        -Texture heartTexture
        -CombatStatsComponent combatStats
        +create()
        +updatePlayerHealthUI(int health)
        -growHeartsTo(int desiredCount)
        -flickerAndHide(Image heart)
    }

    class CombatStatsComponent {
        -int health
        -int maxHealth
        -int baseAttack
        +getHealth() int
        +getMaxHealth() int
        +setHealth(int)
        +addHealth(int)
        +addMaxHealth(int)
        +hit(CombatStatsComponent attacker)
    }

    UIComponent <|-- PlayerStatsDisplay
    PlayerStatsDisplay --> CombatStatsComponent : reads
    CombatStatsComponent ..> PlayerStatsDisplay : "updateHealth" event
Loading

UML class diagram of PlayerStatsDisplay: it extends UIComponent, reads CombatStatsComponent, composes Table/Group/Image/Texture actors, and depends on ServiceLocator

PlayerStatsDisplay

A UIComponent attached to the player entity. Owns the heart row and keeps it in sync with the player's health:

  • Builds/grows heart Image actors in a positioned Group, layered over a background using a Stack inside the root Table.
  • Listens for the updateHealth event fired by CombatStatsComponent.
  • Plays a flicker-then-hide animation on a heart the instant it's lost.

CombatStatsComponent

Tracks health (current) and maxHealth (the ceiling the HUD sizes itself against), separately:

  • getHealth() / setHealth() / addHealth() — current-health API; health is clamped to [0, maxHealth].
  • getMaxHealth() — used by the HUD to compute how many hearts should exist.
  • addMaxHealth(int amount) — increases maxHealth and heals by the same amount. This is the hook for an "earn an extra life" mechanic.

Player's Health Lifecycle

Each heart is worth HP_PER_HEART (2) points of health. The number of hearts shown is derived using ceiling division:

int maximumHearts = Math.max(1, (combatStats.getMaxHealth() + HP_PER_HEART - 1) / HP_PER_HEART);

Odd health values are supported: 10 HP shows five hearts, 9 HP still shows five, and 8 HP shows four. The row rounds up rather than displaying a fractional heart for every single HP.

updatePlayerHealthUI(int health) runs on every updateHealth event:

  1. Grows the heart row (growHeartsTo) if maxHealth has increased since the last update — this is what makes an earned extra life show up automatically, with no separate "add a heart" call needed anywhere else.
  2. Clamps the supplied health and computes visible hearts using (cappedHealth + HP_PER_HEART - 1) / HP_PER_HEART.
  3. For each heart, compares its current visibility to what it should be:
    • Visible → should hide: plays the flicker animation, then hides it.
    • Hidden → should show: reappears instantly (health restored / new heart earned), no animation.
    • No change: left alone. This comparison is what stops the flicker from re-triggering every time the event fires.

The flicker itself uses libGDX's Actions API — fade out/in a few times, then hide and reset the actor's colour so it's ready to reappear at full opacity later:

heart.addAction(
    Actions.sequence(
        Actions.repeat(FLICKER_COUNT,
            Actions.sequence(Actions.fadeOut(FLICKER_DURATION), Actions.fadeIn(FLICKER_DURATION))),
        Actions.run(() -> {
          heart.setVisible(false);
          heart.setColor(1f, 1f, 1f, 1f);
        })));

Usage

Key Public Methods

Method Purpose
combatStats.getMaxHealth() Read the player's current max health
combatStats.addMaxHealth(amount) Increase max health and heal by amount — use this to grant an extra life/heart
combatStats.addHealth(amount) Heal or damage without changing the heart count
playerStatsDisplay.updatePlayerHealthUI(health) Normally driven by the updateHealth event; rarely called directly

Granting an extra life

CombatStatsComponent stats = player.getComponent(CombatStatsComponent.class);
stats.addMaxHealth(2); // one more heart's worth of max health

The next updateHealth event (fired automatically by addMaxHealth → addHealth → setHealth) will grow the heart row and show the new heart as full.

Inventory Hotbar

InventoryBarDisplay

A second UIComponent on the player entity, positioned along the bottom edge of the screen. It renders the first row of the inventory (the hotbar) as a row of Table-based slots, each drawn with a rounded nine-patch background rather than an image atlas:

classDiagram
    class UIComponent {
        <<abstract>>
    }

    class InventoryBarDisplay {
        -Table root
        -Table table
        +create()
        -populateSlots()
        -createSlot(ItemType item, int count, boolean selected) Table
        -createEmptySlot(boolean selected) Table
        -refresh()
        -hideBar()
        -showBar()
    }

    class InventoryComponent {
        -List~InventorySlot~ slots
        +getHotbarSlotCount() int
        +getSelectedSlotIndex() int
        +getSlot(int index) InventorySlot
    }

    UIComponent <|-- InventoryBarDisplay
    InventoryBarDisplay --> InventoryComponent : reads (fetched live, not cached)
    InventoryComponent ..> InventoryBarDisplay : "inventoryChanged" event
    InventoryBarDisplay ..> InventorySlotStyle : styles slots
Loading

UML class diagram of InventoryBarDisplay: it extends UIComponent, reads InventoryComponent live on every refresh, and depends on InventorySlotStyle and ServiceLocator

InventoryComponent and InventorySlot are the data model; see Player Inventory for how items are added, stacked and swapped. This page only covers how that state gets drawn.

  • Listens for four events fired on the player entity: inventoryChanged and inventorySelectionChanged both trigger a full populateSlots() rebuild; backpackOpened / backpackClosed hide and show the hotbar so it doesn't compete with the full inventory grid.
  • populateSlots() clears the table and rebuilds one slot per hotbar index (inventory.getHotbarSlotCount()), asking InventoryComponent for the slot at each index rather than keeping its own copy of the state.
  • Each slot's background comes from InventorySlotStyle — a shared, cached NinePatchDrawable per state (normal vs. selected), rather than a separate texture per slot.
  • Occupied slots (createSlot) show the item's icon (looked up via ItemType.getTexturePath()) and a x<count> label; empty slots (createEmptySlot) are just the background.

Hotbar Lifecycle

Unlike the heart row, the hotbar is rebuilt rather than incrementally updated — populateSlots() calls table.clearChildren() and recreates every slot actor from scratch on every inventoryChanged / inventorySelectionChanged event. This is simpler than the heart row's pool-and-hide approach, but means the hotbar redraws in full for a change to any one slot (e.g. selecting a different item redraws all eight slots, not just the two whose selection state changed).

Tuning

Setting Value Purpose
HP_PER_HEART 2 Health represented by each visible heart
FLICKER_COUNT 3 Repetitions when hiding a lost heart
FLICKER_DURATION 0.1 seconds Duration of each fade-out/fade-in step
SLOT_WIDTH / SLOT_HEIGHT 90px Size of each hotbar slot
SLOT_SPACING 10px Gap between hotbar slots
ICON_SIZE 56px Size of the item icon drawn inside a slot

These UI values are separate from player health and i-frame configuration in Player Health and Damage.

Game Currency

The player's gold is shown in the HUD directly below the hearts, as a number followed by a gold coin icon (images/items/gold_coin.png, drawn 40x40px). It is built by PlayerStatsDisplay in the same root Table as the health row.

  • Gold is stored on InventoryComponent (getGold(), hasGold(int), addGold(int)). addGold clamps the result to [0, Integer.MAX_VALUE], so gold can never go negative.
  • The player starts with the gold value from configs/player.json (currently 50), passed to new InventoryComponent(stats.gold) in PlayerFactory.
  • PlayerStatsDisplay listens for the goldChanged event on the player and rewrites the label text, so the counter updates the moment gold is picked up or spent. The label is only created if the player has an InventoryComponent.
  • Gold is earned by pressing F near a gold pickup (PlayerInteractionComponent.pickupGold), which adds the pickup's amount and fires goldPickedUp. It is spent in the shop.

Gold counter and health bar in the HUD

Arrow ScrollWheel

Holding Tab while a bow is equipped opens a wheel for choosing the arrow type; releasing Tab confirms the choice. The state and input logic are covered in Arrow Selection Wheel; this section covers how it looks.

Arrow wheel

  • The wheel is three icon tiles arranged in a triangle: the standard arrow on top, the ice arrow bottom-left and the fire arrow bottom-right.
  • Each tile is a rounded dark-teal box matching the hotbar slot style, with the arrow's own icon centred in it.
  • The tile currently pointed at gets the brown border used for the selected hotbar slot (fire in the screenshot above).

Item Dictionary

Pressing I toggles the item dictionary, a full-screen overlay listing every ItemType. The data and discovery rules are covered in Item Dictionary; this section covers the UI.

ItemDictionaryDisplay has two pages, switched by a DictionaryPage enum:

  • Grid page - a title and a 5-column grid of 110x110px slots, one per ItemType. Discovered items show their icon; undiscovered items show a ? placeholder and cannot be clicked.
  • Details page - opened by clicking a discovered slot. Shows the item's icon, name and description, plus only the stats that apply to it (damage, range, cooldown, heal amount, burn damage/time, slow speed/time, speed boost, duration, poison damage/duration). A < Back label returns to the grid.

An item is discovered the first time the player picks it up (ItemDictionaryComponent listens for itemPickedUp). Unlocking fires itemDictionaryChanged, which makes the display refresh. Opening the backpack (backpackOpened) hides the dictionary so the two overlays do not stack, and closing it fires dictionaryClosed.

Item dictionary grid

Item dictionary details page

Player Shop

The shop is a centred overlay opened by pressing F next to a shopkeeper NPC; pressing F again, or the Close button, closes it. Opening it pauses the game (EntityService.setPaused(true)) and closing it resumes, and KeyboardPlayerInputComponent ignores gameplay keys other than F while it is open.

classDiagram
    class UIComponent {
        <<abstract>>
    }

    class ShopDisplay {
        -Table listingsTable
        -Label goldLabel
        -Label statusLabel
        -boolean open
        +open()
        +close()
        +isOpen() boolean
    }

    class ShopComponent {
        +buy(ShopListing) PurchaseResult
        +canBuy(ShopListing) boolean
        +canAfford(ShopListing) boolean
        +canStore(ShopListing) boolean
    }

    class ShopCatalog {
        +getListings() List~ShopListing~
        +getListing(ItemType) ShopListing
    }

    class ShopListing {
        -ItemType itemType
        -int price
        -int quantity
    }

    UIComponent <|-- ShopDisplay
    ShopDisplay --> ShopComponent : buy / canBuy
    ShopDisplay --> ShopCatalog : reads listings
    ShopComponent --> ShopCatalog : validates against
    ShopCatalog o-- ShopListing
    ShopComponent ..> ShopDisplay : "itemPurchased" / "purchaseFailed"
Loading

Shop overlay

ShopDisplay

Builds a panel with the title, the player's gold (Gold: N), one row per catalog listing, a status line and a Close button. Each row shows the item icon, name, quantity per purchase (x5), price (10g) and a Buy button. The Buy button is disabled whenever ShopComponent.canBuy is false (not enough gold, or no room in the inventory).

The display does not rebuild on every event. goldChanged, inventoryChanged, itemPurchased and purchaseFailed set a dirty flag, and the next draw() refreshes the gold label and rows once. This is done in draw() because render callbacks still run while gameplay is paused.

ShopComponent

Owns the purchase rules. buy returns a PurchaseResult of SUCCESS, INSUFFICIENT_GOLD, INVENTORY_FULL, ALREADY_OWNED or INVALID. It re-looks-up the listing in ShopCatalog and ignores any price supplied by the caller. Gold is only deducted after the item has been added to the inventory, so a failed purchase never costs gold. Success fires itemPurchased; failure fires purchaseFailed with a message that the display shows in the status line.

ShopCatalog

The single place prices and quantities are defined:

Item Quantity Price
Standard arrow 5 10g
Fire arrow 5 15g
Ice arrow 5 15g
Health potion 1 8g
Speed potion 1 12g
Poison potion 1 12g
Great Sword 1 25g
Spear 1 20g

These are placeholder prices (the player starts with 50 gold) and can be retuned in ShopCatalog without touching the UI.

Game Icon

The game window is titled Odysseus and uses a custom icon, set in DesktopLauncher:

  • Lwjgl3ApplicationConfiguration.setWindowIcon is given three sizes (desktop_icon_256.png, _64.png, _32.png in assets/images/ui/) so the OS can pick the best fit for the title bar, taskbar and alt-tab.
  • GLFW ignores window icons on macOS, so MacDockIcon.set("images/ui/desktop_icon_256.png") sets the Dock icon through Cocoa once the game has started (called from create() after super.create()).

Game icon in the macOS Dock

Relevant Development Links

See the existing Player HUD Test Plan for visual checks.

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