Team 3 ‐ Battle System & Game Loop - UQcsse3200/2026-studio-3 GitHub Wiki

Game Loop

Overview

The battle system is the foundation for the turn-based combat within the game. Fundamentally, it uses a finite-state machine (FSM) to keep track of the current phase and ensure only permitted actions are taken.

stateDiagram-v2
    direction LR

    [*] --> SETUP

    SETUP --> REVEAL_INTENTS: SETUP_COMPLETE
    REVEAL_INTENTS --> PLAYER_START: INTENTS_REVEALED

    PLAYER_START --> PLAYER_TURN: PLAYER_TURN_STARTED
    PLAYER_START --> VICTORY: ENEMIES_DEFEATED
    PLAYER_START --> DEFEAT: PLAYER_DEFEATED
 
    PLAYER_TURN --> CARD_RESOLVING: CARD_PLAY_REQUESTED
    PLAYER_TURN --> PLAYER_END: PLAYER_END_REQUESTED

    CARD_RESOLVING --> PLAYER_TURN: CARD_RESOLVED
    CARD_RESOLVING --> VICTORY: ENEMIES_DEFEATED

    PLAYER_RESOLVED --> PLAYER_TURN: PLAYER_CONTINUES
    PLAYER_RESOLVED --> VICTORY: ENEMIES_DEFEATED
    PLAYER_RESOLVED --> DEFEAT: PLAYER_DEFEATED

    PLAYER_END --> ENEMY_TURN: PLAYER_TURN_ENDED
    PLAYER_END --> VICTORY: ENEMIES_DEFEATED
    PLAYER_END --> DEFEAT: PLAYER_DEFEATED

    ENEMY_TURN --> ENEMY_ATTACK: ENEMY_ATTACK_SELECTED
    ENEMY_TURN --> ENEMY_DEFEND: ENEMY_DEFEND_SELECTED
    ENEMY_TURN --> ENEMY_OTHER: ENEMY_OTHER_SELECTED

    ENEMY_ATTACK --> ENEMY_RESOLVED: ENEMY_ACTION_RESOLVED
    ENEMY_DEFEND --> ENEMY_RESOLVED: ENEMY_ACTION_RESOLVED
    ENEMY_OTHER --> ENEMY_RESOLVED: ENEMY_ACTION_RESOLVED

    ENEMY_RESOLVED --> ENEMY_TURN: MORE_ENEMIES
    ENEMY_RESOLVED --> REVEAL_INTENTS: ENEMY_PHASE_COMPLETE
    ENEMY_RESOLVED --> VICTORY: ENEMIES_DEFEATED
    ENEMY_RESOLVED --> DEFEAT: PLAYER_DEFEATED

    VICTORY --> [*]
    DEFEAT --> [*]
Loading

At the start of each round, the enemies reveal what they plan to do. The player can then react with the possible actions of attacking, defending, or something completely different. After the player has finished. Each of the living enemies performs their action. The system then checks the victory and defeat conditions, after the resolution of all their actions.

Player Turn Banner

At the start of each player turn, the battle controller updates the turn number. After the enemies finish acting, a “PLAYER TURN” banner displays that number. It fades in, stays briefly, and fades out. The banner does not appear on the first turn or after each card.

Enemy Turn Banner

When the player finishes their turn a "Enemy Turn" banner displays that the game transitions to the enemy's turn. The banner fades in, stays breifly, and fades out.

Main Components

  • BattleController – Controls the battle loop, processes events, manages enemy turns, and checks for victory or defeat.
  • BattleTransitions – Defines all permitted transitions between battle phases.
  • BattlePhase and BattleEvent – Define the possible battle states and the events that move the battle between them.
  • BattleActions – Connects player input from the battle UI to the controller.
  • BattleDisplay and BattleArea – Provide the current battle interface and controls.

Difficulty Scaling

Enemy statistics increase as the player progresses through the map. The active map node's height is used as the current progression value.

The progression value moves through the following components:

RunState.getMapProgression() → BattleScreen → ForestGameArea → EnemyFactory.create(id, progression)

EnemyFactory uses EnemyScaling to create a scaled copy of the enemy configuration. The original configuration is not changed.

Enemy Tier Health Increase Attack Increase
Normal 8% per progression step 5% per progression step
Elite 10% per progression step 7% per progression step

The scaled statistic is calculated using:

round(base statistic × (1 + scaling rate × progression))

Negative progression values are treated as zero. If there is no valid active map node, progression defaults to zero and enemies use their base statistics.

This integration only changes enemy health and attack. Enemy selection, enemy count, bounty rewards, and player energy are handled separately.

Victory & Defeat Conditions

The battle ends when either:

  • all enemies are defeated, resulting in victory; or
  • the player reaches zero health, resulting in defeat.

Enemy Release Animation

After the player wins, each defeated enemy is purified with a soft beam and rising particles. The enemy returns to its idle animation and gradually rises from a reduced height to normal, retaining its purified colour. The victory screen waits until the release animations finish.

Public Methods & Usages

BattleController class signature - requires a List<Entity> enemies and Entity player as arguments:

