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/flat-earth/skin/flat-earth-ui.json b/source/core/assets/flat-earth/skin/flat-earth-ui.json index ba7414e6f..3337baaf1 100644 --- a/source/core/assets/flat-earth/skin/flat-earth-ui.json +++ b/source/core/assets/flat-earth/skin/flat-earth-ui.json @@ -327,7 +327,7 @@ com.badlogic.gdx.scenes.scene2d.ui.Label$LabelStyle: { default: { font: font - fontColor: black + fontColor: white } button: { font: button 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/GdxGame.java b/source/core/src/main/com/csse3200/game/GdxGame.java index 42fdb3f7b..43f525990 100644 --- a/source/core/src/main/com/csse3200/game/GdxGame.java +++ b/source/core/src/main/com/csse3200/game/GdxGame.java @@ -77,13 +77,15 @@ private Screen newScreen(ScreenType screenType) { return new LevelSelectScreen(this); case LOSING_SCREEN: return new LosingScreen(this); + case TURRET_SELECTION: + return new TurretSelectionScreen(this); default: return null; } } public enum ScreenType { - MAIN_MENU, MAIN_GAME, SETTINGS, STORY_SCREEN, LEVEL_SELECT, LOSING_SCREEN + MAIN_MENU, MAIN_GAME, SETTINGS, STORY_SCREEN, LEVEL_SELECT, TURRET_SELECTION, LOSING_SCREEN } /** 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 c53e75268..f93c6c5f4 100644 --- a/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java +++ b/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java @@ -17,6 +17,8 @@ import com.csse3200.game.components.gamearea.GameAreaDisplay; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import java.util.Random; import java.util.Timer; @@ -52,7 +54,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 +73,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 +100,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", @@ -106,12 +118,23 @@ public class ForestGameArea extends GameArea { "images/ghostKing.atlas", "images/towers/turret.atlas", "images/towers/turret01.atlas", + "images/towers/fire_tower_atlas.atlas", + "images/towers/stun_tower.atlas", + "images/mobs/xenoGruntRunning.atlas", + "images/mobs/robot.atlas", + "images/mobs/rangeBossRight.atlas", + "images/towers/DroidTower.atlas", "images/xenoGrunt.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 +142,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}; @@ -160,16 +185,28 @@ public void create() { 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(); spawnScrap(); spawnIncome(); spawnGhosts(); spawnWeaponTower(); + spawnTNTTower(); + spawnDroidTower(); spawnEngineer(); + spawnIncome(); bossKing1 = spawnBossKing1(); bossKing2 = spawnBossKing2(); - spawnTNTTower(); + + + } private void displayUI() { @@ -190,34 +227,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); @@ -239,6 +273,7 @@ private void spawnMountains() { } } + private Entity spawnPlayer() { Entity newPlayer = PlayerFactory.createPlayer(); spawnEntityAt(newPlayer, PLAYER_SPAWN, true, true); @@ -298,8 +333,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); } @@ -348,17 +383,17 @@ private void spawnXenoGrunts() { } } - 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); @@ -388,6 +423,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++) { @@ -396,6 +432,7 @@ private void spawnMultiProjectile(Vector2 position, short targetLayer, int direc } } + /** * Returns projectile that can do an area of effect damage * @@ -413,16 +450,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); } } @@ -438,6 +529,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); @@ -493,6 +596,7 @@ private void spawnScrap() { } } + private void spawnIncome() { GridPoint2 minPos = new GridPoint2(0, 0); GridPoint2 maxPos = terrain.getMapBounds(0).sub(2, 2); @@ -510,11 +614,5 @@ private void spawnEngineer() { Entity engineer = EngineerFactory.createEngineer(); spawnEntityAt(engineer, new GridPoint2(1, i), true, true); } -// GridPoint2 minPos = new GridPoint2(0, 0); -// GridPoint2 maxPos = new GridPoint2(5, terrain.getMapBounds(0).sub(2, 2).y); -// GridPoint2 randomPos = RandomUtils.random(minPos, maxPos); -// -// Entity engineer = EngineerFactory.createEngineer(); -// spawnEntityAt(engineer, randomPos, true, true); } } \ No newline at end of file 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 c42945db3..9570f7d25 100644 --- a/source/core/src/main/com/csse3200/game/components/TouchAttackComponent.java +++ b/source/core/src/main/com/csse3200/game/components/TouchAttackComponent.java @@ -11,6 +11,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. * @@ -103,6 +104,10 @@ private void onCollisionStart(Fixture me, Fixture other) { public void setDisposeOnHit(boolean disposeOnHit) { this.disposeOnHit = disposeOnHit; } + + public void setKnockBack(float knockback) { + this.knockbackForce = knockback; + } // private void onCollisionEnd(Fixture me, Fixture other) { // // Nothing to do on collision end // } 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..57b91f06e --- /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.components.ProjectileEffects; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.factories.ProjectileFactory; +import com.csse3200.game.physics.PhysicsEngine; +import com.csse3200.game.physics.PhysicsLayer; +import com.csse3200.game.physics.raycast.RaycastHit; +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 + private static final String GO_UP = "goUpStart"; + private static final String GO_DOWN = "goDownStart"; + private static final String ATTACK_UP = "attackUpStart"; + private static final String ATTACK_DOWN = "attackDownStart"; + private static final String WALK = "walkStart"; + private static final String DEATH = "deathStart"; + private static final String IDLE = "idleStart"; + + + // 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(); + + private enum STATE { + IDLE, UP, DOWN, SHOOT_UP, SHOOT_DOWN, WALK, DIE + } + private 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); + towerState = STATE.DOWN; + } else { + owner.getEntity().getEvents().trigger(IDLE); + } + } + case SHOOT_DOWN -> { + if (isTargetVisible()) { + owner.getEntity().getEvents().trigger(ATTACK_DOWN); + Entity Projectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, new Vector2(100, + owner.getEntity().getPosition().y), new Vector2(2,2), ProjectileEffects.SLOW, false); + Projectile.setScale(new Vector2(0.5f,0.5f)); + Projectile.setPosition((float) (owner.getEntity().getPosition().x + 0.2), + (float) (owner.getEntity().getPosition().y - 0.2)); + ServiceLocator.getEntityService().register(Projectile); + towerState = STATE.UP; + } else { + owner.getEntity().getEvents().trigger(GO_UP); + towerState = STATE.UP; + } + } + case SHOOT_UP -> { + if (isTargetVisible()) { + + owner.getEntity().getEvents().trigger(ATTACK_UP); + towerState = STATE.DOWN; + Entity Projectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, new Vector2(100, + owner.getEntity().getPosition().y), new Vector2(2,2), ProjectileEffects.SLOW, false); + Projectile.setScale(new Vector2(0.5f,0.5f)); + Projectile.setPosition((float) (owner.getEntity().getPosition().x + 0.2), + (float) (owner.getEntity().getPosition().y + 0.5)); + ServiceLocator.getEntityService().register(Projectile); + } else { + owner.getEntity().getEvents().trigger(IDLE); + towerState = STATE.IDLE; + } + } + 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(); +// owner.getEntity().getEvents().trigger(STOW); + } + + /** + * 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 + */ + 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); + } +} 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 e6a83c610..f027205be 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 @@ -19,7 +19,7 @@ */ public class MobAttackTask extends DefaultTask implements PriorityTask { private static final int INTERVAL = 1; // time interval to scan for towers in - private static final short TARGET = PhysicsLayer.OBSTACLE; // mobs detecting for towers + private static final short TARGET = PhysicsLayer.HUMANS; // mobs detecting for towers // ^ fix this private static final String STOW = "stowStart"; @@ -127,8 +127,8 @@ public void updateMobState() { } else { owner.getEntity().getEvents().trigger(FIRING); 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); + newProjectile.setPosition((float) (owner.getEntity().getPosition().x), (float) (owner.getEntity().getPosition().y + 0.1)); + newProjectile.setScale(-0.7f, 0.7f); ServiceLocator.getEntityService().register(newProjectile); mobState = STATE.STOW; owner.getEntity().getEvents().trigger("shootStart"); 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/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/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/EngineerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/human/EngineerCombatTask.java similarity index 97% rename from source/core/src/main/com/csse3200/game/components/tasks/EngineerCombatTask.java rename to source/core/src/main/com/csse3200/game/components/tasks/human/EngineerCombatTask.java index 40e91edea..23dbea9c0 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/EngineerCombatTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/human/EngineerCombatTask.java @@ -1,4 +1,4 @@ -package com.csse3200.game.components.tasks; +package com.csse3200.game.components.tasks.human; import com.badlogic.gdx.math.Vector2; import com.csse3200.game.ai.tasks.DefaultTask; @@ -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/HumanWanderTask.java b/source/core/src/main/com/csse3200/game/components/tasks/human/HumanWanderTask.java index be2c570f7..a9b497130 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 @@ -5,8 +5,6 @@ import com.csse3200.game.ai.tasks.PriorityTask; import com.csse3200.game.ai.tasks.Task; import com.csse3200.game.components.CombatStatsComponent; -import com.csse3200.game.components.tasks.EngineerCombatTask; -import com.csse3200.game.entities.Entity; import com.csse3200.game.physics.PhysicsLayer; import com.csse3200.game.physics.components.ColliderComponent; import com.csse3200.game.physics.components.HitboxComponent; 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..be123d483 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tower/DroidAnimationController.java @@ -0,0 +1,86 @@ +package com.csse3200.game.components.tower; + +import com.csse3200.game.components.Component; +import com.csse3200.game.rendering.AnimationRenderComponent; + +/** + * 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); + + } + + /** + * 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");} + +} 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..c6acb3c28 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 @@ -78,7 +78,7 @@ 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); 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..a6a7b531f 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; + // } } /** @@ -314,4 +321,7 @@ public int setLayer(int layer) { return layer; } + public String getName() { + return this.getClass().getSimpleName(); + } } 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/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/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/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 8b051c490..9fe31184b 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 882ad4935..bb7258f4c 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 73c5904aa..c72e1e137 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 HUMANS = (1 << 1) | (1 << 5); public static final short ALL = ~0; 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/main/com/csse3200/game/screens/LevelSelectScreen.java b/source/core/src/main/com/csse3200/game/screens/LevelSelectScreen.java index c1b0c2428..4403de6f5 100644 --- a/source/core/src/main/com/csse3200/game/screens/LevelSelectScreen.java +++ b/source/core/src/main/com/csse3200/game/screens/LevelSelectScreen.java @@ -99,7 +99,7 @@ private void spawnPlanetBorders() { if (Gdx.input.justTouched()) { dispose(); logger.info("Loading level {}", planet[4]); - game.setScreen(new MainGameScreen(game)); + game.setScreen(new TurretSelectionScreen(game)); } else { Sprite planetBorder = new Sprite(new Texture("planets/planetBorder.png")); batch.draw(planetBorder, planet[0] - 2.0f, planet[1] - 2.0f, planet[2] + 3.0f, planet[3] + 3.0f); diff --git a/source/core/src/main/com/csse3200/game/screens/StoryScreen.java b/source/core/src/main/com/csse3200/game/screens/StoryScreen.java index 760e39e19..7f81669f9 100644 --- a/source/core/src/main/com/csse3200/game/screens/StoryScreen.java +++ b/source/core/src/main/com/csse3200/game/screens/StoryScreen.java @@ -89,6 +89,16 @@ public void render(float delta) { stage.draw(); } + /** + * Fixes the + * @param width + * @param height + */ + @Override + public void resize(int width, int height) { + stage.getViewport().update(width, height, true); + } + @Override public void dispose() { batch.dispose(); diff --git a/source/core/src/main/com/csse3200/game/screens/TowerType.java b/source/core/src/main/com/csse3200/game/screens/TowerType.java new file mode 100644 index 000000000..deed94733 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/screens/TowerType.java @@ -0,0 +1,27 @@ +package com.csse3200.game.screens; + +public enum TowerType { + WEAPON("images/towers/turret_deployed.png", "Weapon Tower"), + TNT("images/towers/turret_deployed.png", "TNT Tower"), + DROID("images/towers/turret_deployed.png", "Droid Tower"), + WALL("images/towers/turret_deployed.png", "Wall Tower"), + FIRE("images/towers/turret_deployed.png", "Fire Tower"), + STUN("images/towers/turret_deployed.png", "Stun Tower"), + INCOME("images/towers/turret_deployed.png", "Income Tower"); + + private final String imagePath; + private final String towerName; + + TowerType(String imagePath, String towerName) { + this.imagePath = imagePath; + this.towerName = towerName; + } + + public String getImagePath() { + return imagePath; + } + + public String getTowerName() { + return towerName; + } +} diff --git a/source/core/src/main/com/csse3200/game/screens/TurretSelectionScreen.java b/source/core/src/main/com/csse3200/game/screens/TurretSelectionScreen.java new file mode 100644 index 000000000..17cd21319 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/screens/TurretSelectionScreen.java @@ -0,0 +1,149 @@ +package com.csse3200.game.screens; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.ScreenAdapter; +import com.badlogic.gdx.graphics.GL20; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.Sprite; +import com.badlogic.gdx.graphics.g2d.SpriteBatch; +import com.badlogic.gdx.scenes.scene2d.InputEvent; +import com.badlogic.gdx.scenes.scene2d.Stage; +import com.badlogic.gdx.scenes.scene2d.ui.Label; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.Table; +import com.badlogic.gdx.scenes.scene2d.ui.TextButton; +import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.viewport.ScreenViewport; +import com.csse3200.game.GdxGame; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.factories.TowerFactory; +import com.csse3200.game.physics.PhysicsService; +import com.csse3200.game.services.ResourceService; +import com.csse3200.game.services.ServiceLocator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +public class TurretSelectionScreen extends ScreenAdapter { + + private static final int MAX_SELECTED_TURRETS = 5; + private Stage stage; + private List turretList; + private TextButton confirmButton; + + private GdxGame game; + + private SpriteBatch batch; + + private Sprite introSprite; + + private Label message; + private Label turretsPicked; + private Table table; + private static final String TEXTURE = "planets/background.png"; + private Set selectedTurrets = new HashSet<>(); + + private static final Logger logger = LoggerFactory.getLogger(MainMenuScreen.class); + + public TurretSelectionScreen(GdxGame game) { + this.game = game; + stage = new Stage(new ScreenViewport()); + table = new Table(); + + // Set up the background + batch = new SpriteBatch(); + Texture backgroundImage = new Texture(TEXTURE); + introSprite = new Sprite(backgroundImage); + introSprite.setSize(Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); + + + turretList = new ArrayList<>(); + // Add turrets to the list + turretList.addAll(Arrays.asList(TowerType.values())); + // Restrictions can be added to the arrays i.e. map == "Forest" && level == 1 + + + Skin skin = new Skin(Gdx.files.internal("flat-earth/skin/flat-earth-ui.json")); + message = new Label("Select your turrets", skin); + turretsPicked = new Label("Turrets picked: ", skin); + + confirmButton = new TextButton("Continue", skin); + confirmButton.addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + game.setScreen(GdxGame.ScreenType.MAIN_GAME); + } + }); + + + + table.add(message).row(); + table.add(turretsPicked).row(); + for (TowerType turret : turretList) { + TextButton turretButton = new TextButton(turret.getTowerName(), skin); + turretButton.addListener(new ClickListener() { + @Override + public void clicked(InputEvent event, float x, float y) { + logger.info(String.valueOf(selectedTurrets.size())); + if (selectedTurrets.size() > MAX_SELECTED_TURRETS) { + message.setText("You can only select up to 5 turrets."); + } else { + message.setText("Select your turrets"); + } + if (selectedTurrets.contains(turret)) { + // Turret is already selected, unselect it + selectedTurrets.remove(turret); + // You can also change the button appearance to indicate unselection + logger.info(selectedTurrets.toString()); + turretsPicked.setText("Turrets picked: " + selectedTurrets.toString()); + } else if (selectedTurrets.size() == MAX_SELECTED_TURRETS) { + // Turret is not selected, but the max number of turrets has been reached + message.setText("You can only select up to 5 turrets."); + } else if (selectedTurrets.size() < MAX_SELECTED_TURRETS) { + // Turret is not selected, select it + selectedTurrets.add(turret); + turretsPicked.setText("Turrets picked: " + selectedTurrets.toString()); + logger.info(selectedTurrets.toString()); + } + else { + // Turret is not selected, select it + selectedTurrets.add(turret); + turretsPicked.setText("Turrets picked: " + selectedTurrets.toString()); + logger.info(selectedTurrets.toString()); + + // You can change the button appearance to indicate selection + } + + } + }); + table.add(turretButton).row(); + } + table.add(confirmButton).padBottom(20).row(); + + stage.addActor(table); + table.setFillParent(true); + Gdx.input.setInputProcessor(stage); + + } + @Override + public void render(float delta) { + Gdx.gl.glClearColor(0, 0, 0, 1); + Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + batch.begin(); + introSprite.draw(batch); + batch.end(); + stage.draw(); + } + + public List getTurretList() { + return turretList; + } + + @Override + public void dispose() { + stage.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 bf104bfbb..99019845a 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 @@ -1,15 +1,63 @@ package com.csse3200.game.components.player; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.factories.EngineerFactory; +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.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +@ExtendWith(GameExtension.class) class HumanAnimationControllerTest { + private final String[] atlas = {"images/engineers/engineer.atlas"}; + private static final String[] sounds = { + "sounds/engineers/firing_auto.mp3", + "sounds/engineers/firing_single.mp3" + }; + + private final String[] animations = { + "idle_right", + "walk_left", + "walk_right", + "walk_prep", + "prep", + "firing_auto", + "firing_single", + "hit", + "death" + }; + + private Entity engineer; + + @Mock + GameTime gameTime; @BeforeEach void setUp() { + gameTime = mock(GameTime.class); + ServiceLocator.registerTimeSource(gameTime); + ServiceLocator.registerPhysicsService(new PhysicsService()); + RenderService render = new RenderService(); + render.setDebug(mock(DebugRenderer.class)); + ServiceLocator.registerRenderService(render); + ResourceService resourceService = new ResourceService(); + ServiceLocator.registerResourceService(resourceService); + resourceService.loadTextureAtlases(atlas); + resourceService.loadSounds(sounds); + resourceService.loadAll(); + engineer = EngineerFactory.createEngineer(); } @AfterEach @@ -17,34 +65,51 @@ void tearDown() { } @Test - void create() { - } - - @Test - void animateIdleLeft() { - } - - @Test - void animateIdleRight() { - } - - @Test - void animateLeftWalk() { + void shouldHaveAnimationController() { + assertNotNull(engineer.getComponent(HumanAnimationController.class), + "Created Engineer entity should have a HumanAnimationController"); } - @Test - void animateRightWalk() { - } - - @Test - void animateFiring() { - } - - @Test - void animateHit() { - } - - @Test - void animateDeath() { - } +// @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'"); +// } } \ 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 7be8641f4..3d80accb0 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 @@ -39,13 +39,13 @@ @ExtendWith(GameExtension.class) class EngineerFactoryTest { - private String[] atlas = {"images/engineers/engineer.atlas"}; + private final String[] atlas = {"images/engineers/engineer.atlas"}; private static final String[] sounds = { "sounds/engineers/firing_auto.mp3", "sounds/engineers/firing_single.mp3" }; - private String[] animations = { + private final String[] animations = { "idle_right", "walk_left", "walk_right",