Special Interaction for Air 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 Griffin, the Air Area boss, the special ability is that, instead of chasing the player, it runs away from the player while shooting gust of wind at the player.
Implementation
GriffinTask
class - an amalgamation of RunTask
and ShootTask
public class GriffinTask extends DefaultTask implements PriorityTask {
private static final Logger logger = LoggerFactory.getLogger(GriffinTask.class);
private final Entity target;
private final int priority;
private final float viewDistance;
private final PhysicsEngine physics;
private final DebugRenderer debugRenderer;
private final RaycastHit hit = new RaycastHit();
private MovementTask movementTask;
// Shooting
private final float waitTime; // Time to wait between firing
private long lastShotTime; // Time of the last shot
private final float shootRange; // Range within which to shoot gust of wind
private final GameTime timer; // Game timer for tracking shot intervals
public GriffinTask(Entity target, int priority, float viewDistance, float waitTime, float shootRange) {
this.target = target;
this.priority = priority;
this.viewDistance = viewDistance;
this.physics = ServiceLocator.getPhysicsService().getPhysics();
this.debugRenderer = ServiceLocator.getRenderService().getDebug();
this.waitTime = waitTime;
this.shootRange = shootRange;
this.timer = ServiceLocator.getTimeSource();
this.lastShotTime = timer.getTime();
}
@Override
public void start() {
super.start();
movementTask = new MovementTask(newPosition(true));
movementTask.create(owner);
movementTask.start();
}
@Override
public void update() {
movementTask.setTarget(newPosition(false));
movementTask.update();
if (movementTask.getStatus() != Status.ACTIVE) {
movementTask.start();
}
if (timer.getTime() - lastShotTime > waitTime || getDistanceToTarget() >= shootRange) {
shootWindGust();
}
}
@Override
public void stop() {
super.stop();
movementTask.stop();
}
@Override
public int getPriority() {
float dst = getDistanceToTarget();
if (dst < viewDistance || isTargetVisible()) {
return priority;
}
return -1;
}
private float getDistanceToTarget() {
return owner.getEntity().getPosition().dst(target.getPosition());
}
private boolean isTargetVisible() {
Vector2 from = owner.getEntity().getCenterPosition();
Vector2 to = target.getCenterPosition();
if (physics.raycast(from, to, PhysicsLayer.OBSTACLE, hit)) {
debugRenderer.drawLine(from, hit.point);
return false;
}
debugRenderer.drawLine(from, to);
return true;
}
private Vector2 newPosition(boolean trigger) {
Vector2 currentPos = owner.getEntity().getPosition();
Vector2 targetPos = target.getPosition();
float deltaX = currentPos.x - targetPos.x;
float deltaY = currentPos.y - targetPos.y;
Vector2 newPos = new Vector2(currentPos.x + deltaX, currentPos.y + deltaY);
if (trigger) {
triggerDirection(newPos, owner.getEntity().getPosition());
}
return newPos;
}
private void triggerDirection(Vector2 targetPos, Vector2 startPos) {
float deltaX = targetPos.x - startPos.x;
if (deltaX > 0) { // Moving Left
this.owner.getEntity().getEvents().trigger("chaseLeft");
} else { // Moving Right
this.owner.getEntity().getEvents().trigger("chaseRight");
}
}
private void shootWindGust() {
logger.debug("Shooting gust of wind at target");
lastShotTime = timer.getTime(); // Update the time of the last shot
owner.getEntity().getEvents().trigger("spawnWindGust", owner.getEntity()); // Trigger the event to shoot
}
/**
* Plays the tension music to enhance the experience during the chase.
*/
void playTensionMusic() {
// Play the music using AudioManager
AudioManager.stopMusic();
AudioManager.playMusic("sounds/tension-air-boss.mp3", true);
}
/**
* Stops playing the tension music and play the background music.
*/
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 'GriffinTask' task to an AITaskComponent for Griffin boss, which is then added to its Entity
AITaskComponent aiComponent = new AITaskComponent();
if (type == Entity.EnemyType.AIR_BOSS) {
aiComponent.addTask(new GriffinTask(target, 10, 8f, 300, 100f));
}
Entity npc = new Entity().addComponent(aiComponent);
ForestGameArea
class, add listener to the boss entity to listen to the event to spawn Wind Gust
In private void spawnAirBoss() {
if (!airBossSpawned) {
Entity airBoss = BossFactory.createAirBossEntity(player);
airBoss.getEvents().addListener("spawnWindGust", this::spawnWindGust);
spawnBossOnMap(airBoss);
enemies.add(airBoss);
airBossSpawned = true;
}
}
private void spawnWindGust(Entity boss) {
if (boss != null) {
Entity windGust = ProjectileFactory.createWindGust(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(windGust, pos);
}
}
ProjectileFactory
class
where the Wind Gust is created in the public static Entity createWindGust(Entity target) {
Entity windGust = createBaseProjectile(target);
BaseEnemyEntityConfig config = configs.windGust;
AITaskComponent aiTaskComponent = new AITaskComponent();
aiTaskComponent.addTask(new ProjectileMovementTask(target, 10));
windGust.addComponent(aiTaskComponent);
TextureAtlas windGustAtlas = ServiceLocator.getResourceService().getAsset(config.getSpritePath(), TextureAtlas.class);
AnimationRenderComponent animator = new AnimationRenderComponent(windGustAtlas);
animator.addAnimation("windGust", 0.1f, Animation.PlayMode.LOOP);
windGust
.addComponent(animator)
.addComponent(new WindGustAnimationController());
windGust.setScale(5.0f, 5.0f);
windGust.getComponent(PhysicsMovementComponent.class).changeMaxSpeed(new Vector2(config.getSpeed(), config.getSpeed()));
return windGust;
}
Testing Plan
Wind Gust Projectile
Unit Tests documentation for thehttps://youtu.be/yU9CXT3zjS0
Visual Testing -https://github.com/user-attachments/assets/8cd3d9bb-1fea-43e9-b8b3-2c146fc2736d