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());
}
}
BossFactory
class in createBossNPC()
method
Usage in 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);
ForestGameArea
class, add listener to the boss entity to listen to the event to spawn joey
In 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);
}
}
EnemyFactory
class
where the Joey enemy is created in the /**
* 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
https://youtu.be/yU9CXT3zjS0
Visual Testing -https://github.com/user-attachments/assets/8cd3d9bb-1fea-43e9-b8b3-2c146fc2736d
Testing for the Joey entity
EnemyFactoryTest
class:
In /**
* 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());
}