Skip to content

Water Boss Special Move

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

Overview

The SpecialWaterMove is a special move used in combat by the Water Boss (Leviathan), 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.
  • POISONED: Lasts 2 turns. Sleep will restore stamina but not health. The Player's stamina is also decreased by 30% before each affected turn.

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

Implementation

SpecialWaterMove - special move unique to Leviathan

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

    /**
     * Constructs the SpecialWaterMove 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 SpecialWaterMove(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 20 and defense by 10.
     *
     * @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(-20);
        targetStats.addDefense(-10);

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

    /**
     * Buffs Water Boss's strength and defense stats after the special move.
     * This method increases Water Boss's strength by 10 and defense by 25.
     *
     * @param attackerStats combat stats of Kanga, who is performing the special move.
     */
    @Override
    protected void applyBuffs(CombatStatsComponent attackerStats) {
        attackerStats.addStrength(10);
        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 handlePoisoned() {
        if (statusEffectDuration == 0) {
            statusEffectDuration = playerStats.getStatusEffectDuration(CombatStatsComponent.StatusEffect.POISONED);
        } else {
            // Poison reduces hunger by 30% each round.
            playerStats.addHunger((int) (-0.3 * playerStats.getMaxHunger()));
            if (--statusEffectDuration <= 0) {
                playerStats.removeStatusEffect(CombatStatsComponent.StatusEffect.POISONED);
            }
        }
    }

    .
    .
    .

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 SpecialWaterMove are tested method-wise.

SpecialWaterMoveTest

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

    private SpecialWaterMove specialWaterMove;
    private CombatStatsComponent mockTargetStats;
    private CombatStatsComponent mockAttackerStats;

    /**
     * Initial setup before each test. Creates an instance of SpecialWaterMove and
     * mocks the necessary dependencies.
     */
    @BeforeEach
    void setUp() {
        // Create an instance of SpecialWaterMove with a mock move name and hunger cost.
        specialWaterMove = new SpecialWaterMove("Water Fury", 30);

        // 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.
        specialWaterMove.applyDebuffs(mockTargetStats);

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

        // Capture the added status effect (CONFUSED or POISONED).
        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.POISONED,
                "Random status effect should be CONFUSED or POISONED.");
    }

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

        // Assert: Verify that the attacker's strength and defense are increased.
        verify(mockAttackerStats).addStrength(10);
        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.
        specialWaterMove.applyDebuffs(mockTargetStats);

        // Since logger is static and logs to output, here we focus on behaviour verification (mock calls).
        verify(mockTargetStats).addStrength(-20);
        verify(mockTargetStats).addDefense(-10);
        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(50);
        when(mockAttackerStats.getDefense()).thenReturn(75);

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

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

UML Diagram

Sequence Diagram

SpecialWaterMove_new

Class Diagram

special water move

Clone this wiki locally