Skip to content

Transition to Boss

Phurin Vanasrivilai edited this page Aug 29, 2024 · 17 revisions

Overview

The transition from regular game to boss functionality covers the event from triggering the spawn of the Final Boss (Kanga), the visual/audio effects during the boss chases, up to the point where both the player and enemy hitboxes collide triggering the boss cutscene which then go into combat with the boss.

Implementation

Triggering the spawn

In class ForestGameArea, add a listener to detect the event triggering the spawn of Kanga Boss

    // Add listener for event which will trigger the spawn of Kanga Boss
    player.getEvents().addListener("spawnKangaBoss", this::spawnKangarooBoss);
    // Initialize the flag when creating the area
    kangarooBossSpawned = false;

which then calls the method to spawn the boss, with a flag to check whether it has spawned already to prevent multiple spawn of the boss

  private void spawnKangarooBoss() {
    if (!kangarooBossSpawned) {
      // Create entity
      Entity kangarooBoss = NPCFactory.createKangaBossEntity(player);
      // Create in the world
      spawnEntityOnMap(kangarooBoss);
      // Set flag to true after Kanga Boss is spawned
      kangarooBossSpawned = true;
    }
  }

Currently, the task which trigger the spawn of Kanga: to walk, and to attack (press spacebar)

image

After completing the required tasks, the Kanga Boss will spawn

image

Boss Chase

Adding an attribute to specify if NPC entity in class ChaseTask and WanderTask (used in AITaskComponent when creating an NPC)

private final boolean isBoss;

  public ChaseTask(Entity target, int priority, float viewDistance, float maxChaseDistance, boolean isBoss) {
    ...
    this.isBoss = isBoss;
  }

The method to create a Boss NPC entity, where WanderTask and ChaseTask are attached with isBoss as true

public static Entity createBossNPC(Entity target) {
    AITaskComponent aiComponent =
            new AITaskComponent()
                    .addTask(new WanderTask(new Vector2(2f, 2f), 2f, true))
                    .addTask(new ChaseTask(target, 10, 3f, 4f, true));
    Entity npc =
            new Entity()
                    .addComponent(new PhysicsComponent())
                    .addComponent(new PhysicsMovementComponent())
                    .addComponent(new ColliderComponent())
                    .addComponent(new HitboxComponent().setLayer(PhysicsLayer.NPC))
                    .addComponent(new TouchAttackComponent(PhysicsLayer.PLAYER, 1.5f))
                    .addComponent(aiComponent);

    PhysicsUtils.setScaledCollider(npc, 0.9f, 0.4f);
    return npc;
  }

In method start() and stop() in ChaseTask, add conditions to start/stop the effects once the chase started/stopped

public void start() {
    ...
    if (this.isBoss) {
        playTensionSound();
        this.target.getEvents().trigger("startHealthBarBeating");
    }
}
public void stop() {
    ...
    if (this.isBoss) {
        stopTensionSound();
        this.target.getEvents().trigger("stopHealthBarBeating");
    }
}

With these two methods to play/stop the tension (heart beating) sound

  void playTensionSound() {
    if (heartbeatSound == null && ServiceLocator.getResourceService() != null) {
      heartbeatSound = ServiceLocator.getResourceService().getAsset(heartbeat, Music.class);
      heartbeatSound.setLooping(true);
      heartbeatSound.setVolume(1.0f);
    }
    if (heartbeatSound != null) {
      ForestGameArea.pauseMusic();
      heartbeatSound.play();
    }
  }

  void stopTensionSound() {
    if (heartbeatSound != null) {
      ForestGameArea.playMusic();
      heartbeatSound.stop();
    }
  }

In the PlayerStatsDisplay class, add listeners to detect the triggers mentioned

    // Add listener for kanga chase start/stop to trigger beating effect
    entity.getEvents().addListener("startHealthBarBeating", this::startHealthBarBeating);
    entity.getEvents().addListener("stopHealthBarBeating", this::stopHealthBarBeating);

Then calls the said methods

  public void startHealthBarBeating() {
    // Stop any existing beating actions
    heartImage.clearActions();
    vignetteImage.clearActions();

    vignetteImage.setVisible(true);

    heartImage.addAction(Actions.forever(
            Actions.sequence(
                    Actions.scaleTo(1.0f, 1.05f, 0.3f), // Slightly enlarge
                    Actions.scaleTo(1.0f, 0.95f, 0.3f)  // Return to normal size
            )
    ));

    vignetteImage.addAction(Actions.forever(
            Actions.sequence(
                    Actions.fadeIn(0.3f), // Fade in for vignette effect
                    Actions.fadeOut(0.3f)  // Fade out for vignette effect
            )
    ));
  }

  public void stopHealthBarBeating() {
    heartImage.clearActions();
    vignetteImage.clearActions();
    vignetteImage.setVisible(false); // Hide vignette when not beating
    heartImage.setScale(1.0f); // Reset to normal scale
  }

The visual effects include the screen corners dimming and health bar beating.

image

Triggering combat

In PlayerActions class, add a listener to listen to the trigger to start combat

entity.getEvents().addListener("startCombat", this::startCombat);

which calls this method which will determine whether to go into a cutscene first before the actual combat if that enemy is a boss

  public void startCombat(Entity enemy){
    AITaskComponent aiTaskComponent = enemy.getComponent(AITaskComponent.class);
    PriorityTask currentTask = aiTaskComponent.getCurrentTask();

    if ((currentTask instanceof WanderTask && ((WanderTask) currentTask).isBoss() ||
            (currentTask instanceof ChaseTask  && ((ChaseTask) currentTask).isBoss()))) {
      currentTask.stop();
      game.addBossCutsceneScreen(enemy);
    } else {
      game.addCombatScreen(enemy);
    }
  }

if addBossCutsceneScreen(enemy) is called, the screen will go transitioned into the screen from BossCutsceneScreen class, which after 3 seconds will transition into to the combat screen.

public class BossCutsceneScreen extends ScreenAdapter {
    private static final float CUTSCENE_DURATION = 3.0f; // Cutscene lasts for 3 seconds
    private float timeElapsed = 0;
    private boolean transition;
    ...

    public void render(float delta) {
        if (!isPaused) {
            physicsEngine.update();
            ServiceLocator.getEntityService().update();
            renderer.render();

            timeElapsed += delta;
            if (timeElapsed >= CUTSCENE_DURATION && !transition) {
                transition = true;
                logger.info("Cutscene finished, transitioning to combat screen");
                // dispose();
                game.setScreen(new CombatScreen(game, oldScreen, oldScreenServices, enemy));
            }
        }
    }

    private void createUI() {
        // animate the cutscene
    }
    ...
}

The cutscene shows an animation of the Boss NPC sprite, with its name sliding from the sides of the screen.

image

Testing Plan

Testing ensures that the functionalities being developed work as expected, which roughly includes:

  • Triggering the boss spawn correctly
  • Starting/Stopping audio/visual effects at the right time
  • Transitioning into cutscene and combat works correctly

The approaches for testing are by using JUnit tests and also by inspection, which is done casually to manually review and debug the code.

Video testing: https://www.youtube.com/watch?v=9uUHNTqCTns

UML Diagram

image
Clone this wiki locally