diff --git a/README.md b/README.md index dbdb2da3b..7531bd696 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,8 @@ You are welcome to use the game engine for your own purposes. It is released und - [JavaDoc](https://uqcsse3200.github.io/2023-studio-3/) - [SonarCloud](https://sonarcloud.io/project/overview?id=UQcsse3200_2023-studio-3) + + +## Team 2 - Tower Branch + +Implements defense towers in the Outworld Outposts game. Any questions or discussion, please contact Team 2 diff --git a/source/core/assets/configs/tower.json b/source/core/assets/configs/tower.json new file mode 100644 index 000000000..a15ab4d27 --- /dev/null +++ b/source/core/assets/configs/tower.json @@ -0,0 +1,12 @@ +{ + "weapon": { + "health": 10, + "baseAttack": 10, + "cost": 10 + }, + "wall": { + "health": 20, + "baseAttack": 0, + "cost": 5 + } +} \ No newline at end of file diff --git a/source/core/assets/images/turret.atlas b/source/core/assets/images/turret.atlas new file mode 100644 index 000000000..e09479efe --- /dev/null +++ b/source/core/assets/images/turret.atlas @@ -0,0 +1,160 @@ + +turret.png +size: 256, 256 +format: RGBA8888 +filter: Nearest, Nearest +repeat: xy +deploy + rotate: false + xy: 20, 77 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 140, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 20, 20 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 80, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 80, 77 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 80, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 20, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 20, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +default + rotate: false + xy: 20, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +firing + rotate: false + xy: 200, 195 + size: 35, 33 + orig: 35, 33 + offset: 0, 0 + index: -1 +firing + rotate: false + xy: 140, 81 + size: 35, 33 + orig: 35, 33 + offset: 0, 0 + index: -1 +firing + rotate: false + xy: 80, 24 + size: 35, 33 + orig: 35, 33 + offset: 0, 0 + index: -1 +idle + rotate: false + xy: 140, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +idle + rotate: false + xy: 20, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 20, 77 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 140, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 20, 20 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 80, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 80, 77 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 80, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 20, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 20, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 diff --git a/source/core/assets/images/turret.png b/source/core/assets/images/turret.png new file mode 100644 index 000000000..025f18707 Binary files /dev/null and b/source/core/assets/images/turret.png differ diff --git a/source/core/assets/images/turret01.atlas b/source/core/assets/images/turret01.atlas new file mode 100644 index 000000000..75082330c --- /dev/null +++ b/source/core/assets/images/turret01.atlas @@ -0,0 +1,160 @@ + +turret01.png +size: 256, 256 +format: RGBA8888 +filter: Nearest, Nearest +repeat: xy +deploy + rotate: false + xy: 20, 77 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 140, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 20, 20 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 80, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 80, 77 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 80, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 20, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +firing + rotate: false + xy: 200, 195 + size: 35, 33 + orig: 35, 33 + offset: 0, 0 + index: -1 +firing + rotate: false + xy: 140, 81 + size: 35, 33 + orig: 35, 33 + offset: 0, 0 + index: -1 +firing + rotate: false + xy: 80, 24 + size: 35, 33 + orig: 35, 33 + offset: 0, 0 + index: -1 +idle + rotate: false + xy: 140, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +idle + rotate: false + xy: 20, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 20, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +default + rotate: false + xy: 20, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 20, 77 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 140, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 20, 20 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 80, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 80, 77 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 80, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 20, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 20, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 \ No newline at end of file diff --git a/source/core/assets/images/turret01.png b/source/core/assets/images/turret01.png new file mode 100644 index 000000000..025f18707 Binary files /dev/null and b/source/core/assets/images/turret01.png differ diff --git a/source/core/assets/images/turret_deployed.png b/source/core/assets/images/turret_deployed.png new file mode 100644 index 000000000..d395cb727 Binary files /dev/null and b/source/core/assets/images/turret_deployed.png differ diff --git a/source/core/assets/test/files/turret01.atlas b/source/core/assets/test/files/turret01.atlas new file mode 100644 index 000000000..75082330c --- /dev/null +++ b/source/core/assets/test/files/turret01.atlas @@ -0,0 +1,160 @@ + +turret01.png +size: 256, 256 +format: RGBA8888 +filter: Nearest, Nearest +repeat: xy +deploy + rotate: false + xy: 20, 77 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 140, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 20, 20 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 80, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 80, 77 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 80, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 20, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +firing + rotate: false + xy: 200, 195 + size: 35, 33 + orig: 35, 33 + offset: 0, 0 + index: -1 +firing + rotate: false + xy: 140, 81 + size: 35, 33 + orig: 35, 33 + offset: 0, 0 + index: -1 +firing + rotate: false + xy: 80, 24 + size: 35, 33 + orig: 35, 33 + offset: 0, 0 + index: -1 +idle + rotate: false + xy: 140, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +idle + rotate: false + xy: 20, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +deploy + rotate: false + xy: 20, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +default + rotate: false + xy: 20, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 20, 77 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 140, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 20, 20 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 80, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 80, 77 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 80, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 20, 191 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 +stow + rotate: false + xy: 20, 134 + size: 40, 37 + orig: 40, 37 + offset: 0, 0 + index: -1 \ No newline at end of file diff --git a/source/core/assets/test/files/turret01.png b/source/core/assets/test/files/turret01.png new file mode 100644 index 000000000..025f18707 Binary files /dev/null and b/source/core/assets/test/files/turret01.png differ diff --git a/source/core/assets/test/files/turret_deployed.png b/source/core/assets/test/files/turret_deployed.png new file mode 100644 index 000000000..d395cb727 Binary files /dev/null and b/source/core/assets/test/files/turret_deployed.png differ diff --git a/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java b/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java index b1e5d2325..29ef2d3f9 100644 --- a/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java +++ b/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java @@ -9,6 +9,7 @@ import com.csse3200.game.entities.factories.NPCFactory; import com.csse3200.game.entities.factories.ObstacleFactory; import com.csse3200.game.entities.factories.PlayerFactory; +import com.csse3200.game.entities.factories.TowerFactory; import com.csse3200.game.utils.math.GridPoint2Utils; import com.csse3200.game.utils.math.RandomUtils; import com.csse3200.game.services.ResourceService; @@ -24,6 +25,8 @@ public class ForestGameArea extends GameArea { private static final Logger logger = LoggerFactory.getLogger(ForestGameArea.class); private static final int NUM_TREES = 7; private static final int NUM_GHOSTS = 2; + + private static final int NUM_WEAPON_TOWERS = 3; private static final GridPoint2 PLAYER_SPAWN = new GridPoint2(10, 10); private static final float WALL_WIDTH = 0.1f; @@ -42,10 +45,17 @@ public class ForestGameArea extends GameArea { "images/hex_grass_3.png", "images/iso_grass_1.png", "images/iso_grass_2.png", - "images/iso_grass_3.png" + "images/iso_grass_3.png", + "images/turret.png", + "images/turret01.png", + "images/turret_deployed.png" }; private static final String[] forestTextureAtlases = { - "images/terrain_iso_grass.atlas", "images/ghost.atlas", "images/ghostKing.atlas" + "images/terrain_iso_grass.atlas", + "images/ghost.atlas", + "images/ghostKing.atlas", + "images/turret.atlas", + "images/turret01.atlas" }; private static final String[] forestSounds = {"sounds/Impact4.ogg"}; private static final String backgroundMusic = "sounds/BGM_03_mp3.mp3"; @@ -76,7 +86,11 @@ public void create() { spawnTerrain(); spawnTrees(); player = spawnPlayer(); - // spawnGhosts(); + + spawnGhosts(); + spawnGhostKing(); + spawnWeaponTower(); + ghostking = spawnGhostKing(); playMusic(); @@ -184,6 +198,17 @@ private void spawnMultiProjectile(Vector2 speed) { spawnEntity(newBottomProjectile); } + 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(); + spawnEntityAt(weaponTower, randomPos, true, true); + } + } + private void playMusic() { Music music = ServiceLocator.getResourceService().getAsset(backgroundMusic, Music.class); music.setLooping(true); diff --git a/source/core/src/main/com/csse3200/game/components/CostComponent.java b/source/core/src/main/com/csse3200/game/components/CostComponent.java new file mode 100644 index 000000000..0e2d165e2 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/CostComponent.java @@ -0,0 +1,40 @@ +package com.csse3200.game.components; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Component used to store information related to cost. + * Any entities that necessitate a cost should register an instance of this class. + */ +public class CostComponent extends Component { + + private static final Logger logger = LoggerFactory.getLogger(CostComponent.class); + private int cost; + + public CostComponent(int cost) { + setCost(cost); + + } + + /** + * Sets the entity's cost. Cost has a minimum bound of 0. + * @param cost + */ + public void setCost(int cost) { + if(cost >= 0) { + this.cost = cost; + } else { + this.cost = 0; + } + } + + /** + * Returns the entity's cost + * @return entity's cost + */ + public int getCost() { + return this.cost; + } + +} \ No newline at end of file diff --git a/source/core/src/main/com/csse3200/game/components/popupmenu/PopupMenuInputComponent.java b/source/core/src/main/com/csse3200/game/components/popupmenu/PopupMenuInputComponent.java new file mode 100644 index 000000000..29bba5619 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/popupmenu/PopupMenuInputComponent.java @@ -0,0 +1,45 @@ +package com.csse3200.game.components.popupmenu; + +import com.badlogic.gdx.InputProcessor; +import com.csse3200.game.input.InputComponent; + +/** + * Input handler for the player for keyboard and touch (mouse) input. + * This input handler only uses keyboard input. + */ +public class PopupMenuInputComponent extends InputComponent{ + /** TO DO: + * This component's end goal is to send a deactivation trigger when the + * user clicks on anything other than the menu entity, and reactivate it + * if the user clicks on a tower, with the new tower's stats as per its + * config file. + * Current implementation step: trigger a generic event whenever + * the mouse is clicked, with no checks for the entity clicked on. + */ + public PopupMenuInputComponent() {super(1);} + // Note: will need to change constructor's priority when merging with other + // branches that add other input components. + /** + * Triggers "popupEvent" when the mouse is clicked. + * + * @return whether the input was processed + */ + @Override + public boolean touchDown(int screenX, int screenY, int pointer, int button) { + System.out.println(9); + entity.getEvents().trigger("popupEvent"); + return true; + } + + /** + * Triggers "popupEvent" when the mouse is clicked. + * + * @return whether the input was processed + */ + @Override + public boolean touchDown(float screenX, float screenY, int pointer, int button) { + System.out.println(9); + entity.getEvents().trigger("popupEvent"); + return true; + } +} 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 new file mode 100644 index 000000000..c2660f9e7 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tasks/TowerCombatTask.java @@ -0,0 +1,187 @@ +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.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.rendering.DebugRenderer; +import com.csse3200.game.services.GameTime; +import com.csse3200.game.services.ServiceLocator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The TowerCombatTask runs the AI for the WeaponTower 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 TowerCombatTask extends DefaultTask implements PriorityTask { + private static final Logger logger = LoggerFactory.getLogger(MovementTask.class); + private final int priority; // The active priority this task will have + private final float maxRange; // the maximum detection range of the tower + private Vector2 towerPosition = new Vector2(10,10); + private final Vector2 maxRangePosition = new Vector2(); + private final PhysicsEngine physics; + private final int SCAN_INTERVAL = 1; + private final GameTime timeSource; + private long endTime; + private final DebugRenderer debugRenderer; + private final RaycastHit hit = new RaycastHit(); + + private final short TARGET = PhysicsLayer.NPC; // The type of targets that the tower will detect + + private enum STATE { + IDLE, DEPLOY, FIRING, STOW + } + private STATE towerState = STATE.IDLE; + + /** + * @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 TowerCombatTask(int priority, float maxRange) { + this.priority = priority; + this.maxRange = maxRange; + this.maxRangePosition.set(towerPosition.x + maxRange, towerPosition.y); + physics = ServiceLocator.getPhysicsService().getPhysics(); + timeSource = ServiceLocator.getTimeSource(); + debugRenderer = ServiceLocator.getRenderService().getDebug(); + logger.debug("TowerCombatTask started"); + } + + /** + * 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(); + + // Default to idle mode + owner.getEntity().getEvents().trigger("idleStart"); + + endTime = timeSource.getTime() + (int)(SCAN_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() + (int)(SCAN_INTERVAL * 1000); + } + } + + public void updateTowerState() { + // configure tower state depending on target visibility + switch (towerState) { + case IDLE -> { + // targets detected in idle mode - start deployment + if (isTargetVisible()) { + owner.getEntity().getEvents().trigger("deployStart"); + towerState = STATE.DEPLOY; + } + break; + } + case DEPLOY -> { + // currently deploying, +// if (owner.getEntity().getComponent(AnimationRenderComponent.class) + if (isTargetVisible()) { + owner.getEntity().getEvents().trigger("firingStart"); + towerState = STATE.FIRING; + } + break; + } + case FIRING -> { + if (isTargetVisible()) { + owner.getEntity().getEvents().trigger("firingStart"); + } else { + towerState = STATE.STOW; + } + break; + } + case STOW -> { + if (isTargetVisible()) { + towerState = STATE.DEPLOY; + } else { + owner.getEntity().getEvents().trigger("idleStart"); + } + break; + } + } + } + /** + * For stopping the running task + */ + @Override + public void stop() { + super.stop(); + owner.getEntity().getEvents().trigger("stowStart"); + } + + /** + * Returns the current priority of the task. + * @return active priority value if targets detected, inactive priority otherwise + */ + @Override + public int getPriority() { + if (isTargetVisible()) { + return getActivePriority(); + } + return getInactivePriority(); + } + + /** + * Finds the distance to the nearest target, if any in range. + * @return (float) distance to nearest target, otherwise 0 if nothing in range. + */ + private float getDistanceToTarget() { + if (physics.raycast(towerPosition, maxRangePosition, TARGET, hit)) { + return towerPosition.dst(hit.point.x, hit.point.y); + }; + return 0; + } + + /** + * Fetches the active priority of the Task if a target is visible. + * @return (int) active priority if a target is visible, -1 otherwise + */ + private int getActivePriority() { + if (!isTargetVisible()) { + return 0; // Too far, stop firing + } + return priority; + } + + /** + * Fetches the inactive priority of the Task if a target is not visible. + * @return (int) -1 if a target is not visible, active priority otherwise + */ + private int getInactivePriority() { + if (isTargetVisible()) { + return priority; + } + return 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. + if (physics.raycast(towerPosition, maxRangePosition, TARGET, hit)) { + return true; + } + return false; + } +} diff --git a/source/core/src/main/com/csse3200/game/components/tasks/TowerIdleTask.java b/source/core/src/main/com/csse3200/game/components/tasks/TowerIdleTask.java new file mode 100644 index 000000000..0120b1610 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tasks/TowerIdleTask.java @@ -0,0 +1,110 @@ +package com.csse3200.game.components.tasks; + +import com.badlogic.gdx.math.Vector2; +import com.csse3200.game.ai.tasks.AITaskComponent; +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.physics.PhysicsEngine; +import com.csse3200.game.physics.PhysicsLayer; +import com.csse3200.game.physics.raycast.RaycastHit; +import com.csse3200.game.rendering.DebugRenderer; +import com.csse3200.game.services.GameTime; +import com.csse3200.game.services.ServiceLocator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Task for the Tower Idle State - specifically for the + * Weapon Tower Entity that can move between combat and + * idle states. Scans for enemy mobs but does nothing else. + * + * ====================== CURRENTLY NOT IN USE ================================== + * This task may be modified for use in future features but is not required + * for current functionality + */ +public class TowerIdleTask extends DefaultTask implements PriorityTask { + private static final Logger logger = LoggerFactory.getLogger(MovementTask.class); + private static final float SCAN_RANGE = 100; + private static final int ACTIVE_PRIORITY = 1; + private static final int INACTIVE_PRIORITY = 0; + private final GameTime timeSource; + private final float interval; + private long endTime; + private final PhysicsEngine physics; + private final DebugRenderer debugRenderer; + private final RaycastHit hit = new RaycastHit(); + + private final short TARGET = PhysicsLayer.NPC; + + /** + * Instantiates a TowerIdleTask which scans for mobs at a certain + * time interval. + * @param interval time between scanning for mobs, in seconds. + */ + public TowerIdleTask(float interval) { + timeSource = ServiceLocator.getTimeSource(); + physics = ServiceLocator.getPhysicsService().getPhysics(); + this.interval = interval; + debugRenderer = ServiceLocator.getRenderService().getDebug(); + logger.debug("Weapon Tower idleTask started"); + } + + /** + * Start the Idle task - waiting from current game time until interval has passed. + */ + @Override + public void start() { + super.start(); + // Trigger the idle event + owner.getEntity().getEvents().trigger("idleStart"); + endTime = timeSource.getTime() + (int)(this.interval * 1000); + } + + @Override + public void update() { + if (timeSource.getTime() >= endTime) { + if (isTargetVisible()) { +// owner.getEntity().getEvents().trigger("deployStart"); + TowerCombatTask combatTask = new TowerCombatTask(ACTIVE_PRIORITY + 2, SCAN_RANGE - 10); + combatTask.create(owner); + logger.debug("Idle Task update function: Detected a target!"); + } + } + } + + /** + * Scan for any mob entities in the lane. Triggers the 'mobsVisible' event + **/ + private boolean isTargetVisible() { + Vector2 from = owner.getEntity().getCenterPosition(); + Vector2 to = new Vector2(from.x + SCAN_RANGE, from.y); + + // If there is an obstacle in the path to the end of the tower scan range + // must be mobs present. + // TODO change layer to detect + if (physics.raycast(from, to, TARGET, hit)) { +// debugRenderer.drawLine(from, hit.point); + return true; + } +// debugRenderer.drawLine(from, to); + return false; + } + + @Override + public int getPriority() { +// if (status == Status.ACTIVE) { +// return getActivePriority(); +// } + return getActivePriority(); + } + + public int getActivePriority() { + return this.ACTIVE_PRIORITY; + } + + public int getInactivePriority() { + return this.INACTIVE_PRIORITY; + } + +} \ No newline at end of file diff --git a/source/core/src/main/com/csse3200/game/components/tower/TowerAnimationController.java b/source/core/src/main/com/csse3200/game/components/tower/TowerAnimationController.java new file mode 100644 index 000000000..cddb893be --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tower/TowerAnimationController.java @@ -0,0 +1,39 @@ +package com.csse3200.game.components.tower; + +import com.csse3200.game.components.Component; +import com.csse3200.game.rendering.AnimationRenderComponent; + +/** + * Listens for events relevant to a weapon tower state. + * Each event will trigger a certain animation + */ +public class TowerAnimationController extends Component { + AnimationRenderComponent animator; + + @Override + public void create() { + super.create(); + animator = this.entity.getComponent(AnimationRenderComponent.class); + entity.getEvents().addListener("idleStart", this::animateIdle); + entity.getEvents().addListener("stowStart", this::animateStow); + entity.getEvents().addListener("deployStart", this::animateDeploy); + entity.getEvents().addListener("firingStart", this::animateFiring); + + } + + void animateIdle() { + animator.startAnimation("idle"); + } + + void animateStow() { + animator.startAnimation("stow"); + } + + void animateDeploy() { + animator.startAnimation("deploy"); + } + + void animateFiring() { + animator.startAnimation("firing"); + } +} \ No newline at end of file diff --git a/source/core/src/main/com/csse3200/game/entities/configs/WallTowerConfig.java b/source/core/src/main/com/csse3200/game/entities/configs/WallTowerConfig.java new file mode 100644 index 000000000..452eaf7e0 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/entities/configs/WallTowerConfig.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 WallTowerConfig { + public int health = 1; + public int baseAttack = 0; + public int cost = 1; +} diff --git a/source/core/src/main/com/csse3200/game/entities/configs/WeaponTowerConfig.java b/source/core/src/main/com/csse3200/game/entities/configs/WeaponTowerConfig.java new file mode 100644 index 000000000..7d00ccb65 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/entities/configs/WeaponTowerConfig.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 WeaponTowerConfig { + 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 new file mode 100644 index 000000000..c641a053c --- /dev/null +++ b/source/core/src/main/com/csse3200/game/entities/configs/baseTowerConfigs.java @@ -0,0 +1,9 @@ +package com.csse3200.game.entities.configs; + +/** + * Defines all tower configs to be loaded by the Tower Factory. + */ +public class baseTowerConfigs { + public WeaponTowerConfig weapon = new WeaponTowerConfig(); + public WallTowerConfig wall = new WallTowerConfig(); +} \ No newline at end of file diff --git a/source/core/src/main/com/csse3200/game/entities/factories/NPCFactory.java b/source/core/src/main/com/csse3200/game/entities/factories/NPCFactory.java index efd59ca29..c546343c0 100644 --- a/source/core/src/main/com/csse3200/game/entities/factories/NPCFactory.java +++ b/source/core/src/main/com/csse3200/game/entities/factories/NPCFactory.java @@ -77,8 +77,8 @@ public static Entity createGhostKing(Entity target) { new AnimationRenderComponent( ServiceLocator.getResourceService() .getAsset("images/ghostKing.atlas", TextureAtlas.class)); - animator.addAnimation("float", 0.1f, Animation.PlayMode.LOOP); - animator.addAnimation("angry_float", 0.1f, Animation.PlayMode.LOOP); + animator.addAnimation("float", 0.2f, Animation.PlayMode.LOOP); + animator.addAnimation("angry_float", 0.2f, Animation.PlayMode.LOOP); ghostKing .addComponent(new CombatStatsComponent(config.health, config.baseAttack)) 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 ac0704e97..936fbea6f 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 @@ -28,7 +28,7 @@ public static Entity createTree() { tree.getComponent(PhysicsComponent.class).setBodyType(BodyType.StaticBody); tree.getComponent(TextureRenderComponent.class).scaleEntity(); - tree.scaleHeight(2.5f); + tree.scaleHeight(1.5f); PhysicsUtils.setScaledCollider(tree, 0.5f, 0.2f); return tree; } @@ -50,4 +50,4 @@ public static Entity createWall(float width, float height) { private ObstacleFactory() { throw new IllegalStateException("Instantiating static util class"); } -} +} \ No newline at end of file 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 new file mode 100644 index 000000000..8b7c0985e --- /dev/null +++ b/source/core/src/main/com/csse3200/game/entities/factories/TowerFactory.java @@ -0,0 +1,105 @@ +package com.csse3200.game.entities.factories; + + +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.Animation; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.badlogic.gdx.physics.box2d.BodyDef.BodyType; +import com.csse3200.game.ai.tasks.AITaskComponent; +import com.csse3200.game.ai.tasks.PriorityTask; +import com.csse3200.game.components.CombatStatsComponent; +import com.csse3200.game.components.CostComponent; +import com.csse3200.game.components.tasks.TowerCombatTask; +import com.csse3200.game.components.tasks.TowerIdleTask; +import com.csse3200.game.components.tower.TowerAnimationController; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.configs.WallTowerConfig; +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.entities.configs.WeaponTowerConfig; +import com.csse3200.game.entities.configs.baseTowerConfigs; +import com.csse3200.game.files.FileLoader; +import com.csse3200.game.rendering.AnimationRenderComponent; +import com.csse3200.game.rendering.TextureRenderComponent; +import com.csse3200.game.services.ServiceLocator; + +/** + * Factory to create a tower entity. + * + * Predefined tower properties are loaded from a config stored as a json file and should have + * the properties stores in 'baseTowerConfigs'. + */ +public class TowerFactory { + + private static final int WEAPON_SCAN_INTERVAL = 1; + private static final int COMBAT_TASK_PRIORITY = 2; + public static final int WEAPON_TOWER_MAX_RANGE = 40; + private static final baseTowerConfigs configs = + FileLoader.readClass(baseTowerConfigs.class, "configs/tower.json"); + + + + public static Entity createWallTower() { + Entity wall = createBaseTower(); + WallTowerConfig config = configs.wall; + + wall + .addComponent(new CombatStatsComponent(config.health, config.baseAttack)) + .addComponent(new CostComponent(config.cost)); + + return wall; + } + + /** + * Creates a weaponry tower that shoots at mobs - This will most likely need to be extended + * once other types of weapon towers are developed + * @return entity + */ + public static Entity createWeaponTower() { + Entity weapon = createBaseTower(); + WeaponTowerConfig config = configs.weapon; + + // AiTaskComponent will run the tower task which carries out detection of targets and trigger events + AITaskComponent aiTaskComponent = new AITaskComponent() + .addTask(new TowerCombatTask(COMBAT_TASK_PRIORITY, WEAPON_TOWER_MAX_RANGE)); + + // Contains all the animations that the tower will have + AnimationRenderComponent animator = + new AnimationRenderComponent( + ServiceLocator.getResourceService() + .getAsset("images/turret01.atlas", TextureAtlas.class)); + animator.addAnimation("idle", 0.3f, Animation.PlayMode.LOOP); + animator.addAnimation("stow", 0.2f, Animation.PlayMode.NORMAL); + animator.addAnimation("deploy", 0.2f, Animation.PlayMode.REVERSED); + animator.addAnimation("firing", 0.1f, Animation.PlayMode.LOOP); + + weapon + .addComponent(new CombatStatsComponent(config.health, config.baseAttack)) + .addComponent(new CostComponent(config.cost)) + .addComponent(aiTaskComponent) + .addComponent(animator) + .addComponent(new TowerAnimationController()); + +// weapon.getComponent(AnimationRenderComponent.class).scaleEntity(); + + return weapon; + + } + /** + * Creates a generic tower entity to be used as a base entity by more specific tower creation methods. + * @return entity + */ + 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.OBSTACLE)) // we might have to change the names of the layers + .addComponent(new PhysicsComponent().setBodyType(BodyType.StaticBody)); + + //PhysicsUtils.setScaledCollider(tower, 0.5f, 0.2f); //values might vary according to entity scale value + + return tower; + } +} \ No newline at end of file diff --git a/source/core/src/test/com/csse3200/game/components/CostComponentTest.java b/source/core/src/test/com/csse3200/game/components/CostComponentTest.java new file mode 100644 index 000000000..d882a0e31 --- /dev/null +++ b/source/core/src/test/com/csse3200/game/components/CostComponentTest.java @@ -0,0 +1,48 @@ +package com.csse3200.game.components; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class CostComponentTest { + + private CostComponent costComponent; + + @BeforeEach + public void setUp() { + costComponent = new CostComponent(0); // initializing with default value + } + + @Test + public void testSetAndGetPositiveCost() { + costComponent.setCost(100); + assertEquals(100, costComponent.getCost(), "The cost should be set to 100"); + } + + @Test + public void testSetAndGetZeroCost() { + costComponent.setCost(0); + assertEquals(0, costComponent.getCost(), "The cost should be set to 0"); + } + + @Test + public void testNegativeCostIsNormalizedToZero() { + costComponent.setCost(-100); + assertEquals(0, costComponent.getCost(), "The cost should be normalized to 0 if set as negative"); + } + + @Test + public void testCostComponentConstructorWithNegativeValue() { + CostComponent tempComponent = new CostComponent(50); + tempComponent.setCost(tempComponent.getCost() - 60); + assertEquals(0, tempComponent.getCost(), "The cost should be 0"); + } + + @Test + public void testCostComponentConstructorWithPositiveValue() { + CostComponent tempComponent = new CostComponent(-50); + tempComponent.setCost(tempComponent.getCost() + 60); + assertEquals(60, tempComponent.getCost(), "The cost should be 60"); + } +} diff --git a/source/core/src/test/com/csse3200/game/components/popupmenu/PopupMenuInputComponentTest.java b/source/core/src/test/com/csse3200/game/components/popupmenu/PopupMenuInputComponentTest.java new file mode 100644 index 000000000..9acd1bf7f --- /dev/null +++ b/source/core/src/test/com/csse3200/game/components/popupmenu/PopupMenuInputComponentTest.java @@ -0,0 +1,31 @@ +package com.csse3200.game.components.popupmenu; + +import com.csse3200.game.input.InputService; +import com.csse3200.game.services.ServiceLocator; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.events.EventHandler; +import com.csse3200.game.events.listeners.EventListener0; +import com.csse3200.game.extensions.GameExtension; +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 PopupMenuInputComponentTest { + + @Test + void handlesTouchDownEntityIncluded(){ + EventListener0 listener = mock(EventListener0.class); + Entity entity = new Entity(); + InputService inputService = new InputService(); + ServiceLocator.registerInputService(inputService); + PopupMenuInputComponent popupMenuInputComponent = new PopupMenuInputComponent();//spy(PopupMenuInputComponent.class); + entity.addComponent(popupMenuInputComponent); + inputService.register(popupMenuInputComponent); + entity.getEvents().addListener("popupEvent", listener); + entity.create(); + assertTrue(popupMenuInputComponent.touchDown(5, 6, 7, 8)); + verify(listener).handle(); + } +} diff --git a/source/core/src/test/com/csse3200/game/entities/factories/TowerFactoryTest.java b/source/core/src/test/com/csse3200/game/entities/factories/TowerFactoryTest.java new file mode 100644 index 000000000..eaec8f784 --- /dev/null +++ b/source/core/src/test/com/csse3200/game/entities/factories/TowerFactoryTest.java @@ -0,0 +1,164 @@ +package com.csse3200.game.entities.factories; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.badlogic.gdx.assets.AssetManager; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.badlogic.gdx.physics.box2d.BodyDef.BodyType; +import com.csse3200.game.components.CombatStatsComponent; +import com.csse3200.game.components.CostComponent; +import com.csse3200.game.components.TouchAttackComponent; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.extensions.GameExtension; +import com.csse3200.game.physics.PhysicsService; +import com.csse3200.game.physics.components.ColliderComponent; +import com.csse3200.game.physics.components.HitboxComponent; +import com.csse3200.game.physics.components.PhysicsComponent; +import com.csse3200.game.rendering.DebugRenderer; +import com.csse3200.game.rendering.RenderService; +import com.csse3200.game.services.GameTime; +import com.csse3200.game.services.ResourceService; +import com.csse3200.game.services.ServiceLocator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; + +import java.security.Provider; + +@ExtendWith(GameExtension.class) +public class TowerFactoryTest { + + private Entity baseTower; + private Entity weaponTower; + private Entity wallTower; + private String[] texture = {"images/turret_deployed.png", "images/turret01.png"}; + private String[] atlas = {"images/turret01.atlas"}; + @BeforeEach + public void setUp() { + GameTime gameTime = mock(GameTime.class); + when(gameTime.getDeltaTime()).thenReturn(0.02f); + ServiceLocator.registerTimeSource(gameTime); + ServiceLocator.registerPhysicsService(new PhysicsService()); + RenderService render = new RenderService(); + render.setDebug(mock(DebugRenderer.class)); + ServiceLocator.registerRenderService(render); + ResourceService resourceService = new ResourceService(); + ServiceLocator.registerResourceService(resourceService); + resourceService.loadTextures(texture); + resourceService.loadTextureAtlases(atlas); + resourceService.loadAll(); + ServiceLocator.getResourceService() + .getAsset("images/turret01.atlas", TextureAtlas.class); + baseTower = TowerFactory.createBaseTower(); + weaponTower = TowerFactory.createWeaponTower(); + wallTower = TowerFactory.createWallTower(); + } + + @Test + public void testCreateBaseTowerNotNull() { + + assertNotNull(baseTower, "Base tower should not be null"); + assertNotNull(weaponTower, "Weaponry tower should not be null"); + assertNotNull(wallTower, "Wall tower should not be null"); + } + + @Test + public void testCreateBaseTowerHasColliderComponent() { + assertNotNull(baseTower.getComponent(ColliderComponent.class), + "Base tower should have ColliderComponent"); + assertNotNull(weaponTower.getComponent(ColliderComponent.class), + "Weaponry tower should have ColliderComponent"); + assertNotNull(wallTower.getComponent(ColliderComponent.class), + "Wall tower should have ColliderComponent"); + } + + @Test + public void testCreateBaseTowerHasHitboxComponent() { + assertNotNull(baseTower.getComponent(HitboxComponent.class), + "Base tower should have HitboxComponent"); + assertNotNull(weaponTower.getComponent(HitboxComponent.class), + "Weaponry tower should have HitboxComponent"); + assertNotNull(wallTower.getComponent(HitboxComponent.class), + "Wall tower should have HitboxComponent"); + } + + @Test + public void testCreateBaseTowerHasPhysicsComponent() { + assertNotNull(baseTower.getComponent(PhysicsComponent.class), + "Base tower should have PhysicsComponent"); + assertNotNull(weaponTower.getComponent(PhysicsComponent.class), + "Weaponry tower should have PhysicsComponent"); + assertNotNull(wallTower.getComponent(PhysicsComponent.class), + "Wall tower should have PhysicsComponent"); + } + + @Test + public void testCreateBaseTowerPhysicsComponentStaticBody() { + PhysicsComponent physicsComponent = baseTower.getComponent(PhysicsComponent.class); + PhysicsComponent physicsComponent1 = weaponTower.getComponent(PhysicsComponent.class); + PhysicsComponent physicsComponent2 = wallTower.getComponent(PhysicsComponent.class); + + assertTrue(physicsComponent.getBody().getType() == BodyType.StaticBody, + "PhysicsComponent should be of type StaticBody"); + assertTrue(physicsComponent1.getBody().getType() == BodyType.StaticBody, + "PhysicsComponent1 should be of type StaticBody"); + assertTrue(physicsComponent2.getBody().getType() == BodyType.StaticBody, + "PhysicsComponent2 should be of type StaticBody"); + } + + @Test + public void testWeaponTowerCombatStatsComponentAndCostComponent() { + + assertTrue(weaponTower.getComponent(CombatStatsComponent.class).getHealth() == 10, + "Health should be 10"); + assertTrue(weaponTower.getComponent(CombatStatsComponent.class).getBaseAttack() == 10, + "BaseAttack should be 10"); + assertTrue(weaponTower.getComponent(CostComponent.class).getCost() == 10, + "Cost should be 10"); + + } + + @Test + public void testWallTowerCombatStatsComponentAndCostComponent() { + + assertTrue(wallTower.getComponent(CombatStatsComponent.class).getHealth() == 20, + "Health should be 20"); + assertTrue(wallTower.getComponent(CombatStatsComponent.class).getBaseAttack() == 0, + "BaseAttack should be 0"); + assertTrue(wallTower.getComponent(CostComponent.class).getCost() == 5, + "Cost should be 5"); + + } + + @Test + public void testAttackerCollisionWithWall() { + Entity attacker = createAttacker(wallTower.getComponent(HitboxComponent.class).getLayer()); + + wallTower.setPosition(10f,10f); + attacker.setPosition(10f,10f); + wallTower.create(); + + assertEquals(20, wallTower.getComponent(CombatStatsComponent.class).getHealth()); + + ServiceLocator.getPhysicsService().getPhysics().update(); + + assertEquals(10, wallTower.getComponent(CombatStatsComponent.class).getHealth()); + + } + + Entity createAttacker(short targetLayer) { + Entity entity = + new Entity() + .addComponent(new TouchAttackComponent(targetLayer)) + .addComponent(new CombatStatsComponent(0, 10)) + .addComponent(new PhysicsComponent()) + .addComponent(new HitboxComponent()); + entity.create(); + return entity; + } +} + +