Special Interaction for Water 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 Leviathan, the Water Area boss, its special ability allows it to shoot out water stream continuously while chasing towards the player, which will do damage to the player if touched.
Implementation
LeviathanTask
class - an amalgamation of ChaseTask
and ShootTask
/**
* A task that allows an entity to chase the player and fire projectiles at the player when within range.
*/
public class LeviathanTask extends DefaultTask implements PriorityTask {
private static final Logger logger = LoggerFactory.getLogger(LeviathanTask.class);
private final int priority;
private final float viewDistance;
private final float maxChaseDistance;
private final float fireRange;
private final float waitTime;
private final Entity target;
private final PhysicsEngine physics;
private final DebugRenderer debugRenderer;
private final RaycastHit hit = new RaycastHit();
private final GameTime timer;
private long lastShotTime;
private int numShots = 0;
private MovementTask movementTask;
private Music heartbeatSound;
private static final String heartbeat = "sounds/heartbeat.mp3";
private final Vector2 bossSpeed;
/**
* @param target The entity to chase.
* @param priority Task priority when chasing.
* @param viewDistance Maximum distance to start chasing.
* @param maxChaseDistance Maximum distance for the entity to chase before giving up.
* @param fireRange Distance within which the entity will start firing projectiles.
* @param waitTime Time between firing projectiles.
*/
public LeviathanTask(Entity target, int priority, float viewDistance, float maxChaseDistance, float fireRange, float waitTime) {
this.target = target;
this.priority = priority;
this.viewDistance = viewDistance;
this.maxChaseDistance = maxChaseDistance;
this.fireRange = fireRange;
this.waitTime = waitTime;
this.physics = ServiceLocator.getPhysicsService().getPhysics();
this.debugRenderer = ServiceLocator.getRenderService().getDebug();
this.timer = ServiceLocator.getTimeSource();
this.bossSpeed = new Vector2(2.0f, 2.0f);
this.lastShotTime = timer.getTime();
}
@Override
public void start() {
super.start();
Vector2 targetPos = target.getPosition();
movementTask = new MovementTask(targetPos, bossSpeed);
movementTask.create(owner);
movementTask.start();
playTensionSound();
target.getEvents().trigger("startHealthBarBeating");
}
@Override
public void update() {
Vector2 targetPos = target.getPosition();
Vector2 currentPos = owner.getEntity().getPosition();
if (targetPos.x - currentPos.x < 0) {
owner.getEntity().getEvents().trigger("chaseLeft");
} else {
owner.getEntity().getEvents().trigger("chaseRight");
}
movementTask.setTarget(targetPos);
movementTask.update();
if (movementTask.getStatus() != Status.ACTIVE) {
movementTask.start();
}
if ((getDistanceToTarget() <= fireRange && timer.getTime() - lastShotTime > waitTime) || numShots == 0) {
startShooting();
}
}
@Override
public void stop() {
super.stop();
movementTask.stop();
stopTensionSound();
target.getEvents().trigger("stopHealthBarBeating");
}
@Override
public int getPriority() {
if (status == Status.ACTIVE) {
return getActivePriority();
}
return getInactivePriority();
}
private float getDistanceToTarget() {
return owner.getEntity().getPosition().dst(target.getPosition());
}
private int getActivePriority() {
float dst = getDistanceToTarget();
if (dst > maxChaseDistance || !isTargetVisible()) {
return -1; // Too far, stop chasing
}
return priority;
}
private int getInactivePriority() {
float dst = getDistanceToTarget();
if (dst < viewDistance && isTargetVisible()) {
return priority;
}
return -1;
}
private boolean isTargetVisible() {
Vector2 from = owner.getEntity().getCenterPosition();
Vector2 to = target.getCenterPosition();
// If there is an obstacle in the path to the player, not visible.
if (physics.raycast(from, to, PhysicsLayer.OBSTACLE, hit)) {
debugRenderer.drawLine(from, hit.point);
return false;
}
debugRenderer.drawLine(from, to);
return true;
}
private void startShooting() {
logger.debug("Shooting at target");
lastShotTime = timer.getTime();
numShots++;
owner.getEntity().getEvents().trigger("spawnWaterSpiral", owner.getEntity());
}
void playTensionMusic() {
// Play the music using AudioManager
AudioManager.stopMusic();
AudioManager.playMusic("sounds/tension-water-boss.mp3", true);
}
void stopTensionMusic() {
// Stop the music using AudioManager
AudioManager.stopMusic();
// Get the selected music track from the user settings
UserSettings.Settings settings = UserSettings.get();
String selectedTrack = settings.selectedMusicTrack; // This will be "Track 1" or "Track 2"
if (Objects.equals(selectedTrack, "Track 1")) {
AudioManager.playMusic("sounds/BGM_03_mp3.mp3", true);
} else if (Objects.equals(selectedTrack, "Track 2")) {
AudioManager.playMusic("sounds/track_2.mp3", true);
}
}
}
BossFactory
class in createBossNPC()
method
Usage in Attach the 'LeviathanTask' task to an AITaskComponent for Leviathan boss, which is then added to its Entity
AITaskComponent aiComponent = new AITaskComponent();
if (type == Entity.EnemyType.WATER_BOSS) {
aiComponent.addTask(new LeviathanTask(target, 10, 10f, 16f, 100f, 300));
}
Entity npc = new Entity().addComponent(aiComponent);
ForestGameArea
class, add listener to the boss entity to listen to the event to spawn Water Spiral
In private void spawnWaterBoss() {
if (!waterBossSpawned) {
Entity waterBoss = BossFactory.createWaterBossEntity(player);
waterBoss.getEvents().addListener("spawnWaterSpiral", this::spawnWaterSpiral);
spawnBossOnMap(waterBoss);
enemies.add(waterBoss);
waterBossSpawned = true;
}
}
private void spawnWaterSpiral(Entity boss) {
if (boss != null) {
Entity waterSpiral = ProjectileFactory.createWaterSpiral(player);
float posX = (boss.getPosition().x - player.getPosition().x) > 0 ? -1 : 1;
float posY = (boss.getPosition().y - player.getPosition().y) > 0 ? 1 : -1;
Vector2 pos = new Vector2(boss.getPosition().x + posX, boss.getPosition().y + posY);
spawnEntityAtVector(waterSpiral, pos);
}
}
ProjectileFactory
class
where the Water Spiral is created in the public static Entity createWaterSpiral(Entity target) {
Entity waterSpiral = createBaseProjectile(target);
BaseEnemyEntityConfig config = configs.waterSpiral;
AITaskComponent aiTaskComponent = new AITaskComponent();
aiTaskComponent.addTask(new ProjectileMovementTask(target, 10));
waterSpiral.addComponent(aiTaskComponent);
TextureAtlas waterSpiralAtlas = ServiceLocator.getResourceService().getAsset(config.getSpritePath(), TextureAtlas.class);
AnimationRenderComponent animator = new AnimationRenderComponent(waterSpiralAtlas);
animator.addAnimation("waterSpiral", 0.1f, Animation.PlayMode.LOOP);
waterSpiral
.addComponent(animator)
.addComponent(new WaterSpiralAnimationController());
waterSpiral.setScale(3.0f, 3.0f);
waterSpiral.getComponent(PhysicsMovementComponent.class).changeMaxSpeed(new Vector2(config.getSpeed(), config.getSpeed()));
return waterSpiral;
}
Testing Plan
Water Spiral Projectile
Unit Tests documentation for thehttps://youtu.be/yU9CXT3zjS0
Visual Testing -https://github.com/user-attachments/assets/8cd3d9bb-1fea-43e9-b8b3-2c146fc2736d