Coding Task 5 - Polymorphism


Time Estimate: 8 hours

Jump to current week
Requirements

Project Structure


You will continue to add functionality to your existing project from the previous task. There is no new repository to clone.



Graphics Engine Revisited


In addition to setting static sprites, the provided graphics engine also has capabilities to display animations. As with static graphics, understanding exactly how the graphics engine works is not necessary, and we will provide any information you need.

Every GameObject (and thus, any child class of GameObject) has an instance variable called animations, which is a HashMap mapping String to ArrayList of SpriteLocation. The String represents the name of the animation (e.g. "default"), and the ArrayList represents the series of locations in the spritesheet that will be cycled through while the object is animating (see task 3 handout for refresher on SpriteLocations). Along with storing various unrelated sprites in a spritesheet, common practice for games of this sort is to store every frame of an object's animations in the same spritesheet.

Thus, you can add new animations to a GameObject by calling put on this HashMap with a name for the animation and an ArrayList of SpriteLocations representing each frame of animation.

When a GameObject is created, it'll have the default animation state of "default", so if you have an animation named "default" in your animations HashMap, that animation will automatically play when the object is created. If you have multiple animations, you can call setAnimationState on the object and pass in the name of the animation you wish to play at that moment. You can call freezeAnimations on an object to stop the object from animating until setAnimationState is called again.

From here, everything else is handled completely automatically. The engine will progress each frame once per every the interval stated in Configuration.java.

As with setting sprites in general, if an animation is recommended we will specify the inputs you should use. Animations will also not be tested.



Specification


In this task, you will implement the following specs.

In the app.games.topdownobjects package, add the following methods to the Projectile class:

  • getDamage: this method should take no parameters and return an int. It should return the value of the damage instance variable.
  • setDamage: this method should take in an int and return void. It should set the value of the damage instance variable to the parameter.

In the app.games.topdownobjects package, create a class named PlayerMagicProjectile that extends Projectile. This class should have a constructor that takes in a Vector2D representing location.

  • This projectile should always deal 3 damage.
  • This projectile will inherit the default projectile sprite; you do not need to change it.

In the app.games.topdownobjects package, create a class named PlayerAxeProjectile that extends Projectile. This class should also have a constructor that takes in a Vector2D representing location.

  • This projectile should always deal 6 damage.
  • The suggested spriteSheetFilename is "Objects/Axe.png". The suggested defaultSpriteLocation is column 1, row 1.
  • The suggested default animation consists of the following locations: [(0, 0), (1, 0), (2, 0), (3, 0), (0, 1), (1, 1), (2, 1), (3,1)]
    • Refer to the Player class for an example of adding an animation. The key should be "default".

In the app.gameengine.model.gameobjects package, create an abstract class named CollectibleGameObject that extends DynamicGameObject.

  • The constructor should take in a Vector2D representing location and a String representing the ID of the item.
    • When calling the super constructor, pass in a maxHP of 10.
  • Create a method that takes no parameters and returns a String called getItemID that returns the ID of the item that was passed in the constructor.
  • Override the takeDamage method to do nothing.
    • This prevents it from being destroyed by projectiles or enemies.
  • Create an abstract method called use that takes in a Level and returns void.
    • Child classes will override this method to define what happens when this object is used.

In the app.gameengine.model.gameobjects package, make the following additions to the DynamicGameObject class to implement a functioning inventory system:

  • Add a method named addInventoryItem that takes in a CollectibleGameObject and returns void. This method should add the object from the parameter to the DynamicGameObject's inventory.
    • Adding an item should never change the currently active item, except for when you pick up your first item
  • Add a method named removeActiveItem that takes no parameters and returns void. This method should remove whatever the currently active item is from the DynamicGameObject's inventory.
    • Once the currently active item is removed, the currently active item should become whatever was next in the inventory. For example, if you collected an Axe, then Magic, then a Health Potion, the currently active item would be the Axe. If this method is called, the remaining items are the Magic and the Health Potion, and the active item would be the Magic.
    • If the item removed is the last item in the inventory, the active item would become the first item in the inventory. Be sure to account for this looping behavior in your code
  • Add a method named getActiveItem that takes no parameters and returns a CollectibleGameObject. This method should return whichever CollectibleGameObject in the inventory is active.
    • If the inventory is empty, this method should return null.
    • If an item is collected and the inventory was previously empty, that item should be the active item.
  • Add a method called getActiveItemID that takes in no parameters and returns a String.
    • This method should return the ID of the active CollectableGameObject from the inventory.
    • If the inventory is empty, this method should return the string "No item equipped".
  • Add a method called cycleInventory that takes no parameters and returns void. This method will cycle through the inventory, changing the item currently marked as active.
    • This should be done in the order the objects are collected. For example, if a Player collected an Axe, then Magic, then a Potion, the first call to cycleInventory will change the active item from the Axe to Magic. The second call to cycleInventory will change the active item from Magic to the Potion.
    • Once cycleInventory is called while the most recently collected item is active, the active item should be set back to the front of the inventory to the least recently collected item. Thus in our previous example, the third call to cycleInventory will change the active item from the Potion to the Axe.
  • Note that since these methods are the only way the inventory can be accessed and none of them rely on a specific data structure, however you choose to represent the inventory internally does not matter so long as your implementation follows the expected behavior. The only requirements are that you are able to keep track of every item that is added, as well as the object that is currently active.
  • The inventory should initially be empty.
  • You can cycle your character's inventory in-game with the tab key. Recall that you can fire projectiles with the space bar. Shortly you will modify this to use whichever item is currently active

IMPORTANT

The Player class already contains a cycleInventory method that does nothing. With the cycleInventory method implemented in DynamicGameObject, you can now remove the method in Player to use the inherited one instead. If you do not, whenever you have a Player object, this method will be called instead, and your code will fail our tests

