diff --git a/source/core/assets/images/projectiles/burn_effect.atlas b/source/core/assets/images/projectiles/burn_effect.atlas new file mode 100644 index 000000000..140875f0f --- /dev/null +++ b/source/core/assets/images/projectiles/burn_effect.atlas @@ -0,0 +1,55 @@ + +burn_effect.png +size: 256, 64 +format: RGBA8888 +filter: Nearest, Nearest +repeat: none +projectile + rotate: false + xy: 78, 2 + size: 36, 31 + orig: 36, 31 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 2, 2 + size: 36, 31 + orig: 36, 31 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 154, 2 + size: 35, 31 + orig: 35, 31 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 40, 2 + size: 36, 31 + orig: 36, 31 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 116, 2 + size: 36, 31 + orig: 36, 31 + offset: 0, 0 + index: -1 +projectileFinal + rotate: false + xy: 191, 2 + size: 31, 31 + orig: 31, 31 + offset: 0, 0 + index: -1 +default + rotate: false + xy: 78, 2 + size: 36, 31 + orig: 36, 31 + offset: 0, 0 + index: -1 \ No newline at end of file diff --git a/source/core/assets/images/projectiles/burn_effect.png b/source/core/assets/images/projectiles/burn_effect.png new file mode 100644 index 000000000..18df3b6d5 Binary files /dev/null and b/source/core/assets/images/projectiles/burn_effect.png differ diff --git a/source/core/assets/images/projectiles/firework_anim.atlas b/source/core/assets/images/projectiles/firework_anim.atlas new file mode 100644 index 000000000..45f2d54c9 --- /dev/null +++ b/source/core/assets/images/projectiles/firework_anim.atlas @@ -0,0 +1,41 @@ + +firework_anim.png +size: 128, 32 +format: RGBA8888 +filter: Nearest, Nearest +repeat: none +projectile + rotate: false + xy: 23, 2 + size: 19, 16 + orig: 19, 16 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 65, 2 + size: 19, 16 + orig: 19, 16 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 44, 2 + size: 19, 16 + orig: 19, 16 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 2, 2 + size: 19, 16 + orig: 19, 16 + offset: 0, 0 + index: -1 +default + rotate: false + xy: 23, 2 + size: 19, 16 + orig: 19, 16 + offset: 0, 0 + index: -1 diff --git a/source/core/assets/images/projectiles/firework_anim.png b/source/core/assets/images/projectiles/firework_anim.png new file mode 100644 index 000000000..56a46b556 Binary files /dev/null and b/source/core/assets/images/projectiles/firework_anim.png differ diff --git a/source/core/assets/images/projectiles/oldstun_effect.atlas b/source/core/assets/images/projectiles/oldstun_effect.atlas new file mode 100644 index 000000000..a50132d0b --- /dev/null +++ b/source/core/assets/images/projectiles/oldstun_effect.atlas @@ -0,0 +1,41 @@ + +stun_effect.png +size: 256, 32 +format: RGBA8888 +filter: Nearest, Nearest +repeat: none +projectile + rotate: false + xy: 86, 3 + size: 41, 27 + orig: 41, 27 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 2, 2 + size: 40, 28 + orig: 40, 28 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 129, 2 + size: 40, 28 + orig: 40, 28 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 44, 2 + size: 40, 28 + orig: 40, 28 + offset: 0, 0 + index: -1 +default + rotate: false + xy: 86, 3 + size: 41, 27 + orig: 41, 27 + offset: 0, 0 + index: -1 \ No newline at end of file diff --git a/source/core/assets/images/projectiles/oldstun_effect.png b/source/core/assets/images/projectiles/oldstun_effect.png new file mode 100644 index 000000000..238eac60d Binary files /dev/null and b/source/core/assets/images/projectiles/oldstun_effect.png differ diff --git a/source/core/assets/images/projectiles/pierce_anim.atlas b/source/core/assets/images/projectiles/pierce_anim.atlas new file mode 100644 index 000000000..0dea23b97 --- /dev/null +++ b/source/core/assets/images/projectiles/pierce_anim.atlas @@ -0,0 +1,42 @@ + +pierce_anim.png +size: 256, 32 +format: RGBA8888 +filter: Nearest, Nearest +repeat: none +projectile + rotate: false + xy: 2, 2 + size: 35, 26 + orig: 35, 26 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 76, 2 + size: 35, 26 + orig: 35, 26 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 39, 2 + size: 35, 26 + orig: 35, 26 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 113, 2 + size: 35, 26 + orig: 35, 26 + offset: 0, 0 + index: -1 +default +projectile + rotate: false + xy: 2, 2 + size: 35, 26 + orig: 35, 26 + offset: 0, 0 + index: -1 \ No newline at end of file diff --git a/source/core/assets/images/projectiles/pierce_anim.png b/source/core/assets/images/projectiles/pierce_anim.png new file mode 100644 index 000000000..e9c349e6f Binary files /dev/null and b/source/core/assets/images/projectiles/pierce_anim.png differ diff --git a/source/core/assets/images/projectiles/stun_effect.atlas b/source/core/assets/images/projectiles/stun_effect.atlas new file mode 100644 index 000000000..6aa98a315 --- /dev/null +++ b/source/core/assets/images/projectiles/stun_effect.atlas @@ -0,0 +1,41 @@ + +stun_effect.png +size: 128, 32 +format: RGBA8888 +filter: Nearest, Nearest +repeat: none +projectile + rotate: false + xy: 2, 2 + size: 21, 19 + orig: 21, 19 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 48, 2 + size: 20, 19 + orig: 20, 19 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 25, 2 + size: 21, 19 + orig: 21, 19 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 70, 2 + size: 17, 19 + orig: 17, 19 + offset: 0, 0 + index: -1 +default + rotate: false + xy: 2, 2 + size: 21, 19 + orig: 21, 19 + offset: 0, 0 + index: -1 \ No newline at end of file diff --git a/source/core/assets/images/projectiles/stun_effect.png b/source/core/assets/images/projectiles/stun_effect.png new file mode 100644 index 000000000..cd862a426 Binary files /dev/null and b/source/core/assets/images/projectiles/stun_effect.png differ diff --git a/source/core/src/main/com/csse3200/game/ai/tasks/AITaskComponent.java b/source/core/src/main/com/csse3200/game/ai/tasks/AITaskComponent.java index 7b4dd37da..a0a91095b 100644 --- a/source/core/src/main/com/csse3200/game/ai/tasks/AITaskComponent.java +++ b/source/core/src/main/com/csse3200/game/ai/tasks/AITaskComponent.java @@ -18,8 +18,8 @@ public class AITaskComponent extends Component implements TaskRunner { private static final Logger logger = LoggerFactory.getLogger(AITaskComponent.class); private final List priorityTasks = new ArrayList<>(2); + private final List priorityTasksToBeRestored = new ArrayList<>(2); private PriorityTask currentTask; - /** * Add a priority task to the list of tasks. This task will be run only when it has the highest * priority, and can be stopped to run a higher priority task. @@ -59,6 +59,33 @@ public void dispose() { } } + /** + * Empties the priorityTasks List. Disposes all of the entity's tasks. + */ + public void disposeAll() { + currentTask = null; + for (int i = 0; i < priorityTasks.size(); i++) { + priorityTasksToBeRestored.add(priorityTasks.get(i)); + } + for (int i = 0; i < priorityTasks.size(); i++) { + priorityTasks.remove(i); + } + } + + /** + * Restores the priorityTasks List. Adds all of the entity's disposed tasks + * back into priorityTasks. + */ + public void restore() { + for (int i = 0; i < priorityTasksToBeRestored.size(); i++) { + priorityTasks.add(priorityTasksToBeRestored.get(i)); + } + for (int i = 0; i < priorityTasksToBeRestored.size(); i++) { + priorityTasksToBeRestored.remove(i); + } + this.update(); + } + private PriorityTask getHighestPriorityTask() { try { return Collections.max(priorityTasks, Comparator.comparingInt(PriorityTask::getPriority)); 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 390516eaa..0c3c3e46a 100644 --- a/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java +++ b/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java @@ -88,6 +88,11 @@ public class ForestGameArea extends GameArea { "images/projectiles/mobProjectile.png", "images/projectiles/engineer_projectile.png", "images/projectiles/mobKing_projectile.png", + "images/projectiles/snow_ball.png", + "images/projectiles/burn_effect.png", + "images/projectiles/stun_effect.png", + "images/projectiles/firework_anim.png", + "images/projectiles/pierce_anim.png", "images/projectiles/snow_ball.png" }; private static final String[] forestTextureAtlases = { @@ -110,7 +115,12 @@ public class ForestGameArea extends GameArea { "images/projectiles/mobProjectile.atlas", "images/projectiles/engineer_projectile.atlas", "images/projectiles/mobKing_projectile.atlas", - "images/projectiles/snow_ball.atlas" + "images/projectiles/snow_ball.atlas", + "images/projectiles/pierce_anim.atlas", + "images/projectiles/burn_effect.atlas", + "images/projectiles/firework_anim.atlas", + "images/projectiles/mobProjectile.atlas", + "images/projectiles/stun_effect.atlas" }; private static final String[] forestSounds = { "sounds/Impact4.ogg", @@ -166,6 +176,8 @@ public void create() { spawnRicochetFireball(new Vector2(2, 4), PhysicsLayer.NPC, towardsMobs, new Vector2(2f, 2f)); spawnSplitFireWorksFireBall(new Vector2(2, 5), PhysicsLayer.NPC, towardsMobs, new Vector2(2f, 2f), 12); spawnEffectProjectile(new Vector2(2, 6), PhysicsLayer.NPC, towardsMobs, new Vector2(2f, 2f), ProjectileEffects.SLOW, false); + // spawnProjectileTest(new Vector2(0, 8), PhysicsLayer.NPC, towardsMobs, new Vector2(2f, 2f)); + spawnXenoGrunts(); spawnScrap(); diff --git a/source/core/src/main/com/csse3200/game/components/EffectsComponent.java b/source/core/src/main/com/csse3200/game/components/EffectsComponent.java index 843d65288..76314fd87 100644 --- a/source/core/src/main/com/csse3200/game/components/EffectsComponent.java +++ b/source/core/src/main/com/csse3200/game/components/EffectsComponent.java @@ -1,14 +1,15 @@ package com.csse3200.game.components; +import java.util.ArrayList; + import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.physics.box2d.Fixture; +import com.csse3200.game.ai.tasks.AITaskComponent; +import com.csse3200.game.components.tower.TowerUpgraderComponent; import com.csse3200.game.entities.Entity; -import com.csse3200.game.entities.factories.ProjectileFactory; import com.csse3200.game.physics.BodyUserData; import com.csse3200.game.physics.PhysicsLayer; -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.services.ServiceLocator; @@ -29,6 +30,7 @@ public class EffectsComponent extends Component { private HitboxComponent hitboxComponent; private final short targetLayer; private Array burnEntities = new Array<>(); + private ArrayList stunnedEntities = new ArrayList<>(); /** * Constructor for the AoEComponent. @@ -50,7 +52,7 @@ public void create() { } private void onCollisionStart(Fixture me, Fixture other) { - // Nothing to do on collision start + // Nothing to do in collision start } private void onCollisionEnd(Fixture me, Fixture other) { @@ -72,28 +74,19 @@ private void onCollisionEnd(Fixture me, Fixture other) { return; } + System.out.println("target layer: " + otherEntity.getLayer()); + // Apply effect - switch (effect) { - case FIREBALL -> { - if (aoe) { - applyAoeEffect(ProjectileEffects.FIREBALL); - } - } - case BURN -> { - if (aoe) { - applyAoeEffect(ProjectileEffects.BURN); - } else { - applySingleEffect(ProjectileEffects.BURN, otherCombatStats, otherEntity); - } + if (effect == ProjectileEffects.FIREBALL) { + if (aoe) { + applyAoeEffect(ProjectileEffects.FIREBALL); } - case SLOW -> { - if (aoe) { - applyAoeEffect(ProjectileEffects.SLOW); - } else { - applySingleEffect(ProjectileEffects.SLOW, otherCombatStats, otherEntity); - } + } else { + if (aoe) { + applyAoeEffect(effect); + } else { + applySingleEffect(effect, otherCombatStats, otherEntity); } - case STUN -> {} } } @@ -117,7 +110,7 @@ public void applySingleEffect(ProjectileEffects effect, CombatStatsComponent tar burnEffect(targetCombatStats, hostCombatStats); } case SLOW -> {slowEffect(targetEntity);} - case STUN -> {} + case STUN -> {stunEffect(targetEntity);} } } /** @@ -152,7 +145,9 @@ public void applyAoeEffect(ProjectileEffects effect) { case FIREBALL -> {fireballEffect(targetCombatStats, hostCombatStats);} case BURN -> {burnEffect(targetCombatStats, hostCombatStats);} case SLOW -> {slowEffect(targetEntity);} - case STUN -> {} + case STUN -> { + stunEffect(targetEntity); + } } } else { return; @@ -180,7 +175,6 @@ private void burnEffect(CombatStatsComponent target, CombatStatsComponent host) return; } burnEntities.add(target); - // Create a timer task to apply the effect repeatedly int numberOfTicks = 5; long delay = 1; @@ -218,7 +212,7 @@ private void slowEffect(Entity targetEntity) { if (PhysicsLayer.contains(PhysicsLayer.HUMANS, targetEntity.getComponent(HitboxComponent.class).getLayer())) { // towers towerFlag = true; - //targetEntity.getEvents().trigger("upgradeTower", TowerUpgraderComponent.UPGRADE.FIRERATE, -30); + targetEntity.getEvents().trigger("upgradeTower", TowerUpgraderComponent.UPGRADE.FIRERATE, -30); } else if (PhysicsLayer.contains(PhysicsLayer.NPC, targetEntity.getComponent(HitboxComponent.class).getLayer())) { // mobs mobFlag = true; @@ -245,11 +239,47 @@ private void slowEffect(Entity targetEntity) { @Override public void run() { if (finalTowerFlag) { - //targetEntity.getEvents().trigger("upgradeTower", TowerUpgraderComponent.UPGRADE.FIRERATE, 30); + targetEntity.getEvents().trigger("upgradeTower", TowerUpgraderComponent.UPGRADE.FIRERATE, 30); } else if (finalMobFlag) { finalTargetPhysics.setSpeed(new Vector2(finalXSpeed, finalYSpeed)); } } }, 5); // 5 seconds delay } + + /** + * Applies stun effect to a taget entity. + * @param targetEntity Entity for stun effect to be applied to. + */ + private void stunEffect(Entity targetEntity) { + CombatStatsComponent hostCombatStats = targetEntity.getComponent(CombatStatsComponent.class); + AITaskComponent taskComponent = targetEntity.getComponent(AITaskComponent.class); + + if (hostCombatStats == null || taskComponent == null) { + return; + } + + hostCombatStats.setBaseAttack(0); + + if (stunnedEntities.contains(targetEntity)) { + return; + } + + taskComponent.disposeAll(); + stunnedEntities.add(targetEntity); + + new java.util.Timer().schedule( + new java.util.TimerTask() { + @Override + public void run() { + taskComponent.restore(); + for (int i = 0; i < stunnedEntities.size(); i++) { + if (stunnedEntities.get(i).equals(targetEntity)) { + stunnedEntities.remove(stunnedEntities.get(i)); + } + } + this.cancel(); + } + }, 5000); + } } diff --git a/source/core/src/main/com/csse3200/game/components/SplitFireworksComponent.java b/source/core/src/main/com/csse3200/game/components/SplitFireworksComponent.java index a48f6595a..6527271e8 100644 --- a/source/core/src/main/com/csse3200/game/components/SplitFireworksComponent.java +++ b/source/core/src/main/com/csse3200/game/components/SplitFireworksComponent.java @@ -18,16 +18,17 @@ public class SplitFireworksComponent extends Component { private HitboxComponent hitboxComponent; private int amount; private static int TOTAL_RANGE = 450; + private static double SPAWN_OFFSET_X = 1.75; /** - * Initialises a component that splits the projectile into multiple fireballs + * Initialises a component that splits the projectile into multiple fireballs * upon collision on a specified target layer. - * The spawned projectiles will be spawned just before original projectile + * The spawned projectiles will be spawned just before original projectile * and spread out in multiple direction set by a certain range. * Assumes amount of split projectiles is greater or equal than 2. * * @param targetLayer Target layer upon collision. - * @param amount Amount of projectiles that is split after collision event. + * @param amount Amount of projectiles that is split after collision event. */ public SplitFireworksComponent(short targetLayer, int amount) { this.targetLayer = targetLayer; @@ -52,13 +53,13 @@ private void onCollisionEnd(Fixture me, Fixture other) { int newDirection = (i * TOTAL_RANGE) / (amount - 1); // Boundaries - float newXPosition = (float) (projectile.getPosition().x + 1.75); + float newXPosition = (float) (projectile.getPosition().x + SPAWN_OFFSET_X); if (newXPosition >= 18 || newXPosition <= 1) return; // * RIGHT NOW TARGET IS NPC, SUBJECT TO CHANGE // Speed is a bit faster than normal but can change. - Entity newProjectile = ProjectileFactory.createFireBall(PhysicsLayer.NPC, + Entity newProjectile = ProjectileFactory.createFireworks(PhysicsLayer.NPC, new Vector2(100, projectile.getPosition().y + (newDirection - (TOTAL_RANGE/2))), new Vector2(3f, 3f)); newProjectile.setPosition(newXPosition, (float) projectile.getPosition().y); diff --git a/source/core/src/main/com/csse3200/game/components/TouchAttackComponent.java b/source/core/src/main/com/csse3200/game/components/TouchAttackComponent.java index 3a203d3d0..30cf1b69d 100644 --- a/source/core/src/main/com/csse3200/game/components/TouchAttackComponent.java +++ b/source/core/src/main/com/csse3200/game/components/TouchAttackComponent.java @@ -60,7 +60,7 @@ public TouchAttackComponent(short targetLayer, float knockback, boolean disposeO @Override public void create() { entity.getEvents().addListener("collisionStart", this::onCollisionStart); - // entity.getEvents().addListener("collisionEnd", this::onCollisionEnd); + entity.getEvents().addListener("collisionEnd", this::onCollisionEnd); combatStats = entity.getComponent(CombatStatsComponent.class); hitboxComponent = entity.getComponent(HitboxComponent.class); } @@ -110,6 +110,25 @@ public void setDisposeOnHit(boolean disposeOnHit) { public void setKnockBack(float knockback) { this.knockbackForce = knockback; } + + private void onCollisionEnd(Fixture me, Fixture other) { + // Nothing to do on collision end + if (hitboxComponent.getFixture() != me) { + // Not triggered by hitbox, ignore + return; + } + + if (!PhysicsLayer.contains(targetLayer, other.getFilterData().categoryBits)) { + // Doesn't match our target layer, ignore + return; + } + + if (disposeOnHit) { + Entity projectile = ((BodyUserData) me.getBody().getUserData()).entity; + projectile.setFlagForDelete(true); + } + } + public Weapon chooseWeapon(Fixture other) { Entity target = ((BodyUserData) other.getBody().getUserData()).entity; Weapon weapon = null; @@ -118,12 +137,5 @@ public Weapon chooseWeapon(Fixture other) { } return weapon; } - - private void onCollisionEnd(Fixture me, Fixture other) { - // Nothing to do on collision end - } - // private void onCollisionEnd(Fixture me, Fixture other) { - // // Nothing to do on collision end - // } } diff --git a/source/core/src/main/com/csse3200/game/components/npc/XenoAnimationController.java b/source/core/src/main/com/csse3200/game/components/npc/XenoAnimationController.java index 270f5afa8..b25b91e00 100644 --- a/source/core/src/main/com/csse3200/game/components/npc/XenoAnimationController.java +++ b/source/core/src/main/com/csse3200/game/components/npc/XenoAnimationController.java @@ -1,8 +1,11 @@ package com.csse3200.game.components.npc; -import com.badlogic.gdx.graphics.g2d.Animation; + +import com.badlogic.gdx.audio.Sound; + import com.csse3200.game.components.Component; import com.csse3200.game.rendering.AnimationRenderComponent; +import com.csse3200.game.services.ServiceLocator; import java.util.Objects; @@ -11,6 +14,10 @@ * of the events is triggered. */ public class XenoAnimationController extends Component { + // // For on collision sounds later + // private static final String COLLISION_SFX = "sounds/projectiles/on_collision.mp3"; + // Sound onCollisionSound = ServiceLocator.getResourceService().getAsset( + // COLLISION_SFX, Sound.class); AnimationRenderComponent animator; @Override diff --git a/source/core/src/main/com/csse3200/game/components/projectile/BurnEffectProjectileAnimationController.java b/source/core/src/main/com/csse3200/game/components/projectile/BurnEffectProjectileAnimationController.java new file mode 100644 index 000000000..847f77027 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/projectile/BurnEffectProjectileAnimationController.java @@ -0,0 +1,33 @@ +package com.csse3200.game.components.projectile; + +import com.csse3200.game.components.Component; +import com.csse3200.game.rendering.AnimationRenderComponent; + +public class BurnEffectProjectileAnimationController extends Component { + /** Event name constants */ + private static final String START = "startProjectile"; + private static final String FINAL = "startProjectileFinal"; + + /** Animation name constants */ + private static final String START_ANIM = "projectile"; + private static final String FINAL_ANIM = "projectileFinal"; + AnimationRenderComponent animator; + + + @Override + public void create() { + super.create(); + animator = this.entity.getComponent(AnimationRenderComponent.class); + entity.getEvents().addListener(START, this::animateStart); + entity.getEvents().addListener(FINAL, this::animateFinal); + + } + + void animateStart() { + animator.startAnimation(START_ANIM); + } + + void animateFinal() { + animator.startAnimation(FINAL_ANIM); + } +} diff --git a/source/core/src/main/com/csse3200/game/components/projectile/FireworkAnimationController.java b/source/core/src/main/com/csse3200/game/components/projectile/FireworkAnimationController.java new file mode 100644 index 000000000..cc2239b18 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/projectile/FireworkAnimationController.java @@ -0,0 +1,26 @@ +package com.csse3200.game.components.projectile; + +import com.csse3200.game.components.Component; +import com.csse3200.game.rendering.AnimationRenderComponent; + +public class FireworkAnimationController extends Component { + /** Event name constants */ + private static final String START = "startProjectile"; + + /** Animation name constants */ + private static final String START_ANIM = "projectile"; + AnimationRenderComponent animator; + + + @Override + public void create() { + super.create(); + animator = this.entity.getComponent(AnimationRenderComponent.class); + entity.getEvents().addListener(START, this::animateStart); + + } + + void animateStart() { + animator.startAnimation(START_ANIM); + } +} diff --git a/source/core/src/main/com/csse3200/game/components/projectile/PierceProjectileAnimationController.java b/source/core/src/main/com/csse3200/game/components/projectile/PierceProjectileAnimationController.java new file mode 100644 index 000000000..a5801aa80 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/projectile/PierceProjectileAnimationController.java @@ -0,0 +1,25 @@ +package com.csse3200.game.components.projectile; + +import com.csse3200.game.components.Component; +import com.csse3200.game.rendering.AnimationRenderComponent; + +public class PierceProjectileAnimationController extends Component { + /** Event name constants */ + private static final String START = "startProjectile"; + /** Animation name constants */ + private static final String START_ANIM = "projectile"; + AnimationRenderComponent animator; + + + @Override + public void create() { + super.create(); + animator = this.entity.getComponent(AnimationRenderComponent.class); + entity.getEvents().addListener(START, this::animateStart); + + } + + void animateStart() { + animator.startAnimation(START_ANIM); + } +} diff --git a/source/core/src/main/com/csse3200/game/components/projectile/ProjectileAnimationController.java b/source/core/src/main/com/csse3200/game/components/projectile/ProjectileAnimationController.java index d39e91f46..9610554c9 100644 --- a/source/core/src/main/com/csse3200/game/components/projectile/ProjectileAnimationController.java +++ b/source/core/src/main/com/csse3200/game/components/projectile/ProjectileAnimationController.java @@ -13,6 +13,7 @@ public class ProjectileAnimationController extends Component{ private static final String FINAL_ANIM = "projectileFinal"; AnimationRenderComponent animator; + @Override public void create() { super.create(); diff --git a/source/core/src/main/com/csse3200/game/components/projectile/StunEffectProjectileAnimationController.java b/source/core/src/main/com/csse3200/game/components/projectile/StunEffectProjectileAnimationController.java new file mode 100644 index 000000000..94899ff17 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/projectile/StunEffectProjectileAnimationController.java @@ -0,0 +1,27 @@ +package com.csse3200.game.components.projectile; + +import com.csse3200.game.components.Component; +import com.csse3200.game.rendering.AnimationRenderComponent; + +public class StunEffectProjectileAnimationController extends Component { + /** Event name constants */ + private static final String START = "startProjectile"; + + /** Animation name constants */ + private static final String START_ANIM = "projectile"; + AnimationRenderComponent animator; + + + @Override + public void create() { + super.create(); + animator = this.entity.getComponent(AnimationRenderComponent.class); + entity.getEvents().addListener(START, this::animateStart); + + } + + void animateStart() { + animator.startAnimation(START_ANIM); + } + +} diff --git a/source/core/src/main/com/csse3200/game/components/tasks/FireTowerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/FireTowerCombatTask.java index 3ac568f1b..c70919487 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/FireTowerCombatTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/FireTowerCombatTask.java @@ -3,6 +3,7 @@ import com.badlogic.gdx.math.Vector2; import com.csse3200.game.ai.tasks.DefaultTask; import com.csse3200.game.ai.tasks.PriorityTask; +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; @@ -102,8 +103,8 @@ public void updateTowerState() { towerState = STATE.IDLE; } else { owner.getEntity().getEvents().trigger(ATTACK); - Entity newProjectile = ProjectileFactory.createFireBall(PhysicsLayer.NPC, - new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f, 2f)); + Entity newProjectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, + new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f, 2f), ProjectileEffects.BURN, false); newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.25), (float) (owner.getEntity().getPosition().y + 0.25)); ServiceLocator.getEntityService().register(newProjectile); diff --git a/source/core/src/main/com/csse3200/game/components/tasks/RangeBossMovementTask.java b/source/core/src/main/com/csse3200/game/components/tasks/RangeBossMovementTask.java index be0e655c6..81e0399d1 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/RangeBossMovementTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/RangeBossMovementTask.java @@ -79,7 +79,7 @@ public void update() { switchMobKingBallState(); // newProjectile.scaleHeight(-1f); newProjectile.setScale(-1.3f, 0.82f); - newProjectile.setPosition((float) (currentPos.x), (float) (currentPos.y+0.75f)); + newProjectile.setPosition((float) (currentPos.x), (float) (currentPos.y + 0.55f)); ServiceLocator.getEntityService().register(newProjectile); startWaiting(); } else { diff --git a/source/core/src/main/com/csse3200/game/components/tasks/StunTowerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/StunTowerCombatTask.java index 2919d49bc..2340ac3e6 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/StunTowerCombatTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/StunTowerCombatTask.java @@ -3,6 +3,7 @@ import com.badlogic.gdx.math.Vector2; import com.csse3200.game.ai.tasks.DefaultTask; import com.csse3200.game.ai.tasks.PriorityTask; +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; @@ -95,10 +96,11 @@ public void updateTowerState() { towerState = STATE.IDLE; } else { owner.getEntity().getEvents().trigger(ATTACK); - Entity newProjectile = ProjectileFactory.createFireBall(PhysicsLayer.NPC, - new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f, 2f)); + Entity newProjectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, new Vector2(100, + owner.getEntity().getPosition().y), new Vector2(2,2), ProjectileEffects.STUN, false); + newProjectile.setScale(-0.4f, -0.4f); newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.25), - (float) (owner.getEntity().getPosition().y + 0.25)); + (float) (owner.getEntity().getPosition().y + 0.85)); ServiceLocator.getEntityService().register(newProjectile); } } diff --git a/source/core/src/main/com/csse3200/game/components/tasks/TowerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/TowerCombatTask.java index a84528c42..f986f9667 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/TowerCombatTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/TowerCombatTask.java @@ -138,7 +138,7 @@ public void updateTowerState() { Entity newProjectile = ProjectileFactory.createFireBall(PhysicsLayer.NPC, new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f,2f)); newProjectile.setScale(1.1f, 0.8f); - newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.75), (float) (owner.getEntity().getPosition().y + 0.5)); + newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.5), (float) (owner.getEntity().getPosition().y + 0.5)); ServiceLocator.getEntityService().register(newProjectile); // * TEMPRORARYYYYYYYY PLS DON'T DELETE THIS diff --git a/source/core/src/main/com/csse3200/game/entities/factories/ProjectileFactory.java b/source/core/src/main/com/csse3200/game/entities/factories/ProjectileFactory.java index 152685fdb..578b1abf4 100644 --- a/source/core/src/main/com/csse3200/game/entities/factories/ProjectileFactory.java +++ b/source/core/src/main/com/csse3200/game/entities/factories/ProjectileFactory.java @@ -7,7 +7,7 @@ import com.csse3200.game.components.TouchAttackComponent; import com.csse3200.game.components.RicochetComponent; import com.csse3200.game.components.SplitFireworksComponent; -import com.csse3200.game.components.projectile.MobKingProjectAnimController; +import com.csse3200.game.components.projectile.*; import com.csse3200.game.components.tasks.TrajectTask; import com.csse3200.game.ai.tasks.AITaskComponent; import com.csse3200.game.components.CombatStatsComponent; @@ -25,76 +25,74 @@ import com.csse3200.game.physics.components.PhysicsComponent; import com.csse3200.game.physics.components.PhysicsMovementComponent; import com.badlogic.gdx.math.Vector2; -import com.csse3200.game.components.projectile.EngineerBulletsAnimationController; -import com.csse3200.game.components.projectile.MobProjectileAnimationController; -import com.csse3200.game.components.projectile.ProjectileAnimationController; -import com.csse3200.game.components.projectile.SnowBallProjectileAnimationController; /** * Responsible for creating projectiles within the game. */ public class ProjectileFactory { - /** Animation constants */ + /** + * Animation constants + */ private static final String BASE_PROJECTILE_ATLAS = "images/projectiles/basic_projectile.atlas"; private static final String START_ANIM = "projectile"; private static final String FINAL_ANIM = "projectileFinal"; private static final float START_SPEED = 0.1f; private static final float FINAL_SPEED = 0.1f; - private static final NPCConfigs configs = - FileLoader.readClass(NPCConfigs.class, "configs/NPCs.json"); + private static final NPCConfigs configs = + FileLoader.readClass(NPCConfigs.class, "configs/NPCs.json"); /** * Creates a single-targeting projectile with specified effect * * @param targetLayer The enemy layer that the projectile collides with. * @param destination The destination the projectile heads towards. - * @param speed The speed of the projectile. - * @param effect Specified effect from the ProjectileEffects enums + * @param speed The speed of the projectile. + * @param effect Specified effect from the ProjectileEffects enums * @return Returns a new single-target projectile entity */ public static Entity createEffectProjectile(short targetLayer, Vector2 destination, Vector2 speed, ProjectileEffects effect, boolean aoe) { Entity projectile = createBaseProjectile(targetLayer, destination, speed); - switch(effect) { + switch (effect) { case FIREBALL -> { projectile.addComponent(new EffectsComponent(targetLayer, 3, ProjectileEffects.FIREBALL, aoe)); AnimationRenderComponent animator = - new AnimationRenderComponent( - ServiceLocator.getResourceService() - .getAsset(BASE_PROJECTILE_ATLAS, TextureAtlas.class)); + new AnimationRenderComponent( + ServiceLocator.getResourceService() + .getAsset(BASE_PROJECTILE_ATLAS, TextureAtlas.class)); animator.addAnimation(START_ANIM, START_SPEED, Animation.PlayMode.NORMAL); animator.addAnimation(FINAL_ANIM, FINAL_SPEED, Animation.PlayMode.NORMAL); - projectile - .addComponent(animator) - .addComponent(new ProjectileAnimationController()); + projectile + .addComponent(animator) + .addComponent(new ProjectileAnimationController()); } case BURN -> { projectile.addComponent(new EffectsComponent(targetLayer, 3, ProjectileEffects.BURN, aoe)); AnimationRenderComponent animator = - new AnimationRenderComponent( - ServiceLocator.getResourceService() - .getAsset(BASE_PROJECTILE_ATLAS, TextureAtlas.class)); + new AnimationRenderComponent( + ServiceLocator.getResourceService() + .getAsset("images/projectiles/burn_effect.atlas", TextureAtlas.class)); animator.addAnimation(START_ANIM, START_SPEED, Animation.PlayMode.NORMAL); animator.addAnimation(FINAL_ANIM, FINAL_SPEED, Animation.PlayMode.NORMAL); - projectile - .addComponent(animator) - .addComponent(new ProjectileAnimationController()); + projectile + .addComponent(animator) + .addComponent(new BurnEffectProjectileAnimationController()); } case SLOW -> { projectile.addComponent(new EffectsComponent(targetLayer, 3, ProjectileEffects.SLOW, aoe)); AnimationRenderComponent animator = - new AnimationRenderComponent( - ServiceLocator.getResourceService() - .getAsset("images/projectiles/snow_ball.atlas", TextureAtlas.class)); + new AnimationRenderComponent( + ServiceLocator.getResourceService() + .getAsset("images/projectiles/snow_ball.atlas", TextureAtlas.class)); animator.addAnimation(START_ANIM, START_SPEED, Animation.PlayMode.NORMAL); animator.addAnimation(FINAL_ANIM, FINAL_SPEED, Animation.PlayMode.NORMAL); - projectile - .addComponent(animator) - .addComponent(new SnowBallProjectileAnimationController()); + projectile + .addComponent(animator) + .addComponent(new SnowBallProjectileAnimationController()); // * TEMPORARY // .addComponent(new DeleteOnMapEdgeComponent()); // .addComponent(new SelfDestructOnHitComponent(PhysicsLayer.OBSTACLE)); @@ -103,9 +101,18 @@ public static Entity createEffectProjectile(short targetLayer, Vector2 destinati } case STUN -> { projectile.addComponent(new EffectsComponent(targetLayer, 3, ProjectileEffects.STUN, aoe)); + AnimationRenderComponent animator = + new AnimationRenderComponent( + ServiceLocator.getResourceService() + .getAsset("images/projectiles/stun_effect.atlas", TextureAtlas.class)); + animator.addAnimation(START_ANIM, 0.1f, Animation.PlayMode.LOOP); + + projectile + .addComponent(animator) + .addComponent(new StunEffectProjectileAnimationController()); } } - return projectile; + return projectile; } /** @@ -113,7 +120,7 @@ public static Entity createEffectProjectile(short targetLayer, Vector2 destinati * Pierce fireball is basically a fireball that does damage but won't self destruct on hit. */ public static Entity createPierceFireBall(short targetLayer, Vector2 destination, Vector2 speed) { - Entity fireBall = createFireBall(targetLayer, destination, speed); + Entity fireBall = createPierceBallAnim(targetLayer, destination, speed); fireBall.getComponent(TouchAttackComponent.class).setDisposeOnHit(false); fireBall.getComponent(TouchAttackComponent.class).setKnockBack(0f); @@ -127,8 +134,8 @@ public static Entity createPierceFireBall(short targetLayer, Vector2 destination public static Entity createRicochetFireball(short targetLayer, Vector2 destination, Vector2 speed, int bounceCount) { Entity fireBall = createFireBall(targetLayer, destination, speed); fireBall - .addComponent(new RicochetComponent(targetLayer, bounceCount)); - + .addComponent(new RicochetComponent(targetLayer, bounceCount)); + setColliderSize(fireBall, (float) 0.1, (float) 0.1); return fireBall; @@ -137,17 +144,17 @@ public static Entity createRicochetFireball(short targetLayer, Vector2 destinati public static Entity createSplitFireWorksFireball(short targetLayer, Vector2 destination, Vector2 speed, int amount) { Entity fireBall = createFireBall(targetLayer, destination, speed); fireBall - .addComponent(new SplitFireworksComponent(targetLayer, amount)); - + .addComponent(new SplitFireworksComponent(targetLayer, amount)); + return fireBall; } /** * Creates a fireball Entity. - * + * * @param targetLayer The enemy layer that the projectile collides with. * @param destination The destination the projectile heads towards. - * @param speed The speed of the projectile. + * @param speed The speed of the projectile. * @return Returns a new fireball projectile entity. */ public static Entity createFireBall(short targetLayer, Vector2 destination, Vector2 speed) { @@ -161,14 +168,54 @@ public static Entity createFireBall(short targetLayer, Vector2 destination, Vect animator.addAnimation(FINAL_ANIM, FINAL_SPEED, Animation.PlayMode.NORMAL); projectile - .addComponent(animator) - .addComponent(new ProjectileAnimationController()); - // * TEMPORARY - // .addComponent(new DeleteOnMapEdgeComponent()); - // .addComponent(new SelfDestructOnHitComponent(PhysicsLayer.OBSTACLE)); + .addComponent(animator) + .addComponent(new ProjectileAnimationController()); + // * TEMPORARY + // .addComponent(new DeleteOnMapEdgeComponent()); + // .addComponent(new SelfDestructOnHitComponent(PhysicsLayer.OBSTACLE)); + + return projectile; + } + + /** + * Creates new animation and fireballs for SplitFireworkComponent. + * + * @param targetLayer The enemy layer that the projectile collides with. + * @param destination The destination the projectile heads towards. + * @param speed The speed of the projectile. + * @return Returns a new fireball projectile entity. + */ + public static Entity createFireworks(short targetLayer, Vector2 destination, Vector2 speed) { + Entity projectile = createBaseProjectile(targetLayer, destination, speed); + + AnimationRenderComponent animator = + new AnimationRenderComponent( + ServiceLocator.getResourceService() + .getAsset("images/projectiles/firework_anim.atlas", TextureAtlas.class)); + animator.addAnimation(START_ANIM, 0.2f, Animation.PlayMode.LOOP); + projectile + .addComponent(animator) + .addComponent(new FireworkAnimationController()); + + return projectile; + } + + public static Entity createPierceBallAnim(short targetLayer, Vector2 destination, Vector2 speed) { + Entity projectile = createBaseProjectile(targetLayer, destination, speed); + + AnimationRenderComponent animator = + new AnimationRenderComponent( + ServiceLocator.getResourceService() + .getAsset("images/projectiles/pierce_anim.atlas", TextureAtlas.class)); + animator.addAnimation(START_ANIM, 0.05f, Animation.PlayMode.LOOP); + projectile + .addComponent(animator) + .addComponent(new PierceProjectileAnimationController()); return projectile; } + + /** * Creates a engineer bullet * diff --git a/source/core/src/test/com/csse3200/game/components/RicochetComponentTest.java b/source/core/src/test/com/csse3200/game/components/RicochetComponentTest.java new file mode 100644 index 000000000..c92371344 --- /dev/null +++ b/source/core/src/test/com/csse3200/game/components/RicochetComponentTest.java @@ -0,0 +1,197 @@ +package com.csse3200.game.components; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.BeforeEach; + +import com.badlogic.gdx.math.Vector2; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.EntityService; +import com.csse3200.game.entities.factories.ProjectileFactory; +import com.csse3200.game.extensions.GameExtension; +import com.csse3200.game.physics.PhysicsLayer; +import com.csse3200.game.physics.PhysicsService; +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.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; + +@ExtendWith(GameExtension.class) +public class RicochetComponentTest { + Entity projectile; + Entity mob; + + private final String[] atlas = { + "images/projectiles/mobProjectile.atlas", + "images/projectiles/basic_projectile.atlas", + "images/projectiles/mobKing_projectile.atlas", + "images/projectiles/engineer_projectile.atlas" + }; + + @BeforeEach + public void setUp() { + GameTime gameTime = mock(GameTime.class); + when(gameTime.getDeltaTime()).thenReturn(0.02f); + ServiceLocator.registerTimeSource(gameTime); + ServiceLocator.registerPhysicsService(new PhysicsService()); + RenderService render = new RenderService(); + render.setDebug(mock(DebugRenderer.class)); + ServiceLocator.registerRenderService(render); + ResourceService resourceService = new ResourceService(); + ServiceLocator.registerResourceService(resourceService); + resourceService.loadTextureAtlases(atlas); + resourceService.loadAll(); + ServiceLocator.registerEntityService(new EntityService()); + + // For the time being, NPC is treated as an enemy. + projectile = createProjectile(PhysicsLayer.NPC, 0); + mob = createMobTarget(PhysicsLayer.NPC); + ServiceLocator.getEntityService().register(projectile); + ServiceLocator.getEntityService().register(mob); + } + + @Test + public void shouldNotBeNull() { + assertNotNull(projectile, "Ricochet projectile does not exist"); + } + + @Test + public void shouldHaveRicochetComponent() { + assertNotNull(projectile.getComponent(RicochetComponent.class), + "Projectile does not contain RicochetComponent"); + } + + @Test + public void shouldDisposeAferCollision() { + int currentEntities = ServiceLocator.getEntityService().getEntities().size; + + triggerCollisionEnd(projectile, mob); + + assertTrue("projectile entity flag should be true after collision", + projectile.getFlagForDelete()); + + ServiceLocator.getPhysicsService().getPhysics().update(); + ServiceLocator.getEntityService().update(); + + assertEquals("Projectile should be deleted after collision upon update", currentEntities - 1, + ServiceLocator.getEntityService().getEntities().size); + } + + // @Ignore + @Test + public void shouldSpawnAnotherProjWithinMapBounds() { + projectile.setPosition(3, 3); + int currentEntities = ServiceLocator.getEntityService().getEntities().size; + + triggerCollisionEnd(projectile, mob); + + ServiceLocator.getPhysicsService().getPhysics().update(); + ServiceLocator.getEntityService().update(); + + assertEquals("Should spawn another ricochet projectile within map bounds", currentEntities, + ServiceLocator.getEntityService().getEntities().size); + } + + @Test + public void shouldNotSpawnAnotherProjOutOfMapBounds() { + projectile.setPosition(-1, -1); + int currentEntities = ServiceLocator.getEntityService().getEntities().size; + + triggerCollisionEnd(projectile, mob); + + ServiceLocator.getPhysicsService().getPhysics().update(); + ServiceLocator.getEntityService().update(); + + assertNotEquals(currentEntities, + ServiceLocator.getEntityService().getEntities().size, + "Should not have spawned another projectile upon collision"); + } + + @Test + public void testWithinRangeSpawnedProjectile() { + projectile.setPosition(3, 3); + mob.setPosition(3, 3); + + triggerCollisionEnd(projectile, mob); + + ServiceLocator.getPhysicsService().getPhysics().update(); + ServiceLocator.getEntityService().update(); + + // For the time being, 2f seems to be the justifiable range + // for the new projectile to be spawned. + assertEquals("Projectile should be spawned within the range provided.", 1, + ServiceLocator.getEntityService().getNearbyEntities(mob, 2f).size); + } + + @Test + public void testNotWithinRangeShouldNotSpawnProjectile() { + projectile.setPosition(3, 3); + mob.setPosition(3, 3); + triggerCollisionEnd(projectile, mob); + + ServiceLocator.getPhysicsService().getPhysics().update(); + ServiceLocator.getEntityService().update(); + + assertEquals("Projectile should not be spawned too close to the original (now disposed) projectile and mob", 0, + ServiceLocator.getEntityService().getNearbyEntities(mob, 0.5f).size); + } + + @Test + public void shouldNotSpawnAnotherProjWithMaxBounceCount() { + Entity newProjectile = createProjectile(PhysicsLayer.NPC, 3); + ServiceLocator.getEntityService().register(newProjectile); + int currentEntities = ServiceLocator.getEntityService().getEntities().size; + + newProjectile.setPosition(3, 3); + mob.setPosition(3, 3); + + triggerCollisionEnd(newProjectile, mob); + + ServiceLocator.getPhysicsService().getPhysics().update(); + ServiceLocator.getEntityService().update(); + + assertNotEquals(currentEntities, + ServiceLocator.getEntityService().getEntities().size, + "Should not have spawned another projectile upon collision with a max bounce count"); + } + + Entity createProjectile(short targetLayer, int bounceCount) { + Entity projectile = ProjectileFactory.createRicochetFireball(targetLayer, new Vector2(0.1f, 0.1f), + new Vector2(2f, 2f), bounceCount); + + return projectile; + } + + Entity createMobTarget(short layer) { + Entity target = new Entity(); + + target + .addComponent(new CombatStatsComponent(100, 0)) + .addComponent(new PhysicsComponent()) + .addComponent(new HitboxComponent().setLayer(layer)); + + return target; + } + + /** + * Assumes both entity has hitbox components. + * + * @param projectile + * @param mob + */ + void triggerCollisionEnd(Entity projectile, Entity mob) { + projectile.getEvents().trigger("collisionEnd", + projectile.getComponent(HitboxComponent.class).getFixture(), + mob.getComponent(HitboxComponent.class).getFixture()); + } +} diff --git a/source/core/src/test/com/csse3200/game/components/SplitFireworksComponentTest.java b/source/core/src/test/com/csse3200/game/components/SplitFireworksComponentTest.java new file mode 100644 index 000000000..3a25dbd68 --- /dev/null +++ b/source/core/src/test/com/csse3200/game/components/SplitFireworksComponentTest.java @@ -0,0 +1,214 @@ +package com.csse3200.game.components; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.utils.Array; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.EntityService; +import com.csse3200.game.entities.factories.ProjectileFactory; +import com.csse3200.game.extensions.GameExtension; +import com.csse3200.game.physics.PhysicsLayer; +import com.csse3200.game.physics.PhysicsService; +import com.csse3200.game.physics.components.HitboxComponent; +import com.csse3200.game.physics.components.PhysicsComponent; +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; + +@ExtendWith(GameExtension.class) +public class SplitFireworksComponentTest { + Entity projectile; + Entity mob; + static double OFFSET_X = 1.75; + + private final String[] atlas = { + "images/projectiles/mobProjectile.atlas", + "images/projectiles/basic_projectile.atlas", + "images/projectiles/mobKing_projectile.atlas", + "images/projectiles/engineer_projectile.atlas", + "images/projectiles/firework_anim.atlas" + }; + + @BeforeEach + public void setUp() { + GameTime gameTime = mock(GameTime.class); + when(gameTime.getDeltaTime()).thenReturn(0.02f); + ServiceLocator.registerTimeSource(gameTime); + ServiceLocator.registerPhysicsService(new PhysicsService()); + RenderService render = new RenderService(); + render.setDebug(mock(DebugRenderer.class)); + ServiceLocator.registerRenderService(render); + ResourceService resourceService = new ResourceService(); + ServiceLocator.registerResourceService(resourceService); + resourceService.loadTextureAtlases(atlas); + resourceService.loadAll(); + ServiceLocator.registerEntityService(new EntityService()); + + // For the time being, NPC is treated as an enemy. + projectile = createSplitFireworkProjectile(PhysicsLayer.NPC, 3); + mob = createMobTarget(PhysicsLayer.NPC); + ServiceLocator.getEntityService().register(projectile); + ServiceLocator.getEntityService().register(mob); + } + + @Test + public void shouldNotBeNull() { + assertNotNull(projectile, "Ricochet projectile does not exist"); + } + + @Test + public void shouldHaveSplitFireworksComponent() { + assertNotNull(projectile.getComponent(SplitFireworksComponent.class), + "Projectile does not contain SplitFireworksComponent"); + } + + @Test + public void shouldDisposeAferCollision() { + triggerCollisionEnd(projectile, mob); + + assertTrue("original projectile entity flag should be true after collision", + projectile.getFlagForDelete()); + } + + @Test + void shouldSpawnCorrectNumberOfProjs() { + projectile.setPosition(3, 3); + + int initialNumEntities = ServiceLocator.getEntityService().getEntities().size; + + triggerCollisionEnd(projectile, mob); + + ServiceLocator.getPhysicsService().getPhysics().update(); + ServiceLocator.getEntityService().update(); + + // initialNumEntities + 2 to account for the dispose of the original projectile. + assertEquals("Should spawn correct number of projectiles after collision based on amount given", + initialNumEntities + 2, ServiceLocator.getEntityService().getEntities().size); + } + + @Test + public void shouldSpawnMultProjWithinMapBounds() { + projectile.setPosition(3, 3); + mob.setPosition(3, 3); + + int initialNumEntities = ServiceLocator.getEntityService().getEntities().size; + + triggerCollisionEnd(projectile, mob); + + ServiceLocator.getPhysicsService().getPhysics().update(); + ServiceLocator.getEntityService().update(); + + assertTrue("SplitFireWorks projectile should spawn multiple projectile out of map bounds", + ServiceLocator.getEntityService().getEntities().size > initialNumEntities); + } + + @Test + public void shouldNotSpawnMultProjOutOfMapBounds() { + projectile.setPosition(22, 22); + mob.setPosition(22, 22); + + int initialNumEntities = ServiceLocator.getEntityService().getEntities().size; + + triggerCollisionEnd(projectile, mob); + + ServiceLocator.getPhysicsService().getPhysics().update(); + ServiceLocator.getEntityService().update(); + + assertFalse(ServiceLocator.getEntityService().getEntities().size > initialNumEntities, + "SplitFireWorks projectile should not spawn multiple projectile out of map bounds"); + } + + @Test + public void testWithinRangeSpawnedProjectiles() { + projectile.setPosition(3, 3); + mob.setPosition(3, 3); + + triggerCollisionEnd(projectile, mob); + + ServiceLocator.getPhysicsService().getPhysics().update(); + ServiceLocator.getEntityService().update(); + + assertEquals("Projectiles should be spawned within the range provided.", 3, + ServiceLocator.getEntityService().getNearbyEntities(mob, 2f).size); + } + + @Test + public void testTooCloseRangeSpawnedProjectiles() { + projectile.setPosition(3, 3); + mob.setPosition(3, 3); + + triggerCollisionEnd(projectile, mob); + + ServiceLocator.getPhysicsService().getPhysics().update(); + ServiceLocator.getEntityService().update(); + + assertNotEquals(3, + ServiceLocator.getEntityService().getNearbyEntities(mob, 0.5f).size, + "Projectiles should not be spawned too close upon impact."); + } + + @Test + public void shouldSpawnAtSpecifiedLocation() { + projectile.setPosition(3, 3); + mob.setPosition(3, 3); + float currPosition = projectile.getPosition().x; + + triggerCollisionEnd(projectile, mob); + + ServiceLocator.getPhysicsService().getPhysics().update(); + ServiceLocator.getEntityService().update(); + + float newXPosition = (float) (currPosition + OFFSET_X); + + Array allEntities = ServiceLocator.getEntityService().getEntities(); + + for (Entity entity : allEntities) { + if (entity == mob) + continue; + + assertEquals("Projectiles were not spawned at the right offset x placement", newXPosition, entity.getPosition().x, + 0.02); + } + } + + Entity createSplitFireworkProjectile(short targetLayer, int amount) { + Entity projectile = ProjectileFactory.createSplitFireWorksFireball(targetLayer, new Vector2(100, 3), + new Vector2(2f, 2f), amount); + + return projectile; + } + + Entity createMobTarget(short layer) { + Entity target = new Entity(); + + target + .addComponent(new CombatStatsComponent(100, 0)) + .addComponent(new PhysicsComponent()) + .addComponent(new HitboxComponent().setLayer(layer)); + + return target; + } + + /** + * Assumes both entity has hitbox components. + * + * @param projectile + * @param mob + */ + void triggerCollisionEnd(Entity projectile, Entity mob) { + projectile.getEvents().trigger("collisionEnd", + projectile.getComponent(HitboxComponent.class).getFixture(), + mob.getComponent(HitboxComponent.class).getFixture()); + } +} diff --git a/source/core/src/test/com/csse3200/game/entities/factories/ProjectileFactoryTest.java b/source/core/src/test/com/csse3200/game/entities/factories/ProjectileFactoryTest.java index 925f44e03..9c4129a40 100644 --- a/source/core/src/test/com/csse3200/game/entities/factories/ProjectileFactoryTest.java +++ b/source/core/src/test/com/csse3200/game/entities/factories/ProjectileFactoryTest.java @@ -10,13 +10,13 @@ import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.physics.box2d.BodyDef.BodyType; -import com.csse3200.game.components.CombatStatsComponent; -import com.csse3200.game.components.CostComponent; -import com.csse3200.game.components.TouchAttackComponent; +import com.csse3200.game.components.*; +import com.csse3200.game.components.projectile.*; import com.csse3200.game.entities.Entity; 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; @@ -37,7 +37,25 @@ @ExtendWith(GameExtension.class) class ProjectileFactoryTest { - private Entity projectile; + + private final String[] atlas = { + "images/projectiles/mobProjectile.atlas", + "images/projectiles/basic_projectile.atlas", + "images/projectiles/mobKing_projectile.atlas", + "images/projectiles/engineer_projectile.atlas", + "images/projectiles/stun_effect.atlas", + "images/projectiles/burn_effect.atlas", + "images/projectiles/snow_ball.atlas", + "images/projectiles/firework_anim.atlas" + }; + + private final String[] animations = { + "rotate", + "projectile", + "projectileFinal", + "mob_boss", + "mob_bossFinal" + }; @BeforeEach public void setUp() { @@ -50,37 +68,232 @@ public void setUp() { ServiceLocator.registerRenderService(render); ResourceService resourceService = new ResourceService(); ServiceLocator.registerResourceService(resourceService); - // resourceService.loadTextures(texture); - // resourceService.loadTextureAtlases(atlas); - resourceService.loadAll(); - // ServiceLocator.getResourceService() - // .getAsset("images/projectiles/basic_projectile.atlas", TextureAtlas.class); - Vector2 destination = new Vector2(0.1f, 0.1f); - short targetLayer = PhysicsLayer.HUMANS; - Vector2 speed = new Vector2(2f, 2f); - projectile = ProjectileFactory.createBaseProjectile(targetLayer, destination, speed); + resourceService.loadTextureAtlases(atlas); + resourceService.loadAll(); + } + + @Test + public void createBaseProjectile() { + Entity projectile = ProjectileFactory.createBaseProjectile(PhysicsLayer.NPC, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(projectile); + } + + @Test + public void testBaseProjectileColliderComponent() { + Entity projectile = ProjectileFactory.createBaseProjectile(PhysicsLayer.NPC, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(projectile.getComponent(ColliderComponent.class), + "Projectile does not have a ColliderComponent"); + } + + @Test + public void testBaseProjectileTouchAttackComponent() { + Entity projectile = ProjectileFactory.createBaseProjectile(PhysicsLayer.NPC, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(projectile.getComponent(TouchAttackComponent.class), + "Projectile does not have a TouchAttackComponent"); } @Test - public void testBaseProjectileNotNull() { - assertNotNull(projectile, "Base projectile is null"); + public void testBaseProjectileDeleteOnMapEdgeComponent() { + Entity projectile = ProjectileFactory.createBaseProjectile(PhysicsLayer.NPC, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(projectile.getComponent(DeleteOnMapEdgeComponent.class), + "Projectile does not have a DeleteOnMapEdgeComponent"); } + @Test + public void testBaseProjectileSpeed() { + Vector2 testSpeed = new Vector2(1f, 1f); + Entity projectile = ProjectileFactory.createBaseProjectile(PhysicsLayer.NPC, new Vector2(0.1f, 0.1f), testSpeed); + assertEquals(testSpeed, projectile.getComponent(PhysicsMovementComponent.class).getSpeed(), + "Projectile speed does not match testSpeed"); + } + @Test public void testBaseProjectileHitbox() { + Entity projectile = ProjectileFactory.createBaseProjectile(PhysicsLayer.NPC, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); assertNotNull(projectile.getComponent(HitboxComponent.class), "Projectile does not contain Hotbox component"); } @Test public void testBaseProjectilePhysics() { + Entity projectile = ProjectileFactory.createBaseProjectile(PhysicsLayer.NPC, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); assertNotNull(projectile.getComponent(PhysicsComponent.class), - "Projectile does not have Physics component"); + "Projectile does not have Physics component"); } - + @Test public void testBaseProjectilePhysicsMovement() { + Entity projectile = ProjectileFactory.createBaseProjectile(PhysicsLayer.NPC, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); assertNotNull(projectile.getComponent(PhysicsMovementComponent.class), "Projectile does not have PhysicsMovement component"); } + + @Test + public void testFireBallProjectileCreation() { + Entity fireBall = ProjectileFactory.createFireBall(PhysicsLayer.NPC, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(fireBall); + } + + @Test + public void testFireBallAnimationRenderComponent() { + Entity fireBall = ProjectileFactory.createFireBall(PhysicsLayer.NPC, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(fireBall.getComponent(AnimationRenderComponent.class), + "Fire Ball does not have an AnimationRenderComponent"); + } + @Test + public void testFireBallAnimationController() { + Entity fireBall = ProjectileFactory.createFireBall(PhysicsLayer.NPC, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(fireBall.getComponent(ProjectileAnimationController.class), + "Fire Ball does not have an Animation Controller"); + } + + @Test + public void createMobBallProjectile() { + Entity mobBallProjectile = ProjectileFactory.createMobBall(PhysicsLayer.HUMANS, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(mobBallProjectile, "Mob King Ball is Null"); + } + + @Test + public void testMobBallProjectileAnimationRenderComponent() { + Entity mobBallProjectile = ProjectileFactory.createMobBall(PhysicsLayer.HUMANS, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(mobBallProjectile.getComponent(AnimationRenderComponent.class), + "Mob Ball Projectile does not have an AnimationRenderComponent"); + } + + @Test + public void testMobBallProjectileAnimationController() { + Entity mobBallProjectile = ProjectileFactory.createMobBall(PhysicsLayer.HUMANS, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(mobBallProjectile.getComponent(MobProjectileAnimationController.class), + "Mob Ball Projectile does not have an AnimationController"); + } + + @Test + public void testMobKingBallCreation() { + Entity mobKingBall = ProjectileFactory.createMobKingBall(PhysicsLayer.TOWER, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(mobKingBall, "Mob King Ball is null"); + } + + @Test + public void testMobKingBallAnimationRenderComponent() { + Entity mobKingBall = ProjectileFactory.createMobKingBall(PhysicsLayer.TOWER, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(mobKingBall.getComponent(AnimationRenderComponent.class), + "Mob King Ball does not have AnimationRenderComponent"); + } + + @Test + public void testMobKingBallAnimationController() { + Entity mobKingBall = ProjectileFactory.createMobKingBall(PhysicsLayer.TOWER, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(mobKingBall.getComponent(MobKingProjectAnimController.class), + "Mob King Ball does not have Animation Controller"); + } + + @Test + public void testEngineerBulletCreation() { + Entity engineerBullet = ProjectileFactory.createEngineerBullet(PhysicsLayer.NPC, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(engineerBullet, "engineerBullet is null"); + } + + @Test + public void testEngineerBulletAnimationRenderComponent() { + Entity engineerBulllet = ProjectileFactory.createEngineerBullet(PhysicsLayer.NPC, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(engineerBulllet.getComponent(AnimationRenderComponent.class), + "Engineer Bullet does not have AnimationRenderComponent"); + } + + @Test + public void testEngineerAnimationController() { + Entity engineerBullet = ProjectileFactory.createEngineerBullet(PhysicsLayer.NPC, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(engineerBullet.getComponent(EngineerBulletsAnimationController.class), + "Engineer Bullet does not have Animation Controller"); + } + + @Test + public void testStunProjectileCreation() { + Entity stunProjectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, new Vector2(0.1f, + 0.1f), new Vector2(2,2), ProjectileEffects.STUN, false); + assertNotNull(stunProjectile, "stunProjectile is null"); + } + + @Test + public void testStunProjectileAnimationRenderComponent() { + Entity stunProjectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, new Vector2(0.1f,01f), + new Vector2(2,2), ProjectileEffects.STUN, false); + assertNotNull(stunProjectile.getComponent(AnimationRenderComponent.class), + "Stun Projectile does not have AnimationRenderComponent"); + } + + @Test + public void testStunProjectileAnimationController() { + Entity stunProjectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.TOWER, new Vector2(0.1f, 0.1f) + , new Vector2(2,2), ProjectileEffects.STUN, false); + assertNotNull(stunProjectile.getComponent(StunEffectProjectileAnimationController.class), + "Stun Projectile does not have Animation Controller"); + } + + @Test + public void testBurnProjectileCreation() { + Entity burnProjectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, new Vector2(0.1f, + 0.1f), new Vector2(2,2), ProjectileEffects.BURN, false); + assertNotNull(burnProjectile, "burnProjectile is null"); + } + + @Test + public void testBurnProjectileAnimationRenderComponent() { + Entity burnProjectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, new Vector2(0.1f,01f), + new Vector2(2,2), ProjectileEffects.BURN, false); + assertNotNull(burnProjectile.getComponent(AnimationRenderComponent.class), + "Burn Projectile does not have AnimationRenderComponent"); + } + @Test + public void testBurnProjectileAnimationController() { + Entity burnProjectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.TOWER, new Vector2(0.1f, 0.1f) + , new Vector2(2,2), ProjectileEffects.BURN, false); + assertNotNull(burnProjectile.getComponent(BurnEffectProjectileAnimationController.class), + "Burn Projectile does not have Animation Controller"); + } + + @Test + public void testSlowProjectileCreation() { + Entity slowProjectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, new Vector2(0.1f, + 0.1f), new Vector2(2,2), ProjectileEffects.SLOW, false); + assertNotNull(slowProjectile, "slowProjectile is null"); + } + + @Test + public void testSlowProjectileAnimationRenderComponent() { + Entity slowProjectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, new Vector2(0.1f,01f), + new Vector2(2,2), ProjectileEffects.SLOW, false); + assertNotNull(slowProjectile.getComponent(AnimationRenderComponent.class), + "Slow Projectile does not have AnimationRenderComponent"); + } + @Test + public void testSlowProjectileAnimationController() { + Entity slowProjectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.TOWER, new Vector2(0.1f, 0.1f) + , new Vector2(2,2), ProjectileEffects.SLOW, false); + assertNotNull(slowProjectile.getComponent(SnowBallProjectileAnimationController.class), + "Slow Projectile does not have Animation Controller"); + } + + @Test + public void testFireworkProjectileCreation() { + Entity fireworkProjectile = ProjectileFactory.createFireworks( + PhysicsLayer.TOWER, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(fireworkProjectile, "fireworkProjectile is null"); + } + + @Test + public void testFireworkProjectileAnimationRenderComponent() { + Entity fireworkProjectile = ProjectileFactory.createFireworks( + PhysicsLayer.TOWER, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(fireworkProjectile.getComponent(AnimationRenderComponent.class), + "Fire Projectile does not have AnimationRenderComponent"); + } + @Test + public void testFireworkProjectileAnimationController() { + Entity fireworkProjectile = ProjectileFactory.createFireworks( + PhysicsLayer.TOWER, new Vector2(0.1f, 0.1f), new Vector2(1f, 1f)); + assertNotNull(fireworkProjectile.getComponent(FireworkAnimationController.class), + "Fire Projectile does not have Animation Controller"); + } } +