Skip to content

Kangaroo Boss Special Move

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

Overview

The SpecialKangaMove is a special move used in combat by the Kanga Boss, 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.
  • BLEEDING: Lasts 3 turns. Guard will block only 30% of incoming damage instead of 50%. The player's health and stamina are also decreased by 9% (Turn 1 of Bleeding), 6% (Turn 2), then 3% (Turn 3) before each affected turn.

In addition to debuffing the player, the move also buffs Kanga’s strength and defense.

Implementation

SpecialKangaMove - special move unique to Kanga

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

    /**
     * Constructs the SpecialKangaMove with the given move name and hunger cost.
     *
     * @param moveName    the name of the special Kanga move.
     * @param hungerCost the hunger cost required to perform the special Kanga move.
     */
    public SpecialKangaMove(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 15 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(-15);
        targetStats.addDefense(-15);

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

    /**
     * Buffs Kanga's strength and defense stats after the special move.
     * This method increases Kanga's strength by 15 and defense by 10.
     *
     * @param attackerStats combat stats of Kanga, who is performing the special move.
     */
    @Override
    protected void applyBuffs(CombatStatsComponent attackerStats) {
        attackerStats.addStrength(15);
        attackerStats.addDefense(10);
        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 handleBleed() {
        if (statusEffectDuration == 0) {
            statusEffectDuration = playerStats.getStatusEffectDuration(CombatStatsComponent.StatusEffect.BLEEDING);
        } else {
            // Bleeding reduces health and hunger by 9%, 6%, and 3% respectively each round.
            double reductionMultiplier = (double) (-3 * statusEffectDuration) / 100;
            playerStats.addHealth((int) (reductionMultiplier * playerStats.getMaxHealth()));
            playerStats.addHunger((int) (reductionMultiplier * playerStats.getMaxHunger()));
            if (--statusEffectDuration <= 0) {
                playerStats.removeStatusEffect(CombatStatsComponent.StatusEffect.BLEEDING);
            }
        }
    }

    .
    .
    .

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

SpecialKangaMoveTest

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

    private SpecialKangaMove specialKangaMove;
    private CombatStatsComponent mockTargetStats;
    private CombatStatsComponent mockAttackerStats;

    /**
     * Initial setup before each test. Creates an instance of SpecialKangaMove and
     * mocks the necessary dependencies.
     */
    @BeforeEach
    void setUp() {
        // Create an instance of SpecialKangaMove with a mock move name and hunger cost.
        specialKangaMove = new SpecialKangaMove("Kanga Rage", 20);

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

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

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

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

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

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

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

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

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

UML Diagram

Sequence Diagram

SpecialKangaMove_new

Class Diagram

special kanga move

Clone this wiki locally