Cool Down System - UQcsse3200/2026-studio-3 GitHub Wiki

Card Cool Down

Each card in the the player deck is now has a unique card ID attached to it via the cardInstanceFactory

  public void addCard(String cardId) {
    if (!canAddCard(cardId)) {
      throw new IllegalArgumentException("cardId must be registered");
    }
    cards.add(cardInstanceFactory.create(cardId));
  }

This allow the cards to have a "cool down" that disable the card when played for x amount of runs.

This disablement of card is done through the UI level; the status and tracking is stored in CardPlayService (specifically its internal CardCooldownTracker, backed by BattleDeck's discard pile). The UI never queries the tracker directly, it just checks whether a card's exact instance is currently in the discard pile, and renders that slot as dimmed and non-interactive if so.

New, instance-aware methods sit alongside them for code that actually needs to tell copies apart:

Method Purpose
PlayerDeck.getCards() every owned card, as instances
BattleDeck.getHandInstances() / getDiscardPileInstances() / getDrawPileInstances() per-pile snapshots, as instances
BattleDeck.getAllInstances() every card in the battle deck, any pile
BattleDeck.discardCardInstance(cardId) discards the first match and returns which instance moved
BattleDeck.retrieveInstanceFromDiscard(instance) moves one exact instance back to hand, not "first match by ID"
BattleDeck.setHandInstances(instances) replaces the hand with an exact set of instances (used by deck rearrange)

All of these return/accept com.csse3200.game.cards.runtime.CardInstance.

How the cooldown works

sequenceDiagram
    actor Player
    participant BC as BattleController
    participant CPS as CardPlayService
    participant CD as CardCooldown
    participant Tr as CardCooldownTracker
    participant Deck as BattleDeck
    participant UI as BattleScreen

    Note over Player,UI: Turn N: playing a card
    Player->>BC: play card (instanceId, target)
    BC->>CPS: playCard(request)
    CPS->>CPS: validate, spend energy, resolve effects
    CPS->>Deck: discardCardInstance(instanceId)
    CPS->>CD: roundsFor(card)
    CD-->>CPS: 1, 2 or 3 rounds
    CPS->>Tr: trackDiscard(instance, rounds)
    CPS-->>BC: CardPlayResult
    BC->>UI: "handChanged", card shown greyed out (disabled)

    Note over Player,UI: Enemy turn: cooldowns do not tick

    Note over Player,UI: Turn N+1: start of player round
    BC->>BC: enterPlayerStart(): refill energy, reset block, ...
    BC->>CPS: onPlayerRoundStart()
    CPS->>Tr: tickRoundAndRetrieve()
    loop each tracked card
        Tr->>Tr: roundsRemaining - 1
        opt roundsRemaining <= 0
            Tr->>Deck: retrieveInstanceFromDiscard(instance)
            Deck->>Deck: discard pile to hand
            Tr->>Tr: stop tracking it
        end
    end
    Tr-->>CPS: retrieved instances
    CPS-->>BC: retrieved instances
    opt any retrieved
        BC->>BC: narrate("A card cooled down and returned to your hand.")
        BC->>UI: "handChanged", card playable again
    end
Loading
graph LR
    A["Card played"] --> B["BattleDeck.discardCardInstance"]
    B --> C["Instance moves: hand → discard"]
    C --> D["CardCooldownTracker starts counting"]
    D --> E{"Round tick:<br/>roundsRemaining reaches 0?"}
    E -- no --> D
    E -- yes --> F["BattleDeck.retrieveInstanceFromDiscard"]
    F --> G["Instance moves: discard → hand"]
    G --> H["BattleScreen slot for that instance<br/>renders enabled again"]
Loading

Deck rearrange interaction

The deck-rearrange popup (DeckEditorComponent) lets the player pick which 5 CardInstances make up their hand from their entire pool (draw pile + hand + discard pile combined). A card currently on cooldown can be selected too, but since it can't physically be "in hand" while still cooling down, CardPlayService.rearrangeHand:

  • moves only the not-currently-discarded part of the selection into the real battle-deck hand (BattleDeck.setHandInstances), and
  • leaves any selected-but-discarded instances exactly where they are, cooldown untouched.

The full selection (including the still-cooling-down picks) becomes the new handRowOrder in BattleScreen, so a reserved cooldown card shows up dimmed in its slot and automatically un-dims itself the moment CardCooldownTracker retrieves it. No special-casing needed, because that's just the normal dimming check described above.

sequenceDiagram
    actor Player
    participant UI as BattleScreen (battleUi)
    participant DE as DeckEditorComponent
    participant Popup as PopupDisplay
    participant CPS as CardPlayService
    participant Deck as BattleDeck
    participant Pool as ClickableFactory (poolFactory)
    participant Disp as Deck editor displays

    Note over Player,Disp: Opening the editor
    Player->>UI: click "Card Inventory"
    UI->>DE: "openMenu" event, open()
    DE->>CPS: currentHand(), allInstances(), discardedInstances()
    CPS->>Deck: read hand, all piles, discard pile
    Note right of DE: pool = hand first, then the rest<br/>selected = current hand<br/>scrollRow = 0
    DE->>Popup: show()
    Popup-->>DE: onShow, onOpened()
    DE->>Disp: OPENED
    DE->>Pool: rebuildByTrigger("toggleDeckCard", page of card records)
    DE->>Pool: show scroll buttons (if > 2 rows) and "Set Deck"
    DE->>Disp: BADGES, PREVIEW, SUMMARY, ERROR("")
    DE->>Pool: bringToFront()

    Note over Player,Disp: Selecting cards
    Player->>Pool: click a card
    Pool->>DE: "toggleDeckCard"(instanceId), toggleSelection()
    alt already selected
        DE->>DE: remove from selected
    else hand already has 5
        DE->>Disp: ERROR("Hand is full...")
    else
        DE->>DE: add to selected (selection order = hand order)
    end
    DE->>Disp: PREVIEW, BADGES (numbered), SUMMARY

    Note over Player,Disp: Scrolling
    Player->>Pool: click ^ or v (or mouse wheel)
    Pool->>DE: "deckScrollUp"/"deckScrollDown", scrollBy(±1)
    DE->>Pool: rebuildByTrigger("toggleDeckCard", new page)

    Note over Player,Disp: Confirming
    Player->>Pool: click "Set Deck"
    Pool->>DE: "deckSetDeck", onSetDeckClicked()
    alt selected != 5
        DE->>Disp: ERROR("Hand must contain exactly 5 cards...")
    else
        DE->>CPS: rearrangeHand(selected)
        CPS->>CPS: drop cards still in the discard pile (on cooldown)
        alt playable cards match the current hand
            CPS-->>DE: false (no change, no energy spent)
        else not enough energy
            CPS-->>DE: throws IllegalStateException
            DE->>Disp: ERROR("Not enough energy...")
        else
            CPS->>CPS: spend 1 energy
            CPS->>Deck: setHandInstances(playable)
            Note right of Deck: old hand + draw pile reshuffled<br/>discard pile untouched
            CPS-->>DE: true
        end
        DE->>UI: onDeckChanged(selected), onDeckRearranged()
        UI->>UI: handRowOrder = selected, rebuildHandWidgets()
        DE->>Popup: hide()
        Popup-->>DE: onHide, onClosed()
        DE->>Disp: CLOSED (clears badges, preview, text)
        DE->>Pool: remove card widgets, hide scroll and "Set Deck"
    end

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