Return to the CollectableGameObject class and add the following behavior:

  • Override the collideWithDynamicObject method to add the current CollectableGameObject to the DynamicGameObject's inventory by calling addInventoryItem. It should then destroy itself. It should only do these two things if the DynamicGameObject is the player (i.e. isPlayer returns true).

Now, you will write a method to fire projectiles from a DynamicGameObject. In the DynamicGameObject class, write a method called fireProjectile that returns void and takes in (in this order) a Projectile, a double representing speed, and a Level.

  • This method should set the projectile's location x and y coordinates to be the same as the DynamicGameObject's location
  • Then, it should set the projectile's x and y velocity to be the DynamicGameObject's respective orientation times the speed.
  • Finally, it should add the projectile to the Level's list of DynamicGameObjects from calling getDynamicObjects.
  • This method will be very similar to the current implementation of the actionButtonPressed method in the Level class.

Now that the functionality for firing projectiles is in the DynamicGameObject class, you will replace the action button functionality in the Level class to be based off of the Player's active item.

  • Replace the actionButtonPressed method in Level to call the use method on the Level's Player's currently active object.
    • If the currently active object is null, this method should do nothing.
    • You can access the Level's player with the getPlayer method

At this point, the engine's collectable functionality is built up enough that you can now add new, interactable collectables to the game. You may wish to edit the getUI method in the Game class to show the currently active item using the getActiveItemID method, otherwise there will be no way to tell which item is equipped.

In the app.games.topdownobjects package, create a class called MagicPickup that extends CollectibleGameObject.

  • Add a constructor that takes in a Vector2D representing location.
    • When calling the super constructor, pass in "Magic" for the item's ID.
    • The suggested spriteSheetFilename for this class is "User Interface/Icons-Essentials.png" and the suggested defaultSpriteLocation is column 1, row 0.
  • Override the use method from CollectibleGameObject. This method should access the Player from the Level parameter and call the fireProjectile method on said Player.
    • The projectile parameter should be a new instance of PlayerMagicProjectile. Since the fireProjectile method sets the location of the projectile, the initial location passed into the constructor does not matter.
    • The speed should be 10, and the Level should be the same level that was passed into the use method.

In the app.games.topdownobjects package, create a class called AxePickup that extends CollectibleGameObject.

  • Add a constructor that takes in a Vector2D representing location.
    • When calling the super constructor, pass in "Axe" for the item's ID.
    • The suggested spriteSheetFilename for this class is "Objects/Axe.png" and the suggested defaultSpriteLocation is column 0, row 0.
  • Override the use method from CollectibleGameObject. This method should access the Player from the Level parameter and call the fireProjectile method on said Player.
    • The projectile parameter should be a new instance of PlayerAxeProjectile. As with the previous class, the initial location does not matter.
    • The speed should be 5, and the Level should be the same level that was passed into the use method.

In the app.games.topdownobjects package, create a class called PotionPickup that extends CollectibleGameObject.

  • Create a constructor that takes in a Vector2D representing location and an int representing the amount to heal.
    • When calling the super constructor, pass in "Health Potion" for the item's ID.
    • The suggested spriteSheetFilename for this class is "User Interface/Icons-Essentials.png" and the suggested defaultSpriteLocation is column 3, row 1.
  • Override the use method.
    • This method should access the player from the Level parameter and set their HP to be their current HP plus the amount to heal specified in the constructor.
    • This method should then call removeActiveItem on the player.


Testing Utilities


There is no testing utility for this task.



Testing Requirements


In the app.tests package, create a class called TestTask5 and test the following:

  • DynamicGameObject
    • addInventoryItem and cycleInventory:
      • Write tests to ensure that adding items to the inventory has the proper behavior when cycling through the inventory, i.e. calling cycleInventory will go through every added CollectableGameObject and wrap around to the beginning
    • removeActiveItem:
      • Write tests to ensure that calling removeActiveItem properly removes the active item from the inventory and sets the new active item.
    • When testing these methods, you only need to check that the item IDs match the expected.


Programming Requirements


Implement all the classes/methods described in the Specification section. As you're implementing inventory, you should run your tests to see how you are progressing. It's recommended that you write your tests first, submit to Autolab to make sure you have good testing, then use those verified tests, along with the debugger, to check your code.



Autolab Feedback


The feedback in Autolab will be given in 3 phases. If you don't complete a phase, then feedback for the following phase(s) will not be provided.

  1. Running your tests on a correct solution
    • Your tests will be run against a solution that is known to be correct. If your tests do not pass this correct solution, there is an error somewhere in your tests that must be fixed before you can move on with the assignment. If your tests don't get past this check, you should re-read this document and make sure you implemented your tests and code according the specification. You should also make sure that if there are multiple correct outputs to the input in your tests cases that you accept any of the outputs as correct
  2. Checking your tests for feature coverage
    • The next phase is to check if your tests check for a variety of features defined by different inputs. You should write at least one test case for each feature to pass this phase
    • Passing this phase does not necessarily mean that your testing is completely thorough. Satisfying Autolab is the bare minimum testing requirement. Not all possible inputs are checked, and it is sometimes possible to pass this phase with weak testing. If you are struggling to earn credit for code that you believe is correct, you should write more than the required tests
  3. Running my tests on your solution
    • Once Autolab is happy with your tests, it will run my tests against your code to check it for correctness. If your testing is thorough, and your code passes your tests, then you should pass this phase. If you pass your tests, but fail one of mine, it is an indicator that you should write more tests to help expose your bug

Once you complete all 3 phases, you will have completed this Task and Autolab will confirm this with a score of 1.0 for complete.