diff --git a/MapFiles/test_map.txt b/MapFiles/test_map.txt index 32d1335..23f66ec 100644 --- a/MapFiles/test_map.txt +++ b/MapFiles/test_map.txt @@ -5,11 +5,11 @@ 1 3 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 8 8 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 8 8 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 7 1 1 1 1 1 1 1 1 1 1 1 1 1 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 17 1 5 11 11 12 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 1 1 18 1 1 1 1 17 1 5 11 11 12 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 13 11 6 1 1 1 1 1 1 1 1 1 1 1 -1 1 1 1 1 8 8 8 1 1 1 1 1 1 9 1 1 1 1 1 7 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 1 1 1 1 8 8 8 1 1 1 1 1 1 9 1 1 1 1 1 7 1 1 1 1 1 1 20 1 1 1 1 1 1 1 1 1 1 1 8 8 8 1 1 1 1 1 0 0 0 0 1 1 1 7 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 7 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 -1 1 1 1 1 1 7 1 1 10 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 14 14 14 14 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 15 15 15 15 0 0 0 0 +1 1 1 1 1 1 7 1 1 10 1 0 0 0 0 0 0 0 0 0 0 0 0 0 19 19 14 14 14 14 19 19 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 19 15 15 15 15 19 0 0 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 16 16 16 16 2 2 2 2 \ No newline at end of file diff --git a/MapFiles/test_map3.txt b/MapFiles/test_map3.txt index 9109457..5960f12 100644 --- a/MapFiles/test_map3.txt +++ b/MapFiles/test_map3.txt @@ -11,5 +11,5 @@ 1 1 1 1 1 5 11 11 4 7 1 1 1 1 1 1 1 1 1 1 7 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 7 7 1 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 9 0 0 0 0 0 1 10 1 1 1 1 1 1 1 1 7 7 10 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 14 14 14 14 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -2 2 2 2 2 2 2 2 2 2 2 2 2 16 16 16 16 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 \ No newline at end of file +0 0 0 0 0 0 0 0 0 0 0 0 19 14 14 14 14 19 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 2 2 2 2 2 2 2 19 16 16 16 16 19 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 \ No newline at end of file diff --git a/MapFiles/test_map4.txt b/MapFiles/test_map4.txt index afd1802..c4a5580 100644 --- a/MapFiles/test_map4.txt +++ b/MapFiles/test_map4.txt @@ -8,8 +8,8 @@ 1 1 1 1 1 1 1 1 1 1 1 1 1 1 17 1 1 5 11 11 12 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 13 11 6 1 1 1 1 1 1 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 9 1 1 1 1 1 7 1 1 1 1 1 1 1 5 8 8 8 1 1 -1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 1 1 1 7 1 1 1 1 1 1 1 1 1 4 1 1 1 +1 1 1 1 1 1 18 1 1 1 1 1 1 0 0 0 0 1 1 1 7 1 1 1 1 1 1 1 1 1 4 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 7 1 1 1 -1 1 1 1 10 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 14 14 14 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -2 2 2 2 2 16 16 16 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 16 16 16 16 2 2 2 2 \ No newline at end of file +1 1 1 10 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +0 0 0 0 19 14 14 14 19 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 19 16 16 16 19 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 16 16 16 16 2 2 2 2 \ No newline at end of file diff --git a/MapFiles/test_map5.txt b/MapFiles/test_map5.txt index 3dc62c5..5e25dea 100644 --- a/MapFiles/test_map5.txt +++ b/MapFiles/test_map5.txt @@ -11,5 +11,5 @@ 1 1 1 1 5 11 7 1 1 1 1 1 1 0 0 0 0 0 1 1 7 1 1 1 1 1 1 1 1 5 11 11 13 1 1 1 1 1 1 1 1 1 1 7 1 1 1 1 1 0 0 0 0 0 0 0 1 7 1 1 1 1 1 1 1 1 1 1 1 7 1 1 1 1 1 1 1 1 1 1 7 1 1 10 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 7 1 1 1 1 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 14 14 14 0 0 0 0 0 0 0 0 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 16 16 16 2 2 2 2 2 2 2 2 \ No newline at end of file +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 19 14 14 14 19 0 0 0 0 0 0 0 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 19 16 16 16 19 2 2 2 2 2 2 2 \ No newline at end of file diff --git a/MapFiles/test_map7.txt b/MapFiles/test_map7.txt index 175a59b..9cd277b 100644 --- a/MapFiles/test_map7.txt +++ b/MapFiles/test_map7.txt @@ -11,5 +11,5 @@ 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 7 11 11 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 5 7 7 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 0 0 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 7 7 1 1 1 1 1 1 1 1 1 1 1 2 2 1 1 1 1 2 2 1 1 1 1 2 2 1 1 1 1 1 1 1 -0 0 0 14 14 14 14 14 14 14 0 0 0 0 0 0 0 0 0 0 0 0 14 14 14 14 14 0 0 16 16 0 0 0 0 16 16 0 0 0 0 16 16 0 0 0 0 0 0 0 -2 2 2 2 15 15 15 15 15 2 2 2 2 2 2 2 2 2 2 2 2 2 2 15 15 15 2 2 2 16 16 2 2 2 2 16 16 2 2 2 2 16 16 2 2 2 2 2 2 2 \ No newline at end of file +0 0 19 14 14 14 14 14 14 14 19 0 0 0 0 0 0 0 0 0 0 19 14 14 14 14 14 19 0 16 16 0 0 0 0 16 16 0 0 0 0 16 16 0 0 0 0 0 0 0 +2 2 19 19 15 15 15 15 15 19 19 2 2 2 2 2 2 2 2 2 2 19 19 15 15 15 19 19 2 16 16 2 2 2 2 16 16 2 2 2 2 16 16 2 2 2 2 2 2 2 \ No newline at end of file diff --git a/MapFiles/test_map_2.txt b/MapFiles/test_map_2.txt index 0e156fe..67857f3 100644 --- a/MapFiles/test_map_2.txt +++ b/MapFiles/test_map_2.txt @@ -11,5 +11,5 @@ 1 1 1 5 8 8 8 1 1 1 1 1 4 1 1 1 1 1 0 9 1 1 1 1 1 1 4 1 1 1 1 1 1 1 1 1 1 1 1 7 1 1 1 1 1 5 7 6 1 1 1 0 0 0 0 1 1 1 1 5 7 1 1 1 1 1 1 1 1 1 1 1 10 7 9 1 1 1 1 1 7 1 1 1 0 0 0 0 0 0 1 1 1 9 7 1 1 1 1 1 1 1 -0 0 0 0 0 0 0 0 0 14 14 14 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -2 2 2 2 2 2 2 2 2 16 16 16 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 \ No newline at end of file +0 0 0 0 0 0 0 0 19 14 14 14 19 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +2 2 2 2 2 2 2 2 19 16 16 16 19 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 \ No newline at end of file diff --git a/MapFiles/test_tutorial.txt b/MapFiles/test_tutorial.txt index 80b8f73..4b12489 100644 --- a/MapFiles/test_tutorial.txt +++ b/MapFiles/test_tutorial.txt @@ -6,10 +6,10 @@ 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 17 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 -1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 18 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 8 8 8 1 1 1 1 1 1 9 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 7 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 -1 1 1 1 1 1 7 1 1 10 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 14 14 14 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 15 15 15 0 0 0 0 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 16 16 16 2 2 2 2 \ No newline at end of file +1 1 1 1 1 1 7 1 1 10 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 19 14 14 14 19 0 0 0 +0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 19 15 15 15 19 0 0 0 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 19 16 16 16 19 2 2 2 \ No newline at end of file diff --git a/Resources/CommonTileset.png b/Resources/CommonTileset.png index 04a727a..03391b2 100644 Binary files a/Resources/CommonTileset.png and b/Resources/CommonTileset.png differ diff --git a/Team A2/Test Cases/Test Case SCP-15.md b/Team A2/Test Cases/Test Case SCP-15.md index 74bf5ba..a345e64 100644 --- a/Team A2/Test Cases/Test Case SCP-15.md +++ b/Team A2/Test Cases/Test Case SCP-15.md @@ -1,5 +1,5 @@ ### Test Case Information -| TEST CASE ID | SCP-10| +| TEST CASE ID | SCP-15| | :--- | :--- | | Owner of Test | Ty Hutchison| | Test Name | Lower Base Volume | @@ -20,4 +20,4 @@ - **Tester**: Thomas Kwashnak - **Date of Test**: 11/9/2021 - **Test Result**: Passed -- **Notes:** *Thank god this is done now* \ No newline at end of file +- **Notes:** *Thank god this is done now* diff --git a/Team A2/Test Cases/Test Case SCP-32.md b/Team A2/Test Cases/Test Case SCP-32.md index 322cdb0..e565e94 100644 --- a/Team A2/Test Cases/Test Case SCP-32.md +++ b/Team A2/Test Cases/Test Case SCP-32.md @@ -1,5 +1,5 @@ ### Test Case Information -| TEST CASE ID | SCP-10| +| TEST CASE ID | SCP-32| | :--- | :--- | | Owner of Test | Ty Hutchison| | Test Name | Update Instructions | @@ -19,4 +19,4 @@ ### Test Completion - **Tester**: Thomas Kwashnak - **Date of Test**: 11/9/2021 -- **Test Result**: Passed \ No newline at end of file +- **Test Result**: Passed diff --git a/Team A2/Test Cases/Test Case SCP-34.md b/Team A2/Test Cases/Test Case SCP-34.md new file mode 100644 index 0000000..b9b69bb --- /dev/null +++ b/Team A2/Test Cases/Test Case SCP-34.md @@ -0,0 +1,22 @@ +### Test Case Information +| TEST CASE ID | SCP-34| +| :--- | :--- | +| Owner of Test | Ty Hutchison | +| Test Name | Adding Map Tile | +| Date of Last Revision | 11/8/2021 | +| Test Objective | Adding another map tile to the game | + +### Procedure + +|Step | Action | Expected Result | Pass/Fail | +|:---:| :--- | :---- | :---: | +|1| Run the game | The game successfully opens |Passed| +|2| Enter level 1 | The User will enter level 1 |Passed| +|3| Find new map tile next to water tile | User will find the new map tile, sand, next to water tiles in game |Passed| +|4| User will exit the game | The User will properly exit the game |Passed| + + +### Test Completion +- **Tester**: Thomas Kwashnak +- **Date of Test**: 11/29/2021 +- **Test Result**: Passed \ No newline at end of file diff --git a/Team A2/Test Cases/Test Case SCP-47.md b/Team A2/Test Cases/Test Case SCP-47.md new file mode 100644 index 0000000..18c7f0d --- /dev/null +++ b/Team A2/Test Cases/Test Case SCP-47.md @@ -0,0 +1,22 @@ +### Test Case Information +| TEST CASE ID | SCP-47| +| :--- | :--- | +| Owner of Test | Ty Hutchison| +| Test Name | Add Solid Floating Block | +| Date of Last Revision | 11/29/2021 | +| Test Objective | Add Solid Floating block for user to use | + +### Procedure + +|Step | Action | Expected Result | Pass/Fail | +|:---:| :--- | :---- | :---: | +|1| Run the game| The game successfully opens |Passed| +|2| Enter Level 1 | The User will enter level 1 |Passed| +|3| Go through the level and test implemented floating block | The User will go through the level completing it while using the floating block |Passed| +|4| Complete level 1 | The User will complete level 1 |Passed| +|5| Exit the game | The User will exit the game |Passed| + +### Test Completion +- **Tester**: Thomas Kwashnak +- **Date of Test**: 11/29/2021 4:21 PM +- **Test Result**: Passed \ No newline at end of file diff --git a/Team A2/Test Cases/Test Case SCP-49.md b/Team A2/Test Cases/Test Case SCP-49.md new file mode 100644 index 0000000..c11eb6d --- /dev/null +++ b/Team A2/Test Cases/Test Case SCP-49.md @@ -0,0 +1,31 @@ +### Test Case Information +| TEST CASE ID | SCP-49 | +| :--- | :--- | +| Owner of Test | Nicholas Tourony | +| Test Name | Difficulty Test | +| Date of Last Revision | 11/30/2021 | +| Test Objective | Ensure that the difficulty settings change the amount of health the cat has and changes the movement speed of the enemies and their projectiles. | + +### Procedure + +|Step | Action | Expected Result | Pass/Fail | +|:---:| :--- | :---- | :---: | +|1| Run the game| The game successfully opens |Pass| +|2| Select the difficulty menu. | The difficulty menu opens. | Pass | +|3| Select normal difficulty | The game lets you select normal difficulty and automatically puts you back to the main menu. | Pass| +|4| Start the game. | The cat is loaded into the first level with 3 health and the enemies and projectiles move at their slowest speed. | Pass| +|5| Take projectile damage until the character dies | The cat can take 3 projectile hits before dying. | Pass| +|6| Respawn and run into an enemy. | The cat instantly dies. | Pass | +|7| Select hard difficulty and start the game | The cat is loaded into the first level with 2 health and the enemies move faster. | Pass | +|8| Take projectile damage until the character dies | The cat can take 2 projectile hits before dying. | Pass| +|9| Respawn and run into an enemy. | The cat instantly dies. | Pass | +|10| Select hardcore difficulty and start the game. | The cat is loaded into the first level with 1 health and the enemies move even faster. | Pass | +|11| Take damage. | The cat dies. | Pass | +|12| Press escape. | The player is returned to the main menu. | Pass | +|13| Load into level 6, die to any enemy, and press space. | The player is restarted at the tutorial level. | Pass | +|14| Restart the game and beat each level at every difficulty to ensure the levels are still possible. (Use the level select to make hardcore mode easier) | Each level is beat on every difficulty | Passed | + +### Test Completion +- **Tester**: Thomas Kwashnak +- **Date of Test**: 11/30/2021 +- **Test Result**: Passed \ No newline at end of file diff --git a/Team A2/Test Cases/Test Case SCP-51.md b/Team A2/Test Cases/Test Case SCP-51.md new file mode 100644 index 0000000..95d43ec --- /dev/null +++ b/Team A2/Test Cases/Test Case SCP-51.md @@ -0,0 +1,21 @@ +### Test Case Information +| TEST CASE ID | SCP-51| +| :--- | :--- | +| Owner of Test | Ty Hutchison| +| Test Name | Implement Quit Option | +| Date of Last Revision | 11/29/2021 | +| Test Objective | Add Quit option in main menu | + +### Procedure + +|Step | Action | Expected Result | Pass/Fail | +|:---:| :--- | :---- | :---: | +|1| Run the game| The game successfully opens |Pass| +|2| Hit Quit Option | The User will select the quit option in the main menu |Pass| +|3| The game will close | The game will close |Pass| + + +### Test Completion +- **Tester**: Thomas Kwashnak +- **Date of Test**: 11/29/2021 3:59 PM +- **Test Result**: Passed \ No newline at end of file diff --git a/Team A2/Test Cases/Test Case SCP-52.md b/Team A2/Test Cases/Test Case SCP-52.md new file mode 100644 index 0000000..b2c28d8 --- /dev/null +++ b/Team A2/Test Cases/Test Case SCP-52.md @@ -0,0 +1,38 @@ +### Test Case Information +| TEST CASE ID | SCP-52 | +| :--- | :--- | +| Owner of Test | Thomas Kwashnak | +| Test Name | Test Speed-Running Enhancements | +| Date of Last Revision | 11/23/2021 | +| Test Objective | Testing Additions and Functionality Changes of Speed-Running Enhancements | + +#### Tested Enhancements + - Stopwatch + - Top-Right of screen when playing + - Entering from main menu resets timer + - Stores time took to complete each level, displays current level's time, as well as total time for all previous times + - Some way to display the user's final result (breakdown of levels) after beating final boss + - Deaths are in some way detrimental to a user's level time (either make death animation slower, or make death screen longer) + - Pausing the game pauses the time + - Level Completed Screens display quicker (shorter time) + +### Procedure + +|Step | Action | Expected Result | Pass/Fail | +|:---:| :--- | :---- | :---: | +|1|Run the game|The game loads up. There is no timer on the main menu.|Pass| +|2|Click Play|The tutorial level is loaded. On the top right is one timer that times the amount of elapsed time since the player started.|Pass| +|3|Navigate and die in the tutorial level|The dead screen has a count-down displayed.|Not implemented| +|4|While the death screen's countdown is counting down, try to hit space to respawn|The player is not able to respawn until the countdown timer is over|Not implemented| +|5|Navigate through the level and die once again. Once the death screen is loaded, hit escape to go back to the main menu|The main menu is loaded and the timer no longer displays|Pass| +|6|Hit "Play" again to go back into the main menu|The tutorial level is loaded, and the timer is reset back to 0|Pass| +|7|Complete the tutorial level|The level completed screen is shown, but only for a brief second before the next level is loaded. On the top right is now two timers, one that displays the total time, and another that displays the elapsed time for the current level|Pass| +|8|Hit `P/Escape` to pause the game|While the game is paused, timers are also paused|Pass| +|9|Exit back to main menu, Select "Difficulty", and select "Hardcore"|The player is brought back to the main menu, and the hardcore difficulty is selected|Pass| +|10|Select "Level Select", then select "Level 3"|Level 3 is loaded, the timer starts running|Pass| +|11|Die, and then respawn|The timer is reset back to 0|Pass| + +### Test Completion +- **Tester**: Nicholas Tourony +- **Date of Test**: 11/30/2021 +- **Test Result**: Pass \ No newline at end of file diff --git a/Team A2/Test Cases/Test Case SCP-55.md b/Team A2/Test Cases/Test Case SCP-55.md new file mode 100644 index 0000000..983eca9 --- /dev/null +++ b/Team A2/Test Cases/Test Case SCP-55.md @@ -0,0 +1,23 @@ +### Test Case Information +| TEST CASE ID | SCP-55 | +| :--- | :--- | +| Owner of Test | Jacob Conrad | +| Test Name | Character preview not updating properly | +| Date of Last Revision | 11/16/2021 | +| Test Objective | Verify that the character preview shows with the last selected option not the last highlighted option. | + +### Procedure + +|Step | Action | Expected Result | Pass/Fail | +|:---:| :--- | :---- | :---: | +|1|Run the game|The main menu displays successfully|Pass| +|2|Press space on "Options"|The options menu should load|Pass| +|3|Click on orange and then hover over blue or green|The orange cat should be displayed|Pass| +|4|Click on blue and then hover over orange or green|The blue cat should be displayed|Pass| +|5|Click on green and then hover over blue or orange|The green cat should be displayed|Pass| + + +### Test Completion +- **Tester**: Thomas Kwashnak +- **Date of Test**: 11/29/2021 10:17 AM +- **Test Result**: Passed \ No newline at end of file diff --git a/Team A2/Test Cases/Test Case SCP-56.md b/Team A2/Test Cases/Test Case SCP-56.md new file mode 100644 index 0000000..ad163af --- /dev/null +++ b/Team A2/Test Cases/Test Case SCP-56.md @@ -0,0 +1,48 @@ +### Test Case Information +| TEST CASE ID | SCP-56 | +| :--- | :--- | +| Owner of Test | Thomas Kwashnak | +| Test Name | Test Game Speed | +| Date of Last Revision | 11/16/2021 | +| Test Objective | Ensure that the game runs consistently through multiple distributions | +| Test Notes| Due to the nature of this test case, multiple runs of the test must be conducted on various machines of different make and models. Measurements will be taken using a recording software, and should not be expected to be perfect| +|Software Requirement|**Screen recording software:** Needs to be able to record the screen such that the tester can go back and accurately measure the time between events. *Suggested:* OBS| +|Prerequisites|Test Case SCP-57 must be completed prior to this merge| + + +### Procedure + +|Step | Action | Expected Result | Measurement Field | +|:---:| :--- | :---- | :----:| +|1|Run the game|The game successfully opens|pass| +|2|Begin recording|A screen recorder is started that records the game|pass| +|3|Click *Play Game*|The tutorial level is successfully opened|pass| +|4|Walk to the left-most edge of the level|The cat is at the left most bound of the tutorial level|pass| +|5|Hold `D/Right Arrow` until the cat is blocked from moving by the tree|The cat moves from the edge of the map into the tree|**Measurement 1:** The time it takes starting when the cat begins moving to when the cat finishes moving| +|6|Hold `Shift`, then hold `A/Left Arrow` until the cat stops moving at the left edge of the map|The player sprints to the left-most edge of the map|**Measurement 2:** The time it takes for the cat to move starting when the cat begins moving to when the cat is stopped by the map edge| +|7|Hold `Space/W/Up Arrow` until the cat reaches the ground again|The cat jumps and lands back on the ground|**Measurement 3:** The time it takes starting when the cat begins jumping to when the cat hits the ground again| +|8|Hold `e` until the player shoots a reasonable amount of fireballs|The cat shoots multiple fireballs|**Measurement 4:** The time between two consecutive fireballs spawning (multiple attempts can be averaged if needed)|\ +|9|Complete the tutorial level|When the tutorial level is completed, the `level complete` is shown, and after a few seconds the player is loaded into the next level|**Measurement 5:** The time that the `level complete` screen is shown + +### Test Execution +Instances of tests are listed in table below (template provided). Measurements listed in instructions above are listed below for test completer to evaluate. + +**Overall Test Completion** indicates whether all steps were completed and no errors / inability to follow instructions occurred. + + +|Date|Name|Device Information | Measure 1|Measure 2|Measure 3|Measure 5|Measure 4|Pass/Fail| +|:---:|:---|:---|:---:|:---:|:---:|:---:|:---:|:---:| +|[DATE]|[TESTER_NAME]|[CONFIG]|[MEASURE 1]|[MEASURE 2]|[MEASURE 3]|[MEASURE 4]|[MEASURE 5]|[PASS/FAIL]| +|11/29/2021 10:41 AM|Thomas Kwashnak|Kubuntu 21.10 Laptop, Intel i7 CPU, NVIDIA MX330 GPU|\~ 2 seconds|\~1 second|\~1 second|\~1-2 seconds|~3 seconds|Pass. Game runs at expected speed| +|11/29/2021 7:05 PM|Thomas Kwashnak|Windows 11 Desktop, Ryzen 5 3600 CPU, Nvidia RTX 2060 GPU|\~ 1.5 Seconds|\~ 1 second|\~ 1 second|\~1-2 seconds|3 seconds|Pass. Game runs at expected speed| +|11/30/2021 5:13 PM|Nicholas Tourony|Windows 10 Laptop, Intel i7-9750H CPU, Nvidia GTX 1660Ti GPU|\~ 1.75 Seconds|\~ 1 second|\~ 1 second|\~1.5 seconds|~3 seconds|Pass. Game runs at expected speed| + +[comment]: <> (Add test rows to end here ^^) + +### Completion Criteria + - There are no outliers on all measurements + +### Test Completion +- **Tester**: Nicholas Tourony +- **Date of Test**: 11/30/2021 +- **Test Result**: Pass \ No newline at end of file diff --git a/Team A2/Test Cases/Test Case SCP-57.md b/Team A2/Test Cases/Test Case SCP-57.md new file mode 100644 index 0000000..ca6eca9 --- /dev/null +++ b/Team A2/Test Cases/Test Case SCP-57.md @@ -0,0 +1,53 @@ +### Test Case Information +| TEST CASE ID | SCP-57 | +| :--- | :--- | +| Owner of Test | Thomas Kwashnak | +| Test Name | Test Player Functionality | +| Date of Last Revision | 11/16/2021 | +| Test Objective | Ensure that all current functionalities of the player is implemented in new Player class and still functional | + +Functionality of `Player.java` tested: + - Movement, and movement animations + - Jumping, including gravity and hitting floor + - Animation while jumping + - Collision with enemies + - Shooting, including animations while standing still and moving + - Collision with map boundaries and map walls + - Ability to complete all levels with physics + +### Procedure + +|Step | Action | Expected Result | Pass/Fail | +|:---:| :--- | :---- | :---: | +|0a| **Evaluated on Each Step:** | Player Animation changes as expected (*If a step does not pass this result, fail that step and indicate "Failed Step 0a"*) |`N/A`| +|1| Run the game| The game successfully opens |Pass| +|2| Click "*Play*" |The tutorial level is entered, game does not crash. 3 hearts are displayed on the top of the screen.|Pass| +|3| Hold `A/Left Arrow` |The player moves to the left. The player does not fall off the map and is instead stopped at the edge|Pass| +|4| Hold `D/Right Arrow`|The player moves to the right. The player is stopped once they hit the tree|Pass| +|5| Hold both `A/Left Arrow` and `D/Right Arrow`|The player stands still|Pass| +|6| Tap `Space/W/Up Arrow` | The player short-jumps. The player then falls back down after the button is let go and lands on the ground|Pass| +|7|Hold `Space/W/Up Arrow` | The player high-jumps. The player falls back down after a certain height and and falls back down to the ground|Pass| +|8|Hold `Shift` and repeat steps 3-5|The player moves in the same ways as stated above, but faster|Pass| +|9|Hold `ctrl/S/Down Arrow`|The player crouches|Pass| +|10|Tap `A/Left Arrow`, then hit `Space/W/Up Arrow` quickly followed by `D/Right Arrow`|The player faces left, then switches to facing right while in the air|Pass| +|11|Tap `D/Right Arrow`, then hit `Space/W/Up Arrow` quickly followed by `A/Left Arrow`|The player faces right, then switches to facing left while in the air|Pass| +|12|Tap `A/Left Arrow`, then tap `e` (*Attack Button*)|The player faces to the left, and attacks, sending a projectile to the left|Pass| +|13|Hold `A/Left Arrow`, then tap `e` |The player moves to the left, and attacks while moving|Pass| +|14|Tap `Space/W/Up Arrow`, then tap `e` while in the air|The player jumps while facing left, and then shoots a projectile while in the air|Pass| +|15|Tap `D/Right Arrow`, then tap `e` |The player faces to the right, and attacks, sending a projectile to the right|Pass| +|16|Hold `D/Right Arrow`, then tap `e` |The player moves to the right, and attacks while moving|Pass| +|17|Tap `Space/W/Up Arrow`, then tap `e` while in the air|The player jumps while facing right, and then shoots a projectile while in the air|Pass| +|18|Hold `e`|The player shoots once, then cannot shoot for a specified duration of time|Pass| +|19|Navigate and run into enemy|The player's health is set to 0 and the player dies. Death animation is played and player falls down to the ground|Pass| +|20|Restart the game|The level is loaded again, and the player's health is back to full|Pass| +|21|Navigate and run into a projectile from an enemy|The player's health is now 2 (reduced by 1)|Pass| +|22|Navigate and attempt to run off the right-most edge of the map (*without hitting the gold level-complete box*)|The player is stopped and cannot run off of the map|Pass| +|23|Hit the complete-level gold box|The level is completed and the player walks off to the right of the screen|Pass| +|24|Hit Escape, go back to main menu|The main menu is displayed|Pass| +|25|Hit "Play Game"|The first level is loaded|Pass| +|26|Play all levels of the game|Each level is completable|Pass| + +### Test Completion +- **Tester**: Nicholas Tourony +- **Date of Test**: 11/30/2021 +- **Test Result**: Passed \ No newline at end of file diff --git a/Team A2/Test Cases/Test Case SCP-58.md b/Team A2/Test Cases/Test Case SCP-58.md index 4072af2..a55f4c8 100644 --- a/Team A2/Test Cases/Test Case SCP-58.md +++ b/Team A2/Test Cases/Test Case SCP-58.md @@ -10,14 +10,14 @@ |Step | Action | Expected Result | Pass/Fail | |:---:| :--- | :---- | :---: | -|1|Run the game|The main menu displays successfully|| -|2|Press space on "Level Select"|The list of levels should load|| -|3|Press Space on "Boss Battle"|The user should be at the start of the level "Boss Battle"|| -|4|Get within one tile in front of the boss |The cat should not die|| -|5|Get within one tile behind the boss |The cat should not die|| -|4|Touch the boss |The cat should die|| +|1|Run the game|The main menu displays successfully|Pass| +|2|Press space on "Level Select"|The list of levels should load|Pass| +|3|Press Space on "Boss Battle"|The user should be at the start of the level "Boss Battle"|Pass| +|4|Get within one tile in front of the boss |The cat should not die|Pass| +|5|Get within one tile behind the boss |The cat should not die|Pass| +|4|Touch the boss |The cat should die|Pass| ### Test Completion -- **Tester**: -- **Date of Test**: -- **Test Result**: \ No newline at end of file +- **Tester**: Thomas Kwashnak +- **Date of Test**: 11/29/2021 10:20 AM +- **Test Result**: Passed \ No newline at end of file diff --git a/Team A2/Test Cases/Test Case SCP-59.md b/Team A2/Test Cases/Test Case SCP-59.md new file mode 100644 index 0000000..b284fc0 --- /dev/null +++ b/Team A2/Test Cases/Test Case SCP-59.md @@ -0,0 +1,20 @@ +### Test Case Information +| TEST CASE ID | SCP-59 | +| :--- | :--- | +| Owner of Test | Thomas Kwashnak | +| Test Name | Test Level Select Navigation | +| Date of Last Revision | 11/16/2021 | +| Test Objective | Test the keyboard navigation of the level select screen | + +### Procedure + +|Step | Action | Expected Result | Pass/Fail | +|:---:| :--- | :---- | :---: | +|1| Run the game| The game successfully opens |Pass| +|2|Select "Level Select"|The level select screen is opened. Levels are listed in a grid formation|Pass| +|3|Use `w/up arrow`,`a/left arrow`,`s/down arrow`,`d/right arrow` to navigate from and to each level listed|Navigating by a direction selects the first option visually in that direction. If the option is at the edge, the selection does not change.|Pass| + +### Test Completion +- **Tester**: Nicholas Tourony +- **Date of Test**: 11/18/2021 +- **Test Result**: Passed \ No newline at end of file diff --git a/Team A2/Test Cases/Test Case SCP-60.md b/Team A2/Test Cases/Test Case SCP-60.md new file mode 100644 index 0000000..a7a9aa6 --- /dev/null +++ b/Team A2/Test Cases/Test Case SCP-60.md @@ -0,0 +1,21 @@ +### Test Case Information +| TEST CASE ID | SCP-60| +| :--- | :--- | +| Owner of Test | Ty Hutchison| +| Test Name | Fix back-to-menu not working | +| Date of Last Revision | 11/29/2021 | +| Test Objective | Fix bug preventing back to menu option not working in options menu | + +### Procedure + +|Step | Action | Expected Result | Pass/Fail | +|:---:| :--- | :---- | :---: | +|1| Run the game| The game successfully opens |Pass| +|2| Enter Options Menu | The User will enter option menu|Pass| +|3| Use the back to main menu button at bottom | The User will exit the options screen using the back to menu feature at the bottom |Pass| +|4| Enter Main Menu Screen | The User will enter the Main Menu screen |Pass| + +### Test Completion +- **Tester**: Thomas Kwashnak +- **Date of Test**: 11/29/2021 3:57 PM +- **Test Result**: Passed \ No newline at end of file diff --git a/Team A2/Test Cases/Test Case SCP-62.md b/Team A2/Test Cases/Test Case SCP-62.md new file mode 100644 index 0000000..c906095 --- /dev/null +++ b/Team A2/Test Cases/Test Case SCP-62.md @@ -0,0 +1,23 @@ +### Test Case Information +| TEST CASE ID | SCP-46 | +| :--- | :--- | +| Owner of Test | Nicholas Tourony | +| Test Name | Passable Floating Platform Test | +| Date of Last Revision | 11/28/21 | +| Test Objective | Ensure that the passable floating platform is added to the game, functions properly, and is in the map editor. | + +### Procedure + +|Step | Action | Expected Result | Pass/Fail | +|:---:| :--- | :---- | :---: | +|1| Run the game| The game successfully opens | Pass | +|2| Press the spacebar to start the game.| The game starts and the character is loaded into the first level. | Pass | +|3| Navigate to the passable floating platform by going to the right and jumping over the tree. | The character is under the passable floating platform. | Pass | +|4| Jump onto the passable floating platform by jumping from directly below it. | The character passes through the platform and lands on top of it. | Pass | +|5| Close the game and launch the map editor | The map editor opens | Pass | +|6| Select the passable floating platform tile and click on the map to place it. | The passable floating platform is placed on the map. | Pass | + +### Test Completion +- **Tester**: Thomas Kwashnak +- **Date of Test**: 11/29/2021 10:14 AM +- **Test Result**: Passed \ No newline at end of file diff --git a/src/Enemies/BugEnemy.java b/src/Enemies/BugEnemy.java index 71ff785..a46ebc9 100644 --- a/src/Enemies/BugEnemy.java +++ b/src/Enemies/BugEnemy.java @@ -1,13 +1,13 @@ package Enemies; import Builders.FrameBuilder; +import Engine.GamePanel; import Engine.ImageLoader; import GameObject.Frame; import GameObject.ImageEffect; import GameObject.SpriteSheet; import Level.Enemy; import Level.Player; -import Utils.AirGroundState; import Utils.Direction; import Utils.Point; @@ -19,10 +19,14 @@ public class BugEnemy extends Enemy { private float gravity = .5f; - private float movementSpeed = .5f; + + // different speeds depending on the difficulty + private static final float NORMAL_SPEED = 0.5f, HARD_SPEED = 0.7f, HARDCORE_SPEED = 0.9f; + float movementSpeed = NORMAL_SPEED; + private Direction startFacingDirection; private Direction facingDirection; - private AirGroundState airGroundState; + private boolean isInAir; public BugEnemy(Point location, Direction facingDirection) { super(location.x, location.y, new SpriteSheet(ImageLoader.load("BugEnemy.png"), 24, 15), "WALK_LEFT"); @@ -39,7 +43,7 @@ public void initialize() { } else if (facingDirection == Direction.LEFT) { currentAnimationName = "WALK_LEFT"; } - airGroundState = AirGroundState.GROUND; + isInAir = false; } @Override @@ -47,11 +51,21 @@ public void update(Player player) { float moveAmountX = 0; float moveAmountY = 0; + // set the movement speed of the enemy depending on what difficulty it selected + if (GamePanel.getDifficulty() == 2) + { + movementSpeed = HARD_SPEED; + } + else if (GamePanel.getDifficulty() == 1) + { + movementSpeed = HARDCORE_SPEED; + } + // add gravity (if in air, this will cause bug to fall) moveAmountY += gravity; // if on ground, walk forward based on facing direction - if (airGroundState == AirGroundState.GROUND) { + if (!isInAir) { if (facingDirection == Direction.RIGHT) { moveAmountX += movementSpeed; } else { @@ -86,11 +100,7 @@ public void onEndCollisionCheckY(boolean hasCollided, Direction direction) { // if bug is colliding with the ground, change its air ground state to GROUND // if it is not colliding with the ground, it means that it's currently in the air, so its air ground state is changed to AIR if (direction == Direction.DOWN) { - if (hasCollided) { - airGroundState = AirGroundState.GROUND; - } else { - airGroundState = AirGroundState.AIR; - } + isInAir = !hasCollided; } } diff --git a/src/Enemies/CyborgEnemy.java b/src/Enemies/CyborgEnemy.java index 2ff25f7..395beed 100644 --- a/src/Enemies/CyborgEnemy.java +++ b/src/Enemies/CyborgEnemy.java @@ -1,6 +1,7 @@ package Enemies; import Builders.FrameBuilder; +import Engine.GamePanel; import Engine.ImageLoader; import GameObject.Frame; import GameObject.ImageEffect; @@ -8,13 +9,11 @@ import Level.Enemy; import Level.Player; import Projectiles.LazerBeam; -import Utils.AirGroundState; import Utils.Direction; import Utils.Point; import Utils.Stopwatch; import java.util.HashMap; -import java.util.Timer; // This class is for the cyborg // It walks back and forth between two set points (startLocation and endLocation) @@ -29,10 +28,14 @@ public class CyborgEnemy extends Enemy { // Whether or not the game is waiting for LazerBeams to launch private boolean wait = false; - protected float movementSpeed = 0.5f; + // different speeds depending on the difficulty + private static final float NORMAL_SPEED = 0.5f, HARD_SPEED = 0.7f, HARDCORE_SPEED = 0.9f; + float movementSpeed = NORMAL_SPEED; + private static final float NORMAL_LAZER_SPEED = 1.5f, HARD_LAZER_SPEED = 1.7f, HARDCORE_LAZER_SPEED = 1.9f; + private Direction startFacingDirection; protected Direction facingDirection; - protected AirGroundState airGroundState; + protected boolean isInAir; // timer is used to determine when a lazer is to be shot out protected Stopwatch shootTimer = new Stopwatch(); @@ -62,7 +65,7 @@ public void initialize() { } else if (facingDirection == Direction.LEFT) { currentAnimationName = "WALK_LEFT"; } - airGroundState = AirGroundState.GROUND; + isInAir = false; // every 2 seconds, the lazer will be shot out shootTimer.setWaitTime(2000); @@ -72,7 +75,20 @@ public void initialize() { public void update(Player player) { float startBound = startLocation.x; float endBound = endLocation.x; + float lazerMovementSpeed = NORMAL_LAZER_SPEED; + // set the movement speed of the enemy and lazer attack depending on what difficulty it selected + if (GamePanel.getDifficulty() == 2) + { + movementSpeed = HARD_SPEED; + lazerMovementSpeed = HARD_LAZER_SPEED; + } + else if (GamePanel.getDifficulty() == 1) + { + movementSpeed = HARDCORE_SPEED; + lazerMovementSpeed = HARDCORE_LAZER_SPEED; + } + // if shoot timer is up and cyborg is not currently shooting, set its state to SHOOT if (shootTimer.isTimeUp() && cyborgState != cyborgState.SHOOT) { cyborgState = cyborgState.SHOOT; @@ -114,16 +130,14 @@ public void update(Player player) { // define where LazerBeam will spawn on map (x location) relative to cyborg enemy's location // and define its movement speed int LazerBeamX; - float movementSpeed; LazerBeamX = Math.round(getX()); - movementSpeed = 1.5f; // define where LazerBeam will spawn on the map (y location) relative to cyborg enemy's location int LazerBeamY = Math.round(getY() + 15); // create LazerBeam enemy - LazerBeam LazerBeam = new LazerBeam(new Point(LazerBeamX + 20, LazerBeamY), movementSpeed, 1000); - LazerBeam LazerBeam2 = new LazerBeam(new Point(LazerBeamX - 10, LazerBeamY), -movementSpeed, 1000); + LazerBeam LazerBeam = new LazerBeam(new Point(LazerBeamX + 20, LazerBeamY), lazerMovementSpeed, 1000); + LazerBeam LazerBeam2 = new LazerBeam(new Point(LazerBeamX - 10, LazerBeamY), -lazerMovementSpeed, 1000); // add LazerBeam enemy to the map for it to offically spawn in the level map.addProjectile(LazerBeam); diff --git a/src/Enemies/DinosaurEnemy.java b/src/Enemies/DinosaurEnemy.java index 3921528..5994cb0 100644 --- a/src/Enemies/DinosaurEnemy.java +++ b/src/Enemies/DinosaurEnemy.java @@ -1,6 +1,7 @@ package Enemies; import Builders.FrameBuilder; +import Engine.GamePanel; import Engine.ImageLoader; import GameObject.Frame; import GameObject.ImageEffect; @@ -8,7 +9,6 @@ import Level.Enemy; import Level.Player; import Projectiles.Fireball; -import Utils.AirGroundState; import Utils.Direction; import Utils.Point; import Utils.Stopwatch; @@ -25,10 +25,14 @@ public class DinosaurEnemy extends Enemy { protected Point startLocation; protected Point endLocation; - protected float movementSpeed = 1f; + // different speeds depending on the difficulty + private static final float NORMAL_SPEED = 1f, HARD_SPEED = 1.2f, HARDCORE_SPEED = 1.4f; + float movementSpeed = NORMAL_SPEED; + + private static final float NORMAL_FIREBALL_SPEED = 1.5f, HARD_FIREBALL_SPEED = 1.7f, HARDCORE_FIREBALL_SPEED = 1.9f; private Direction startFacingDirection; protected Direction facingDirection; - protected AirGroundState airGroundState; + protected boolean isInAir; // timer is used to determine when a fireball is to be shot out protected Stopwatch shootTimer = new Stopwatch(); @@ -56,7 +60,7 @@ public void initialize() { } else if (facingDirection == Direction.LEFT) { currentAnimationName = "WALK_LEFT"; } - airGroundState = AirGroundState.GROUND; + isInAir = false; // every 2 seconds, the fireball will be shot out shootTimer.setWaitTime(2000); @@ -66,7 +70,19 @@ public void initialize() { public void update(Player player) { float startBound = startLocation.x; float endBound = endLocation.x; + float fireballMovementSpeed = NORMAL_FIREBALL_SPEED; + // set the movement speed of the enemy and fireball attack depending on what difficulty it selected + if (GamePanel.getDifficulty() == 2) + { + movementSpeed = HARD_SPEED; + fireballMovementSpeed = HARD_FIREBALL_SPEED; + } + else if (GamePanel.getDifficulty() == 1) + { + movementSpeed = HARDCORE_SPEED; + fireballMovementSpeed = HARDCORE_FIREBALL_SPEED; + } // if shoot timer is up and dinosaur is not currently shooting, set its state to SHOOT if (shootTimer.isTimeUp() && dinosaurState != DinosaurState.SHOOT) { dinosaurState = DinosaurState.SHOOT; @@ -108,20 +124,18 @@ public void update(Player player) { // define where fireball will spawn on map (x location) relative to dinosaur enemy's location // and define its movement speed int fireballX; - float movementSpeed; if (facingDirection == Direction.RIGHT) { fireballX = Math.round(getX()) + getScaledWidth(); - movementSpeed = 1.5f; } else { fireballX = Math.round(getX()); - movementSpeed = -1.5f; + fireballMovementSpeed = -fireballMovementSpeed; } // define where fireball will spawn on the map (y location) relative to dinosaur enemy's location int fireballY = Math.round(getY()) + 4; // create Fireball enemy - Fireball fireball = new Fireball(new Point(fireballX, fireballY), movementSpeed, 1000); + Fireball fireball = new Fireball(new Point(fireballX, fireballY), fireballMovementSpeed, 1000); // add fireball enemy to the map for it to offically spawn in the level map.addProjectile(fireball); diff --git a/src/Enemies/Dog.java b/src/Enemies/Dog.java index ab43a19..6679362 100644 --- a/src/Enemies/Dog.java +++ b/src/Enemies/Dog.java @@ -1,13 +1,13 @@ package Enemies; import Builders.FrameBuilder; +import Engine.GamePanel; import Engine.ImageLoader; import GameObject.Frame; import GameObject.SpriteSheet; import Level.Enemy; import Level.Player; import Projectiles.Bone; -import Utils.AirGroundState; import Utils.Direction; import Utils.Point; import Utils.Stopwatch; @@ -24,10 +24,14 @@ public class Dog extends Enemy { protected Point startLocation; protected Point endLocation; - protected float movementSpeed = 1f; + // different speeds depending on the difficulty + private static final float NORMAL_SPEED = 1f, HARD_SPEED = 1.2f, HARDCORE_SPEED = 1.4f; + float movementSpeed = NORMAL_SPEED; + + private static final float NORMAL_BONE_SPEED = 1.5f, HARD_BONE_SPEED = 1.7f, HARDCORE_BONE_SPEED = 1.9f; private Direction startFacingDirection; protected Direction facingDirection; - protected AirGroundState airGroundState; + protected boolean isInAir; // timer is used to determine when a bone is to be shot out protected Stopwatch shootTimer = new Stopwatch(); @@ -55,8 +59,7 @@ public void initialize() { } else if (facingDirection == Direction.LEFT) { currentAnimationName = "WALK_LEFT"; } - airGroundState = AirGroundState.GROUND; - + isInAir = false; // every 2 seconds, the bone will be shot out shootTimer.setWaitTime(2000); } @@ -65,6 +68,19 @@ public void initialize() { public void update(Player player) { float startBound = startLocation.x; float endBound = endLocation.x; + float boneMovementSpeed = NORMAL_BONE_SPEED; + + // set the movement speed of the enemy and fireball attack depending on what difficulty it selected + if (GamePanel.getDifficulty() == 2) + { + movementSpeed = HARD_SPEED; + boneMovementSpeed = HARD_BONE_SPEED; + } + else if (GamePanel.getDifficulty() == 1) + { + movementSpeed = HARDCORE_SPEED; + boneMovementSpeed = HARDCORE_BONE_SPEED; + } // if shoot timer is up and dog is not currently shooting, set its state to SHOOT if (shootTimer.isTimeUp() && dogState != dogState.SHOOT) { @@ -100,27 +116,25 @@ public void update(Player player) { // then the bone is actually shot out } else if (dogState == dogState.SHOOT) { if (previousdogState == dogState.WALK) { - shootTimer.setWaitTime(1000); + shootTimer.setWaitTime(500); currentAnimationName = facingDirection == Direction.RIGHT ? "SHOOT_RIGHT" : "SHOOT_LEFT"; } else if (shootTimer.isTimeUp()) { // define where bone will spawn on map (x location) relative to dog enemy's location // and define its movement speed int boneX; - float movementSpeed; if (facingDirection == Direction.RIGHT) { boneX = Math.round(getX()) + getScaledWidth(); - movementSpeed = 1.5f; } else { boneX = Math.round(getX()); - movementSpeed = -1.5f; + boneMovementSpeed = -boneMovementSpeed; } // define where bone will spawn on the map (y location) relative to dog enemy's location int boneY = Math.round(getY()) + 4; // create bone enemy - Bone bone = new Bone(new Point(boneX, boneY), movementSpeed, 2000); + Bone bone = new Bone(new Point(boneX, boneY), boneMovementSpeed, 2000); // add bone enemy to the map for it to offically spawn in the level map.addProjectile(bone); @@ -153,44 +167,44 @@ public HashMap getAnimations(SpriteSheet spriteSheet) { put("WALK_LEFT", new Frame[]{ new FrameBuilder(spriteSheet.getSprite(0, 0), 200) .withScale(2) - .withBounds(0, 0, 30, 13) + .withBounds(-5, 0, 35, 13) .build(), new FrameBuilder(spriteSheet.getSprite(0, 1), 200) .withScale(2) - .withBounds(0, 0, 30, 13) + .withBounds(-5, 0, 35, 13) .build(), new FrameBuilder(spriteSheet.getSprite(0, 2), 200) .withScale(2) - .withBounds(0, 0, 30, 13) + .withBounds(-5, 0, 35, 13) .build() }); put("WALK_RIGHT", new Frame[]{ new FrameBuilder(spriteSheet.getSprite(1, 0), 200) .withScale(2) - .withBounds(0, 0, 30, 13) + .withBounds(0, 0, 35, 13) .build(), new FrameBuilder(spriteSheet.getSprite(1, 1), 200) .withScale(2) - .withBounds(0, 0, 30, 13) + .withBounds(0, 0, 35, 13) .build(), new FrameBuilder(spriteSheet.getSprite(1, 2), 200) .withScale(2) - .withBounds(0, 0, 30, 13) + .withBounds(0, 0, 35, 13) .build() }); put("SHOOT_LEFT", new Frame[]{ new FrameBuilder(spriteSheet.getSprite(0, 2), 0) .withScale(2) - .withBounds(30, 0, 30, 13) + .withBounds(-5, 0, 35, 13) .build(), }); put("SHOOT_RIGHT", new Frame[]{ new FrameBuilder(spriteSheet.getSprite(1, 2), 0) .withScale(2) - .withBounds(0, 0, 30, 13) + .withBounds(0, 0, 35, 13) .build(), }); }}; diff --git a/src/Engine/Collidable.java b/src/Engine/Collidable.java new file mode 100644 index 0000000..d9623e1 --- /dev/null +++ b/src/Engine/Collidable.java @@ -0,0 +1,20 @@ +package Engine; + +import GameObject.IntersectableRectangle; + +/** + * Indicates that an object is able to collide + * @author Thomas Kwashnak + */ +public interface Collidable extends IntersectableRectangle { + boolean intersects(Collidable other); + boolean overlaps(Collidable other); + + interface InstantDeath extends Collidable {} + interface Damage extends Collidable { + int getDamage(); + } + interface PreventJump extends Collidable { + int getJumpDelay(); + } +} diff --git a/src/Engine/DifficultyHolder.java b/src/Engine/DifficultyHolder.java new file mode 100644 index 0000000..d2263b8 --- /dev/null +++ b/src/Engine/DifficultyHolder.java @@ -0,0 +1,21 @@ +package Engine; + +@Deprecated +public class DifficultyHolder + +{ + private int difficulty = 3; + public DifficultyHolder(int difficulty) + { + this.difficulty = difficulty; + } + + public int getDifficulty() + { + return difficulty; + } + public void setDifficulty(int difficulty) + { + this.difficulty = difficulty; + } +} diff --git a/src/Engine/GamePanel.java b/src/Engine/GamePanel.java index 313e7a1..f130743 100644 --- a/src/Engine/GamePanel.java +++ b/src/Engine/GamePanel.java @@ -1,55 +1,40 @@ package Engine; +import Game.GameState; +import Game.GameThread; +import Game.ScreenCoordinator; import GameObject.Rectangle; import Level.Player; -import Players.Avatar; -import Players.Cat; -import Screens.OptionsScreen; -import SpriteFont.SpriteFont; import Utils.Colors; -import Utils.Stopwatch; -import javax.imageio.ImageIO; import javax.sound.sampled.*; -//import sun.audio.AudioData; import javax.swing.*; - -import Game.GameState; -import Game.ScreenCoordinator; - import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.awt.event.MouseEvent; -import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; -/* +/** * This is where the game loop starts * The JPanel uses a timer to continually call cycles of update and draw */ -public class GamePanel extends JPanel { - // loads Screens on to the JPanel - // each screen has its own update and draw methods defined to handle a "section" - // of the game. - protected KeyLocker keyLocker = new KeyLocker(); - private ScreenManager screenManager; - // used to create the game loop and cycle between update and draw calls - private Timer timer; - // used to draw graphics to the panel - private GraphicsHandler graphicsHandler; +public class GamePanel extends JPanel implements Updatable { + private final ScreenManager screenManager; + private final GraphicsHandler graphicsHandler; private boolean doPaint = false; - protected int pointerLocationX, pointerLocationY; - protected Stopwatch keyTimer = new Stopwatch(); protected static GameWindow gameWindow; private static ScreenCoordinator coordinator; public static Clip clip; - private Point previousMousePoint = new Point(0,0); - private JLabel health; + + // these difficulty values are not only just used for the the logic of the game but + // to determine how much health the user gets at each difficulty; + private final static int NORMAL = 3, HARD = 2, HARDCORE = 1; + private static int difficulty; + private final JLabel health; + private final GameThread gameThread; - /* + /** * The JPanel and various important class instances are setup here */ public GamePanel(ScreenCoordinator c1,GameWindow gameWindow) { @@ -62,33 +47,31 @@ public GamePanel(ScreenCoordinator c1,GameWindow gameWindow) { this.setSize(Config.WIDTH, Config.HEIGHT); // attaches Keyboard class's keyListener to this JPanel - this.addKeyListener(Keyboard.getKeyListener()); + this.addKeyListener(new Keyboard()); graphicsHandler = new GraphicsHandler(); screenManager = new ScreenManager(); coordinator = c1; - - + difficulty = NORMAL; - - timer = new Timer(1000 / Config.FPS, new ActionListener() { - public void actionPerformed(ActionEvent e) { - update(); - changeHealth(); - if(coordinator.getGameState() == GameState.LEVEL) { - health.show(); - } - else { - health.hide(); - } - repaint(); - } - }); - timer.setRepeats(true); + gameThread = new GameThread(this::repaint, this::update); + } + + public static void setDifficulty(int newDifficulty) + { + difficulty = newDifficulty; + } + public static int getDifficulty() + { + return difficulty; } + public static String getDifficultyString() { + return difficulty == 3 ? "Normal" : difficulty == 2 ? "Hard" : "Hardcore"; + } + public static ScreenCoordinator getScreenCoordinator() { return coordinator; } @@ -99,8 +82,8 @@ public void setupGame() { setBackground(Colors.CORNFLOWER_BLUE); screenManager.initialize(new Rectangle(getX(), getY(), getWidth(), getHeight())); doPaint = true; - } + public static GameWindow getGameWindow() { return gameWindow; } @@ -148,7 +131,7 @@ public static void setVolumeHigh() { // this starts the timer (the game loop is started here public void startGame() { - timer.start(); + gameThread.start(); try { music("Resources/Music/music.wav",.05); @@ -166,27 +149,26 @@ public ScreenManager getScreenManager() { } public void update() { - screenManager.update(); + screenManager.update(); + changeHealth(); } public void draw() { screenManager.draw(graphicsHandler); - - } // Checks the players health and accordingly changes to the image with the corresponding number of hearts public void changeHealth() { if(coordinator.getGameState() == GameState.LEVEL) { - if(Player.playerHealth == 3) { + if(Player.PLAYER_HEALTH == 3) { health.setIcon(new ImageIcon(ImageLoader.load("3 Hearts.png"))); } - else if(Player.playerHealth == 2) { + else if(Player.PLAYER_HEALTH == 2) { health.setIcon(new ImageIcon(ImageLoader.load("2 Hearts.png"))); } - else if(Player.playerHealth == 1) { + else if(Player.PLAYER_HEALTH == 1) { health.setIcon(new ImageIcon(ImageLoader.load("1 Heart.png"))); } @@ -195,8 +177,17 @@ else if(Player.playerHealth == 1) { } } + // Each difficulty is represented as an integer while also representing the amount of health the user has + // normal is 3 hard is 2 and hardcore is 1 + // hide the health whenever in a menu if(coordinator.getGameState() == GameState.MENU) { - Player.playerHealth = 3; + Player.PLAYER_HEALTH = difficulty; + health.hide(); + } + // show the health when in a level + else if (coordinator.getGameState() == GameState.LEVEL) + { + health.show(); } } @@ -213,7 +204,6 @@ protected void paintComponent(Graphics g) { } public static void mouseClicked(MouseEvent e) { -// System.out.println("Click: " + e.getPoint()); coordinator.mouseClicked(e); } diff --git a/src/Engine/GameWindow.java b/src/Engine/GameWindow.java index 0f12d10..e5f01d0 100644 --- a/src/Engine/GameWindow.java +++ b/src/Engine/GameWindow.java @@ -1,13 +1,12 @@ package Engine; +import Game.ScreenCoordinator; + +import javax.swing.*; import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import javax.swing.*; - -import Game.ScreenCoordinator; - /* * The JFrame that holds the GamePanel * Just does some setup and exposes the gamePanel's screenManager to allow an external class to setup their own content and attach it to this engine. @@ -26,7 +25,6 @@ public void mouseClicked(MouseEvent e) { GamePanel.mouseClicked(e); } }); - gamePanel = new GamePanel(c1,this); gamePanel.setFocusable(true); gamePanel.requestFocusInWindow(); diff --git a/src/Engine/Key.java b/src/Engine/Key.java index 6f69570..ad435ec 100644 --- a/src/Engine/Key.java +++ b/src/Engine/Key.java @@ -45,7 +45,8 @@ public enum Key { NINE(57), ZERO(48), SPACE(32), - ESC(27); + ESC(27), + CTRL(17); private int keyCode; Key(int keyCode) { diff --git a/src/Engine/KeyLocker.java b/src/Engine/KeyLocker.java index 5077672..95e1b88 100644 --- a/src/Engine/KeyLocker.java +++ b/src/Engine/KeyLocker.java @@ -31,6 +31,10 @@ public boolean isActionLocked(KeyboardAction keyboardAction) { return lockedAdapters.contains(keyboardAction); } + public boolean isActionUnlocked(KeyboardAction keyboardAction) { + return !lockedAdapters.contains(keyboardAction); + } + public void clear() { lockedAdapters.clear(); } diff --git a/src/Engine/Keyboard.java b/src/Engine/Keyboard.java index 24a3971..f5fc5f6 100644 --- a/src/Engine/Keyboard.java +++ b/src/Engine/Keyboard.java @@ -10,46 +10,33 @@ * This class is used throughout the engine for detecting keyboard state * This includes if a key is pressed, if a key is not pressed, and if multiple keys are pressed/not pressed at the same time */ -public class Keyboard { +public class Keyboard extends KeyAdapter { - // hashmaps keep track of if a key is currently down or up private static final Set keysDown; - private static final KeyListener keyListener; - static { - keyListener = new KeyAdapter() { - - @Override - public void keyPressed(KeyEvent e) { - keysDown.add(e.getKeyCode()); - } - - @Override - public void keyReleased(KeyEvent e) { - keysDown.remove(e.getKeyCode()); - } - }; - keysDown = new HashSet<>(); } - // prevents Keyboard from being instantiated -- it's my way of making a "static" class like C# has - private Keyboard() { } - - public static KeyListener getKeyListener() { - return keyListener; - } + @Override + public void keyPressed(KeyEvent keyEvent) { + keysDown.add(keyEvent.getKeyCode()); + } - // returns if a key is currently being pressed - public static boolean isKeyDown(Key key) { - return keysDown.contains(key.getKeyCode()); - } + @Override + public void keyReleased(KeyEvent keyEvent) { + keysDown.remove(keyEvent.getKeyCode()); + } + + // returns if a key is currently being pressed + public static boolean isKeyDown(Key key) { + return keysDown.contains(key.getKeyCode()); + } - // returns if a key is currently not being pressed - public static boolean isKeyUp(Key key) { - return !keysDown.contains(key.getKeyCode()); - } + // returns if a key is currently not being pressed + public static boolean isKeyUp(Key key) { + return !keysDown.contains(key.getKeyCode()); + } /** * Returns if one of the keys is down @@ -64,4 +51,33 @@ public static boolean isKeyDown(int... keyCode) { } return false; } + // +// // hashmaps keep track of if a key is currently down or up +// private static final Set keysDown; +// +// private static final KeyListener keyListener; +// +// static { +// keyListener = new KeyAdapter() { +// +// @Override +// public void keyPressed(KeyEvent e) { +// keysDown.add(e.getKeyCode()); +// System.out.println(e.getKeyCode()); +// } +// +// @Override +// public void keyReleased(KeyEvent e) { +// keysDown.remove(e.getKeyCode()); +// } +// }; +// +// keysDown = new HashSet<>(); +// } +// +// // prevents Keyboard from being instantiated -- it's my way of making a "static" class like C# has +// private Keyboard() { } +// + + } diff --git a/src/Engine/KeyboardAction.java b/src/Engine/KeyboardAction.java index f034845..4b12c43 100644 --- a/src/Engine/KeyboardAction.java +++ b/src/Engine/KeyboardAction.java @@ -16,7 +16,7 @@ public enum KeyboardAction { GAME_JUMP(Key.W,Key.SPACE,Key.UP), GAME_INSTRUCTIONS(Key.X), GAME_INTERACT(Key.SPACE), - GAME_CROUCH(Key.S,Key.DOWN), + GAME_CROUCH(Key.S,Key.DOWN,Key.CTRL), GAME_ATTACK(Key.E), GAME_SPRINT(Key.SHIFT), GAME_RESPAWN(Key.SPACE); diff --git a/src/Engine/Pausable.java b/src/Engine/Pausable.java new file mode 100644 index 0000000..a09e8c3 --- /dev/null +++ b/src/Engine/Pausable.java @@ -0,0 +1,6 @@ +package Engine; + +public interface Pausable { + void resume(); + void pause(); +} diff --git a/src/Engine/Screen.java b/src/Engine/Screen.java index e29bc74..9c721b2 100644 --- a/src/Engine/Screen.java +++ b/src/Engine/Screen.java @@ -1,15 +1,17 @@ package Engine; -import java.awt.Graphics; import Level.Map; + import java.awt.event.MouseEvent; // Base Screen class // This game engine runs off the idea of "screens", which are classes that contain their own update/draw methods for a particular piece of the game // For example, there may be a "MenuScreen" or a "PlayGameScreen" -public abstract class Screen { +public abstract class Screen implements Updatable, Drawable { public abstract void initialize(); + @Override public abstract void update(); + @Override public abstract void draw(GraphicsHandler graphicsHandler); public abstract void mouseClicked(MouseEvent e); private Map background; diff --git a/src/Engine/ScreenManager.java b/src/Engine/ScreenManager.java index ee6ea2a..142edd6 100644 --- a/src/Engine/ScreenManager.java +++ b/src/Engine/ScreenManager.java @@ -1,8 +1,6 @@ package Engine; -import Game.GameState; - import GameObject.Rectangle; /* diff --git a/src/Engine/Updatable.java b/src/Engine/Updatable.java new file mode 100644 index 0000000..8d186ea --- /dev/null +++ b/src/Engine/Updatable.java @@ -0,0 +1,5 @@ +package Engine; + +public interface Updatable { + void update(); +} diff --git a/src/EnhancedMapTiles/HorizontalMovingPlatform.java b/src/EnhancedMapTiles/HorizontalMovingPlatform.java index 7fc36b6..b290500 100644 --- a/src/EnhancedMapTiles/HorizontalMovingPlatform.java +++ b/src/EnhancedMapTiles/HorizontalMovingPlatform.java @@ -6,7 +6,6 @@ import Level.EnhancedMapTile; import Level.Player; import Level.TileType; -import Utils.AirGroundState; import Utils.Direction; import Utils.Point; @@ -78,7 +77,7 @@ public void update(Player player) { // if player is on standing on top of platform, move player by the amount the platform is moving // this will cause the player to "ride" with the moving platform // without this code, the platform would slide right out from under the player - if (overlaps(player) && player.getScaledBoundsY2() == getScaledBoundsY1() && player.getAirGroundState() == AirGroundState.GROUND) { + if (overlaps(player) && player.getScaledBoundsY2() == getScaledBoundsY1() && !player.isInAir()) { player.moveXHandleCollision(moveAmountX); } diff --git a/src/Game/Game.java b/src/Game/Game.java index ca2a96f..81c6b32 100644 --- a/src/Game/Game.java +++ b/src/Game/Game.java @@ -1,9 +1,8 @@ package Game; import Engine.GameWindow; - import Engine.ScreenManager; -import Menu.DebugScreen; +import Maps.GameMaps; /* * The game starts here @@ -13,13 +12,14 @@ public class Game { public static void main(String[] args) { + //Loads all maps before anything else happens + new GameMaps(); new Game(); } public Game() { ScreenCoordinator c1 = new ScreenCoordinator(); GameWindow gameWindow = new GameWindow(c1); - gameWindow.startGame(); ScreenManager screenManager = gameWindow.getScreenManager(); screenManager.setCurrentScreen(c1); diff --git a/src/Game/GameState.java b/src/Game/GameState.java index 89b3b6b..216681b 100644 --- a/src/Game/GameState.java +++ b/src/Game/GameState.java @@ -4,5 +4,5 @@ * This is used by the ScreenCoordinator class to determine which "state" the game is currently in */ public enum GameState { - MENU, LEVEL, LEVELSELECT, CREDITS, INSTRUCTIONS , OPENING, OPTIONS; + MENU, LEVEL, LEVELSELECT, CREDITS, INSTRUCTIONS , OPENING, OPTIONS, DIFFICULTYSELECT; } diff --git a/src/Game/GameThread.java b/src/Game/GameThread.java new file mode 100644 index 0000000..f4d9452 --- /dev/null +++ b/src/Game/GameThread.java @@ -0,0 +1,79 @@ +package Game; + +/** + * Runs the base loops of the game, including updates and renders. Multiple updates may happen between frames if it takes too long, meaning the + * game will catch up once it gets too far ahead. + * @author Thomas Kwashnak + */ +public class GameThread implements Runnable { + + /** + * Static number of ms between updates + */ + private static final long UPDATE_FIXED_MS = 12; + + /** + * Main thread to run the game on + */ + private final Thread thread; + /** + * Methods to run for rendering and updating + */ + private final Runnable render, update; + /** + * Provides a means of ending the loop manually + */ + private boolean running = true; + + + /** + * Creates a new game thread + * @param render Runnable called when a render is needed + * @param update Runnable called when an update is needed + */ + public GameThread(Runnable render, Runnable update) { + thread = new Thread(this); + this.render = render; + this.update = update; + } + + /** + * Starts the thread + */ + public void start() { + thread.start(); + } + + /** + * Stops the thread + */ + public void stop() { + running = false; + } + + @Override + public void run() { + //Sets the next update to the current time + long nextUpdateTime = System.currentTimeMillis(); + long currentTime; + + //Loop until running is false + while(running) { + //Update current time + currentTime = System.currentTimeMillis(); + + //Repeat until the update time is after (in the future) current time + if(currentTime > nextUpdateTime) { + do { + update.run(); + nextUpdateTime += UPDATE_FIXED_MS; + } while(currentTime > nextUpdateTime); + //Run a render + render.run(); + } + } + //Reset running once it's set to false + running = true; + } +} + diff --git a/src/Game/ScreenCoordinator.java b/src/Game/ScreenCoordinator.java index 299ef71..43c71cf 100644 --- a/src/Game/ScreenCoordinator.java +++ b/src/Game/ScreenCoordinator.java @@ -58,6 +58,7 @@ public void update() { case LEVELSELECT -> currentScreen = new LevelSelectScreen(); case OPENING -> currentScreen = new OpeningScreen(this); case OPTIONS -> currentScreen = new OptionsScreen(); + case DIFFICULTYSELECT -> currentScreen = new DifficultySelectScreen(); } currentScreen.initialize(); } diff --git a/src/Game/TimeTracker.java b/src/Game/TimeTracker.java new file mode 100644 index 0000000..9bde3ef --- /dev/null +++ b/src/Game/TimeTracker.java @@ -0,0 +1,108 @@ +package Game; + +import Engine.Drawable; +import Engine.GraphicsHandler; +import Engine.ScreenManager; +import Maps.GameMaps; +import Utils.GameTimer; +import Utils.TimeParser; + +import java.awt.*; + +public class TimeTracker implements Drawable { + + private static final Font FONT_BIG, FONT_SMALL; + + static { + String fontName = "Times New Roman"; + FONT_BIG = new Font(fontName,Font.PLAIN, 40); + FONT_SMALL = new Font(fontName,Font.PLAIN,30); + } + + + private int currentLevel; + + private GameTimer[] levels; + private GameTimer total; + + private int xMap, yMap, xTotal, yTotal, charsTotal, charsMap; + + /** + * Whether the level-specific time should be listed + */ + private boolean showLevel; + + public TimeTracker() { + total = new GameTimer(); + levels = new GameTimer[GameMaps.MAPS.length]; + + for(int i = 0; i < levels.length; i++) { + levels[i] = new GameTimer(); + } + + currentLevel = -1; + showLevel = false; + } + + public void setCurrentLevel(int currentLevel) { + if(this.currentLevel != currentLevel) { + total.start(); + if(this.currentLevel > -1) { + levels[this.currentLevel].stop(); + showLevel = true; + } + this.currentLevel = currentLevel; + levels[currentLevel].start(); + } + } + + /** + * Usable when dealing with pausing + */ + public void start() { + total.start(); + levels[currentLevel].start(); + } + + public void stop() { + total.stop(); + levels[currentLevel].stop(); + } + + public TimeParser getElapsedTime() { + TimeParser timeParser = new TimeParser(); + for(GameTimer gameTimer : levels) { + timeParser.addTime(gameTimer.getElapsed()); + } + return timeParser; + } + + @Override + public void draw(GraphicsHandler graphicsHandler) { + String totalString = total.toString(); + if(totalString.length() != charsTotal) { + FontMetrics metrics = graphicsHandler.getGraphics2D().getFontMetrics(FONT_BIG); + yTotal = metrics.getHeight(); + xTotal = ScreenManager.getScreenWidth() - metrics.stringWidth(totalString.replace(' ','0')); + charsTotal = totalString.length(); + } + graphicsHandler.drawString(totalString, xTotal, yTotal, FONT_BIG, Color.white); + + + if(showLevel) { + String mapString = levels[currentLevel].toString(); + if(mapString.length() != charsMap) { + FontMetrics metrics = graphicsHandler.getGraphics2D().getFontMetrics(FONT_SMALL); + yMap = metrics.getHeight() + yTotal; + xMap = ScreenManager.getScreenWidth() - metrics.stringWidth(mapString.replace(' ','0')); + charsMap = mapString.length(); + } + graphicsHandler.drawString(mapString,xMap,yMap,FONT_SMALL,Color.white); + } + + } + + public GameTimer[] getLevels() { + return levels; + } +} diff --git a/src/GameObject/AnimatedSprite.java b/src/GameObject/AnimatedSprite.java index 10ff80a..88b5324 100644 --- a/src/GameObject/AnimatedSprite.java +++ b/src/GameObject/AnimatedSprite.java @@ -1,5 +1,6 @@ package GameObject; +import Engine.Collidable; import Engine.GraphicsHandler; import Utils.Stopwatch; @@ -14,7 +15,7 @@ Subclasses need to call down to this class's update method in order for animation logic to be performed While this calls does not extend from Sprite, it is set up in a way where it is still treated by other classes as if it is a singular sprite (based on value of currentFrame) */ -public class AnimatedSprite implements IntersectableRectangle { +public class AnimatedSprite implements Collidable { // location of entity protected float x, y; @@ -262,11 +263,13 @@ public Rectangle getIntersectRectangle() { return currentFrame.getIntersectRectangle(); } - public boolean intersects(IntersectableRectangle other) { + public boolean intersects(Collidable other) { return currentFrame.intersects(other); } - public boolean overlaps(IntersectableRectangle other) { return currentFrame.overlaps(other); } + public boolean overlaps(Collidable other) { + return currentFrame.overlaps(other); + } @Override public String toString() { diff --git a/src/GameObject/GameObject.java b/src/GameObject/GameObject.java index 1cf6e76..d1c8bf8 100644 --- a/src/GameObject/GameObject.java +++ b/src/GameObject/GameObject.java @@ -34,6 +34,8 @@ public class GameObject extends AnimatedSprite implements Drawable { // previous location the game object was in from the last frame protected float previousX, previousY; + + // the map instance this game object "belongs" to. protected Map map; diff --git a/src/GameObject/Rectangle.java b/src/GameObject/Rectangle.java index bf9dc34..77d1f82 100644 --- a/src/GameObject/Rectangle.java +++ b/src/GameObject/Rectangle.java @@ -1,5 +1,6 @@ package GameObject; +import Engine.Collidable; import Engine.GraphicsHandler; import java.awt.*; @@ -7,7 +8,7 @@ // This class represents a rectangle, which at its core is (x, y, width, height) // it has some properties, rectangle math methods, and draw logic // the methods here are pretty self explanatory -public class Rectangle implements IntersectableRectangle { +public class Rectangle implements Collidable { protected float x; protected float y; protected int width; @@ -174,7 +175,7 @@ public Rectangle getIntersectRectangle() { } // check if this intersects with another rectangle - public boolean intersects(IntersectableRectangle other) { + public boolean intersects(Collidable other) { Rectangle intersectRectangle = getIntersectRectangle(); Rectangle otherIntersectRectangle = other.getIntersectRectangle(); return Math.round(intersectRectangle.getX1()) < Math.round(otherIntersectRectangle.getX2()) && Math.round(intersectRectangle.getX2()) > Math.round(otherIntersectRectangle.getX1()) && @@ -182,10 +183,11 @@ public boolean intersects(IntersectableRectangle other) { } // check if this overlaps with another rectangle - public boolean overlaps(IntersectableRectangle other) { + public boolean overlaps(Collidable other) { Rectangle intersectRectangle = getIntersectRectangle(); Rectangle otherIntersectRectangle = other.getIntersectRectangle(); return Math.round(intersectRectangle.getX1()) <= Math.round(otherIntersectRectangle.getX2()) && Math.round(intersectRectangle.getX2()) >= Math.round(otherIntersectRectangle.getX1()) && Math.round(intersectRectangle.getY1()) <= Math.round(otherIntersectRectangle.getY2()) && Math.round(intersectRectangle.getY2()) >= Math.round(otherIntersectRectangle.getY1()); } + } diff --git a/src/Level/Camera.java b/src/Level/Camera.java index a2902cf..ffeab9e 100644 --- a/src/Level/Camera.java +++ b/src/Level/Camera.java @@ -1,6 +1,5 @@ package Level; -import Engine.Config; import Engine.GraphicsHandler; import Engine.ScreenManager; import GameObject.GameObject; diff --git a/src/Level/Enemy.java b/src/Level/Enemy.java index 4ddf6e9..1a47320 100644 --- a/src/Level/Enemy.java +++ b/src/Level/Enemy.java @@ -1,5 +1,6 @@ package Level; +import Engine.Collidable; import GameObject.Frame; import GameObject.ImageEffect; import GameObject.Rectangle; @@ -9,7 +10,7 @@ import java.util.HashMap; // This class is a base class for all enemies in the game -- all enemies should extend from it -public class Enemy extends MapEntity { +public class Enemy extends MapEntity implements Collidable.InstantDeath { public Enemy(float x, float y, SpriteSheet spriteSheet, String startingAnimation) { super(x, y, spriteSheet, startingAnimation); diff --git a/src/Level/LevelState.java b/src/Level/LevelState.java index ab711b7..775a9ce 100644 --- a/src/Level/LevelState.java +++ b/src/Level/LevelState.java @@ -2,5 +2,5 @@ // This enum represents the potential states a level can be public enum LevelState { - RUNNING, LEVEL_COMPLETED, PLAYER_DEAD, LEVEL_WIN_MESSAGE, LEVEL_LOSE_MESSAGE + PLAYING,DEAD,WIN } diff --git a/src/Level/Map.java b/src/Level/Map.java index 69cbfae..3365f8b 100644 --- a/src/Level/Map.java +++ b/src/Level/Map.java @@ -24,6 +24,9 @@ */ public abstract class Map implements Drawable { + + protected String name; + // the tile map (map tiles that make up the entire map image) protected MapTile[] mapTiles; @@ -31,6 +34,9 @@ public abstract class Map implements Drawable { protected int width; protected int height; + //right most bound + private int rightBound; + // the tileset this map uses for its map tiles protected Tileset tileset; @@ -61,7 +67,8 @@ public abstract class Map implements Drawable { // if set to false, camera will not move as player moves protected boolean adjustCamera = true; - public Map(String mapFileName, Tileset tileset, Point playerStartTile) { + public Map(String name, String mapFileName, Tileset tileset, Point playerStartTile) { + this.name = name; this.mapFileName = mapFileName; this.tileset = tileset; setupMap(); @@ -72,6 +79,7 @@ public Map(String mapFileName, Tileset tileset, Point playerStartTile) { this.xMidPoint = ScreenManager.getScreenWidth() / 2; this.yMidPoint = (ScreenManager.getScreenHeight() / 2); this.playerStartTile = playerStartTile; + rightBound = getWidthPixels() - getTileset().getScaledSpriteWidth(); } // sets up map by reading in the map file to create the tile map @@ -402,4 +410,16 @@ public void reset() { public void draw(GraphicsHandler graphicsHandler) { camera.draw(graphicsHandler); } + + public int getRightBound() { + return rightBound; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } diff --git a/src/Level/Player.java b/src/Level/Player.java index a54b4da..777ce5d 100644 --- a/src/Level/Player.java +++ b/src/Level/Player.java @@ -1,535 +1,234 @@ package Level; -import Engine.KeyLocker; +import Engine.Collidable; import Engine.KeyboardAction; import GameObject.GameObject; import GameObject.SpriteSheet; -import Projectiles.Bone; -import Projectiles.Fireball; -import Utils.AirGroundState; +import Level.PlayerState.Facing; import Utils.Direction; import Utils.Point; import Utils.Stopwatch; import java.util.ArrayList; +import java.util.List; +/** + * Represents the player as it navigates through a map. + * @author Thomas Kwashnak + */ public abstract class Player extends GameObject { - // values that affect player movement - // these should be set in a subclass - protected float walkSpeed = 0; - protected float maxWalkSpeed = 0; - protected float minWalkSpeed = 0; - protected float walkAcceleration = 0; - protected float gravity = 0; - protected float jumpHeight = 0; - protected float jumpDegrade = 0; - protected float terminalVelocityX = 0; - - // Number of times a player can be hit by a projectile before dying - public static int playerHealth = 3; - - protected float momentumXIncrease = 0; - // values used to handle player movement - protected float jumpForce = 0; - protected float momentumX = 0; - protected float terminalVelocityY = 0; - protected float momentumYIncrease = 0; - - - // values used to handle player movement - protected float momentumY = 0; - protected float moveAmountX, moveAmountY; - - protected Stopwatch attackCooldown = new Stopwatch(); - - // Variables used to apply effect to player from the Boss' bone - protected Stopwatch boneEffect = new Stopwatch(); - protected boolean canJump = true; + private static final int ATTACK_DELAY = 1500; + private static final float MAX_FALL_VELOCITY = 6f, MAX_DEATH_FALL_VELOCITY = 10f, DEATH_Y_VELOCITY = -2.5f; + public static int PLAYER_HEALTH = 3; + protected float gravity, jumpHeight, walkSpeed, sprintSpeed, sprintAcceleration; + private final List playerListeners = new ArrayList<>(); + private final Stopwatch attackDelay = new Stopwatch(), jumpDelay = new Stopwatch(); + private PlayerState playerState; + private Facing facing; + private LevelState levelState; + private boolean inAir; + private float absVelocityX, velocityY; - // values used to keep track of player's current state - protected PlayerState playerState; - protected Direction facingDirection; - protected AirGroundState airGroundState; - protected AirGroundState previousAirGroundState; - protected LevelState levelState; - - // classes that listen to player events can be added to this list - protected ArrayList listeners = new ArrayList<>(); - - // define keys - protected KeyLocker keyLocker = new KeyLocker(); - - // if true, player cannot be hurt by enemies (good for testing) - protected boolean isInvincible = false; - // added 11/19 - protected PlayerAttack currentProjectile; public Player(SpriteSheet spriteSheet, float x, float y, String startingAnimationName) { super(spriteSheet, x, y, startingAnimationName); - facingDirection = Direction.RIGHT; - airGroundState = AirGroundState.AIR; - previousAirGroundState = airGroundState; - playerState = PlayerState.STANDING; - levelState = LevelState.RUNNING; - this.x = x; - this.y = y; - - // 11/19 - currentProjectile = null; - } - - public void update() { - moveAmountX = 0; - moveAmountY = 0; - - // if player is currently playing through level (has not won or lost) - if (levelState == LevelState.RUNNING) { - applyGravity(); - - handlePlayerState(); - - previousAirGroundState = airGroundState; - - // update player's animation - super.update(); - - // move player with respect to map collisions based on how much player needs to move this frame - super.moveYHandleCollision(moveAmountY); - super.moveXHandleCollision(moveAmountX); - - updateLockedKeys(); - } - // if player has beaten level - else if (levelState == LevelState.LEVEL_COMPLETED) { - updateLevelCompleted(); - } - - // if player has lost level - else if (levelState == LevelState.PLAYER_DEAD) { - updatePlayerDead(); - } - - if(boneEffect.isTimeUp()) { - canJump = true; - } - - if(PlayerAttack.dogHealth == 0) { - canJump = true; - jumpHeight = 16; - } - } - - // add gravity to player, which is a downward force - protected void applyGravity() { - moveAmountY += gravity + momentumY; - } - - // based on player's current state, call appropriate player state handling method - protected void handlePlayerState() { - switch (playerState) { - case STANDING: - playerStanding(); - break; - case WALKING: - playerWalking(); - break; - case CROUCHING: - playerCrouching(); - break; - case JUMPING: - playerJumping(); - break; - // 11/19 - case ATTACKING: - playerAttacking(); - break; - } + playerState = PlayerState.STAND; + levelState = LevelState.PLAYING; + facing = Facing.RIGHT; } - // player STANDING state logic - protected void playerStanding() { - // sets animation to a STAND animation based on which way player is facing - currentAnimationName = facingDirection == Direction.RIGHT ? "STAND_RIGHT" : "STAND_LEFT"; - - // if walk left or walk right key is pressed, player enters WALKING state - if (KeyboardAction.GAME_MOVE_LEFT.isDown() || KeyboardAction.GAME_MOVE_RIGHT.isDown()) { - playerState = PlayerState.WALKING; - } - - // if jump key is pressed, player enters JUMPING state - else if (KeyboardAction.GAME_JUMP.isDown() && !keyLocker.isActionLocked(KeyboardAction.GAME_JUMP) && canJump == true) { - playerState = PlayerState.JUMPING; + public void update() { + super.update(); + + //Redirects to update depending on player state + switch (levelState) { + case PLAYING -> updatePlaying(); + case DEAD -> updateDead(); + case WIN -> updateWin(); + } + + //Updates animation using the player's current playerState + setCurrentAnimationName(playerState.get(facing)); + } + + /** + * Updates the player as whenever it is in the playing state + */ + private void updatePlaying() { + //Applies gravity + applyGravity(MAX_FALL_VELOCITY); + + //Update Player Action and Direction + boolean moveLeftDown = KeyboardAction.GAME_MOVE_LEFT.isDown(); + boolean playerMove = moveLeftDown ^ KeyboardAction.GAME_MOVE_RIGHT.isDown(); //Only true if the player is moving in a direction + facing = playerMove ? moveLeftDown ? Facing.LEFT : Facing.RIGHT : facing; //Update facing if the player moved + //Only move if the player moved and is not going out of bounds + if (playerMove && ((facing == Facing.LEFT && x > 0) ^ (facing == Facing.RIGHT && x < map.getRightBound()))) { + playerState = PlayerState.WALK; + if (KeyboardAction.GAME_SPRINT.isDown() && absVelocityX >= walkSpeed) { + if (absVelocityX < sprintSpeed) { + absVelocityX *= sprintAcceleration; + } + } else { //If player is not sprinting, or if the player started off sprinting from rest (where velocityX is 0) + absVelocityX = walkSpeed; + } + } else { //Player is not moving + absVelocityX = 0; + playerState = PlayerState.STAND; } - // if crouch key is pressed, player enters CROUCHING state - else if (KeyboardAction.GAME_CROUCH.isDown()) { - playerState = PlayerState.CROUCHING; - } - - // enter the attacking state if the attack key is pressed and the attack cooldown is up - else if(KeyboardAction.GAME_ATTACK.isDown() && attackCooldown.isTimeUp()) { - //keyLocker.lockKey(attackKey); - playerState = PlayerState.ATTACKING; - //System.out.println(previousPlayerState.toString()); + //Update Jump + if (KeyboardAction.GAME_JUMP.isDown()) { + if (jumpDelay.isTimeUp() && !inAir) { + velocityY = -jumpHeight; + } + } else if (velocityY < 0) { //if the player releases while velocity is still up, cut short, remember that velocityY is inverse + velocityY = 0; } - } - // player WALKING state logic - protected void playerWalking() { - // sets animation to a WALK animation based on which way player is facing - currentAnimationName = facingDirection == Direction.RIGHT ? "WALK_RIGHT" : "WALK_LEFT"; - - // if the running key, shift, is held down acceleration the walk speed until the character reaches the max speed - if (KeyboardAction.GAME_SPRINT.isDown()) - { - if (walkSpeed < maxWalkSpeed) - { - walkSpeed = walkSpeed * walkAcceleration; - } - - } - // once the user lets go of the running key reset the walk speed - else { - walkSpeed = minWalkSpeed; - } - - if (KeyboardAction.GAME_MOVE_LEFT.isDown()) { - //System.out.println("s"); - moveAmountX -= walkSpeed; - facingDirection = Direction.LEFT; - } - - // if walk right key is pressed, move player to the right - else if (KeyboardAction.GAME_MOVE_RIGHT.isDown()) { - //System.out.println("d"); - moveAmountX += walkSpeed; - facingDirection = Direction.RIGHT; - } else { - playerState = PlayerState.STANDING; + //Update Attack + if (KeyboardAction.GAME_ATTACK.isDown() && attackDelay.isTimeUp()) { + int attackX = facing == Facing.RIGHT ? Math.round(getX()) + getScaledWidth() - 20 : Math.round(getX()); + map.addEnemy(new PlayerAttack(new Point(attackX, Math.round(getY()) + 10), facing.mod * 1.5f, 1000)); + attackDelay.setWaitTime(ATTACK_DELAY); } - // if jump key is pressed, player enters JUMPING state - if (KeyboardAction.GAME_JUMP.isDown() && !keyLocker.isActionLocked(KeyboardAction.GAME_JUMP) && canJump == true) { - //System.out.println("w"); - playerState = PlayerState.JUMPING; + //If the player is in the air, set its animation based on velocityY + if (inAir) { + playerState = velocityY < 0 ? PlayerState.JUMP : PlayerState.FALL; + } else if(KeyboardAction.GAME_CROUCH.isDown()) { + absVelocityX = 0; + playerState = PlayerState.CROUCH; } - // if crouch key is pressed, - else if (KeyboardAction.GAME_CROUCH.isDown()) { - playerState = PlayerState.CROUCHING; - } - - // enter the attacking state if the attack key is pressed and the attack cooldown is up - else if(KeyboardAction.GAME_ATTACK.isDown() && attackCooldown.isTimeUp()) { - playerState = PlayerState.ATTACKING; - //System.out.println(previousPlayerState.toString()); - } - } - - // player CROUCHING state logic - protected void playerCrouching() { - // sets animation to a CROUCH animation based on which way player is facing - currentAnimationName = facingDirection == Direction.RIGHT ? "CROUCH_RIGHT" : "CROUCH_LEFT"; + inAir = true; //inAir is updated in collisions + //Moves while handling collisions + moveYHandleCollision(velocityY); + moveXHandleCollision(absVelocityX * facing.mod); - // if crouch key is released, player enters STANDING state - if (!KeyboardAction.GAME_CROUCH.isDown()) { - playerState = PlayerState.STANDING; - } - // if jump key is pressed, player enters JUMPING state - if (KeyboardAction.GAME_JUMP.isDown() && !keyLocker.isActionLocked(KeyboardAction.GAME_JUMP)) { - playerState = PlayerState.JUMPING; + //Updates player to death if their health hits 0 + if (PLAYER_HEALTH <= 0) { + levelState = LevelState.DEAD; } } - // player JUMPING state logic - protected void playerJumping() { - // if last frame player was on ground and this frame player is still on ground, the jump needs to be setup - if (previousAirGroundState == AirGroundState.GROUND && airGroundState == AirGroundState.GROUND && canJump == true) { - // sets animation to a JUMP animation based on which way player is facing - currentAnimationName = facingDirection == Direction.RIGHT ? "JUMP_RIGHT" : "JUMP_LEFT"; - - // player is set to be in air and then player is sent into the air - airGroundState = AirGroundState.AIR; - jumpForce = jumpHeight; - if (jumpForce > 0) { - moveAmountY -= jumpForce; - jumpForce -= jumpDegrade; - if (jumpForce < 0) { - jumpForce = 0; - } - } - } - // if the player is no longer holding any of the jump keys set the jump force to 0 to stop the jump this allows the player to control how high they want to jump - if (!KeyboardAction.GAME_JUMP.isDown()) - { - jumpForce = 0; - } - - - // if player is in air (currently in a jump) and has more jumpForce, continue sending player upwards - if (airGroundState == AirGroundState.AIR) { - if (jumpForce > 0) { - moveAmountY -= jumpForce; - jumpForce -= jumpDegrade; - if (jumpForce < 0) { - jumpForce = 0; - } - } - - // if player is moving upwards, set player's animation to jump. if player moving downwards, set player's animation to fall - if (previousY > Math.round(y)) { - currentAnimationName = facingDirection == Direction.RIGHT ? "JUMP_RIGHT" : "JUMP_LEFT"; - } else { - currentAnimationName = facingDirection == Direction.RIGHT ? "FALL_RIGHT" : "FALL_LEFT"; - } - - // allows you to move left and right while in the air - if (KeyboardAction.GAME_MOVE_LEFT.isDown()) { - moveAmountX -= walkSpeed; - facingDirection = Direction.LEFT; - } - - else if (KeyboardAction.GAME_MOVE_RIGHT.isDown()) { - moveAmountX += walkSpeed; - facingDirection = Direction.RIGHT; + /** + * Updates the player while it is dead + */ + private void updateDead() { + playerState = PlayerState.DEATH; + if (currentFrameIndex > 0) { //Checks if it is not the first frame of the player's death animation + if (map.getCamera().containsDraw(this)) { + applyGravity(MAX_DEATH_FALL_VELOCITY); + } else for (PlayerListener listener : playerListeners) { + listener.onDeath(); } - - // if player is falling, increases momentum as player falls so it falls faster over time - if (moveAmountY > 0) { - increaseMomentum(); + } else { //Only activates on the first frame of death animation + velocityY = DEATH_Y_VELOCITY; + } + //Updates Y. Does not use "moveYHandleCollision" since the player can move through the map + setY(y + velocityY); + } + + /** + * Updates the player after it completes a level + */ + private void updateWin() { + if (map.getCamera().containsDraw(this)) { //If the player can seen on the map + facing = Facing.RIGHT; + if (inAir) { //If the player is still falling + playerState = PlayerState.FALL; + applyGravity(MAX_FALL_VELOCITY); + moveYHandleCollision(velocityY); + } else { //Once the player hits the ground, walk to the right + playerState = PlayerState.WALK; + moveX(walkSpeed); } - } - - // if player last frame was in air and this frame is now on ground, player enters STANDING state - else if (previousAirGroundState == AirGroundState.AIR && airGroundState == AirGroundState.GROUND) { - playerState = PlayerState.STANDING; - + } else for (PlayerListener listener : playerListeners) { + //When the player is off the map, send a onLevelCompleted() to all listeners + listener.onLevelCompleted(); } } - - // 11/19 - public void playerAttacking() { - if (playerState == PlayerState.ATTACKING) { - - // this allows the player to still move left or right will in the attacking state - if (KeyboardAction.GAME_MOVE_LEFT.isDown()) { - moveAmountX -= walkSpeed; - facingDirection = Direction.LEFT; - } - else if (KeyboardAction.GAME_MOVE_RIGHT.isDown()) { - moveAmountX += walkSpeed; - facingDirection = Direction.RIGHT; - } - - - // define where projectile will spawn on map (x location) relative to cat's - // location - // and define its movement speed - int attackX; - float movementSpeed; - if (facingDirection == Direction.RIGHT) { - attackX = Math.round(getX()) + getScaledWidth() - 20; - movementSpeed = 1.5f; - } else { - attackX = Math.round(getX()); - movementSpeed = -1.5f; - } - - // define where projectile will spawn on the map (y location) relative to - // dinosaur enemy's location - int attackY = Math.round(getY()) + 10; - - // create projectile - PlayerAttack projectile = new PlayerAttack(new Point(attackX, attackY), movementSpeed, 1000); - currentProjectile = projectile; - // add projectile enemy to the map for it to officially spawn in the level - map.addEnemy(projectile); - - attackCooldown.setWaitTime(1500); - - // after an attack finished set the player to a standing state - playerState = PlayerState.STANDING; - - } - } - - // while player is in air, this is called, and will increase momentumY by a set amount until player reaches terminal velocity - protected void increaseMomentum() { - momentumY += momentumYIncrease; - if (momentumY > terminalVelocityY) { - momentumY = terminalVelocityY; + /** + * Updates the current velocityY with the current gravity + * @param maxFallVelocity Gravity Force + */ + private void applyGravity(float maxFallVelocity) { + if (velocityY < maxFallVelocity) { + velocityY += gravity; } } - protected void increaseMomentumX() { - momentumX += momentumXIncrease; - if (momentumX > terminalVelocityX) { - momentumX = terminalVelocityX; - } - } - - protected void updateLockedKeys() { - keyLocker.setAction(KeyboardAction.GAME_JUMP,KeyboardAction.GAME_ATTACK); - } - @Override public void onEndCollisionCheckX(boolean hasCollided, Direction direction) { - // if the player collides with the coordinates specified below, it either stops (beginning of level) or goes back to the start (end of level) - - if (direction == Direction.LEFT || direction == Direction.RIGHT) { - int rightBound = map.getWidthPixels() - map.getTileset().getScaledSpriteWidth(); - if (x < 0) { - hasCollided = true; - momentumX = 0; - setX(0); - } else if (levelState != LevelState.LEVEL_COMPLETED && x > rightBound) { - hasCollided = true; - momentumX = 0; - setX(rightBound); - } - } - if (hasCollided && MapTileCollisionHandler.lastCollidedTileX != null) { - if (MapTileCollisionHandler.lastCollidedTileX.getTileType() == TileType.LETHAL) { - levelState = LevelState.PLAYER_DEAD; - } - } - } - - @Override - public void onEndCollisionCheckY(boolean hasCollided, Direction direction) { - // if player collides with a map tile below it, it is now on the ground - // if player does not collide with a map tile below, it is in air - if (direction == Direction.DOWN) { - if (hasCollided) { - momentumY = 0; - airGroundState = AirGroundState.GROUND; - } else { - playerState = PlayerState.JUMPING; - airGroundState = AirGroundState.AIR; - } - } - - // if player collides with map tile upwards, it means it was jumping and then hit into a ceiling -- immediately stop upwards jump velocity - else if (direction == Direction.UP) { - if (hasCollided) { - jumpForce = 0; + if (hasCollided) { //If it has collided, handles the collision and sets the player's animation to standing + handleCollision(MapTileCollisionHandler.lastCollidedTileX); + if (playerState == PlayerState.WALK) { //If the player is walking (aka, not falling), then make the player standing + playerState = PlayerState.STAND; } } - if (hasCollided && MapTileCollisionHandler.lastCollidedTileY != null) { - if (MapTileCollisionHandler.lastCollidedTileY.getTileType() == TileType.LETHAL) { - levelState = LevelState.PLAYER_DEAD; - } - } } - // other entities can call this method to hurt the player - public void hurtPlayer(MapEntity mapEntity) { - if (!isInvincible) { - // if map entity is an enemy, kill player on touch - if (mapEntity instanceof Enemy) { - playerHealth = 0; - } - if (mapEntity instanceof Projectile) { - playerHealth -= 1; - } - if(mapEntity instanceof Bone) { - canJump = false; - boneEffect.setWaitTime(5000); + @Override + public void onEndCollisionCheckY(boolean hasCollided, Direction direction) { + if (hasCollided) { + if (direction == Direction.DOWN) { //If direction is down, make inAir false as the player is ontop of something + inAir = false; } - if (playerHealth <= 0) { - levelState = LevelState.PLAYER_DEAD; - } + velocityY = 0; //Reset velocity + handleCollision(MapTileCollisionHandler.lastCollidedTileY); } } - // other entities can call this to tell the player they beat a level - public void completeLevel() { - levelState = LevelState.LEVEL_COMPLETED; - - } - - // if player has beaten level, this will be the update cycle - public void updateLevelCompleted() { - // if player is not on ground, player should fall until it touches the ground - if (airGroundState != AirGroundState.GROUND && map.getCamera().containsDraw(this)) { - currentAnimationName = "FALL_RIGHT"; - applyGravity(); - increaseMomentum(); - super.update(); - moveYHandleCollision(moveAmountY); - } - // move player to the right until it walks off screen - else if (map.getCamera().containsDraw(this)) { - currentAnimationName = "WALK_RIGHT"; - super.update(); - moveXHandleCollision(walkSpeed); - } else { - // tell all player listeners that the player has finished the level - for (PlayerListener listener : listeners) { - listener.onLevelCompleted(); - } + private void handleCollision(MapTile tile) { + if (tile != null && tile.getTileType() == TileType.LETHAL) { + //If the player hits a lethal tile, set its state to dead + levelState = LevelState.DEAD; } } - // if player has died, this will be the update cycle - public void updatePlayerDead() { - // change player animation to DEATH - if (!currentAnimationName.startsWith("DEATH")) { - if (facingDirection == Direction.RIGHT) { - currentAnimationName = "DEATH_RIGHT"; - } else { - currentAnimationName = "DEATH_LEFT"; - } - super.update(); + public void hurtPlayer(MapEntity mapEntity) { + if(mapEntity instanceof Collidable.Damage) { + PLAYER_HEALTH -= ((Damage) mapEntity).getDamage(); + } else if(mapEntity instanceof Collidable.InstantDeath) { + PLAYER_HEALTH = 0; } - // if death animation not on last frame yet, continue to play out death animation - else if (currentFrameIndex != getCurrentAnimation().length - 1) { - super.update(); + if(mapEntity instanceof Collidable.PreventJump) { + jumpDelay.setWaitTime(((Collidable.PreventJump) mapEntity).getJumpDelay()); } - // if death animation on last frame (it is set up not to loop back to start), player should continually fall until it goes off screen - else if (currentFrameIndex == getCurrentAnimation().length - 1) { - if (map.getCamera().containsDraw(this)) { - moveY(3); - } else { - // tell all player listeners that the player has died in the level - for (PlayerListener listener : listeners) { - listener.onDeath(); - } - } - } - } - - public PlayerState getPlayerState() { - return playerState; - } - - public void setPlayerState(PlayerState playerState) { - this.playerState = playerState; } - public AirGroundState getAirGroundState() { - return airGroundState; + /** + * Indicates that the player wins, and starts the winning animation + */ + public void completeLevel() { + levelState = LevelState.WIN; } - public Direction getFacingDirection() { - return facingDirection; + /** + * @return {@code True} if the player is in the air, {@code false} if the player is on some surface + */ + public boolean isInAir() { + return inAir; } - public void setFacingDirection(Direction facingDirection) { - this.facingDirection = facingDirection; + /** + * Adds a listener + * @param listener Methods to be checked whenever the player executes an event + */ + public void addListener(PlayerListener listener) { + playerListeners.add(listener); } - public void setLevelState(LevelState levelState) { - this.levelState = levelState; + /** + * Updates the jump height of the player + * @param height Velocity to apply when the player jumps + */ + public void setJumpHeight(int height) { + this.jumpHeight = height; } - - public void addListener(PlayerListener listener) { - listeners.add(listener); - } } - - diff --git a/src/Level/PlayerAttack.java b/src/Level/PlayerAttack.java index 8f11657..481c423 100644 --- a/src/Level/PlayerAttack.java +++ b/src/Level/PlayerAttack.java @@ -6,13 +6,10 @@ import Engine.ImageLoader; import GameObject.Frame; import GameObject.SpriteSheet; -import Level.Enemy; -import Level.MapEntityStatus; -import Level.Player; import Utils.Direction; import Utils.Point; import Utils.Stopwatch; -import Level.Map; + import java.util.ArrayList; import java.util.HashMap; diff --git a/src/Level/PlayerListener.java b/src/Level/PlayerListener.java index fdeccd7..6f2ff55 100644 --- a/src/Level/PlayerListener.java +++ b/src/Level/PlayerListener.java @@ -2,6 +2,15 @@ // Other classes can use this interface to listen for events from the Player class public interface PlayerListener { + + /** + * Whenever the player leaves the level in its completed state + */ void onLevelCompleted(); void onDeath(); + + /** + * Whenever the player finishes the level by hitting the finish button + */ + void onLevelFinished(); } diff --git a/src/Level/PlayerState.java b/src/Level/PlayerState.java index 6a6d945..479fdfd 100644 --- a/src/Level/PlayerState.java +++ b/src/Level/PlayerState.java @@ -2,5 +2,34 @@ // This enum represents different states the Player can be in public enum PlayerState { - STANDING, WALKING, JUMPING, CROUCHING, ATTACKING + STAND, WALK, JUMP, CROUCH, FALL, + DEATH; + + private String left,right; + + PlayerState() { + left = this + "_LEFT"; + right = this + "_RIGHT"; + } + + public String getLeft() { + return left; + } + + public String getRight() { + return right; + } + + public String get(Facing facing) { + return facing == Facing.LEFT ? left : right; + } + + public enum Facing { + LEFT(-1),RIGHT(1); + + public final int mod; + Facing(int i) { + this.mod = i; + } + } } diff --git a/src/Level/Projectile.java b/src/Level/Projectile.java index d35ea92..1ff1e99 100644 --- a/src/Level/Projectile.java +++ b/src/Level/Projectile.java @@ -1,14 +1,16 @@ package Level; +import Engine.Collidable; import GameObject.Frame; import GameObject.ImageEffect; import GameObject.Rectangle; import GameObject.SpriteSheet; + import java.awt.image.BufferedImage; import java.util.HashMap; // This class is a base class for all projectiles in the game -- all projectiles should extend from it -public class Projectile extends MapEntity { +public class Projectile extends MapEntity implements Collidable.Damage { public Projectile(float x, float y, SpriteSheet spriteSheet, String startingAnimation) { super(x, y, spriteSheet, startingAnimation); @@ -50,6 +52,10 @@ public void update(Player player) { } } + public int getDamage() { + return 1; + } + // A subclass can override this method to specify what it does when it touches the player public void touchedPlayer(Player player) { player.hurtPlayer(this); diff --git a/src/Maps/BossBattle.java b/src/Maps/BossBattle.java index 1997d4c..0c66ef8 100644 --- a/src/Maps/BossBattle.java +++ b/src/Maps/BossBattle.java @@ -1,25 +1,27 @@ package Maps; -import Enemies.BugEnemy; +import Enemies.CyborgEnemy; import Enemies.DinosaurEnemy; import Enemies.Dog; -import Enemies.CyborgEnemy; import Engine.ImageLoader; import EnhancedMapTiles.EndLevelBox; import EnhancedMapTiles.HorizontalMovingPlatform; import GameObject.Rectangle; import Level.*; -import NPCs.Walrus; import Tilesets.CommonTileset; import Utils.Direction; import Utils.Point; + import java.util.ArrayList; // Represents a test map to be used in a level public class BossBattle extends Map { + private boolean bossKilled = false; + + //original 17 public BossBattle() { - super("BossBattle.txt", new CommonTileset(), new Point(1, 17)); + super("Final Boss","BossBattle.txt", new CommonTileset(), new Point(1, 17)); } @Override @@ -55,4 +57,13 @@ public ArrayList loadEnhancedMapTiles() { return enhancedMapTiles; } + @Override + public void update(Player player) { + super.update(player); + if(!bossKilled && PlayerAttack.dogHealth <= 0) { + bossKilled = true; + player.setJumpHeight(16); + } + } + } diff --git a/src/Maps/GameMaps.java b/src/Maps/GameMaps.java new file mode 100644 index 0000000..ff1893c --- /dev/null +++ b/src/Maps/GameMaps.java @@ -0,0 +1,60 @@ +package Maps; + +import Level.Map; + +/** + * Contains a centralized list of all game maps. Includes the name of the map, the function to create the map. + */ +public class GameMaps { + public static final MapManager[] MAPS; + + static { + /* + List of all maps registered in the game, in order + */ + MapFactory[] MAP_FACTORIES = new MapFactory[]{ + TestTutorial::new, + TestMap::new, + TestMap2::new, + TestMap3::new, + TestMap4::new, + TestMap5::new, + TestMap6::new, + TestMap7::new, + BossBattle::new + }; + + MAPS = new MapManager[MAP_FACTORIES.length]; + for(int i = 0; i < MAPS.length; i++) { + MAPS[i] = new MapManager(MAP_FACTORIES[i]); + } + } + + /** + * Sub-class that pulls out + */ + public static class MapManager implements MapFactory { + + private MapFactory mapFactory; + private String name; + + public MapManager(MapFactory mapFactory) { + this.mapFactory = mapFactory; + + Map map = mapFactory.generateMap(); + name = map.getName(); + } + + public String getName() { + return name; + } + + public Map generateMap() { + return mapFactory.generateMap(); + } + } + + public interface MapFactory { + Map generateMap(); + } +} diff --git a/src/Maps/LevelSelectMap.java b/src/Maps/LevelSelectMap.java index a2196c2..408633f 100644 --- a/src/Maps/LevelSelectMap.java +++ b/src/Maps/LevelSelectMap.java @@ -8,7 +8,7 @@ public class LevelSelectMap extends Map { public LevelSelectMap() { - super("level_select_map.txt", new CommonTileset(), new Point(5, 9)); + super("","level_select_map.txt", new CommonTileset(), new Point(5, 9)); } } diff --git a/src/Maps/TestMap.java b/src/Maps/TestMap.java index 831cd12..5835c65 100644 --- a/src/Maps/TestMap.java +++ b/src/Maps/TestMap.java @@ -17,7 +17,7 @@ // Represents a test map to be used in a level public class TestMap extends Map { public TestMap() { - super("test_map.txt", new CommonTileset(), new Point(1, 11)); + super("Level 1","test_map.txt", new CommonTileset(), new Point(1, 11)); } @Override diff --git a/src/Maps/TestMap2.java b/src/Maps/TestMap2.java index 23e5cd3..eaa6f66 100644 --- a/src/Maps/TestMap2.java +++ b/src/Maps/TestMap2.java @@ -2,12 +2,11 @@ import Enemies.BugEnemy; import Enemies.DinosaurEnemy; -import Engine.ImageLoader; import EnhancedMapTiles.EndLevelBox; -import EnhancedMapTiles.HorizontalMovingPlatform; -import GameObject.Rectangle; -import Level.*; -import NPCs.Walrus; +import Level.Enemy; +import Level.EnhancedMapTile; +import Level.Map; +import Level.NPC; import Tilesets.CommonTileset; import Utils.Direction; import Utils.Point; @@ -18,7 +17,7 @@ public class TestMap2 extends Map { public TestMap2() { - super("test_map_2.txt", new CommonTileset(), new Point(1, 11)); + super("Level 2","test_map_2.txt", new CommonTileset(), new Point(1, 11)); } @Override diff --git a/src/Maps/TestMap3.java b/src/Maps/TestMap3.java index 3fe66b7..7e4130e 100644 --- a/src/Maps/TestMap3.java +++ b/src/Maps/TestMap3.java @@ -7,7 +7,6 @@ import EnhancedMapTiles.HorizontalMovingPlatform; import GameObject.Rectangle; import Level.*; -import NPCs.Walrus; import Tilesets.CommonTileset; import Utils.Direction; import Utils.Point; @@ -18,7 +17,7 @@ public class TestMap3 extends Map { public TestMap3() { - super("test_map3.txt", new CommonTileset(), new Point(1, 11)); + super("Level 3","test_map3.txt", new CommonTileset(), new Point(1, 11)); } @Override diff --git a/src/Maps/TestMap4.java b/src/Maps/TestMap4.java index 1c913e1..4f0622d 100644 --- a/src/Maps/TestMap4.java +++ b/src/Maps/TestMap4.java @@ -2,13 +2,11 @@ import Enemies.BugEnemy; import Enemies.CyborgEnemy; -import Enemies.DinosaurEnemy; import Engine.ImageLoader; import EnhancedMapTiles.EndLevelBox; import EnhancedMapTiles.HorizontalMovingPlatform; import GameObject.Rectangle; import Level.*; -import NPCs.Walrus; import Tilesets.CommonTileset; import Utils.Direction; import Utils.Point; @@ -19,7 +17,7 @@ public class TestMap4 extends Map { public TestMap4() { - super("test_map4.txt", new CommonTileset(), new Point(1, 11)); + super("Level 4","test_map4.txt", new CommonTileset(), new Point(1, 11)); } @Override diff --git a/src/Maps/TestMap5.java b/src/Maps/TestMap5.java index c00d753..fee6dae 100644 --- a/src/Maps/TestMap5.java +++ b/src/Maps/TestMap5.java @@ -7,7 +7,6 @@ import EnhancedMapTiles.HorizontalMovingPlatform; import GameObject.Rectangle; import Level.*; -import NPCs.Walrus; import Tilesets.CommonTileset; import Utils.Direction; import Utils.Point; @@ -18,7 +17,7 @@ public class TestMap5 extends Map { public TestMap5() { - super("test_map5.txt", new CommonTileset(), new Point(1, 11)); + super("Level 5","test_map5.txt", new CommonTileset(), new Point(1, 11)); } @Override diff --git a/src/Maps/TestMap6.java b/src/Maps/TestMap6.java index 1e321d8..ff04e47 100644 --- a/src/Maps/TestMap6.java +++ b/src/Maps/TestMap6.java @@ -1,14 +1,16 @@ package Maps; import Enemies.BugEnemy; -import Enemies.DinosaurEnemy; import Enemies.CyborgEnemy; +import Enemies.DinosaurEnemy; import Engine.ImageLoader; import EnhancedMapTiles.EndLevelBox; import EnhancedMapTiles.HorizontalMovingPlatform; import GameObject.Rectangle; -import Level.*; -import NPCs.Walrus; +import Level.Enemy; +import Level.EnhancedMapTile; +import Level.Map; +import Level.TileType; import Tilesets.CommonTileset; import Utils.Direction; import Utils.Point; @@ -19,7 +21,7 @@ public class TestMap6 extends Map { public TestMap6() { - super("test_map6.txt", new CommonTileset(), new Point(1, 15)); + super("Level 6","test_map6.txt", new CommonTileset(), new Point(1, 15)); } @Override diff --git a/src/Maps/TestMap7.java b/src/Maps/TestMap7.java index a753c22..f6cbda5 100644 --- a/src/Maps/TestMap7.java +++ b/src/Maps/TestMap7.java @@ -1,14 +1,16 @@ package Maps; import Enemies.BugEnemy; -import Enemies.DinosaurEnemy; import Enemies.CyborgEnemy; +import Enemies.DinosaurEnemy; import Engine.ImageLoader; import EnhancedMapTiles.EndLevelBox; import EnhancedMapTiles.HorizontalMovingPlatform; import GameObject.Rectangle; -import Level.*; -import NPCs.Walrus; +import Level.Enemy; +import Level.EnhancedMapTile; +import Level.Map; +import Level.TileType; import Tilesets.CommonTileset; import Utils.Direction; import Utils.Point; @@ -19,7 +21,7 @@ public class TestMap7 extends Map { public TestMap7() { - super("test_map7.txt", new CommonTileset(), new Point(1, 11)); + super("Level 7","test_map7.txt", new CommonTileset(), new Point(1, 11)); } @Override diff --git a/src/Maps/TestTutorial.java b/src/Maps/TestTutorial.java index 69bb925..f1d2c4c 100644 --- a/src/Maps/TestTutorial.java +++ b/src/Maps/TestTutorial.java @@ -2,16 +2,16 @@ import Enemies.BugEnemy; import Enemies.DinosaurEnemy; -import Engine.*; +import Engine.GraphicsHandler; +import Engine.ImageLoader; import EnhancedMapTiles.EndLevelBox; import EnhancedMapTiles.HorizontalMovingPlatform; import GameObject.Rectangle; import Level.*; -import NPCs.Walrus; +import SpriteFont.SpriteFont; import Tilesets.CommonTileset; import Utils.Direction; import Utils.Point; -import SpriteFont.*; import java.awt.*; import java.util.ArrayList; @@ -20,7 +20,7 @@ public class TestTutorial extends Map { public TestTutorial() { - super("test_tutorial.txt", new CommonTileset(), new Point(1, 11)); + super("Tutorial","test_tutorial.txt", new CommonTileset(), new Point(1, 11)); } @Override diff --git a/src/Maps/TitleScreenMap.java b/src/Maps/TitleScreenMap.java index 23ac457..408b1db 100644 --- a/src/Maps/TitleScreenMap.java +++ b/src/Maps/TitleScreenMap.java @@ -8,7 +8,7 @@ public class TitleScreenMap extends Map { public TitleScreenMap() { - super("title_screen_map.txt", new CommonTileset(), new Point(1, 9)); + super("","title_screen_map.txt", new CommonTileset(), new Point(1, 9)); } } diff --git a/src/Menu/Menu.java b/src/Menu/Menu.java index a5ef189..dde82eb 100644 --- a/src/Menu/Menu.java +++ b/src/Menu/Menu.java @@ -8,7 +8,7 @@ import java.awt.*; import java.awt.event.MouseEvent; -public abstract class Menu extends Screen { +public abstract class Menu extends Screen implements SelectableMenu { private final Stopwatch keyTimer = new Stopwatch(); private MenuOption[] menuOptions; @@ -47,7 +47,7 @@ protected void updateBackground() { protected void updateMenuEscape() { if (KeyboardAction.MENU_ESCAPE.isDown()) { - GamePanel.getScreenCoordinator().setGameState(GameState.MENU); + backToMainMenu(); } } @@ -125,8 +125,10 @@ protected void drawDrawables(GraphicsHandler graphicsHandler) { } public void mouseClicked(MouseEvent e) { - for (MenuOption option : menuOptions) { - option.mouseClicked(e); + if(menuOptions != null) { + for (MenuOption option : menuOptions) { + option.mouseClicked(e); + } } } @@ -174,33 +176,33 @@ protected void setMenuItemsAsGrid(MenuOption[][] grid) { } } - SelectFunction function = (newSelection) -> { - selectedItem.setSelected(false); - newSelection.setSelected(true); - selectedItem = newSelection; - }; - menuOptions = new MenuOption[count]; for (int i = 0; i < grid.length; i++) { for (int j = 0; j < grid[i].length; j++) { if (grid[i][j] != null) { menuOptions[menuOptions.length - (count--)] = grid[i][j]; - grid[i][j].setSelectFunction(function); + grid[i][j].setSelectFunction(this); } } } } + @Override + public void select(MenuOption menuOption) { + selectedItem.setSelected(false); + menuOption.setSelected(true); + selectedItem = menuOption; + } + protected void setBackground(Map background) { this.background = background; } - protected void setDrawables(Drawable[] drawables) { + protected void setDrawables(Drawable... drawables) { this.drawables = drawables; } - public interface SelectFunction { - - void select(MenuOption item); + protected void backToMainMenu() { + GamePanel.getScreenCoordinator().setGameState(GameState.MENU); } } diff --git a/src/Menu/MenuOption.java b/src/Menu/MenuOption.java index 6766842..755d45b 100644 --- a/src/Menu/MenuOption.java +++ b/src/Menu/MenuOption.java @@ -1,11 +1,14 @@ package Menu; +import Engine.Drawable; +import Engine.GamePanel; import Engine.GraphicsHandler; +import Game.GameState; import java.awt.*; import java.awt.event.MouseEvent; -public class MenuOption { +public class MenuOption implements Drawable { private static final Font DEFAULT_MENU_FONT = new Font("Comic sans", Font.PLAIN, 30); private static final Color DEFAULT_COLOR = new Color(49, 207, 240); @@ -13,7 +16,7 @@ public class MenuOption { private static final Color OUTLINE_COLOR = new Color(0, 0, 0); private static final int OUTLINE_THICKNESS = 3; - private Menu.SelectFunction selectFunction; + private SelectableMenu selectableMenu; private final MenuOption[] neighbors; @@ -29,6 +32,8 @@ public class MenuOption { private final int y; private int width; private int height; + + private CloseOnExecute actionAfterExecute = CloseOnExecute.REMAINOPEN; public MenuOption(String text, int x, int y, MenuItemListener listener) { this(text, x, y); @@ -41,6 +46,12 @@ public MenuOption(String text, int x, int y) { this.x = x; this.y = y; } + + public MenuOption(String text, int x, int y, MenuItemListener listener, CloseOnExecute closeOnExecute) { + this(text, x, y); + setListener(listener); + actionAfterExecute = closeOnExecute; + } public void setNeighborItem(MenuOption item, Direction direction) { neighbors[direction.getIndex()] = item; @@ -128,8 +139,8 @@ public void drawWithParsedNewLines(GraphicsHandler graphicsHandler) { public void mouseMoved(Point p) { softSelected = contains(p); - if (selected != softSelected && selectFunction != null) { - selectFunction.select(this); + if (selected != softSelected && selectableMenu != null) { + selectableMenu.select(this); } } @@ -147,9 +158,19 @@ public void execute() { if (listener != null) { listener.event(); } + // allows a menu option the ability to close after selecting them + if (actionAfterExecute == CloseOnExecute.CLOSE) + { + GamePanel.getScreenCoordinator().setGameState(GameState.MENU); + } } - public void setSelectFunction(Menu.SelectFunction function) { - this.selectFunction = function; + public void setSelectFunction(SelectableMenu function) { + this.selectableMenu = function; + } + + public enum CloseOnExecute + { + REMAINOPEN, CLOSE; } } diff --git a/src/Menu/SelectableMenu.java b/src/Menu/SelectableMenu.java new file mode 100644 index 0000000..f94ac0e --- /dev/null +++ b/src/Menu/SelectableMenu.java @@ -0,0 +1,5 @@ +package Menu; + +public interface SelectableMenu { + void select(MenuOption menuOption); +} diff --git a/src/Players/Avatar.java b/src/Players/Avatar.java index c39ae30..91b730a 100644 --- a/src/Players/Avatar.java +++ b/src/Players/Avatar.java @@ -6,9 +6,9 @@ //TODO make this the new Cat Options with a factory to create the player public enum Avatar { - CAT_ORANGE(p -> new Cat("Cat.png",p)), - CAT_BLUE(p -> new Cat("CatBlue.png",p)), - CAT_GREEN(p -> new Cat("CatGreen.png",p)); + CAT_ORANGE(p -> new Cat("Cat.png", p)), + CAT_BLUE(p -> new Cat("CatBlue.png", p)), + CAT_GREEN(p -> new Cat("CatGreen.png", p)); private PlayerFactory factory; diff --git a/src/Players/Cat.java b/src/Players/Cat.java index 4225bcd..a226e67 100644 --- a/src/Players/Cat.java +++ b/src/Players/Cat.java @@ -16,21 +16,25 @@ public class Cat extends Player { public Cat(String name, Point point) { - this(name,point.x,point.y); + this(name, point.x, point.y); } - public Cat(String name,float x, float y) { + public Cat(String name, float x, float y) { super(new SpriteSheet(ImageLoader.load(name), 24, 24), x, y, "STAND_RIGHT"); - + gravity = .5f; - terminalVelocityY = 6f; jumpHeight = 14.5f; - jumpDegrade = .5f; walkSpeed = 2.1f; - minWalkSpeed = 2.1f; - maxWalkSpeed = 3.3f; - walkAcceleration = 1.05f; - momentumYIncrease = .5f; + sprintSpeed = 3.3f; + sprintAcceleration = 1.05f; + // terminalVelocityY = 6f; + // jumpHeight = 14.5f; + // jumpDegrade = .5f; + // walkSpeed = 2.1f; + // minWalkSpeed = 2.1f; + // maxWalkSpeed = 3.3f; + // walkAcceleration = 1.05f; + // momentumYIncrease = .5f; } public void update() { @@ -45,133 +49,69 @@ public void draw(GraphicsHandler graphicsHandler) { @Override public HashMap getAnimations(SpriteSheet spriteSheet) { return new HashMap() {{ - put("STAND_RIGHT", new Frame[] { - new FrameBuilder(spriteSheet.getSprite(0, 0), 0) - .withScale(3) - .withBounds(8, 9, 8, 9) - .build() + put("STAND_RIGHT", new Frame[]{ + new FrameBuilder(spriteSheet.getSprite(0, 0), 0).withScale(3).withBounds(8, 9, 8, 9).build() }); - put("STAND_LEFT", new Frame[] { - new FrameBuilder(spriteSheet.getSprite(0, 0), 0) - .withScale(3) - .withImageEffect(ImageEffect.FLIP_HORIZONTAL) - .withBounds(8, 9, 8, 9) - .build() + put("STAND_LEFT", new Frame[]{ + new FrameBuilder(spriteSheet.getSprite(0, 0), 0).withScale(3).withImageEffect(ImageEffect.FLIP_HORIZONTAL).withBounds( + 8, 9, 8, 9).build() }); - put("WALK_RIGHT", new Frame[] { - new FrameBuilder(spriteSheet.getSprite(1, 0), 200) - .withScale(3) - .withBounds(8, 9, 8, 9) - .build(), - new FrameBuilder(spriteSheet.getSprite(1, 1), 200) - .withScale(3) - .withBounds(8, 9, 8, 9) - .build(), - new FrameBuilder(spriteSheet.getSprite(1, 2), 200) - .withScale(3) - .withBounds(8, 9, 8, 9) - .build(), - new FrameBuilder(spriteSheet.getSprite(1, 3), 200) - .withScale(3) - .withBounds(8, 9, 8, 9) - .build() + put("WALK_RIGHT", new Frame[]{ + new FrameBuilder(spriteSheet.getSprite(1, 0), 200).withScale(3).withBounds(8, 9, 8, 9).build(), new FrameBuilder( + spriteSheet.getSprite(1, 1), 200).withScale(3).withBounds(8, 9, 8, 9).build(), new FrameBuilder(spriteSheet.getSprite(1, 2), 200) + .withScale(3).withBounds(8, 9, 8, 9).build(), new FrameBuilder(spriteSheet.getSprite(1, 3), 200).withScale(3).withBounds( + 8, 9, 8, 9).build() }); - put("WALK_LEFT", new Frame[] { - new FrameBuilder(spriteSheet.getSprite(1, 0), 200) - .withScale(3) - .withImageEffect(ImageEffect.FLIP_HORIZONTAL) - .withBounds(8, 9, 8, 9) - .build(), - new FrameBuilder(spriteSheet.getSprite(1, 1), 200) - .withScale(3) - .withImageEffect(ImageEffect.FLIP_HORIZONTAL) - .withBounds(8, 9, 8, 9) - .build(), - new FrameBuilder(spriteSheet.getSprite(1, 2), 200) - .withScale(3) - .withImageEffect(ImageEffect.FLIP_HORIZONTAL) - .withBounds(8, 9, 8, 9) - .build(), - new FrameBuilder(spriteSheet.getSprite(1, 3), 200) - .withScale(3) - .withImageEffect(ImageEffect.FLIP_HORIZONTAL) - .withBounds(8, 9, 8, 9) - .build() + put("WALK_LEFT", new Frame[]{ + new FrameBuilder(spriteSheet.getSprite(1, 0), 200).withScale(3).withImageEffect(ImageEffect.FLIP_HORIZONTAL).withBounds( + 8, 9, 8, 9).build(), + new FrameBuilder(spriteSheet.getSprite(1, 1), 200).withScale(3).withImageEffect(ImageEffect.FLIP_HORIZONTAL).withBounds( + 8, 9, 8, 9).build(), + new FrameBuilder(spriteSheet.getSprite(1, 2), 200).withScale(3).withImageEffect(ImageEffect.FLIP_HORIZONTAL).withBounds( + 8, 9, 8, 9).build(), + new FrameBuilder(spriteSheet.getSprite(1, 3), 200).withScale(3).withImageEffect(ImageEffect.FLIP_HORIZONTAL).withBounds( + 8, 9, 8, 9).build() }); - put("JUMP_RIGHT", new Frame[] { - new FrameBuilder(spriteSheet.getSprite(2, 0), 0) - .withScale(3) - .withBounds(8, 9, 8, 9) - .build() + put("JUMP_RIGHT", new Frame[]{ + new FrameBuilder(spriteSheet.getSprite(2, 0), 0).withScale(3).withBounds(8, 9, 8, 9).build() }); - put("JUMP_LEFT", new Frame[] { - new FrameBuilder(spriteSheet.getSprite(2, 0), 0) - .withScale(3) - .withImageEffect(ImageEffect.FLIP_HORIZONTAL) - .withBounds(8, 9, 8, 9) - .build() + put("JUMP_LEFT", new Frame[]{ + new FrameBuilder(spriteSheet.getSprite(2, 0), 0).withScale(3).withImageEffect(ImageEffect.FLIP_HORIZONTAL).withBounds( + 8, 9, 8, 9).build() }); - put("FALL_RIGHT", new Frame[] { - new FrameBuilder(spriteSheet.getSprite(3, 0), 0) - .withScale(3) - .withBounds(8, 9, 8, 9) - .build() + put("FALL_RIGHT", new Frame[]{ + new FrameBuilder(spriteSheet.getSprite(3, 0), 0).withScale(3).withBounds(8, 9, 8, 9).build() }); - put("FALL_LEFT", new Frame[] { - new FrameBuilder(spriteSheet.getSprite(3, 0), 0) - .withScale(3) - .withImageEffect(ImageEffect.FLIP_HORIZONTAL) - .withBounds(8, 9, 8, 9) - .build() + put("FALL_LEFT", new Frame[]{ + new FrameBuilder(spriteSheet.getSprite(3, 0), 0).withScale(3).withImageEffect(ImageEffect.FLIP_HORIZONTAL).withBounds( + 8, 9, 8, 9).build() }); - put("CROUCH_RIGHT", new Frame[] { - new FrameBuilder(spriteSheet.getSprite(4, 0), 0) - .withScale(3) - .withBounds(8, 12, 8, 6) - .build() + put("CROUCH_RIGHT", new Frame[]{ + new FrameBuilder(spriteSheet.getSprite(4, 0), 0).withScale(3).withBounds(8, 12, 8, 6).build() }); - put("CROUCH_LEFT", new Frame[] { - new FrameBuilder(spriteSheet.getSprite(4, 0), 0) - .withScale(3) - .withImageEffect(ImageEffect.FLIP_HORIZONTAL) - .withBounds(8, 12, 8, 6) - .build() + put("CROUCH_LEFT", new Frame[]{ + new FrameBuilder(spriteSheet.getSprite(4, 0), 0).withScale(3).withImageEffect(ImageEffect.FLIP_HORIZONTAL).withBounds( + 8, 12, 8, 6).build() }); - put("DEATH_RIGHT", new Frame[] { - new FrameBuilder(spriteSheet.getSprite(5, 0), 100) - .withScale(3) - .build(), - new FrameBuilder(spriteSheet.getSprite(5, 1), 100) - .withScale(3) - .build(), - new FrameBuilder(spriteSheet.getSprite(5, 2), -1) - .withScale(3) - .build() + put("DEATH_RIGHT", new Frame[]{ + new FrameBuilder(spriteSheet.getSprite(5, 0), 100).withScale(3).build(), new FrameBuilder( + spriteSheet.getSprite(5, 1), 100).withScale(3).build(), new FrameBuilder(spriteSheet.getSprite(5, 2), -1).withScale(3).build() }); - put("DEATH_LEFT", new Frame[] { - new FrameBuilder(spriteSheet.getSprite(5, 0), 100) - .withScale(3) - .withImageEffect(ImageEffect.FLIP_HORIZONTAL) - .build(), - new FrameBuilder(spriteSheet.getSprite(5, 1), 100) - .withScale(3) - .withImageEffect(ImageEffect.FLIP_HORIZONTAL) - .build(), - new FrameBuilder(spriteSheet.getSprite(5, 2), -1) - .withScale(3) - .withImageEffect(ImageEffect.FLIP_HORIZONTAL) - .build() + put("DEATH_LEFT", new Frame[]{ + new FrameBuilder(spriteSheet.getSprite(5, 0), 100).withScale(3).withImageEffect(ImageEffect.FLIP_HORIZONTAL).build(), + new FrameBuilder(spriteSheet.getSprite(5, 1), 100).withScale(3).withImageEffect(ImageEffect.FLIP_HORIZONTAL).build(), + new FrameBuilder(spriteSheet.getSprite(5, 2), -1).withScale(3).withImageEffect(ImageEffect.FLIP_HORIZONTAL).build() }); }}; } diff --git a/src/Projectiles/Bone.java b/src/Projectiles/Bone.java index 9a73392..d4a4771 100644 --- a/src/Projectiles/Bone.java +++ b/src/Projectiles/Bone.java @@ -1,6 +1,7 @@ package Projectiles; import Builders.FrameBuilder; +import Engine.Collidable; import Engine.ImageLoader; import GameObject.Frame; import GameObject.ImageEffect; @@ -12,12 +13,13 @@ import Utils.Direction; import Utils.Point; import Utils.Stopwatch; + import java.util.HashMap; // This class is for the bone enemy that the dog class shoots out // it will travel in a straight line (x axis) for a set time before disappearing // it will disappear early if it collides with a solid map tile -public class Bone extends Projectile { +public class Bone extends Projectile implements Collidable.PreventJump { private float movementSpeed; private Stopwatch existenceTimer = new Stopwatch(); @@ -90,4 +92,8 @@ public HashMap getAnimations(SpriteSheet spriteSheet) { }); }}; } + + public int getJumpDelay() { + return 5000; + } } diff --git a/src/Projectiles/Fireball.java b/src/Projectiles/Fireball.java index 0d2f98a..c73ae76 100644 --- a/src/Projectiles/Fireball.java +++ b/src/Projectiles/Fireball.java @@ -4,7 +4,6 @@ import Engine.ImageLoader; import GameObject.Frame; import GameObject.SpriteSheet; -import Level.Enemy; import Level.MapEntityStatus; import Level.Player; import Level.Projectile; diff --git a/src/Projectiles/LazerBeam.java b/src/Projectiles/LazerBeam.java index 9780bbe..61c4eb1 100644 --- a/src/Projectiles/LazerBeam.java +++ b/src/Projectiles/LazerBeam.java @@ -11,6 +11,7 @@ import Utils.Direction; import Utils.Point; import Utils.Stopwatch; + import java.util.HashMap; // This class is for the lazer beam enemy that the DinosaurEnemy class shoots out diff --git a/src/Screens/CreditsScreen.java b/src/Screens/CreditsScreen.java index e8d8c3d..2dbabda 100644 --- a/src/Screens/CreditsScreen.java +++ b/src/Screens/CreditsScreen.java @@ -18,16 +18,15 @@ public CreditsScreen() { setMenuItemsAsGrid(new MenuOption[][]{ { new MenuOption( - "Hit [Escape] to go back to main menu", 100, 450, () -> GamePanel.getScreenCoordinator().setGameState(GameState.MENU)) + "Hit [Escape] to go back to main menu", 100, 450, this::backToMainMenu) } }); - setDrawables(new Drawable[]{ + setDrawables( new SpriteFont("Credits", 15, 35, "Times New Roman", 30, Color.black), new SpriteFont("Created by Alex Thimineur for Quinnipiac's SER225 Course.", 130, 140, "Times New Roman", 20, Color.black), new SpriteFont("Thank you to QU Alumni Brian Carducci, Joseph White,\nand Alex Hutman for their contributions.", 60, 220, "Times New Roman", 20, Color.black - ), - new SpriteFont("Press Space to return to the menu", 20, 560, "Times New Roman", 30, Color.black) - }); + ), new SpriteFont("Press Space to return to the menu", 20, 560, "Times New Roman", 30, Color.black) + ); } } diff --git a/src/Screens/DifficultySelectScreen.java b/src/Screens/DifficultySelectScreen.java new file mode 100644 index 0000000..fd4f09a --- /dev/null +++ b/src/Screens/DifficultySelectScreen.java @@ -0,0 +1,40 @@ +package Screens; + +import Engine.DifficultyHolder; +import Engine.GamePanel; +import Game.GameState; +import Maps.TitleScreenMap; +import Menu.Menu; +import Menu.Direction; +import Menu.MenuOption; +import Menu.MenuOption.CloseOnExecute; + +public class DifficultySelectScreen extends Menu { + + + //these values also correspond to the health given at each difficulty + private final static int NORMAL = 3; + private final static int HARD = 2; + private final static int HARDCORE = 1; + + public DifficultySelectScreen() { + MenuOption[][] menu = new MenuOption[][]{ + { + new MenuOption("Normal", 100, 150, () -> GamePanel.setDifficulty(NORMAL), CloseOnExecute.CLOSE), + new MenuOption("Hard", 320, 150, () -> GamePanel.setDifficulty(HARD), CloseOnExecute.CLOSE), + new MenuOption("Hardcore", 500, 150, () -> GamePanel.setDifficulty(HARDCORE), CloseOnExecute.CLOSE) + }, + { + new MenuOption( + "Back to Main Menu", 225, 375, () -> GamePanel.getScreenCoordinator().setGameState(GameState.MENU)) + } + }; + setBackground(new TitleScreenMap()); + setMenuItemsAsGrid(menu); + menu[0][0].setNeighborItem(menu[1][0], Direction.DOWN); + menu[0][1].setNeighborItem(menu[1][0], Direction.DOWN); + menu[0][2].setNeighborItem(menu[1][0], Direction.DOWN); + menu[1][0].setNeighborItem(menu[0][1], Direction.UP); + } + +} diff --git a/src/Screens/GameScoreScreen.java b/src/Screens/GameScoreScreen.java new file mode 100644 index 0000000..30304d4 --- /dev/null +++ b/src/Screens/GameScoreScreen.java @@ -0,0 +1,89 @@ +package Screens; + +import Engine.Drawable; +import Engine.GamePanel; +import Engine.GraphicsHandler; +import Game.TimeTracker; +import Maps.GameMaps; +import Maps.TitleScreenMap; +import Menu.Menu; +import SpriteFont.SpriteFont; +import Utils.GameTimer; +import Utils.TimeParser; +import Menu.MenuOption; + +import java.awt.*; + +public class GameScoreScreen extends Menu { + + private static final Font FONT_LEVEL, FONT_TITLE; + private static final Color COLOR_LEVEL, COLOR_TITLE; + + static { + String fontName = "Times New Roman"; + FONT_LEVEL = new Font(fontName, Font.PLAIN, 30); + FONT_TITLE = new Font(fontName, Font.BOLD,60); + COLOR_LEVEL = COLOR_TITLE = Color.WHITE; + } + + + public GameScoreScreen(TimeTracker timeTracker) { + setBackground(new TitleScreenMap()); + System.out.println(levelsToString(timeTracker)); + + SpriteFont levels = new SpriteFont(levelsToString(timeTracker), 10, 150, FONT_LEVEL, COLOR_LEVEL); + levels.setMultiLine(true); + SpriteFont title = new SpriteFont("Game Complete",200,75,FONT_TITLE,COLOR_TITLE); +// SpriteFont total = new SpriteFont(totalToString(timeTracker),300,100,FONT_TOTAL,COLOR_TOTAL); + SpriteFont difficulty = new SpriteFont(GamePanel.getDifficultyString() + " Difficulty",500,150,FONT_LEVEL,COLOR_LEVEL); + setDrawables(levels,title,difficulty); + + MenuOption close = new MenuOption("Main Menu", 550, 550,this::backToMainMenu); + setMenuItemsAsGrid(new MenuOption[][]{{close}}); + } + + private String totalToString(TimeTracker timeTracker) { + return new StringBuilder("Total: ").append(getTotalTimes(timeTracker)).toString(); + } + + private String levelsToString(TimeTracker timeTracker) { + StringBuilder stringBuilder = new StringBuilder(); + GameTimer[] gameTimers = timeTracker.getLevels(); + + for(int i = 0; i < GameMaps.MAPS.length; i++) { + if(gameTimers[i].getElapsed() > 0) { + if(!stringBuilder.isEmpty()) { + stringBuilder.append('\n'); + } + stringBuilder.append(GameMaps.MAPS[i].getName()).append(": ").append(gameTimers[i].toString()); + } + } + stringBuilder.append("\n\nTotal: ").append(getTotalTimes(timeTracker)); + + return stringBuilder.toString(); + } + + private TimeParser getTotalTimes(TimeTracker timeTracker) { + TimeParser totalTime = new TimeParser(); + for(GameTimer gameTimer : timeTracker.getLevels()) { + totalTime.addTime(gameTimer.getElapsed()); + } + return totalTime; + } + + @Deprecated //Maybe? + public class LevelScores implements Drawable { + + private int x, y, height, width; + + + public LevelScores(GameTimer[] scores, int x, int y, int height, int width) { + + } + + @Override + public void draw(GraphicsHandler graphicsHandler) { + + } + } +} diff --git a/src/Screens/LevelLoseScreen.java b/src/Screens/LevelLoseScreen.java index 413098b..e63941c 100644 --- a/src/Screens/LevelLoseScreen.java +++ b/src/Screens/LevelLoseScreen.java @@ -1,6 +1,7 @@ package Screens; import Engine.Drawable; +import Engine.GamePanel; import Engine.KeyboardAction; import Level.Player; import Level.PlayerAttack; @@ -16,17 +17,25 @@ public class LevelLoseScreen extends Menu { public LevelLoseScreen(PlayLevelScreen playLevelScreen) { super(); this.playLevelScreen = playLevelScreen; - setDrawables(new Drawable[]{ + setDrawables( new SpriteFont("You lose!", 350, 270, "Comic Sans", 30, Color.white), new SpriteFont("Press Space to try again or Escape to go back to the main menu", 120, 300, "Comic Sans", 20, Color.white) - }); + ); } public void update() { super.update(); if (KeyboardAction.GAME_RESPAWN.isDown()) { - playLevelScreen.resetLevel(); - Player.playerHealth = 3; + // if the player is in hardcore difficulty restart them at the first level otherwise restart them on the current level + if (GamePanel.getDifficulty() == 1) + { + playLevelScreen.resetHardcore(); + } + else + { + playLevelScreen.resetLevel(); + } + Player.PLAYER_HEALTH = GamePanel.getDifficulty(); PlayerAttack.dogHealth = 8; } } diff --git a/src/Screens/LevelSelectScreen.java b/src/Screens/LevelSelectScreen.java index 0eb2ae5..5e7f57b 100644 --- a/src/Screens/LevelSelectScreen.java +++ b/src/Screens/LevelSelectScreen.java @@ -5,6 +5,7 @@ import Game.ScreenCoordinator; import Maps.TitleScreenMap; import Menu.Menu; +import Menu.Direction; import Menu.MenuOption; public class LevelSelectScreen extends Menu { @@ -12,31 +13,30 @@ public class LevelSelectScreen extends Menu { private final ScreenCoordinator coordinator; public LevelSelectScreen() { + final int x_left_column = 150, x_right_column = 350; coordinator = GamePanel.getScreenCoordinator(); MenuOption[][] menu = new MenuOption[][]{ { - new MenuOption("Tutorial", 200, 100, () -> coordinator.loadLevel(0)) + new MenuOption("Tutorial", x_left_column, 100, () -> coordinator.loadLevel(0)) }, { - new MenuOption("Level One", 200, 150, () -> coordinator.loadLevel(1)) + new MenuOption("Level One", x_left_column, 150, () -> coordinator.loadLevel(1)), + new MenuOption("Level Five", x_right_column, 150, () -> coordinator.loadLevel(5)) }, { - new MenuOption("Level Two", 200, 200, () -> coordinator.loadLevel(2)) + new MenuOption("Level Two", x_left_column, 200, () -> coordinator.loadLevel(2)), + new MenuOption("Level Six", x_right_column, 200, () -> coordinator.loadLevel(6)) }, { - new MenuOption("Level Three", 200, 250, () -> coordinator.loadLevel(3)) + new MenuOption("Level Three", x_left_column, 250, () -> coordinator.loadLevel(3)), + new MenuOption("Level Seven", x_right_column, 250, () -> coordinator.loadLevel(7)) }, { - new MenuOption("Level Four", 200, 300, () -> coordinator.loadLevel(4)) - }, { - new MenuOption("Level Five", 200, 350, () -> coordinator.loadLevel(5)) - }, { - new MenuOption("Level Six", 200, 400, () -> coordinator.loadLevel(6)) - }, { - new MenuOption("Level Seven", 200, 450, () -> coordinator.loadLevel(7)) - }, { - new MenuOption("Boss Battle", 400, 100, () -> coordinator.loadLevel(8)) + new MenuOption("Level Four", x_left_column, 300, () -> coordinator.loadLevel(4)), + new MenuOption("Boss Battle", x_right_column, 300, () -> coordinator.loadLevel(8)) }, { new MenuOption( - "Hit [Escape] to go back to main menu", 100, 550, () -> GamePanel.getScreenCoordinator().setGameState(GameState.MENU)) + "Back to Main Menu", x_left_column, 375, this::backToMainMenu) } }; + menu[4][1].setNeighborItem(menu[5][0], Direction.DOWN); + menu[1][1].setNeighborItem(menu[0][0],Direction.UP); setBackground(new TitleScreenMap()); setMenuItemsAsGrid(menu); } diff --git a/src/Screens/MenuScreen.java b/src/Screens/MenuScreen.java index 0d9568d..5bf6a4c 100644 --- a/src/Screens/MenuScreen.java +++ b/src/Screens/MenuScreen.java @@ -3,6 +3,7 @@ import Engine.GamePanel; import Game.GameState; import Maps.TitleScreenMap; +import Menu.Direction; import Menu.Menu; import Menu.MenuOption; @@ -10,18 +11,23 @@ public class MenuScreen extends Menu { public MenuScreen() { super(); - setMenuItemsAsGrid(new MenuOption[][]{ - { - new MenuOption("PLAY GAME", 80, 100, () -> GamePanel.getScreenCoordinator().setGameState(GameState.LEVEL)), - new MenuOption("LEVEL SELECT", 350, 100, () -> GamePanel.getScreenCoordinator().setGameState(GameState.LEVELSELECT)) - }, { - new MenuOption("CREDITS", 80, 200, () -> GamePanel.getScreenCoordinator().setGameState(GameState.CREDITS)), - new MenuOption("NARRATIVE", 350, 200, () -> GamePanel.getScreenCoordinator().setGameState(GameState.OPENING)) - }, { - new MenuOption("INSTRUCTIONS", 80, 300, () -> GamePanel.getScreenCoordinator().setGameState(GameState.INSTRUCTIONS)), - new MenuOption("OPTIONS", 350, 300, () -> GamePanel.getScreenCoordinator().setGameState(GameState.OPTIONS)) - } - }); + MenuOption[][] menu = new MenuOption[][]{ + { + new MenuOption("PLAY GAME", 60, 100, () -> GamePanel.getScreenCoordinator().setGameState(GameState.LEVEL)), + new MenuOption("LEVEL SELECT", 392, 100, () -> GamePanel.getScreenCoordinator().setGameState(GameState.LEVELSELECT)) + }, { + new MenuOption("CREDITS", 60, 200, () -> GamePanel.getScreenCoordinator().setGameState(GameState.CREDITS)), + new MenuOption("NARRATIVE", 392, 200, () -> GamePanel.getScreenCoordinator().setGameState(GameState.OPENING)) + }, { + new MenuOption("INSTRUCTIONS", 60, 300, () -> GamePanel.getScreenCoordinator().setGameState(GameState.INSTRUCTIONS)), + new MenuOption("OPTIONS", 392, 300, () -> GamePanel.getScreenCoordinator().setGameState(GameState.OPTIONS)) + }, + { + new MenuOption("SELECT DIFFICULTY", 60, 400, () -> GamePanel.getScreenCoordinator().setGameState(GameState.DIFFICULTYSELECT)), + new MenuOption("QUIT", 392, 400, () -> System.exit(0)) + } + }; + setMenuItemsAsGrid(menu); setBackground(new TitleScreenMap()); } } diff --git a/src/Screens/OptionsScreen.java b/src/Screens/OptionsScreen.java index 0c4ffc1..d678ed4 100644 --- a/src/Screens/OptionsScreen.java +++ b/src/Screens/OptionsScreen.java @@ -4,25 +4,21 @@ import Engine.GamePanel; import Engine.GraphicsHandler; import Engine.ImageLoader; +import Game.GameState; import GameObject.SpriteSheet; import Maps.LevelSelectMap; import Menu.Direction; import Menu.Menu; import Menu.MenuOption; import Players.Avatar; -import Players.Cat; import java.awt.*; import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; - -import javax.imageio.ImageIO; public class OptionsScreen extends Menu { private MenuOption[][] items; private BufferedImage cat; - public static int catColor; + public int catColor = 0; public OptionsScreen() { setBackground(new LevelSelectMap()); @@ -40,7 +36,7 @@ public OptionsScreen() { new MenuOption("Blue",500,300, () -> Config.playerAvatar = Avatar.CAT_BLUE), new MenuOption("Green",630,300, () -> Config.playerAvatar = Avatar.CAT_GREEN) },{ - new MenuOption("Hit [Escape] to go back to main menu",100,450) + new MenuOption("Hit [Escape] to go back to main menu",100,450, () -> GamePanel.getScreenCoordinator().setGameState(GameState.MENU)) } }; setMenuItemsAsGrid(items); @@ -61,17 +57,17 @@ public OptionsScreen() { public void draw(GraphicsHandler handler) { super.draw(handler); - if(items[1][1].isSelected()) { + if(Config.playerAvatar == Avatar.CAT_ORANGE) { SpriteSheet orange = new SpriteSheet(ImageLoader.load("Cat.png"), 24, 24); cat = orange.getSprite(0, 0); handler.drawImage(cat, 210, 225, 100, 100); } - else if(items[1][2].isSelected()) { + else if(Config.playerAvatar == Avatar.CAT_BLUE) { SpriteSheet blue = new SpriteSheet(ImageLoader.load("CatBlue.png"), 24, 24); cat = blue.getSprite(0, 0); handler.drawImage(cat, 210, 225, 100, 100); } - else if(items[1][3].isSelected()) { + else if(Config.playerAvatar == Avatar.CAT_GREEN) { SpriteSheet green = new SpriteSheet(ImageLoader.load("CatGreen.png"), 24, 24); cat = green.getSprite(0, 0); handler.drawImage(cat, 210, 225, 100, 100); diff --git a/src/Screens/PauseScreen.java b/src/Screens/PauseScreen.java index 288b477..d3963ab 100644 --- a/src/Screens/PauseScreen.java +++ b/src/Screens/PauseScreen.java @@ -21,18 +21,18 @@ public class PauseScreen extends Menu { PAUSE_INSTRUCTIONS = new SpriteFont("SOMETHING SOMETHING ", 350, 250, "Comic Sans", 30, Color.white); } - private final PlayLevelScreen playLevelScreen; + private final Pausable parent; private boolean menuEscape; - public PauseScreen(Map map, Player player, PlayLevelScreen playLevelScreen) { - this.playLevelScreen = playLevelScreen; + public PauseScreen(Map map, Player player, Pausable parent) { + this.parent = parent; menuEscape = false; setDrawables(new Drawable[]{ player, map }); setMenuItemsAsGrid(new MenuOption[][]{ { - new MenuOption("Return to Game", 100, 100, () -> this.playLevelScreen.resume()) + new MenuOption("Return to Game", 100, 100, this.parent::resume) }, { new MenuOption("Back to Menu", 100, 200, () -> GamePanel.getScreenCoordinator().setGameState(GameState.MENU)) } @@ -45,7 +45,7 @@ public PauseScreen(Map map, Player player, PlayLevelScreen playLevelScreen) { public void update() { updateMenu(); if (menuEscape && KeyboardAction.GAME_PAUSE.isDown()) { - playLevelScreen.resume(); + parent.resume(); } if (!KeyboardAction.GAME_PAUSE.isDown()) { menuEscape = true; diff --git a/src/Screens/PlayLevelScreen.java b/src/Screens/PlayLevelScreen.java index 5d397f3..6da04ef 100644 --- a/src/Screens/PlayLevelScreen.java +++ b/src/Screens/PlayLevelScreen.java @@ -2,42 +2,32 @@ import Engine.*; import Game.GameState; +import Game.TimeTracker; import Level.Map; import Level.Player; import Level.PlayerListener; -import Maps.*; +import Maps.GameMaps; import SpriteFont.SpriteFont; import Utils.Stopwatch; import java.awt.*; import java.awt.event.MouseEvent; -public class PlayLevelScreen extends Screen implements PlayerListener { +public class PlayLevelScreen extends Screen implements PlayerListener, Pausable { - private static final MapFactory[] MAPS; private static final Stopwatch screenTimer; private static final KeyLocker keyLocker; - private static final SpriteFont SPRITE_FONT_PAUSE; private static final SpriteFont[] SPRITE_FONT_INSTRUCTIONS; private static final Color COLOR_GREY_BACKGROUND; private static Map loadedMap; private static Screen alternateScreen; private static Player player; + private static TimeTracker timeTracker; static { screenTimer = new Stopwatch(); keyLocker = new KeyLocker(); - /* - * List of maps in the game, each map is given a constructor - * This is some new java funky stuff :D - */ - MAPS = new MapFactory[]{ - TestTutorial::new, TestMap::new, TestMap2::new, TestMap3::new, TestMap4::new, TestMap5::new, TestMap6::new, TestMap7::new, BossBattle::new - }; - - SPRITE_FONT_PAUSE = new SpriteFont("Pause", 350, 250, "Comic Sans", 30, Color.white); - SPRITE_FONT_INSTRUCTIONS = new SpriteFont[] { new SpriteFont("To JUMP: UP arrow key, or 'W', or SPACEBAR", 130, 140, "Times New Roman", 20, Color.white), @@ -64,7 +54,7 @@ public class PlayLevelScreen extends Screen implements PlayerListener { COLOR_GREY_BACKGROUND = new Color(0, 0, 0, 100); } - private State screenState; + private State screenState = State.RUNNING; private int currentMap; public PlayLevelScreen() { @@ -79,8 +69,8 @@ public PlayLevelScreen(int initialMap) { @Override public void initialize() { + timeTracker = new TimeTracker(); loadMap(currentMap); - screenState = State.RUNNING; } @Override @@ -88,9 +78,10 @@ public void update() { switch (screenState) { case RUNNING -> { if (KeyboardAction.GAME_PAUSE.isDown() && !keyLocker.isActionLocked(KeyboardAction.GAME_PAUSE)) { - screenState = State.PAUSE; + pause(); } else if (KeyboardAction.GAME_INSTRUCTIONS.isDown() && !keyLocker.isActionLocked(KeyboardAction.GAME_INSTRUCTIONS)) { screenState = State.INSTRUCTIONS; + timeTracker.stop(); } else { player.update(); loadedMap.update(player); @@ -101,19 +92,23 @@ public void update() { screenState = State.RUNNING; } } - case PAUSE,LEVEL_LOSE_MESSAGE -> alternateScreen.update(); + case PAUSE,LEVEL_LOSE_MESSAGE,GAME_COMPLETED -> { + if(alternateScreen != null) { + alternateScreen.update(); + } + } case PLAYER_DEAD -> screenState = State.LEVEL_LOSE_MESSAGE; case LEVEL_COMPLETED -> { alternateScreen = new LevelClearedScreen(); alternateScreen.initialize(); - screenTimer.setWaitTime(2500); + screenTimer.setWaitTime(750); screenState = State.LEVEL_WIN_MESSAGE; } case LEVEL_WIN_MESSAGE -> { alternateScreen.update(); if (screenTimer.isTimeUp()) { - nextLevel(); screenState = State.RUNNING; + nextLevel(); } } } @@ -127,6 +122,7 @@ public void draw(GraphicsHandler graphicsHandler) { alternateScreen = null; loadedMap.draw(graphicsHandler); player.draw(graphicsHandler); + timeTracker.draw(graphicsHandler); } case LEVEL_WIN_MESSAGE -> { if (!(alternateScreen instanceof LevelClearedScreen)) { @@ -134,6 +130,7 @@ public void draw(GraphicsHandler graphicsHandler) { alternateScreen.initialize(); } alternateScreen.draw(graphicsHandler); + timeTracker.draw(graphicsHandler); } case LEVEL_LOSE_MESSAGE -> { if (!(alternateScreen instanceof LevelLoseScreen)) { @@ -141,6 +138,7 @@ public void draw(GraphicsHandler graphicsHandler) { alternateScreen.initialize(); } alternateScreen.draw(graphicsHandler); + timeTracker.draw(graphicsHandler); } case PAUSE -> { if (!(alternateScreen instanceof PauseScreen)) { @@ -148,6 +146,7 @@ public void draw(GraphicsHandler graphicsHandler) { alternateScreen.initialize(); } alternateScreen.draw(graphicsHandler); + timeTracker.draw(graphicsHandler); } case INSTRUCTIONS -> { loadedMap.draw(graphicsHandler); @@ -157,9 +156,20 @@ public void draw(GraphicsHandler graphicsHandler) { } graphicsHandler.drawFilledRectangle(0, 0, ScreenManager.getScreenWidth(), ScreenManager.getScreenHeight(), COLOR_GREY_BACKGROUND); } + case GAME_COMPLETED -> { + if(!(alternateScreen instanceof GameScoreScreen)) { + alternateScreen = new GameScoreScreen(timeTracker); + alternateScreen.initialize(); + } + alternateScreen.draw(graphicsHandler); + } } } + public Map getLoadedMap() { + return loadedMap; + } + @Override public void mouseClicked(MouseEvent e) { if (alternateScreen != null) { @@ -176,11 +186,11 @@ public void nextLevel() { * * @param index index of map to load */ - private void loadMap(int index) { - if(index < MAPS.length) { + public void loadMap(int index) { + if(index < GameMaps.MAPS.length) { currentMap = index; //Load map using the MapFactory - loadedMap = MAPS[index].generateMap(); + loadedMap = GameMaps.MAPS[index].generateMap(); loadedMap.reset(); //Load the cat using the Config setting @@ -188,8 +198,21 @@ private void loadMap(int index) { player.setMap(loadedMap); player.addListener(this); screenState = State.RUNNING; + + //Update Time + timeTracker.setCurrentLevel(index); } else { - GamePanel.getScreenCoordinator().setGameState(GameState.MENU); + //Add the other menu + System.out.println("complete game"); + screenState = State.GAME_COMPLETED; + } + } + + @Override + public void onLevelFinished() { + //Only complete on final level + if(currentMap == GameMaps.MAPS.length - 1) { + timeTracker.stop(); } } @@ -211,18 +234,27 @@ public void backToMenu() { GamePanel.getScreenCoordinator().setGameState(GameState.MENU); } + @Override public void resume() { if (screenState == State.PAUSE || screenState == State.INSTRUCTIONS) { screenState = State.RUNNING; + timeTracker.start(); } } - public enum State { - RUNNING, LEVEL_COMPLETED, PLAYER_DEAD, LEVEL_WIN_MESSAGE, LEVEL_LOSE_MESSAGE, LEVEL_SELECT, PAUSE, INSTRUCTIONS, OPTIONS + @Override + public void pause() { + screenState = State.PAUSE; + timeTracker.stop(); + } + + public void resetHardcore() { + timeTracker = new TimeTracker(); + loadMap(0); } - private interface MapFactory { - Map generateMap(); + public enum State { + RUNNING, LEVEL_COMPLETED, PLAYER_DEAD, LEVEL_WIN_MESSAGE, LEVEL_LOSE_MESSAGE, PAUSE, INSTRUCTIONS, GAME_COMPLETED } } diff --git a/src/SpriteFont/SpriteFont.java b/src/SpriteFont/SpriteFont.java index 217e97b..395d8c4 100644 --- a/src/SpriteFont/SpriteFont.java +++ b/src/SpriteFont/SpriteFont.java @@ -4,9 +4,11 @@ import Engine.GraphicsHandler; import java.awt.*; -import java.awt.font.FontRenderContext; +import java.util.Arrays; // This class represents a sprite font, which is graphic text (text drawn to the screen as if it were an image) + + public class SpriteFont implements Drawable { protected String text; protected Font font; @@ -16,20 +18,33 @@ public class SpriteFont implements Drawable { protected Color color; protected Color outlineColor; protected float outlineThickness = 1f; + /** + * Whether to automatically multi-line the text if `\n` is present + */ + protected boolean multiLine = false, containsNewLines; public SpriteFont(String text, float x, float y, String fontName, int fontSize, Color color) { + this(text,x,y,new Font(fontName,Font.PLAIN,fontSize),color); + } + + public SpriteFont(String text, float x, float y, Font font, Color color) { this.text = text; - font = new Font(fontName, Font.PLAIN, fontSize); + this.font = font; this.x = x; this.y = y; this.color = color; updateDimensions(); + updateContainsNewLines(); } public void setColor(Color color) { this.color = color; } + public Color getColor() { + return color; + } + public String getText() { return text; } @@ -37,6 +52,11 @@ public String getText() { public void setText(String text) { this.text = text; updateDimensions(); + updateContainsNewLines(); + } + + public void updateContainsNewLines() { + containsNewLines = text.contains("\n"); } public void setFontName(String fontName) { @@ -108,11 +128,17 @@ public void moveUp(float dy) { y -= dy; } + + public void draw(GraphicsHandler graphicsHandler) { - if (outlineColor != null && !outlineColor.equals(color)) { - graphicsHandler.drawStringWithOutline(text, Math.round(x), Math.round(y), font, color, outlineColor, outlineThickness); + if(multiLine && containsNewLines) { + drawWithParsedNewLines(graphicsHandler); } else { - graphicsHandler.drawString(text, Math.round(x), Math.round(y), font, color); + if (outlineColor != null && !outlineColor.equals(color)) { + graphicsHandler.drawStringWithOutline(getText(), Math.round(x), Math.round(y), font, color, outlineColor, outlineThickness); + } else { + graphicsHandler.drawString(getText(), Math.round(x), Math.round(y), font, color); + } } } @@ -126,7 +152,7 @@ public void drawWithParsedNewLines(GraphicsHandler graphicsHandler) { if(width == -1) { FontMetrics metrics = graphicsHandler.getGraphics2D().getFontMetrics(font); height = metrics.getHeight(); - width = metrics.stringWidth(text); + width = metrics.stringWidth(getText()); } @@ -147,6 +173,14 @@ public void updateDimensions() { height = -1; } + public void setMultiLine(boolean multiLine) { + this.multiLine = multiLine; + } + + public boolean isMultiLine() { + return multiLine; + } + public boolean contains(Point point) { return point.x > x && point.y > y && point.x < x + width && point.y < y + height; } diff --git a/src/SpriteFont/TimerElement.java b/src/SpriteFont/TimerElement.java new file mode 100644 index 0000000..5f8d8d1 --- /dev/null +++ b/src/SpriteFont/TimerElement.java @@ -0,0 +1,38 @@ +package SpriteFont; + +import Utils.TimeParser; + +import java.awt.*; + +@Deprecated +public class TimerElement extends SpriteFont { + + private TimeParser timeParser; + + public TimerElement(String text, float x, float y, String fontName, int fontSize, Color color) { + super(text, x, y, fontName, fontSize, color); + } + + public TimerElement(TimeParser timeParser, float x, float y, String fontName, int fontSize, Color color) { + super(null,x,y,fontName,fontSize,color); + this.timeParser= timeParser; + } + + public TimerElement(TimeParser timeParser, float x, float y, Font font, Color color) { + super(null,x,y,font,color); + this.timeParser = timeParser; + } + + public TimeParser getGameTimer() { + return timeParser; + } + + public void setGameTimer(TimeParser timeParser) { + this.timeParser = timeParser; + } + + @Override + public String getText() { + return timeParser != null ? timeParser.toString() : "NO TIME SPECIFIED"; + } +} diff --git a/src/Tilesets/CommonTileset.java b/src/Tilesets/CommonTileset.java index ff3e500..a050f3f 100644 --- a/src/Tilesets/CommonTileset.java +++ b/src/Tilesets/CommonTileset.java @@ -228,6 +228,41 @@ public ArrayList defineTiles() { .withTileType(TileType.LETHAL); mapTiles.add(lethalSpikeTile); + + Frame floatingPlatformFrame = new FrameBuilder(getSubImage(3, 4), 0) + .withScale(tileScale) + .withBounds(0, 6, 16, 4) + .build(); + + MapTileBuilder floatingPlatformTile = new MapTileBuilder(floatingPlatformFrame) + .withTileType(TileType.JUMP_THROUGH_PLATFORM); + + mapTiles.add(floatingPlatformTile); + + + // Sand + Frame sandFrame = new FrameBuilder(getSubImage(4, 0), 0) + .withScale(tileScale) + .build(); + + MapTileBuilder sandTile = new MapTileBuilder(sandFrame) + .withTileType(TileType.NOT_PASSABLE); + + mapTiles.add(sandTile); + + + // solid floating block + Frame solidFloatingFrame = new FrameBuilder(getSubImage(3, 5), 0) + .withScale(tileScale) + .withBounds(0, 6, 16, 3) + .build(); + + MapTileBuilder solidFloatingTile = new MapTileBuilder(solidFloatingFrame) + .withTileType(TileType.NOT_PASSABLE); + + mapTiles.add(solidFloatingTile); + + return mapTiles; } } diff --git a/src/Utils/AirGroundState.java b/src/Utils/AirGroundState.java deleted file mode 100644 index e8b756a..0000000 --- a/src/Utils/AirGroundState.java +++ /dev/null @@ -1,7 +0,0 @@ -package Utils; - -// Simple enum for keeping track of it something is on the ground or in the air -// I don't like the name of this enum, but I couldn't think of a better name... -public enum AirGroundState { - GROUND, AIR -} diff --git a/src/Utils/GameTimer.java b/src/Utils/GameTimer.java new file mode 100644 index 0000000..e18878c --- /dev/null +++ b/src/Utils/GameTimer.java @@ -0,0 +1,43 @@ +package Utils; + +public class GameTimer extends TimeParser { + + private long startTime, endTime; + private boolean running; + + public GameTimer() { + startTime = 0; + endTime = 0; + time = 0; + running = false; + } + + public void start() { + if(!running) { + time = getElapsed(); + startTime = System.currentTimeMillis(); + running = true; + } + } + + public void stop() { + if(running) { + endTime = System.currentTimeMillis(); + running = false; + } + } + + public void reset() { + time = 0; + startTime = System.currentTimeMillis(); + } + + public long getElapsed() { + return (running ? System.currentTimeMillis() : endTime ) + time - startTime; + } + + @Override + public long getTime() { + return getElapsed(); + } +} diff --git a/src/Utils/Stopwatch.java b/src/Utils/Stopwatch.java index a57f11f..7bae0d1 100644 --- a/src/Utils/Stopwatch.java +++ b/src/Utils/Stopwatch.java @@ -4,16 +4,18 @@ public class Stopwatch { private long beforeTime = System.currentTimeMillis(); private int millisecondsToWait = 0; + private boolean timeUp = true; // tell stopwatch how many milliseconds to "time" public void setWaitTime(int millisecondsToWait) { this.millisecondsToWait = millisecondsToWait; beforeTime = System.currentTimeMillis(); + timeUp = false; } // will return true or false based on if the "time" is up (a specified number of milliseconds have passed) public boolean isTimeUp() { - return System.currentTimeMillis() - beforeTime > millisecondsToWait; + return timeUp || (timeUp = System.currentTimeMillis() - beforeTime > millisecondsToWait); } // reset timer to wait again for specified number of milliseconds diff --git a/src/Utils/TimeParser.java b/src/Utils/TimeParser.java new file mode 100644 index 0000000..7d7b729 --- /dev/null +++ b/src/Utils/TimeParser.java @@ -0,0 +1,78 @@ +package Utils; + +public class TimeParser { + private static final long MS_PER_SECOND, SECOND_PER_MINUTE, MINUTE_PER_HOUR, MS_PER_HOUR, MS_PER_MINUTE; + + protected long time; + + static { + MS_PER_SECOND = 1000; + SECOND_PER_MINUTE = 60; + MINUTE_PER_HOUR = 60; + MS_PER_MINUTE = MS_PER_SECOND * SECOND_PER_MINUTE; + MS_PER_HOUR = MS_PER_MINUTE * MINUTE_PER_HOUR; + } + + public TimeParser() { + this(0); + } + + public TimeParser(long time) { + this.time = time; + } + + public void setTime(long time) { + this.time = time; + } + + public long getTime() { + return time; + } + + public void addTime(long time) { + this.time += time; + } + + public void subtractTime(long time) { + this.time -= time; + } + + /** + * Converts the time to a readable string + * @return + */ + public String toString() { + /* + Implementation Note: + While yes, there is a more efficient method... I did it this way + Refrain from changing as the ' ' characters are important for rendering in the right spot + */ + long elapsed = getTime(); + long hours = elapsed / MS_PER_HOUR; + long minutes = (elapsed %= MS_PER_HOUR) / MS_PER_MINUTE; + long seconds = (elapsed %= MS_PER_MINUTE) / MS_PER_SECOND; + elapsed %= MS_PER_SECOND; + + StringBuilder stringBuilder = new StringBuilder(); + + if(hours > 0) { + if(hours > 9) { + stringBuilder.append((char) ('0' + hours / 10)); + } + stringBuilder.append((char) ('0' + hours % 10)).append(':').append((char) ('0' + minutes / 10)).append((char) ('0' + minutes % 10)).append(':'); + stringBuilder.append((char) ('0' + seconds / 10)); + } else if(minutes > 0) { + if(minutes > 9) { + stringBuilder.append((char) ('0' + minutes / 10)); + } + stringBuilder.append((char) ('0' + minutes % 10)).append(':'); + stringBuilder.append((char) ('0' + seconds / 10)); + } else if(seconds > 9) { + stringBuilder.append((char) ('0' + seconds / 10)); + } + stringBuilder.append((char) ('0' + seconds % 10)).append('.'); + stringBuilder.append((char) ('0' + elapsed / 100)).append((char) ('0' + (elapsed / 10) % 10)); + + return stringBuilder.toString(); + } +}