diff --git a/source/core/src/main/com/csse3200/game/components/gamearea/CurrencyDisplay.java b/source/core/src/main/com/csse3200/game/components/gamearea/CurrencyDisplay.java index a887ed896..a2c348b74 100644 --- a/source/core/src/main/com/csse3200/game/components/gamearea/CurrencyDisplay.java +++ b/source/core/src/main/com/csse3200/game/components/gamearea/CurrencyDisplay.java @@ -2,6 +2,7 @@ import com.badlogic.gdx.audio.Sound; import com.badlogic.gdx.graphics.Camera; +import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.BitmapFont; import com.badlogic.gdx.graphics.g2d.SpriteBatch; @@ -15,6 +16,7 @@ import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable; import com.badlogic.gdx.utils.Align; +import com.csse3200.game.services.GameTime; import com.csse3200.game.services.ServiceLocator; import com.csse3200.game.ui.UIComponent; import com.badlogic.gdx.scenes.scene2d.actions.Actions; @@ -89,6 +91,15 @@ public void updateScrapsStats() { scrapsTb.getLabel().setText(text); } + /** + * Displays a warning animation of the scraps display if the player tries to + * build something that costs more than the balance + */ + public void scrapBalanceFlash() { + // TODO: IMPLEMENT THIS + scrapsTb.setText("Insufficient!"); + } + /** * Updates the currency (Crystals) value on the UI component */ diff --git a/source/core/src/main/com/csse3200/game/components/maingame/UIElementsDisplay.java b/source/core/src/main/com/csse3200/game/components/maingame/UIElementsDisplay.java index 1f38a21c4..3cfba6eeb 100644 --- a/source/core/src/main/com/csse3200/game/components/maingame/UIElementsDisplay.java +++ b/source/core/src/main/com/csse3200/game/components/maingame/UIElementsDisplay.java @@ -2,28 +2,19 @@ import com.badlogic.gdx.Gdx; import com.badlogic.gdx.audio.Sound; -import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.g2d.SpriteBatch; -import com.badlogic.gdx.math.GridPoint2; -import com.badlogic.gdx.math.Vector3; import com.badlogic.gdx.scenes.scene2d.Actor; -import com.badlogic.gdx.scenes.scene2d.ui.Button; 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.ChangeListener; import com.badlogic.gdx.utils.Array; -import com.csse3200.game.entities.Entity; -import com.csse3200.game.entities.factories.TowerFactory; import com.csse3200.game.screens.TowerType; import com.csse3200.game.services.ServiceLocator; -import com.csse3200.game.ui.ButtonFactory; import com.csse3200.game.ui.UIComponent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.HashSet; -import java.util.Set; /** * Displays a button to represent the remaining mobs left in the current wave and a button to skip to the next wave. @@ -40,8 +31,6 @@ public class UIElementsDisplay extends UIComponent { }; private Sound click; private Sound hover; -// private TextButton remainingMobsButton = new ButtonFactory().createButton("Mobs left:"); -// private final TextButton timerButton = new ButtonFactory().createButton("Next wave:"); private TextButton remainingMobsButton; private TextButton timerButton; private final int timer = 110; @@ -62,14 +51,11 @@ private void addActors() { remainingMobsButton = new TextButton("Mobs:" + ServiceLocator.getWaveService().getEnemyCount(), skin); buttonTable.top().right(); - towerTable.top(); + towerTable.top().padTop(80f); buttonTable.setFillParent(true); towerTable.setFillParent(true); - towerTable.setDebug(true); - towerTable.padTop(50f); - TowerType[] defaultTowers = { TowerType.TNT, TowerType.DROID, @@ -98,6 +84,10 @@ private void addActors() { } } + // Update the centrally located towerTypes list - + ServiceLocator.setTowerTypes(towers); + + // Create the buttons - TODO This needs overhauling to pretty buttons TextButton tower1 = new TextButton(towers.get(0).getTowerName(), skin); TextButton tower2 = new TextButton(towers.get(1).getTowerName(), skin); TextButton tower3 = new TextButton(towers.get(2).getTowerName(), skin); @@ -196,8 +186,13 @@ public void createTimerButton() { * This method updates the text for timer button. */ public void updateTimerButton() { - int totalSecs = (int) ((ServiceLocator.getWaveService().getNextWaveTime() - - ServiceLocator.getTimeSource().getTime()) / 1000); + int totalSecs = (int) (timer - (ServiceLocator.getTimeSource().getTime() / 1000)); + + // TODO : THESE SHOULD BE REMOVED AND PLACED WHEREVER THE BOSS MOB GETS SPAWNED + if (totalSecs % 20 == 0) { + ServiceLocator.getMapService().shakeCameraMap(); + ServiceLocator.getMapService().shakeCameraGrid(); + } int seconds = totalSecs % 60; int minutes = (totalSecs % 3600) / 60; String finalTime = String.format("%02d:%02d", minutes, seconds); diff --git a/source/core/src/main/com/csse3200/game/components/tasks/CurrencyTask.java b/source/core/src/main/com/csse3200/game/components/tasks/CurrencyTask.java index 1793115c6..8f2ea18cd 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/CurrencyTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/CurrencyTask.java @@ -3,7 +3,9 @@ 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.currency.Scrap; +import com.csse3200.game.rendering.AnimationRenderComponent; import com.csse3200.game.services.GameTime; import com.csse3200.game.services.ServiceLocator; import org.slf4j.Logger; @@ -22,6 +24,13 @@ public class CurrencyTask extends DefaultTask implements PriorityTask { private final int currencyAmount = scrap.getAmount(); // amount of currency to update private static final String IDLE = "idleStartEco"; private static final String MOVE = "moveStartEco"; + private static final String DEATH = "deathStartEco"; + + public enum STATE { + IDLE, DEATH + } + public STATE towerState = STATE.IDLE; + /** * @param priority Task priority for currency updates. Must be a positive integer. @@ -40,7 +49,8 @@ public CurrencyTask(int priority, int interval) { public void start() { super.start(); owner.getEntity().getEvents().addListener("addIncome",this::changeInterval); - endTime = timeSource.getTime() + (30 * 1000L); + // TODO: changed 30 TIMES MULTIPLIER to 5 times + endTime = timeSource.getTime() + (interval * 1500L); owner.getEntity().getEvents().trigger(IDLE); } @@ -52,14 +62,38 @@ public void start() { @Override public void update() { if (timeSource.getTime() >= endTime) { - owner.getEntity().getEvents().trigger(MOVE); - updateCurrency(); // update currency + updateTowerState(); logger.info(String.format("Interval: %d", interval)); endTime = timeSource.getTime() + (interval * 1000L); // reset end time } } + /** + * This method acts is the state machine for IncomeTower. Relevant animations are triggered based on relevant state + * of the game. If the tower runs out of health it dies. + */ + public void updateTowerState() { + if (owner.getEntity().getComponent(CombatStatsComponent.class).getHealth() <= 0 && towerState != STATE.DEATH) { + owner.getEntity().getEvents().trigger(DEATH); + towerState = STATE.DEATH; + } + + switch (towerState) { + case IDLE -> { + owner.getEntity().getEvents().trigger(MOVE); + updateCurrency(); // update currency + towerState = STATE.IDLE; + } + case DEATH -> { + if (owner.getEntity().getComponent(AnimationRenderComponent.class).isFinished()) { + owner.getEntity().setFlagForDelete(true); + } + } + } + } + + /** * Updates the currency based on time intervals. */ 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 index 384549af7..1c597fc2e 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/DroidCombatTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/DroidCombatTask.java @@ -74,7 +74,7 @@ public void start() { // Default to idle mode owner.getEntity().getEvents().trigger(WALK); owner.getEntity().getEvents().addListener("addFireRate",this::changeFireRateInterval); - endTime = timeSource.getTime() + (INTERVAL * 500); + endTime = timeSource.getTime() + (INTERVAL * 1000); } /** diff --git a/source/core/src/main/com/csse3200/game/components/tasks/FireTowerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/FireTowerCombatTask.java index abf01bb79..13aa11a18 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/FireTowerCombatTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/FireTowerCombatTask.java @@ -42,6 +42,7 @@ public class FireTowerCombatTask extends DefaultTask implements PriorityTask { private GameTime timeSource; private long endTime; private final RaycastHit hit = new RaycastHit(); + private boolean shoot = true; public enum STATE { IDLE, PREP_ATTACK, ATTACK, DEATH @@ -115,17 +116,21 @@ public void updateTowerState() { } } case ATTACK -> { - if (!isTargetVisible()) { - owner.getEntity().getEvents().trigger(IDLE); - towerState = STATE.IDLE; - } else { - owner.getEntity().getEvents().trigger(ATTACK); - Entity newProjectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, - new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f, 2f), ProjectileEffects.BURN, false); - newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.25), - (float) (owner.getEntity().getPosition().y + 0.25)); - ServiceLocator.getEntityService().register(newProjectile); + if (shoot) { + if (!isTargetVisible()) { + owner.getEntity().getEvents().trigger(IDLE); + towerState = STATE.IDLE; + } else { + owner.getEntity().getEvents().trigger(ATTACK); + Entity newProjectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, + new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f, 2f), ProjectileEffects.BURN, false); + newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.25), + (float) (owner.getEntity().getPosition().y)); + ServiceLocator.getEntityService().register(newProjectile); + } } + shoot = !shoot; + } case DEATH -> { if (owner.getEntity().getComponent(AnimationRenderComponent.class).isFinished()) { diff --git a/source/core/src/main/com/csse3200/game/components/tasks/FireworksTowerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/FireworksTowerCombatTask.java index ef7c74f89..8d10036ae 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/FireworksTowerCombatTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/FireworksTowerCombatTask.java @@ -40,6 +40,7 @@ public class FireworksTowerCombatTask extends DefaultTask implements PriorityTas private GameTime timeSource; private long endTime; private final RaycastHit hit = new RaycastHit(); + private boolean shoot = true; public enum STATE { IDLE, ATTACK, DEATH @@ -69,7 +70,7 @@ public void start() { // Set the default state to IDLE state owner.getEntity().getEvents().trigger(IDLE); - endTime = timeSource.getTime() + (INTERVAL * 5000); + endTime = timeSource.getTime() + (INTERVAL * 1000); } /** @@ -103,17 +104,21 @@ public void updateTowerState() { } } case ATTACK -> { - if (isTargetVisible()) { - owner.getEntity().getEvents().trigger(ATTACK); - Entity newProjectile = ProjectileFactory.createSplitFireWorksFireball(PhysicsLayer.NPC, - new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f, 2f), 3); - newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.25), - (float) (owner.getEntity().getPosition().y + 0.25)); - ServiceLocator.getEntityService().register(newProjectile); - } else { - owner.getEntity().getEvents().trigger(IDLE); - towerState=STATE.IDLE; + // check if fired last time if not fire if so hold + if (shoot) { + if (isTargetVisible()) { + owner.getEntity().getEvents().trigger(ATTACK); + Entity newProjectile = ProjectileFactory.createSplitFireWorksFireball(PhysicsLayer.NPC, + new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f, 2f), 3); + newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.25), + (float) (owner.getEntity().getPosition().y)); + ServiceLocator.getEntityService().register(newProjectile); + } else { + owner.getEntity().getEvents().trigger(IDLE); + towerState=STATE.IDLE; + } } + shoot = !shoot; } case DEATH -> { if (owner.getEntity().getComponent(AnimationRenderComponent.class).isFinished()) { diff --git a/source/core/src/main/com/csse3200/game/components/tasks/PierceTowerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/PierceTowerCombatTask.java index 8d8723f65..b6c6f7619 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/PierceTowerCombatTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/PierceTowerCombatTask.java @@ -39,6 +39,7 @@ public class PierceTowerCombatTask extends DefaultTask implements PriorityTask { private GameTime timeSource; private long endTime; private final RaycastHit hit = new RaycastHit(); + private boolean shoot = true; public enum STATE { IDLE, ATTACK, DEATH @@ -103,18 +104,22 @@ public void updateTowerState() { } } case ATTACK -> { - if (!isTargetVisible()) { - owner.getEntity().getEvents().trigger(IDLE); - towerState = STATE.IDLE; - } else { - owner.getEntity().getEvents().trigger(ALERT); - owner.getEntity().getEvents().trigger(ATTACK); - Entity newProjectile = ProjectileFactory.createPierceFireBall(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); + if (shoot) { + if (!isTargetVisible()) { + owner.getEntity().getEvents().trigger(IDLE); + towerState = STATE.IDLE; + } else { + owner.getEntity().getEvents().trigger(ALERT); + owner.getEntity().getEvents().trigger(ATTACK); + Entity newProjectile = ProjectileFactory.createPierceFireBall(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)); + ServiceLocator.getEntityService().register(newProjectile); + } } + + shoot = !shoot; } case DEATH -> { if (owner.getEntity().getComponent(AnimationRenderComponent.class).isFinished()) { diff --git a/source/core/src/main/com/csse3200/game/components/tasks/RicochetTowerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/RicochetTowerCombatTask.java index d057c1ab0..63c662417 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/RicochetTowerCombatTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/RicochetTowerCombatTask.java @@ -40,6 +40,7 @@ public class RicochetTowerCombatTask extends DefaultTask implements PriorityTask private GameTime timeSource; private long endTime; private final RaycastHit hit = new RaycastHit(); + private boolean shoot = true; //enums for the state triggers public enum STATE { @@ -104,18 +105,21 @@ public void updateTowerState() { } } case ATTACK -> { - if (!isTargetVisible()) { - owner.getEntity().getEvents().trigger(IDLE); - towerState = STATE.IDLE; - } else { - owner.getEntity().getEvents().trigger(ATTACK); - Entity newProjectile = ProjectileFactory.createRicochetFireball(PhysicsLayer.NPC, - // NEED TO DO USER TESTING TO FIGURE OUT THE BOUNCE COUNT - new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f, 2f), 3); - newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.25), - (float) (owner.getEntity().getPosition().y + 0.25)); - ServiceLocator.getEntityService().register(newProjectile); + if (shoot) { + if (!isTargetVisible()) { + owner.getEntity().getEvents().trigger(IDLE); + towerState = STATE.IDLE; + } else { + owner.getEntity().getEvents().trigger(ATTACK); + Entity newProjectile = ProjectileFactory.createRicochetFireball(PhysicsLayer.NPC, + // NEED TO DO USER TESTING TO FIGURE OUT THE BOUNCE COUNT + new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f, 2f), 3); + newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.25), + (float) (owner.getEntity().getPosition().y)); + ServiceLocator.getEntityService().register(newProjectile); + } } + shoot = !shoot; } case DEATH -> { if (owner.getEntity().getComponent(AnimationRenderComponent.class).isFinished()) { diff --git a/source/core/src/main/com/csse3200/game/components/tasks/StunTowerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/StunTowerCombatTask.java index cce8ec833..ec469b269 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/StunTowerCombatTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/StunTowerCombatTask.java @@ -40,6 +40,7 @@ public class StunTowerCombatTask extends DefaultTask implements PriorityTask { private GameTime timeSource; private long endTime; private final RaycastHit hit = new RaycastHit(); + private boolean shoot = true; //enums for the state triggers public enum STATE { @@ -111,20 +112,25 @@ public void updateTowerState() { } } case ATTACK -> { - if (!isTargetVisible()) { - owner.getEntity().getEvents().trigger(IDLE); - towerState = STATE.IDLE; - } else { - owner.getEntity().getEvents().trigger(ATTACK); + if (shoot) { + 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)); - Entity newProjectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, - new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f, 2f), - ProjectileEffects.STUN, false); - newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.25), - (float) (owner.getEntity().getPosition().y + 0.25)); - ServiceLocator.getEntityService().register(newProjectile); + Entity newProjectile = ProjectileFactory.createEffectProjectile(PhysicsLayer.NPC, + new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f, 2f), + ProjectileEffects.STUN, false); + newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.25), + (float) (owner.getEntity().getPosition().y)); + ServiceLocator.getEntityService().register(newProjectile); + owner.getEntity().getEvents().trigger(IDLE); + towerState = STATE.IDLE; + } } + shoot = !shoot; } case DIE -> { if (owner.getEntity().getComponent(AnimationRenderComponent.class).isFinished()) { 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 f1d0eb9ca..c9f5c2b33 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 @@ -4,11 +4,13 @@ import com.csse3200.game.ai.tasks.DefaultTask; import com.csse3200.game.ai.tasks.PriorityTask; import com.csse3200.game.areas.ForestGameArea; +import com.csse3200.game.components.CombatStatsComponent; 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; import org.slf4j.Logger; @@ -30,6 +32,7 @@ public class TowerCombatTask extends DefaultTask implements PriorityTask { private static final String DEPLOY = "deployStart"; private static final String FIRING = "firingStart"; private static final String IDLE = "idleStart"; + private static final String DEATH = "deathStart"; // class attributes private final int priority; // The active priority this task will have @@ -42,9 +45,10 @@ public class TowerCombatTask extends DefaultTask implements PriorityTask { private long endTime; private final RaycastHit hit = new RaycastHit(); private static final Logger logger = LoggerFactory.getLogger(ForestGameArea.class); + private boolean shoot = true; private enum STATE { - IDLE, DEPLOY, FIRING, STOW + IDLE, DEPLOY, FIRING, STOW, DEATH } private STATE towerState = STATE.IDLE; @@ -114,6 +118,11 @@ public void update() { */ public void updateTowerState() { // configure tower state depending on target visibility + if (owner.getEntity().getComponent(CombatStatsComponent.class).getHealth() <= 0 && towerState != STATE.DEATH) { + owner.getEntity().getEvents().trigger(DEATH); + towerState = STATE.DEATH; + return; + } switch (towerState) { case IDLE -> { // targets detected in idle mode - start deployment @@ -133,38 +142,41 @@ public void updateTowerState() { } } case FIRING -> { - // targets gone - stop firing - if (!isTargetVisible()) { + if (shoot) { + // targets gone - stop firing + if (!isTargetVisible()) { - owner.getEntity().getEvents().trigger(STOW); - towerState = STATE.STOW; - } 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.5), (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); - - // RICOCHET FIREBALL - // Entity ricochetProjectile = ProjectileFactory.createRicochetFireball(PhysicsLayer.NPC, new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f,2f), 0); - - // 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); + owner.getEntity().getEvents().trigger(STOW); + towerState = STATE.STOW; + } 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.5), (float) (owner.getEntity().getPosition().y)); + 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); + + // RICOCHET FIREBALL + // Entity ricochetProjectile = ProjectileFactory.createRicochetFireball(PhysicsLayer.NPC, new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f,2f), 0); + + // 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); + } } + shoot = !shoot; } case STOW -> { // currently stowing @@ -177,6 +189,11 @@ public void updateTowerState() { towerState = STATE.IDLE; } } + case DEATH -> { + if (owner.getEntity().getComponent(AnimationRenderComponent.class).isFinished()) { + owner.getEntity().setFlagForDelete(true); + } + } } } /** diff --git a/source/core/src/main/com/csse3200/game/components/tasks/WallTowerDestructionTask.java b/source/core/src/main/com/csse3200/game/components/tasks/WallTowerDestructionTask.java index 988ff0071..749647851 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/WallTowerDestructionTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/WallTowerDestructionTask.java @@ -41,7 +41,7 @@ public class WallTowerDestructionTask extends DefaultTask implements PriorityTas private final RaycastHit hit = new RaycastHit(); public enum STATE { - IDLE, ATTACK, DEATH + IDLE, DEATH } public STATE towerState = STATE.IDLE; @@ -68,7 +68,7 @@ public void start() { // Set the default state to IDLE state owner.getEntity().getEvents().trigger(IDLE); - endTime = timeSource.getTime() + (INTERVAL * 5000); + endTime = timeSource.getTime() + (INTERVAL * 1000); } /** @@ -97,7 +97,7 @@ public void updateTowerState() { switch (towerState) { case IDLE -> { owner.getEntity().getEvents().trigger(IDLE); - towerState = STATE.ATTACK; + towerState = STATE.IDLE; } case DEATH -> { if (owner.getEntity().getComponent(AnimationRenderComponent.class).isFinished()) { 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 index d8307e0e4..19406ff99 100644 --- a/source/core/src/main/com/csse3200/game/components/tower/DroidAnimationController.java +++ b/source/core/src/main/com/csse3200/game/components/tower/DroidAnimationController.java @@ -114,9 +114,8 @@ void shootDown() { entity.getPosition().y), new Vector2(2,2), ProjectileEffects.SLOW, false); Projectile.setScale(new Vector2(0.5f,0.5f)); Projectile.setPosition((float) (entity.getPosition().x + 0.2), - (float) (entity.getPosition().y - 0.2)); + (float) (entity.getPosition().y)); ServiceLocator.getEntityService().register(Projectile); - } } diff --git a/source/core/src/main/com/csse3200/game/entities/EntityService.java b/source/core/src/main/com/csse3200/game/entities/EntityService.java index d7dc5631e..a5e6c4d6f 100644 --- a/source/core/src/main/com/csse3200/game/entities/EntityService.java +++ b/source/core/src/main/com/csse3200/game/entities/EntityService.java @@ -226,7 +226,7 @@ public boolean entitiesInTile(int x_coord, int y_coord) { try { mp = (TiledMapTileLayer)ServiceLocator.getMapService().getComponent().getMap().getLayers().get(0); } catch (NullPointerException e) { - // MapService is not running + // MapService is not running - consider this occupied (invalid tile) return true; } if (mp.getCell(x_coord, y_coord) != null) { diff --git a/source/core/src/main/com/csse3200/game/entities/configs/IncomeTowerConfig.java b/source/core/src/main/com/csse3200/game/entities/configs/IncomeTowerConfig.java index 9e79376e2..bdcf77c06 100644 --- a/source/core/src/main/com/csse3200/game/entities/configs/IncomeTowerConfig.java +++ b/source/core/src/main/com/csse3200/game/entities/configs/IncomeTowerConfig.java @@ -9,6 +9,6 @@ public class IncomeTowerConfig { public int cost = 1; public float attackRate = 0; - public float incomeRate = 30; + public float incomeRate = 10; } 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 a92dbb234..81ff0012f 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,5 +1,6 @@ package com.csse3200.game.entities.factories; +import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.physics.box2d.Filter; import com.csse3200.game.components.tasks.DroidCombatTask; import com.csse3200.game.components.tasks.TNTTowerCombatTask; @@ -25,6 +26,7 @@ import com.csse3200.game.input.UpgradeUIComponent;import java.util.HashSet; import java.util.Set; + /** * Factory to create a tower entity. * @@ -34,7 +36,6 @@ public class TowerFactory { // Define a set to keep track of occupied lanes private static final Set occupiedLanes = new HashSet<>(); - 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; @@ -180,6 +181,10 @@ public static Entity createWallTower() { */ public static Entity createTNTTower() { Entity TNTTower = createBaseTower(); + TNTTower.getComponent(HitboxComponent.class) + .setLayer(PhysicsLayer.NONE) + .setSensor(true); + TNTTower.getComponent(ColliderComponent.class).setSensor(true); TNTTowerConfigs config = configs.TNTTower; AITaskComponent aiTaskComponent = new AITaskComponent() @@ -269,7 +274,7 @@ public static Entity createWeaponTower() { animator.addAnimation(IDLE_ANIM, IDLE_SPEED, Animation.PlayMode.LOOP); animator.addAnimation(STOW_ANIM, STOW_SPEED, Animation.PlayMode.NORMAL); animator.addAnimation(DEPLOY_ANIM, DEPLOY_SPEED, Animation.PlayMode.REVERSED); - animator.addAnimation(FIRE_ANIM, FIRE_SPEED, Animation.PlayMode.LOOP); + animator.addAnimation(FIRE_ANIM, (2*FIRE_SPEED), Animation.PlayMode.LOOP); weapon .addComponent(new CombatStatsComponent(config.health, config.baseAttack)) @@ -302,7 +307,7 @@ public static Entity createFireTower() { .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+ 0.25f, Animation.PlayMode.LOOP); + animator.addAnimation(FIRE_TOWER_ATTACK_ANIM, (2*(FIRE_TOWER_ATTACK_SPEED+ 0.25f)), Animation.PlayMode.LOOP); animator.addAnimation(FIRE_TOWER_DEATH_ANIM, FIRE_TOWER_DEATH_SPEED, Animation.PlayMode.NORMAL); fireTower @@ -332,7 +337,7 @@ public static Entity createStunTower() { 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+ 0.25f, Animation.PlayMode.LOOP); + animator.addAnimation(STUN_TOWER_ATTACK_ANIM, ((STUN_TOWER_ATTACK_SPEED+ 0.20f)), Animation.PlayMode.LOOP); animator.addAnimation(STUN_TOWER_DEATH_ANIM, STUN_TOWER_DEATH_SPEED, Animation.PlayMode.NORMAL); stunTower @@ -344,7 +349,6 @@ public static Entity createStunTower() { .addComponent(new StunTowerAnimationController()); stunTower.setScale(1.5f, 1.5f); - PhysicsUtils.setScaledCollider(stunTower, 0.5f, 0.5f); return stunTower; } @@ -364,7 +368,7 @@ public static Entity createFireworksTower() { new AnimationRenderComponent( ServiceLocator.getResourceService() .getAsset(FIREWORKS_TOWER_ATLAS, TextureAtlas.class)); - animator.addAnimation(FIREWORKS_TOWER_ATTACK_ANIM, FIREWORKS_TOWER_ANIM_ATTACK_SPEED, Animation.PlayMode.NORMAL); + animator.addAnimation(FIREWORKS_TOWER_ATTACK_ANIM, (2*FIREWORKS_TOWER_ANIM_ATTACK_SPEED), Animation.PlayMode.NORMAL); animator.addAnimation(FIREWORKS_TOWER_IDLE_ANIM, FIREWORKS_TOWER_ANIM_SPEED, Animation.PlayMode.LOOP); animator.addAnimation(FIREWORKS_TOWER_DEATH_ANIM, FIREWORKS_TOWER_ANIM_SPEED, Animation.PlayMode.NORMAL); @@ -375,8 +379,6 @@ public static Entity createFireworksTower() { .addComponent(animator) .addComponent(new FireworksTowerAnimationController()); - fireworksTower.setScale(1.5f, 1.5f); - PhysicsUtils.setScaledCollider(fireworksTower, 0.2f, 0.2f); return fireworksTower; } @@ -395,7 +397,7 @@ public static Entity createPierceTower() { new AnimationRenderComponent( ServiceLocator.getResourceService() .getAsset(PIERCE_TOWER_ATLAS, TextureAtlas.class)); - animator.addAnimation(PIERCE_TOWER_ATTACK_ANIM, PIERCE_TOWER_ANIM_ATTACK_SPEED, Animation.PlayMode.LOOP); + animator.addAnimation(PIERCE_TOWER_ATTACK_ANIM, (2*PIERCE_TOWER_ANIM_ATTACK_SPEED), Animation.PlayMode.LOOP); animator.addAnimation(PIERCE_TOWER_IDLE_ANIM, PIERCE_TOWER_ANIM_ATTACK_SPEED, Animation.PlayMode.LOOP); animator.addAnimation(PIERCE_TOWER_DEATH_ANIM, PIERCE_TOWER_ANIM_ATTACK_SPEED, Animation.PlayMode.NORMAL); animator.addAnimation(PIERCE_TOWER_ALERT_ANIM, PIERCE_TOWER_ANIM_ATTACK_SPEED, Animation.PlayMode.NORMAL); @@ -409,7 +411,6 @@ public static Entity createPierceTower() { .addComponent(aiTaskComponent); pierceTower.setScale(1.5f, 1.5f); - PhysicsUtils.setScaledCollider(pierceTower, 0.5f, 0.5f); return pierceTower; } @@ -426,7 +427,7 @@ public static Entity createRicochetTower() { AnimationRenderComponent animator = new AnimationRenderComponent( ServiceLocator.getResourceService().getAsset(RICOCHET_TOWER_ATLAS,TextureAtlas.class)); - animator.addAnimation(RICOCHET_TOWER_ATTACK_ANIM,RICOCHET_TOWER_ANIM_ATTACK_SPEED,Animation.PlayMode.LOOP); + animator.addAnimation(RICOCHET_TOWER_ATTACK_ANIM,(2*RICOCHET_TOWER_ANIM_ATTACK_SPEED),Animation.PlayMode.LOOP); animator.addAnimation(RICOCHET_TOWER_DEATH_ANIM,RICOCHET_TOWER_ANIM_ATTACK_SPEED,Animation.PlayMode.NORMAL); animator.addAnimation(RICOCHET_TOWER_IDLE_ANIM,RICOCHET_TOWER_ANIM_ATTACK_SPEED,Animation.PlayMode.LOOP); ricochetTower @@ -438,26 +439,6 @@ public static Entity createRicochetTower() { // ADD ANIMATION COMPONENTS ricochetTower.setScale(1.5f, 1.5f); - PhysicsUtils.setScaledCollider(ricochetTower, 0.5f, 0.5f); - return ricochetTower; - } - public static Entity createHealTower() { - Entity ricochetTower = createBaseTower(); - HealTowerConfig config = configs.HealTower; - - AITaskComponent aiTaskComponent = new AITaskComponent() - .addTask(new RicochetTowerCombatTask(COMBAT_TASK_PRIORITY, WEAPON_TOWER_MAX_RANGE)); - - // ADD AnimationRenderComponent - - ricochetTower - .addComponent(new CombatStatsComponent(config.health, config.baseAttack)) - .addComponent((new CostComponent(config.cost))) - .addComponent(aiTaskComponent); - // ADD ANIMATION COMPONENTS - - ricochetTower.setScale(1.5f, 1.5f); - PhysicsUtils.setScaledCollider(ricochetTower, 0.5f, 0.5f); return ricochetTower; } @@ -467,6 +448,8 @@ public static Entity createHealTower() { */ 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 @@ -475,6 +458,9 @@ public static Entity createBaseTower() { tower.setLayer(1); // Set priority to 1, which is 1 below scrap (which is 0) + // Set hitbox and collider to a vector of size 1 and align the hitbox and collider to the center of the tower + tower.getComponent(HitboxComponent.class).setAsBoxAligned(new Vector2(1f, 1f), PhysicsComponent.AlignX.CENTER, PhysicsComponent.AlignY.CENTER); + tower.getComponent(ColliderComponent.class).setAsBoxAligned(new Vector2(1f, 1f), PhysicsComponent.AlignX.CENTER, PhysicsComponent.AlignY.CENTER); return tower; } public static Entity createAndPlaceTower(int lane) { diff --git a/source/core/src/main/com/csse3200/game/input/BuildInputComponent.java b/source/core/src/main/com/csse3200/game/input/BuildInputComponent.java index 451c99129..1dcf527cf 100644 --- a/source/core/src/main/com/csse3200/game/input/BuildInputComponent.java +++ b/source/core/src/main/com/csse3200/game/input/BuildInputComponent.java @@ -1,14 +1,18 @@ package com.csse3200.game.input; +import com.badlogic.gdx.Input; +import com.badlogic.gdx.InputProcessor; import com.badlogic.gdx.audio.Sound; import com.badlogic.gdx.graphics.Camera; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.math.Vector3; +import com.badlogic.gdx.utils.Array; import com.csse3200.game.areas.ForestGameArea; import com.csse3200.game.entities.Entity; import com.csse3200.game.entities.EntityService; import com.csse3200.game.entities.factories.TowerFactory; import com.csse3200.game.screens.TowerType; +import com.csse3200.game.services.CurrencyService; import com.csse3200.game.services.ServiceLocator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,6 +32,9 @@ public class BuildInputComponent extends InputComponent { }; private Sound buildSound; private Sound errorSound; + private Array towers = new Array<>(); + private Array defaultTowers = new Array<>(); + private boolean multipleTowerBuild = false; /** * Constructor for the BuildInputComponent @@ -37,6 +44,22 @@ public BuildInputComponent(Camera camera) { this.entityService = ServiceLocator.getEntityService(); this.camera = camera; loadSounds(); + towers.addAll(ServiceLocator.getTowerTypes()); + + logger.debug("selected towers in buildInputComponent are " + towers); + TowerType[] defaultTowerTypes = { + TowerType.TNT, + TowerType.DROID, + TowerType.INCOME, + TowerType.WALL, + TowerType.WEAPON + }; + defaultTowers.addAll(defaultTowerTypes); + + if (towers.isEmpty()) { + ServiceLocator.setTowerTypes(defaultTowers); + towers = defaultTowers; + } } /** @@ -72,8 +95,57 @@ public boolean touchDown(int screenX, int screenY, int pointer, int button) { // check that no entities are occupying the tile if (!tileOccupied) { - buildTower((int)cursorPosition.x, (int)cursorPosition.y); logger.debug("spawning a tower at {}, {}", cursorPosition.x, cursorPosition.y); + return buildTower((int)cursorPosition.x, (int)cursorPosition.y); + } else { + // TODO: Create a tile indication of invalid placement here?? + return false; + } + } + + /** + * Configures shortcut keys for building towers. Pressing the shortcut key + * sets the 'tower to build' variable in CurrencyService + * + * @return whether the input was processed + * @see InputProcessor#keyDown(int) + */ + @Override + public boolean keyUp(int keycode) { + switch (keycode) { + case Input.Keys.NUM_1: + ServiceLocator.getCurrencyService().setTowerType(towers.get(0)); + return true; + case Input.Keys.NUM_2: + ServiceLocator.getCurrencyService().setTowerType(towers.get(1)); + return true; + case Input.Keys.NUM_3: + ServiceLocator.getCurrencyService().setTowerType(towers.get(2)); + return true; + case Input.Keys.NUM_4: + ServiceLocator.getCurrencyService().setTowerType(towers.get(3)); + return true; + case Input.Keys.NUM_5: + ServiceLocator.getCurrencyService().setTowerType(towers.get(4)); + return true; + case Input.Keys.CONTROL_LEFT: + // After multiple placement, deselect tower and prevent further builds + ServiceLocator.getCurrencyService().setTowerType(null); + multipleTowerBuild = false; + return true; + default: + return false; + } + } + + /** + * + * @param keycode one of the constants in {@link Input.Keys} + * @return + */ + public boolean keyDown(int keycode) { + if (keycode == Input.Keys.CONTROL_LEFT) { + multipleTowerBuild = true; return true; } return false; @@ -86,14 +158,21 @@ public boolean touchDown(int screenX, int screenY, int pointer, int button) { * @param x x-coordinate int value * @param y y-coordinate int value */ - public void buildTower(int x, int y) { + public boolean buildTower(int x, int y) { + TowerType tower; + CurrencyService currencyService; // fetch the currently set TowerType in the currency service, and its associated build cost. - TowerType tower = ServiceLocator.getCurrencyService().getTower(); + currencyService = ServiceLocator.getCurrencyService(); + if (currencyService == null) { + // if the currency service fails or is not running + return false; + } + tower = currencyService.getTower(); if (tower != null) { // fetch the price of the selected tower and attempt to instantiate - int cost = Integer.parseInt(ServiceLocator.getCurrencyService().getTower().getPrice()); + int cost = Integer.parseInt(currencyService.getTower().getPrice()); - if (cost <= ServiceLocator.getCurrencyService().getScrap().getAmount()) { + if (cost <= currencyService.getScrap().getAmount()) { Entity newTower = switch (tower) { case WEAPON -> TowerFactory.createWeaponTower(); case INCOME -> TowerFactory.createIncomeTower(); @@ -105,7 +184,14 @@ public void buildTower(int x, int y) { }; // build the selected tower newTower.setPosition(x, y); - ServiceLocator.getEntityService().register(newTower); + EntityService entityService; + + entityService = ServiceLocator.getEntityService(); + if (entityService == null){ + return false; + } + entityService.register(newTower); + // Decrement currency and show a popup that reflects the cost of the build ServiceLocator.getCurrencyService().getScrap().modify(-cost); ServiceLocator.getCurrencyService().getDisplay().updateScrapsStats(); @@ -113,12 +199,23 @@ public void buildTower(int x, int y) { long soundId = buildSound.play(); buildSound.setVolume(soundId, 0.4f); + + // deselect the tower after building + if (!multipleTowerBuild) { + ServiceLocator.getCurrencyService().setTowerType(null); + } + return true; } else { // play a sound to indicate an invalid action long soundId = errorSound.play(); - errorSound.setVolume(soundId, 0.5f); + errorSound.setVolume(soundId, 1f); + ServiceLocator.getCurrencyService().getDisplay().scrapBalanceFlash(); + // TODO: add a visual indication of the build fail, through + // currency display flash + } } + return false; } /** diff --git a/source/core/src/main/com/csse3200/game/rendering/CameraShaker.java b/source/core/src/main/com/csse3200/game/rendering/CameraShaker.java new file mode 100644 index 000000000..11359805d --- /dev/null +++ b/source/core/src/main/com/csse3200/game/rendering/CameraShaker.java @@ -0,0 +1,187 @@ +package com.csse3200.game.rendering; + +import com.badlogic.gdx.graphics.Camera; +import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.math.Vector3; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + @author antz + @version 1.0.0 + November 2022 + See https://github.com/antzGames/libGDX-cameraShake for more information. + + Loosely based on 'Mastering LibGDX Game Development' - Chapter 9 - Camera Shake + Book: https://www.amazon.com/Mastering-LibGDX-Game-Development-Patrick/dp/1785289365 + + */ + +public class CameraShaker { + private static final Logger logger = LoggerFactory.getLogger(CameraShaker.class); + private Camera camera; + private boolean isShaking = false; + private float origShakeRadius; + private float minimumShakeRadius; + private float radiusFallOffFactor; + private float shakeRadius; + private float randomAngle; + private float timer; + private Vector3 offset; + private Vector3 currentPosition; + public Vector3 origPosition; + + /** + * Constructor + * + * @param camera supports any camera implementation + * @param shakeRadius original was set to 30.0f, must be greater than 0 + * @param minimumShakeRadius original was set to 2.0f, must be greater than 0 and less than shakeRadius + * @param radiusFallOffFactor original was set to 0.9f, must be greater than 0 and less than 1 + */ + public CameraShaker(Camera camera, float shakeRadius, float minimumShakeRadius, float radiusFallOffFactor){ + checkParameters(shakeRadius, minimumShakeRadius, radiusFallOffFactor); + this.camera = camera; + this.offset = new Vector3(); + this.currentPosition = new Vector3(); + this.origPosition = camera.position.cpy(); + logger.info("Start | Camera Position: " + camera.position); + reset(); + } + + /** + * Constructor - simple version + * + * Use this constructor to create a camera shaker with default values + * + * shakeRadius = 0.05f; // must be positive + * minimumShakeRadius = 0.001f; // must be positive and less than shakeRadius, aim for 5-15% of shake radius + * radiusFallOffFactor = 0.8f; // must be greater than 0 and less than 1 + * + * @param camera supports any camera implementation + */ + public CameraShaker(Camera camera){ + shakeRadius = 0.05f; + minimumShakeRadius = 0.001f; + radiusFallOffFactor = 0.8f; + checkParameters(shakeRadius, minimumShakeRadius, radiusFallOffFactor); + this.camera = camera; + this.offset = new Vector3(); + this.currentPosition = new Vector3(); + this.origPosition = camera.position.cpy(); + logger.info("Start | Camera Position: " + camera.position); + reset(); + } + + /** + * Call this after a player collision/impact/explosion to start the camera shaking. + */ + public void startShaking(){ + reset(); + isShaking = true; + } + + /** + * Always call this in your game's main update/render method. + * + * Make sure batch.setProjectionMatrix(camera.combined) is set prior to call. + */ + public void update(float delta){ + if (!isCameraShaking()) return; + + // only update camera shake 60 times a second max + timer += delta; + if (timer >= 1f/60f) { + computeCameraOffset(); + computeCurrentPosition(); + diminishShake(); + camera.position.set(currentPosition); + camera.update(); + timer = 0; + } + } + + /** + * Called by diminishShake() when minimum shake radius reached. + * + * But you can also stop a camera shake by calling this method if needed. + */ + public void reset(){ + shakeRadius = origShakeRadius; + isShaking = false; + seedRandomAngle(); + currentPosition = origPosition.cpy(); + timer = 0; + + } + + /** + * This allows to reconfigure parameters. Check if not shaking before calling, or else current shake will end. + * + * @param shakeRadius original was set to 30.0f, must be greater than 0 + * @param minimumShakeRadius original was set to 2.0f, must be greater than 0 and less than shakeRadius + * @param radiusFallOffFactor original was set to 0.9f, must be greater than 0 and less than 1 + */ + public void resetAndReconfigure(float shakeRadius, float minimumShakeRadius, float radiusFallOffFactor){ + checkParameters(shakeRadius, minimumShakeRadius, radiusFallOffFactor); + isShaking = false; + seedRandomAngle(); + currentPosition = origPosition.cpy(); + timer = 0; + } + + /** + * You can check if camera is currently shaking. + * + * @return is the camera currently shaking. + */ + public boolean isCameraShaking(){ + return isShaking; + } + + /** + Private methods below + */ + + private void seedRandomAngle(){ + randomAngle = MathUtils.random(1, 360); + } + + private void computeCameraOffset(){ + float sine = MathUtils.sinDeg(randomAngle); + float cosine = MathUtils.cosDeg(randomAngle); + offset.x = cosine * shakeRadius; + offset.y = sine * shakeRadius; + } + + private void computeCurrentPosition() { + currentPosition.x = origPosition.x + offset.x; + currentPosition.y = origPosition.y + offset.y; + + } + + private void diminishShake(){ + if(shakeRadius < minimumShakeRadius){ + reset(); + return; + } + isShaking = true; + shakeRadius *= radiusFallOffFactor; + randomAngle = MathUtils.random(1, 360); + } + + private void checkParameters(float shakeRadius, float minimumShakeRadius, float radiusFallOffFactor) { + // validation checks on parameters + if (radiusFallOffFactor >= 1f) radiusFallOffFactor = 0.9f; // radius fall off factor must be less than 1 + if (radiusFallOffFactor <= 0) radiusFallOffFactor = 0.9f; // radius fall off factor must be greater than 0 + if (shakeRadius <= 0) shakeRadius = 30f; // shake radius must be greater than 0 + if (minimumShakeRadius < 0) minimumShakeRadius = 0; // minimum shake radius must be greater than 0 + if (minimumShakeRadius >= shakeRadius) // minimum shake radius must be less than shake radius, if not + minimumShakeRadius = 0.15f * shakeRadius; // then set minimum shake radius to 15% of shake radius + + this.shakeRadius = shakeRadius; + this.origShakeRadius = shakeRadius; + this.minimumShakeRadius = minimumShakeRadius; + this.radiusFallOffFactor = radiusFallOffFactor; + } +} \ No newline at end of file diff --git a/source/core/src/main/com/csse3200/game/screens/MainGameScreen.java b/source/core/src/main/com/csse3200/game/screens/MainGameScreen.java index 55623b881..90ea35ef5 100644 --- a/source/core/src/main/com/csse3200/game/screens/MainGameScreen.java +++ b/source/core/src/main/com/csse3200/game/screens/MainGameScreen.java @@ -134,6 +134,7 @@ public MainGameScreen(GdxGame game) { renderer = RenderFactory.createRenderer(); renderer.getCamera().getEntity().setPosition(CAMERA_POSITION); + renderer.getCamera().getCamera().position.set(CAMERA_POSITION.x,CAMERA_POSITION.y,0); renderer.getDebug().renderPhysicsWorld(physicsEngine.getWorld()); InputComponent inputHandler = new DropInputComponent(renderer.getCamera().getCamera()); InputComponent buildHandler = new BuildInputComponent(renderer.getCamera().getCamera()); @@ -147,7 +148,7 @@ public MainGameScreen(GdxGame game) { loadAssets(); createUI(); - ServiceLocator.registerMapService(new MapService(renderer.getCamera())); + ServiceLocator.registerMapService(new MapService(renderer.getCamera(),camera)); logger.debug("Initialising main game screen entities"); ForestGameArea forestGameArea = new ForestGameArea(); forestGameArea.create(); @@ -195,9 +196,11 @@ public void render(float delta) { Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); + ServiceLocator.getMapService().updateShakerGrid(delta); // Update the camera and set the batch's projection matrix camera.update(); batch.setProjectionMatrix(camera.combined); + ServiceLocator.getMapService().updateShakerMap(delta); // Begin the batch batch.begin(); diff --git a/source/core/src/main/com/csse3200/game/screens/TurretSelectionScreen.java b/source/core/src/main/com/csse3200/game/screens/TurretSelectionScreen.java index e27598388..fc768374d 100644 --- a/source/core/src/main/com/csse3200/game/screens/TurretSelectionScreen.java +++ b/source/core/src/main/com/csse3200/game/screens/TurretSelectionScreen.java @@ -20,6 +20,7 @@ import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable; +import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.viewport.ScreenViewport; import com.csse3200.game.GdxGame; import com.csse3200.game.services.ResourceService; @@ -115,7 +116,12 @@ public void clicked(InputEvent event, float x, float y) { @Override public void clicked(InputEvent event, float x, float y) { // Store the selected towers in the ServiceLocator for transferring across screens - ServiceLocator.setTowerTypes(selectedTurrets);; + // (as an Array) + Array towers = new Array<>(); + for (TowerType t : selectedTurrets) { + towers.add(t); + } + ServiceLocator.setTowerTypes(towers);; game.setScreen(GdxGame.ScreenType.MAIN_GAME); } }); diff --git a/source/core/src/main/com/csse3200/game/services/CurrencyService.java b/source/core/src/main/com/csse3200/game/services/CurrencyService.java index d76f7b147..4221d7a9c 100644 --- a/source/core/src/main/com/csse3200/game/services/CurrencyService.java +++ b/source/core/src/main/com/csse3200/game/services/CurrencyService.java @@ -49,6 +49,11 @@ public CurrencyDisplay getDisplay() { return display; } + /** + * Sets the tower type to build - triggered by pressing a tower build button in-game + * newTower can be a towertype or a null value to indicate clearing the value? + * @param newTower The towertype to be set for building, null if deselecting + */ public void setTowerType(TowerType newTower) { if (tower == newTower) { tower = null; diff --git a/source/core/src/main/com/csse3200/game/services/MapService.java b/source/core/src/main/com/csse3200/game/services/MapService.java index e677c9586..e82a0d0ea 100644 --- a/source/core/src/main/com/csse3200/game/services/MapService.java +++ b/source/core/src/main/com/csse3200/game/services/MapService.java @@ -1,9 +1,14 @@ package com.csse3200.game.services; +import com.badlogic.gdx.graphics.Camera; +import com.badlogic.gdx.graphics.OrthographicCamera; import com.csse3200.game.areas.terrain.TerrainComponent; import com.csse3200.game.areas.terrain.TerrainFactory; import com.csse3200.game.components.CameraComponent; import com.csse3200.game.entities.Entity; +import com.csse3200.game.rendering.CameraShaker; + + /** * Provides services related to map functionalities such as tiles and lanes in genral. @@ -12,15 +17,20 @@ public class MapService { private Entity entity; private final TerrainFactory terrainFactory; + private final CameraShaker cameraShakerMap; + private final CameraShaker cameraShakerGrid; /** * Constructs a new MapService instance based on the provided camera. * * @param camera The camera component used for the terrain creation. */ - public MapService(CameraComponent camera) { + public MapService(CameraComponent camera,Camera cam) { this.terrainFactory = new TerrainFactory(camera); this.entity = new Entity().addComponent(terrainFactory.createTerrain(TerrainFactory.TerrainType.ALL_DEMO)); + this.cameraShakerGrid = new CameraShaker(camera.getCamera()); + this.cameraShakerMap = new CameraShaker(cam,2,0.1f,0.8f); + } /** @@ -32,6 +42,8 @@ public MapService(CameraComponent camera) { public MapService(Entity entity, TerrainFactory terrainFactory) { this.entity = entity; this.terrainFactory = terrainFactory; + this.cameraShakerMap = new CameraShaker(new OrthographicCamera()); + this.cameraShakerGrid = new CameraShaker(new OrthographicCamera()); } /** @@ -73,4 +85,37 @@ public int getWidth() { return entity.getComponent(TerrainComponent.class).getMapBounds(0).x; } + + /** + * Update method for the Grid Camera Shaker. Calls this method in the main game loop. + * + * @param delta Time since the last frame. + */ + public void updateShakerGrid(float delta) { + cameraShakerGrid.update(delta); + } + + /** + * Starts the Grid shaking process + */ + public void shakeCameraGrid() { + cameraShakerGrid.startShaking(); + } + + /** + * Update method for the Background Camera Shaker. Calls this method in the main game loop. + * + * @param delta Time since the last frame. + */ + public void updateShakerMap(float delta) { + cameraShakerMap.update(delta); + } + + /** + * Starts the Background shaking process + */ + public void shakeCameraMap() { + cameraShakerMap.startShaking(); + } + } diff --git a/source/core/src/main/com/csse3200/game/services/ServiceLocator.java b/source/core/src/main/com/csse3200/game/services/ServiceLocator.java index 8f3c72154..7c16fc03f 100644 --- a/source/core/src/main/com/csse3200/game/services/ServiceLocator.java +++ b/source/core/src/main/com/csse3200/game/services/ServiceLocator.java @@ -33,7 +33,7 @@ public class ServiceLocator { private static WaveService waveService; private static MapService mapService; - private static Set towerTypes = new HashSet<>(); + private static Array towerTypes = new Array<>(); public static CurrencyService getCurrencyService() { return currencyService; @@ -120,12 +120,13 @@ public static void registerMapService(MapService source) { mapService = source; } - public static void setTowerTypes(Set selectedTowers) { + public static void setTowerTypes(Array selectedTowers) { + towerTypes.clear(); towerTypes.addAll(selectedTowers); } - public static Set getTowerTypes() { + public static Array getTowerTypes() { return towerTypes; } @@ -139,6 +140,7 @@ public static void clear() { gameEndService = null; waveService = null; mapService = null; + towerTypes.clear(); } private ServiceLocator() { diff --git a/source/core/src/main/com/csse3200/game/ui/UIComponent.java b/source/core/src/main/com/csse3200/game/ui/UIComponent.java index 60cfe7009..025282e70 100644 --- a/source/core/src/main/com/csse3200/game/ui/UIComponent.java +++ b/source/core/src/main/com/csse3200/game/ui/UIComponent.java @@ -32,4 +32,8 @@ public int getLayer() { public float getZIndex() { return 1f; } + + public static Skin getSkin() { + return skin; + } } diff --git a/source/core/src/test/com/csse3200/game/input/BuildInputComponentTest.java b/source/core/src/test/com/csse3200/game/input/BuildInputComponentTest.java index 3cd427659..1b967498c 100644 --- a/source/core/src/test/com/csse3200/game/input/BuildInputComponentTest.java +++ b/source/core/src/test/com/csse3200/game/input/BuildInputComponentTest.java @@ -1,159 +1,141 @@ -//package com.csse3200.game.input; -// -//import com.badlogic.gdx.graphics.g2d.TextureAtlas; -//import com.badlogic.gdx.maps.tiled.TiledMap; -//import com.badlogic.gdx.math.Vector2; -//import com.csse3200.game.components.CameraComponent; -//import com.csse3200.game.currency.Currency; -//import com.csse3200.game.entities.Entity; -//import com.csse3200.game.entities.EntityService; -//import com.csse3200.game.entities.factories.TowerFactory; -//import com.csse3200.game.extensions.GameExtension; -//import com.csse3200.game.physics.PhysicsService; -//import com.csse3200.game.rendering.DebugRenderer; -//import com.csse3200.game.rendering.RenderService; -//import com.csse3200.game.services.*; -//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 static org.junit.jupiter.api.Assertions.assertEquals; -//import static org.junit.jupiter.api.Assertions.assertFalse; -//import static org.mockito.Mockito.*; -// -//@ExtendWith(GameExtension.class) -//class BuildInputComponentTest { -// -// private BuildInputComponent buildInputComponent; -// private Entity baseTower; -// private Entity weaponTower; -// private Entity wallTower; -// private Entity stunTower; -// private Entity fireTower; -// private Entity tntTower; -// private Entity droidTower; -// private String[] texture = { -// "images/towers/turret_deployed.png", -// "images/towers/turret01.png", -// "images/towers/wall_tower.png", -// "images/towers/fire_tower_atlas.png", -// "images/towers/stun_tower.png", -// "images/towers/DroidTower.png", -// "images/towers/TNTTower.png" -// }; -// private String[] atlas = { -// "images/towers/turret01.atlas", -// "images/towers/stun_tower.atlas", -// "images/towers/fire_tower_atlas.atlas", -// "images/towers/DroidTower.atlas", -// "images/towers/TNTTower.atlas", -// "images/towers/barrier.atlas" -// }; -// private static final String[] sounds = { -// "sounds/towers/gun_shot_trimmed.mp3", -// "sounds/towers/deploy.mp3", -// "sounds/towers/stow.mp3" -// }; -// -// @BeforeEach -// void setup() { -// GameTime gameTime = mock(GameTime.class); -// CameraComponent camera = mock(CameraComponent.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); -// -// CurrencyService currencyService = new CurrencyService(); -// ResourceService resourceService = new ResourceService(); -// MapService mapService = new MapService(camera); -// EntityService entityService = new EntityService(); -// -// ServiceLocator.registerResourceService(resourceService); -// ServiceLocator.registerCurrencyService(currencyService); -// ServiceLocator.registerMapService(mapService); -// ServiceLocator.registerEntityService(entityService); -// -// resourceService.loadTextures(texture); -// resourceService.loadTextureAtlases(atlas); -// resourceService.loadSounds(sounds); -// resourceService.loadAll(); -// -// ServiceLocator.getResourceService() -// .getAsset("images/towers/turret01.atlas", TextureAtlas.class); -// baseTower = TowerFactory.createBaseTower(); -// weaponTower = TowerFactory.createWeaponTower(); -// wallTower = TowerFactory.createWallTower(); -// fireTower = TowerFactory.createFireTower(); -// stunTower = TowerFactory.createFireTower(); -// tntTower = TowerFactory.createTNTTower(); -// droidTower = TowerFactory.createDroidTower(); -// -// buildInputComponent = new BuildInputComponent(camera.getCamera()); -// } -// -// @Test -// void shouldUpdatePriority() { -// int newPriority = 100; -// InputComponent inputComponent = spy(InputComponent.class); -// -// inputComponent.setPriority(newPriority); -// verify(inputComponent).setPriority(newPriority); -// -// int priority = inputComponent.getPriority(); -// verify(inputComponent).getPriority(); -// -// assertEquals(newPriority, priority); -// } -// -// @Test -// void shouldRegisterOnCreate() { -// InputService inputService = spy(InputService.class); -// ServiceLocator.registerInputService(inputService); -// -// InputComponent inputComponent = spy(InputComponent.class); -// inputComponent.create(); -// verify(inputService).register(inputComponent); -// } -// -// @Test -// void shouldHandleTouchDown() { -// BuildInputComponent inputComponent = spy(BuildInputComponent.class); -// assertFalse(inputComponent.touchDown( 5, 6, 7, 8)); -// } -// -// @Test -// void shouldRejectOccupiedTile() { -// Vector2 tile = ServiceLocator.getMapService().getComponent().tileToWorldPosition(0, 0); -// tntTower.setPosition(0,0); -// assertFalse(buildInputComponent.touchDown(0,0, 7,8)); -// } -// -// @Test -// void shouldRejectInvalidTile() { -// -// } -// -// @Test -// void shouldHandleMissingMapService() { -// -// } -// -// @Test -// void shouldHandleMissingCurrencyService() { -// -// } -// -// @Test -// void shouldHandleInvalidTower() { -// -// } -// -// @Test -// void shouldHandleMissingEntityService() { -// -// } -//} +package com.csse3200.game.input; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Graphics; +import com.badlogic.gdx.graphics.Camera; +import com.csse3200.game.areas.terrain.TerrainComponent; +import com.csse3200.game.components.CameraComponent; +import com.csse3200.game.entities.EntityService; +import com.csse3200.game.extensions.GameExtension; +import com.csse3200.game.physics.PhysicsService; +import com.csse3200.game.rendering.DebugRenderer; +import com.csse3200.game.rendering.RenderService; +import com.csse3200.game.screens.TowerType; +import com.csse3200.game.services.*; +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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.*; + +@ExtendWith(GameExtension.class) +class BuildInputComponentTest { + + private BuildInputComponent buildInputComponent; + EntityService entityService; + + @BeforeEach + void setup() { + Gdx.graphics = mock(Graphics.class); + when(Gdx.graphics.getDeltaTime()).thenReturn(10f); + + GameTime gameTime = mock(GameTime.class); + CameraComponent camera = mock(CameraComponent.class); + when(camera.getCamera()).thenReturn(mock(Camera.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); + + CurrencyService currencyService = new CurrencyService(); + ResourceService resourceService = new ResourceService(); + MapService mapService = mock(MapService.class); + when(mapService.getComponent()).thenReturn(mock(TerrainComponent.class)); + entityService = mock(EntityService.class); + + ServiceLocator.registerResourceService(resourceService); + ServiceLocator.registerCurrencyService(currencyService); + ServiceLocator.registerMapService(mapService); + ServiceLocator.registerEntityService(entityService); + + buildInputComponent = new BuildInputComponent(camera.getCamera()); + } + + @Test + void shouldUpdatePriority() { + int newPriority = 100; + InputComponent inputComponent = spy(InputComponent.class); + + inputComponent.setPriority(newPriority); + verify(inputComponent).setPriority(newPriority); + + int priority = inputComponent.getPriority(); + verify(inputComponent).getPriority(); + + assertEquals(newPriority, priority); + } + + @Test + void shouldRegisterOnCreate() { + InputService inputService = spy(InputService.class); + ServiceLocator.registerInputService(inputService); + + InputComponent inputComponent = spy(InputComponent.class); + inputComponent.create(); + verify(inputService).register(inputComponent); + } + + @Test + void shouldHandleTouchDown() { + when(entityService.entitiesInTile(5, 5)).thenReturn(false); + assertFalse(buildInputComponent.touchDown( 5, 5, 7, 8)); + } + + @Test + void shouldRejectOccupiedOrInvalidTile() { + // entitiesInTile checks for out of bounds condition as well + when(entityService.entitiesInTile(5, 5)).thenReturn(true); + assertFalse(buildInputComponent.touchDown(5,5, 7,8), + "Attempting to build on an existing tower should return False"); + } + + @Test + void shouldHandleMissingMapService() { + when(ServiceLocator.getMapService()).thenReturn(null); + assertFalse(buildInputComponent.touchDown(5,5,7,8)); + } + + @Test + void shouldHandleMissingCurrencyService() { + when(ServiceLocator.getCurrencyService()).thenReturn(null); + assertFalse(buildInputComponent.touchDown(5,5,7,8)); + } + + @Test + void shouldHandleNullTowerName() { + TowerType towerType = mock(TowerType.class); + when(towerType.getTowerName()).thenReturn(null); + ServiceLocator.getCurrencyService().setTowerType(towerType); + } + + @Test + void shouldHandleNullTowerDesc() { + TowerType towerType = mock(TowerType.class); + when(towerType.getDescription()).thenReturn(null); + ServiceLocator.getCurrencyService().setTowerType(towerType); + } + + @Test + void shouldHandleNullTowerCost() { + TowerType towerType = mock(TowerType.class); + when(towerType.getPrice()).thenReturn(null); + ServiceLocator.getCurrencyService().setTowerType(towerType); + } + + @Test + void shouldHandleInvalidTowerName() { + TowerType towerType = mock(TowerType.class); + when(towerType.getTowerName()).thenReturn(null); + ServiceLocator.getCurrencyService().setTowerType(towerType); + } + + @Test + void shouldHandleMissingEntityService() { + when(ServiceLocator.getEntityService()).thenReturn(null); + assertFalse(buildInputComponent.touchDown(5,5,7,8)); + } +}