diff --git a/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java b/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java index 8409382ba..83b392814 100644 --- a/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java +++ b/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java @@ -34,15 +34,15 @@ public class ForestGameArea extends GameArea { private static final int NUM_GHOSTS = 0; private static final int NUM_GRUNTS = 5; private static final int NUM_BOSS = 4; - - + + private Timer bossSpawnTimer; private int bossSpawnInterval = 10000; // 1 minute in milliseconds private static final int NUM_WEAPON_TOWERS = 3; private static final GridPoint2 PLAYER_SPAWN = new GridPoint2(0, 0); // Temporary spawn point for testing private static final float WALL_WIDTH = 0.1f; - + // Required to load assets before using them private static final String[] forestTextures = { "images/ingamebg.png", @@ -142,17 +142,17 @@ public class ForestGameArea extends GameArea { }; private static final String backgroundMusic = "sounds/background/Sci-Fi1.ogg"; private static final String[] forestMusic = {backgroundMusic}; - + private final TerrainFactory terrainFactory; - + private Entity player; - + // Variables to be used with spawn projectile methods. This is the variable // that should occupy the direction param. private static final int towardsMobs = 100; private Entity bossKing2; - - + + /** * Initialise this ForestGameArea to use the provided TerrainFactory. * @@ -163,7 +163,7 @@ public ForestGameArea(TerrainFactory terrainFactory) { super(); this.terrainFactory = terrainFactory; } - + /** * Create the game area, including terrain, static entities (trees), dynamic entities (player) */ @@ -176,13 +176,13 @@ public void create() { // spawnBuilding1(); // spawnBuilding2(); // spawnMountains(); - + // Set up infrastructure for end game tracking player = spawnPlayer(); player.getEvents().addListener("spawnWave", this::spawnXenoGrunts); - + playMusic(); - + // Types of projectile // spawnAoeProjectile(new Vector2(0, 10), player, towardsMobs, new Vector2(2f, 2f), 1); spawnProjectile(new Vector2(0, 10), PhysicsLayer.NPC, towardsMobs, new Vector2(2f, 2f)); @@ -203,10 +203,10 @@ public void create() { // spawnGapScanners(); // bossKing1 = spawnBossKing1(); // bossKing2 = spawnBossKing2(); - + bossKing2 = spawnBossKing2(); } - + private void displayUI() { Entity ui = new Entity(); ui.addComponent(new GameAreaDisplay("Box Forest")); @@ -214,17 +214,17 @@ private void displayUI() { ui.addComponent(ServiceLocator.getCurrencyService().getDisplay()); spawnEntity(ui); } - + private void spawnTerrain() { // Background terrain terrain = terrainFactory.createTerrain(TerrainType.FOREST_DEMO); spawnEntity(new Entity().addComponent(terrain)); - + // Terrain walls float tileSize = terrain.getTileSize(); GridPoint2 tileBounds = terrain.getMapBounds(0); Vector2 worldBounds = new Vector2(tileBounds.x * tileSize, tileBounds.y * tileSize); - + // Left // ! THIS ONE DOESNT WORK. GRIDPOINTS2UTIL.ZERO is (0, 4), not (0, 0) spawnEntityAt( @@ -288,26 +288,26 @@ private void spawnTerrain() { // spawnEntityAt(building1, randomPos, true, false); // } // } - + private void spawnBuilding2() { GridPoint2 minPos = new GridPoint2(0, 0); GridPoint2 maxPos = terrain.getMapBounds(0).sub(2, 2); - + for (int i = 0; i < NUM_BUILDINGS; i++) { GridPoint2 randomPos = RandomUtils.random(minPos, maxPos); Entity building2 = ObstacleFactory.createBuilding2(); spawnEntityAt(building2, randomPos, true, false); } } - - + + private Entity spawnPlayer() { Entity newPlayer = PlayerFactory.createPlayer(); spawnEntityAt(newPlayer, PLAYER_SPAWN, true, true); newPlayer.addComponent(new TouchAttackComponent(PhysicsLayer.NPC)); return newPlayer; } - + // Spawn player at a specific position private Entity spawnPlayer(GridPoint2 position) { Entity newPlayer = PlayerFactory.createPlayer(); @@ -337,7 +337,7 @@ private Entity spawnPlayer(GridPoint2 position) { // spawnEntityAt(ghostKing, randomPos, true, true); // return ghostKing; // } - + /** * Spawns a projectile that only heads towards the enemies in its lane. * @@ -351,7 +351,7 @@ private void spawnProjectile(Vector2 position, short targetLayer, int direction, Projectile.setPosition(position); spawnEntity(Projectile); } - + /** * Spawns a projectile specifically for general mobs/xenohunters * @@ -365,7 +365,7 @@ private void spawnProjectileTest(Vector2 position, short targetLayer, int direct Projectile.setPosition(position); spawnEntity(Projectile); } - + /** * Spawns a projectile to be used for multiple projectile function. * @@ -396,8 +396,8 @@ private void spawnProjectile(Vector2 position, short targetLayer, int space, int // return bossKing1; // // } - - + + private void spawnXenoGrunts() { int[] pickedLanes = new Random().ints(1, 7) .distinct().limit(5).toArray(); @@ -438,11 +438,11 @@ private void spawnXenoGrunts() { // } // return bossKing2; // } - + private Entity spawnBossKing2() { GridPoint2 minPos = new GridPoint2(0, 0); GridPoint2 maxPos = terrain.getMapBounds(0).sub(2, 2); - + for (int i = 0; i < NUM_BOSS; i++) { int fixedX = terrain.getMapBounds(0).x - 1; // Rightmost x-coordinate int randomY = MathUtils.random(0, maxPos.y); @@ -455,7 +455,7 @@ private Entity spawnBossKing2() { } return bossKing2; } - + /** * Creates multiple projectiles that travel simultaneous. They all have same * the starting point but different destinations. @@ -474,7 +474,7 @@ private void spawnMultiProjectile(Vector2 position, short targetLayer, int direc --half; } } - + /** * Returns projectile that can do an area of effect damage * @@ -491,7 +491,7 @@ private void spawnEffectProjectile(Vector2 position, short targetLayer, int dire Projectile.setPosition(position); spawnEntity(Projectile); } - + /** * Spawns a pierce fireball. * Pierce fireball can go through targetlayers without disappearing but damage @@ -507,7 +507,7 @@ private void spawnPierceFireBall(Vector2 position, short targetLayer, int direct projectile.setPosition(position); spawnEntity(projectile); } - + /** * Spawns a ricochet fireball * Ricochet fireballs bounce off targets with a specified maximum count of 3 @@ -524,7 +524,7 @@ private void spawnRicochetFireball(Vector2 position, short targetLayer, int dire projectile.setPosition(position); spawnEntity(projectile); } - + /** * Spawns a split firework fireball. * Splits into mini projectiles that spreads out after collision. @@ -540,11 +540,11 @@ private void spawnSplitFireWorksFireBall(Vector2 position, short targetLayer, in projectile.setPosition(position); spawnEntity(projectile); } - + private void spawnWeaponTower() { GridPoint2 minPos = new GridPoint2(0, 0); GridPoint2 maxPos = terrain.getMapBounds(0).sub(2, 2); - + for (int i = 0; i < NUM_WEAPON_TOWERS; i++) { GridPoint2 randomPos1 = RandomUtils.random(minPos, maxPos); GridPoint2 randomPos2 = RandomUtils.random(minPos, maxPos); @@ -555,27 +555,27 @@ private void spawnWeaponTower() { spawnEntityAt(stunTower, randomPos2, true, true); } } - + private void spawnTNTTower() { GridPoint2 minPos = new GridPoint2(0, 0); GridPoint2 maxPos = terrain.getMapBounds(0).sub(2, 2); - + for (int i = 0; i < NUM_WEAPON_TOWERS; i++) { GridPoint2 randomPos = RandomUtils.random(minPos, maxPos); Entity weaponTower = TowerFactory.createTNTTower(); spawnEntityAt(weaponTower, randomPos, true, true); } - + } - - + + private void playMusic() { Music music = ServiceLocator.getResourceService().getAsset(backgroundMusic, Music.class); music.setLooping(true); music.setVolume(0.3f); music.play(); } - + private void loadAssets() { logger.debug("Loading assets"); ResourceService resourceService = ServiceLocator.getResourceService(); @@ -583,13 +583,13 @@ private void loadAssets() { resourceService.loadTextureAtlases(forestTextureAtlases); resourceService.loadSounds(forestSounds); resourceService.loadMusic(forestMusic); - + while (!resourceService.loadForMillis(10)) { // This could be upgraded to a loading screen logger.info("Loading... {}%", resourceService.getProgress()); } } - + private void unloadAssets() { logger.debug("Unloading assets"); ResourceService resourceService = ServiceLocator.getResourceService(); @@ -598,53 +598,53 @@ private void unloadAssets() { resourceService.unloadAssets(forestSounds); resourceService.unloadAssets(forestMusic); } - + @Override public void dispose() { super.dispose(); ServiceLocator.getResourceService().getAsset(backgroundMusic, Music.class).stop(); this.unloadAssets(); } - + private void spawnScrap() { GridPoint2 minPos = new GridPoint2(0, 0); GridPoint2 maxPos = terrain.getMapBounds(0).sub(2, 2); - + for (int i = 0; i < 5; i++) { GridPoint2 randomPos = RandomUtils.random(minPos, maxPos); Entity scrap = DropFactory.createScrapDrop(); spawnEntityAt(scrap, randomPos, true, false); } - + for (int i = 0; i < 5; i++) { GridPoint2 randomPos = RandomUtils.random(minPos, maxPos); Entity crystal = DropFactory.createCrystalDrop(); spawnEntityAt(crystal, randomPos, true, false); } } - + private void spawnIncome() { GridPoint2 minPos = new GridPoint2(0, 0); GridPoint2 maxPos = terrain.getMapBounds(0).sub(2, 2); - + for (int i = 0; i < 50; i++) { GridPoint2 randomPos = RandomUtils.random(minPos, maxPos); Entity towerfactory = TowerFactory.createIncomeTower(); spawnEntityAt(towerfactory, randomPos, true, true); } } - + private void spawnEngineer() { for (int i = 0; i < terrain.getMapBounds(0).x; i += 3) { Entity engineer = EngineerFactory.createEngineer(); spawnEntityAt(engineer, new GridPoint2(1, i), true, true); } } - - /** - * Creates the scanners (one per lane) that detect absence of towers and presence of mobs, - * and trigger engineer spawning - */ + + /** + * Creates the scanners (one per lane) that detect absence of towers and presence of mobs, + * and trigger engineer spawning + */ // private void spawnGapScanners() { // for (int i = 0; i < terrain.getMapBounds(0).y; i++) { // Entity scanner = GapScannerFactory.createScanner(); diff --git a/source/core/src/main/com/csse3200/game/components/tasks/human/EngineerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/human/EngineerCombatTask.java index 23dbea9c0..337218153 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/human/EngineerCombatTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/human/EngineerCombatTask.java @@ -26,12 +26,9 @@ public class EngineerCombatTask extends DefaultTask implements PriorityTask { private static final short TARGET = PhysicsLayer.NPC; // The type of targets that the Engineer will detect. // Animation event names for the Engineer's state machine. - private static final String STOW = ""; - private static final String DEPLOY = ""; private static final String FIRING = "firingSingleStart"; - private static final String IDLE_LEFT = "idleLeft"; private static final String IDLE_RIGHT = "idleRight"; - private static final String DYING = "deathStart"; + private static final String ENGINEER_PROJECTILE_FIRED = "engineerProjectileFired"; // The Engineer's attributes. private final float maxRange; // The maximum range of the Engineer's weapon. @@ -52,7 +49,7 @@ public class EngineerCombatTask extends DefaultTask implements PriorityTask { /** The Engineer's states. */ private enum STATE { - IDLE_LEFT, IDLE_RIGHT, DEPLOY, FIRING, STOW + IDLE_RIGHT, FIRING } private STATE engineerState = STATE.IDLE_RIGHT; @@ -96,48 +93,32 @@ public void update() { * Engineer state machine */ public void updateEngineerState() { - // configure tower state depending on target visibility + // configure engineer state depending on target visibility switch (engineerState) { - case IDLE_LEFT -> { - // targets detected in idle mode - start deployment - if (isTargetVisible()) { - owner.getEntity().getEvents().trigger(FIRING); - engineerState = STATE.FIRING; - } else { - - } - } case IDLE_RIGHT -> { // targets detected in idle mode - start deployment if (isTargetVisible()) { combatState(); } } - case DEPLOY -> { - // currently deploying, - if (isTargetVisible()) { - combatState(); - } else { - owner.getEntity().getEvents().trigger(STOW); - engineerState = STATE.STOW; - } - } case FIRING -> { // targets gone - stop firing if (!isTargetVisible()) { owner.getEntity().getEvents().trigger(IDLE_RIGHT); engineerState = STATE.IDLE_RIGHT; } else { - if (shotsFired <= 10) { + if (shotsFired <= weaponCapacity) { owner.getEntity().getEvents().trigger(FIRING); + owner.getEntity().getEvents().trigger(ENGINEER_PROJECTILE_FIRED); // this might be changed to an event which gets triggered everytime the tower enters the firing state Entity newProjectile = ProjectileFactory.createEngineerBullet(PhysicsLayer.NPC, new Vector2(100, owner.getEntity().getPosition().y), new Vector2(4f, 4f)); newProjectile.setScale(0.8f, 0.8f); - newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.3), (float) (owner.getEntity().getPosition().y + 0.15)); + newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.3), + (float) (owner.getEntity().getPosition().y + 0.15)); ServiceLocator.getEntityService().register(newProjectile); - shotsFired += 1; + shotsFired ++; reloadTime = timeSource.getTime(); } else { // engineer needs to reload @@ -185,7 +166,7 @@ public int getPriority() { public boolean isTargetVisible() { // If there is an obstacle in the path to the max range point, mobs visible. Vector2 position = owner.getEntity().getCenterPosition(); - + hits.clear(); for (int i = 5; i > -5; i--) { if (physics.raycast(position, new Vector2(position.x + maxRange, position.y + i), TARGET, hit)) { hits.add(hit); diff --git a/source/core/src/main/com/csse3200/game/entities/factories/EngineerFactory.java b/source/core/src/main/com/csse3200/game/entities/factories/EngineerFactory.java index 3f0edc0a5..7bdd6396a 100644 --- a/source/core/src/main/com/csse3200/game/entities/factories/EngineerFactory.java +++ b/source/core/src/main/com/csse3200/game/entities/factories/EngineerFactory.java @@ -35,11 +35,11 @@ public class EngineerFactory { private static final int COMBAT_TASK_PRIORITY = 2; private static final int ENGINEER_RANGE = 10; private static final EngineerConfigs configs = - FileLoader.readClass(EngineerConfigs.class, "configs/Engineers.json"); - + FileLoader.readClass(EngineerConfigs.class, "configs/Engineers.json"); + private static final float HUMAN_SCALE_X = 1f; private static final float HUMAN_SCALE_Y = 0.8f; - + /** * Creates an Engineer entity, based on a base Human entity, with the appropriate components and animations * @@ -49,7 +49,7 @@ public class EngineerFactory { public static Entity createEngineer() { Entity engineer = createBaseHumanNPC(); BaseEntityConfig config = configs.engineer; - + AnimationRenderComponent animator = new AnimationRenderComponent( new TextureAtlas("images/engineers/engineer.atlas")); animator.addAnimation("walk_left", 0.2f, Animation.PlayMode.LOOP); @@ -61,43 +61,43 @@ public static Entity createEngineer() { animator.addAnimation("prep", 0.05f, Animation.PlayMode.NORMAL); animator.addAnimation("hit", 0.01f, Animation.PlayMode.NORMAL); animator.addAnimation("death", 0.1f, Animation.PlayMode.NORMAL); - + AITaskComponent aiComponent = new AITaskComponent(); - + engineer .addComponent(new CombatStatsComponent(config.health, config.baseAttack)) .addComponent(animator) .addComponent(new HumanAnimationController()) .addComponent(aiComponent); - + engineer.getComponent(AITaskComponent.class).addTask(new HumanWanderTask(COMBAT_TASK_PRIORITY, ENGINEER_RANGE)); engineer.getComponent(AnimationRenderComponent.class).scaleEntity(); engineer.setScale(HUMAN_SCALE_X, HUMAN_SCALE_Y); return engineer; } - + /** * Creates a generic human npc to be used as a base entity by more specific NPC creation methods. * * @return entity */ public static Entity createBaseHumanNPC() { - - + + Entity human = - new Entity() - .addComponent(new PhysicsComponent()) - .addComponent(new PhysicsMovementComponent()) - .addComponent(new ColliderComponent()) - .addComponent(new HitboxComponent().setLayer(PhysicsLayer.ENGINEER)) - .addComponent(new TouchAttackComponent(PhysicsLayer.NPC, 1.5f)); - - + new Entity() + .addComponent(new PhysicsComponent()) + .addComponent(new PhysicsMovementComponent()) + .addComponent(new ColliderComponent()) + .addComponent(new HitboxComponent().setLayer(PhysicsLayer.ENGINEER)) + .addComponent(new TouchAttackComponent(PhysicsLayer.NPC, 1.5f)); + + PhysicsUtils.setScaledCollider(human, 0.9f, 0.4f); return human; } - + private EngineerFactory() { throw new IllegalStateException("Instantiating static util class"); } -} +} \ No newline at end of file diff --git a/source/core/src/test/com/csse3200/game/components/tasks/EngineerCombatTaskTest.java b/source/core/src/test/com/csse3200/game/components/tasks/EngineerCombatTaskTest.java new file mode 100644 index 000000000..8a5954efd --- /dev/null +++ b/source/core/src/test/com/csse3200/game/components/tasks/EngineerCombatTaskTest.java @@ -0,0 +1,211 @@ +package com.csse3200.game.entities.factories; + +import com.badlogic.gdx.audio.Sound; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.csse3200.game.ai.tasks.AITaskComponent; +import com.csse3200.game.components.CombatStatsComponent; +import com.csse3200.game.components.TouchAttackComponent; +import com.csse3200.game.components.player.HumanAnimationController; +import com.csse3200.game.components.tasks.FinalBossMovementTask; +import com.csse3200.game.components.tasks.human.EngineerCombatTask; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.EntityService; +import com.csse3200.game.events.listeners.EventListener0; +import com.csse3200.game.extensions.GameExtension; +import com.csse3200.game.physics.PhysicsLayer; +import com.csse3200.game.physics.PhysicsService; +import com.csse3200.game.physics.components.ColliderComponent; +import com.csse3200.game.physics.components.HitboxComponent; +import com.csse3200.game.physics.components.PhysicsComponent; +import com.csse3200.game.physics.components.PhysicsMovementComponent; +import com.csse3200.game.rendering.AnimationRenderComponent; +import com.csse3200.game.rendering.DebugRenderer; +import com.csse3200.game.rendering.RenderService; +import com.csse3200.game.services.GameTime; +import com.csse3200.game.services.ResourceService; +import com.csse3200.game.services.ServiceLocator; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + + +import java.util.ArrayList; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(GameExtension.class) +class EngineerCombatTaskTest { + private final int MAX_RANGE = 10; + private EngineerCombatTask COMBAT_TASK; + private Entity MOCK_ENGINEER; + + private final String[] atlas = {"images/engineers/engineer.atlas"}; + private final String[] projectileAtlas = {"images/projectiles/engineer_projectile.atlas"}; + private static final String[] sounds = { + "sounds/engineers/firing_auto.mp3", + "sounds/engineers/firing_single.mp3" + }; + + private final String[] animations = { + "idle_right", + "walk_left", + "walk_right", + "walk_prep", + "prep", + "firing_auto", + "firing_single", + "hit", + "death" + }; + + @BeforeEach + void setUp() { + GameTime gameTime = mock(GameTime.class); + when(gameTime.getDeltaTime()).thenReturn(0.02f); + + ServiceLocator.registerTimeSource(gameTime); + ServiceLocator.registerPhysicsService(new PhysicsService()); + ServiceLocator.registerEntityService(new EntityService()); + RenderService render = new RenderService(); + render.setDebug(mock(DebugRenderer.class)); + ServiceLocator.registerRenderService(render); + ResourceService resourceService = new ResourceService(); + ServiceLocator.registerResourceService(resourceService); + resourceService.loadTextureAtlases(atlas); + resourceService.loadTextureAtlases(projectileAtlas); + resourceService.loadSounds(sounds); + resourceService.loadAll(); + + // Create a mock engineer and add the combat task. + COMBAT_TASK = new EngineerCombatTask(MAX_RANGE); + MOCK_ENGINEER = EngineerFactory.createEngineer(); + MOCK_ENGINEER.getComponent(AITaskComponent.class).addTask(COMBAT_TASK); + COMBAT_TASK.create(MOCK_ENGINEER.getComponent(AITaskComponent.class)); + } + + /** + * Tests that the task correctly triggers an idle event. + */ + @Test + void testIdleEvent() { + + // Add a listener to the engineer to test for the idle animation trigger. + EventListener0 idleListener = mock(EventListener0.class); + MOCK_ENGINEER.getEvents().addListener("idleRight", idleListener); + + // Start the task. + COMBAT_TASK.start(); + + // Update physics and check whether the event is triggered. + updatePhysics(); + verify(idleListener).handle(); + } + + /** + * Adapted from EngineerFactoryTest: Author - The-AhmadAA + * + * Tests that the task correctly switches to the + * firing state and triggers a firing event. + */ + @Test + void testFiringEvent() { + + // Create an attacker entity. + Entity MOCK_ATTACKER = createAttacker(PhysicsLayer.NPC); + + // Add a listener to the engineer to test for the firing animation trigger. + EventListener0 firingListener = mock(EventListener0.class); + MOCK_ENGINEER.getEvents().addListener("firingSingleStart", firingListener); + + // Set testing positions for the engineer and the attacker and start the task. + MOCK_ENGINEER.setPosition(10,10); + MOCK_ATTACKER.setPosition(12,10); + COMBAT_TASK.start(); + + // Update physics and check whether the event is triggered. + updatePhysics(); + verify(firingListener).handle(); + } + + /** + * Adapted from EngineerFactoryTest: Author - The-AhmadAA + * + * Test that the task can switch the state back to idle after mob isn't visible. + */ + @Test + void testIdleAfterMobKilled() { + // Create an attacker entity. + Entity MOCK_ATTACKER = createAttacker(PhysicsLayer.NPC); + + // Set testing positions for the engineer and the attacker and start the task. + MOCK_ENGINEER.setPosition(10,10); + MOCK_ATTACKER.setPosition(12,10); + COMBAT_TASK.start(); + + // Update physics, move the attacker out of range + // and check whether the event is triggered. + updatePhysics(); + MOCK_ATTACKER.dispose(); + EventListener0 idleListener = mock(EventListener0.class); + MOCK_ENGINEER.getEvents().addListener("idleRight", idleListener); + updatePhysics(); + + // Verify the idle animation trigger's occurrence if the attacker is gone. + if (!COMBAT_TASK.isTargetVisible()) { + verify(idleListener).handle(); + + // Fail the test if the attacked remains visible after deletion. + } else { + fail(); + } + } + + @Test + void testProjectileFired() { + // Create an attacker entity. + Entity MOCK_ATTACKER = createAttacker(PhysicsLayer.NPC); + + // Set testing positions for the engineer and the attacker and start the task. + MOCK_ENGINEER.setPosition(10,10); + MOCK_ATTACKER.setPosition(12,10); + COMBAT_TASK.start(); + + // Update physics and check whether a projectile is fired. + updatePhysics(); + EventListener0 projectileListener = mock(EventListener0.class); + MOCK_ENGINEER.getEvents().addListener("engineerProjectileFired", projectileListener); + updatePhysics(); + + verify(projectileListener).handle(); + + } + + /** + * Adapted from EngineerFactoryTest: Author - The-AhmadAA + */ + Entity createAttacker(short targetLayer) { + Entity entity = + new Entity() + .addComponent(new TouchAttackComponent(targetLayer)) + .addComponent(new CombatStatsComponent(0, 10)) + .addComponent(new PhysicsComponent()) + .addComponent(new HitboxComponent().setLayer(targetLayer)); + entity.create(); + return entity; + } + + /** + * Update the game physics and the task state. + */ + void updatePhysics() { + ServiceLocator.getPhysicsService().getPhysics().update(); + COMBAT_TASK.updateEngineerState(); + } +} \ No newline at end of file