Special Interaction for Land Boss - UQcsse3200/2024-studio-2 GitHub Wiki

Overview

Each boss in the game areas features a unique special interaction designed to introduce distinct behavioral traits, influencing how players approach encounters beyond combat alone.

For Kanga, the Land Area boss, its special ability allows it to summon two smaller joey enemies. Upon spawning, these joeys rush toward the player, initiating combat.

Implementation

KangaJoeyTask class

/**
 * A task that allows an entity to wait for a set time and then spawn a small kangaroo joey entity near the owner.
 */
public class KangaJoeyTask extends DefaultTask implements PriorityTask {
    private static final Logger logger = LoggerFactory.getLogger(KangaJoeyTask.class);
    private final int priority = 5;
    private final Entity target;  // The target entity to aim at (could be the player or another entity).
    private final float range;    // Range within which to spawn the joey
    private int numSpawns = 0;    // Number of joeys spawned
    private final int maxSpawns;  // Max number of Joeys that can be spawned

    /**
     * A task that allows an entity to wait for a set time and then spawn a small kangaroo joey near the owner.
     *
     * @param target   The target entity (could be the player or another entity).
     * @param range    The distance within which the joey will be spawned.
     * @param maxSpawns Maximum number of Joeys that can be spawned.
     */
    public KangaJoeyTask(Entity target, float range, int maxSpawns) {
        this.target = target;
        this.range = range;
        this.maxSpawns = maxSpawns;
    }

    @Override
    public int getPriority() {
        if (status == Status.ACTIVE) {
            return getActivePriority();
        }
        update();
        return getInactivePriority();
    }

    @Override
    public void start() {
        super.start();
    }

    @Override
    public void update() {
        if (getDistanceToTarget() < range && numSpawns < maxSpawns) {
            spawnJoey();
        }
    }

    /**
     * Returns the distance between the current entity and the target location.
     *
     * @return The distance between the owner's entity and the target location.
     */
    private float getDistanceToTarget() {
        return owner.getEntity().getPosition().dst(target.getPosition());
    }

    /**
     * Gets the priority when the task is active, based on the distance to the target.
     *
     * @return The priority value, or -1 if out of range.
     */
    private int getActivePriority() {
        float dst = getDistanceToTarget();
        if (dst > range) {
            return -1; // Too far, stop spawning
        }
        return priority;
    }

    /**
     * Gets the priority when the task is inactive, based on the distance to the target.
     *
     * @return The priority value, or -1 if out of range.
     */
    private int getInactivePriority() {
        float dst = getDistanceToTarget();
        if (dst <= range) {
            return priority;
        }
        return -1;
    }

    /**
     * Spawns a small kangaroo joey near the owner.
     */
    private void spawnJoey() {
        numSpawns++;
        owner.getEntity().getEvents().trigger("spawnJoey", owner.getEntity());
    }
}

Usage in BossFactory class in createBossNPC() method

Attach the 'KangaJoeyTask' task to an AITaskComponent for Kanga boss, which is then added to its Entity

AITaskComponent aiComponent = new AITaskComponent();

if (type == Entity.EnemyType.KANGAROO) {
     aiComponent.addTask(new ChaseTask(target, 10, 12f, 14f, true))
         .addTask(new KangaJoeyTask(target, 9f, 2));
}

Entity npc = new Entity().addComponent(aiComponent);

In ForestGameArea class, add listener to the boss entity to listen to the event to spawn joey

  private void spawnKangarooBoss() {
      if (!kangarooBossSpawned) {
          Entity kangarooBoss = BossFactory.createKangaBossEntity(player);
          kangarooBoss.getEvents().addListener("spawnJoey", this::spawnJoeyEnemy);
          spawnBossOnMap(kangarooBoss);
          enemies.add(kangarooBoss);
          kangarooBossSpawned = true;
      }
  }

  private void spawnJoeyEnemy(Entity kanga) {
    if (kanga != null) {
      Entity joey = EnemyFactory.createJoey(player);

      Vector2 kangarooBossPos = kanga.getPosition();

      // Define the area around the Kangaroo boss where the Joey can be spawned
      GridPoint2 minPos = new GridPoint2((int) kangarooBossPos.x - 2, (int) kangarooBossPos.y - 2);
      GridPoint2 maxPos = new GridPoint2((int) kangarooBossPos.x + 2, (int) kangarooBossPos.y + 2);

      GridPoint2 spawnPos = RandomUtils.random(minPos, maxPos);

      spawnEntityAt(joey, spawnPos, true, false);
      enemies.add(joey);
    }
  }

