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..864f5a3f3 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 @@ -4,9 +4,6 @@ import com.csse3200.game.ai.tasks.DefaultTask; import com.csse3200.game.ai.tasks.PriorityTask; import com.csse3200.game.components.CombatStatsComponent; -import com.csse3200.game.components.ProjectileEffects; -import com.csse3200.game.entities.Entity; -import com.csse3200.game.entities.factories.ProjectileFactory; import com.csse3200.game.physics.PhysicsEngine; import com.csse3200.game.physics.PhysicsLayer; import com.csse3200.game.physics.raycast.RaycastHit; @@ -24,13 +21,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 +42,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 +104,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 +113,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 +124,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; @@ -173,7 +163,15 @@ public void updateTowerState() { @Override public void stop() { super.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; } /** @@ -189,8 +187,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 c55a92450..bcb210846 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 @@ -4,8 +4,6 @@ import com.badlogic.gdx.math.Vector2; import com.csse3200.game.ai.tasks.DefaultTask; import com.csse3200.game.ai.tasks.PriorityTask; -import com.csse3200.game.entities.Entity; -import com.csse3200.game.entities.EntityService; import com.csse3200.game.physics.PhysicsEngine; import com.csse3200.game.physics.PhysicsLayer; import com.csse3200.game.physics.raycast.RaycastHit; @@ -22,10 +20,10 @@ public class TNTTowerCombatTask 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 DIG = "digStart"; - private static final String EXPLOSION = "explodeStart"; - private static final String DEFAULT = "defaultStart"; - private static final String DAMAGE = "TNTDamageStart"; + public static final String DIG = "digStart"; + public static final String EXPLOSION = "explodeStart"; + public static final String DEFAULT = "defaultStart"; + public static final String DAMAGE = "TNTDamageStart"; // class attributes @@ -37,9 +35,9 @@ public class TNTTowerCombatTask extends DefaultTask implements PriorityTask { private final GameTime timeSource; private long endTime; private final RaycastHit hit = new RaycastHit(); - private boolean readToDelete = false; + public boolean readToDelete = false; - private enum STATE { + public enum STATE { IDLE, EXPLODE, REMOVE } private STATE towerState = STATE.IDLE; @@ -136,36 +134,29 @@ public int getPriority() { } /** - * Fetches the active priority of the Task if a target is visible. - * @return (int) active priority if a target is visible, -1 otherwise + * Returns the current state of the tower. + * + * @return the current state of the tower. */ - private int getActivePriority() { - - return !isTargetVisible() ? 0 : priority; - } - - /** - * Fetches the inactive priority of the Task if a target is not visible. - * @return (int) -1 if a target is not visible, active priority otherwise - */ - private int getInactivePriority() { - - return isTargetVisible() ? priority : 0; + public STATE getState() { + return this.towerState; } /** * 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); } - private boolean isReadyToDelete() { + public boolean isReadyToDelete() { return readToDelete; } + + } diff --git a/source/core/src/main/com/csse3200/game/components/tower/DroidAnimationController.java b/source/core/src/main/com/csse3200/game/components/tower/DroidAnimationController.java index be123d483..d8307e0e4 100644 --- a/source/core/src/main/com/csse3200/game/components/tower/DroidAnimationController.java +++ b/source/core/src/main/com/csse3200/game/components/tower/DroidAnimationController.java @@ -1,7 +1,13 @@ package com.csse3200.game.components.tower; +import com.badlogic.gdx.math.Vector2; import com.csse3200.game.components.Component; +import com.csse3200.game.components.ProjectileEffects; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.factories.ProjectileFactory; +import com.csse3200.game.physics.PhysicsLayer; import com.csse3200.game.rendering.AnimationRenderComponent; +import com.csse3200.game.services.ServiceLocator; /** * This class listens to events relevant to DroidTower entity's state and plays the animation when one @@ -25,6 +31,8 @@ public void create() { entity.getEvents().addListener("attackUpStart",this::animateAttackUp); entity.getEvents().addListener("attackDownStart",this::animateAttackDown); entity.getEvents().addListener("deathStart",this::animateDeath); + entity.getEvents().addListener("ShootUp",this::shootUp); + entity.getEvents().addListener("ShootDown",this::shootDown); } @@ -83,4 +91,32 @@ void animateDeath() { */ void animateDefault() { animator.startAnimation("idle");} + + //TODO: For the time being, these items will be positioned here. Next, we should create a component that enables an entity to fire projectiles. + + /** + * Fires a projectile upwards from the entity's current position. + */ + void shootUp() { + Entity Projectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, new Vector2(100, + entity.getPosition().y), new Vector2(2,2), ProjectileEffects.SLOW, false); + Projectile.setScale(new Vector2(0.5f,0.5f)); + Projectile.setPosition((float) (entity.getPosition().x + 0.2), + (float) (entity.getPosition().y + 0.5)); + ServiceLocator.getEntityService().register(Projectile); + } + + /** + * Fires a projectile downwards from the entity's current position. + */ + void shootDown() { + Entity Projectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, new Vector2(100, + entity.getPosition().y), new Vector2(2,2), ProjectileEffects.SLOW, false); + Projectile.setScale(new Vector2(0.5f,0.5f)); + Projectile.setPosition((float) (entity.getPosition().x + 0.2), + (float) (entity.getPosition().y - 0.2)); + ServiceLocator.getEntityService().register(Projectile); + + } + } diff --git a/source/core/src/main/com/csse3200/game/components/tower/TNTDamageComponent.java b/source/core/src/main/com/csse3200/game/components/tower/TNTDamageComponent.java index c6acb3c28..ece920971 100644 --- a/source/core/src/main/com/csse3200/game/components/tower/TNTDamageComponent.java +++ b/source/core/src/main/com/csse3200/game/components/tower/TNTDamageComponent.java @@ -24,7 +24,6 @@ * Utilizes HitboxComponent and CombatStatsComponent for functionality. */ public class TNTDamageComponent extends Component { - private static final Logger logger = LoggerFactory.getLogger(TNTDamageComponent.class); private short targetLayer; private float knockbackForce = 0f; private float radius; @@ -84,12 +83,7 @@ private void applyTNTDamage() { // Check for null components and log specifics if (sourceHitbox == null || otherHitbox == null) { - if (sourceHitbox == null) { - logger.debug("Warning: Source Entity without HitboxComponent. Source Entity: " + entity); - } - if (otherHitbox == null) { - logger.debug("Warning: Other Entity without HitboxComponent. Other Entity: " + otherEntity); - } + continue; } 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 new file mode 100644 index 000000000..65a0e4724 --- /dev/null +++ b/source/core/src/test/com/csse3200/game/components/tasks/TNTTowerCombatTaskTest.java @@ -0,0 +1,144 @@ +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.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 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 TNTTowerCombatTaskTest { + + + TNTTowerCombatTask tntTowerCombatTask; + + @BeforeEach + void setUp() { + GameTime gameTime = mock(GameTime.class); + + ServiceLocator.registerTimeSource(gameTime); + ServiceLocator.registerPhysicsService(new PhysicsService()); + ServiceLocator.registerEntityService(new EntityService()); + + tntTowerCombatTask = new TNTTowerCombatTask(2,4); + } + + @Test + public void testStartTriggersDefaultEvent() { + + Entity entity = createTNT(); + + EventListener0 defaultStartListener = mock(EventListener0.class); + entity.getEvents().addListener(TNTTowerCombatTask.DEFAULT, defaultStartListener); + + tntTowerCombatTask.start(); + + verify(defaultStartListener).handle(); + } + + @Test + public void testUpdateTowerStateWithTargetInRange() { + + Entity entity = createTNT(); + entity.setPosition(10,10); + + Entity Target = createNPC(); + Target.setPosition(12,10); + + 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, 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(); + // 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 testStayAtIdleWhenNoTargetInRange() { + + Entity entity = createTNT(); + entity.setPosition(10,10); + + 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()); + + } + + Entity createTNT() { + AITaskComponent aiTaskComponent = new AITaskComponent().addTask(tntTowerCombatTask); + Entity entity = new Entity().addComponent(aiTaskComponent). + addComponent(new PhysicsComponent()) + .addComponent(new HitboxComponent()) + .addComponent(new ColliderComponent()); + 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; + } + + +}