Skip to content

Air Boss Special Move

Phurin Vanasrivilai edited this page Oct 16, 2024 · 6 revisions

Overview

The SpecialAirMove is a special move used in combat by the Air Boss (Griffin), extending from the SpecialMove abstract class (as described in Combat Moves).

This move applies at random one of the two status effects to the player:

  • CONFUSED: The player executes a random move.
  • SHOCKED: Lasts 3 turns. Attacks are debuffed by 30%. The player's health is also decreased by 15% before each affected turn.

In addition to debuffing the player, the move also buffs Griffin's strength and defense.

Implementation

SpecialAirMove - special move unique to Griffin

/**
 * The SpecialAirMove class represents Air boss's special combat move, which inflicts debuffs
 * on the player and buffs Air boss's own stats. This move is unique to Air boss and impacts both
 * the target and the attacker.
 */
public class SpecialAirMove extends SpecialMove {
    private static final Logger logger = LoggerFactory.getLogger(SpecialAirMove.class);

    /**
     * Constructs the SpecialAirMove with the given move name and hunger cost.
     *
     * @param moveName    the name of the special move.
     * @param hungerCost the hunger cost required to perform the special move.
     */
    public SpecialAirMove(String moveName, int hungerCost) {
        super(moveName, hungerCost);
    }

    /**
     * Applies a random status effect to the target player after the move is executed
     * Also apply debuff which decreases Player's strength by 30 and defense by 25.
     *
     * @param targetStats combat stats of the target (player) that will be affected by the debuffs.
     */
    @Override
    protected void applyDebuffs(CombatStatsComponent targetStats) {
        // Applies debuffs to target's stats
        targetStats.addStrength(-30);
        targetStats.addDefense(-25);

        int rand = (int) (MathUtils.random() * 2);
        CombatStatsComponent.StatusEffect statusEffect = switch (rand) {
            case 0 -> CombatStatsComponent.StatusEffect.CONFUSED;
            case 1 -> CombatStatsComponent.StatusEffect.SHOCKED;
            default -> throw new IllegalStateException("Unexpected value: " + rand);
        };
        targetStats.addStatusEffect(statusEffect);
        logger.info("Status effect {} applied to the {}", statusEffect.name(), targetStats.isPlayer() ? "PLAYER" : "ENEMY");
    }

    /**
     * Buffs Air Boss's strength and defense stats after the special move.
     * This method increases Water Boss's strength by 25 and defense by 25.
     *
     * @param attackerStats combat stats of Kanga, who is performing the special move.
     */
    @Override
    protected void applyBuffs(CombatStatsComponent attackerStats) {
        attackerStats.addStrength(25);
        attackerStats.addDefense(25);
        logger.info("{} increased its strength to {} and defense to {}.",
                attackerStats.isPlayer() ? "PLAYER" : "ENEMY",
                attackerStats.getStrength(),
                attackerStats.getDefense());
    }
}

In CombatManager, handles the status effects

    .
    .
    .

    /**
     * Sets the player's action based on input and triggers enemy action selection.
     * The move combination is then executed, and status effects are processed at the end of the turn.
     *
     * @param playerActionStr the action chosen by the player as a string.
     */
    public void onPlayerActionSelected(String playerActionStr) {
        try {
            playerAction = Action.valueOf(playerActionStr.toUpperCase());
        } catch (IllegalArgumentException e) {
            logger.error("Invalid player action: {}", playerActionStr);
            return;
        }

        enemyAction = selectEnemyMove();

        handlePlayerConfusion();

        logger.info("(BEFORE) PLAYER {}: health {}, hunger {}", playerAction, playerStats.getHealth(), playerStats.getHunger());
        logger.info("(BEFORE) ENEMY {}: health {}, hunger {}", enemyAction, enemyStats.getHealth(), enemyStats.getHunger());

        // Execute the selected moves for both player and enemy.
        executeMoveCombination(playerAction, enemyAction);

        handleStatusEffects();

        checkCombatEnd();
    }

    /**
     * Randomly select a move to replace the player's selected move if the player has the Confusion status effect
     */
    public void handlePlayerConfusion() {
        if (playerStats.hasStatusEffect(CombatStatsComponent.StatusEffect.CONFUSED)) {
            logger.info("PLAYER is CONFUSED");
            ArrayList<Action> actions = new ArrayList<>(List.of(Action.ATTACK, Action.GUARD, Action.SLEEP));
            actions.remove(playerAction);
            playerAction = actions.get((int) (MathUtils.random() * actions.size()));
            moveChangedByConfusion = true;
        }
    }

    /**
     * Process Special Move status effects on the Player by reducing Player health and/or hunger.
     * Updates the statusEffectDuration and removes expired effects. Confusion only lasts 1 round and is always removed.
     */
    public void handleStatusEffects() {
        // Don't have a status effect, can skip the rest
        if (!playerStats.hasStatusEffect()) return;
        
        //Player has been confused
        if (playerStats.hasStatusEffect(CombatStatsComponent.StatusEffect.CONFUSED) && moveChangedByConfusion) {
                playerStats.removeStatusEffect(CombatStatsComponent.StatusEffect.CONFUSED);
                moveChangedByConfusion = false;
            }
        
        //check if player has been affected by other status effects, handle appropriately
        //note current implementation means if a player is both poisoned and bleeding, they will only be affected by bleed
        if (playerStats.hasStatusEffect(CombatStatsComponent.StatusEffect.BLEEDING)) {
            handleBleed();
        } else if (playerStats.hasStatusEffect(CombatStatsComponent.StatusEffect.POISONED)) {
            handlePoisoned();
        } else if (playerStats.hasStatusEffect(CombatStatsComponent.StatusEffect.SHOCKED)) {
            handleShocked();
        }
    }
    
    private void handleShocked() {
        if (statusEffectDuration == 0) {
            statusEffectDuration = playerStats.getStatusEffectDuration(CombatStatsComponent.StatusEffect.SHOCKED);
        } else {
            // Shock reduces health by 15% each round.
            playerStats.addHealth((int) (-0.15 * playerStats.getMaxHealth()));
            if (--statusEffectDuration <= 0) {
                playerStats.removeStatusEffect(CombatStatsComponent.StatusEffect.SHOCKED);
            }
        }
    }

    .
    .
    .