where the Joey enemy is created in the EnemyFactory class

  /**
   * Creates a joey enemy.
   *
   * @param target entity to chase (player in most cases, but does not have to be)
   * @return enemy joey entity
   */
  public static Entity createJoey(Entity target) {
    Entity joey = createBaseEnemy(target, EnemyType.JOEY);
    BaseEnemyEntityConfig config = configs.joey;
    joey.setEnemyType(Entity.EnemyType.JOEY);

    AnimationRenderComponent animator =
            new AnimationRenderComponent(
                    ServiceLocator.getResourceService().getAsset(config.getSpritePath(), TextureAtlas.class));
    animator.addAnimation("wander", 0.1f, Animation.PlayMode.LOOP);
    animator.addAnimation("chase", 0.1f, Animation.PlayMode.LOOP);
    animator.addAnimation("spawn", 1.0f, Animation.PlayMode.NORMAL);

    joey
            .addComponent(new CombatStatsComponent(config.getHealth(), config.getHunger(), config.getBaseAttack(), config.getDefense(), config.getSpeed(), config.getExperience(), 100, false, false))
            .addComponent(new CombatMoveComponent(moveSet))
            .addComponent(animator)
            .addComponent(new JoeyAnimationController());

    joey.getComponent(AnimationRenderComponent.class).scaleEntity();
    joey.getComponent(PhysicsMovementComponent.class).changeMaxSpeed(new Vector2(config.getSpeed(), config.getSpeed()));

    return joey;
  }

Testing Plan

Visual Testing - https://youtu.be/yU9CXT3zjS0

https://github.com/user-attachments/assets/8cd3d9bb-1fea-43e9-b8b3-2c146fc2736d

Testing for the Joey entity

In EnemyFactoryTest class:

/**
     * Tests Creation of a joey.
     */
    @Test
    void TestJoeyCreation() {
        assertNotNull(joey, "Joey should not be null.");
    }

    /**
     * Tests that the joey is an Entity.
     */
    @Test
    void TestJoeyIsEntity() {
        assertEquals(joey.getClass(), Entity.class);
    }

    /**
     * Tests that the joey has the correct components.
     */
    @Test
    void TestJoeyHasComponents() {
        assertNotNull(joey.getComponent(PhysicsComponent.class));
        assertNotNull(joey.getComponent(PhysicsMovementComponent.class));
        assertNotNull(joey.getComponent(JoeyAnimationController.class));
        assertNotNull(joey.getComponent(CombatStatsComponent.class));
        assertNotNull(joey.getComponent(HitboxComponent.class));
        assertNotNull(joey.getComponent(ColliderComponent.class));
    }

    /**
     * Tests that the joey has the correct stats.
     */
    @Test
    void TestJoeyStats() {
        assertTrue((joey.getComponent(CombatStatsComponent.class).getHealth() > 45)
                        && (joey.getComponent(CombatStatsComponent.class).getHealth() < 55),
                "joey should have between 45 and 55 HP.");
        assertTrue((joey.getComponent(CombatStatsComponent.class).getStrength() > 20)
                        && (joey.getComponent(CombatStatsComponent.class).getStrength() < 30),
                "joey should have between 20 and 30 Attack.");
        assertTrue((joey.getComponent(CombatStatsComponent.class).getDefense() > 20)
                        && (joey.getComponent(CombatStatsComponent.class).getDefense() < 30),
                "joey should have between 20 and 30 defense.");
        assertEquals(400,
                joey.getComponent(CombatStatsComponent.class).getSpeed(),
                "joey should have 400 speed.");
        assertEquals(85,
                joey.getComponent(CombatStatsComponent.class).getExperience(),
                "joey should have 85 experience.");
    }

    /**
     * Tests that the joey has correct animations.
     */
    @Test
    void TestJoeyAnimation() {
        assertTrue(joey.getComponent(AnimationRenderComponent.class).hasAnimation("wander") ,
                "Joey should have wander animation.");
        assertTrue(joey.getComponent(AnimationRenderComponent.class).hasAnimation("chase") ,
                "Joey should have chase animation.");
    }

    /**
     * Tests that the joey is in the correct spot when placed.
     */
    @Test
    void TestJoeySetPosition() {
        Vector2 pos = new Vector2(0f, 0f);
        joey.setPosition(pos);
        assertEquals(pos, joey.getPosition());
    }

UML Diagram

Sequence Diagram

KangaJoeyTask

Class Diagram

kanga joey task