diff --git a/source/core/assets/images/projectiles/mobProjectile.atlas b/source/core/assets/images/projectiles/mobProjectile.atlas new file mode 100644 index 000000000..2a0f5357f --- /dev/null +++ b/source/core/assets/images/projectiles/mobProjectile.atlas @@ -0,0 +1,41 @@ + +mobProjectile.png +size: 128, 32 +format: RGBA8888 +filter: Nearest, Nearest +repeat: none +rotate + rotate: false + xy: 54, 2 + size: 24, 23 + orig: 24, 23 + offset: 0, 0 + index: -1 +rotate + rotate: false + xy: 2, 2 + size: 24, 23 + orig: 24, 23 + offset: 0, 0 + index: -1 +default + rotate: false + xy: 54, 2 + size: 24, 23 + orig: 24, 23 + offset: 0, 0 + index: -1 +rotate + rotate: false + xy: 28, 2 + size: 24, 23 + orig: 24, 23 + offset: 0, 0 + index: -1 +rotate + rotate: false + xy: 80, 2 + size: 24, 23 + orig: 24, 23 + offset: 0, 0 + index: -1 diff --git a/source/core/assets/images/projectiles/mobProjectile.png b/source/core/assets/images/projectiles/mobProjectile.png new file mode 100644 index 000000000..919476050 Binary files /dev/null and b/source/core/assets/images/projectiles/mobProjectile.png differ 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 7450ae13a..212217b2b 100644 --- a/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java +++ b/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java @@ -5,11 +5,13 @@ import com.badlogic.gdx.math.GridPoint2; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2; +import com.csse3200.game.components.ProjectileEffects; import com.csse3200.game.input.DropInputComponent; import com.csse3200.game.areas.terrain.TerrainFactory; import com.csse3200.game.areas.terrain.TerrainFactory.TerrainType; import com.csse3200.game.entities.Entity; import com.csse3200.game.entities.factories.*; +import com.csse3200.game.physics.PhysicsLayer; import com.csse3200.game.utils.math.GridPoint2Utils; import com.csse3200.game.utils.math.RandomUtils; import com.csse3200.game.services.ResourceService; @@ -85,7 +87,9 @@ public class ForestGameArea extends GameArea { "images/iso_grass_3.png", "images/economy/scrap.png", "images/towers/mine_tower.png", - "images/projectiles/basic_projectile.png" + "images/projectiles/basic_projectile.png", + "images/projectiles/mobProjectile.png" + }; private static final String[] forestTextureAtlases = { "images/terrain_iso_grass.atlas", @@ -96,7 +100,8 @@ public class ForestGameArea extends GameArea { "images/mobs/xenoGruntRunning.atlas", "images/mobs/robot.atlas", "images/mobs/rangeBossRight.atlas", - "images/projectiles/basic_projectile.atlas" + "images/projectiles/basic_projectile.atlas", + "images/projectiles/mobProjectile.atlas" }; private static final String[] forestSounds = { "sounds/Impact4.ogg", @@ -114,6 +119,7 @@ public class ForestGameArea extends GameArea { // Variables to be used with spawn projectile methods. This is the variable // that should occupy the direction param. private static final int towardsMobs = 100; + private static final int towardsTowers = 0; private Entity bossKing1; private Entity bossKing2; @@ -143,10 +149,12 @@ public void create() { playMusic(); - // Types of projectile - spawnAoeProjectile(new Vector2(0, 10), player, towardsMobs, new Vector2(2f, 2f), 1); - spawnProjectile(new Vector2(0, 10), player, towardsMobs, new Vector2(2f, 2f)); - spawnMultiProjectile(new Vector2(0, 10), player, towardsMobs, 20, new Vector2(2f, 2f), 7); + spawnEffectProjectile(new Vector2(0, 10), PhysicsLayer.PLAYER, towardsMobs, new Vector2(2f, 2f), ProjectileEffects.FIREBALL, true); +// spawnProjectile(new Vector2(0, 10), player, towardsMobs, new Vector2(2f, 2f)); +// + spawnMobBall(new Vector2(15, 10), PhysicsLayer.PLAYER, towardsTowers, new Vector2(2f, 2f)); + spawnMultiProjectile(new Vector2(0, 10), PhysicsLayer.PLAYER, towardsMobs, 20, new Vector2(2f, 2f), 7); + spawnXenoGrunts(); spawnGhosts(); @@ -273,13 +281,13 @@ private Entity spawnBossKing1() { * Spawns a projectile that only heads towards the enemies in its lane. * * @param position The position of the Entity that's shooting the projectile. - * @param target The enemy entities of the "shooter". + * @param targetLayer The enemy layer of the "shooter". * @param direction The direction the projectile should head towards. * @param speed The speed of the projectiles. * */ - private void spawnProjectile(Vector2 position, Entity target, int direction, Vector2 speed) { - Entity Projectile = ProjectileFactory.createFireBall(target, new Vector2(direction, position.y), speed); + private void spawnProjectile(Vector2 position, short targetLayer, int direction, Vector2 speed) { + Entity Projectile = ProjectileFactory.createFireBall(targetLayer, new Vector2(direction, position.y), speed); Projectile.setPosition(position); spawnEntity(Projectile); } @@ -288,14 +296,29 @@ private void spawnProjectile(Vector2 position, Entity target, int direction, Vec * Spawns a projectile to be used for multiple projectile function. * * @param position The position of the Entity that's shooting the projectile. - * @param target The enemy entities of the "shooter". + * @param targetLayer The enemy layer of the "shooter". * @param space The space between the projectiles' destination. * @param direction The direction the projectile should head towards. * @param speed The speed of the projectiles. * */ - private void spawnProjectile(Vector2 position, Entity target, int space, int direction, Vector2 speed) { - Entity Projectile = ProjectileFactory.createFireBall(target, new Vector2(direction, position.y + space), speed); + private void spawnProjectile(Vector2 position, short targetLayer, int space, int direction, Vector2 speed) { + Entity Projectile = ProjectileFactory.createFireBall(targetLayer, new Vector2(direction, position.y + space), speed); + Projectile.setPosition(position); + spawnEntity(Projectile); + } + + /** + * Spawns a mob based projectile to be used for multiple projectile function. + * + * @param position The position of the Entity that's shooting the projectile. + * @param targetLayer The enemy layer of the "shooter". + * @param direction The direction the projectile should head towards. + * @param speed The speed of the projectiles. + * + */ + private void spawnMobBall(Vector2 position, short targetLayer, int direction, Vector2 speed) { + Entity Projectile = ProjectileFactory.createMobBall(targetLayer, new Vector2(direction, position.y), speed); Projectile.setPosition(position); spawnEntity(Projectile); } @@ -359,16 +382,16 @@ private Entity spawnBossKing2() { * the starting point but different destinations. * * @param position The position of the Entity that's shooting the projectile. - * @param target The enemy entities of the "shooter". + * @param targetLayer The enemy layer of the "shooter". * @param direction The direction the projectile should head towards. * @param space The space between the projectiles' destination. * @param speed The speed of the projectiles. * @param quantity The amount of projectiles to spawn. */ - private void spawnMultiProjectile(Vector2 position, Entity target, int direction, int space, Vector2 speed, int quantity) { + private void spawnMultiProjectile(Vector2 position, short targetLayer, int direction, int space, Vector2 speed, int quantity) { int half = quantity / 2; for (int i = 0; i < quantity; i++) { - spawnProjectile(position, target, space * half, direction, speed); + spawnProjectile(position, targetLayer, space * half, direction, speed); --half; } } @@ -377,13 +400,15 @@ private void spawnMultiProjectile(Vector2 position, Entity target, int direction * Returns projectile that can do an area of effect damage * * @param position The position of the Entity that's shooting the projectile. - * @param target The enemy entities of the "shooter". + * @param targetLayer The enemy layer of the "shooter". * @param direction The direction the projectile should head towards. * @param speed The speed of the projectiles. - * @param aoeSize The size of the area of effect. + * @param effect Type of effect. + * @param aoe Whether it is an aoe projectile. */ - private void spawnAoeProjectile(Vector2 position, Entity target, int direction, Vector2 speed, int aoeSize) { - Entity Projectile = ProjectileFactory.createAOEFireBall(target, new Vector2(direction, position.y), speed, aoeSize); + private void spawnEffectProjectile(Vector2 position, short targetLayer, int direction, Vector2 speed, + ProjectileEffects effect, boolean aoe) { + Entity Projectile = ProjectileFactory.createEffectProjectile(targetLayer, new Vector2(direction, position.y), speed, effect, aoe); Projectile.setPosition(position); spawnEntity(Projectile); } diff --git a/source/core/src/main/com/csse3200/game/components/AoeComponent.java b/source/core/src/main/com/csse3200/game/components/EffectsComponent.java similarity index 56% rename from source/core/src/main/com/csse3200/game/components/AoeComponent.java rename to source/core/src/main/com/csse3200/game/components/EffectsComponent.java index 93403e8aa..0fb98fcf7 100644 --- a/source/core/src/main/com/csse3200/game/components/AoeComponent.java +++ b/source/core/src/main/com/csse3200/game/components/EffectsComponent.java @@ -2,29 +2,36 @@ import com.badlogic.gdx.physics.box2d.Fixture; import com.csse3200.game.entities.Entity; -import com.csse3200.game.physics.BodyUserData; +import com.csse3200.game.entities.factories.ProjectileFactory; +import com.csse3200.game.physics.PhysicsLayer; import com.csse3200.game.physics.components.HitboxComponent; import com.csse3200.game.services.ServiceLocator; import com.badlogic.gdx.utils.Array; -public class AoeComponent extends Component { +public class EffectsComponent extends Component { private final float radius; + private final ProjectileEffects effect; + private final boolean aoe; private HitboxComponent hitboxComponent; + private final short targetLayer; /** * Constructor for the AoEComponent. * * @param radius The radius of the area-of-effect. */ - public AoeComponent(float radius) { + public EffectsComponent(short targetLayer, float radius, ProjectileEffects effect, boolean aoe) { + this.targetLayer = targetLayer; this.radius = radius; + this.effect = effect; + this.aoe = aoe; } @Override public void create() { - entity.getEvents().addListener("collisionStart", this::onCollisionStart); - entity.getEvents().addListener("collisionEnd", this::onCollisionEnd); + entity.getEvents().addListener("projectileCollisionStart", this::onCollisionStart); + entity.getEvents().addListener("projectileCollisionEnd", this::onCollisionEnd); hitboxComponent = entity.getComponent(HitboxComponent.class); } @@ -37,12 +44,27 @@ private void onCollisionEnd(Fixture me, Fixture other) { // Not triggered by hitbox, ignore return; } - applyAoeDamage(); + + if (!PhysicsLayer.contains(targetLayer, other.getFilterData().categoryBits)) { + // Doesn't match our target layer, ignore + return; + } + + switch (effect) { + case FIREBALL -> { + if (aoe) { + applyAoeEffect(ProjectileEffects.FIREBALL); + } + } + case BURN -> {} + case SLOW -> {} + case STUN -> {} + } } /** - * Apply damage to all entities within the area of effect (radius). + * Used for aoe fireball projectile to apply damage to all entities within the area of effect (radius). */ - public void applyAoeDamage() { + public void applyAoeEffect(ProjectileEffects effect) { Entity hostEntity = getEntity(); CombatStatsComponent hostCombatStats = hostEntity.getComponent(CombatStatsComponent.class); diff --git a/source/core/src/main/com/csse3200/game/components/MobProjectileAnimationController.java b/source/core/src/main/com/csse3200/game/components/MobProjectileAnimationController.java new file mode 100644 index 000000000..e1a55775e --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/MobProjectileAnimationController.java @@ -0,0 +1,23 @@ +package com.csse3200.game.components; + +import com.csse3200.game.rendering.AnimationRenderComponent; + + +public class MobProjectileAnimationController extends Component { + AnimationRenderComponent animator; + + /** + * Creation call for a TowerAnimationController, fetches the animationRenderComponent that this controller will + * be attached to and registers all the event listeners required to trigger the animations and sounds. + */ + @Override + public void create() { + super.create(); + animator = this.entity.getComponent(AnimationRenderComponent.class); + entity.getEvents().addListener("rotate", this::animateStartRotate); + } + + void animateStartRotate() { + animator.startAnimation("rotate"); + } +} diff --git a/source/core/src/main/com/csse3200/game/components/ProjectileEffects.java b/source/core/src/main/com/csse3200/game/components/ProjectileEffects.java new file mode 100644 index 000000000..39259b414 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/ProjectileEffects.java @@ -0,0 +1,8 @@ +package com.csse3200.game.components; + +public enum ProjectileEffects { + FIREBALL, //fireball projectile - deals damage based on baseAttack + BURN, //burn projectile - does 5 extra ticks of damage over 5 seconds + SLOW, //slow projectile - slows entity by half for 5 seconds + STUN //stun projectile - stuns entity for 5 seconds +} 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 0d10d8dd6..e622cc2b1 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 @@ -112,8 +112,10 @@ public void updateTowerState() { } else { owner.getEntity().getEvents().trigger(FIRING); // this might be changed to an event which gets triggered everytime the tower enters the firing state - Entity newProjectile = ProjectileFactory.createFireBall(owner.getEntity(), new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f,2f)); + + Entity newProjectile = ProjectileFactory.createFireBall(PhysicsLayer.PLAYER, new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f,2f)); newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.75), (float) (owner.getEntity().getPosition().y + 0.4)); + ServiceLocator.getEntityService().register(newProjectile); } } diff --git a/source/core/src/main/com/csse3200/game/components/tasks/TrajectTask.java b/source/core/src/main/com/csse3200/game/components/tasks/TrajectTask.java index 106c90fcb..56c81912b 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/TrajectTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/TrajectTask.java @@ -13,6 +13,8 @@ public class TrajectTask extends DefaultTask implements PriorityTask { private static final String START = "startProjectile"; private static final String FINAL = "startProjectileFinal"; + + private enum STATE { START, FINAL } @@ -36,7 +38,9 @@ public void start() { movementTask = new MovementTask(destination); movementTask.create(owner); movementTask.start(); + this.owner.getEntity().getEvents().trigger(START); + this.owner.getEntity().getEvents().trigger("rotate"); } public void switchProjectileState() { diff --git a/source/core/src/main/com/csse3200/game/entities/EntityService.java b/source/core/src/main/com/csse3200/game/entities/EntityService.java index c8c8823df..9c20b93d4 100644 --- a/source/core/src/main/com/csse3200/game/entities/EntityService.java +++ b/source/core/src/main/com/csse3200/game/entities/EntityService.java @@ -85,7 +85,9 @@ public Array getEntities() { public Array getNearbyEntities(Entity source, float radius) { Array nearbyEntities = new Array(); Array allEntities = ServiceLocator.getEntityService().getEntities(); - for (Entity otherEntity : allEntities) { + for (int i = 0; i < allEntities.size; i++) { + Entity otherEntity = allEntities.get(i); + if (source == otherEntity) continue; // Skip the source entity Vector2 positionSource = source.getPosition(); 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 42b6e7034..f92349ef0 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 @@ -2,23 +2,28 @@ import com.badlogic.gdx.graphics.g2d.Animation; import com.badlogic.gdx.graphics.g2d.TextureAtlas; -import com.csse3200.game.components.AoeComponent; +import com.csse3200.game.components.EffectsComponent; +import com.csse3200.game.components.ProjectileEffects; import com.csse3200.game.components.TouchAttackComponent; import com.csse3200.game.components.tasks.TrajectTask; import com.csse3200.game.ai.tasks.AITaskComponent; import com.csse3200.game.components.CombatStatsComponent; +import com.csse3200.game.components.MobProjectileAnimationController; import com.csse3200.game.entities.configs.BaseEntityConfig; import com.csse3200.game.entities.configs.NPCConfigs; import com.csse3200.game.files.FileLoader; import com.csse3200.game.entities.Entity; import com.csse3200.game.rendering.AnimationRenderComponent; import com.csse3200.game.rendering.TextureRenderComponent; +import com.csse3200.game.services.ServiceLocator; import com.csse3200.game.physics.PhysicsLayer; import com.csse3200.game.physics.PhysicsUtils; 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.badlogic.gdx.graphics.g2d.Animation; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.math.Vector2; import com.csse3200.game.components.projectile.ProjectileAnimationController; import com.csse3200.game.services.ServiceLocator; @@ -37,18 +42,49 @@ public class ProjectileFactory { 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 + * @return Returns a new single-target projectile entity + */ + public static Entity createEffectProjectile(short targetLayer, Vector2 destination, Vector2 speed, + ProjectileEffects effect, boolean aoe) { + BaseEntityConfig config = configs.fireBall; + Entity projectile = createFireBall(targetLayer, destination, speed); + + switch(effect) { + case FIREBALL -> { + projectile.addComponent(new EffectsComponent(targetLayer, 3, ProjectileEffects.FIREBALL, aoe)); + } + case BURN -> { + projectile.addComponent(new EffectsComponent(targetLayer, 3, ProjectileEffects.BURN, aoe)); + } + case SLOW -> { + projectile.addComponent(new EffectsComponent(targetLayer, 3, ProjectileEffects.SLOW, aoe)); + } + case STUN -> { + projectile.addComponent(new EffectsComponent(targetLayer, 3, ProjectileEffects.STUN, aoe)); + } + } + return projectile; + } + /** * Creates a fireball Entity. * - * @param target The enemy entities that the projectile collides with. + * @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 createFireBall(Entity target, Vector2 destination, Vector2 speed) { + public static Entity createFireBall(short targetLayer, Vector2 destination, Vector2 speed) { BaseEntityConfig config = configs.fireBall; - Entity projectile = createBaseProjectile(target, destination); + Entity projectile = createBaseProjectile(destination); AnimationRenderComponent animator = new AnimationRenderComponent( @@ -63,7 +99,7 @@ public static Entity createFireBall(Entity target, Vector2 destination, Vector2 .addComponent(animator) // This is the component that allows the projectile to damage a specified target. - .addComponent(new TouchAttackComponent(PhysicsLayer.PLAYER, 1.5f, true)) + .addComponent(new TouchAttackComponent(targetLayer, 1.5f, true)) .addComponent(new CombatStatsComponent(config.health, config.baseAttack)); // projectile @@ -75,22 +111,41 @@ public static Entity createFireBall(Entity target, Vector2 destination, Vector2 return projectile; } - + /** - * Creates an AOE fireball Entity. + * Creates a projectile specifically for mobs to shoot * - * @param target The enemy entities that the projectile collides with. + * @param targetLayer The enemy entities that the projectile collides with. * @param destination The destination the projectile heads towards. * @param speed The speed of the projectile. - * @param aoeSize The size of the AOE. - * @return Returns the new aoe projectile entity. + * @return Returns a new fireball projectile entity. */ - public static Entity createAOEFireBall(Entity target, Vector2 destination, Vector2 speed, int aoeSize) { + public static Entity createMobBall(short targetLayer, Vector2 destination, Vector2 speed) { BaseEntityConfig config = configs.fireBall; - Entity projectile = createFireBall(target, destination, speed); + + Entity projectile = createBaseProjectile(destination); + + AnimationRenderComponent animator = + new AnimationRenderComponent( + ServiceLocator.getResourceService() + .getAsset("images/projectiles/mobProjectile.atlas", TextureAtlas.class)); + + animator.addAnimation("rotate", 0.15f, Animation.PlayMode.LOOP); + + projectile + .addComponent(new ColliderComponent().setSensor(true)) + + // This is the component that allows the projectile to damage a specified target. + .addComponent(new TouchAttackComponent(PhysicsLayer.PLAYER, 1.5f, true)) + .addComponent(new CombatStatsComponent(config.health, config.baseAttack)) + .addComponent(animator) + .addComponent(new MobProjectileAnimationController()); + projectile - // This is the component that allows the projectile to damage a specified target. - .addComponent(new AoeComponent(aoeSize)); + .getComponent(AnimationRenderComponent.class).scaleEntity(); + + projectile + .getComponent(PhysicsMovementComponent.class).setSpeed(speed); return projectile; } @@ -98,11 +153,10 @@ public static Entity createAOEFireBall(Entity target, Vector2 destination, Vecto /** * Creates a generic projectile entity that can be used for multiple types of * projectiles. * - * @param target The enemy entities that the projectile collides with. * @param destination The destination the projectile heads towards. * @return Returns a generic projectile entity. */ - public static Entity createBaseProjectile(Entity target, Vector2 destination) { + public static Entity createBaseProjectile(Vector2 destination) { AITaskComponent aiComponent = new AITaskComponent() .addTask(new TrajectTask(destination)); 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 1e5c4fb27..5d4f9d686 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 @@ -11,6 +11,7 @@ import com.csse3200.game.entities.EntityService; import com.csse3200.game.areas.ForestGameArea; 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; @@ -51,7 +52,7 @@ public void setUp() { Vector2 destination = new Vector2(0.1f, 0.1f); Vector2 speed = new Vector2(0.2f, 0.2f); - projectile = ProjectileFactory.createBaseProjectile(new Entity(), destination); + projectile = ProjectileFactory.createBaseProjectile(destination); } @Test