Testing Plan

Unit Testing

  • The CombatMoveComponent class is extensively unit tested for each method.
  • The base CombatMove class is extensively unit tested for each method.
  • The SpecialMove and it's concrete subclass SpecialAirMove are tested method-wise.

SpecialAirMoveTest

/**
 * Unit tests for the SpecialAirMove class.
 * These tests use Mockito to mock the behaviour of dependent components (e.g., CombatStatsComponent).
 */
@ExtendWith(GameExtension.class)
class SpecialAirMoveTest {

    private SpecialAirMove specialAirMove;
    private CombatStatsComponent mockTargetStats;
    private CombatStatsComponent mockAttackerStats;

    /**
     * Initial setup before each test. Creates an instance of SpecialAirMove and
     * mocks the necessary dependencies.
     */
    @BeforeEach
    void setUp() {
        // Create an instance of SpecialAirMove with a mock move name and hunger cost.
        specialAirMove = new SpecialAirMove("Air Strike", 40);

        // Mock the target and attacker stats (CombatStatsComponent).
        mockTargetStats = mock(CombatStatsComponent.class);
        mockAttackerStats = mock(CombatStatsComponent.class);
    }

    /**
     * Test to verify that the applyDebuffs method correctly applies the debuff to the target
     * by reducing strength and defense, and applies a random status effect.
     */
    @Test
    void testApplyDebuffs() {
        // Act: Apply the debuffs to the target stats.
        specialAirMove.applyDebuffs(mockTargetStats);

        // Assert: Verify that the target's strength and defense are decreased.
        verify(mockTargetStats).addStrength(-30);
        verify(mockTargetStats).addDefense(-25);

        // Capture the added status effect (CONFUSED or SHOCKED).
        ArgumentCaptor<CombatStatsComponent.StatusEffect> statusCaptor = ArgumentCaptor.forClass(CombatStatsComponent.StatusEffect.class);
        verify(mockTargetStats).addStatusEffect(statusCaptor.capture());

        CombatStatsComponent.StatusEffect appliedEffect = statusCaptor.getValue();
        assertTrue(appliedEffect == CombatStatsComponent.StatusEffect.CONFUSED ||
                        appliedEffect == CombatStatsComponent.StatusEffect.SHOCKED,
                "Random status effect should be CONFUSED or SHOCKED.");
    }

    /**
     * Test to verify that the applyBuffs method correctly buffs Air Boss's strength
     * and defense by the expected amounts.
     */
    @Test
    void testApplyBuffs() {
        // Act: Apply the buffs to the attacker's stats.
        specialAirMove.applyBuffs(mockAttackerStats);

        // Assert: Verify that the attacker's strength and defense are increased.
        verify(mockAttackerStats).addStrength(25);
        verify(mockAttackerStats).addDefense(25);
    }

    /**
     * Test to ensure that the logger outputs the correct message when applyDebuffs is called.
     * We can test the side effects (logging) of the method using Mockito's verification features.
     */
    @Test
    void testApplyDebuffsLogsCorrectMessage() {
        // Act: Apply the debuffs to trigger the logger.
        specialAirMove.applyDebuffs(mockTargetStats);

        // Since logger is static and logs to output, here we focus on behaviour verification (mock calls).
        verify(mockTargetStats).addStrength(-30);
        verify(mockTargetStats).addDefense(-25);
        verify(mockTargetStats, times(1)).addStatusEffect(any(CombatStatsComponent.StatusEffect.class));
    }

    /**
     * Test to ensure that the logger outputs the correct message when applyBuffs is called.
     * Again, this is focused on verifying behaviour and state, not direct logging output.
     */
    @Test
    void testApplyBuffsLogsCorrectMessage() {
        // Set up mock stats to return specific values.
        when(mockAttackerStats.getStrength()).thenReturn(75);
        when(mockAttackerStats.getDefense()).thenReturn(100);

        // Act: Apply the buffs to trigger the logger.
        specialAirMove.applyBuffs(mockAttackerStats);

        // Assert: Verify that the logger logs the correct message for buffs.
        verify(mockAttackerStats, times(1)).addStrength(25);
        verify(mockAttackerStats, times(1)).addDefense(25);
    }
}

UML Diagram

Sequence Diagram

SpecialAirMove_new

Class Diagram

special air move

Clone this wiki locally