diff --git a/source/core/assets/configs/tower.json b/source/core/assets/configs/tower.json index fe44c4d17..c7a95392e 100644 --- a/source/core/assets/configs/tower.json +++ b/source/core/assets/configs/tower.json @@ -18,5 +18,21 @@ "health": 10, "baseAttack": 5, "cost": 1 + }, + "DroidTower": { + "health": 50, + "baseAttack": 5, + "cost": 1 + }, + "fireTower": { + "health": 10, + "baseAttack": 10, + "cost": 10 + }, + "stunTower": { + "health": 10, + "baseAttack": 10, + "cost": 10 + } } \ No newline at end of file diff --git a/source/core/assets/images/projectiles/basic_projectile.atlas b/source/core/assets/images/projectiles/basic_projectile.atlas index 2f09b83bf..9d295b888 100644 --- a/source/core/assets/images/projectiles/basic_projectile.atlas +++ b/source/core/assets/images/projectiles/basic_projectile.atlas @@ -4,31 +4,38 @@ size: 256, 32 format: RGBA8888 filter: Nearest, Nearest repeat: none -projectile +projectileFinal rotate: false xy: 2, 2 size: 36, 19 orig: 36, 19 offset: 0, 0 - index: 4 -projectileFinal - rotate: false - xy: 103, 3 - size: 30, 18 - orig: 30, 18 - offset: 0, 0 - index: 3 + index: 2 projectile rotate: false xy: 40, 2 - size: 30, 19 - orig: 30, 19 + size: 36, 19 + orig: 36, 19 offset: 0, 0 - index: 2 + index: 4 projectile rotate: false - xy: 72, 2 - size: 29, 19 - orig: 29, 19 + xy: 78, 2 + size: 36, 19 + orig: 36, 19 offset: 0, 0 index: 1 +projectile + rotate: false + xy: 116, 2 + size: 36, 19 + orig: 36, 19 + offset: 0, 0 + index: 3 +default + rotate: false + xy: 116, 2 + size: 36, 19 + orig: 36, 19 + offset: 0, 0 + index: 3 diff --git a/source/core/assets/images/projectiles/basic_projectile.png b/source/core/assets/images/projectiles/basic_projectile.png index abf6cfc59..5fa3bb42c 100644 Binary files a/source/core/assets/images/projectiles/basic_projectile.png and b/source/core/assets/images/projectiles/basic_projectile.png differ diff --git a/source/core/assets/images/projectiles/engineer_projectile.atlas b/source/core/assets/images/projectiles/engineer_projectile.atlas new file mode 100644 index 000000000..9790d718e --- /dev/null +++ b/source/core/assets/images/projectiles/engineer_projectile.atlas @@ -0,0 +1,35 @@ + +engineer_projectile.png +size: 128, 32 +format: RGBA8888 +filter: Nearest, Nearest +repeat: none +bullet + rotate: false + xy: 47, 2 + size: 19, 19 + orig: 19, 19 + offset: 0, 0 + index: -1 +bullet + rotate: false + xy: 2, 2 + size: 21, 19 + orig: 21, 19 + offset: 0, 0 + index: -1 +bulletFinal + rotate: false + xy: 68, 2 + size: 19, 19 + orig: 19, 19 + offset: 0, 0 + index: -1 +bullet + rotate: false + xy: 25, 2 + size: 20, 19 + orig: 20, 19 + offset: 0, 0 + index: -1 + diff --git a/source/core/assets/images/projectiles/engineer_projectile.png b/source/core/assets/images/projectiles/engineer_projectile.png new file mode 100644 index 000000000..a98ae11e2 Binary files /dev/null and b/source/core/assets/images/projectiles/engineer_projectile.png differ diff --git a/source/core/assets/images/projectiles/mobKing_projectile.atlas b/source/core/assets/images/projectiles/mobKing_projectile.atlas new file mode 100644 index 000000000..ae151e03b --- /dev/null +++ b/source/core/assets/images/projectiles/mobKing_projectile.atlas @@ -0,0 +1,62 @@ + +mobKing_projectile.png +size: 256, 32 +format: RGBA8888 +filter: Nearest, Nearest +repeat: none +mob_boss + rotate: false + xy: 142, 2 + size: 26, 19 + orig: 26, 19 + offset: 0, 0 + index: -1 +mob_boss + rotate: false + xy: 198, 3 + size: 24, 18 + orig: 24, 18 + offset: 0, 0 + index: -1 +mob_boss + rotate: false + xy: 170, 2 + size: 26, 19 + orig: 26, 19 + offset: 0, 0 + index: -1 +mob_boss + rotate: false + xy: 37, 2 + size: 33, 19 + orig: 33, 19 + offset: 0, 0 + index: -1 +mob_boss + rotate: false + xy: 107, 2 + size: 33, 19 + orig: 33, 19 + offset: 0, 0 + index: -1 +mob_boss + rotate: false + xy: 72, 2 + size: 33, 19 + orig: 33, 19 + offset: 0, 0 + index: -1 +mob_bossFinal + rotate: false + xy: 2, 2 + size: 33, 19 + orig: 33, 19 + offset: 0, 0 + index: -1 +default + rotate: false + xy: 142, 2 + size: 26, 19 + orig: 26, 19 + offset: 0, 0 + index: -1 diff --git a/source/core/assets/images/projectiles/mobKing_projectile.png b/source/core/assets/images/projectiles/mobKing_projectile.png new file mode 100644 index 000000000..167bf6e4e Binary files /dev/null and b/source/core/assets/images/projectiles/mobKing_projectile.png differ diff --git a/source/core/assets/images/projectiles/mobProjectile.atlas b/source/core/assets/images/projectiles/mobProjectile.atlas index d069c0394..5dabfa025 100644 --- a/source/core/assets/images/projectiles/mobProjectile.atlas +++ b/source/core/assets/images/projectiles/mobProjectile.atlas @@ -20,7 +20,7 @@ rotate index: -1 default rotate: false - xy: 54, 2 + xy: 28, 2 size: 24, 23 orig: 24, 23 offset: 0, 0 diff --git a/source/core/assets/images/projectiles/projectile_explosion.atlas b/source/core/assets/images/projectiles/projectile_explosion.atlas new file mode 100644 index 000000000..d285fa835 --- /dev/null +++ b/source/core/assets/images/projectiles/projectile_explosion.atlas @@ -0,0 +1,97 @@ + +projectile_explosion.png +size: 2048, 256 +format: RGBA8888 +filter: Nearest, Nearest +repeat: none +explosion + rotate: false + xy: 1042, 2 + size: 128, 128 + orig: 128, 128 + offset: 0, 0 + index: -1 +explosion + rotate: false + xy: 782, 2 + size: 128, 128 + orig: 128, 128 + offset: 0, 0 + index: -1 +explosion + rotate: false + xy: 262, 2 + size: 128, 128 + orig: 128, 128 + offset: 0, 0 + index: -1 +explosionFinal + rotate: false + xy: 1302, 2 + size: 128, 128 + orig: 128, 128 + offset: 0, 0 + index: -1 +explosion + rotate: false + xy: 392, 2 + size: 128, 128 + orig: 128, 128 + offset: 0, 0 + index: -1 +explosion + rotate: false + xy: 1432, 2 + size: 128, 128 + orig: 128, 128 + offset: 0, 0 + index: -1 +explosion + rotate: false + xy: 652, 2 + size: 128, 128 + orig: 128, 128 + offset: 0, 0 + index: -1 +explosion + rotate: false + xy: 132, 2 + size: 128, 128 + orig: 128, 128 + offset: 0, 0 + index: -1 +explosion + rotate: false + xy: 1172, 2 + size: 128, 128 + orig: 128, 128 + offset: 0, 0 + index: -1 +explosion + rotate: false + xy: 522, 2 + size: 128, 128 + orig: 128, 128 + offset: 0, 0 + index: -1 +explosion + rotate: false + xy: 2, 2 + size: 128, 128 + orig: 128, 128 + offset: 0, 0 + index: -1 +explosion + rotate: false + xy: 912, 2 + size: 128, 128 + orig: 128, 128 + offset: 0, 0 + index: -1 +default + rotate: false + xy: 1042, 2 + size: 128, 128 + orig: 128, 128 + offset: 0, 0 + index: -1 diff --git a/source/core/assets/images/projectiles/projectile_explosion.png b/source/core/assets/images/projectiles/projectile_explosion.png new file mode 100644 index 000000000..cc46711cf Binary files /dev/null and b/source/core/assets/images/projectiles/projectile_explosion.png differ diff --git a/source/core/assets/images/projectiles/snow_ball.atlas b/source/core/assets/images/projectiles/snow_ball.atlas new file mode 100644 index 000000000..20e4eb554 --- /dev/null +++ b/source/core/assets/images/projectiles/snow_ball.atlas @@ -0,0 +1,48 @@ + +snow_ball.png +size: 128, 32 +format: RGBA8888 +filter: Nearest, Nearest +repeat: none +projectile + rotate: false + xy: 20, 2 + size: 16, 19 + orig: 16, 19 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 56, 2 + size: 16, 19 + orig: 16, 19 + offset: 0, 0 + index: -1 +projectile + rotate: false + xy: 38, 2 + size: 16, 19 + orig: 16, 19 + offset: 0, 0 + index: -1 +projectileFinal + rotate: false + xy: 2, 2 + size: 16, 19 + orig: 16, 19 + offset: 0, 0 + index: -1 +collision + rotate: false + xy: 92, 3 + size: 16, 18 + orig: 16, 18 + offset: 0, 0 + index: -1 +collision + rotate: false + xy: 74, 3 + size: 16, 18 + orig: 16, 18 + offset: 0, 0 + index: -1 diff --git a/source/core/assets/images/projectiles/snow_ball.png b/source/core/assets/images/projectiles/snow_ball.png new file mode 100644 index 000000000..f110a1e02 Binary files /dev/null and b/source/core/assets/images/projectiles/snow_ball.png differ diff --git a/source/core/assets/images/towers/DroidTower.atlas b/source/core/assets/images/towers/DroidTower.atlas new file mode 100644 index 000000000..47c4d1626 --- /dev/null +++ b/source/core/assets/images/towers/DroidTower.atlas @@ -0,0 +1,251 @@ + +DroidTower.png +size: 1024, 64 +format: RGBA8888 +filter: Nearest, Nearest +repeat: none +attackDown + rotate: false + xy: 72, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 4 +attackDown + rotate: false + xy: 212, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 1 +attackDown + rotate: false + xy: 457, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 3 +attackDown + rotate: false + xy: 597, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 0 +attackDown + rotate: false + xy: 737, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 2 +attackUp + rotate: false + xy: 247, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 3 +attackUp + rotate: false + xy: 422, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 0 +attackUp + rotate: false + xy: 422, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 4 +death + rotate: false + xy: 422, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 0 +idle + rotate: false + xy: 422, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 0 +attackUp + rotate: false + xy: 632, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 2 +attackUp + rotate: false + xy: 842, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 1 +death + rotate: false + xy: 2, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 1 +death + rotate: false + xy: 177, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 6 +death + rotate: false + xy: 387, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 8 +death + rotate: false + xy: 527, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 5 +death + rotate: false + xy: 702, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 7 +goDown + rotate: false + xy: 37, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 1 +death + rotate: false + xy: 37, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 3 +goUp + rotate: false + xy: 37, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 2 +goDown + rotate: false + xy: 282, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 3 +goUp + rotate: false + xy: 282, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 0 +goDown + rotate: false + xy: 562, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 5 +goDown + rotate: false + xy: 772, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 4 +goUp + rotate: false + xy: 142, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 1 +death + rotate: false + xy: 142, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 4 +goDown + rotate: false + xy: 142, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 2 +goUp + rotate: false + xy: 352, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 3 +death + rotate: false + xy: 352, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 2 +goDown + rotate: false + xy: 352, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 0 +walk + rotate: false + xy: 107, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 1 +walk + rotate: false + xy: 317, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 3 +walk + rotate: false + xy: 492, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 0 +walk + rotate: false + xy: 667, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 2 +walk + rotate: false + xy: 807, 2 + size: 33, 38 + orig: 33, 38 + offset: 0, 0 + index: 4 diff --git a/source/core/assets/images/towers/DroidTower.png b/source/core/assets/images/towers/DroidTower.png new file mode 100644 index 000000000..6fab7ed2d Binary files /dev/null and b/source/core/assets/images/towers/DroidTower.png differ diff --git a/source/core/assets/images/towers/fire_tower_atlas.atlas b/source/core/assets/images/towers/fire_tower_atlas.atlas new file mode 100644 index 000000000..7c9ce2206 --- /dev/null +++ b/source/core/assets/images/towers/fire_tower_atlas.atlas @@ -0,0 +1,83 @@ + +fire_tower_atlas.png +size: 1024, 64 +format: RGBA8888 +filter: Nearest, Nearest +repeat: none +attack + rotate: false + xy: 122, 2 + size: 58, 58 + orig: 58, 58 + offset: 0, 0 + index: 1 +attack + rotate: false + xy: 302, 2 + size: 58, 58 + orig: 58, 58 + offset: 0, 0 + index: 3 +attack + rotate: false + xy: 422, 2 + size: 58, 58 + orig: 58, 58 + offset: 0, 0 + index: 0 +attack + rotate: false + xy: 602, 2 + size: 58, 58 + orig: 58, 58 + offset: 0, 0 + index: 2 +idle + rotate: false + xy: 62, 2 + size: 58, 58 + orig: 58, 58 + offset: 0, 0 + index: 1 +idle + rotate: false + xy: 182, 2 + size: 58, 58 + orig: 58, 58 + offset: 0, 0 + index: 3 +idle + rotate: false + xy: 362, 2 + size: 58, 58 + orig: 58, 58 + offset: 0, 0 + index: 0 +idle + rotate: false + xy: 542, 2 + size: 58, 58 + orig: 58, 58 + offset: 0, 0 + index: 2 +prep_attack + rotate: false + xy: 2, 2 + size: 58, 58 + orig: 58, 58 + offset: 0, 0 + index: 1 +prep_attack + rotate: false + xy: 242, 2 + size: 58, 58 + orig: 58, 58 + offset: 0, 0 + index: 0 +prep_attack + rotate: false + xy: 482, 2 + size: 58, 58 + orig: 58, 58 + offset: 0, 0 + index: 2 diff --git a/source/core/assets/images/towers/fire_tower_atlas.png b/source/core/assets/images/towers/fire_tower_atlas.png new file mode 100644 index 000000000..a8c5cc3ee Binary files /dev/null and b/source/core/assets/images/towers/fire_tower_atlas.png differ diff --git a/source/core/assets/images/towers/stun_tower.atlas b/source/core/assets/images/towers/stun_tower.atlas new file mode 100644 index 000000000..f0033a197 --- /dev/null +++ b/source/core/assets/images/towers/stun_tower.atlas @@ -0,0 +1,118 @@ + +stun_tower.png +size: 1024, 64 +format: RGBA8888 +filter: Nearest, Nearest +repeat: none +attack + rotate: false + xy: 2, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 4 +attack + rotate: false + xy: 116, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 9 +attack + rotate: false + xy: 173, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 1 +attack + rotate: false + xy: 230, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 6 +attack + rotate: false + xy: 344, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 3 +attack + rotate: false + xy: 458, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 8 +attack + rotate: false + xy: 572, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 0 +attack + rotate: false + xy: 629, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 5 +attack + rotate: false + xy: 743, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 2 +attack + rotate: false + xy: 800, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 7 +idle + rotate: false + xy: 59, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 1 +idle + rotate: false + xy: 287, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 3 +idle + rotate: false + xy: 401, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 0 +idle + rotate: false + xy: 515, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 5 +idle + rotate: false + xy: 686, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 2 +idle + rotate: false + xy: 857, 2 + size: 55, 45 + orig: 55, 45 + offset: 0, 0 + index: 4 diff --git a/source/core/assets/images/towers/stun_tower.png b/source/core/assets/images/towers/stun_tower.png new file mode 100644 index 000000000..025b35a4c Binary files /dev/null and b/source/core/assets/images/towers/stun_tower.png differ diff --git a/source/core/assets/sounds/projectiles/explosion.mp3 b/source/core/assets/sounds/projectiles/explosion.mp3 new file mode 100644 index 000000000..d07beaf97 Binary files /dev/null and b/source/core/assets/sounds/projectiles/explosion.mp3 differ diff --git a/source/core/assets/sounds/projectiles/on_collision.mp3 b/source/core/assets/sounds/projectiles/on_collision.mp3 new file mode 100644 index 000000000..af209d670 Binary files /dev/null and b/source/core/assets/sounds/projectiles/on_collision.mp3 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 0006c54f2..d45c99dda 100644 --- a/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java +++ b/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java @@ -4,9 +4,11 @@ import com.badlogic.gdx.math.GridPoint2; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2; +import com.csse3200.game.components.CombatStatsComponent; import com.csse3200.game.components.ProjectileEffects; import com.csse3200.game.areas.terrain.TerrainFactory; import com.csse3200.game.areas.terrain.TerrainFactory.TerrainType; +import com.csse3200.game.components.player.PlayerStatsDisplay; import com.csse3200.game.entities.Entity; import com.csse3200.game.entities.factories.*; import com.csse3200.game.physics.PhysicsLayer; @@ -17,6 +19,8 @@ import com.csse3200.game.components.gamearea.GameAreaDisplay; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import java.util.Random; import java.util.Timer; @@ -28,6 +32,8 @@ public class ForestGameArea extends GameArea { private static final Logger logger = LoggerFactory.getLogger(ForestGameArea.class); + // Counts the number of humans left, if this reaches zero, game over. + private int endStateCounter = 2; private static final int NUM_BUILDINGS = 4; private static final int NUM_WALLS = 7; @@ -38,10 +44,10 @@ public class ForestGameArea extends GameArea { private static final int NUM_BOSS=4; + private Timer bossSpawnTimer; private int bossSpawnInterval = 10000; // 1 minute in milliseconds - private static final int NUM_WEAPON_TOWERS = 3; private static final GridPoint2 PLAYER_SPAWN = new GridPoint2(0, 0); // Temporary spawn point for testing @@ -52,7 +58,6 @@ public class ForestGameArea extends GameArea { // Required to load assets before using them private static final String[] forestTextures = { "images/ingamebg.png", - "images/projectiles/projectile.png", "images/box_boy_leaf.png", "images/background/building1.png", "images/ghost_1.png", @@ -72,6 +77,8 @@ public class ForestGameArea extends GameArea { "images/towers/turret.png", "images/towers/turret01.png", "images/towers/turret_deployed.png", + "images/towers/fire_tower_atlas.png", + "images/towers/stun_tower.png", "images/background/building2.png", "images/mobs/robot.png", "images/mobs/Attack_1.png", @@ -97,7 +104,16 @@ public class ForestGameArea extends GameArea { "images/towers/mine_tower.png", - "images/towers/TNTTower.png" + "images/towers/TNTTower.png", + + "images/towers/DroidTower.png", + "images/projectiles/basic_projectile.png", + "images/projectiles/mobProjectile.png", + "images/projectiles/engineer_projectile.png", + "images/projectiles/mobKing_projectile.png", + "images/projectiles/snow_ball.png" + + }; private static final String[] forestTextureAtlases = { "images/economy/econ-tower.atlas", @@ -107,11 +123,20 @@ public class ForestGameArea extends GameArea { "images/towers/turret.atlas", "images/towers/turret01.atlas", "images/mobs/xenoGrunt.atlas", + "images/towers/fire_tower_atlas.atlas", + "images/towers/stun_tower.atlas", + "images/mobs/robot.atlas", + "images/mobs/rangeBossRight.atlas", + "images/towers/DroidTower.atlas", "images/mobs/robot.atlas", "images/mobs/rangeBossRight.atlas", "images/towers/TNTTower.atlas", "images/projectiles/basic_projectile.atlas", - "images/projectiles/mobProjectile.atlas" + "images/projectiles/mobProjectile.atlas", + "images/projectiles/engineer_projectile.atlas", + "images/projectiles/mobKing_projectile.atlas", + "images/projectiles/snow_ball.atlas" + }; private static final String[] forestSounds = { "sounds/Impact4.ogg", @@ -119,7 +144,9 @@ public class ForestGameArea extends GameArea { "sounds/towers/deploy.mp3", "sounds/towers/stow.mp3", "sounds/engineers/firing_auto.mp3", - "sounds/engineers/firing_single.mp3" + "sounds/engineers/firing_single.mp3", + "sounds/projectiles/on_collision.mp3", + "sounds/projectiles/explosion.mp3" }; private static final String backgroundMusic = "sounds/background/Sci-Fi1.ogg"; private static final String[] forestMusic = {backgroundMusic}; @@ -154,21 +181,35 @@ public void create() { displayUI(); spawnTerrain(); + // Set up infrastructure for end game tracking + player = spawnPlayer(); player.getEvents().addListener("spawnWave", this::spawnXenoGrunts); playMusic(); // Types of projectile - spawnEffectProjectile(new Vector2(0, 10), PhysicsLayer.HUMANS, towardsMobs, new Vector2(2f, 2f), ProjectileEffects.BURN, true); + + spawnPierceFireBall(new Vector2(2, 3), PhysicsLayer.NPC, towardsMobs, new Vector2(2f, 2f)); + 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(); spawnGhosts(); spawnWeaponTower(); - spawnEngineer(); - bossKing1 = spawnBossKing1(); - bossKing2 = spawnBossKing2(); spawnTNTTower(); + spawnDroidTower(); + spawnGapScanners(); + spawnIncome(); +// bossKing1 = spawnBossKing1(); + bossKing2 = spawnBossKing2(); + + + } private void displayUI() { @@ -189,34 +230,31 @@ private void spawnTerrain() { Vector2 worldBounds = new Vector2(tileBounds.x * tileSize, tileBounds.y * tileSize); // Left + // ! THIS ONE DOESNT WORK. GRIDPOINTS2UTIL.ZERO is (0, 4), not (0, 0) + // spawnEntityAt( + // ObstacleFactory.createWall(WALL_WIDTH, worldBounds.y), GridPoint2Utils.ZERO, false, false); spawnEntityAt( - ObstacleFactory.createWall(WALL_WIDTH, worldBounds.y), GridPoint2Utils.ZERO, false, false); + ObstacleFactory.createWall(WALL_WIDTH, worldBounds.y), new GridPoint2(1, 0), false, false); // Right spawnEntityAt( ObstacleFactory.createWall(WALL_WIDTH, worldBounds.y), - new GridPoint2(tileBounds.x, 0), + new GridPoint2(tileBounds.x , 0), false, false); // Top spawnEntityAt( - ObstacleFactory.createWall(worldBounds.x, WALL_WIDTH), - new GridPoint2(0, tileBounds.y), + ObstacleFactory.createWall(worldBounds.x, WALL_WIDTH * 7), + new GridPoint2(0, tileBounds.y - 1), false, false); // Bottom + // spawnEntityAt( + // ObstacleFactory.createWall(worldBounds.x, WALL_WIDTH), GridPoint2Utils.ZERO, false, false); + // * TMPORARY spawnEntityAt( - ObstacleFactory.createWall(worldBounds.x, WALL_WIDTH), GridPoint2Utils.ZERO, false, false); + ObstacleFactory.createWall(worldBounds.x, WALL_WIDTH * 7), new GridPoint2(0, 0), false, false); } - private void spawnBuilding1() { - GridPoint2 minPos = new GridPoint2(0, 0); - GridPoint2 maxPos = terrain.getMapBounds(0).sub(2, 2); - for (int i = 0; i < NUM_BUILDINGS; i++) { - GridPoint2 randomPos = RandomUtils.random(minPos, maxPos); - Entity building1 = ObstacleFactory.createBuilding1(); - spawnEntityAt(building1, randomPos, true, false); - } - } private void spawnBuilding2() { GridPoint2 minPos = new GridPoint2(0, 0); GridPoint2 maxPos = terrain.getMapBounds(0).sub(2, 2); @@ -238,6 +276,7 @@ private void spawnMountains() { } } + private Entity spawnPlayer() { Entity newPlayer = PlayerFactory.createPlayer(); spawnEntityAt(newPlayer, PLAYER_SPAWN, true, true); @@ -267,7 +306,7 @@ private void spawnGhosts() { private Entity spawnBossKing1() { GridPoint2 minPos = new GridPoint2(0, 0); GridPoint2 maxPos = terrain.getMapBounds(0).sub(2, 2); - GridPoint2 randomPos + GridPoint2 randomPos = new GridPoint2(0, 0); Entity ghostKing = NPCFactory.createGhostKing(player); spawnEntityAt(ghostKing, randomPos, true, true); @@ -297,8 +336,8 @@ private void spawnProjectile(Vector2 position, short targetLayer, int direction, * @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); + private void spawnProjectileTest(Vector2 position, short targetLayer, int direction, Vector2 speed) { + Entity Projectile = ProjectileFactory.createEngineerBullet(targetLayer, new Vector2(direction, position.y), speed); Projectile.setPosition(position); spawnEntity(Projectile); } @@ -336,28 +375,28 @@ private void spawnProjectile(Vector2 position, short targetLayer, int space, in private void spawnXenoGrunts() { - GridPoint2 minPos = terrain.getMapBounds(0).sub(1, 0); - GridPoint2 maxPos = terrain.getMapBounds(0).sub(1, 10); + int[] pickedLanes = new Random().ints(1, 7) + .distinct().limit(5).toArray(); for (int i = 0; i < NUM_GRUNTS; i++) { - GridPoint2 randomPos = RandomUtils.random(maxPos, minPos); + GridPoint2 randomPos = new GridPoint2(19, pickedLanes[i]); System.out.println(randomPos); Entity xenoGrunt = NPCFactory.createXenoGrunt(player); xenoGrunt.setScale(1.5f, 1.5f); - spawnEntityAt(xenoGrunt, randomPos, true, true); + spawnEntityAt(xenoGrunt, randomPos, true, false); } } - private Entity spawnGhostKing() { - GridPoint2 minPos = new GridPoint2(0, 0); - GridPoint2 maxPos = terrain.getMapBounds(0).sub(0, 0); - GridPoint2 randomPos - = RandomUtils.random(minPos, maxPos); - // = new GridPoint2(26, 26); - Entity ghostKing = NPCFactory.createGhostKing(player); - spawnEntityAt(ghostKing, randomPos, true, true); - return ghostKing; - - } +// private Entity spawnGhostKing() { +// GridPoint2 minPos = new GridPoint2(0, 0); +// GridPoint2 maxPos = terrain.getMapBounds(0).sub(0, 0); +// GridPoint2 randomPos +// = RandomUtils.random(minPos, maxPos); +// // = new GridPoint2(26, 26); +// Entity ghostKing = NPCFactory.createGhostKing(player); +// spawnEntityAt(ghostKing, randomPos, true, true); +// return ghostKing; +// +// } private Entity spawnBossKing2() { GridPoint2 minPos = new GridPoint2(0, 0); @@ -387,6 +426,7 @@ private Entity spawnBossKing2() { * @param speed The speed of the projectiles. * @param quantity The amount of projectiles to spawn. */ + 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++) { @@ -395,6 +435,7 @@ private void spawnMultiProjectile(Vector2 position, short targetLayer, int direc } } + /** * Returns projectile that can do an area of effect damage * @@ -412,16 +453,70 @@ private void spawnEffectProjectile(Vector2 position, short targetLayer, int dire spawnEntity(Projectile); } + /** + * Spawns a pierce fireball. + * Pierce fireball can go through targetlayers without disappearing but damage + * will still be applied. + * + * @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 spawnPierceFireBall(Vector2 position, short targetLayer, int direction, Vector2 speed) { + Entity projectile = ProjectileFactory.createPierceFireBall(targetLayer, new Vector2(direction, position.y), speed); + projectile.setPosition(position); + spawnEntity(projectile); + } + + /** + * Spawns a ricochet fireball + * Ricochet fireballs bounce off targets with a specified maximum count of 3 + * Possible extensions: Make the bounce count flexible with a param. + * + * @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 spawnRicochetFireball(Vector2 position, short targetLayer, int direction, Vector2 speed) { + // Bounce count set to 0. + Entity projectile = ProjectileFactory.createRicochetFireball(targetLayer, new Vector2(direction, position.y), speed, 0); + projectile.setPosition(position); + spawnEntity(projectile); + } + + /** + * Spawns a split firework fireball. + * Splits into mini projectiles that spreads out after collision. + * + * @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. + * @param amount The amount of projectiles appearing after collision. + */ + private void spawnSplitFireWorksFireBall(Vector2 position, short targetLayer, int direction, Vector2 speed, int amount) { + Entity projectile = ProjectileFactory.createSplitFireWorksFireball(targetLayer, new Vector2(direction, position.y), speed, amount); + projectile.setPosition(position); + spawnEntity(projectile); + } + private void spawnWeaponTower() { GridPoint2 minPos = new GridPoint2(0, 0); GridPoint2 maxPos = terrain.getMapBounds(0).sub(2, 2); for (int i = 0; i < NUM_WEAPON_TOWERS; i++) { - GridPoint2 randomPos = RandomUtils.random(minPos, maxPos); - Entity weaponTower = TowerFactory.createWeaponTower(); + GridPoint2 randomPos1 = RandomUtils.random(minPos, maxPos); + GridPoint2 randomPos2 = RandomUtils.random(minPos, maxPos); + //Entity weaponTower = TowerFactory.createWeaponTower(); Entity wallTower = TowerFactory.createWallTower(); - spawnEntityAt(weaponTower, randomPos, true, true); - spawnEntityAt(wallTower, new GridPoint2(randomPos.x + 3, randomPos.y), true, true); + Entity fireTower = TowerFactory.createFireTower(); + Entity stunTower = TowerFactory.createStunTower(); + //spawnEntityAt(weaponTower, randomPos, true, true); + spawnEntityAt(fireTower, randomPos1, true, true); + spawnEntityAt(stunTower, randomPos2, true, true); + //spawnEntityAt(wallTower, new GridPoint2(randomPos1.x + 3, randomPos1.y), true, true); } } @@ -437,6 +532,18 @@ private void spawnTNTTower() { } + private void spawnDroidTower() { + GridPoint2 minPos = new GridPoint2(0, 0); + GridPoint2 maxPos = terrain.getMapBounds(0).sub(2, 2); + + for (int i = 0; i < NUM_WEAPON_TOWERS; i++) { + GridPoint2 randomPos = RandomUtils.random(minPos, maxPos); + Entity weaponTower = TowerFactory.createDroidTower(); + spawnEntityAt(weaponTower, randomPos, true, true); + } + + } + private void playMusic() { Music music = ServiceLocator.getResourceService().getAsset(backgroundMusic, Music.class); @@ -492,22 +599,22 @@ private void spawnScrap() { } } + private void spawnIncome() { GridPoint2 minPos = new GridPoint2(0, 0); GridPoint2 maxPos = terrain.getMapBounds(0).sub(2, 2); - for (int i = 0; i < 50; i++) { + for (int i = 0; i < 2; i++) { GridPoint2 randomPos = RandomUtils.random(minPos, maxPos); Entity towerfactory = TowerFactory.createIncomeTower(); spawnEntityAt(towerfactory, randomPos, true, true); } } - - private void spawnEngineer() { - for (int i = 0; i < terrain.getMapBounds(0).x; i += 3) { - Entity engineer = EngineerFactory.createEngineer(); - spawnEntityAt(engineer, new GridPoint2(1, i), true, true); + private void spawnGapScanners() { + for (int i = 0; i < terrain.getMapBounds(0).y; i++) { + Entity scanner = GapScannerFactory.createScanner(); + spawnEntityAt(scanner, new GridPoint2(0, i), true, true); } // GridPoint2 minPos = new GridPoint2(0, 0); // GridPoint2 maxPos = new GridPoint2(5, terrain.getMapBounds(0).sub(2, 2).y); @@ -516,4 +623,23 @@ private void spawnEngineer() { // Entity engineer = EngineerFactory.createEngineer(); // spawnEntityAt(engineer, randomPos, true, true); } + +// private void gameTrackerStart() { +// Entity endGameTracker = new Entity(); +// +// endGameTracker +// .addComponent(new CombatStatsComponent(2, 0)) +// .addComponent(new PlayerStatsDisplay()); +//// .getEvents().addListener("engineerKilled" , this::decrementCounter); +// endGameTracker.create(); +// } +// +// private void decrementCounter() { +// this.endStateCounter -= 1; +// logger.info("Engineer killed"); +// if (endStateCounter <= 0) { +// // we've reached the end, game over +// this.dispose(); +// } +// } } \ No newline at end of file diff --git a/source/core/src/main/com/csse3200/game/areas/terrain/TerrainFactory.java b/source/core/src/main/com/csse3200/game/areas/terrain/TerrainFactory.java index 991a3589d..794878666 100644 --- a/source/core/src/main/com/csse3200/game/areas/terrain/TerrainFactory.java +++ b/source/core/src/main/com/csse3200/game/areas/terrain/TerrainFactory.java @@ -152,6 +152,111 @@ private void fillInvisibleTiles(TiledMapTileLayer layer, GridPoint2 mapSize) { } } +//tile class + public static class Tile { + private int row; + private int col; + private Object object; + + public Tile(int row, int col) { + this.row = row; + this.col = col; + this.object = null; // Initially, no object is placed on the tile + } + + public void setObject(Object object) { + this.object = object; + } + + public Object getObject() { + return object; + } + + public String getLogCoordinates() { + return "(" + row + ", " + col + ")"; + } + } + +// grid class +public static class Grid { + private Tile[][] tiles; + + public Grid(int numRows, int numCols) { + tiles = new Tile[numRows][numCols]; + + for (int row = 0; row < numRows; row++) { + for (int col = 0; col < numCols; col++) { + tiles[row][col] = new Tile(row,col); + } + } + } + + public void placeObject(int row, int col, Object object) { + if (isValidCoordinate(row, col)) { + tiles[row][col].setObject(object); + } else { + System.out.println("Invalid coordinates."); + } + } + + public Object getObject(int row, int col) { + if (isValidCoordinate(row, col)) { + return tiles[row][col].getObject(); + } else { + System.out.println("Invalid coordinates."); + return null; + } + } + + public String getLogCoordinates(int row, int col) { + if (isValidCoordinate(row, col)) { + return tiles[row][col].getLogCoordinates(); + } else { + return "Invalid coordinates."; + } + } + + private boolean isValidCoordinate(int row, int col) { + return row >= 0 && row < tiles.length && col >= 0 && col < tiles[0].length; + } + + public void placeEntity(int row, int col, Object existingEntity) { + } + + public Object getEntity(int row, int col) { + return null; + } +} + +// Array class 1+2 +public class Array { + public static void main(String[] args) { + int numRows = 8; + int numCols = 20; + + Grid grid = new Grid(numRows, numCols); + + // Place an existing entity in a specific tile + int row = 3; + int col = 5; + // Replace 'Object' with the type of existing entity you want to place + Object existingEntity = new YourExistingEntity(); + + grid.placeEntity(row, col, existingEntity); + + // Get the entity from a tile + Object entity = grid.getEntity(row, col); + System.out.println("Entity at " + grid.getLogCoordinates(row, col) + ": " + entity); + } + + private static class YourExistingEntity { + } +} + + + + + private static void fillTiles(TiledMapTileLayer layer, GridPoint2 mapSize, TerrainTile tile) { BitmapFont font = new BitmapFont(); TextButton.TextButtonStyle textButtonStyle = new TextButton.TextButtonStyle(); diff --git a/source/core/src/main/com/csse3200/game/components/CombatStatsComponent.java b/source/core/src/main/com/csse3200/game/components/CombatStatsComponent.java index 0aae20c67..77e5a224c 100644 --- a/source/core/src/main/com/csse3200/game/components/CombatStatsComponent.java +++ b/source/core/src/main/com/csse3200/game/components/CombatStatsComponent.java @@ -31,7 +31,7 @@ public class CombatStatsComponent extends Component { private static final Logger logger = LoggerFactory.getLogger(CombatStatsComponent.class); private int health; private int baseAttack; - private final int fullHealth; + private int fullHealth; private String state; private ArrayList drops; private ArrayList closeRangeAbilities; @@ -102,6 +102,25 @@ public void addHealth(int health) { changeState(); } + /** + * Returns the entity's fullHealth value (note that this does not influence the ability to set its actual health) + * + * @return The entity's fullHealth variable + */ + public int getMaxHealth() { + return fullHealth; + } + + /** + * Sets the entity's fullHealth variable. + * Intended for when the entity's maximum health must be changed after creation, like upgrading a turret's HP. + * + * @param newMaxHealth The new value fullHealth should be set to + */ + public void setMaxHealth(int newMaxHealth) { + fullHealth = newMaxHealth; + } + /** * Returns the entity's base attack damage. * diff --git a/source/core/src/main/com/csse3200/game/components/DeleteOnMapEdgeComponent.java b/source/core/src/main/com/csse3200/game/components/DeleteOnMapEdgeComponent.java new file mode 100644 index 000000000..4359c118d --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/DeleteOnMapEdgeComponent.java @@ -0,0 +1,34 @@ +package com.csse3200.game.components; + +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.physics.box2d.Fixture; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.physics.BodyUserData; +import com.csse3200.game.physics.PhysicsLayer; + +/** + * Entities with this component will self destruct after hitting the grid edge + * upon collision. + */ +public class DeleteOnMapEdgeComponent extends Component { + + @Override + public void create() { + entity.getEvents().addListener("collisionEnd", this::onCollisionEnd); + } + + private void onCollisionEnd(Fixture me, Fixture other) { + Entity selfEntity = ((BodyUserData) me.getBody().getUserData()).entity; + + // * Should change the PhysicLayer to WALL / BOUNDARIES when established + if (!PhysicsLayer.contains(PhysicsLayer.WALL, other.getFilterData().categoryBits)) + return; + + Vector2 position = selfEntity.getPosition(); + + if (position.x <= 1 || position.x >= 18 || position.y < 0 || position.y >= 6.5) { + System.out.println("DELETION POSITION: " + position); + selfEntity.setFlagForDelete(true); + } + } +} 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 036f68592..843d65288 100644 --- a/source/core/src/main/com/csse3200/game/components/EffectsComponent.java +++ b/source/core/src/main/com/csse3200/game/components/EffectsComponent.java @@ -1,26 +1,34 @@ package com.csse3200.game.components; +import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.physics.box2d.Fixture; 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; import com.badlogic.gdx.utils.Timer; +import com.badlogic.gdx.utils.Timer.Task; import com.badlogic.gdx.utils.Array; -import java.util.ArrayList; - +/** + * This component applies an effect from the ProjectileEffects enum. This consists of fireball, burn, + * slow, and stun. Component also handles the targeting of specific layers and an area of effect + * application of effects. + */ public class EffectsComponent extends Component { private final float radius; private final ProjectileEffects effect; private final boolean aoe; private HitboxComponent hitboxComponent; private final short targetLayer; - private ArrayList burnEntities = new ArrayList<>(); + private Array burnEntities = new Array<>(); /** * Constructor for the AoEComponent. @@ -64,6 +72,7 @@ private void onCollisionEnd(Fixture me, Fixture other) { return; } + // Apply effect switch (effect) { case FIREBALL -> { if (aoe) { @@ -74,10 +83,16 @@ private void onCollisionEnd(Fixture me, Fixture other) { if (aoe) { applyAoeEffect(ProjectileEffects.BURN); } else { - applySingleEffect(ProjectileEffects.BURN, otherCombatStats); + applySingleEffect(ProjectileEffects.BURN, otherCombatStats, otherEntity); + } + } + case SLOW -> { + if (aoe) { + applyAoeEffect(ProjectileEffects.SLOW); + } else { + applySingleEffect(ProjectileEffects.SLOW, otherCombatStats, otherEntity); } } - case SLOW -> {} case STUN -> {} } } @@ -86,7 +101,7 @@ private void onCollisionEnd(Fixture me, Fixture other) { * Used for singe targeting projectiles to apply effects entity it collides with. * @param effect effect to be applied to entity */ - public void applySingleEffect(ProjectileEffects effect, CombatStatsComponent targetCombatStats) { + public void applySingleEffect(ProjectileEffects effect, CombatStatsComponent targetCombatStats, Entity targetEntity) { Entity hostEntity = getEntity(); CombatStatsComponent hostCombatStats = hostEntity.getComponent(CombatStatsComponent.class); @@ -95,12 +110,13 @@ public void applySingleEffect(ProjectileEffects effect, CombatStatsComponent tar return; } + // Apply effect switch (effect) { case FIREBALL -> {} case BURN -> { burnEffect(targetCombatStats, hostCombatStats); } - case SLOW -> {} + case SLOW -> {slowEffect(targetEntity);} case STUN -> {} } } @@ -119,31 +135,48 @@ public void applyAoeEffect(ProjectileEffects effect) { Array nearbyEntities = ServiceLocator.getEntityService().getNearbyEntities(hostEntity, radius); + // Iterate through nearby entities and apply effects for (int i = 0; i < nearbyEntities.size; i++) { Entity targetEntity = nearbyEntities.get(i); + + HitboxComponent targetHitbox = targetEntity.getComponent(HitboxComponent.class); + if (targetHitbox == null) { return; } + if (!PhysicsLayer.contains(targetLayer, targetHitbox.getLayer())) { + // Doesn't match our target layer, ignore + return; + } + CombatStatsComponent targetCombatStats = targetEntity.getComponent(CombatStatsComponent.class); if (targetCombatStats != null) { switch (effect) { - case FIREBALL -> { - fireballEffect(targetCombatStats, hostCombatStats); - } - case BURN -> { - burnEffect(targetCombatStats, hostCombatStats); - } - case SLOW -> {} + case FIREBALL -> {fireballEffect(targetCombatStats, hostCombatStats);} + case BURN -> {burnEffect(targetCombatStats, hostCombatStats);} + case SLOW -> {slowEffect(targetEntity);} case STUN -> {} } + } else { + return; } } } + /** + * Deals damage to target based on hosts' CombatStatsComponent + * @param target CombatStatsComponent of entity hit by projectile + * @param host CombatStatsComponent of projectile + */ private void fireballEffect(CombatStatsComponent target, CombatStatsComponent host) { target.hit(host); } + /** + * Applies 5 ticks of damage from hosts' CombatStatsComponent over 5 seconds + * @param target CombatStatsComponent of entity hit by projectile + * @param host CombatStatsComponent of projectile + */ private void burnEffect(CombatStatsComponent target, CombatStatsComponent host) { // Ensure burn effects aren't applied multiple times by same projectile - if (burnEntities.contains(target)) { + if (burnEntities.contains(target, false)) { return; } burnEntities.add(target); @@ -166,4 +199,57 @@ public void run() { } }, delay, delay); } + + /** + * Applies slow effect to targetEntity. If entity is a mob, speed + * and firing rate will be slowed. If entity is a tower, firing rate + * will be slowed + * @param targetEntity Entity for slow effect to be applied to + */ + private void slowEffect(Entity targetEntity) { + boolean towerFlag = false; + boolean mobFlag = false; + + PhysicsMovementComponent targetPhysics = null; + float xSpeed = 0; + float ySpeed = 0; + + // Create a timer task to apply the effect repeatedly + if (PhysicsLayer.contains(PhysicsLayer.HUMANS, targetEntity.getComponent(HitboxComponent.class).getLayer())) { + // towers + towerFlag = true; + //targetEntity.getEvents().trigger("upgradeTower", TowerUpgraderComponent.UPGRADE.FIRERATE, -30); + } else if (PhysicsLayer.contains(PhysicsLayer.NPC, targetEntity.getComponent(HitboxComponent.class).getLayer())) { + // mobs + mobFlag = true; + targetPhysics = targetEntity.getComponent(PhysicsMovementComponent.class); + if (targetPhysics == null) { + return; + } + + // Halve the mob speed + xSpeed = targetPhysics.getSpeed().x; + ySpeed = targetPhysics.getSpeed().y; + targetPhysics.setSpeed(new Vector2(xSpeed/2, ySpeed/2)); + } else { + return; + } + + // Reset speed + boolean finalTowerFlag = towerFlag; + boolean finalMobFlag = mobFlag; + PhysicsMovementComponent finalTargetPhysics = targetPhysics; + float finalXSpeed = xSpeed; + float finalYSpeed = ySpeed; + Timer.schedule(new Task() { + @Override + public void run() { + if (finalTowerFlag) { + //targetEntity.getEvents().trigger("upgradeTower", TowerUpgraderComponent.UPGRADE.FIRERATE, 30); + } else if (finalMobFlag) { + finalTargetPhysics.setSpeed(new Vector2(finalXSpeed, finalYSpeed)); + } + } + }, 5); // 5 seconds delay + } } diff --git a/source/core/src/main/com/csse3200/game/components/RicochetComponent.java b/source/core/src/main/com/csse3200/game/components/RicochetComponent.java index f2732375e..4203dfa6a 100644 --- a/source/core/src/main/com/csse3200/game/components/RicochetComponent.java +++ b/source/core/src/main/com/csse3200/game/components/RicochetComponent.java @@ -1,29 +1,35 @@ package com.csse3200.game.components; - import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.physics.box2d.Body; import com.badlogic.gdx.physics.box2d.Fixture; -import com.csse3200.game.ai.tasks.AITaskComponent; -import com.csse3200.game.components.tasks.TrajectTask; 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.HitboxComponent; -import com.csse3200.game.physics.components.PhysicsComponent; import com.csse3200.game.services.ServiceLocator; /** * Ricochet based on target layers. - * Basically a bouncing effect that continues to bounce off entities - * Possible extensions: Have a limitied amount of bounce until self-destruct. + * A bouncing effect that continues to bounce off desired entities. */ public class RicochetComponent extends Component { private short targetLayer; private HitboxComponent hitBoxComponent; + private int bounceCount; + private static int MAX_BOUNCE_Y_DIRECTION = 250; + private static int MIN_BOUNCE_Y_DIRECTION = -250; - public RicochetComponent(short targetLayer) { + /** + * Initialise a RicochetComponent that spawns another projectile upon collision. + * Projectile has a chance to head upwards or downwards and upon spawning, + * it will be slighlty up or down in respect to original disappearance. + * @param targetLayer Target layer upon collision + * @param bounceCount Keeps track of the bounce count upon initial collision + * Stops self-spawning when bounce count is greater or equal than two. + */ + public RicochetComponent(short targetLayer, int bounceCount) { this.targetLayer = targetLayer; + this.bounceCount = bounceCount; } @Override @@ -31,26 +37,45 @@ public void create() { entity.getEvents().addListener("collisionEnd", this::onCollisionEnd); hitBoxComponent = entity.getComponent(HitboxComponent.class); } - + /** - * After collision ends, make another fireball that spawns just before the original one. This assumes - * that the original fireball is already deleted. Set TouchAttackComponent disposeOnHit to true. + * After collision ends, make another fireball that spawns just before the + * original one. This assumes + * that the original fireball is already deleted. Set TouchAttackComponent + * disposeOnHit to true. + * * @param me * @param other */ private void onCollisionEnd(Fixture me, Fixture other) { - if(hitBoxComponent.getFixture() != me) return; - - if(!PhysicsLayer.contains(targetLayer, other.getFilterData().categoryBits)) return; - + if (hitBoxComponent.getFixture() != me + || !PhysicsLayer.contains(targetLayer, other.getFilterData().categoryBits) + || bounceCount >= 3) // BounceCount base case of 3 + return; + Entity projectile = ((BodyUserData) me.getBody().getUserData()).entity; - Entity newProjectile = ProjectileFactory.createRicochetFireball(PhysicsLayer.NPC, new Vector2(100, projectile.getPosition().y + getRandomNumFrom(-250, 250)), new Vector2(2f, 2f)); - newProjectile.setPosition((float) (projectile.getPosition().x -1.5), (float) (projectile.getPosition().y)); + // Projectile heads upwards or downwards. + int randomDirection = getRandomNumFrom(MIN_BOUNCE_Y_DIRECTION, MAX_BOUNCE_Y_DIRECTION); + + // Spawning of the projectile to be above (+ve) or below (-ve) upon + // collision + int up_or_down = randomDirection <= 0 ? -1 : 1; + + float newXPosition = (float) (projectile.getPosition().x - 0.75); + float newYPosition = (float) (projectile.getPosition().y + (0.65 * up_or_down)); + + // Prevent spawn of new projectile if it goes out of boundaries. + if (newYPosition >= 8 || newYPosition <= 1 || newXPosition >= 17 || newXPosition <= 1) + return; + + // * RIGHT NOW TARGET IS NPC, SUBJECT TO CHANGE + Entity newProjectile = ProjectileFactory.createRicochetFireball(PhysicsLayer.NPC, + new Vector2(100, projectile.getPosition().y + randomDirection), new Vector2(2f, 2f), ++bounceCount); // Increment bounceCount + + newProjectile.setPosition(newXPosition, newYPosition); + newProjectile.setScale(0.75f, 0.75f); - // projectile.getComponent(AITaskComponent.class).addTask(new TrajectTask(new Vector2(100, projectile.getPosition().y + 50))); - - ServiceLocator.getEntityService().register(newProjectile); } diff --git a/source/core/src/main/com/csse3200/game/components/SplitFireworksComponent.java b/source/core/src/main/com/csse3200/game/components/SplitFireworksComponent.java new file mode 100644 index 000000000..a48f6595a --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/SplitFireworksComponent.java @@ -0,0 +1,71 @@ +package com.csse3200.game.components; + +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.physics.box2d.Fixture; +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.HitboxComponent; +import com.csse3200.game.services.ServiceLocator; + +/** + * A component that splits the projectile into multiple mini projectiles. + * Assumes projectile has a disposesOnHit functionality. + */ +public class SplitFireworksComponent extends Component { + private short targetLayer; + private HitboxComponent hitboxComponent; + private int amount; + private static int TOTAL_RANGE = 450; + + /** + * 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 + * 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. + */ + public SplitFireworksComponent(short targetLayer, int amount) { + this.targetLayer = targetLayer; + this.amount = amount; + } + + @Override + public void create() { + entity.getEvents().addListener("collisionEnd", this::onCollisionEnd); + hitboxComponent = entity.getComponent(HitboxComponent.class); + } + + private void onCollisionEnd(Fixture me, Fixture other) { + if (hitboxComponent.getFixture() != me + || !PhysicsLayer.contains(targetLayer, other.getFilterData().categoryBits) + || amount < 2) // Amount of split projectiles must be >= 2 + return; + + Entity projectile = ((BodyUserData) me.getBody().getUserData()).entity; + + for (int i = 0; i < amount; i++) { + int newDirection = (i * TOTAL_RANGE) / (amount - 1); + + // Boundaries + float newXPosition = (float) (projectile.getPosition().x + 1.75); + 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, + new Vector2(100, projectile.getPosition().y + (newDirection - (TOTAL_RANGE/2))), new Vector2(3f, 3f)); + + newProjectile.setPosition(newXPosition, (float) projectile.getPosition().y); + + newProjectile.setScale(0.5f, 0.5f); + + ServiceLocator.getEntityService().register(newProjectile); + } + } +} 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 01ed762a4..3a203d3d0 100644 --- a/source/core/src/main/com/csse3200/game/components/TouchAttackComponent.java +++ b/source/core/src/main/com/csse3200/game/components/TouchAttackComponent.java @@ -12,6 +12,7 @@ /** * When this entity touches a valid enemy's hitbox, deal damage to them and apply a knockback. + * Has an optional disposeOnHit property that disposes projectile upon collision. * *

Requires CombatStatsComponent, HitboxComponent on this entity. * @@ -64,7 +65,7 @@ public void create() { hitboxComponent = entity.getComponent(HitboxComponent.class); } - private void onCollisionStart(Fixture me, Fixture other) { + public void onCollisionStart(Fixture me, Fixture other) { if (hitboxComponent.getFixture() != me) { // Not triggered by hitbox, ignore return; @@ -104,6 +105,11 @@ private void onCollisionStart(Fixture me, Fixture other) { public void setDisposeOnHit(boolean disposeOnHit) { this.disposeOnHit = disposeOnHit; } + + + public void setKnockBack(float knockback) { + this.knockbackForce = knockback; + } public Weapon chooseWeapon(Fixture other) { Entity target = ((BodyUserData) other.getBody().getUserData()).entity; Weapon weapon = null; 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 d8eaa7be5..270f5afa8 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 @@ -27,38 +27,30 @@ public void create() { } void animateRun() { - if (!Objects.equals(animator.getCurrentAnimation(), "xeno_shoot")) { - animator.stopAnimation(); - animator.startAnimation("xeno_run"); - } + animator.startAnimation("xeno_run"); } void animateHurt() { - animator.stopAnimation(); animator.startAnimation("xeno_hurt"); } void animateShoot() { - animator.stopAnimation(); animator.startAnimation("xeno_shoot"); } void animateMelee1() { - animator.stopAnimation(); animator.startAnimation("xeno_melee_1"); } void animateMelee2() { - animator.stopAnimation(); animator.startAnimation("xeno_melee_2"); } void animateDie() { - animator.stopAnimation(); animator.startAnimation("xeno_die"); } void stopAnimation() { - animator.stopAnimation(); + animator.startAnimation("default"); } } diff --git a/source/core/src/main/com/csse3200/game/components/player/HumanAnimationController.java b/source/core/src/main/com/csse3200/game/components/player/HumanAnimationController.java index e65fc8763..c6231f29e 100644 --- a/source/core/src/main/com/csse3200/game/components/player/HumanAnimationController.java +++ b/source/core/src/main/com/csse3200/game/components/player/HumanAnimationController.java @@ -35,10 +35,10 @@ public class HumanAnimationController extends Component { private static final String FIRE_AUTO_SFX = "sounds/engineers/firing_auto.mp3"; private static final String FIRE_SINGLE_SFX = "sounds/engineers/firing_single.mp3"; - AnimationRenderComponent animator; - Sound fireAutoSound = ServiceLocator.getResourceService().getAsset( + private AnimationRenderComponent animator; + private final Sound fireAutoSound = ServiceLocator.getResourceService().getAsset( FIRE_AUTO_SFX, Sound.class); - Sound fireSingleSound = ServiceLocator.getResourceService().getAsset( + private final Sound fireSingleSound = ServiceLocator.getResourceService().getAsset( FIRE_SINGLE_SFX, Sound.class); /** @@ -56,7 +56,7 @@ public void create() { entity.getEvents().addListener(PREP, this::animatePrep); entity.getEvents().addListener(WALK_PREP, this::animatePrepWalk); entity.getEvents().addListener(FIRING_SINGLE, this::animateSingleFiring); - entity.getEvents().addListener(FIRING_AUTO, this::animateFiring); + entity.getEvents().addListener(FIRING_AUTO, this::animateFiringAuto); entity.getEvents().addListener(HIT, this::animateHit); entity.getEvents().addListener(DEATH, this::animateDeath); } @@ -110,7 +110,7 @@ void animateSingleFiring() { * Callback that starts the shoot animation in auto mode and plays the auto fire sound. * Currently unused, but intended to be incorporated as engineer functionality expands. */ - void animateFiring() { + void animateFiringAuto() { animator.startAnimation(FIRE_AUTO_ANIM); fireAutoSound.play(); } diff --git a/source/core/src/main/com/csse3200/game/components/projectile/EngineerBulletsAnimationController.java b/source/core/src/main/com/csse3200/game/components/projectile/EngineerBulletsAnimationController.java new file mode 100644 index 000000000..a9dc1a63f --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/projectile/EngineerBulletsAnimationController.java @@ -0,0 +1,29 @@ +package com.csse3200.game.components.projectile; + +import com.csse3200.game.components.Component; +import com.csse3200.game.rendering.AnimationRenderComponent; +import com.csse3200.game.services.ServiceLocator; //used for sound + +public class EngineerBulletsAnimationController extends Component{ + /** Event name constants */ + + AnimationRenderComponent animator; + + @Override + public void create() { + super.create(); + animator = this.entity.getComponent(AnimationRenderComponent.class); + entity.getEvents().addListener("startProjectile", this::animateStart); + entity.getEvents().addListener("startProjectileFinal", this::animateFinal); + + } + + void animateStart() { + animator.startAnimation("bullet"); + } + + void animateFinal() { + animator.startAnimation("bulletFinal"); + } +} + diff --git a/source/core/src/main/com/csse3200/game/components/projectile/ExplosionAnimationController.java b/source/core/src/main/com/csse3200/game/components/projectile/ExplosionAnimationController.java new file mode 100644 index 000000000..6fd9962e0 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/projectile/ExplosionAnimationController.java @@ -0,0 +1,44 @@ +package com.csse3200.game.components.projectile; + +import com.csse3200.game.components.Component; +import com.csse3200.game.rendering.AnimationRenderComponent; +import com.csse3200.game.services.ServiceLocator; +import com.badlogic.gdx.audio.Sound; + + +//can be used for aoe +public class ExplosionAnimationController extends Component { + /** Event name constants */ + private static final String START = "startExplosion"; + private static final String FINAL = "startExplosionFinal"; + + /** Animation name constants */ + private static final String START_ANIM = "explosion"; + private static final String FINAL_ANIM = "explosionFinal"; + /** Sound effects constant */ + private static final String FINAL_SFX = "sounds/projectiles/explosion.mp3"; + + AnimationRenderComponent animator; + + Sound explosionSound = ServiceLocator.getResourceService().getAsset( + FINAL_SFX, Sound.class); + + @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); + explosionSound.play(); + } + + void animateFinal() { + animator.startAnimation(FINAL_ANIM); + } + +} diff --git a/source/core/src/main/com/csse3200/game/components/projectile/MobKingProjectAnimController.java b/source/core/src/main/com/csse3200/game/components/projectile/MobKingProjectAnimController.java new file mode 100644 index 000000000..828daf597 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/projectile/MobKingProjectAnimController.java @@ -0,0 +1,32 @@ +package com.csse3200.game.components.projectile; + +import com.csse3200.game.components.Component; +import com.csse3200.game.rendering.AnimationRenderComponent; + +public class MobKingProjectAnimController extends Component { + /** Event names */ + private static final String START = "startMobKing"; + private static final String FINAL = "startMobKingFinal"; + + /** Animation name constants */ + private static final String START_ANIM = "mob_boss"; + private static final String FINAL_ANIM = "mob_bossFinal"; + + 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/MobProjectileAnimationController.java b/source/core/src/main/com/csse3200/game/components/projectile/MobProjectileAnimationController.java similarity index 88% rename from source/core/src/main/com/csse3200/game/components/MobProjectileAnimationController.java rename to source/core/src/main/com/csse3200/game/components/projectile/MobProjectileAnimationController.java index e1a55775e..e72fe612e 100644 --- a/source/core/src/main/com/csse3200/game/components/MobProjectileAnimationController.java +++ b/source/core/src/main/com/csse3200/game/components/projectile/MobProjectileAnimationController.java @@ -1,5 +1,6 @@ -package com.csse3200.game.components; +package com.csse3200.game.components.projectile; +import com.csse3200.game.components.Component; import com.csse3200.game.rendering.AnimationRenderComponent; diff --git a/source/core/src/main/com/csse3200/game/components/projectile/OnCollisionAnimationController.java b/source/core/src/main/com/csse3200/game/components/projectile/OnCollisionAnimationController.java new file mode 100644 index 000000000..7efbfa55d --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/projectile/OnCollisionAnimationController.java @@ -0,0 +1,23 @@ +package com.csse3200.game.components.projectile; + +import com.csse3200.game.components.Component; +import com.csse3200.game.rendering.AnimationRenderComponent; +import com.csse3200.game.services.ServiceLocator; +import com.badlogic.gdx.audio.Sound; + +public class OnCollisionAnimationController extends Component { + private static final String COLLISION_SFX = "sounds/projectiles/on_collision.mp3"; + private static final String PLAYSOUND = "collisionStart"; + Sound onCollisionSound = ServiceLocator.getResourceService().getAsset( + COLLISION_SFX, Sound.class); + + @Override + public void create() { + super.create(); + entity.getEvents().addListener(PLAYSOUND, this::animateCollision); + } + + void animateCollision() { + onCollisionSound.play(); + } +} 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 9c808a557..d39e91f46 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 @@ -2,7 +2,6 @@ import com.csse3200.game.components.Component; import com.csse3200.game.rendering.AnimationRenderComponent; -import com.csse3200.game.services.ServiceLocator; //used for sound public class ProjectileAnimationController extends Component{ /** Event name constants */ @@ -12,7 +11,6 @@ public class ProjectileAnimationController extends Component{ /** Animation name constants */ private static final String START_ANIM = "projectile"; private static final String FINAL_ANIM = "projectileFinal"; - AnimationRenderComponent animator; @Override diff --git a/source/core/src/main/com/csse3200/game/components/projectile/SnowBallProjectileAnimationController.java b/source/core/src/main/com/csse3200/game/components/projectile/SnowBallProjectileAnimationController.java new file mode 100644 index 000000000..adb1f2869 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/projectile/SnowBallProjectileAnimationController.java @@ -0,0 +1,31 @@ +package com.csse3200.game.components.projectile; + +import com.csse3200.game.components.Component; +import com.csse3200.game.rendering.AnimationRenderComponent; + +public class SnowBallProjectileAnimationController extends Component{ + 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/tasks/DroidCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/DroidCombatTask.java new file mode 100644 index 000000000..864f5a3f3 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tasks/DroidCombatTask.java @@ -0,0 +1,196 @@ +package com.csse3200.game.components.tasks; + +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.CombatStatsComponent; +import com.csse3200.game.physics.PhysicsEngine; +import com.csse3200.game.physics.PhysicsLayer; +import com.csse3200.game.physics.raycast.RaycastHit; +import com.csse3200.game.rendering.AnimationRenderComponent; +import com.csse3200.game.services.GameTime; +import com.csse3200.game.services.ServiceLocator; + +/** + * The DroidCombatTask runs the AI for the DroidTower class. The tower will scan for targets in a straight line + * from its center point until a point at (x + maxRange, y), where x,y are the cooridinates of the tower's center + * position. This component should be added to an AiTaskComponent attached to the tower instance. + */ +public class DroidCombatTask extends DefaultTask implements PriorityTask { + // Constants + 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 + 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 + private final int priority; // The active priority this task will have + private final float maxRange; + private Vector2 towerPosition = new Vector2(10, 10); // initial placeholder value - will be overwritten + private final Vector2 maxRangePosition = new Vector2(); + private PhysicsEngine physics; + private GameTime timeSource; + private long endTime; + private final RaycastHit hit = new RaycastHit(); + + public enum STATE { + IDLE, UP, DOWN, SHOOT_UP, SHOOT_DOWN, WALK, DIE + } + public STATE towerState = STATE.WALK; + + /** + * @param priority Task priority when targets are detected (0 when nothing detected). Must be a positive integer. + * @param maxRange Maximum effective range of the weapon tower. This determines the detection distance of targets + */ + public DroidCombatTask(int priority, float maxRange) { + this.priority = priority; + this.maxRange = maxRange; + physics = ServiceLocator.getPhysicsService().getPhysics(); + timeSource = ServiceLocator.getTimeSource(); + } + + /** + * Starts the Task running, triggers the initial "idleStart" event. + */ + @Override + public void start() { + super.start(); + // Set the tower's coordinates + this.towerPosition = owner.getEntity().getCenterPosition(); + this.maxRangePosition.set(towerPosition.x + maxRange, towerPosition.y); + // Default to idle mode + owner.getEntity().getEvents().trigger(WALK); + + endTime = timeSource.getTime() + (INTERVAL * 500); + } + + /** + * The update method is what is run every time the TaskRunner in the AiTaskComponent calls update(). + * triggers events depending on the presence or otherwise of targets in the detection range + */ + @Override + public void update() { + if (timeSource.getTime() >= endTime) { + updateTowerState(); + endTime = timeSource.getTime() + (INTERVAL * 1000); + } + } + + /** + * Droid tower state machine. Updates tower state by scanning for mobs, and + * triggers the appropriate events corresponding to the STATE enum. + */ + public void updateTowerState() { + // configure tower state depending on target visibility + if (owner.getEntity().getComponent(CombatStatsComponent.class).getHealth() <= 0 && towerState != STATE.DIE) { + owner.getEntity().getEvents().trigger(DEATH); + towerState = STATE.DIE; + return; + } + switch (towerState) { + case WALK -> { + owner.getEntity().getEvents().trigger(WALK); + towerState = STATE.IDLE; + } + 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); + } + } + case SHOOT_DOWN -> { + if (isTargetVisible()) { + owner.getEntity().getEvents().trigger(ATTACK_DOWN); + owner.getEntity().getEvents().trigger(SHOOT_DOWN); + towerState = STATE.UP; + } else { + owner.getEntity().getEvents().trigger(GO_UP); + towerState = STATE.UP; + } + } + case SHOOT_UP -> { + if (isTargetVisible()) { + + owner.getEntity().getEvents().trigger(ATTACK_UP); + owner.getEntity().getEvents().trigger(SHOOT_UP); + towerState = STATE.DOWN; + } else { + owner.getEntity().getEvents().trigger(IDLE); + towerState = STATE.IDLE; + } + } + case DOWN -> { + if (isTargetVisible()) { + owner.getEntity().getEvents().trigger(GO_DOWN); + towerState = STATE.SHOOT_DOWN; + } else { + owner.getEntity().getEvents().trigger(IDLE); + towerState = STATE.IDLE; + } + } + case UP -> { + if (isTargetVisible()) { + owner.getEntity().getEvents().trigger(GO_UP); + towerState = STATE.SHOOT_UP; + } else { + owner.getEntity().getEvents().trigger(GO_UP); + towerState = STATE.IDLE; + + + } + } + case DIE -> { + if (owner.getEntity().getComponent(AnimationRenderComponent.class).isFinished()) + owner.getEntity().setFlagForDelete(true); + } + } + } + /** + * For stopping the running task + */ + @Override + public void stop() { + super.stop(); + } + + /** + * Returns the current state of the tower. + * + * @return the current state of the tower. + */ + public STATE getState() { + return this.towerState; + } + + /** + * Returns the current priority of the task. + * @return active priority value if targets detected, inactive priority otherwise + */ + @Override + public int getPriority() { + return isTargetVisible() ? priority : 0; + } + + /** + * Uses a raycast to determine whether there are any targets in detection range + * @return true if a target is visible, false otherwise + */ + 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/FireTowerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/FireTowerCombatTask.java new file mode 100644 index 000000000..3ac568f1b --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tasks/FireTowerCombatTask.java @@ -0,0 +1,154 @@ +package com.csse3200.game.components.tasks; + +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.factories.ProjectileFactory; +import com.csse3200.game.physics.PhysicsEngine; +import com.csse3200.game.physics.PhysicsLayer; +import com.csse3200.game.physics.raycast.RaycastHit; +import com.csse3200.game.services.GameTime; +import com.csse3200.game.services.ServiceLocator; + +/** + * The FireTowerCombatTask runs the AI for the FireTower class. The tower implementing this task will scan for enemies + * in a straight line from the current position to a maxRange, and change the state of the tower. + */ +public class FireTowerCombatTask extends DefaultTask implements PriorityTask { + //constants + 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 this tower will detect + //The constants are names of events that will be triggered in the state machine + private static final String IDLE = "startIdle"; + private static final String PREP_ATTACK = "startAttackPrep"; + private static final String ATTACK = "startAttack"; + + //Class attributes + private final int priority; + private final float maxRange; + + private Vector2 towerPosition = new Vector2(10, 10); + private final Vector2 maxRangePosition = new Vector2(); + private PhysicsEngine physics; + private GameTime timeSource; + private long endTime; + private final RaycastHit hit = new RaycastHit(); + + private enum STATE { + IDLE, PREP_ATTACK, ATTACK + } + private STATE towerState = STATE.IDLE; + + /** + * Starts the task running, triggers the initial 'IDLE' event + */ + public FireTowerCombatTask(int priority, float maxRange) { + this.priority = priority; + this.maxRange = maxRange; + physics = ServiceLocator.getPhysicsService().getPhysics(); + timeSource = ServiceLocator.getTimeSource(); + } + + /** + * starts this task and triggers the IDLE animation + */ + @Override + public void start() { + super.start(); + // get the tower coordinates + this.towerPosition = owner.getEntity().getCenterPosition(); + this.maxRangePosition.set(towerPosition.x + maxRange, towerPosition.y); + //default to idle state + owner.getEntity().getEvents().trigger(IDLE); + + endTime = timeSource.getTime() + (INTERVAL * 500); + } + + /** + * this method is called everytime state of the tower needs to be changed. + */ + @Override + public void update() { + if (timeSource.getTime() >= endTime) { + updateTowerState(); + endTime = timeSource.getTime() + (INTERVAL * 1000); + } + } + + /** + * finite state machine for the FireTower. Detects mobs in a straight line and changes the state of the tower. + */ + public void updateTowerState() { + switch (towerState) { + case IDLE -> { + if (isTargetVisible()) { + owner.getEntity().getEvents().trigger(PREP_ATTACK); + towerState = STATE.PREP_ATTACK; + } + } + case PREP_ATTACK -> { + if (isTargetVisible()) { + owner.getEntity().getEvents().trigger(ATTACK); + towerState = STATE.ATTACK; + } else { + owner.getEntity().getEvents().trigger(IDLE); + towerState = STATE.IDLE; + } + } + case ATTACK -> { + if (!isTargetVisible()) { + owner.getEntity().getEvents().trigger(IDLE); + 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)); + newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.25), + (float) (owner.getEntity().getPosition().y + 0.25)); + ServiceLocator.getEntityService().register(newProjectile); + } + } + } + } + + /** + * stops the current animation. + */ + public void stop() { + super.stop(); + owner.getEntity().getEvents().trigger(IDLE); + } + + /** + * gets the priority for the current task. + * @return (int) active priority if target is visible and inactive priority otherwise + */ + public int getPriority() { + return !isTargetVisible() ? 0 : priority; + } + + /** + * not currently used. + * @return the priority for this task + */ + private int getActivePriority() { + return !isTargetVisible() ? 0 : priority; + } + + /** + * not currently used. + * @return + */ + private int getInactivePriority() { + return isTargetVisible() ? priority : 0; + } + + /** + * detects targets from the centre of the tower to maxRange in a straight line. + * @return true if mobs are present and false otherwise. + */ + public boolean isTargetVisible() { + return physics.raycast(towerPosition, maxRangePosition, TARGET, hit); + } +} diff --git a/source/core/src/main/com/csse3200/game/components/tasks/MobAttackTask.java b/source/core/src/main/com/csse3200/game/components/tasks/MobAttackTask.java index 703024c8d..fa6e41433 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/MobAttackTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/MobAttackTask.java @@ -1,23 +1,22 @@ package com.csse3200.game.components.tasks; +import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.physics.box2d.Fixture; 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.TouchAttackComponent; import com.csse3200.game.entities.Entity; -import com.badlogic.gdx.math.Vector2; import com.csse3200.game.entities.Melee; import com.csse3200.game.entities.Weapon; -import com.csse3200.game.physics.BodyUserData; +import com.csse3200.game.entities.factories.ProjectileFactory; import com.csse3200.game.physics.PhysicsEngine; import com.csse3200.game.physics.PhysicsLayer; +import com.csse3200.game.physics.components.HitboxComponent; import com.csse3200.game.physics.components.PhysicsMovementComponent; import com.csse3200.game.physics.raycast.RaycastHit; import com.csse3200.game.rendering.AnimationRenderComponent; -import com.csse3200.game.services.ServiceLocator; import com.csse3200.game.services.GameTime; -import com.csse3200.game.entities.factories.ProjectileFactory; +import com.csse3200.game.services.ServiceLocator; /** @@ -31,7 +30,9 @@ public class MobAttackTask extends DefaultTask implements PriorityTask { private static final String STOW = "wanderStart"; private static final String DEPLOY = "deployStart"; private static final String FIRING = "shootStart"; - private static final String IDLE = "idleStart"; + private static final String IDLE = "stop"; + + private Fixture target; private final int priority; private final float maxRange; @@ -75,7 +76,7 @@ public void start() { this.maxRangePosition.set(0, mobPosition.y); //owner.getEntity().getEvents().trigger(IDLE); endTime = timeSource.getTime() + (INTERVAL * 500); - owner.getEntity().getEvents().trigger("shootStart"); +// owner.getEntity().getEvents().trigger("shootStart"); } /** @@ -101,7 +102,7 @@ public void updateMobState() { case IDLE -> { if (isTargetVisible()) { // targets detected in idle mode - start deployment - owner.getEntity().getEvents().trigger(DEPLOY); +// owner.getEntity().getEvents().trigger(DEPLOY); mobState = STATE.DEPLOY; } } @@ -110,10 +111,10 @@ public void updateMobState() { // currently deploying, if (isTargetVisible() || this.meleeOrProjectile() != null) { owner.getEntity().getComponent(PhysicsMovementComponent.class).setEnabled(false); - owner.getEntity().getEvents().trigger(FIRING); + this.owner.getEntity().getEvents().trigger(FIRING); mobState = STATE.FIRING; } else { - owner.getEntity().getEvents().trigger(STOW); + this.owner.getEntity().getEvents().trigger(STOW); mobState = STATE.STOW; } } @@ -121,20 +122,25 @@ public void updateMobState() { case FIRING -> { // targets gone or cannot be attacked - stop firing if (!isTargetVisible() || this.meleeOrProjectile() == null) { - owner.getEntity().getEvents().trigger(STOW); + this.owner.getEntity().getEvents().trigger(STOW); mobState = STATE.STOW; } else { if (this.meleeOrProjectile() instanceof Melee) { - + System.out.println("Melee attack"); + TouchAttackComponent attackComp = owner.getEntity().getComponent(TouchAttackComponent.class); + HitboxComponent hitboxComp = owner.getEntity().getComponent(HitboxComponent.class); + attackComp.onCollisionStart(hitboxComp.getFixture(), target); + this.owner.getEntity().getEvents().trigger("meleeStart"); + } else { + Entity newProjectile = ProjectileFactory.createMobBall(PhysicsLayer.HUMANS, new Vector2(0, owner.getEntity().getPosition().y), new Vector2(2f,2f)); + newProjectile.setPosition((float) (owner.getEntity().getPosition().x), (float) (owner.getEntity().getPosition().y)); +// newProjectile.setScale(-1f, 0.5f); + ServiceLocator.getEntityService().register(newProjectile); + +// System.out.printf("ANIMATION: " + owner.getEntity().getComponent(AnimationRenderComponent.class).getCurrentAnimation() + "\n"); + this.owner.getEntity().getEvents().trigger(FIRING); + mobState = STATE.STOW; } - Entity newProjectile = ProjectileFactory.createMobBall(PhysicsLayer.HUMANS, new Vector2(0, owner.getEntity().getPosition().y), new Vector2(2f,2f)); - newProjectile.setPosition((float) (owner.getEntity().getPosition().x), (float) (owner.getEntity().getPosition().y)); - newProjectile.setScale(-1f, 0.5f); - ServiceLocator.getEntityService().register(newProjectile); - - System.out.printf("ANIMATION: " + owner.getEntity().getComponent(AnimationRenderComponent.class).getCurrentAnimation() + "\n"); - owner.getEntity().getEvents().trigger(FIRING); - mobState = STATE.STOW; } owner.getEntity().getComponent(PhysicsMovementComponent.class).setEnabled(true); @@ -143,7 +149,7 @@ public void updateMobState() { case STOW -> { // currently stowing if (isTargetVisible()) { - owner.getEntity().getEvents().trigger(DEPLOY); +// owner.getEntity().getEvents().trigger(DEPLOY); mobState = STATE.DEPLOY; } else { owner.getEntity().getEvents().trigger(IDLE); @@ -220,12 +226,13 @@ private boolean isTargetVisible() { * returns the Weapon (Melee or Projectile) the mob will use to attack the target. null if immune target or no target * */ private Weapon meleeOrProjectile() { - Vector2 newVector = new Vector2(owner.getEntity().getPosition().x - 10f, owner.getEntity().getPosition().y - 2f); - Fixture hitraycast = physics.raycastGetHit(owner.getEntity().getPosition(), newVector, TARGET); +// Vector2 newVector = new Vector2(owner.getEntity().getPosition().x - 10f, owner.getEntity().getPosition().y - 2f); +// Fixture hitraycast = physics.raycastGetHit(owner.getEntity().getPosition(), newVector, TARGET); + setTarget(); TouchAttackComponent comp = owner.getEntity().getComponent(TouchAttackComponent.class); Weapon chosenWeapon = null; if (comp != null) { - chosenWeapon = comp.chooseWeapon(hitraycast); + chosenWeapon = comp.chooseWeapon(target); } return chosenWeapon; @@ -233,6 +240,6 @@ private Weapon meleeOrProjectile() { private void setTarget() { Vector2 newVector = new Vector2(owner.getEntity().getPosition().x - 10f, owner.getEntity().getPosition().y - 2f); - Fixture hitraycast = physics.raycastGetHit(owner.getEntity().getPosition(), newVector, TARGET); + target = physics.raycastGetHit(owner.getEntity().getPosition(), newVector, TARGET); } } diff --git a/source/core/src/main/com/csse3200/game/components/tasks/MobDeathTask.java b/source/core/src/main/com/csse3200/game/components/tasks/MobDeathTask.java deleted file mode 100644 index e8e480980..000000000 --- a/source/core/src/main/com/csse3200/game/components/tasks/MobDeathTask.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.csse3200.game.components.tasks; - -import com.csse3200.game.ai.tasks.DefaultTask; -import com.csse3200.game.ai.tasks.PriorityTask; -import com.csse3200.game.components.CombatStatsComponent; -import com.badlogic.gdx.math.Vector2; -import com.csse3200.game.currency.Currency; -import com.csse3200.game.entities.Entity; -import com.csse3200.game.entities.factories.DropFactory; -import com.csse3200.game.entities.factories.ProjectileFactory; -import com.csse3200.game.physics.PhysicsEngine; -import com.csse3200.game.physics.raycast.RaycastHit; -import com.csse3200.game.services.ServiceLocator; -import com.csse3200.game.services.GameTime; -//import com.csse3200.game.rendering.DebugRenderer; - - -/** - * Task that prints a message to the terminal whenever it is called. - */ -public class MobDeathTask extends DefaultTask implements PriorityTask { - private static final int INTERVAL = 1; // time interval to scan for towers in - - private final int priority; - private Vector2 mobPosition = new Vector2(10f,10f); - private final PhysicsEngine physics; - private GameTime timeSource; - private long endTime; - private final RaycastHit hit = new RaycastHit(); - - private int mobHealth; - - /** - * @param priority Task priority when shooting (0 when not chasing). - */ - public MobDeathTask(int priority) { - this.priority = priority; - - physics = ServiceLocator.getPhysicsService().getPhysics(); - - timeSource = ServiceLocator.getTimeSource(); - } - - @Override - public void start() { - super.start(); - // gets starting health - this.mobHealth = owner.getEntity().getComponent(CombatStatsComponent.class).getHealth(); - //sets mob position - this.mobPosition = owner.getEntity().getCenterPosition(); - //sets endTime - endTime = timeSource.getTime() + (INTERVAL * 500); - this.owner.getEntity().getEvents().trigger("dieStart"); - } - - @Override - public void update() { - if (timeSource.getTime() >= endTime) { - updateMobState(); - endTime = timeSource.getTime() + (INTERVAL * 1000); - } - } - - public void updateMobState() { - - mobHealth = owner.getEntity().getComponent(CombatStatsComponent.class).getHealth(); - // TODO: inset a bit that picks from a list of drop options and drops this - - if (mobIsDead(mobHealth)) { - killMob(); - dropCurrency(); - } - - } - - @Override - public void stop() { - super.stop(); - } - - @Override - public int getPriority() { - if (status == Status.ACTIVE) { - return getActivePriority(); - } - - return getInactivePriority(); - } - - private int getActivePriority() { - if (mobHealth > 0) { - return -1; - } - return priority; - } - - private int getInactivePriority() { - if (mobHealth <= 0) { - return priority; - } - return -1; - } - private boolean mobIsDead(int mobhealth) { - - if (mobhealth <= 0) { - return true; - } - return false; - } - - private void killMob() { - owner.getEntity().dispose(); - } - - private void dropCurrency() { - - Entity scrap = DropFactory.createScrapDrop(); - scrap.setPosition(mobPosition.x,mobPosition.y); - ServiceLocator.getEntityService().register(scrap); - - } - -} diff --git a/source/core/src/main/com/csse3200/game/components/tasks/WanderTask.java b/source/core/src/main/com/csse3200/game/components/tasks/MobWanderTask.java similarity index 50% rename from source/core/src/main/com/csse3200/game/components/tasks/WanderTask.java rename to source/core/src/main/com/csse3200/game/components/tasks/MobWanderTask.java index 6fd754880..ccd7acf8c 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/WanderTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/MobWanderTask.java @@ -4,7 +4,11 @@ import com.csse3200.game.ai.tasks.DefaultTask; import com.csse3200.game.ai.tasks.PriorityTask; import com.csse3200.game.ai.tasks.Task; -import com.csse3200.game.utils.math.RandomUtils; +import com.csse3200.game.components.CombatStatsComponent; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.factories.DropFactory; +import com.csse3200.game.rendering.AnimationRenderComponent; +import com.csse3200.game.services.ServiceLocator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,8 +16,8 @@ * Wander around by moving a random position within a range of the starting position. Wait a little * bit between movements. Requires an entity with a PhysicsMovementComponent. */ -public class WanderTask extends DefaultTask implements PriorityTask { - private static final Logger logger = LoggerFactory.getLogger(WanderTask.class); +public class MobWanderTask extends DefaultTask implements PriorityTask { + private static final Logger logger = LoggerFactory.getLogger(MobWanderTask.class); private final Vector2 wanderRange; private final float waitTime; @@ -21,13 +25,15 @@ public class WanderTask extends DefaultTask implements PriorityTask { private MovementTask movementTask; private WaitTask waitTask; private Task currentTask; + private boolean isDead = false; + private Vector2 mobPosition; /** * @param wanderRange Distance in X and Y the entity can move from its position when start() is * called. * @param waitTime How long in seconds to wait between wandering. */ - public WanderTask(Vector2 wanderRange, float waitTime) { + public MobWanderTask(Vector2 wanderRange, float waitTime) { this.wanderRange = wanderRange; this.waitTime = waitTime; } @@ -53,29 +59,62 @@ public void start() { currentTask = movementTask; - this.owner.getEntity().getEvents().trigger("wanderStart"); +// this.owner.getEntity().getEvents().trigger("wanderStart"); } @Override public void update() { - if (currentTask.getStatus() != Status.ACTIVE) { - if (currentTask == movementTask) { - startWaiting(); - } else { - startMoving(); + + //Update the position of the mob + mobPosition = owner.getEntity().getPosition(); + + // If the mob is at zero health, kill the mob, + // play the death animation and stop the task + // This method is the idea of Ahmad who very kindly helped + // with section, massive props to him for his help! + if (!isDead && owner.getEntity().getComponent(CombatStatsComponent.class).isDead()) { + this.owner.getEntity().getEvents().trigger("dieStart"); + currentTask.stop(); + isDead = true; + } + + // Check if the mob has finished death animation + else if (isDead && owner.getEntity().getComponent(AnimationRenderComponent.class).isFinished()) { + + // Drop scrap at the mobs location for player + // to collect. + Entity scrap = DropFactory.createScrapDrop(); + scrap.setPosition(mobPosition.x,mobPosition.y); + ServiceLocator.getEntityService().register(scrap); + + // Delete the mob. + owner.getEntity().setFlagForDelete(true); + + } + // If not dead, do normal things... + else if (!isDead) { + + if (currentTask.getStatus() != Status.ACTIVE) { + if (currentTask == movementTask) { + startWaiting(); + } else { + startMoving(); + } } + currentTask.update(); } - currentTask.update(); } private void startWaiting() { logger.debug("Starting waiting"); + this.owner.getEntity().getEvents().trigger("stop"); swapTask(waitTask); } private void startMoving() { logger.debug("Starting moving"); movementTask.setTarget(getDirection()); + this.owner.getEntity().getEvents().trigger("wanderStart"); swapTask(movementTask); } 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 15db419ea..be0e655c6 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 @@ -4,6 +4,10 @@ import com.csse3200.game.ai.tasks.DefaultTask; import com.csse3200.game.ai.tasks.PriorityTask; import com.csse3200.game.ai.tasks.Task; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.factories.ProjectileFactory; +import com.csse3200.game.services.ServiceLocator; +import com.csse3200.game.physics.PhysicsLayer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,10 +19,17 @@ public class RangeBossMovementTask extends DefaultTask implements PriorityTask { private static final Logger logger = LoggerFactory.getLogger(RangeBossMovementTask.class); private final float waitTime; - private Vector2 startPos; + private Vector2 currentPos; private MovementTask movementTask; private WaitTask waitTask; private Task currentTask; + /** Animation event names */ + private static final String START = "startMobKing"; + private static final String FINAL = "startMobKingFinal"; + private enum STATE { + START, FINAL + } + private STATE bossBallState = STATE.START; /** * @param waitTime How long in seconds to wait between wandering. @@ -36,11 +47,11 @@ public int getPriority() { @Override public void start() { super.start(); - startPos = owner.getEntity().getPosition(); + currentPos = owner.getEntity().getPosition(); waitTask = new WaitTask(waitTime); waitTask.create(owner); - movementTask = new MovementTask(startPos.sub(2,0)); + movementTask = new MovementTask(currentPos.sub(2,0)); movementTask.create(owner); movementTask.start(); @@ -50,10 +61,26 @@ public void start() { this.owner.getEntity().getEvents().trigger("rangeBossMovementStart"); } + public void switchMobKingBallState() { + switch (bossBallState) { + case START: + owner.getEntity().getEvents().trigger(FINAL); + bossBallState = STATE.FINAL; + } + } + @Override public void update() { if (currentTask.getStatus() != Status.ACTIVE) { if (currentTask == movementTask) { + Entity newProjectile = ProjectileFactory.createMobKingBall( + PhysicsLayer.HUMANS, new Vector2(0, currentPos.y + 0.75f), new Vector2(2f,2f)); + owner.getEntity().getEvents().trigger(START); + switchMobKingBallState(); + // newProjectile.scaleHeight(-1f); + newProjectile.setScale(-1.3f, 0.82f); + newProjectile.setPosition((float) (currentPos.x), (float) (currentPos.y+0.75f)); + ServiceLocator.getEntityService().register(newProjectile); startWaiting(); } else { startMoving(); @@ -69,7 +96,7 @@ private void startWaiting() { private void startMoving() { logger.debug("Starting moving"); - movementTask.setTarget(startPos.sub(2,0)); + movementTask.setTarget(currentPos.sub(2,0)); swapTask(movementTask); } @@ -81,4 +108,4 @@ private void swapTask(Task newTask) { currentTask.start(); } -} \ No newline at end of file +} diff --git a/source/core/src/main/com/csse3200/game/components/tasks/SpawnWaveTask.java b/source/core/src/main/com/csse3200/game/components/tasks/SpawnWaveTask.java index cb326b8b4..d6c4a3d85 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/SpawnWaveTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/SpawnWaveTask.java @@ -1,9 +1,7 @@ package com.csse3200.game.components.tasks; -import com.badlogic.gdx.math.Vector2; import com.csse3200.game.ai.tasks.DefaultTask; import com.csse3200.game.ai.tasks.PriorityTask; -import com.csse3200.game.ai.tasks.Task; import com.csse3200.game.services.GameTime; import com.csse3200.game.services.ServiceLocator; import org.slf4j.Logger; @@ -12,17 +10,15 @@ public class SpawnWaveTask extends DefaultTask implements PriorityTask { private static final Logger logger = LoggerFactory.getLogger(SpawnWaveTask.class); private final GameTime globalTime; - private long endTime; - + private long endTime = 0; private final int SPAWNING_INTERVAL = 10; - public SpawnWaveTask() { this.globalTime = ServiceLocator.getTimeSource(); } @Override public int getPriority() { - return 10; // Low priority task + return 10; // High priority task } @Override 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 new file mode 100644 index 000000000..2919d49bc --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tasks/StunTowerCombatTask.java @@ -0,0 +1,139 @@ +package com.csse3200.game.components.tasks; + +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.factories.ProjectileFactory; +import com.csse3200.game.physics.PhysicsEngine; +import com.csse3200.game.physics.PhysicsLayer; +import com.csse3200.game.physics.raycast.RaycastHit; +import com.csse3200.game.services.GameTime; +import com.csse3200.game.services.ServiceLocator; + + +/** + * The StunTowerCombatTask runs the AI for the StunTower class. The tower scans for mobs and targets in a straight line + * from its centre coordinate and executes the trigger phrases for animations depending on the current state of tower. + */ +public class StunTowerCombatTask extends DefaultTask implements PriorityTask { + //constants + private static final int INTERVAL = 1; + private static final short TARGET = PhysicsLayer.NPC; + //Following constants are names of events that will be triggered in the state machine + public static final String IDLE = "startIdle"; + public static final String ATTACK = "startAttack"; + + //Following are the class constants + private final int priority; + private final float maxRange; + private Vector2 towerPosition = new Vector2(10, 10); + private final Vector2 maxRangePosition = new Vector2(); + private PhysicsEngine physics; + private GameTime timeSource; + private long endTime; + private final RaycastHit hit = new RaycastHit(); + + //enums for the state triggers + private enum STATE { + IDLE, ATTACK + } + private STATE towerState = STATE.IDLE; + + /** + * @param priority Task priority when targets are detected (0 when nothing is present) + * @param maxRange Maximum effective range of the StunTower. + */ + public StunTowerCombatTask(int priority, float maxRange) { + this.priority = priority; + this.maxRange = maxRange; + physics = ServiceLocator.getPhysicsService().getPhysics(); + timeSource = ServiceLocator.getTimeSource(); + } + + /** + * Starts the task running and starts the Idle animation + */ + @Override + public void start() { + super.start(); + //get the tower coordinates + this.towerPosition = owner.getEntity().getCenterPosition(); + this.maxRangePosition.set(towerPosition.x + maxRange, towerPosition.y); + //set the default state to IDLE state + owner.getEntity().getEvents().trigger(IDLE); + + endTime = timeSource.getTime() + (INTERVAL * 5000); + } + + /** + * updates the current state of the tower based on the current state of the game. If enemies are detected, attack + * state is activated and otherwise idle state remains. + */ + public void update() { + if (timeSource.getTime() >= endTime) { + updateTowerState(); + endTime = timeSource.getTime() + (INTERVAL * 1000); + } + } + + /** + * This method acts is the state machine for StunTower. Relevant animations are triggered based on relevant state + * of the game. If enemies are detected, state of the tower is changed to attack state. + */ + public void updateTowerState() { + switch (towerState) { + case IDLE -> { + if(isTargetVisible()) { + owner.getEntity().getEvents().trigger(ATTACK); + towerState = STATE.ATTACK; + } + } + case ATTACK -> { + if (!isTargetVisible()) { + owner.getEntity().getEvents().trigger(IDLE); + 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)); + newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.25), + (float) (owner.getEntity().getPosition().y + 0.25)); + ServiceLocator.getEntityService().register(newProjectile); + } + } + } + } + + /** + * stops the current animation and switches back the state of the tower to IDLE. + */ + public void stop() { + super.stop(); + owner.getEntity().getEvents().trigger(IDLE); + } + + /** + * returns the current priority of the task + * @return (int) active priority if target is visible and inactive priority otherwise + */ + public int getPriority() { + return !isTargetVisible() ? 0 : priority; + } + + public int getActivePriority() { + return !isTargetVisible() ? 0 : priority; + } + + public int getInactivePriority() { + return isTargetVisible() ? priority : 0; + } + + /** + * Searches for enemies/mobs in a straight line from the centre of the tower to maxRange in a straight line. + * @return true if targets are detected, false otherwise + */ + public boolean isTargetVisible() { + 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/tasks/TowerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/TowerCombatTask.java index 752a67859..a84528c42 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 @@ -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; @@ -10,6 +11,7 @@ import com.csse3200.game.physics.raycast.RaycastHit; import com.csse3200.game.services.GameTime; import com.csse3200.game.services.ServiceLocator; +import static java.lang.Math.round; /** * The TowerCombatTask runs the AI for the WeaponTower class. The tower will scan for targets in a straight line @@ -28,6 +30,7 @@ public class TowerCombatTask extends DefaultTask implements PriorityTask { // class attributes private final int priority; // The active priority this task will have + private float fireRateInterval; // time interval to fire projectiles at enemies in seconds private final float maxRange; private Vector2 towerPosition = new Vector2(10, 10); // initial placeholder value - will be overwritten private final Vector2 maxRangePosition = new Vector2(); @@ -48,6 +51,20 @@ private enum STATE { public TowerCombatTask(int priority, float maxRange) { this.priority = priority; this.maxRange = maxRange; + this.fireRateInterval = 1; + physics = ServiceLocator.getPhysicsService().getPhysics(); + timeSource = ServiceLocator.getTimeSource(); + } + + /** + * @param priority Task priority when targets are detected (0 when nothing detected). Must be a positive integer. + * @param maxRange Maximum effective range of the weapon tower. This determines the detection distance of targets + * @param fireRate The number of times per second this tower should fire its weapon + */ + public TowerCombatTask(int priority, float maxRange, float fireRate) { + this.priority = priority; + this.maxRange = maxRange; + this.fireRateInterval = 1/fireRate; physics = ServiceLocator.getPhysicsService().getPhysics(); timeSource = ServiceLocator.getTimeSource(); } @@ -63,6 +80,8 @@ public void start() { this.maxRangePosition.set(towerPosition.x + maxRange, towerPosition.y); // Default to idle mode owner.getEntity().getEvents().trigger(IDLE); + // Set up listener to change firerate + owner.getEntity().getEvents().addListener("addFireRate",this::changeFireRateInterval); endTime = timeSource.getTime() + (INTERVAL * 500); } @@ -75,7 +94,11 @@ public void start() { public void update() { if (timeSource.getTime() >= endTime) { updateTowerState(); - endTime = timeSource.getTime() + (INTERVAL * 1000); + if (towerState == STATE.FIRING) { + endTime = timeSource.getTime() + round(fireRateInterval * 1000); + } else { + endTime = timeSource.getTime() + (INTERVAL * 1000); + } } } @@ -112,13 +135,29 @@ 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(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)); + ServiceLocator.getEntityService().register(newProjectile); + + // * TEMPRORARYYYYYYYY PLS DON'T DELETE THIS + // PIERCE FIREBALL + // Entity pierceFireball = ProjectileFactory.createPierceFireBall(PhysicsLayer.NPC, new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f,2f)); + // pierceFireball.setPosition((float) (owner.getEntity().getPosition().x + 0), (float) (owner.getEntity().getPosition().y + 0.4)); + // ServiceLocator.getEntityService().register(pierceFireball); - // * TEMPORARYYYYYYY - // Entity newProjectile = ProjectileFactory.createRicochetFireball(PhysicsLayer.NPC, new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f,2f)); + // RICOCHET FIREBALL + // Entity ricochetProjectile = ProjectileFactory.createRicochetFireball(PhysicsLayer.NPC, new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f,2f), 0); - newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.75), (float) (owner.getEntity().getPosition().y + 0.4)); - ServiceLocator.getEntityService().register(newProjectile); + // ricochetProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0), (float) (owner.getEntity().getPosition().y + 0.4)); + // ServiceLocator.getEntityService().register(ricochetProjectile); + + // SPLIT FIREWORKS FIREBALLL + // Entity splitFireWorksProjectile = ProjectileFactory.createSplitFireWorksFireball(PhysicsLayer.NPC, new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f,2f), 16); + + // splitFireWorksProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.75), (float) (owner.getEntity().getPosition().y + 0.4)); + // ServiceLocator.getEntityService().register(splitFireWorksProjectile); } } case STOW -> { @@ -176,4 +215,28 @@ private boolean isTargetVisible() { // If there is an obstacle in the path to the max range point, mobs visible. return physics.raycast(towerPosition, maxRangePosition, TARGET, hit); } + + /** + * Increases the fireRateInterval, changing how frequently the turret fires. Will decrease if the argument is negative. + * + * @param perMinute The number of times per minute the turret's fire rate should increase + */ + private void changeFireRateInterval(int perMinute) { + float oldFireSpeed = 1/fireRateInterval; + float newFireSpeed = oldFireSpeed + perMinute/60f; + if (newFireSpeed == 0) { + return; + } else { + fireRateInterval = 1 / newFireSpeed; + } + } + + /** + * Function for getting the turret's fire rate. + * + * @return The fireRateInterval variable + */ + public float getFireRateInterval() { + return fireRateInterval; + } } 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 56c81912b..04967ba8e 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,8 +13,6 @@ public class TrajectTask extends DefaultTask implements PriorityTask { private static final String START = "startProjectile"; private static final String FINAL = "startProjectileFinal"; - - private enum STATE { START, FINAL } @@ -41,6 +39,8 @@ public void start() { this.owner.getEntity().getEvents().trigger(START); this.owner.getEntity().getEvents().trigger("rotate"); + this.owner.getEntity().getEvents().trigger("start"); + this.owner.getEntity().getEvents().trigger("startMobKing"); } public void switchProjectileState() { diff --git a/source/core/src/main/com/csse3200/game/components/tasks/human/EngineerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/human/EngineerCombatTask.java index 1487fa714..23dbea9c0 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/human/EngineerCombatTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/human/EngineerCombatTask.java @@ -131,10 +131,11 @@ public void updateEngineerState() { if (shotsFired <= 10) { 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(PhysicsLayer.NPC, + Entity newProjectile = ProjectileFactory.createEngineerBullet(PhysicsLayer.NPC, new Vector2(100, owner.getEntity().getPosition().y), new Vector2(4f, 4f)); - newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.75), (float) (owner.getEntity().getPosition().y + 0.4)); + newProjectile.setScale(0.8f, 0.8f); + newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.3), (float) (owner.getEntity().getPosition().y + 0.15)); ServiceLocator.getEntityService().register(newProjectile); shotsFired += 1; reloadTime = timeSource.getTime(); diff --git a/source/core/src/main/com/csse3200/game/components/tasks/human/HumanMovementTask.java b/source/core/src/main/com/csse3200/game/components/tasks/human/HumanMovementTask.java index 6e957e311..2f647426d 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/human/HumanMovementTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/human/HumanMovementTask.java @@ -10,7 +10,7 @@ import org.slf4j.LoggerFactory; /** - * Move to a given position, finishing when you get close enough. Requires an entity with a + * Move a human entity to a given position, finishing when you get close enough. Requires an entity with a * PhysicsMovementComponent. */ public class HumanMovementTask extends DefaultTask { diff --git a/source/core/src/main/com/csse3200/game/components/tasks/human/HumanWanderTask.java b/source/core/src/main/com/csse3200/game/components/tasks/human/HumanWanderTask.java index a9b497130..b801379ca 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/human/HumanWanderTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/human/HumanWanderTask.java @@ -13,46 +13,59 @@ import org.slf4j.LoggerFactory; /** - * Wander around by moving a random position within a range of the starting position. Wait a little - * bit between movements. Requires an entity with a PhysicsMovementComponent. + * HumanWanderTask is the entry point for the engineer entity's behaviour. Instantiates subtasks HumanWaitTask, + * HumanMovementTask and EngineerCombatTask, and manages transitions between the tasks. Engineer damage and death + * handled in this class. */ public class HumanWanderTask extends DefaultTask implements PriorityTask { private static final Logger logger = LoggerFactory.getLogger(HumanWanderTask.class); + private static final int TOLERANCE = 1; + private static final float STOP_DISTANCE = 0.5f; + private static final int DEFAULT_PRIORITY = 1; + private static final String DEATH_EVENT = "deathStart"; + private static final String IDLE_EVENT = "idleRight"; - private float maxRange; - private Vector2 wanderRange; + private final float maxRange; private final float waitTime; private Vector2 startPos; private HumanMovementTask movementTask; private HumanWaitTask waitTask; - private EngineerCombatTask combatTask; private Task currentTask; - private boolean isDead = false; /** + * Constructor of HumanWanderTask + * * @param waitTime How long in seconds to wait between wandering. + * @param maxRange Maximum detection and fighting range of the entity */ public HumanWanderTask(float waitTime, float maxRange) { this.waitTime = waitTime; this.maxRange = maxRange; } + /** + * Fetches the priority of this task. + * @return current priority of this task. Priority for this task is a set value and does not change. + */ @Override public int getPriority() { - return 1; // Low priority task + return DEFAULT_PRIORITY; // Low priority task } + /** + * Starts the HumanWanderTask instance and instantiates subtasks (HumanWaitTask, HumanWanderTask, EngineerCombatTask). + * + */ @Override public void start() { super.start(); - startPos = owner.getEntity().getPosition(); - this.wanderRange = owner.getEntity().getCenterPosition(); + this.startPos = owner.getEntity().getCenterPosition(); waitTask = new HumanWaitTask(waitTime); waitTask.create(owner); - movementTask = new HumanMovementTask(this.wanderRange, 1f); + movementTask = new HumanMovementTask(this.startPos, STOP_DISTANCE); movementTask.create(owner); movementTask.start(); @@ -63,58 +76,92 @@ public void start() { currentTask = movementTask; } + /** + * Operates the main logic of the entity in this task. All calls to switch to particular states are determined during + * the update phase. + * The logical flow is: + * - Check if the entity has died since last update + * - Check if the entity has finished dying + * - If not dead + */ @Override public void update() { // Check if engineer has died since last update if (!isDead && owner.getEntity().getComponent(CombatStatsComponent.class).isDead()) { - owner.getEntity().getEvents().trigger("deathStart"); - owner.getEntity().getComponent(ColliderComponent.class).setLayer(PhysicsLayer.NONE); - owner.getEntity().getComponent(HitboxComponent.class).setLayer(PhysicsLayer.NONE); - currentTask.stop(); - // Add a time delay here to allow animation to play? - isDead = true; + startDying(); } - // Check if engineer has finished dying + + // Check if engineer has finished dying animation else if (isDead && owner.getEntity().getComponent(AnimationRenderComponent.class).isFinished()) { owner.getEntity().setFlagForDelete(true); // TODO: make the appropriate calls to decrement the human count. } - // otherwise doing engineer things + + // otherwise doing engineer things since engineer is alive else if (!isDead) { if (currentTask.getStatus() != Status.ACTIVE) { + // if the engineer is in move state and update has been called, engineer has arrived at destination if (currentTask == movementTask) { startWaiting(); - owner.getEntity().getEvents().trigger("idleRight"); + owner.getEntity().getEvents().trigger(IDLE_EVENT); + } else if (combatTask.isTargetVisible()) { - if (combatTask.fetchTarget().y < owner.getEntity().getCenterPosition().y + 2 && - combatTask.fetchTarget().y > owner.getEntity().getCenterPosition().y - 2) { - startCombat(); - } else { - startMoving(new Vector2(owner.getEntity().getCenterPosition().x, combatTask.fetchTarget().y)); - } + // if the engineer is positioned within the tolerance range of the mob's y position, enter combat state + if (combatTask.fetchTarget().y < owner.getEntity().getCenterPosition().y + TOLERANCE && + combatTask.fetchTarget().y > owner.getEntity().getCenterPosition().y - TOLERANCE) { + startCombat(); + + // move into position for targeting mob + } else { + startMoving(new Vector2(owner.getEntity().getCenterPosition().x, combatTask.fetchTarget().y)); + } } } currentTask.update(); } } + /** + * Handle the dying phase of the entity. Triggers an event to play the appropriate media, + * sets HitBox and Collider components to ignore contact (stops the body being pushed around) + * and stops the current task. + */ + private void startDying() { + owner.getEntity().getEvents().trigger(DEATH_EVENT); + owner.getEntity().getComponent(ColliderComponent.class).setLayer(PhysicsLayer.NONE); + owner.getEntity().getComponent(HitboxComponent.class).setLayer(PhysicsLayer.NONE); + currentTask.stop(); + isDead = true; + } + + /** + * Starts the wait task. + */ private void startWaiting() { - logger.debug("Starting waiting"); swapTask(waitTask); } + /** + * Starts the movement task, to a particular destination + * @param destination the Vector2 position to which the entity needs to move + */ private void startMoving(Vector2 destination) { - logger.debug("Starting moving"); movementTask.setTarget(destination); swapTask(movementTask); } + /** + * Starts the combat task. + */ private void startCombat() { - logger.debug("Starting Combat"); swapTask(combatTask); } + /** + * Allows manual switching of tasks, from the current task to the supplied newTask. + * @param newTask the task being switched to. + */ private void swapTask(Task newTask) { if (currentTask != null) { currentTask.stop(); @@ -123,9 +170,11 @@ private void swapTask(Task newTask) { currentTask.start(); } - private Vector2 getDirection() { -// float y = startPos.y; -// return new Vector2(0, y); - return this.wanderRange; + /** + * Fetch the start position. + * @return a Vector2 start position + */ + public Vector2 getStartPos() { + return this.startPos; } } diff --git a/source/core/src/main/com/csse3200/game/components/tasks/scanner/ScannerTask.java b/source/core/src/main/com/csse3200/game/components/tasks/scanner/ScannerTask.java new file mode 100644 index 000000000..3405cecd0 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tasks/scanner/ScannerTask.java @@ -0,0 +1,115 @@ +package com.csse3200.game.components.tasks.scanner; + +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.factories.EngineerFactory; +import com.csse3200.game.physics.PhysicsEngine; +import com.csse3200.game.physics.PhysicsLayer; +import com.csse3200.game.physics.raycast.RaycastHit; +import com.csse3200.game.services.GameTime; +import com.csse3200.game.services.ServiceLocator; + +/** + * ScannerTask implements the behaviour of GapScannerEntities that detect the + * conditions to trigger engineer spawning, i.e., No towers, no engineers, mobs within + * a certain distance. + */ +public class ScannerTask extends DefaultTask implements PriorityTask { + + private static final int SCAN_INTERVAL = 1000; // how often to scan, in milliseconds + private final PhysicsEngine physics; + private final GameTime timeSource; + private final RaycastHit hit = new RaycastHit(); + private Vector2 selfPosition; + private long endTime; + + // booleans to track presence of towers, engineers and mobs + private boolean towers = false; + private boolean engineers = false; + private boolean mobs = false; + + // track the number of engineers spawned. + private static final int maxEngineers = 3; + private int engineerCount = 0; + + /** + * ScannerTask Constructor + */ + public ScannerTask() { + physics = ServiceLocator.getPhysicsService().getPhysics(); + timeSource = ServiceLocator.getTimeSource(); + } + + /** + * Start method for the ScannerTask + */ + @Override + public void start() { + super.start(); + endTime = timeSource.getTime() + (SCAN_INTERVAL); + selfPosition = owner.getEntity().getCenterPosition(); + } + + /** + * Update method for the scanner task. Implements the scanning and spawning logic + * for populating the game area with engineers. + */ + @Override + public void update() { + if (timeSource.getTime() >= endTime) { + // clear all presence booleans + towers = false; + engineers = false; + mobs = false; + + // carry out scan and behave accordingly + scan(); + if (!towers && !engineers && mobs) { + // spawn engineers now + if (engineerCount < maxEngineers) { + Entity engineer = EngineerFactory.createEngineer(); + + engineer.setPosition(new Vector2((int)(selfPosition.x + 1),(int) selfPosition.y)); + ServiceLocator.getEntityService().register(engineer); + engineerCount += 1; + } + } + endTime = timeSource.getTime() + SCAN_INTERVAL; + } + } + + /** + * Scanning method that detects the presence of towers/engineers/mobs. + * Sets the tracking booleans for each of the entity types + */ + private void scan() { + + if (physics.raycast(selfPosition, + new Vector2(selfPosition.x + 10, selfPosition.y), + PhysicsLayer.TOWER, + hit)) { + towers = true; + } else if (physics.raycast(selfPosition, + new Vector2(selfPosition.x + 10, selfPosition.y), + PhysicsLayer.ENGINEER, + hit)) { + engineers = true; + } else if (physics.raycast(selfPosition, + new Vector2(selfPosition.x + 10, selfPosition.y), + PhysicsLayer.NPC, + hit)) { + mobs = true; + } + } + + /** + * Return the priority of the task. + * @return the default priority of this task (a fixed value - no other tasks to run) + */ + @Override + public int getPriority() { + return 1; + } +} 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 new file mode 100644 index 000000000..d8307e0e4 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tower/DroidAnimationController.java @@ -0,0 +1,122 @@ +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 + * of the events is triggered. + */ +public class DroidAnimationController extends Component { + private AnimationRenderComponent animator; + + /** + * Creation call for a DroidAnimationController, 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("walkStart", this::animateWalk); + entity.getEvents().addListener("idleStart", this::animateDefault); + entity.getEvents().addListener("goUpStart",this::animateGoUp); + entity.getEvents().addListener("goDownStart",this::animateGoDown); + 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); + + } + + /** + * Initiates the walking animation for the robot. + * This method should be invoked when the robot is moving but not in combat. + */ + void animateWalk() { + animator.startAnimation("walk"); + } + + /** + * Starts the animation sequence for switching aim from above. + * Use this method when the robot is preparing to target mobs after aiming from below. + */ + void animateGoUp() { + animator.startAnimation("goUp"); + } + + /** + * Activates the animation sequence for switching aim from below. + * Use this method when the robot is preparing to target mobs after aiming from above. + */ + void animateGoDown() { + animator.startAnimation("goDown"); + } + + /** + * Triggers the animation for firing projectiles from an elevated aim. + * Invoke this method when the robot engages with mobs and aiming above. + */ + void animateAttackUp() { + animator.startAnimation("attackUp"); + } + + /** + * Starts the animation sequence for firing projectiles from a lowered aim. + * Use this method when the robot engages with mobs and aiming below. + */ + void animateAttackDown() { + animator.startAnimation("attackDown"); + } + + /** + * Triggers the robot's death animation. + * This method should be invoked when the robot's health reaches zero. + */ + void animateDeath() { + animator.startAnimation("death"); + } + + + /** + * Triggers the "default" animation for the entity. + * This method should be invoked when the entity returns to its default state. + */ + 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/FireTowerAnimationController.java b/source/core/src/main/com/csse3200/game/components/tower/FireTowerAnimationController.java new file mode 100644 index 000000000..358d7a3a6 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tower/FireTowerAnimationController.java @@ -0,0 +1,58 @@ +package com.csse3200.game.components.tower; + +import com.badlogic.gdx.audio.Sound; +import com.csse3200.game.components.Component; +import com.csse3200.game.rendering.AnimationRenderComponent; +import com.csse3200.game.services.ServiceLocator; + +/** + * Listens for events relevant to a weapon tower state. + * Each event will have a trigger phrase and have a certain animation attached. + */ +public class FireTowerAnimationController extends Component{ + //Event name constants + private static final String IDLE = "startIdle"; + private static final String PREP_ATTACK = "startAttackPrep"; + private static final String ATTACK = "startAttack"; + + //animation name constants + private static final String IDLE_ANIM = "idle"; + private static final String PREP_ATTACK_ANIM = "prepAttack"; + private static final String ATTACK_ANIM = "attack"; + //here we can add the sounds for the implemented animations + + AnimationRenderComponent animator; + + /** + * Create method for FireTowerAnimationController. + */ + @Override + public void create() { + super.create(); + animator = this.entity.getComponent(AnimationRenderComponent.class); + entity.getEvents().addListener(IDLE, this::animateIdle); + entity.getEvents().addListener(PREP_ATTACK, this::animatePrepAttack); + entity.getEvents().addListener(ATTACK, this::animateAttack); + } + + /** + * Starts the idle animation + */ + void animateIdle() { + animator.startAnimation(IDLE_ANIM); + } + + /** + * starts the prep_attack animation + */ + void animatePrepAttack() { + animator.startAnimation(PREP_ATTACK_ANIM); + } + + /** + * starts the attack animation + */ + void animateAttack() { + animator.startAnimation(ATTACK_ANIM); + } +} diff --git a/source/core/src/main/com/csse3200/game/components/tower/StunTowerAnimationController.java b/source/core/src/main/com/csse3200/game/components/tower/StunTowerAnimationController.java new file mode 100644 index 000000000..fa4868c4c --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tower/StunTowerAnimationController.java @@ -0,0 +1,48 @@ +package com.csse3200.game.components.tower; + +import com.badlogic.gdx.audio.Sound; +import com.csse3200.game.components.Component; +import com.csse3200.game.rendering.AnimationRenderComponent; +import com.csse3200.game.services.ServiceLocator; + +/** + * Listens to triggers phrases and executes the required animations. + */ +public class StunTowerAnimationController extends Component { + //Event name constants + private static final String IDLE = "startIdle"; + private static final String ATTACK = "startAttack"; + //animation name constants + private static final String IDLE_ANIM = "idle"; + private static final String ATTACK_ANIM = "attack"; + + //further sounds can be added for the tower attacks/movement + + AnimationRenderComponent animator; + + /** + * Creation method for StunTowerAnimationController, fetches the animationRenderComponent that this controller will + * be attached to and registers all the event listeners required to trigger the animations. + */ + @Override + public void create() { + super.create(); + animator = this.entity.getComponent(AnimationRenderComponent.class); + entity.getEvents().addListener(IDLE, this::animateIdle); + entity.getEvents().addListener(ATTACK, this::animateAttack); + } + + /** + * Starts the idle animation + */ + void animateIdle() { + animator.startAnimation(IDLE_ANIM); + } + + /** + * starts the attack animation + */ + void animateAttack() { + animator.startAnimation(ATTACK_ANIM); + } +} 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 c019b92fd..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; @@ -78,18 +77,13 @@ private void applyTNTDamage() { Vector2 positionSource = entity.getPosition(); Vector2 positionOther = otherEntity.getPosition(); - if (positionSource.dst(positionOther) <= radius) { + if (positionSource.dst(positionOther) <= radius && positionSource.y -1 <= positionOther.y && positionSource.y +1 >= positionOther.y) { HitboxComponent sourceHitbox = entity.getComponent(HitboxComponent.class); HitboxComponent otherHitbox = otherEntity.getComponent(HitboxComponent.class); // 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/main/com/csse3200/game/components/tower/TowerUpgraderComponent.java b/source/core/src/main/com/csse3200/game/components/tower/TowerUpgraderComponent.java new file mode 100644 index 000000000..07ead7edc --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tower/TowerUpgraderComponent.java @@ -0,0 +1,66 @@ +package com.csse3200.game.components.tower; + +import com.csse3200.game.components.CombatStatsComponent; +import com.csse3200.game.components.Component; +import static java.lang.Math.round; + +/** + * Listens for an event from the popup menu to upgrade + * the turret entity this component is attached to. + */ +public class TowerUpgraderComponent extends Component { + public enum UPGRADE { + ATTACK, MAXHP, FIRERATE, REPAIR + } + + @Override + public void create() { + super.create(); + entity.getEvents().addListener("upgradeTower", this::upgradeTower); + } + + /** + * Determines which type of upgrade to perform based on arguments provided by the event trigger. + * Note: The fire rate upgrade is in shots per minute. + * + * @param upgradeType An enum indicating the type of upgrade to do + * @param value How much the upgrade should change the tower's stats, if applicable + */ + void upgradeTower(UPGRADE upgradeType, int value) { + switch (upgradeType) { + case ATTACK -> {upgradeTowerAttack(value);} + case MAXHP -> {upgradeTowerMaxHealth(value);} + case FIRERATE -> {getEntity().getEvents().trigger("addFireRate", value);} + case REPAIR -> {repairTower();} + } + } + + /** + * Increases the tower's attack stat. + * + * @param increase The amount that the attack stat should increase by. + */ + void upgradeTowerAttack(int increase) { + int oldAttack = getEntity().getComponent(CombatStatsComponent.class).getBaseAttack(); + getEntity().getComponent(CombatStatsComponent.class).setBaseAttack(oldAttack + increase); + } + + /** + * Increases the tower's maximum health, and restores the tower's health to the new maximum. + * + * @param increase The amount that the max health stat should increase by. + */ + void upgradeTowerMaxHealth(int increase) { + int oldMaxHealth = getEntity().getComponent(CombatStatsComponent.class).getMaxHealth(); + getEntity().getComponent(CombatStatsComponent.class).setMaxHealth(oldMaxHealth + increase); + getEntity().getComponent(CombatStatsComponent.class).setHealth(oldMaxHealth + increase); + } + + /** + * Restores the tower's health to its maximum health. + */ + void repairTower() { + int maxHealth = getEntity().getComponent(CombatStatsComponent.class).getMaxHealth(); + getEntity().getComponent(CombatStatsComponent.class).setHealth(maxHealth); + } +} diff --git a/source/core/src/main/com/csse3200/game/entities/Entity.java b/source/core/src/main/com/csse3200/game/entities/Entity.java index c26ba6f00..138c8ed61 100644 --- a/source/core/src/main/com/csse3200/game/entities/Entity.java +++ b/source/core/src/main/com/csse3200/game/entities/Entity.java @@ -251,13 +251,20 @@ public void update() { if (!enabled) { return; } - for (Component component : createdComponents) { - component.triggerUpdate(); - } - if (isFlaggedForDelete) { - dispose(); - return; - } + + // ! ITERATOR CAUSES PROBLEMS + // for (Component component : createdComponents) { + // component.triggerUpdate(); + // } + + for (int i = 0; i < createdComponents.size; i++) { + createdComponents.get(i).triggerUpdate(); + } + + // if (isFlaggedForDelete) { + // dispose(); + // return; + // } } /** diff --git a/source/core/src/main/com/csse3200/game/entities/configs/DroidTowerConfig.java b/source/core/src/main/com/csse3200/game/entities/configs/DroidTowerConfig.java new file mode 100644 index 000000000..18ce675d5 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/entities/configs/DroidTowerConfig.java @@ -0,0 +1,10 @@ +package com.csse3200.game.entities.configs; + +/** + * Defines a basic set of properties stored in entities config files to be loaded by Entity Factories. + */ +public class DroidTowerConfig { + public int health = 1; + public int baseAttack = 0; + public int cost = 1; +} diff --git a/source/core/src/main/com/csse3200/game/entities/configs/FireTowerConfig.java b/source/core/src/main/com/csse3200/game/entities/configs/FireTowerConfig.java new file mode 100644 index 000000000..7e697040b --- /dev/null +++ b/source/core/src/main/com/csse3200/game/entities/configs/FireTowerConfig.java @@ -0,0 +1,7 @@ +package com.csse3200.game.entities.configs; + +public class FireTowerConfig { + public int health = 1; + public int baseAttack = 0; + public int cost = 1; +} diff --git a/source/core/src/main/com/csse3200/game/entities/configs/NPCConfigs.java b/source/core/src/main/com/csse3200/game/entities/configs/NPCConfigs.java index 146e364a4..3bc7d9eb2 100644 --- a/source/core/src/main/com/csse3200/game/entities/configs/NPCConfigs.java +++ b/source/core/src/main/com/csse3200/game/entities/configs/NPCConfigs.java @@ -16,9 +16,12 @@ public class NPCConfigs { public BaseEntityConfig projectile = new ProjectileConfig(); public GhostKingConfig ghostKing = new GhostKingConfig(); public BaseEnemyConfig xenoGrunt = new BaseEnemyConfig( + 10, + 100, new ArrayList(), new ArrayList(), - new ArrayList()); + new ArrayList(), + 10); public BossKingConfigs BossKing = new BossKingConfigs(); diff --git a/source/core/src/main/com/csse3200/game/entities/configs/StunTowerConfig.java b/source/core/src/main/com/csse3200/game/entities/configs/StunTowerConfig.java new file mode 100644 index 000000000..fc711e70f --- /dev/null +++ b/source/core/src/main/com/csse3200/game/entities/configs/StunTowerConfig.java @@ -0,0 +1,7 @@ +package com.csse3200.game.entities.configs; + +public class StunTowerConfig { + public int health = 1; + public int baseAttack = 0; + public int cost = 1; +} diff --git a/source/core/src/main/com/csse3200/game/entities/configs/baseTowerConfigs.java b/source/core/src/main/com/csse3200/game/entities/configs/baseTowerConfigs.java index d0c920d0c..1b3eab1ee 100644 --- a/source/core/src/main/com/csse3200/game/entities/configs/baseTowerConfigs.java +++ b/source/core/src/main/com/csse3200/game/entities/configs/baseTowerConfigs.java @@ -8,4 +8,8 @@ public class baseTowerConfigs { public WallTowerConfig wall = new WallTowerConfig(); public IncomeTowerConfig income = new IncomeTowerConfig(); public TNTTowerConfigs TNTTower = new TNTTowerConfigs(); + public DroidTowerConfig DroidTower = new DroidTowerConfig(); + public FireTowerConfig fireTower = new FireTowerConfig(); + public StunTowerConfig stunTower = new StunTowerConfig(); + } \ No newline at end of file diff --git a/source/core/src/main/com/csse3200/game/entities/factories/BossKingFactory.java b/source/core/src/main/com/csse3200/game/entities/factories/BossKingFactory.java index b4260b2be..99077b50f 100644 --- a/source/core/src/main/com/csse3200/game/entities/factories/BossKingFactory.java +++ b/source/core/src/main/com/csse3200/game/entities/factories/BossKingFactory.java @@ -61,8 +61,8 @@ public static Entity createBossKing1(Entity target) { .addComponent(new BossAnimationController()); bossKing1.getComponent(AnimationRenderComponent.class).scaleEntity(); - bossKing1.scaleHeight(0.5f); - bossKing1.scaleWidth(0.5f); + bossKing1.setScale(-1f,1f); + return bossKing1; } diff --git a/source/core/src/main/com/csse3200/game/entities/factories/GapScannerFactory.java b/source/core/src/main/com/csse3200/game/entities/factories/GapScannerFactory.java new file mode 100644 index 000000000..553747246 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/entities/factories/GapScannerFactory.java @@ -0,0 +1,36 @@ +package com.csse3200.game.entities.factories; + + +import com.csse3200.game.ai.tasks.AITaskComponent; +import com.csse3200.game.components.tasks.scanner.ScannerTask; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.physics.components.PhysicsComponent; + +/** + * Factory to create scanner entities that determine whether to spawn engineer entities. + * These do not interact with any of the entities in the game area except to detect other entities + * + */ +public class GapScannerFactory { + + /** + * Creates a scanner entity + * @return scanner entity + */ + public static Entity createScanner() { + Entity scanner = new Entity(); + + AITaskComponent aiComponent = new AITaskComponent(); + + scanner + .addComponent(new PhysicsComponent()) + .addComponent(aiComponent); + + scanner.getComponent(AITaskComponent.class).addTask(new ScannerTask()); + return scanner; + } + + private GapScannerFactory() { + throw new IllegalStateException("Instantiating static util class"); + } +} diff --git a/source/core/src/main/com/csse3200/game/entities/factories/NPCFactory.java b/source/core/src/main/com/csse3200/game/entities/factories/NPCFactory.java index 2488e4fa2..331ffd8f8 100644 --- a/source/core/src/main/com/csse3200/game/entities/factories/NPCFactory.java +++ b/source/core/src/main/com/csse3200/game/entities/factories/NPCFactory.java @@ -5,17 +5,14 @@ import com.badlogic.gdx.math.Vector2; import com.csse3200.game.ai.tasks.AITaskComponent; import com.csse3200.game.components.CombatStatsComponent; +import com.csse3200.game.components.TouchAttackComponent; import com.csse3200.game.components.npc.GhostAnimationController; import com.csse3200.game.components.npc.XenoAnimationController; -import com.csse3200.game.components.TouchAttackComponent; import com.csse3200.game.components.tasks.MobAttackTask; -import com.csse3200.game.components.tasks.SpawnWaveTask; -import com.csse3200.game.components.tasks.MobDeathTask; -import com.csse3200.game.components.tasks.WanderTask; +import com.csse3200.game.components.tasks.MobWanderTask; import com.csse3200.game.entities.Entity; import com.csse3200.game.entities.Melee; import com.csse3200.game.entities.PredefinedWeapons; -import com.csse3200.game.entities.Weapon; import com.csse3200.game.entities.configs.*; import com.csse3200.game.files.FileLoader; import com.csse3200.game.physics.PhysicsLayer; @@ -141,9 +138,8 @@ public static Entity createXenoGrunt(Entity target) { public static Entity createBaseNPC(Entity target) { AITaskComponent aiComponent = new AITaskComponent() - .addTask(new WanderTask(new Vector2(2f, 2f), 2f)) - .addTask(new MobAttackTask(2, 40)) - .addTask(new MobDeathTask(2)); + .addTask(new MobWanderTask(new Vector2(2f, 2f), 2f)) + .addTask(new MobAttackTask(2, 40)); Entity npc = new Entity() .addComponent(new PhysicsComponent()) diff --git a/source/core/src/main/com/csse3200/game/entities/factories/ObstacleFactory.java b/source/core/src/main/com/csse3200/game/entities/factories/ObstacleFactory.java index c271ea2d6..0fef2bc2e 100644 --- a/source/core/src/main/com/csse3200/game/entities/factories/ObstacleFactory.java +++ b/source/core/src/main/com/csse3200/game/entities/factories/ObstacleFactory.java @@ -70,7 +70,7 @@ public static Entity createWall(float width, float height) { Entity wall = new Entity() .addComponent(new PhysicsComponent().setBodyType(BodyType.StaticBody)) // * TMEPORARRYY WALLL - .addComponent(new ColliderComponent().setLayer(PhysicsLayer.OBSTACLE)); + .addComponent(new ColliderComponent().setLayer(PhysicsLayer.WALL)); wall.setScale(width, height); return wall; } diff --git a/source/core/src/main/com/csse3200/game/entities/factories/PlayerFactory.java b/source/core/src/main/com/csse3200/game/entities/factories/PlayerFactory.java index 1f4f2f3f1..80bbc26b8 100644 --- a/source/core/src/main/com/csse3200/game/entities/factories/PlayerFactory.java +++ b/source/core/src/main/com/csse3200/game/entities/factories/PlayerFactory.java @@ -1,14 +1,11 @@ package com.csse3200.game.entities.factories; -import com.badlogic.gdx.math.Vector2; import com.csse3200.game.ai.tasks.AITaskComponent; import com.csse3200.game.components.CombatStatsComponent; import com.csse3200.game.components.player.InventoryComponent; import com.csse3200.game.components.player.PlayerActions; import com.csse3200.game.components.player.PlayerStatsDisplay; -import com.csse3200.game.components.tasks.MobAttackTask; import com.csse3200.game.components.tasks.SpawnWaveTask; -import com.csse3200.game.components.tasks.WanderTask; import com.csse3200.game.entities.Entity; import com.csse3200.game.entities.configs.PlayerConfig; import com.csse3200.game.files.FileLoader; 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 58a705f0a..152685fdb 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 @@ -6,10 +6,12 @@ import com.csse3200.game.components.ProjectileEffects; 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.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.components.DeleteOnMapEdgeComponent; import com.csse3200.game.entities.configs.BaseEntityConfig; import com.csse3200.game.entities.configs.NPCConfigs; import com.csse3200.game.files.FileLoader; @@ -23,7 +25,10 @@ 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. @@ -49,17 +54,52 @@ public class ProjectileFactory { * @return Returns a new single-target projectile entity */ public static Entity createEffectProjectile(short targetLayer, Vector2 destination, Vector2 speed, ProjectileEffects effect, boolean aoe) { - Entity projectile = createFireBall(targetLayer, destination, speed); + Entity projectile = createBaseProjectile(targetLayer, destination, speed); 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)); + animator.addAnimation(START_ANIM, START_SPEED, Animation.PlayMode.NORMAL); + animator.addAnimation(FINAL_ANIM, FINAL_SPEED, Animation.PlayMode.NORMAL); + + 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)); + animator.addAnimation(START_ANIM, START_SPEED, Animation.PlayMode.NORMAL); + animator.addAnimation(FINAL_ANIM, FINAL_SPEED, Animation.PlayMode.NORMAL); + + projectile + .addComponent(animator) + .addComponent(new ProjectileAnimationController()); } 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)); + animator.addAnimation(START_ANIM, START_SPEED, Animation.PlayMode.NORMAL); + animator.addAnimation(FINAL_ANIM, FINAL_SPEED, Animation.PlayMode.NORMAL); + + projectile + .addComponent(animator) + .addComponent(new SnowBallProjectileAnimationController()); + // * TEMPORARY + // .addComponent(new DeleteOnMapEdgeComponent()); + // .addComponent(new SelfDestructOnHitComponent(PhysicsLayer.OBSTACLE)); + + return projectile; } case STUN -> { projectile.addComponent(new EffectsComponent(targetLayer, 3, ProjectileEffects.STUN, aoe)); @@ -75,6 +115,7 @@ public static Entity createEffectProjectile(short targetLayer, Vector2 destinati public static Entity createPierceFireBall(short targetLayer, Vector2 destination, Vector2 speed) { Entity fireBall = createFireBall(targetLayer, destination, speed); fireBall.getComponent(TouchAttackComponent.class).setDisposeOnHit(false); + fireBall.getComponent(TouchAttackComponent.class).setKnockBack(0f); return fireBall; } @@ -83,13 +124,24 @@ public static Entity createPierceFireBall(short targetLayer, Vector2 destination * Create a ricochet fireball. * Ricochet fireball bounces off specified targets while applying intended effects i.e. damage */ - public static Entity createRicochetFireball(short targetLayer, Vector2 destination, Vector2 speed) { + public static Entity createRicochetFireball(short targetLayer, Vector2 destination, Vector2 speed, int bounceCount) { Entity fireBall = createFireBall(targetLayer, destination, speed); - fireBall.addComponent(new RicochetComponent(targetLayer)); + fireBall + .addComponent(new RicochetComponent(targetLayer, bounceCount)); + + setColliderSize(fireBall, (float) 0.1, (float) 0.1); return fireBall; } + public static Entity createSplitFireWorksFireball(short targetLayer, Vector2 destination, Vector2 speed, int amount) { + Entity fireBall = createFireBall(targetLayer, destination, speed); + fireBall + .addComponent(new SplitFireworksComponent(targetLayer, amount)); + + return fireBall; + } + /** * Creates a fireball Entity. * @@ -111,6 +163,33 @@ public static Entity createFireBall(short targetLayer, Vector2 destination, Vect projectile .addComponent(animator) .addComponent(new ProjectileAnimationController()); + // * TEMPORARY + // .addComponent(new DeleteOnMapEdgeComponent()); + // .addComponent(new SelfDestructOnHitComponent(PhysicsLayer.OBSTACLE)); + + return projectile; + } + /** + * Creates a engineer bullet + * + * @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 createEngineerBullet(short targetLayer, Vector2 destination, Vector2 speed) { + Entity projectile = createBaseProjectile(targetLayer, destination, speed); + + AnimationRenderComponent animator = + new AnimationRenderComponent( + ServiceLocator.getResourceService() + .getAsset("images/projectiles/engineer_projectile.atlas", TextureAtlas.class)); + animator.addAnimation("bullet", START_SPEED, Animation.PlayMode.NORMAL); + animator.addAnimation("bulletFinal", FINAL_SPEED, Animation.PlayMode.NORMAL); + + projectile + .addComponent(animator) + .addComponent(new EngineerBulletsAnimationController()); // .addComponent(new SelfDestructOnHitComponent(PhysicsLayer.OBSTACLE)); return projectile; @@ -137,6 +216,8 @@ public static Entity createMobBall(short targetLayer, Vector2 destination, Vecto projectile .addComponent(animator) .addComponent(new MobProjectileAnimationController()); + // * TEMPORARY + // .addComponent(new DeleteOnMapEdgeComponent()); projectile .getComponent(AnimationRenderComponent.class).scaleEntity(); @@ -144,6 +225,35 @@ public static Entity createMobBall(short targetLayer, Vector2 destination, Vecto return projectile; } + /** + * Creates a projectile to be used by the MobKing + * + * @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 createMobKingBall(short targetLayer, Vector2 destination, Vector2 speed) { + Entity projectile = createBaseProjectile(targetLayer, destination, speed); + + AnimationRenderComponent animator = + new AnimationRenderComponent( + ServiceLocator.getResourceService() + .getAsset("images/projectiles/mobKing_projectile.atlas", TextureAtlas.class)); + animator.addAnimation("mob_boss", 0.17f, Animation.PlayMode.NORMAL); + animator.addAnimation("mob_bossFinal", 0.17f, Animation.PlayMode.NORMAL); + + + projectile + .addComponent(animator) + .addComponent(new MobKingProjectAnimController()); + + projectile + .getComponent(AnimationRenderComponent.class).scaleEntity(); + + return projectile; + } + /** * Creates a generic projectile entity that can be used for multiple types of * projectiles. * @@ -170,7 +280,9 @@ public static Entity createBaseProjectile(short targetLayer, Vector2 // specified target. // Original knockback value: 1.5f .addComponent(new TouchAttackComponent(targetLayer, 1.5f, true)) - .addComponent(new CombatStatsComponent(config.health, config.baseAttack)); + .addComponent(new CombatStatsComponent(config.health, config.baseAttack)) + // *TEMPORARY + .addComponent(new DeleteOnMapEdgeComponent()); projectile .getComponent(PhysicsMovementComponent.class).setSpeed(speed); diff --git a/source/core/src/main/com/csse3200/game/entities/factories/TowerFactory.java b/source/core/src/main/com/csse3200/game/entities/factories/TowerFactory.java index fa71f10b5..cf6e110a6 100644 --- a/source/core/src/main/com/csse3200/game/entities/factories/TowerFactory.java +++ b/source/core/src/main/com/csse3200/game/entities/factories/TowerFactory.java @@ -1,13 +1,19 @@ package com.csse3200.game.entities.factories; -import com.csse3200.game.components.TouchAttackComponent; + + +import com.csse3200.game.components.tasks.DroidCombatTask; import com.csse3200.game.components.tasks.TNTTowerCombatTask; +import com.csse3200.game.components.tower.DroidAnimationController; import com.csse3200.game.components.tower.TNTAnimationController; import com.csse3200.game.components.tower.TNTDamageComponent; import com.csse3200.game.entities.configs.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import com.badlogic.gdx.graphics.Texture; +import com.csse3200.game.components.tasks.FireTowerCombatTask; +import com.csse3200.game.components.tasks.StunTowerCombatTask; +import com.csse3200.game.components.tower.FireTowerAnimationController; +import com.csse3200.game.components.tower.StunTowerAnimationController; +import com.csse3200.game.components.tower.TowerUpgraderComponent; + import com.badlogic.gdx.graphics.g2d.Animation; import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.physics.box2d.BodyDef.BodyType; @@ -20,6 +26,7 @@ import com.csse3200.game.components.tasks.CurrencyTask; import com.csse3200.game.entities.Entity; 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; @@ -28,6 +35,8 @@ import com.csse3200.game.rendering.TextureRenderComponent; import com.csse3200.game.services.ServiceLocator; +import java.util.ServiceConfigurationError; + /** * Factory to create a tower entity. * @@ -39,18 +48,29 @@ public class TowerFactory { private static final int COMBAT_TASK_PRIORITY = 2; private static final int WEAPON_TOWER_MAX_RANGE = 40; private static final int TNT_TOWER_MAX_RANGE = 6; - private static final int TNT_TOWER_RANGE = 5; + private static final int TNT_TOWER_RANGE = 6; private static final int TNT_KNOCK_BACK_FORCE = 10; private static final String WALL_IMAGE = "images/towers/wallTower.png"; private static final String RESOURCE_TOWER = "images/towers/mine_tower.png"; private static final String TURRET_ATLAS = "images/towers/turret01.atlas"; private static final String TNT_ATLAS = "images/towers/TNTTower.atlas"; + private static final String DROID_ATLAS = "images/towers/DroidTower.atlas"; + private static final float DROID_SPEED = 0.25f; private static final String DEFAULT_ANIM = "default"; + private static final String WALK_ANIM = "walk"; + private static final String DEATH_ANIM = "death"; + private static final String GO_UP = "goUp"; + private static final String GO_DOWN = "goDown"; + private static final String SHOOT_UP = "attackUp"; + private static final String SHOOT_DOWN = "attackDown"; private static final float DEFAULT_SPEED= 0.2f; private static final String DIG_ANIM = "dig"; private static final float DIG_SPEED = 0.2f; private static final String EXPLODE_ANIM = "explode"; private static final float EXPLODE_SPEED = 0.2f; + private static final String FIRE_TOWER_ATLAS = "images/towers/fire_tower_atlas.atlas"; + private static final String STUN_TOWER_ATLAS = "images/towers/stun_tower.atlas"; + private static final String IDLE_ANIM = "idle"; private static final float IDLE_SPEED = 0.3f; private static final String DEPLOY_ANIM = "deploy"; @@ -59,7 +79,19 @@ public class TowerFactory { private static final float STOW_SPEED = 0.2f; private static final String FIRE_ANIM = "firing"; private static final float FIRE_SPEED = 0.25f; - private static final int INCOME_INTERVAL = 3; + + private static final String FIRE_TOWER_IDLE_ANIM = "idle"; + private static final float FIRE_TOWER_IDLE_SPEED = 0.3f; + private static final String FIRE_TOWER_PREP_ATTACK_ANIM = "prepAttack"; + private static final float FIRE_TOWER_PREP_ATTACK_SPEED = 0.2f; + private static final String FIRE_TOWER_ATTACK_ANIM = "attack"; + private static final float FIRE_TOWER_ATTACK_SPEED = 0.25f; + private static final String STUN_TOWER_IDLE_ANIM = "idle"; + private static final float STUN_TOWER_IDLE_SPEED = 0.33f; + private static final String STUN_TOWER_ATTACK_ANIM = "attack"; + private static final float STUN_TOWER_ATTACK_SPEED = 0.12f; + private static final int INCOME_INTERVAL = 300; + private static final int INCOME_TASK_PRIORITY = 1; @@ -150,6 +182,44 @@ public static Entity createTNTTower() { return TNTTower; } + /** + * This robotic unit is programmed to detect mobs within its vicinity and fire projectiles at them. + * The droid has the capability to switch its aim from high to low positions, thereby providing a versatile attack strategy. + * When it detects a mob, the droid releases a projectile that inflicts both physical damage and a slow-down effect on the target. + * @return entity + */ + public static Entity createDroidTower() { + Entity DroidTower = createBaseTower(); + DroidTowerConfig config = configs.DroidTower; + + AITaskComponent aiTaskComponent = new AITaskComponent() + .addTask(new DroidCombatTask(COMBAT_TASK_PRIORITY, WEAPON_TOWER_MAX_RANGE)); + + AnimationRenderComponent animator = + new AnimationRenderComponent( + ServiceLocator.getResourceService() + .getAsset(DROID_ATLAS, TextureAtlas.class)); + + animator.addAnimation(IDLE_ANIM, DROID_SPEED, Animation.PlayMode.NORMAL); + animator.addAnimation(SHOOT_UP,DROID_SPEED, Animation.PlayMode.NORMAL); + animator.addAnimation(SHOOT_DOWN,DROID_SPEED, Animation.PlayMode.NORMAL); + animator.addAnimation(WALK_ANIM,DROID_SPEED, Animation.PlayMode.NORMAL); + animator.addAnimation(DEATH_ANIM,DROID_SPEED, Animation.PlayMode.NORMAL); + animator.addAnimation(GO_UP,DROID_SPEED, Animation.PlayMode.NORMAL); + animator.addAnimation(GO_DOWN,DROID_SPEED, Animation.PlayMode.NORMAL); + + + + DroidTower + .addComponent(new CombatStatsComponent(config.health, config.baseAttack)) + .addComponent(new CostComponent(config.cost)) + .addComponent(new DroidAnimationController()) + .addComponent(animator) + .addComponent(aiTaskComponent); + + return DroidTower; + } + /** * Creates a weaponry tower that shoots at mobs - This will most likely need to be extended @@ -184,6 +254,67 @@ public static Entity createWeaponTower() { return weapon; } + + /** + * Creates the FireTower entity which shoots at mobs traversing in a straight line. + * @return FireTower entity with relevant components. + */ + public static Entity createFireTower() { + Entity fireTower = createBaseTower(); + FireTowerConfig config = configs.fireTower; + + //Component that handles triggering events and animations + AITaskComponent aiTaskComponent = new AITaskComponent() + .addTask(new FireTowerCombatTask(COMBAT_TASK_PRIORITY, WEAPON_TOWER_MAX_RANGE)); + + AnimationRenderComponent animator = + new AnimationRenderComponent( + ServiceLocator.getResourceService() + .getAsset(FIRE_TOWER_ATLAS, TextureAtlas.class)); + animator.addAnimation(FIRE_TOWER_IDLE_ANIM, FIRE_TOWER_IDLE_SPEED, Animation.PlayMode.LOOP); + animator.addAnimation(FIRE_TOWER_PREP_ATTACK_ANIM, FIRE_TOWER_PREP_ATTACK_SPEED, Animation.PlayMode.NORMAL); + animator.addAnimation(FIRE_TOWER_ATTACK_ANIM, FIRE_TOWER_ATTACK_SPEED, Animation.PlayMode.LOOP); + + fireTower + .addComponent(new CombatStatsComponent(config.health, config.baseAttack)) + .addComponent(new CostComponent(config.cost)) + .addComponent(aiTaskComponent) + .addComponent(animator) + .addComponent(new FireTowerAnimationController()); + fireTower.setScale(1.25f, 1.25f); + return fireTower; + } + + /** + * Creates the StunTower entity which shoots at mobs traversing in a straight line. + * @return StunTower entity with relevant components. + */ + public static Entity createStunTower() { + Entity stunTower = createBaseTower(); + StunTowerConfig config = configs.stunTower; + + AITaskComponent aiTaskComponent = new AITaskComponent() + .addTask(new StunTowerCombatTask(COMBAT_TASK_PRIORITY, WEAPON_TOWER_MAX_RANGE)); + + AnimationRenderComponent animator = + new AnimationRenderComponent( + ServiceLocator.getResourceService() + .getAsset(STUN_TOWER_ATLAS, TextureAtlas.class)); + animator.addAnimation(STUN_TOWER_IDLE_ANIM, STUN_TOWER_IDLE_SPEED, Animation.PlayMode.LOOP); + animator.addAnimation(STUN_TOWER_ATTACK_ANIM, STUN_TOWER_ATTACK_SPEED, Animation.PlayMode.LOOP); + + stunTower + .addComponent(new CombatStatsComponent(config.health, config.baseAttack)) + .addComponent((new CostComponent(config.cost))) + .addComponent(aiTaskComponent) + .addComponent(animator) + .addComponent(new StunTowerAnimationController()); + + stunTower.setScale(1.5f, 1.5f); + PhysicsUtils.setScaledCollider(stunTower, 0.5f, 0.5f); + return stunTower; + } + /** * Creates a generic tower entity to be used as a base entity by more specific tower creation methods. * @return entity @@ -192,8 +323,11 @@ public static Entity createBaseTower() { // we're going to add more components later on Entity tower = new Entity() .addComponent(new ColliderComponent()) - .addComponent(new HitboxComponent().setLayer(PhysicsLayer.TOWER)) // TODO: we might have to change the names of the layers - .addComponent(new PhysicsComponent().setBodyType(BodyType.StaticBody)); + .addComponent(new PhysicsComponent().setBodyType(BodyType.StaticBody)) + .addComponent(new TowerUpgraderComponent()) + .addComponent(new HitboxComponent().setLayer(PhysicsLayer.TOWER)); // TODO: we might have to change the names of the layers + + return tower; } diff --git a/source/core/src/main/com/csse3200/game/physics/PhysicsEngine.java b/source/core/src/main/com/csse3200/game/physics/PhysicsEngine.java index da988fec7..017b18278 100644 --- a/source/core/src/main/com/csse3200/game/physics/PhysicsEngine.java +++ b/source/core/src/main/com/csse3200/game/physics/PhysicsEngine.java @@ -45,7 +45,7 @@ public PhysicsEngine(World world, GameTime timeSource) { public void update() { // Check for deleted bodies and joints - // checkAndDeleteBodies(); + checkAndDeleteBodies(); // Updating physics isn't as easy as triggering an update every frame. Each frame could take a // different amount of time to run, but physics simulations are only stable if computed at a @@ -90,19 +90,17 @@ public void checkAndDeleteBodies() { world.getBodies(bodies); - // Check for bodies to be deleted - for (Body body : bodies) { - // check for null values - if (body.getUserData() != null) { - Entity entity = ((BodyUserData) body.getUserData()).entity; - // If the entity is flagged for deletion, destroy the body before world.step() is called - if (entity.getFlagForDelete()) { - logger.debug("Destroying physics body {}", body); - ProjectileDestructors.destroyProjectile(entity); - - // Make sure not to delete the body twice - entity.setFlagForDelete(false); - } + // ! CANNOT USE ITERATOR HERE + // ! If you do: "ERROR: #ITERATOR CAN'T BE NESTED" + for(int i = 0; i < bodies.size; i++) { + if(bodies.get(i) != null + && bodies.get(i).getUserData() != null + && (BodyUserData) bodies.get(i).getUserData() != null) { + Entity entity = ((BodyUserData) bodies.get(i).getUserData()).entity; + + if(entity.getFlagForDelete()) { + entity.dispose(); + } } } } diff --git a/source/core/src/main/com/csse3200/game/physics/PhysicsLayer.java b/source/core/src/main/com/csse3200/game/physics/PhysicsLayer.java index b91b9bfcc..437e46b79 100644 --- a/source/core/src/main/com/csse3200/game/physics/PhysicsLayer.java +++ b/source/core/src/main/com/csse3200/game/physics/PhysicsLayer.java @@ -10,6 +10,8 @@ public class PhysicsLayer { public static final short NPC = (1 << 3); public static final short PROJECTILE = (1 << 4); public static final short TOWER = (1 << 5); + // * TEMPORARY WALL BOUNDARIES? + public static final short WALL = (1 << 6); public static final short XENO = (1 << 3); public static final short HUMANS = (1 << 1) | (1 << 5); diff --git a/source/core/src/main/com/csse3200/game/rendering/AnimationRenderComponent.java b/source/core/src/main/com/csse3200/game/rendering/AnimationRenderComponent.java index 8f52f18bb..0143567f3 100644 --- a/source/core/src/main/com/csse3200/game/rendering/AnimationRenderComponent.java +++ b/source/core/src/main/com/csse3200/game/rendering/AnimationRenderComponent.java @@ -179,7 +179,7 @@ protected void draw(SpriteBatch batch) { @Override public void dispose() { - // atlas.dispose(); + // atlas.dispose(); // this has to be disabled to keep the atlas file for other entities that rely on it super.dispose(); } } diff --git a/source/core/src/test/com/csse3200/game/components/player/HumanAnimationControllerTest.java b/source/core/src/test/com/csse3200/game/components/player/HumanAnimationControllerTest.java index 99019845a..1f8f063d6 100644 --- a/source/core/src/test/com/csse3200/game/components/player/HumanAnimationControllerTest.java +++ b/source/core/src/test/com/csse3200/game/components/player/HumanAnimationControllerTest.java @@ -58,6 +58,7 @@ void setUp() { resourceService.loadSounds(sounds); resourceService.loadAll(); engineer = EngineerFactory.createEngineer(); + engineer.create(); } @AfterEach @@ -70,46 +71,45 @@ void shouldHaveAnimationController() { "Created Engineer entity should have a HumanAnimationController"); } -// @Test -// void shouldAnimateIdleRight() { -// engineer.getEvents().trigger("idleStart"); -// when(gameTime.getDeltaTime()).thenReturn(0.1f); -// assertEquals("idle_right", engineer.getComponent(AnimationRenderComponent.class).getCurrentAnimation(), -// "'idleStart' event should trigger 'idle_right' animation'"); -// } -// -// @Test -// void animateLeftWalk() { -// engineer.getEvents().trigger("walkLeftStart"); -// assertEquals("walk_left", engineer.getComponent(AnimationRenderComponent.class).getCurrentAnimation(), -// "'walkLeftStart' event should trigger 'walk_left' animation'"); -// } -// -// @Test -// void animateRightWalk() { -// engineer.getEvents().trigger("walkRightStart"); -// assertEquals("walk_right", engineer.getComponent(AnimationRenderComponent.class).getCurrentAnimation(), -// "'walkRightStart' event should trigger 'walk_right' animation'"); -// } -// -// @Test -// void animateFiring() { -// engineer.getEvents().trigger("firingSingleStart"); -// assertEquals("firing_single", engineer.getComponent(AnimationRenderComponent.class).getCurrentAnimation(), -// "'firingSingleStart' event should trigger 'firing_single' animation'"); -// } -// -// @Test -// void animateHit() { -// engineer.getEvents().trigger("hitStart"); -// assertEquals("hit", engineer.getComponent(AnimationRenderComponent.class).getCurrentAnimation(), -// "'hitStart' event should trigger 'hit' animation'"); -// } -// -// @Test -// void animateDeath() { -// engineer.getEvents().trigger("hitStart"); -// assertEquals("death", engineer.getComponent(AnimationRenderComponent.class).getCurrentAnimation(), -// "'deathStart' event should trigger 'death' animation'"); -// } + @Test + void shouldAnimateIdleRight() { + engineer.getEvents().trigger("idleRight"); + assertEquals("idle_right", engineer.getComponent(AnimationRenderComponent.class).getCurrentAnimation(), + "'idleRight' event should trigger 'idle_right' animation'"); + } + + @Test + void shouldAnimateLeftWalk() { + engineer.getEvents().trigger("walkLeftStart"); + assertEquals("walk_left", engineer.getComponent(AnimationRenderComponent.class).getCurrentAnimation(), + "'walkLeftStart' event should trigger 'walk_left' animation'"); + } + + @Test + void shouldAnimateRightWalk() { + engineer.getEvents().trigger("walkRightStart"); + assertEquals("walk_right", engineer.getComponent(AnimationRenderComponent.class).getCurrentAnimation(), + "'walkRightStart' event should trigger 'walk_right' animation'"); + } + + @Test + void shoudlAnimateFiring() { + engineer.getEvents().trigger("firingSingleStart"); + assertEquals("firing_single", engineer.getComponent(AnimationRenderComponent.class).getCurrentAnimation(), + "'firingSingleStart' event should trigger 'firing_single' animation'"); + } + + @Test + void shouldAnimateHit() { + engineer.getEvents().trigger("hitStart"); + assertEquals("hit", engineer.getComponent(AnimationRenderComponent.class).getCurrentAnimation(), + "'hitStart' event should trigger 'hit' animation'"); + } + + @Test + void shouldAnimateDeath() { + engineer.getEvents().trigger("deathStart"); + assertEquals("death", engineer.getComponent(AnimationRenderComponent.class).getCurrentAnimation(), + "'deathStart' event should trigger 'death' animation'"); + } } \ No newline at end of file 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/WanderTaskTest.java b/source/core/src/test/com/csse3200/game/components/tasks/MobWanderTaskTest.java similarity index 61% rename from source/core/src/test/com/csse3200/game/components/tasks/WanderTaskTest.java rename to source/core/src/test/com/csse3200/game/components/tasks/MobWanderTaskTest.java index 28fedd6c5..46b2a2ab9 100644 --- a/source/core/src/test/com/csse3200/game/components/tasks/WanderTaskTest.java +++ b/source/core/src/test/com/csse3200/game/components/tasks/MobWanderTaskTest.java @@ -19,7 +19,7 @@ @ExtendWith(GameExtension.class) @ExtendWith(MockitoExtension.class) -class WanderTaskTest { +class MobWanderTaskTest { @Mock GameTime gameTime; @@ -28,20 +28,20 @@ void beforeEach() { ServiceLocator.registerTimeSource(gameTime); } - @Test - void shouldTriggerEvent() { - WanderTask wanderTask = new WanderTask(Vector2Utils.ONE, 1f); - - AITaskComponent aiTaskComponent = new AITaskComponent().addTask(wanderTask); - Entity entity = new Entity().addComponent(aiTaskComponent).addComponent(new PhysicsMovementComponent()); - entity.create(); - - // Register callbacks - EventListener0 callback = mock(EventListener0.class); - entity.getEvents().addListener("wanderStart", callback); - - wanderTask.start(); - - verify(callback).handle(); - } +// @Test +// void shouldTriggerEvent() { +// MobWanderTask mobWanderTask = new MobWanderTask(Vector2Utils.ONE, 1f); +// +// AITaskComponent aiTaskComponent = new AITaskComponent().addTask(mobWanderTask); +// Entity entity = new Entity().addComponent(aiTaskComponent).addComponent(new PhysicsMovementComponent()); +// entity.create(); +// +// // Register callbacks +// EventListener0 callback = mock(EventListener0.class); +// entity.getEvents().addListener("wanderStart", callback); +// +// mobWanderTask.start(); +// +// verify(callback).handle(); +// } } \ No newline at end of file diff --git a/source/core/src/test/com/csse3200/game/components/tasks/SpawnWaveTaskTest.java b/source/core/src/test/com/csse3200/game/components/tasks/SpawnWaveTaskTest.java new file mode 100644 index 000000000..55a00537d --- /dev/null +++ b/source/core/src/test/com/csse3200/game/components/tasks/SpawnWaveTaskTest.java @@ -0,0 +1,42 @@ +package com.csse3200.game.components.tasks; + +import com.csse3200.game.ai.tasks.AITaskComponent; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.events.listeners.EventListener0; +import com.csse3200.game.extensions.GameExtension; +import com.csse3200.game.utils.math.Vector2Utils; +import com.csse3200.game.physics.components.PhysicsMovementComponent; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.Mockito.*; + +@ExtendWith(GameExtension.class) +@ExtendWith(MockitoExtension.class) +class SpawnWaveTaskTest { + + @Test + void shouldTriggerSpawning() { + GameTime time = mock(GameTime.class); + when(time.getTime()).thenReturn(11000L); + ServiceLocator.registerTimeSource(time); + SpawnWaveTask waveTask = new SpawnWaveTask(); + + AITaskComponent aiTaskComponent = new AITaskComponent().addTask(waveTask); + Entity entity = new Entity().addComponent(aiTaskComponent).addComponent(new PhysicsMovementComponent()); + entity.create(); + + // Register callbacks + EventListener0 callback = mock(EventListener0.class); + entity.getEvents().addListener("spawnWave", callback); + + waveTask.update(); + + verify(callback).handle(); + } +} \ No newline at end of file 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; + } + + +} diff --git a/source/core/src/test/com/csse3200/game/components/tasks/WaitTaskTest.java b/source/core/src/test/com/csse3200/game/components/tasks/WaitTaskTest.java index 6ba90f421..59e7ea7c8 100644 --- a/source/core/src/test/com/csse3200/game/components/tasks/WaitTaskTest.java +++ b/source/core/src/test/com/csse3200/game/components/tasks/WaitTaskTest.java @@ -14,7 +14,6 @@ @ExtendWith(GameExtension.class) class WaitTaskTest { - @Disabled("Testing without use of WaitTask") @Test void shouldWaitUntilTime() { GameTime time = mock(GameTime.class); diff --git a/source/core/src/test/com/csse3200/game/components/tasks/human/HumanMovementTaskTest.java b/source/core/src/test/com/csse3200/game/components/tasks/human/HumanMovementTaskTest.java deleted file mode 100644 index 4deb83386..000000000 --- a/source/core/src/test/com/csse3200/game/components/tasks/human/HumanMovementTaskTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.csse3200.game.components.tasks.human; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -class HumanMovementTaskTest { - - @BeforeEach - void setUp() { - } - - @AfterEach - void tearDown() { - } - - @Test - void start() { - } - - @Test - void update() { - } - - @Test - void setTarget() { - } - - @Test - void stop() { - } -} \ No newline at end of file diff --git a/source/core/src/test/com/csse3200/game/components/tasks/human/HumanWanderTaskTest.java b/source/core/src/test/com/csse3200/game/components/tasks/human/HumanWanderTaskTest.java index 5a24deb7c..6e7c9eaeb 100644 --- a/source/core/src/test/com/csse3200/game/components/tasks/human/HumanWanderTaskTest.java +++ b/source/core/src/test/com/csse3200/game/components/tasks/human/HumanWanderTaskTest.java @@ -1,30 +1,104 @@ package com.csse3200.game.components.tasks.human; +import com.csse3200.game.components.TouchAttackComponent; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.factories.EngineerFactory; +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; import org.junit.jupiter.api.AfterEach; 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; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +@ExtendWith(GameExtension.class) +@ExtendWith(MockitoExtension.class) class HumanWanderTaskTest { + /** + * Class for testing the HumanWanderTask, adapted from WanderTaskTest by + * Jonathan Tang + */ + + Entity owner; + + private final String[] atlas = {"images/engineers/engineer.atlas"}; + private static final String[] sounds = { + "sounds/engineers/firing_auto.mp3", + "sounds/engineers/firing_single.mp3" + }; @BeforeEach void setUp() { + GameTime gameTime = new GameTime(); + PhysicsService physics = new PhysicsService(); + ServiceLocator.registerTimeSource(gameTime); + ServiceLocator.registerPhysicsService(physics); + RenderService render = new RenderService(); + render.setDebug(mock(DebugRenderer.class)); + ServiceLocator.registerRenderService(render); + ResourceService resourceService = new ResourceService(); + ServiceLocator.registerResourceService(resourceService); + resourceService.loadTextureAtlases(atlas); + resourceService.loadSounds(sounds); + resourceService.loadAll(); + owner = EngineerFactory.createEngineer(); + owner.create(); } @AfterEach void tearDown() { } + @Test + void start() { + + } + @Test void getPriority() { } @Test - void start() { + void shouldStartWaiting() { + + } + + @Test + void shouldStartMoving() { + + } + + @Test + void shouldStartCombat() { + + } + + @Test + void shouldSwapTask() { + } @Test void update() { } + + Entity createEnemy() { + Entity enemy = mock(Entity.class); + enemy + .addComponent(new HitboxComponent().setLayer(PhysicsLayer.NPC)) + .addComponent(new PhysicsComponent()) + .addComponent(new TouchAttackComponent(PhysicsLayer.ENGINEER)); + return enemy; + } } \ No newline at end of file diff --git a/source/core/src/test/com/csse3200/game/components/tower/DroidAnimationControllerTest.java b/source/core/src/test/com/csse3200/game/components/tower/DroidAnimationControllerTest.java new file mode 100644 index 000000000..666c9767a --- /dev/null +++ b/source/core/src/test/com/csse3200/game/components/tower/DroidAnimationControllerTest.java @@ -0,0 +1,84 @@ +package com.csse3200.game.components.tower; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.factories.TowerFactory; +import com.csse3200.game.extensions.GameExtension; +import com.csse3200.game.physics.PhysicsService; +import com.csse3200.game.rendering.AnimationRenderComponent; +import com.csse3200.game.rendering.DebugRenderer; +import com.csse3200.game.rendering.RenderService; +import com.csse3200.game.services.GameTime; +import com.csse3200.game.services.ResourceService; +import com.csse3200.game.services.ServiceLocator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(GameExtension.class) +public class DroidAnimationControllerTest { + + private Entity mockEntity; + private final String[] texture = {"images/towers/DroidTower.png"}; + private final String[] atlas = {"images/towers/DroidTower.atlas"}; + + @BeforeEach + public void setUp() { + ServiceLocator.registerPhysicsService(new PhysicsService()); + RenderService render = new RenderService(); + render.setDebug(mock(DebugRenderer.class)); + ServiceLocator.registerRenderService(render); + ResourceService resourceService = new ResourceService(); + ServiceLocator.registerResourceService(resourceService); + resourceService.loadTextures(texture); + resourceService.loadTextureAtlases(atlas); + resourceService.loadAll(); + + mockEntity = TowerFactory.createDroidTower(); // Replace with actual Droid Tower creation logic + mockEntity.create(); + } + + @Test + public void testAnimateWalk() { + mockEntity.getEvents().trigger("walkStart"); + assertEquals("walk", mockEntity.getComponent(AnimationRenderComponent.class).getCurrentAnimation()); + } + + @Test + public void testAnimateDefault() { + mockEntity.getEvents().trigger("idleStart"); + assertEquals("idle", mockEntity.getComponent(AnimationRenderComponent.class).getCurrentAnimation()); + } + + @Test + public void testAnimateGoUp() { + mockEntity.getEvents().trigger("goUpStart"); + assertEquals("goUp", mockEntity.getComponent(AnimationRenderComponent.class).getCurrentAnimation()); + } + + @Test + public void testAnimateGoDown() { + mockEntity.getEvents().trigger("goDownStart"); + assertEquals("goDown", mockEntity.getComponent(AnimationRenderComponent.class).getCurrentAnimation()); + } + + @Test + public void testAnimateAttackUp() { + mockEntity.getEvents().trigger("attackUpStart"); + assertEquals("attackUp", mockEntity.getComponent(AnimationRenderComponent.class).getCurrentAnimation()); + } + + @Test + public void testAnimateAttackDown() { + mockEntity.getEvents().trigger("attackDownStart"); + assertEquals("attackDown", mockEntity.getComponent(AnimationRenderComponent.class).getCurrentAnimation()); + } + + @Test + public void testAnimateDeath() { + mockEntity.getEvents().trigger("deathStart"); + assertEquals("death", mockEntity.getComponent(AnimationRenderComponent.class).getCurrentAnimation()); + } +} diff --git a/source/core/src/test/com/csse3200/game/components/tower/TNTAnimationControllerTest.java b/source/core/src/test/com/csse3200/game/components/tower/TNTAnimationControllerTest.java new file mode 100644 index 000000000..bdaa14041 --- /dev/null +++ b/source/core/src/test/com/csse3200/game/components/tower/TNTAnimationControllerTest.java @@ -0,0 +1,76 @@ +package com.csse3200.game.components.tower; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +import com.badlogic.gdx.graphics.g2d.Animation; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.factories.TowerFactory; +import com.csse3200.game.extensions.GameExtension; +import com.csse3200.game.physics.PhysicsService; +import com.csse3200.game.rendering.AnimationRenderComponent; +import com.csse3200.game.rendering.DebugRenderer; +import com.csse3200.game.rendering.RenderService; +import com.csse3200.game.services.GameTime; +import com.csse3200.game.services.ResourceService; +import com.csse3200.game.services.ServiceLocator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + + + +@ExtendWith(GameExtension.class) +public class TNTAnimationControllerTest { + + private Entity mockEntity; + private final String[] texture = {"images/towers/TNTTower.png"}; + private final String[] atlas = {"images/towers/TNTTower.atlas"}; + + + @BeforeEach + public void setUp() { + // Initialize the TNTAnimationController object + ServiceLocator.registerPhysicsService(new PhysicsService()); + RenderService render = new RenderService(); + render.setDebug(mock(DebugRenderer.class)); + ServiceLocator.registerRenderService(render); + ResourceService resourceService = new ResourceService(); + ServiceLocator.registerResourceService(resourceService); + resourceService.loadTextures(texture); + resourceService.loadTextureAtlases(atlas); + resourceService.loadAll(); + + mockEntity = TowerFactory.createTNTTower(); + mockEntity.create(); + } + + @Test + public void testAnimateDig() { + + // Trigger the animateDig method + mockEntity.getEvents().trigger("digStart"); + + // Verify if the "dig" animation was started + assertEquals("dig",mockEntity.getComponent(AnimationRenderComponent.class).getCurrentAnimation()); + } + + @Test + public void testAnimateDefault() { + // Trigger the animateDefault method + mockEntity.getEvents().trigger("defaultStart"); + + // Verify if the "default" animation was started + assertEquals("default",mockEntity.getComponent(AnimationRenderComponent.class).getCurrentAnimation()); + } + + @Test + public void testAnimateExplode() { + // Trigger the animateExplode method + mockEntity.getEvents().trigger("explodeStart"); + + // Verify if the "explode" animation was started + assertEquals("explode",mockEntity.getComponent(AnimationRenderComponent.class).getCurrentAnimation()); + } +} diff --git a/source/core/src/test/com/csse3200/game/components/tower/TNTDamageComponentTest.java b/source/core/src/test/com/csse3200/game/components/tower/TNTDamageComponentTest.java new file mode 100644 index 000000000..1b8f988aa --- /dev/null +++ b/source/core/src/test/com/csse3200/game/components/tower/TNTDamageComponentTest.java @@ -0,0 +1,119 @@ +package com.csse3200.game.components.tower; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +import com.csse3200.game.components.CombatStatsComponent; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.EntityService; +import com.csse3200.game.extensions.GameExtension; +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; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(GameExtension.class) +public class TNTDamageComponentTest { + + private GameTime gameTime; + private Entity Attacker; + private Entity Target_1; + private Entity Target_2; + private Entity Entity_3; + private final String[] texture = {"images/towers/TNTTower.png"}; + private final String[] atlas = {"images/towers/TNTTower.atlas"}; + + @BeforeEach + public void setUp() { + gameTime = mock(GameTime.class); + when(gameTime.getDeltaTime()).thenReturn(1f); + ServiceLocator.registerTimeSource(gameTime); + ServiceLocator.registerPhysicsService(new PhysicsService()); + ServiceLocator.registerEntityService(new EntityService()); + RenderService render = new RenderService(); + render.setDebug(mock(DebugRenderer.class)); + ServiceLocator.registerRenderService(render); + ResourceService resourceService = new ResourceService(); + ServiceLocator.registerResourceService(resourceService); + resourceService.loadTextures(texture); + resourceService.loadTextureAtlases(atlas); + resourceService.loadAll(); + Attacker = createAttacker((short) (1 << 3)); + Target_1 = createTarget((short) (1 << 3)); + Target_2 = createTarget((short) (1 << 3)); + Entity_3 = createTarget((short) (1 << 4)); + ServiceLocator.getEntityService().register(Attacker); + ServiceLocator.getEntityService().register(Target_1); + ServiceLocator.getEntityService().register(Target_2); + ServiceLocator.getEntityService().register(Entity_3); + + + } + + @Test + public void TestTNTDamageOnTargetsThatAreInRange() { + Attacker.setPosition(10,10); + Target_1.setPosition(12,10);// Same lane and inside radius + + Attacker.getEvents().trigger("TNTDamageStart"); + + assertEquals(80, Target_1.getComponent(CombatStatsComponent.class).getHealth()); + + + } + + @Test + public void TestTNTDamageOnTargetsThatAreNoTInRange() { + Attacker.setPosition(10,10); + Target_1.setPosition(15,10); // on the same lane but outside the radius + Target_2.setPosition(11,12); // inside the radius but outside the lane + + + Attacker.getEvents().trigger("TNTDamageStart"); + //Nothing should happen + assertEquals(100, Target_2.getComponent(CombatStatsComponent.class).getHealth()); + assertEquals(100, Target_1.getComponent(CombatStatsComponent.class).getHealth()); + } + + @Test + public void TestTNTDamageOnEntitiesThatAreNotTargets() { + Attacker.setPosition(10,10); + Entity_3.setPosition(12,10); // on the same lane and inside the radius but different target layer + + + Attacker.getEvents().trigger("TNTDamageStart"); + //Nothing should happen + assertEquals(100, Entity_3.getComponent(CombatStatsComponent.class).getHealth()); + + } + + Entity createAttacker(short targetLayer) { + Entity entity = + new Entity() + .addComponent(new CombatStatsComponent(100, 20)) + .addComponent(new PhysicsComponent()) + .addComponent(new HitboxComponent()) + .addComponent(new TNTDamageComponent(targetLayer,0,4)); + + return entity; + } + + Entity createTarget(short layer) { + Entity target = + new Entity() + .addComponent(new CombatStatsComponent(100, 10)) + .addComponent(new PhysicsComponent()) + .addComponent(new HitboxComponent().setLayer(layer)); + + return target; + } +} + + diff --git a/source/core/src/test/com/csse3200/game/components/tower/TowerUpgraderComponentTest.java b/source/core/src/test/com/csse3200/game/components/tower/TowerUpgraderComponentTest.java new file mode 100644 index 000000000..703a1299b --- /dev/null +++ b/source/core/src/test/com/csse3200/game/components/tower/TowerUpgraderComponentTest.java @@ -0,0 +1,81 @@ +package com.csse3200.game.components.tower; + +import com.csse3200.game.ai.tasks.AITaskComponent; +import com.csse3200.game.components.CombatStatsComponent; +import com.csse3200.game.components.tasks.TowerCombatTask; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.extensions.GameExtension; +import com.csse3200.game.physics.PhysicsService; +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 static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(GameExtension.class) +class TowerUpgraderComponentTest { + Entity entity; + TowerUpgraderComponent towerUpgraderComponent; + CombatStatsComponent combatStatsComponent; + + @BeforeEach + void beforeEach() { + entity = new Entity(); + towerUpgraderComponent = spy(TowerUpgraderComponent.class); + combatStatsComponent = new CombatStatsComponent(100,10); + } + + @Test + void increaseAttackStat() { + entity.addComponent(towerUpgraderComponent); + entity.addComponent(combatStatsComponent); + entity.create(); + entity.getEvents().trigger("upgradeTower", TowerUpgraderComponent.UPGRADE.ATTACK, 10); + verify(towerUpgraderComponent).upgradeTower(TowerUpgraderComponent.UPGRADE.ATTACK, 10); + assertEquals(20, combatStatsComponent.getBaseAttack()); + } + + @Test + void increaseMaxHealthStat() { + entity.addComponent(towerUpgraderComponent); + entity.addComponent(combatStatsComponent); + entity.create(); + entity.getEvents().trigger("upgradeTower", TowerUpgraderComponent.UPGRADE.MAXHP, 50); + verify(towerUpgraderComponent).upgradeTower(TowerUpgraderComponent.UPGRADE.MAXHP, 50); + assertEquals(150, combatStatsComponent.getMaxHealth()); + } + + @Test + void increaseFireRate() { + entity.addComponent(towerUpgraderComponent); + AITaskComponent aiTaskComponent = new AITaskComponent(); + ServiceLocator.registerPhysicsService(mock(PhysicsService.class)); + ServiceLocator.registerTimeSource(mock(GameTime.class)); + TowerCombatTask towerCombatTask = new TowerCombatTask(10, 10, 1); + aiTaskComponent.addTask(towerCombatTask); + entity.addComponent(aiTaskComponent); + towerCombatTask.start(); + entity.create(); + entity.getEvents().trigger("upgradeTower", TowerUpgraderComponent.UPGRADE.FIRERATE, 60); + verify(towerUpgraderComponent).upgradeTower(TowerUpgraderComponent.UPGRADE.FIRERATE, 60); + assertEquals(0.5, towerCombatTask.getFireRateInterval()); + } + + @Test + void divideByZeroDefaultToIgnore() { + entity.addComponent(towerUpgraderComponent); + AITaskComponent aiTaskComponent = new AITaskComponent(); + ServiceLocator.registerPhysicsService(mock(PhysicsService.class)); + ServiceLocator.registerTimeSource(mock(GameTime.class)); + TowerCombatTask towerCombatTask = new TowerCombatTask(10, 10, 1); + aiTaskComponent.addTask(towerCombatTask); + entity.addComponent(aiTaskComponent); + towerCombatTask.start(); + entity.create(); + entity.getEvents().trigger("upgradeTower", TowerUpgraderComponent.UPGRADE.FIRERATE, -60); + verify(towerUpgraderComponent).upgradeTower(TowerUpgraderComponent.UPGRADE.FIRERATE, -60); + assertEquals(1., towerCombatTask.getFireRateInterval()); + } +} diff --git a/source/core/src/test/com/csse3200/game/entities/factories/EngineerFactoryTest.java b/source/core/src/test/com/csse3200/game/entities/factories/EngineerFactoryTest.java index 3d80accb0..d130ae5c7 100644 --- a/source/core/src/test/com/csse3200/game/entities/factories/EngineerFactoryTest.java +++ b/source/core/src/test/com/csse3200/game/entities/factories/EngineerFactoryTest.java @@ -55,7 +55,7 @@ class EngineerFactoryTest { "firing_single", "hit", "death" - };; + }; @BeforeEach void setUp() { diff --git a/source/core/src/test/com/csse3200/game/entities/factories/NPCFactoryTest.java b/source/core/src/test/com/csse3200/game/entities/factories/NPCFactoryTest.java new file mode 100644 index 000000000..9c02ba0c4 --- /dev/null +++ b/source/core/src/test/com/csse3200/game/entities/factories/NPCFactoryTest.java @@ -0,0 +1,106 @@ +package com.csse3200.game.entities.factories; + +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.csse3200.game.components.CombatStatsComponent; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.extensions.GameExtension; +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.rendering.AnimationRenderComponent; +import com.csse3200.game.rendering.DebugRenderer; +import com.csse3200.game.rendering.RenderService; +import com.csse3200.game.services.GameTime; +import com.csse3200.game.services.ResourceService; +import com.csse3200.game.services.ServiceLocator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(GameExtension.class) +public class NPCFactoryTest { + + private Entity xenoGrunt; + private Entity towerTarget; + private Entity engineerTarget; + private Entity playerTarget; + + private String[] texture = { + "images/towers/turret_deployed.png", + "images/towers/turret01.png", + "images/towers/wallTower.png" + }; + private String[] atlas = {"images/towers/turret01.atlas", + "images/mobs/xenoGrunt.atlas"}; + private static final String[] sounds = { + "sounds/towers/gun_shot_trimmed.mp3", + "sounds/towers/deploy.mp3", + "sounds/towers/stow.mp3" + }; + + + @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.loadTextures(texture); + resourceService.loadTextureAtlases(atlas); + resourceService.loadSounds(sounds); + resourceService.loadAll(); + ServiceLocator.getResourceService() + .getAsset("images/mobs/xenoGrunt.atlas", TextureAtlas.class); + //playerTarget = PlayerFactory.createPlayer(); + towerTarget = TowerFactory.createBaseTower(); + //engineerTarget = EngineerFactory.createEngineer(); + xenoGrunt = NPCFactory.createXenoGrunt(playerTarget); + } + + @Test + public void testCreateXenoGruntNotNull() { + assertNotNull(xenoGrunt, "Xeno Grunt should not be null"); + } + + @Test + public void testCreateXenoGruntHasColliderComponent() { + assertNotNull(xenoGrunt.getComponent(ColliderComponent.class), + "Xeno Grunt should have ColliderComponent"); + } + + @Test + public void testCreateXenoGruntHasHitboxComponent() { + assertNotNull(xenoGrunt.getComponent(HitboxComponent.class), + "Xeno Grunt should have HitboxComponent"); + } + + @Test + public void testCreateXenoGruntHasPhysicsComponent() { + assertNotNull(xenoGrunt.getComponent(PhysicsComponent.class), + "Xeno Grunt should have PhysicsComponent"); + } + + @Test + public void testXenoGruntCombatStatsComponent() { + assertEquals(100, xenoGrunt.getComponent(CombatStatsComponent.class).getHealth(), + "Health should be 100"); + assertEquals(10, xenoGrunt.getComponent(CombatStatsComponent.class).getBaseAttack(), + "BaseAttack should be 10"); + } + + @Test + public void xenoGruntHasAnimationComponent() { + assertNotNull(xenoGrunt.getComponent(AnimationRenderComponent.class)); + } + +} diff --git a/source/wiki/team-2/EngineerFactory Sequence Diagram.png b/source/wiki/team-2/EngineerFactory Sequence Diagram.png new file mode 100644 index 000000000..b8ec04420 Binary files /dev/null and b/source/wiki/team-2/EngineerFactory Sequence Diagram.png differ diff --git a/source/wiki/team-2/EngineerFactory Sequence Diagram.svg b/source/wiki/team-2/EngineerFactory Sequence Diagram.svg new file mode 100644 index 000000000..42ee4e04b --- /dev/null +++ b/source/wiki/team-2/EngineerFactory Sequence Diagram.svg @@ -0,0 +1,210 @@ + + + diff --git a/source/wiki/team-2/EngineerFactory UML.png b/source/wiki/team-2/EngineerFactory UML.png new file mode 100644 index 000000000..3aa08fefb Binary files /dev/null and b/source/wiki/team-2/EngineerFactory UML.png differ diff --git a/source/wiki/team-2/EngineerFactory and GapScannerFactory Sequence Diagram.png b/source/wiki/team-2/EngineerFactory and GapScannerFactory Sequence Diagram.png new file mode 100644 index 000000000..b8a09a56e Binary files /dev/null and b/source/wiki/team-2/EngineerFactory and GapScannerFactory Sequence Diagram.png differ diff --git a/source/wiki/team-2/HumanWanderTask Sequence Diagram.png b/source/wiki/team-2/HumanWanderTask Sequence Diagram.png new file mode 100644 index 000000000..c23337d2e Binary files /dev/null and b/source/wiki/team-2/HumanWanderTask Sequence Diagram.png differ