diff --git a/source/core/src/main/com/csse3200/game/components/tasks/DroidCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/DroidCombatTask.java index 57b91f06e..26eb87fa6 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/DroidCombatTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/DroidCombatTask.java @@ -24,13 +24,15 @@ public class DroidCombatTask extends DefaultTask implements PriorityTask { private static final int INTERVAL = 1; // time interval to scan for enemies in seconds private static final short TARGET = PhysicsLayer.NPC; // The type of targets that the tower will detect // the following four constants are the event names that will be triggered in the state machine - private static final String GO_UP = "goUpStart"; - private static final String GO_DOWN = "goDownStart"; - private static final String ATTACK_UP = "attackUpStart"; - private static final String ATTACK_DOWN = "attackDownStart"; - private static final String WALK = "walkStart"; - private static final String DEATH = "deathStart"; - private static final String IDLE = "idleStart"; + public static final String GO_UP = "goUpStart"; + public static final String GO_DOWN = "goDownStart"; + public static final String ATTACK_UP = "attackUpStart"; + public static final String ATTACK_DOWN = "attackDownStart"; + public static final String WALK = "walkStart"; + public static final String DEATH = "deathStart"; + public static final String IDLE = "idleStart"; + public static final String SHOOT_UP = "ShootUp"; + public static final String SHOOT_DOWN = "ShootDown"; // class attributes @@ -43,10 +45,10 @@ public class DroidCombatTask extends DefaultTask implements PriorityTask { private long endTime; private final RaycastHit hit = new RaycastHit(); - private enum STATE { + public enum STATE { IDLE, UP, DOWN, SHOOT_UP, SHOOT_DOWN, WALK, DIE } - private STATE towerState = STATE.WALK; + public STATE towerState = STATE.WALK; /** * @param priority Task priority when targets are detected (0 when nothing detected). Must be a positive integer. @@ -105,6 +107,7 @@ public void updateTowerState() { case IDLE -> { if (isTargetVisible()) { owner.getEntity().getEvents().trigger(ATTACK_UP); + owner.getEntity().getEvents().trigger(SHOOT_UP); towerState = STATE.DOWN; } else { owner.getEntity().getEvents().trigger(IDLE); @@ -113,12 +116,7 @@ public void updateTowerState() { case SHOOT_DOWN -> { if (isTargetVisible()) { owner.getEntity().getEvents().trigger(ATTACK_DOWN); - Entity Projectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, new Vector2(100, - owner.getEntity().getPosition().y), new Vector2(2,2), ProjectileEffects.SLOW, false); - Projectile.setScale(new Vector2(0.5f,0.5f)); - Projectile.setPosition((float) (owner.getEntity().getPosition().x + 0.2), - (float) (owner.getEntity().getPosition().y - 0.2)); - ServiceLocator.getEntityService().register(Projectile); + owner.getEntity().getEvents().trigger(SHOOT_DOWN); towerState = STATE.UP; } else { owner.getEntity().getEvents().trigger(GO_UP); @@ -129,13 +127,8 @@ public void updateTowerState() { if (isTargetVisible()) { owner.getEntity().getEvents().trigger(ATTACK_UP); + owner.getEntity().getEvents().trigger(SHOOT_UP); towerState = STATE.DOWN; - Entity Projectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, new Vector2(100, - owner.getEntity().getPosition().y), new Vector2(2,2), ProjectileEffects.SLOW, false); - Projectile.setScale(new Vector2(0.5f,0.5f)); - Projectile.setPosition((float) (owner.getEntity().getPosition().x + 0.2), - (float) (owner.getEntity().getPosition().y + 0.5)); - ServiceLocator.getEntityService().register(Projectile); } else { owner.getEntity().getEvents().trigger(IDLE); towerState = STATE.IDLE; @@ -176,6 +169,15 @@ public void stop() { // owner.getEntity().getEvents().trigger(STOW); } + /** + * Returns the current state of the tower. + * + * @return the current state of the tower. + */ + public STATE getState() { + return this.towerState; + } + /** * Returns the current priority of the task. * @return active priority value if targets detected, inactive priority otherwise @@ -189,8 +191,10 @@ public int getPriority() { * Uses a raycast to determine whether there are any targets in detection range * @return true if a target is visible, false otherwise */ - private boolean isTargetVisible() { + public boolean isTargetVisible() { // If there is an obstacle in the path to the max range point, mobs visible. return physics.raycast(towerPosition, maxRangePosition, TARGET, hit); } + + } diff --git a/source/core/src/main/com/csse3200/game/components/tasks/TNTTowerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/TNTTowerCombatTask.java index a59361481..2076eb69d 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/TNTTowerCombatTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/TNTTowerCombatTask.java @@ -171,10 +171,12 @@ public boolean isTargetVisible() { return physics.raycast(towerPosition, maxRangePosition, TARGET, hit); } - private boolean isReadyToDelete() { + public boolean isReadyToDelete() { return readToDelete; } + + } diff --git a/source/core/src/test/com/csse3200/game/components/tasks/DroidCombatTaskTest.java b/source/core/src/test/com/csse3200/game/components/tasks/DroidCombatTaskTest.java new file mode 100644 index 000000000..f03282584 --- /dev/null +++ b/source/core/src/test/com/csse3200/game/components/tasks/DroidCombatTaskTest.java @@ -0,0 +1,148 @@ +package com.csse3200.game.components.tasks; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +import com.csse3200.game.ai.tasks.AITaskComponent; +import com.csse3200.game.components.CombatStatsComponent; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.EntityService; +import com.csse3200.game.events.listeners.EventListener0; +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.services.GameTime; +import com.csse3200.game.services.ServiceLocator; +import com.csse3200.game.entities.factories.ProjectileFactory; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class DroidCombatTaskTest { + DroidCombatTask droidCombatTask; + + @BeforeEach + void setUp() { + GameTime gameTime = mock(GameTime.class); + ServiceLocator.registerTimeSource(gameTime); + ServiceLocator.registerPhysicsService(new PhysicsService()); + ServiceLocator.registerEntityService(new EntityService()); + droidCombatTask = new DroidCombatTask(1, 4); + } + + @Test + public void testStartTriggersWalkEvent() { + Entity entity = createDroid(); + EventListener0 walkListener = mock(EventListener0.class); + // Deploy Droid in the walking state + entity.getEvents().addListener(DroidCombatTask.WALK, walkListener); + droidCombatTask.start(); + verify(walkListener).handle(); + } + + @Test + public void testUpdateTowerStateWithTargetInRange() { + Entity entity = createDroid(); + entity.setPosition(10,10); + + Entity Target = createNPC(); + Target.setPosition(12,10); + + EventListener0 attackUp = mock(EventListener0.class); + EventListener0 attackDown = mock(EventListener0.class); + EventListener0 switchDown = mock(EventListener0.class); + EventListener0 shootUp = mock(EventListener0.class); + EventListener0 shootDown = mock(EventListener0.class); + entity.getEvents().addListener(DroidCombatTask.ATTACK_UP, attackUp); + entity.getEvents().addListener(DroidCombatTask.SHOOT_UP,shootUp); + entity.getEvents().addListener(DroidCombatTask.ATTACK_DOWN, attackDown); + entity.getEvents().addListener(DroidCombatTask.GO_DOWN,switchDown); + entity.getEvents().addListener(DroidCombatTask.SHOOT_DOWN,shootDown); + //Jump to IDLE state + droidCombatTask.start(); + droidCombatTask.towerState = DroidCombatTask.STATE.IDLE; + + ServiceLocator.getPhysicsService().getPhysics().update(); + entity.update(); + + assertTrue(droidCombatTask.isTargetVisible()); + + droidCombatTask.updateTowerState(); + // By default, Droid aims from top, so shoot from top + verify(attackUp).handle(); + // shoot projectiles from top + verify(shootUp).handle(); + assertEquals(DroidCombatTask.STATE.DOWN, droidCombatTask.getState()); + + droidCombatTask.updateTowerState(); + // switch to aim downwards + verify(switchDown).handle(); + assertEquals(DroidCombatTask.STATE.SHOOT_DOWN, droidCombatTask.getState()); + + ServiceLocator.getPhysicsService().getPhysics().update(); + entity.update(); + // check if the target is still there to shoot from below + assertTrue(droidCombatTask.isTargetVisible()); + + droidCombatTask.updateTowerState(); + // Shoot from below + verify(attackDown).handle(); + //shoot projectiles from below + verify(shootUp).handle(); + // switch back to aim from top + assertEquals(DroidCombatTask.STATE.UP, droidCombatTask.getState()); + } + + @Test + public void testUpdateTowerStateWithTargetNotInRange() { + Entity entity = createDroid(); + entity.setPosition(10, 10); + + Entity Target = createNPC(); + Target.setPosition(15, 10); + + EventListener0 idle = mock(EventListener0.class); + EventListener0 attackUp = mock(EventListener0.class); + entity.getEvents().addListener(DroidCombatTask.IDLE, idle); + entity.getEvents().addListener(DroidCombatTask.ATTACK_UP,attackUp); + //Jump to IDLE state + droidCombatTask.towerState = DroidCombatTask.STATE.IDLE; + + ServiceLocator.getPhysicsService().getPhysics().update(); + entity.update(); + // Target out of range + assertFalse(droidCombatTask.isTargetVisible()); + + droidCombatTask.updateTowerState(); + // Droid will remain in Idle and will not shoot + verify(idle).handle(); + verifyNoInteractions(attackUp); + assertEquals(DroidCombatTask.STATE.IDLE, droidCombatTask.getState()); + + } + + + Entity createDroid() { + AITaskComponent aiTaskComponent = new AITaskComponent().addTask(droidCombatTask); + Entity entity = new Entity().addComponent(aiTaskComponent) + .addComponent(new PhysicsComponent()) + .addComponent(new HitboxComponent()) + .addComponent(new ColliderComponent()) + .addComponent(new CombatStatsComponent(100,10)); + entity.create(); + return entity; + } + + Entity createNPC() { + Entity Target = new Entity().addComponent(new HitboxComponent().setLayer(PhysicsLayer.NPC)) + .addComponent(new ColliderComponent()) + .addComponent(new PhysicsComponent()); + Target.create(); + return Target; + } +} diff --git a/source/core/src/test/com/csse3200/game/components/tasks/TNTTowerCombatTaskTest.java b/source/core/src/test/com/csse3200/game/components/tasks/TNTTowerCombatTaskTest.java index 951185ab0..65a0e4724 100644 --- a/source/core/src/test/com/csse3200/game/components/tasks/TNTTowerCombatTaskTest.java +++ b/source/core/src/test/com/csse3200/game/components/tasks/TNTTowerCombatTaskTest.java @@ -2,35 +2,26 @@ import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; -import com.badlogic.gdx.math.Vector2; import com.csse3200.game.ai.tasks.AITaskComponent; import com.csse3200.game.entities.Entity; -import com.csse3200.game.entities.factories.TowerFactory; -import com.csse3200.game.events.EventHandler; +import com.csse3200.game.entities.EntityService; import com.csse3200.game.events.listeners.EventListener0; -import com.csse3200.game.physics.PhysicsEngine; 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.raycast.RaycastHit; import com.csse3200.game.services.GameTime; import com.csse3200.game.services.ServiceLocator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) public class TNTTowerCombatTaskTest { - - Entity entity; - TNTTowerCombatTask tntTowerCombatTask; @BeforeEach @@ -39,6 +30,7 @@ void setUp() { ServiceLocator.registerTimeSource(gameTime); ServiceLocator.registerPhysicsService(new PhysicsService()); + ServiceLocator.registerEntityService(new EntityService()); tntTowerCombatTask = new TNTTowerCombatTask(2,4); } @@ -57,7 +49,7 @@ public void testStartTriggersDefaultEvent() { } @Test - public void testUpdateTowerStateIdleMode() { + public void testUpdateTowerStateWithTargetInRange() { Entity entity = createTNT(); entity.setPosition(10,10); @@ -65,33 +57,67 @@ public void testUpdateTowerStateIdleMode() { Entity Target = createNPC(); Target.setPosition(12,10); - EventListener0 defaultStartListener = mock(EventListener0.class); + EventListener0 dig = mock(EventListener0.class); + EventListener0 explode = mock(EventListener0.class); + EventListener0 damage = mock(EventListener0.class); // still in idle assertEquals(TNTTowerCombatTask.STATE.IDLE, tntTowerCombatTask.getState()); - entity.getEvents().addListener(TNTTowerCombatTask.DIG, defaultStartListener); + entity.getEvents().addListener(TNTTowerCombatTask.DIG, dig); + entity.getEvents().addListener(TNTTowerCombatTask.EXPLOSION,explode); + entity.getEvents().addListener(TNTTowerCombatTask.DAMAGE,damage); ServiceLocator.getPhysicsService().getPhysics().update(); entity.update(); + // TNT saw the target assertTrue(tntTowerCombatTask.isTargetVisible()); tntTowerCombatTask.updateTowerState(); - - verify(defaultStartListener).handle(); - // ready to explode + // TNT just Dug into the ground + verify(dig).handle(); + // READY TO EXPLODE !!! assertEquals(TNTTowerCombatTask.STATE.EXPLODE, tntTowerCombatTask.getState()); + + tntTowerCombatTask.updateTowerState(); + + // BOOOOOOOOM !! + verify(explode).handle(); + // Apply Damage and Knock-back to Target + verify(damage).handle(); + + // Ready to dispose TNT + assertEquals(TNTTowerCombatTask.STATE.REMOVE, tntTowerCombatTask.getState()); + + tntTowerCombatTask.updateTowerState(); + // Set flag to dispose + assertTrue(tntTowerCombatTask.isReadyToDelete()); + } @Test - public void testGetPriority() { - // Arrange - tntTowerCombatTask.readToDelete = false; + public void testStayAtIdleWhenNoTargetInRange() { - // Act - int priority = tntTowerCombatTask.getPriority(); + Entity entity = createTNT(); + entity.setPosition(10,10); - // Assert + Entity Target = createNPC(); + Target.setPosition(15,10); + + EventListener0 defaultStartListener = mock(EventListener0.class); + // still in idle + assertEquals(TNTTowerCombatTask.STATE.IDLE, tntTowerCombatTask.getState()); + entity.getEvents().addListener(TNTTowerCombatTask.DIG, defaultStartListener); + + ServiceLocator.getPhysicsService().getPhysics().update(); + entity.update(); + // Target not in range + assertFalse(tntTowerCombatTask.isTargetVisible()); + + tntTowerCombatTask.updateTowerState(); + + verifyNoInteractions(defaultStartListener); + // still in idle + assertEquals(TNTTowerCombatTask.STATE.IDLE, tntTowerCombatTask.getState()); - assertEquals(2, priority); } Entity createTNT() {