public BattleController(Entity player, List<Entity> enemies) throws IllegalArgumentException
   /**
   * Starts the battle encounter
   * @throws IllegalStateException if the battle has already begun 
   */
  public void start() throws IllegalStateException
   /**
   * Reports whether the player currently has the given status effect.
   *
   * <p>Read-only: exposes a query about the player rather than the player entity itself, so callers
   * cannot mutate player state through this controller.
   *
   * @param effectType identifier of the status effect, as stored by CombatStatsComponent
   * @return true if the player carries an active effect with this identifier
   */
  public boolean playerHasStatusEffect(String effectType)
  /**
   * Player decides to end their turn
   */
  public void endPlayerTurn()
  /**
   * Resets the current battle to a completely new battle that has no previous player and
   * enemy turns
   */
  public void resetBattle()
  /**
   * Retrieves the current phase of the battle (e.g. player turn, enemy turn, player attack, etc.)
   * @return the current phase of the BattleController instance
   */
  public BattlePhase getCurrentPhase()
  /**
   * Returns whether the battle is in the player input phase.
   *
   * @return true only during {@link BattlePhase#PLAYER_TURN}; false during card resolution, setup,
   *     enemy turns, and all other phases
   */
  public boolean isPlayerTurn()
  /**
   * Returns the current targeted enemy.
   *
   * @return An int representing the targeted entity within the array.
   */
  public int getCurrentEnemyIndex()
  /**
   * Adds a listener to the event handler, which ultimately informs external teams about a phase
   * change.
   *
   * @param listener The instantiated external listener.
   */
  public void addPhaseChangeListener(EventListener2<BattlePhase, BattlePhase> listener)
  /**
   * Adds a listener for short, human-readable descriptions of what just happened in the battle.
   * Used by the UI to pop up "you did X" / "the enemy did Y" messages between turns.
   *
   * @param listener receives the message text
   */
  public void addBattleLogListener(EventListener1<String> listener)
  /**
   * Adds a listener for the end of the battle. The argument is {@code true} on a win (all enemies
   * defeated) and {@code false} on a loss (player defeated). This is the hook the UI uses to switch
   * to the victory or defeat screen.
   *
   * @param listener receives the win/loss flag
   */
  public void addBattleEndListener(EventListener1<Boolean> listener)
  /**
   * Adds a listener that receives the resolved enemy-facing effects of a played card, for Team 1 to
   * apply. The controller also applies these itself so the encounter still resolves.
   *
   * @param listener receives the resolved effects
   */
  public void addEnemyEffectsListener(EventListener1<List<ResolvedCardEffect>> listener)
  /**
   * Adds a listener that receives the resolved player-facing effects of a played card, for Team 7
   * to apply. The controller also applies these itself so the encounter still resolves.
   *
   * @param listener receives the resolved effects
   */
  public void addPlayerEffectsListener(EventListener1<List<ResolvedCardEffect>> listener)
  /**
   * Adds a listener that receives the player's hand (card IDs) after it changes, e.g. once a played
   * card has moved from hand to discard. The UI uses this to refresh the on-screen hand.
   *
   * @param listener receives the updated hand
   */
  public void addHandChangedListener(EventListener1<List<String>> listener)
  /** Sends a one-line description of the latest battle action to any log listeners. */
  private void narrate(String message)
  /**
   * Convenience function for returning if a given event can be handled within a state.
   *
   * @param event The event to check.
   * @return True if the event is valid to be applied. False if not.
   */
  public boolean canHandle(BattleEvent event)

start() begins the finite state machine, and resetBattle() brings it back to the start, erasing all previous actions. getCurrentPhase() provides the current stage at which the battle is at. The various Listener methods are for specific teams to use to receive notifications when something changes, ie. the battle phase, resolved effects of a played card, and the hand. getCurrentEnemyIndex returns the current recorded index of List<Entity> enemies, meaning the enemy at that index is the enemy of concern for the player currently. canHandle returns a boolean indicating whether an invent is able to be handled by the BattleController at that point.

An example usage would be:

BattleController controller = new BattleController()

controller.start();

controller.addBattleLogListener(message -> {
    System.out.println(message);
});

int enemyIndex = battleController.getCurrentEnemyIndex();

System.out.println("Currently targeting enemy: " + enemyIndex);

if (battleController.canHandle(event)) {
    battleController.handle(event);
}

Tests

BattleControllerTest covers:

  • Rejecting a null player
  • Rejecting empty enemy lists
  • Rejecting null enemy lists
  • Rejecting null enemies
  • Starting in the SETUP phase with no current enemy
  • Applying valid transitions
  • Reporting handled events
  • Completing player action cycle
  • Processing multiple enemies
  • Completing two battle rounds
  • Skipping dead enemies
  • Rolling living enemy intents
  • Entering victory when all enemies are dead
  • Entering defeat when player is dead
  • Entering defeat when the player is dead
  • Rejecting starting a battle twice
  • Queueing listener events
  • Rejecting events after a victory
  • Rejecting events after a defeat
  • Rejecting null events
  • Rejecting invalid transitions

BattleTransitionsTest currently covers:

  • Allowing diagram transitions
  • Rejecting transitions not in the diagram

All these tests currently pass.

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