diff --git a/.gitignore b/.gitignore index a1c2a23..943cdce 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,8 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + +# build files +build +.gradle +/save/* diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..87495c1 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Project_Ruby \ No newline at end of file diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml new file mode 100644 index 0000000..dcf1d4d --- /dev/null +++ b/.idea/checkstyle-idea.xml @@ -0,0 +1,15 @@ + + + + 10.5.0 + JavaOnly + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..611e7c8 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..01a81b7 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..fdc392f --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/json_simple_1_1_1.xml b/.idea/libraries/json_simple_1_1_1.xml new file mode 100644 index 0000000..2f7b710 --- /dev/null +++ b/.idea/libraries/json_simple_1_1_1.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/mongodb_driver_sync.xml b/.idea/libraries/mongodb_driver_sync.xml new file mode 100644 index 0000000..c21ab3f --- /dev/null +++ b/.idea/libraries/mongodb_driver_sync.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..c9dbdd7 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c589392 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/Project_Ruby.iml b/.idea/modules/Project_Ruby.iml new file mode 100644 index 0000000..9e3449c --- /dev/null +++ b/.idea/modules/Project_Ruby.iml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules/Project_Ruby.main.iml b/.idea/modules/Project_Ruby.main.iml new file mode 100644 index 0000000..9e3449c --- /dev/null +++ b/.idea/modules/Project_Ruby.main.iml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules/Project_Ruby.test.iml b/.idea/modules/Project_Ruby.test.iml new file mode 100644 index 0000000..4e871d1 --- /dev/null +++ b/.idea/modules/Project_Ruby.test.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 39f2fd3..fe567bd 100644 --- a/README.md +++ b/README.md @@ -1,115 +1,50 @@ -# Project +# Project Ruby +img1 +img2 +img3 +Screen Shot 2023-04-09 at 8 55 28 PM -## Technical Project Requirements -The minimum requirements for the project are outlined here to give you a starting point. Meeting the minimum requirements alone will not guarantee you a good mark. You are welcome to meet and exceed the minimum requirements if you have good, creative ideas and would like to discuss them with me. +# Project Pitch +Ruby Rush is a open world game where players find themselves running from slimes and avoiding elements as they search for rubies. -**Requirement 1**: The project must incorporate some visual interface using Processing.org libraries. All user interaction must be conducted via this interface. +# Instructions +1. Run Main.java +2. Follow the Menu interface, using mouse and keyboard inputs, to set a username that will be used to get your saved game, and begin. +3. Collect Rubies and don't get hit by monsters by using the WASD keys. +4. Game elements will teleport at random every couple of seconds. +5. Survive as long as you can. -**Requirement 2**: The project must incorporate some kind of non-blocking concurrent/asynchronous processing that happens at regular intervals. For example, you might push or fetch data from in the background. -**Requirement 3**: The project must incorporate some kind of non-trivial persistent data state that must be read, processed, and written at regular intervals. For example, you might save a game state in a JSON file. This may or may not be included with Requirement 2. +# Requirements +**Requirement 1:** +We used Java Graphics to draw all of our user interfaces, which has similar methods to Processing.org. Then it will be drawn in a JPanel. -**Requirement 4**: The project must incorporate some kind of self-managing custom iterable data structure. For example, you might have a collection of enemies that are added and deleted based on statistics maintained by the data structure. +**Requirement 2:** +We used asynchronous processing to save the game each time each time the player encounters a ruby. -**Requirement 5**: The project must be well-documented, complete, and run without errors on final submission. +**Requirement 3:** +The gamestate is stored as a JSON file, containing two key value pairs of gamePanelData and playerData. The value for gamePanelData is another JSONObject consisting of 3 arrays. -## Project Pitch (group, 1%) +**Requirement 4:** +We created a custom data structure based on a network of nodes to utilize instead of the int-map array in the PathFinder class. This structure stores references to all adjacent nodes (up, down, left, and right) of the current node, making it more efficient for accessing adjacent nodes. The focus of this structure is on efficiency, and at present is only being utilized in Pathfinder where it serves its purpose quite good. -The project pitch will be a short document that describes the kind of interactive application you would like to create with your group. The project pitch must include the following items: +**Requirement 5:** We have added javadoc, comments and provided a UML diagram. -*One-liner*: One-sentence description of your project. -*Outline*: 1-10 sentences that describe how your project will fulfill the project requirements. -*Communication policies*: A description of how your group will meet, communicate, and make decisions (as per Lab 03). -*Roles and responsibilities*: A description of each team member's jobs in the group. -*Milestones*: A rough outline of the major project milestones that you expect to complete and your own estimated timeline. This can and will change, so do your best to estimate and plan for the milestones to change. +# Contributions +Abhishek +- Player, Entity, UI, KeyHandler, UI, Life and Miscellaneous -Draft was due today, final due next lab. Submission here, on GitHub. Make a `.md` file that outlines the above. +Amrit +- Villager, Monster, Node, Pathfinder, ElementHandler and testing -## Initial UML Diagrams (group, 1%) +Nathan +- GamePanel, TileManager, Tile classes, refactoring, design -The initial UML diagrams will outline the class structure that your group will follow for the first milestone of the project. It must include the following items all classes that will be created by the group and important descriptive interfaces from either the Java library or created by the group. I expect that this will change significantly throughout the project, so it does not have to be perfect but it should be a best effort attempt. This is because you will use this to communicate with your group members about what to make. Therefore, the diagram should be *sufficiently complex* to give you a term's worth of work. +Simrat +- Objects package, CollisionDetector, Element, ElementHandler, sound and testing -Draft due next Lab, final due two labs from now. Submission here, on GitHub. Suggestion is to use a tool like [draw.io](https://app.diagrams.net/) but you may use whatever tool is most useful for you. - -## Initial GitHub Issues (group, 1%) - -The initial GitHub issues will be the tasks that are assigned to each of your group members at the beginning of the project. Every team member should have at least five issues to start (20-30 total). You will have to decide within your group how granular you want to make these issues. - -Issues will be tracked here, on GitHub. - -## Final Project Demo (group, 1%) - -The Final Project Demo will be a working version of your project that you will present to your lab section for review. The demo will be in lab, and will include a live demo of the working application, and a short code review. There are no slides required, but you should have practiced your demo to make sure it will run reasonably well. This will be during the last lab of class. - -No submission. - -## Final README.md (group, 1%) - -The Final README.md must give instructions on how to run your program, a list of contributions by each member, and any references/citations for code you may have used from elsewhere. - -Submission is here, on GitHub, in the `README.md` file. - -## Final Product (group, 5%) - -The Final Product will be evaluated for overall code design and documentation and evaluated on the same design principles as individual contributions. If you are below the 1000 line minimum contribution, your mark will be scaled down for this portion. - -Submission is here, on GitHub. - -## Code Contributions (individual, 15%) - -You will be expected to take on a significant individual contribution to the group project (at least 1000 lines of non-trivial code). It may be in a number of forms, but here are some examples: - -**Architect**: you are in charge of the high-level code structuring and organizing. - -**Test maker**: you are in charge of test coverage that supports other group members. - -**UI/UX lead**: you design and implement the user interface. - -**Backend**: you design and implement the data structures. - -**…??**: Make up your own depending on your use case, i.e., collision system designer, animation architect, async code wrangler. - -Contributions must be for functional, working Java code and must be continuous throughout the term. You may not, for example, push all of your changes at the end of term. Code will be marked on following good design principles, i.e., SOLID, design patterns, etc. You are encouraged to work together and use pair programming for components, but you will be marked on your contribution to your own modules individually. - -## Documentation Contributions (individual, 5%) - -Your code must be well-documented with fully-formed method signatures, comments, and necessary README or Wiki pages. This is further broken down into the following. - -### Initial individual pitch (1%) -A description of your individual feature that you plan to implement. - -Due date TBD. - -### Initial individual UML Diagrams (1%) -Any combination of sequence, communcation, or class diagrams that describe your feature's initial planned abilities. - -Due date TBD. - -### Documentation contributions (3%) -Your personal feature documentation, wherever it happens to show up in the final documentation. - -Due with final submission. - -## Issues and Pull Request Contributions (individual, 5%) -You must track your own work in the form of creating and closing GitHub issues, creating and reviewing pull requests, responding to issues that have been assigned to you, and creating issues that you assign to others (all within reason). - -# Errata -The project MUST be managed here, in this GitHub repo. Nothing that happens outside of this GitHub repo will be trackable by me, therefore, it will not be marked or considered for marking. - -You must use the following branching structures: -- `main` branch must always be working, tested, debugged, human-readable code. -- `__` is the format for each ISSUE that you're working on. It should always be a branch off of `main`. -- Every individual branch must regularly merge `main`, and it should be no more than a few days before your branch is either merged into `main` or deleted. - -You must use pull requests to manage your code integration: -- make your branch from `main`, e.g., `git checkout -b pb_71_demo` -- make your commits, e.g., `git add .` and `git commit -m "fixed issue 71 by reloading gradle for the 100th time"` and `git push origin pb_71_demo`. -- merge `main` into your branch, and NOT the other way around. E.g., `git merge main`. YOU fix all the merge conflicts or problems that arise. Commit and push again. -- go to GitHub.com and make a pull request -- one other person (NOT YOU) needs to review the code and either approve or reject your changes with detailed line-by-line comments. -- If needed, make the requested fixes and commit and push again. -- the other person (NOT YOU) will merge your PR into `main` -- the other person (NOT YOU) will delete your branch +Greg Song +- GameLauncher, SaveState, SaveStateHandler, Menu package diff --git a/README1.md b/README1.md new file mode 100644 index 0000000..39f2fd3 --- /dev/null +++ b/README1.md @@ -0,0 +1,115 @@ +# Project + +## Technical Project Requirements + +The minimum requirements for the project are outlined here to give you a starting point. Meeting the minimum requirements alone will not guarantee you a good mark. You are welcome to meet and exceed the minimum requirements if you have good, creative ideas and would like to discuss them with me. + +**Requirement 1**: The project must incorporate some visual interface using Processing.org libraries. All user interaction must be conducted via this interface. + +**Requirement 2**: The project must incorporate some kind of non-blocking concurrent/asynchronous processing that happens at regular intervals. For example, you might push or fetch data from in the background. + +**Requirement 3**: The project must incorporate some kind of non-trivial persistent data state that must be read, processed, and written at regular intervals. For example, you might save a game state in a JSON file. This may or may not be included with Requirement 2. + +**Requirement 4**: The project must incorporate some kind of self-managing custom iterable data structure. For example, you might have a collection of enemies that are added and deleted based on statistics maintained by the data structure. + +**Requirement 5**: The project must be well-documented, complete, and run without errors on final submission. + +## Project Pitch (group, 1%) + +The project pitch will be a short document that describes the kind of interactive application you would like to create with your group. The project pitch must include the following items: + +*One-liner*: One-sentence description of your project. +*Outline*: 1-10 sentences that describe how your project will fulfill the project requirements. +*Communication policies*: A description of how your group will meet, communicate, and make decisions (as per Lab 03). +*Roles and responsibilities*: A description of each team member's jobs in the group. +*Milestones*: A rough outline of the major project milestones that you expect to complete and your own estimated timeline. This can and will change, so do your best to estimate and plan for the milestones to change. + +Draft was due today, final due next lab. Submission here, on GitHub. Make a `.md` file that outlines the above. + +## Initial UML Diagrams (group, 1%) + +The initial UML diagrams will outline the class structure that your group will follow for the first milestone of the project. It must include the following items all classes that will be created by the group and important descriptive interfaces from either the Java library or created by the group. I expect that this will change significantly throughout the project, so it does not have to be perfect but it should be a best effort attempt. This is because you will use this to communicate with your group members about what to make. Therefore, the diagram should be *sufficiently complex* to give you a term's worth of work. + +Draft due next Lab, final due two labs from now. Submission here, on GitHub. Suggestion is to use a tool like [draw.io](https://app.diagrams.net/) but you may use whatever tool is most useful for you. + +## Initial GitHub Issues (group, 1%) + +The initial GitHub issues will be the tasks that are assigned to each of your group members at the beginning of the project. Every team member should have at least five issues to start (20-30 total). You will have to decide within your group how granular you want to make these issues. + +Issues will be tracked here, on GitHub. + +## Final Project Demo (group, 1%) + +The Final Project Demo will be a working version of your project that you will present to your lab section for review. The demo will be in lab, and will include a live demo of the working application, and a short code review. There are no slides required, but you should have practiced your demo to make sure it will run reasonably well. This will be during the last lab of class. + +No submission. + +## Final README.md (group, 1%) + +The Final README.md must give instructions on how to run your program, a list of contributions by each member, and any references/citations for code you may have used from elsewhere. + +Submission is here, on GitHub, in the `README.md` file. + +## Final Product (group, 5%) + +The Final Product will be evaluated for overall code design and documentation and evaluated on the same design principles as individual contributions. If you are below the 1000 line minimum contribution, your mark will be scaled down for this portion. + +Submission is here, on GitHub. + +## Code Contributions (individual, 15%) + +You will be expected to take on a significant individual contribution to the group project (at least 1000 lines of non-trivial code). It may be in a number of forms, but here are some examples: + +**Architect**: you are in charge of the high-level code structuring and organizing. + +**Test maker**: you are in charge of test coverage that supports other group members. + +**UI/UX lead**: you design and implement the user interface. + +**Backend**: you design and implement the data structures. + +**…??**: Make up your own depending on your use case, i.e., collision system designer, animation architect, async code wrangler. + +Contributions must be for functional, working Java code and must be continuous throughout the term. You may not, for example, push all of your changes at the end of term. Code will be marked on following good design principles, i.e., SOLID, design patterns, etc. You are encouraged to work together and use pair programming for components, but you will be marked on your contribution to your own modules individually. + +## Documentation Contributions (individual, 5%) + +Your code must be well-documented with fully-formed method signatures, comments, and necessary README or Wiki pages. This is further broken down into the following. + +### Initial individual pitch (1%) +A description of your individual feature that you plan to implement. + +Due date TBD. + +### Initial individual UML Diagrams (1%) +Any combination of sequence, communcation, or class diagrams that describe your feature's initial planned abilities. + +Due date TBD. + +### Documentation contributions (3%) +Your personal feature documentation, wherever it happens to show up in the final documentation. + +Due with final submission. + +## Issues and Pull Request Contributions (individual, 5%) +You must track your own work in the form of creating and closing GitHub issues, creating and reviewing pull requests, responding to issues that have been assigned to you, and creating issues that you assign to others (all within reason). + +# Errata +The project MUST be managed here, in this GitHub repo. Nothing that happens outside of this GitHub repo will be trackable by me, therefore, it will not be marked or considered for marking. + +You must use the following branching structures: +- `main` branch must always be working, tested, debugged, human-readable code. +- `__` is the format for each ISSUE that you're working on. It should always be a branch off of `main`. +- Every individual branch must regularly merge `main`, and it should be no more than a few days before your branch is either merged into `main` or deleted. + +You must use pull requests to manage your code integration: +- make your branch from `main`, e.g., `git checkout -b pb_71_demo` +- make your commits, e.g., `git add .` and `git commit -m "fixed issue 71 by reloading gradle for the 100th time"` and `git push origin pb_71_demo`. +- merge `main` into your branch, and NOT the other way around. E.g., `git merge main`. YOU fix all the merge conflicts or problems that arise. Commit and push again. +- go to GitHub.com and make a pull request +- one other person (NOT YOU) needs to review the code and either approve or reject your changes with detailed line-by-line comments. +- If needed, make the requested fixes and commit and push again. +- the other person (NOT YOU) will merge your PR into `main` +- the other person (NOT YOU) will delete your branch + + diff --git a/assets/.DS_Store b/assets/.DS_Store new file mode 100644 index 0000000..c5a31f9 Binary files /dev/null and b/assets/.DS_Store differ diff --git a/assets/data/maps/map1.txt b/assets/data/maps/map1.txt new file mode 100644 index 0000000..47348d5 --- /dev/null +++ b/assets/data/maps/map1.txt @@ -0,0 +1,50 @@ +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 0 2 2 2 2 2 0 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 2 2 2 2 2 2 2 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 2 2 2 2 2 2 2 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 2 2 2 2 2 2 2 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 1 1 1 1 1 4 4 4 4 4 4 4 4 2 2 2 2 2 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 1 3 3 3 1 4 4 4 4 4 4 4 0 0 0 5 0 0 0 4 4 4 4 4 4 0 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 1 3 3 3 1 4 4 4 4 4 4 4 4 0 0 0 0 0 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 1 3 3 3 1 4 4 4 4 4 4 4 4 4 0 0 0 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 1 3 3 3 1 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 1 1 0 1 1 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 0 4 4 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 0 4 4 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 0 4 4 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 0 4 4 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 4 4 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 4 4 4 4 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 5 5 5 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 4 4 4 4 4 4 4 4 4 4 4 4 0 0 5 0 0 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 4 4 4 4 4 4 4 4 4 4 4 0 0 0 5 0 0 0 4 4 4 4 4 4 4 4 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 4 4 4 0 4 4 4 4 4 4 4 0 0 0 5 0 0 0 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 4 4 4 0 4 4 4 4 4 4 4 4 0 0 5 0 0 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 4 4 4 0 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 4 4 4 0 0 0 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 4 4 4 4 4 0 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 4 4 4 4 4 0 4 4 4 4 4 4 4 4 5 4 4 4 0 4 4 4 4 4 4 4 0 5 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 4 4 4 4 4 0 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 0 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 0 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 4 0 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 0 0 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 0 0 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 0 0 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 0 0 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 4 4 4 4 4 4 4 4 4 4 4 0 0 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 4 4 4 0 0 0 0 0 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 0 4 4 0 0 0 0 0 0 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 0 0 0 0 4 4 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 0 0 0 0 4 4 0 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 4 0 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 0 0 0 0 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 0 4 4 4 4 4 0 0 0 0 0 0 0 0 4 4 4 4 0 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 0 0 0 0 0 0 4 4 4 4 4 0 0 4 0 0 4 0 0 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 0 0 0 0 0 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 \ No newline at end of file diff --git a/assets/data/maps/map2.txt b/assets/data/maps/map2.txt new file mode 100644 index 0000000..0642d2e --- /dev/null +++ b/assets/data/maps/map2.txt @@ -0,0 +1,50 @@ +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 0 2 2 2 2 2 2 2 0 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 0 2 2 2 2 2 2 2 0 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 0 2 2 2 2 2 2 2 0 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 1 1 1 1 1 1 1 1 1 1 1 1 0 2 2 2 2 2 2 2 0 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 1 3 3 3 3 3 3 3 3 3 3 1 4 0 0 0 0 0 0 0 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 1 3 3 3 3 3 3 3 3 3 3 1 4 0 0 0 5 0 0 0 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 1 3 3 3 3 3 3 3 3 3 3 1 4 4 0 0 0 0 0 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 1 3 3 3 3 3 3 3 3 3 3 1 4 4 0 0 0 0 0 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 1 3 3 3 3 3 3 3 3 3 3 1 4 4 4 0 5 0 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 1 1 1 0 1 1 1 1 1 1 1 1 4 4 4 0 5 0 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 0 0 0 4 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 0 0 0 4 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 0 0 0 4 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 4 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 4 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 0 0 5 0 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 4 4 4 4 4 4 4 4 4 4 4 0 0 5 0 0 4 4 4 4 4 4 4 4 4 0 0 0 5 0 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 4 4 4 4 4 4 4 4 4 4 0 0 0 5 0 0 0 4 4 4 4 4 4 4 4 0 5 5 5 0 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 4 4 4 4 4 4 4 4 4 0 0 0 0 5 0 0 0 0 4 4 4 4 4 4 0 0 5 0 0 0 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 5 0 0 0 0 0 0 0 0 0 0 0 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 4 4 0 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 4 4 0 5 0 0 0 0 0 0 0 0 0 5 0 0 0 0 0 0 0 0 0 0 0 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 4 4 0 5 0 4 4 4 4 0 0 0 0 5 0 0 0 0 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 4 4 0 5 0 0 4 4 4 4 0 0 0 5 0 0 0 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 4 4 0 5 5 0 4 4 4 4 4 0 0 5 0 0 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 4 4 0 0 5 0 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 4 4 4 0 5 0 4 4 4 4 4 4 0 5 0 4 4 0 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 4 4 4 4 5 0 4 4 4 4 4 4 0 5 0 4 4 4 4 4 0 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 0 4 4 4 0 0 0 0 0 0 0 0 4 4 4 4 4 4 0 5 0 4 4 4 0 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 0 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 0 5 0 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 4 4 4 4 4 0 0 0 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 +4 4 4 0 4 4 4 4 0 0 0 0 0 0 0 0 4 0 0 0 0 0 0 0 0 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 4 0 0 0 0 0 0 0 0 0 0 0 0 4 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 0 0 0 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 4 4 4 4 0 0 0 0 0 0 0 4 4 4 4 0 0 0 4 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 4 4 4 4 0 0 0 0 0 0 4 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 0 0 0 0 0 0 0 4 4 4 4 4 0 0 0 0 4 4 4 4 4 4 0 0 0 0 0 0 0 0 0 0 0 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 0 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 0 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 0 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 0 4 4 4 4 4 4 4 4 0 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 0 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 +4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 diff --git a/assets/data/objects/door1.png b/assets/data/objects/door1.png new file mode 100644 index 0000000..d725126 Binary files /dev/null and b/assets/data/objects/door1.png differ diff --git a/assets/data/objects/door2.png b/assets/data/objects/door2.png new file mode 100644 index 0000000..be525e9 Binary files /dev/null and b/assets/data/objects/door2.png differ diff --git a/assets/data/objects/fire1.png b/assets/data/objects/fire1.png new file mode 100644 index 0000000..1080798 Binary files /dev/null and b/assets/data/objects/fire1.png differ diff --git a/assets/data/objects/fire2.png b/assets/data/objects/fire2.png new file mode 100644 index 0000000..1f55eb1 Binary files /dev/null and b/assets/data/objects/fire2.png differ diff --git a/assets/data/objects/fire3.png b/assets/data/objects/fire3.png new file mode 100644 index 0000000..e4d392f Binary files /dev/null and b/assets/data/objects/fire3.png differ diff --git a/assets/data/objects/fire4.png b/assets/data/objects/fire4.png new file mode 100644 index 0000000..7b42333 Binary files /dev/null and b/assets/data/objects/fire4.png differ diff --git a/assets/data/objects/powerup.png b/assets/data/objects/powerup.png new file mode 100644 index 0000000..3497ae3 Binary files /dev/null and b/assets/data/objects/powerup.png differ diff --git a/assets/data/objects/ruby.png b/assets/data/objects/ruby.png new file mode 100644 index 0000000..97a9a0c Binary files /dev/null and b/assets/data/objects/ruby.png differ diff --git a/assets/data/tiles/bush.png b/assets/data/tiles/bush.png new file mode 100644 index 0000000..379cb06 Binary files /dev/null and b/assets/data/tiles/bush.png differ diff --git a/assets/data/tiles/earth.png b/assets/data/tiles/earth.png new file mode 100644 index 0000000..b0fa299 Binary files /dev/null and b/assets/data/tiles/earth.png differ diff --git a/assets/data/tiles/grass.png b/assets/data/tiles/grass.png new file mode 100644 index 0000000..3e503c3 Binary files /dev/null and b/assets/data/tiles/grass.png differ diff --git a/assets/data/tiles/sand.png b/assets/data/tiles/sand.png new file mode 100644 index 0000000..10971d4 Binary files /dev/null and b/assets/data/tiles/sand.png differ diff --git a/assets/data/tiles/tree.png b/assets/data/tiles/tree.png new file mode 100644 index 0000000..b0d91ba Binary files /dev/null and b/assets/data/tiles/tree.png differ diff --git a/assets/data/tiles/wall.png b/assets/data/tiles/wall.png new file mode 100644 index 0000000..39e9fdd Binary files /dev/null and b/assets/data/tiles/wall.png differ diff --git a/assets/data/tiles/water.png b/assets/data/tiles/water.png new file mode 100644 index 0000000..a3e29c2 Binary files /dev/null and b/assets/data/tiles/water.png differ diff --git a/assets/menu/jungle.jpeg b/assets/menu/jungle.jpeg new file mode 100644 index 0000000..4ed6a85 Binary files /dev/null and b/assets/menu/jungle.jpeg differ diff --git a/assets/menu/title.png b/assets/menu/title.png new file mode 100644 index 0000000..58c857b Binary files /dev/null and b/assets/menu/title.png differ diff --git a/assets/monsters/Slime_Contract.png b/assets/monsters/Slime_Contract.png new file mode 100644 index 0000000..0717eae Binary files /dev/null and b/assets/monsters/Slime_Contract.png differ diff --git a/assets/monsters/Slime_Relaxed.png b/assets/monsters/Slime_Relaxed.png new file mode 100644 index 0000000..8bfdd29 Binary files /dev/null and b/assets/monsters/Slime_Relaxed.png differ diff --git a/assets/monsters/beast_down_left.png b/assets/monsters/beast_down_left.png new file mode 100644 index 0000000..bcf02b6 Binary files /dev/null and b/assets/monsters/beast_down_left.png differ diff --git a/assets/monsters/beast_down_right.png b/assets/monsters/beast_down_right.png new file mode 100644 index 0000000..774167a Binary files /dev/null and b/assets/monsters/beast_down_right.png differ diff --git a/assets/monsters/beast_left_left.png b/assets/monsters/beast_left_left.png new file mode 100644 index 0000000..1329506 Binary files /dev/null and b/assets/monsters/beast_left_left.png differ diff --git a/assets/monsters/beast_left_right.png b/assets/monsters/beast_left_right.png new file mode 100644 index 0000000..a9952bc Binary files /dev/null and b/assets/monsters/beast_left_right.png differ diff --git a/assets/monsters/beast_right_left.png b/assets/monsters/beast_right_left.png new file mode 100644 index 0000000..b382ddc Binary files /dev/null and b/assets/monsters/beast_right_left.png differ diff --git a/assets/monsters/beast_right_right.png b/assets/monsters/beast_right_right.png new file mode 100644 index 0000000..1d8e498 Binary files /dev/null and b/assets/monsters/beast_right_right.png differ diff --git a/assets/monsters/beast_up_left.png b/assets/monsters/beast_up_left.png new file mode 100644 index 0000000..33dfd73 Binary files /dev/null and b/assets/monsters/beast_up_left.png differ diff --git a/assets/monsters/beast_up_right.png b/assets/monsters/beast_up_right.png new file mode 100644 index 0000000..b27a4d8 Binary files /dev/null and b/assets/monsters/beast_up_right.png differ diff --git a/assets/player/.DS_Store b/assets/player/.DS_Store new file mode 100644 index 0000000..52955d1 Binary files /dev/null and b/assets/player/.DS_Store differ diff --git a/assets/player/PlayerDownL.png b/assets/player/PlayerDownL.png new file mode 100644 index 0000000..0cd18ec Binary files /dev/null and b/assets/player/PlayerDownL.png differ diff --git a/assets/player/PlayerDownR.png b/assets/player/PlayerDownR.png new file mode 100644 index 0000000..01bb60a Binary files /dev/null and b/assets/player/PlayerDownR.png differ diff --git a/assets/player/PlayerLeftL.png b/assets/player/PlayerLeftL.png new file mode 100644 index 0000000..dd061c0 Binary files /dev/null and b/assets/player/PlayerLeftL.png differ diff --git a/assets/player/PlayerLeftR.png b/assets/player/PlayerLeftR.png new file mode 100644 index 0000000..0606385 Binary files /dev/null and b/assets/player/PlayerLeftR.png differ diff --git a/assets/player/PlayerRightL.png b/assets/player/PlayerRightL.png new file mode 100644 index 0000000..436dd06 Binary files /dev/null and b/assets/player/PlayerRightL.png differ diff --git a/assets/player/PlayerRightR.png b/assets/player/PlayerRightR.png new file mode 100644 index 0000000..24c8f8c Binary files /dev/null and b/assets/player/PlayerRightR.png differ diff --git a/assets/player/PlayerUpL.png b/assets/player/PlayerUpL.png new file mode 100644 index 0000000..81f97aa Binary files /dev/null and b/assets/player/PlayerUpL.png differ diff --git a/assets/player/PlayerUpR.png b/assets/player/PlayerUpR.png new file mode 100644 index 0000000..89ce49e Binary files /dev/null and b/assets/player/PlayerUpR.png differ diff --git a/assets/player/Player_downAttack.png b/assets/player/Player_downAttack.png new file mode 100644 index 0000000..12dc66f Binary files /dev/null and b/assets/player/Player_downAttack.png differ diff --git a/assets/player/Player_leftAttack.png b/assets/player/Player_leftAttack.png new file mode 100644 index 0000000..904a5ac Binary files /dev/null and b/assets/player/Player_leftAttack.png differ diff --git a/assets/player/Player_rightAttack.png b/assets/player/Player_rightAttack.png new file mode 100644 index 0000000..ef9b2e9 Binary files /dev/null and b/assets/player/Player_rightAttack.png differ diff --git a/assets/player/Player_upAttack.png b/assets/player/Player_upAttack.png new file mode 100644 index 0000000..b5edbfa Binary files /dev/null and b/assets/player/Player_upAttack.png differ diff --git a/assets/player/emptyHeart.png b/assets/player/emptyHeart.png new file mode 100644 index 0000000..2937bcf Binary files /dev/null and b/assets/player/emptyHeart.png differ diff --git a/assets/player/fullHeart.png b/assets/player/fullHeart.png new file mode 100644 index 0000000..70254c8 Binary files /dev/null and b/assets/player/fullHeart.png differ diff --git a/assets/player/halfHeart.png b/assets/player/halfHeart.png new file mode 100644 index 0000000..4252b1d Binary files /dev/null and b/assets/player/halfHeart.png differ diff --git a/assets/player/oldman_down_left.png b/assets/player/oldman_down_left.png new file mode 100644 index 0000000..85aa3d6 Binary files /dev/null and b/assets/player/oldman_down_left.png differ diff --git a/assets/player/oldman_down_right.png b/assets/player/oldman_down_right.png new file mode 100644 index 0000000..5dd4c07 Binary files /dev/null and b/assets/player/oldman_down_right.png differ diff --git a/assets/player/oldman_left_left.png b/assets/player/oldman_left_left.png new file mode 100644 index 0000000..147edcf Binary files /dev/null and b/assets/player/oldman_left_left.png differ diff --git a/assets/player/oldman_left_right.png b/assets/player/oldman_left_right.png new file mode 100644 index 0000000..f603226 Binary files /dev/null and b/assets/player/oldman_left_right.png differ diff --git a/assets/player/oldman_right_left.png b/assets/player/oldman_right_left.png new file mode 100644 index 0000000..d9893a9 Binary files /dev/null and b/assets/player/oldman_right_left.png differ diff --git a/assets/player/oldman_right_right.png b/assets/player/oldman_right_right.png new file mode 100644 index 0000000..7c74724 Binary files /dev/null and b/assets/player/oldman_right_right.png differ diff --git a/assets/player/oldman_up_left.png b/assets/player/oldman_up_left.png new file mode 100644 index 0000000..3d69acb Binary files /dev/null and b/assets/player/oldman_up_left.png differ diff --git a/assets/player/oldman_up_right.png b/assets/player/oldman_up_right.png new file mode 100644 index 0000000..e57f3c4 Binary files /dev/null and b/assets/player/oldman_up_right.png differ diff --git a/assets/sound/background.wav b/assets/sound/background.wav new file mode 100644 index 0000000..4177853 Binary files /dev/null and b/assets/sound/background.wav differ diff --git a/assets/sound/doorOpening.wav b/assets/sound/doorOpening.wav new file mode 100644 index 0000000..614c586 Binary files /dev/null and b/assets/sound/doorOpening.wav differ diff --git a/assets/sound/powerup.wav b/assets/sound/powerup.wav new file mode 100644 index 0000000..003c075 Binary files /dev/null and b/assets/sound/powerup.wav differ diff --git a/assets/sound/rubycollection.wav b/assets/sound/rubycollection.wav new file mode 100644 index 0000000..df70f84 Binary files /dev/null and b/assets/sound/rubycollection.wav differ diff --git a/assets/sound/running.wav b/assets/sound/running.wav new file mode 100644 index 0000000..6ae30bb Binary files /dev/null and b/assets/sound/running.wav differ diff --git a/assets/sound/running2.wav b/assets/sound/running2.wav new file mode 100644 index 0000000..e30b220 Binary files /dev/null and b/assets/sound/running2.wav differ diff --git a/assets/test/test_door.png b/assets/test/test_door.png new file mode 100644 index 0000000..d725126 Binary files /dev/null and b/assets/test/test_door.png differ diff --git a/assets/test/test_fire.png b/assets/test/test_fire.png new file mode 100644 index 0000000..e4d392f Binary files /dev/null and b/assets/test/test_fire.png differ diff --git a/assets/test/test_powerup.png b/assets/test/test_powerup.png new file mode 100644 index 0000000..3497ae3 Binary files /dev/null and b/assets/test/test_powerup.png differ diff --git a/assets/test/test_ruby.png b/assets/test/test_ruby.png new file mode 100644 index 0000000..97a9a0c Binary files /dev/null and b/assets/test/test_ruby.png differ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..a647d89 --- /dev/null +++ b/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java' +} + +group 'org.example' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'com.googlecode.json-simple:json-simple:1.1.1' + implementation 'org.jetbrains:annotations:23.0.0' + implementation 'org.mongodb:mongodb-driver-sync:4.9.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/documents/RubyRushUML.pdf b/documents/RubyRushUML.pdf new file mode 100644 index 0000000..0a3b630 Binary files /dev/null and b/documents/RubyRushUML.pdf differ diff --git a/documents/abhishek-pitch.md b/documents/abhishek-pitch.md new file mode 100644 index 0000000..005044d --- /dev/null +++ b/documents/abhishek-pitch.md @@ -0,0 +1,11 @@ +# Abhishek's Pitch + +As part of our project, I will be responsible for developing and maintaining the player/Entity class and the UI class. +The player class will contain the movable avatar while the UI will manage all the different components +and functionalities of our project. + +I am committed to ensuring that these sections are cohesive and functional by the end of our project. +I will work closely with my team members to ensure that we are all on the same page and +to provide support whenever anyone encounters any obstacles or difficulties. +In addition, I am open to feedback and suggestions from my team members as we work together to create a successful project. +I am confident that our collaboration will result in a high-quality final product that we can all be proud of. diff --git a/documents/amrit-pitch.md b/documents/amrit-pitch.md new file mode 100644 index 0000000..c322c29 --- /dev/null +++ b/documents/amrit-pitch.md @@ -0,0 +1,9 @@ +# Amrit's Pitch + +I will handle non player interactions for the most part, and a bit of work here and there which would involve collision detection and movement. I am also in charge of writing test cases for these non player entities, though only up until everything checks out. + + +Past the pitch, I have to say that starting the project up was one of the most difficult parts when you are just trying to figure out what to put on code and waiting for a part here or there. I feel there is a lot I would have done different, and hopefully I can carry some of these lessons to the projects term. + +I learned a lot about GitHub that I was unaware of before, and it has a lot of great tools for organization and actually allowing others to work on branches that do not interfere with the main. I need to work on pushing smaller bits of code faster, and having my issues be steps rather than whole chunks of code. There were times where my branch would become quite outdated because I had spent so long on it and the main branch had warped so much. + diff --git a/documents/greg-pitch.md b/documents/greg-pitch.md new file mode 100644 index 0000000..e155894 --- /dev/null +++ b/documents/greg-pitch.md @@ -0,0 +1,7 @@ +# Greg's Pitch + +As the person in charge of the managing the saving and loading data, I will take ownership of SaveState, SaveStateHandler, Menu and if time permits Client and Server. +My responsibility primarily includes planning and implementing the data structure to store data in json format and also being able to load this data to begin a previously saved game. + +Since we will have to load save data to play a previously saved game, I will also focus on making a UI to start the game. This will be done by implementing a menu. +Then making sure that the game loads from this menu, and to implement a way to save more than one save state. diff --git a/documents/img/img1.png b/documents/img/img1.png new file mode 100644 index 0000000..1ba2580 Binary files /dev/null and b/documents/img/img1.png differ diff --git a/documents/img/img2.png b/documents/img/img2.png new file mode 100644 index 0000000..41a3727 Binary files /dev/null and b/documents/img/img2.png differ diff --git a/documents/img/img3.png b/documents/img/img3.png new file mode 100644 index 0000000..7cdb2a1 Binary files /dev/null and b/documents/img/img3.png differ diff --git a/documents/nathan-pitch.md b/documents/nathan-pitch.md new file mode 100644 index 0000000..7fd9aa2 --- /dev/null +++ b/documents/nathan-pitch.md @@ -0,0 +1,5 @@ +# Nathan's Pitch + +As the person in charge of the visuals, I will take ownership of the GamePanel, TileManager, and Tile classes to make sure everything displays properly in the game. This includes writing the main method and fetching assets for the sprites and tiles. I will still have to be involved in the logical side of the project, of course, but I definitely have my work cut out for me. + + diff --git a/documents/project-pitch.md b/documents/project-pitch.md new file mode 100644 index 0000000..506288f --- /dev/null +++ b/documents/project-pitch.md @@ -0,0 +1,49 @@ +# 2522 Project +* [Project Overview](#project-overview) +* [About Us](#about-us) +* [Contact](#contact) + + +## Project Overview +Our team's project will be a 2D adventure game, similar to the Legend of Zelda. +1. As a 2D video game, our project will make heavy use of Java FX library for the visuals. +2. We will need to constantly process game state and fetch information from the console. +3. In addition, we will need to store the position of objects in the game, so the player can load an previous session. +4. When the player picks up an object, it should delete itself from the data structure. +5. We will constantly check our requirements, test boundaries, and write Javadocs for every class. + + +## About Us +**Meetings**: + * We will meet in-person every Monday to discuss our progress and what we need to get done. + +**Communications**: + * We will use Discord to communicate with each other throughout the day. + +**Roles**: + * Nathan Bartyuk - In charge of the visual design and world map. + * Abhishek Chouhan - In charge of the controls and player character. + * Amrit Jhatu - In charge of non-player characters and interactions. + * Greg Song - In charge of the managing persistent data state and asynchronous processing. + * Simrat Kaur - In charge of collision and item interactions. + +**Expectations**: + * Nathan Bartyuk - I want to learn a lot about visual and sound design, as well as creating an Object-Oriented game. + * Abhishek Chouhan - I want to learn a lot of Java and improve at logic design. + * Amrit Jhatu - I want to improve my Java skills and use it to create other projects after this is over. + * Greg Song - I want to improve my diagram creation skills and OOP design. + * Simrat Kaur - I want to learn more Java concepts and game design. + +**Milestones**: + 1. Create the grid and playable character. + 2. Fill the grid with different objects that interact with each other. + 3. Fill the grid with hazards that the player must avoid. + 4. Create a save system that keeps the player's progress. + + +## Contact +* Nathan Bartyuk - nbartyuk@my.bcit.ca +* Abhishek Chouhan - achouhan4@my.bcit.ca +* Amrit Singh - ajhatu@my.bcit.ca +* Greg Song - jsong118@my.bcit.ca +* Simrat Kaur - simratkaur2@my.bcit.ca diff --git a/documents/simrat-pitch.md b/documents/simrat-pitch.md new file mode 100644 index 0000000..e56e5ef --- /dev/null +++ b/documents/simrat-pitch.md @@ -0,0 +1,18 @@ +# Simrat's Pitch + +As a member of the team, my responsibilities include managing the Element class, which is the super class for Fire, +Ruby, PowerUp, and Door elements in the game. I will also be responsible for implementing the CollisionDetector +class specifically for the parts where the player collides with the elements. +My primary focus will be on developing the necessary functionality for these classes, with the expectation that +these sections will be completed by the end of the semester. + +Throughout the development process, I plan to conduct extensive testing to ensure the functionality of these classes +meets the project's requirements. Additionally, I will collaborate with my group members to share knowledge and offer +support when needed. I will also be available to assist my group members in overcoming challenges and to contribute +whenever my help would be required. + +Overall, my aim is to deliver high-quality, professional work that meets the project's objectives while upholding +the values of our team. + + + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..a69d9cb --- /dev/null +++ b/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/save/.json b/save/.json new file mode 100644 index 0000000..c3955f4 --- /dev/null +++ b/save/.json @@ -0,0 +1 @@ +{"gamePanelData":{"monsterArr":[{"x":1672,"y":1092,"type":"Monster"},{"x":438,"y":1206,"type":"Monster"},{"x":1288,"y":1096,"type":"Monster"},{"x":1672,"y":1312,"type":"Monster"}],"elementArr":[{"x":480,"y":528,"type":"Door"},{"x":1104,"y":336,"type":"PowerUp"},{"x":1104,"y":480,"type":"Ruby"},{"x":576,"y":2016,"type":"Ruby"},{"x":960,"y":336,"type":"Ruby"},{"x":1824,"y":1968,"type":"Ruby"},{"x":912,"y":1776,"type":"Fire"},{"x":960,"y":1728,"type":"Fire"},{"x":1008,"y":1872,"type":"Fire"},{"x":1056,"y":1920,"type":"Fire"}],"npcArr":[{"x":1206,"y":1198,"type":"Villager"}]},"playerData":{"spriteCounter":1,"worldX":1716,"lives":6,"worldY":1444,"spriteNum":2,"speed":4,"direction":3,"rubies":1}} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..9307b60 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'Project_Ruby' + diff --git a/src/main/java/org/project/Datastate/Client.java b/src/main/java/org/project/Datastate/Client.java new file mode 100644 index 0000000..4840d51 --- /dev/null +++ b/src/main/java/org/project/Datastate/Client.java @@ -0,0 +1,133 @@ +package org.project.Datastate; + +import java.io.*; +import java.net.*; +import org.json.simple.*; +import org.json.simple.parser.*; + +/** + * Client handles interactions with Server, sends and receives data. + * + * @author Greg Song + * @version 2023-03-27 + */ +public class Client { + /* Constants */ + private static final String GET = "GET"; + private static final String POST = "POST"; + + /* Instance Variables */ + private String host; + private final int port; + + + /** + * Constructs new Client. + * + * @param port an int, + */ + public Client(int port) { + try { + host = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + System.err.println("Can't find host"); + } + this.port = port; + } + + /** + * Sends JSONObject to Server. + * + * @param request a JSONObject. + * @return response a JSONObject + */ + public JSONObject sendRequest(String request) throws IOException { + // setup + Socket socket = null; + ObjectOutputStream oos = null; + ObjectInputStream ois = null; + + // establish socket connection to server + try { + socket = new Socket(this.host, this.port); + } catch (IOException e) { + System.err.println("Can't connect to host socket."); + } + + // open output stream + try { + assert socket != null; + oos = new ObjectOutputStream(socket.getOutputStream()); + } catch (Exception e) { + System.err.println("Can't connect to Out stream socket."); + } + System.out.println("Sending request to Socket Server"); + + // write to socket output stream + try { + assert oos != null; + oos.writeObject(request); + } catch (Exception e) { + System.err.println("Can't write object request: num"); + } + + // read the server response message + try { + ois = new ObjectInputStream(socket.getInputStream()); + } catch (Exception e) { + System.err.println("Can't connect to input stream socket."); + } + + // create jsonString from server response + String jsonString = null; + try { + assert ois != null; + jsonString = (String) ois.readObject(); + } catch (RuntimeException | ClassNotFoundException e) { + System.err.println("Can't read message from Server."); + } + + // Parse jsonString to JSONObject + JSONObject jsonRes = null; + try { + JSONParser parser = new JSONParser(); + jsonRes = (JSONObject) parser.parse(jsonString); + } catch (ParseException e) { + System.err.println("Can't parse jsonString"); + } + + // close resources + try { + ois.close(); + oos.close(); + } catch (Exception e) { + System.err.println("Can't close stream resources."); + } + return jsonRes; + } + + /** + * Creates a JSONString to be sent to server. + * + * @param reqType "POST" or "GET" + * @return String JSONObject as String + */ + public String createJSON(String reqType) { + JSONObject req = new JSONObject(); + req.put("reqType", reqType); + req.put("uid", SaveStateHandler.getInstance().getUsername()); + // for POST request + if (reqType.equals(POST)) { + System.out.println("creating request"); + req.put("playerData", SaveState.getInstance().playerData); + req.put("gamePanelData", SaveState.getInstance().getGamePanelData()); + } else if (reqType.equals(GET)) { + // create GET request JSONObject + req.put("reqType", GET); + req.put("uid", SaveStateHandler.getInstance().getUsername()); + } else { + System.err.println("Invalid request type"); + } + return req.toJSONString(); + } +} diff --git a/src/main/java/org/project/Datastate/DatabaseHandler.java b/src/main/java/org/project/Datastate/DatabaseHandler.java new file mode 100644 index 0000000..464d6aa --- /dev/null +++ b/src/main/java/org/project/Datastate/DatabaseHandler.java @@ -0,0 +1,106 @@ +package org.project.Datastate; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.ServerApi; +import com.mongodb.ServerApiVersion; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.bson.Document; + +//import static java.util.Collections.eq; +import static com.mongodb.client.model.Filters.eq; + +/** + * Handles Database interactions with MongoDB. DatabaseHandler implements + * Singleton design pattern. + * + * @author Greg Song + * @version 2023-03-27 + */ +public class DatabaseHandler { + /* Class variables */ + private static DatabaseHandler instance; + private final MongoDatabase database; + private final String myCollection = "savestate"; + ExecutorService executor; + + /** + * Constructs new DatabaseHandler singleton. + */ + private DatabaseHandler() { + // connect + String password = "testuser123"; + String user = "testuser"; + String uri = "mongodb+srv://" + user + ":" + password + + "@cluster0.2gojpcl.mongodb.net/?retryWrites=true&w=majority"; + ConnectionString connectionString = new ConnectionString(uri); + MongoClientSettings settings = MongoClientSettings.builder() + .applyConnectionString(connectionString) + .serverApi(ServerApi.builder() + .version(ServerApiVersion.V1) + .build()) + .build(); + + try (MongoClient mongoClient = MongoClients.create(settings)) { + String dbName = "ruby"; + this.database = mongoClient.getDatabase(dbName); + } + + // test if collection exists + try { + this.database.createCollection(this.myCollection); + } catch (Exception e) { + System.err.println("Collection already exists"); + } + this.executor = Executors.newFixedThreadPool(10); + } + + /** + * Gets instance of DatabaseHandler. + * + * @return instance of DatabaseHandler + */ + public static synchronized DatabaseHandler getInstance() { + if (instance == null) { + instance = new DatabaseHandler(); + } + return instance; + } + + + /** + * Writes new Document to collection. + * + * @param document - Document to be written + */ + public void put(Document document) { + database.getCollection("savestate").insertOne(document); + } + + /** + * Gets first Document in collection with key value. + * + * @param key existing key of document + * @param value existing value of kv pair in document + * @return Document + */ + public Document get(String key, String value) { + return this.database.getCollection(this.myCollection).find(eq(key, value)).first(); + } + + /** + * Updates existing document in Collection. + * + * @param key existing key of document + * @param value existing value of kv pair in document + * @param doc new document of new key value pairs + */ + public void update(String key, String value, Document doc) { + this.database.getCollection(this.myCollection).updateOne(eq(key, value), + new Document("$set", doc)); + } +} \ No newline at end of file diff --git a/src/main/java/org/project/Datastate/GetRequestHandler.java b/src/main/java/org/project/Datastate/GetRequestHandler.java new file mode 100644 index 0000000..ef9320f --- /dev/null +++ b/src/main/java/org/project/Datastate/GetRequestHandler.java @@ -0,0 +1,96 @@ +package org.project.Datastate; + +import java.io.*; +import java.net.Socket; +import org.bson.Document; +import org.json.simple.*; + +/** + * Class that handles GET Requests received by Server. + * + * @author Greg Song + * @version 2023-03-27 + */ +public class GetRequestHandler implements Runnable { + private final DatabaseHandler databaseHandler; + private final Socket socket; + private final JSONObject obj; + + /** + * Constructs a new GetRequestHandler. + * + * @param socket the client socket + * @param obj a JSONObject containing request data + */ + public GetRequestHandler(Socket socket, JSONObject obj) { + this.socket = socket; + this.databaseHandler = DatabaseHandler.getInstance(); + this.obj = obj; + } + + /** + * Sends response to client. + * + * @param message - Message to be sent to Client + */ + public void sendResponse(String message) throws Exception { + OutputStream outputStream = this.socket.getOutputStream(); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); + String res = createJSONRes(message); + + // Write data to the ObjectOutputStream + objectOutputStream.writeObject(res); + + // Flush to ensure all data is written to the OutputStream + objectOutputStream.flush(); + + // Close the ObjectOutputStream and socket + objectOutputStream.close(); + this.socket.close(); + } + + /** + * Creates JSON response to send to Client. + * + * @param message - Message to be sent to Client + * @return response, a JSON string + */ + private String createJSONRes(String message) throws Exception { + JSONObject res = new JSONObject(); + res.put("Status", "success"); + res.put("message", message); + + // get doc + Document doc = this.databaseHandler.get("uid", String.valueOf(this.obj.get("uid"))); + if (doc == null) { + throw new Exception("Could not locate document"); + } + + Integer rubies = doc.getInteger("rubies"); + Integer lives = doc.getInteger("lives"); + Integer spriteMap = doc.getInteger("spriteMap"); + + if (rubies == null || lives == null || spriteMap == null) { + throw new Exception("Missing required fields in document"); + } + + res.put("rubies", rubies); + res.put("lives", lives); + res.put("spriteMap", spriteMap); + + return res.toJSONString(); + } + + /** + * Runs the Get request handler. + */ + @Override + public void run() { + System.out.println("getRequestHandler.run() ran"); + try { + sendResponse("Document located successfully"); + } catch (Exception e) { + System.err.println("Get request could not run"); + } + } +} diff --git a/src/main/java/org/project/Datastate/PostRequestHandler.java b/src/main/java/org/project/Datastate/PostRequestHandler.java new file mode 100644 index 0000000..e7acdcc --- /dev/null +++ b/src/main/java/org/project/Datastate/PostRequestHandler.java @@ -0,0 +1,105 @@ +package org.project.Datastate; + +import java.io.*; +import java.net.Socket; +import org.bson.Document; +import org.json.simple.*; + +/** + * Class that handles POST Requests received by Server. + * + * @version 2023-03-27 + */ +public class PostRequestHandler implements Runnable { + private final DatabaseHandler databaseHandler; + private final Socket socket; + private final JSONObject obj; + + /** + * Constructs a new PostRequestHandler. + * + * @param socket the client socket + * @param obj a JSONObject containing request data + */ + public PostRequestHandler(Socket socket, JSONObject obj) { + this.socket = socket; + this.databaseHandler = DatabaseHandler.getInstance(); + this.obj = obj; + } + + /** + * Sends response to Client. + * + * @param message a String + * @throws IOException if error while sending response + */ + public void sendResponse(String message) throws IOException { + OutputStream outputStream = this.socket.getOutputStream(); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); + String res = createJSONRes(message); + // Write data to the ObjectOutputStream + objectOutputStream.writeObject(res); + // Flush the ObjectOutputStream to ensure all data is written to the OutputStream + objectOutputStream.flush(); + // Close the ObjectOutputStream to release any resources it may be holding + objectOutputStream.close(); + this.socket.close(); + } + + /** + * Creates JSON response to send to Client. + * + * @param message a String, message to be sent to Client + * @return response, a JSON string + */ + private String createJSONRes(String message) { + JSONObject res = new JSONObject(); + // put data + res.put("Status", "success"); + res.put("message", message); + + return res.toJSONString(); + } + + /** + * Runs the POST request handler. + */ + @Override + public void run() { + try { + System.out.println("post handler ran"); + // get existing Document to update + String uid = (String) obj.get("uid"); + Document doc = this.databaseHandler.get("uid", uid); + if (doc != null) { + try { + System.out.println("UID found"); + // update document with JSONObject values + doc.put("rubies", obj.get("rubies")); + doc.put("lives", obj.get("lives")); + doc.put("spriteMap", obj.get("spriteMap")); + this.databaseHandler.update("uid", uid, doc); + } catch (Exception e) { + throw new RuntimeException(e); + } + try { + this.sendResponse("Data successfully updated"); + } catch (IOException e) { + // throw new RuntimeException(e); + System.err.println("Cant write to oos"); + } + } else { + // create new document + System.out.println("UID not found"); + Document newDoc = new Document("uid", obj.get("uid")); + newDoc.append("rubies", obj.get("rubies")); + newDoc.append("lives", obj.get("lives")); + newDoc.append("spriteMap", obj.get("spriteMap")); + this.databaseHandler.put(newDoc); + } + } catch (Exception e) { + System.err.println("posthandler could not run"); + e.printStackTrace(); + } + } +} diff --git a/src/main/java/org/project/Datastate/SaveState.java b/src/main/java/org/project/Datastate/SaveState.java new file mode 100644 index 0000000..a01e3e6 --- /dev/null +++ b/src/main/java/org/project/Datastate/SaveState.java @@ -0,0 +1,252 @@ +package org.project.Datastate; + +import java.util.Arrays; +import java.util.Objects; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.project.Entities.Entity; +import org.project.Entities.Monster; +import org.project.Entities.Player; +import org.project.Entities.Villager; +import org.project.Map.Positionable; +import org.project.Objects.*; +import org.project.ui.GamePanel; + +/** + * Defines a save object which stores the current game state in a file. + * + * @author Nathan Bartyuk, Greg Song + * @version 2023-03-31 + */ +public class SaveState { + private static final int ARR_SIZE = 20; + private static SaveState instance; + JSONObject playerData; + JSONObject gamePanelData; + GamePanel gamePanel; + + /** + * Returns instance of this Singleton SaveState. + * + * @return instance, the instance of this SaveState. + */ + public static SaveState getInstance() { + if (instance == null) { + instance = new SaveState(); + } + return instance; + } + + /** + * Loads Player variables from JSON playerData. + * + * @param player a Player object in game + */ + public void load(Player player, GamePanel gamePanel) { + if (playerData == null) { + throw new NullPointerException("PlayerData object is null."); + } + this.gamePanel = player.gp; + + player.setWorldX(((Long) playerData.get("worldX")).intValue()); + player.setWorldY(((Long) playerData.get("worldY")).intValue()); + player.setSpeed(((Long) playerData.get("speed")).intValue()); + player.spriteCounter = ((Long) playerData.get("spriteCounter")).intValue(); + player.spriteNum = ((Long) playerData.get("spriteNum")).intValue(); + player.setLives(((Long) playerData.get("lives")).intValue()); + player.setCurrentRubies(((Long) playerData.get("rubies")).intValue()); + + gamePanel.elements = parseElementArr((JSONArray) this.gamePanelData.get("elementArr")); + gamePanel.npc = parseNPCArr((JSONArray) this.gamePanelData.get("npcArr")); + gamePanel.monster = parseMonsterArr((JSONArray) this.gamePanelData.get("monsterArr")); + } + + /* Helper methods */ + + /** + * Helper method to set the player data as a JSONObject. + * + * @param player current instance of Player + */ + private void setPlayerData(Player player) { + if (player == null) { + throw new NullPointerException("Player object is null."); + } + JSONObject playerData = new JSONObject(); + playerData.put("worldX", player.getWorldX()); + playerData.put("worldY", player.getWorldY()); + playerData.put("speed", player.getSpeed()); + playerData.put("direction", player.peekDirection().ordinal()); + playerData.put("spriteCounter", player.spriteCounter); + playerData.put("spriteNum", player.spriteNum); + playerData.put("lives", player.getLives()); + playerData.put("rubies", player.getCurrentRubies()); + this.playerData = playerData; + } + + /** + * Helper method to set the Game Panel data as a JSONObject. + * + * @param gp current instance of GamePanel. + */ + private void setGamePanelData(GamePanel gp) { + if (gp == null) { + throw new NullPointerException("GamePanel object is null."); + } + JSONObject gamePanelData = new JSONObject(); + gamePanelData.put("elementArr", arrToJSON(gp.elements)); + gamePanelData.put("npcArr", arrToJSON(gp.npc)); + gamePanelData.put("monsterArr", arrToJSON(gp.monster)); + this.gamePanelData = gamePanelData; + } + + /** + * Creates a JSONArray of Positionable objects consisting of each worldX and worldY. + * + * @param positionables a Positionable array + * @return a JSONArray of JSONObjects with x and y properties + */ + private JSONArray arrToJSON(Positionable[] positionables) { + JSONArray jsonArr = new JSONArray(); + Arrays.stream(positionables) + .filter(Objects::nonNull) + .map(positionable -> { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("type", positionable.getClass().getSimpleName()); + jsonObject.put("x", positionable.getWorldX()); + jsonObject.put("y", positionable.getWorldY()); + return jsonObject; + }) + .forEach(jsonArr::add); + return jsonArr; + } + + /** + * Parses JSONArray and returns Element array. + * + * @param objs JSONArray, in save file + * @return Element[] + */ + private Element[] parseElementArr(JSONArray objs) { + Element[] array = new Element[20]; + for (int i = 0; i < objs.size(); i++) { + JSONObject obj = (JSONObject) objs.get(i); + array[i] = jsonToElement(obj); + } + return array; + } + + /** + * Parse JSONArray and returns Monster array. + * + * @param objs a JSONObject representing Monster + * @return Entity[] + */ + private Entity[] parseMonsterArr(JSONArray objs) { + Entity[] array = new Entity[ARR_SIZE]; + for (int i = 0; i < objs.size(); i++) { + JSONObject obj = (JSONObject) objs.get(i); + Monster monster = new Monster(gamePanel, 0, 0); + ((Positionable) monster).setWorldX(((Number) obj.get("x")).intValue()); + ((Positionable) monster).setWorldY(((Number) obj.get("y")).intValue()); + array[i] = monster; + } + return array; + } + + /** + * Parse JSONArray and return NPC array. + * + * @param objs a JSONObject representing NPC + * @return Entity[] + */ + private Entity[] parseNPCArr(JSONArray objs) { + Entity[] array = new Entity[ARR_SIZE]; + for (int i = 0; i < objs.size(); i++) { + JSONObject obj = (JSONObject) objs.get(i); + Villager villager = new Villager(gamePanel, 0, 0); + ((Positionable) villager).setWorldX(((Number) obj.get("x")).intValue()); + ((Positionable) villager).setWorldY(((Number) obj.get("y")).intValue()); + array[i] = villager; + } + return array; + } + + /** + * Converts JSONObject representing Element and converts to Element subtype. + * + * @param obj a JSONObject representing Element + * @return Ruby, Fire, PowerUp or Door with proper worldX and worldY + */ + private Element jsonToElement(JSONObject obj) { + String type = (String) obj.get("type"); + switch (type) { + case "Ruby" -> { + Ruby ruby = new Ruby(); + ((Positionable) ruby).setWorldX(((Number) obj.get("x")).intValue()); + ((Positionable) ruby).setWorldY(((Number) obj.get("y")).intValue()); + return ruby; + } + case "PowerUp" -> { + PowerUp powerUp = new PowerUp(); + ((Positionable) powerUp).setWorldX(((Number) obj.get("x")).intValue()); + ((Positionable) powerUp).setWorldY(((Number) obj.get("y")).intValue()); + return powerUp; + } + case "Fire" -> { + Fire fire = new Fire(); + ((Positionable) fire).setWorldX(((Number) obj.get("x")).intValue()); + ((Positionable) fire).setWorldY(((Number) obj.get("y")).intValue()); + return fire; + } + case "Door" -> { + Door door = new Door(); + ((Positionable) door).setWorldX(((Number) obj.get("x")).intValue()); + ((Positionable) door).setWorldY(((Number) obj.get("y")).intValue()); + return door; + } + default -> throw new IllegalArgumentException("Invalid entity type: " + type); + } + } + + /** + * Returns the playerData, all the data that is required to be saved in Player. + * + * @return playerData, a JSONObject + */ + public JSONObject getPlayerData() { + return this.playerData; + } + + /** + * Gets the gamePanelData, all the data that is required to be saved in gamePanel. + * + * @return gamePanelData, a JSONObject + */ + public JSONObject getGamePanelData() { + return this.gamePanelData; + } + + /** + * Sets the SaveState by taking in a Gamepanel object. Used to Save the game + * data while the game is playing. + * + * @param gp a GamePanel instance. + */ + public void setSaveState(GamePanel gp) { + this.gamePanel = gp; + setPlayerData(gp.player); + setGamePanelData(gp); + } + + /** + * Sets the SaveState by taking in a JSONObject. Used to set SaveState, + * to be used when loading the game from Menu. + * + * @param json a JSONObject, parsed from a JSON file. + */ + public void setSaveState(JSONObject json) { + this.gamePanelData = (JSONObject) json.get("gamePanelData"); + this.playerData = (JSONObject) json.get("playerData"); + } +} diff --git a/src/main/java/org/project/Datastate/SaveStateHandler.java b/src/main/java/org/project/Datastate/SaveStateHandler.java new file mode 100644 index 0000000..9825082 --- /dev/null +++ b/src/main/java/org/project/Datastate/SaveStateHandler.java @@ -0,0 +1,112 @@ +package org.project.Datastate; + +import java.io.*; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; + +/** + * SaveStateHandler manages reading and writing SaveState. Files saved in JSON format. + * + * @author Greg Song + * @version 2023-04-03 + */ +public class SaveStateHandler { + private static SaveStateHandler instance; + private final SaveState saveState; + private final String dirPath = "save/"; + private final String extension = ".json"; + private String username; + private String pathName; + + /** + * Private constructor to enforce Singleton pattern. + */ + private SaveStateHandler() { + this.saveState = SaveState.getInstance(); + } + + /** + * Returns singleton instance of SaveStateHandler. + * + * @return SaveStateHandler instance + */ + public static SaveStateHandler getInstance() { + if (instance == null) { + instance = new SaveStateHandler(); + } + return instance; + } + + /** + * Stores save data as a JSON file in save directory. + */ + public void save() { + Thread saveThread = new Thread(() -> { + JSONObject jsonSave = new JSONObject(); + jsonSave.put("playerData", saveState.getPlayerData()); + jsonSave.put("gamePanelData", saveState.getGamePanelData()); + + FileWriter fileWriter; + try { + File file = new File(pathName); + fileWriter = new FileWriter(file); + fileWriter.write(jsonSave.toJSONString()); + fileWriter.close(); + } catch (IOException e) { + throw new RuntimeException("Could not create or write to file at: " + pathName, e); + } + }); + saveThread.start(); + } + + /** + * Loads save data JSON file from directory and returns saveState. + * + * @return SaveState object + * @throws FileNotFoundException When user file is not found. + */ + public SaveState load() throws FileNotFoundException { + File saveFile = new File(getPathName()); + JSONObject jsonSave; + try { + FileReader fileReader = new FileReader(saveFile); + jsonSave = (JSONObject) JSONValue.parse(fileReader); + } catch (FileNotFoundException e) { + throw new FileNotFoundException("Cannot locate file:" + getPathName()); + } + SaveState saveState = SaveState.getInstance(); + saveState.setSaveState(jsonSave); + return saveState; + } + + /** + * Sets username. + * + * @param username a String, used as file name of save data + */ + public void setUsername(String username) { + this.username = username; + this.pathName = getPathName(); + } + + /** + * Gets the full file path of the save file. + * + * @return full file path + */ + private String getPathName() { + if (username == null || username.isEmpty()) { + throw new IllegalStateException("Username is not set."); + } + return dirPath + username + extension; + } + + /** + * Gets the username of this SaveState. + * + * @return username, a String, used as filename of save data. + */ + public String getUsername() { + return this.username; + } +} diff --git a/src/main/java/org/project/Datastate/Server.java b/src/main/java/org/project/Datastate/Server.java new file mode 100644 index 0000000..cecae7f --- /dev/null +++ b/src/main/java/org/project/Datastate/Server.java @@ -0,0 +1,75 @@ +package org.project.Datastate; + +import java.io.*; +import java.lang.ClassNotFoundException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.*; +import org.json.simple.*; + +/** + * Server that handles requests and interactions with MongoDB. + * + * @author Greg Song + * @version 2023-03-27 + */ +public class Server { + /* Constants */ + private static final int PORT = 5000; + private static final int POOL_SIZE = 10; + private static final String POST = "POST"; + private static final String GET = "GET"; + + private final ServerSocket server; + private final ExecutorService executor; + private final DatabaseHandler databasehandler; + + + /** Constructs a Server. */ + public Server() throws IOException { + this.server = new ServerSocket(PORT); + this.executor = Executors.newFixedThreadPool(POOL_SIZE); + this.databasehandler = DatabaseHandler.getInstance(); + } + + /** + * Parses JSONString Request. + * + * @return JSONObject + */ + public JSONObject parseJSON(String JSONstr) { + return (JSONObject) JSONValue.parse(JSONstr); + } + + /** + * Starts Server. + */ + public void start() throws IOException { + while (true) { + Socket socket = server.accept(); + + // read from socket to ObjectInputStream object + ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); + String jsonString = null; + try { + jsonString = (String) ois.readObject(); + } catch (RuntimeException | ClassNotFoundException e) { + System.err.println("Can't read message from Server."); + } + JSONObject obj = parseJSON(jsonString); + + // handle request + if (obj.get("reqType").equals(POST)) { + System.out.println("RequestType:" + obj.get("reqType")); //thread + Runnable task = new PostRequestHandler(socket, obj); + executor.submit(task); + } else if (obj.get("reqType").equals(GET)) { + System.out.println("RequestType:" + obj.get("reqType")); // handle + Runnable task = new GetRequestHandler(socket, obj); + executor.submit(task); + } else { + System.err.println("ReqType Invalid" + obj.get("reqType")); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/project/Entities/Entity.java b/src/main/java/org/project/Entities/Entity.java new file mode 100644 index 0000000..a384859 --- /dev/null +++ b/src/main/java/org/project/Entities/Entity.java @@ -0,0 +1,220 @@ +package org.project.Entities; + +import java.awt.*; +import java.awt.image.BufferedImage; +import org.project.Map.Positionable; +import org.project.ui.GamePanel; + +import static org.project.SystemVariables.*; + +/** + * Abstract class for an entity that can move across the map. + * + * @author Team Ruby + * @version 2023-04-04 + */ +public abstract class Entity implements Positionable { + + // gamePanel instance being accessed by all Entities + public GamePanel gp; + + // Entity positional, and other variables + protected int worldX, worldY; // Coordinates on the 50 * 50 world map + protected int screenX, screenY; // Coordinates on the 16 * 12 screen + protected int speed; + protected Directions direction; + + // Entity state variables + protected boolean collision = false; // is colliding = false + protected boolean invincible = false; // is invincible = false + protected boolean onPath = false; + protected int invincibleCounter = 0; + // -----------------------------------------------------------// + + public Rectangle hitbox; // Entity hitbox to check for collision (and damage in player's case) + public int hitboxDefaultX, hitboxDefaultY; + + // Sprites (or Images) for different instances of Entity + protected BufferedImage upR, upL, downR, downL, leftR, leftL, rightR, rightL; + protected final int spriteMax = 20; // Should only update every 14 frames, not every frame + protected final int indexMax = 999; // Max number of elements that can be displayed in tile array + + // below two variables are only being accessed by the server + public int spriteCounter = 0; // var to count how many instances of a sprite have been drawn + public int spriteNum = 1; // the current sprite to be displayed + protected int actionLockCounter = 0; + // ---------------------All Animation related stuff above ------------------------/// + + protected int type; // 0 = player, 1 = npc, 2 = monster, 3 = projectile + + /** + * Constructor for Entity. + * + * @param gp GamePanel instance being accessed by all Entities + */ + public Entity(GamePanel gp) { + this.gp = gp; + // default location to spawn entities to (does not really matter + // as this is being overridden in specific constructors of subclasses + this.hitbox = new Rectangle(10, 10, 28, 38); + } + + /** + * Checks if Entity has collided with a tile. + */ + public void checkCollision() { + collision = false; + gp.cDetector.checkTile(this); + gp.cDetector.checkPlayerCollide(this); + boolean contactPlayer = gp.cDetector.checkPlayerCollide(this); + if (this.type == 2 && contactPlayer && !gp.player.invincible) { + gp.player.setLives(gp.player.getLives() - 1); // decrement player lives + gp.player.invincible = true; + } + } + + /** + * Updates the entity depending on its current action. + */ + public void update() { + setAction(); + checkCollision(); + + // update position of entity if it is not colliding with anything + if (!collision) { + switch (direction) { + case LEFT -> worldX -= speed; + case RIGHT -> worldX += speed; + case UP -> worldY -= speed; + default -> worldY += speed; + } + } + + // update Entity sprite after a set counter + // spriteMax is actually the value after which sprite is switched + // say spriteMax was 15, then the next sprite would be drawn after + // the previous sprite has been drawn for 15 times already + spriteCounter++; + if (spriteCounter > spriteMax) { + if (spriteNum == 1) { + spriteNum = 2; + } else if (spriteNum == 2) { + spriteNum = 1; + } + spriteCounter = 0; + } + } + + /** + * Draws the entity on the map, depending on if it is in view. + * + * @param g2 Graphics to draw + */ + public void draw(Graphics2D g2) { + BufferedImage image = null; + int screenX = worldX - gp.player.worldX + gp.player.screenX; + int screenY = worldY - gp.player.worldY + gp.player.screenY; + + // draw an entity if camera is in focus, that is the player is nearby the entity + // otherwise save processing and conserve resources + // (the entities are still updated, just not drawn out of window) + if (worldX + TILE_SIZE > gp.player.worldX - gp.player.screenX + && worldX - TILE_SIZE < gp.player.worldX + gp.player.screenX + && worldY + TILE_SIZE > gp.player.worldY - gp.player.screenY + && worldY - TILE_SIZE < gp.player.worldY + gp.player.screenY) { + switch (direction) { + case UP -> { + if (spriteNum == 1) { + image = upR; + } + if (spriteNum == 2) { + image = upL; + } + } + case DOWN -> { + if (spriteNum == 1) { + image = downR; + } + if (spriteNum == 2) { + image = downL; + } + } + case LEFT -> { + if (spriteNum == 1) { + image = leftR; + } + if (spriteNum == 2) { + image = leftL; + } + } + case RIGHT -> { + if (spriteNum == 1) { + image = rightR; + } + if (spriteNum == 2) { + image = rightL; + } + } + default -> { + } + } + // if entity is invincible, draw it as transparent for few seconds + if (invincible) { + g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f)); + } + g2.drawImage(image, screenX, screenY, TILE_SIZE, TILE_SIZE, null); + // Draw hitbox for debugging purposes + // g2.setColor(Color.red); + // g2.drawRect(screenX + hitboxDefaultX,screenY+ hitboxDefaultY, hitbox.width, hitbox.height); + g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f)); + } + } + + // bunch of Getters and setters + public int getWorldX() { + return worldX; + } + + public void setWorldX(int worldX) { + this.worldX = worldX; + } + + public int getWorldY() { + return worldY; + } + + public void setWorldY(int worldY) { + this.worldY = worldY; + } + + public int getScreenX() { + return screenX; + } + + public int getScreenY() { + return screenY; + } + + public int getSpeed() { + return speed; + } + + public void setSpeed(int newSpeed) { + speed = newSpeed; + } + + public Directions peekDirection() { + return direction; + } + + public void changeDirection(Directions direction) { + this.direction = direction; + } + + public void setCollided(boolean colStat) { + collision = colStat; + } + + public abstract void setAction(); + +} diff --git a/src/main/java/org/project/Entities/KeyHandler.java b/src/main/java/org/project/Entities/KeyHandler.java new file mode 100644 index 0000000..2f713de --- /dev/null +++ b/src/main/java/org/project/Entities/KeyHandler.java @@ -0,0 +1,53 @@ +package org.project.Entities; + +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; + +/** + * Manages all key input from the player. + * + * @author Abhishek Chouhan + * @version 2023-04-04 + */ +public class KeyHandler implements KeyListener { + public boolean upPressed, downPressed, leftPressed, rightPressed; // keys pressed + + @Override + public void keyTyped(KeyEvent e) { + } // not using this one garbage + + @Override + public void keyPressed(KeyEvent e) { + int code = e.getKeyCode(); + if (code == KeyEvent.VK_W) { + upPressed = true; + } + if (code == KeyEvent.VK_S) { + downPressed = true; + } + if (code == KeyEvent.VK_A) { + leftPressed = true; + } + if (code == KeyEvent.VK_D) { + rightPressed = true; + } + } + + @Override + public void keyReleased(KeyEvent e) { + int code = e.getKeyCode(); + if (code == KeyEvent.VK_W) { + upPressed = false; + } + if (code == KeyEvent.VK_S) { + downPressed = false; + } + if (code == KeyEvent.VK_A) { + leftPressed = false; + } + if (code == KeyEvent.VK_D) { + rightPressed = false; + } + } + +} diff --git a/src/main/java/org/project/Entities/Monster.java b/src/main/java/org/project/Entities/Monster.java new file mode 100644 index 0000000..6d7923f --- /dev/null +++ b/src/main/java/org/project/Entities/Monster.java @@ -0,0 +1,150 @@ +package org.project.Entities; + +import java.awt.*; +import java.io.FileInputStream; +import java.io.IOException; +import javax.imageio.ImageIO; +import org.project.ui.GamePanel; + +import static org.project.SystemVariables.*; + +/** + * Monster class for Beast is slated to be in the same vein as NPC. + * The collision for this end will revolve around the player's lives. + * Entity class is extended by this monster class, and al following monster classes. + * Entity, GamePanel, ObjectHandler and CollisionDetector will all require code + * May use pathfinding algorithm if I can develop it to be functional. + * Update the coordinates for future use to have it in different sections + * of the map to guard rubies. + * + * @author Amrit Singh + * @version 2023-03-27 + */ +public class Monster extends Entity { + + /** + * Constructs the Monster object. + * + * @param gp GamePanel it belongs to + * @param posX Monster position X + * @param posY Monster position Y + */ + public Monster(GamePanel gp, int posX, int posY) { + super(gp); + type = 2; + speed = 2; + hitbox = new Rectangle(); + hitbox.x = 8; + hitbox.y = 8; + hitboxDefaultX = hitbox.x; + hitboxDefaultY = hitbox.y; + hitbox.width = 32; + hitbox.height = 32; + this.worldX = posX; + this.worldY = posY; + direction = Directions.DOWN; + getImage(); + } + + /** + * Gets the image for each sprite. + */ + public void getImage() { + try { + downR = ImageIO.read(new FileInputStream("assets/monsters/Slime_Contract.png")); + downL = ImageIO.read(new FileInputStream("assets/monsters/Slime_Relaxed.png")); + upR = ImageIO.read(new FileInputStream("assets/monsters/Slime_Contract.png")); + upL = ImageIO.read(new FileInputStream("assets/monsters/Slime_Relaxed.png")); + leftR = ImageIO.read(new FileInputStream("assets/monsters/Slime_Contract.png")); + leftL = ImageIO.read(new FileInputStream("assets/monsters/Slime_Relaxed.png")); + rightR = ImageIO.read(new FileInputStream("assets/monsters/Slime_Contract.png")); + rightL = ImageIO.read(new FileInputStream("assets/monsters/Slime_Relaxed.png")); + } catch (IOException e) { + System.out.println("Image can't be read ..."); + e.printStackTrace(); + } + } + + /** + * method to set the behaviour of the monsters/slime. + * The searchPath helper function determines the path the entity should take to the player. + * The searchPath utilizes the pFinder (PathFinder class instance) which in turn utilizes + * the concept of A* path finding algorithm, a popular path finding algorithm in games. + */ + @Override + public void setAction() { + int goalCol = (gp.player.worldX + gp.player.hitbox.x) / TILE_SIZE; + int goalRow = (gp.player.worldY + gp.player.hitbox.y) / TILE_SIZE; + searchPath(goalCol, goalRow); + } + + /** + * Determines the path the monster should take to the player. + * + * @param goalCol Column of the goal location + * @param goalRow Row of the goal location + */ + public void searchPath(int goalCol, int goalRow) { + + int startCol = (worldX + hitbox.x) / TILE_SIZE; + int startRow = (worldY + hitbox.y) / TILE_SIZE; + + gp.pFinder.setNodes(startCol, startRow, goalCol, goalRow); + + if (gp.pFinder.search()) { + // Next worldX and worldY + int nextX = gp.pFinder.pathList.get(0).col * TILE_SIZE; + int nextY = gp.pFinder.pathList.get(0).row * TILE_SIZE; + + // Entity's hitbox positioning on the world map + int enLeftX = worldX + hitbox.x; + int enRightX = worldX + hitbox.x + hitbox.width; + int enTopY = worldY + hitbox.y; + int enBottomY = worldY + hitbox.y + hitbox.height; + + if (enTopY > nextY && enLeftX >= nextX && enRightX < nextX + TILE_SIZE) { + direction = Directions.UP; + } else if (enBottomY < nextY && enLeftX >= nextX && enRightX < nextX + TILE_SIZE) { + direction = Directions.DOWN; + } else if (enTopY >= nextY && enBottomY < nextY + TILE_SIZE) { + if (enLeftX > nextX) { + direction = Directions.LEFT; + } else if (enLeftX < nextX) { + direction = Directions.RIGHT; + } + } else if (enTopY > nextY && enLeftX > nextX) { + direction = Directions.UP; + checkCollision(); + if (collision) { + direction = Directions.LEFT; + } + } else if (enTopY > nextY && enLeftX < nextX) { + direction = Directions.UP; + checkCollision(); + if (collision) { + direction = Directions.RIGHT; + } + } else if (enTopY < nextY && enLeftX > nextX) { + direction = Directions.DOWN; + checkCollision(); + if (collision) { + direction = Directions.LEFT; + } + } else if (enTopY < nextY && enLeftX < nextX) { + direction = Directions.DOWN; + checkCollision(); + if (collision) { + direction = Directions.RIGHT; + } + } + + int nextCol = gp.pFinder.pathList.get(0).col; + int nextRow = gp.pFinder.pathList.get(0).row; + if (nextCol == goalCol && nextRow == goalRow) { + onPath = false; + } + } + } + +} + diff --git a/src/main/java/org/project/Entities/Player.java b/src/main/java/org/project/Entities/Player.java new file mode 100644 index 0000000..22a445c --- /dev/null +++ b/src/main/java/org/project/Entities/Player.java @@ -0,0 +1,316 @@ +package org.project.Entities; + +import org.project.Objects.*; +import org.project.Datastate.SaveState; +import org.project.Datastate.SaveStateHandler; +import org.project.ui.GamePanel; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.io.FileInputStream; +import java.io.IOException; + +import static org.project.SystemVariables.*; + +/** + * The player that the user can control. + * Contains a bunch of methods to update player position, Status, sprites and interaction + * with world components in Ruby Rush. + * + * @author Abhishek Chouhan + * @version 2023-02-07 + */ +public class Player extends Entity { + + // keyHandler instance ( can be public because they are no attributes in keyHandler to be modified) + public final KeyHandler handler; + private static Player instance; + + // Player stats + public static final int MAX_LIVES = 6; + + private Status currentStatus; + private int currentLives; + private int currentRubies; + + // player does not need a setAction method because it is being controlled by the User + // hence, we override it with an empty definition that is not being used. + @Override + public void setAction() { + } + + /** + * private constructor to initialize an instance of Player class + * + * @param gp the window/ GamePanel in which the player is being drawn in + * @param kh KeyHandler class that interprets input (to be utilized a lot to update player direction) + */ + private Player(GamePanel gp, KeyHandler kh) { + super(gp); // call the super constructor in entity class + + // initialize window variable and keyHandler instance + this.gp = gp; + this.handler = kh; + + // set Starting position and other attributes (hitbox, speed and initial direction) + this.worldX = TILE_SIZE * 37; // 37 is starting x coordinate on our 50 * 50 map + this.worldY = TILE_SIZE * 9; // 9 is starting y coordinate on our 50 * 50 map + + // fix player to the center of the screen, it is not the player who's actually moving + // it's the camera/ map :) + this.screenX = (gp.screenWidth - TILE_SIZE) / 2; // divide by half to get to centre + this.screenY = (gp.screenHeight - TILE_SIZE) / 2; + + /* + hitbox values being initialized. + the 10, 16 are the x and y coordinate of the hitbox relative to the player + Specifically, these are from the top-left corner of the player's sprite. + */ + this.hitbox = new Rectangle(10, 16, 28, 28); + this.hitboxDefaultX = 10; + this.hitboxDefaultY = 16; + + this.speed = 4; + this.direction = Directions.DOWN; // initial direction of the player + + this.currentLives = MAX_LIVES; + this.currentRubies = 0; + this.currentStatus = Status.ALIVE; + getPlayerImage(); + } + + /** + * method to get an instance of the player class from the private constructor. + * This method ensures only one instance of player can exist at a time. + * + * @param gp the GamePanel used in the game thread + * @param kh the keyhandler object handling all key inputs + * @return new player object if new instance, else return previously instantiated instance. + */ + public static Player getInstance(GamePanel gp, KeyHandler kh) { + if (instance == null) { + instance = new Player(gp, kh); + } + return instance; + } + + /** + * sets up all image instances of the player. + */ + public void getPlayerImage() { + try { + downR = ImageIO.read(new FileInputStream("assets/player/PlayerDownR.png")); + downL = ImageIO.read(new FileInputStream("assets/player/PlayerDownL.png")); + upR = ImageIO.read(new FileInputStream("assets/player/PlayerUpR.png")); + upL = ImageIO.read(new FileInputStream("assets/player/PlayerUpL.png")); + leftR = ImageIO.read(new FileInputStream("assets/player/PlayerLeftR.png")); + leftL = ImageIO.read(new FileInputStream("assets/player/PlayerLeftL.png")); + rightR = ImageIO.read(new FileInputStream("assets/player/PlayerRightR.png")); + rightL = ImageIO.read(new FileInputStream("assets/player/PlayerRightL.png")); + } catch (IOException e) { + System.out.println("Image can't be read"); + e.printStackTrace(); + } + } + + /** + * manages the position, sprite and all interactions of the player. + * Utilizes a bunch of helper and modular functions to achieve above functionality. + * + * @param gp the window Instance in which the player is being drawn in + * @param kh the keyHandler Instance taking care of all inputs + */ + public void update(GamePanel gp, KeyHandler kh) { + if (currentLives == 0) { + currentStatus = Status.DEAD; // set player to be dead if lives reaches + } + // if a keyEvent occurred, update player values + if (kh.upPressed || kh.downPressed || kh.leftPressed || kh.rightPressed) { + // update the direction of player + updateDirection(kh); + + // check collision with tile + collision = false; + gp.cDetector.checkTile(this); + + // Check collision/Interaction with objects + int objectIndex = gp.cDetector.checkObject(this, true); + pickupObject(objectIndex, gp); + + // Check collision with NPC + int npcIndex = gp.cDetector.checkEntityCollide(this, gp.npc); + interactNPC(npcIndex); + + // Check collision with Monster + int monsterIndex = gp.cDetector.checkEntityCollide(this, gp.monster); + interactMonster(monsterIndex); + + // update the position of the player on world map + updatePosition(); + + // update to next sprite the player should be drawn as + updateSprite(); + + } + } + + /** + * method used to update the direction of the player. + * This function uses the input caught by the keyHandler class (stored as a static variable in kh) + * By accessing that variable we know what the last key pressed was and update direction accordingly. + * + * @param kh the keyHandler instance + */ + private void updateDirection(KeyHandler kh) { + if (kh.leftPressed) { + direction = Directions.LEFT; + } else if (kh.rightPressed) { + direction = Directions.RIGHT; + } else if (kh.upPressed) { + direction = Directions.UP; + } else { + direction = Directions.DOWN; + } + } + + /** + * helper function to update player sprite to next frame to animate player movement. + * Utilized in update method. + */ + private void updateSprite() { + /* loop between the frames of the player to animate*/ + spriteCounter++; // increment to count how many frames this sprite has been drawn already + if (spriteCounter > 14) { // change sprite to be drawn after every 14 images drawn + // switch sprites + if (spriteNum == 1) { + spriteNum = 2; + } else if (spriteNum == 2) { + spriteNum = 1; + } + spriteCounter = 0; // reset sprite Counter to 0 to count how many times the next sprite's been drawn + } + // draw 60 frames of INVINCIBLE player, that is show player is invincible for 1 sec after taking damage. + else if (invincible) { + invincibleCounter++; + if (invincibleCounter > 60) { // 60 Frames per second hence player will be shown invincible for 1 sec + invincible = false; + invincibleCounter = 0; + } + } + } + + /** + * helper function to update player position. + * Utilized in update method. + */ + private void updatePosition() { + // update position on worldMap if player is not colliding + if (!collision) { + if (direction == Directions.LEFT) { + worldX = worldX - speed; + } else if (direction == Directions.RIGHT) { + worldX = worldX + speed; + } else if (direction == Directions.UP) { + worldY = worldY - speed; + } else { + worldY = worldY + speed; + } + } + } + + /** + * Defines object collision/pickingUp behaviour of player + * + * @param index The index of the player on the max + * @param gp The game panel the player belongs to + */ + public void pickupObject(int index, GamePanel gp) { + if (index != this.indexMax) { + Class className = gp.elements[index].getClass(); + + // check what kind of element the player ran into + if (className.equals(Ruby.class)) { + gp.playSE(1); + currentRubies++; + gp.elements[index] = null; + gp.ui.showMessage("You got a ruby!"); + SaveState.getInstance().setSaveState(gp); + SaveStateHandler.getInstance().save(); + } + // if player ran into Door object + else if (className.equals(Door.class)) { + if (currentRubies > 1) { // door can only be opened if the player has at least 1 ruby + gp.playSE(2); + gp.elements[index] = null; + gp.ui.showMessage("You opened a door!"); + currentRubies--; + } else { + gp.ui.showMessage("You need more rubies for this door!"); + } + } + // if player picked up a power up object + else if (className.equals(PowerUp.class)) { + gp.playSE(3); + speed += 2; + gp.elements[index] = null; + gp.ui.showMessage("Speed mode: ON"); + } + // if player ran into fire, just decrease player life. + else if (className.equals(Fire.class)) { + gp.ui.showMessage("Fire Hazard!!"); + // set player as invincible to avoid damaging all lives at once in short duration + if (!invincible) { + currentLives--; + } + invincible = true; + } + } + } + + /** + * Method to handle player interaction with NPC- docile characters. + * + * @param index the hit area passed by index + */ + public void interactNPC(int index) { + if (index != this.indexMax) { + gp.ui.showMessage("Watch where you're going!"); + } + } + + /** + * method to handle player interaction with hostile characters. + * + * @param index the hit area passed by index + */ + public void interactMonster(int index) { + if (index != this.indexMax) { + if (!invincible) { + currentLives--; + invincible = true; + } + gp.ui.showMessage("Monster.. RUN!!"); + } + } + + // A bunch of getters and setters for instance variables + public int getLives() { + return currentLives; + } + + public void setLives(int lives) { + this.currentLives = lives; + } + + public int getCurrentRubies() { + return currentRubies; + } + + public void setCurrentRubies(int rubies) { + this.currentRubies = rubies; + } + + public Status getCurrentStatus() { + return currentStatus; + } +} diff --git a/src/main/java/org/project/Entities/Villager.java b/src/main/java/org/project/Entities/Villager.java new file mode 100644 index 0000000..be223b7 --- /dev/null +++ b/src/main/java/org/project/Entities/Villager.java @@ -0,0 +1,89 @@ +package org.project.Entities; + + +import java.awt.*; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Random; +import javax.imageio.ImageIO; + +import org.project.ui.GamePanel; + +import static org.project.SystemVariables.*; + +/** + * Villager is docile. Just randomly moves around. + * This is the base file code independent of anything else. + * Entity class will need to be made. + * Sprite, GamePanel and SpriteManager will all require code + * for this class to function as intended. + * To display, update, collide and interact. + * + * @author Amrit Singh + * @version 2023-02-07 + */ +public class Villager extends Entity { + /** + * Constructs the Villager object. + * + * @param gp GamePanel what it belongs to + * @param posX Villager position X + * @param posY Villager position Y + */ + public Villager(GamePanel gp, int posX, int posY) { + super(gp); + this.gp = gp; + hitbox = new Rectangle(); + hitbox.x = 8; + hitbox.y = 8; + hitboxDefaultX = hitbox.x; + hitboxDefaultY = hitbox.y; + hitbox.width = 32; + hitbox.height = 32; + this.worldX = posX; + this.worldY = posY; + direction = Directions.DOWN; + speed = 2; + getImage(); + } + + /** + * Gets the image for each sprite. + */ + public void getImage() { + try { + downR = ImageIO.read(new FileInputStream("assets/player/oldman_down_right.png")); + downL = ImageIO.read(new FileInputStream("assets/player/oldman_down_left.png")); + upR = ImageIO.read(new FileInputStream("assets/player/oldman_up_right.png")); + upL = ImageIO.read(new FileInputStream("assets/player/oldman_up_left.png")); + leftR = ImageIO.read(new FileInputStream("assets/player/oldman_left_right.png")); + leftL = ImageIO.read(new FileInputStream("assets/player/oldman_left_left.png")); + rightR = ImageIO.read(new FileInputStream("assets/player/oldman_right_right.png")); + rightL = ImageIO.read(new FileInputStream("assets/player/oldman_right_left.png")); + } catch (IOException e) { + System.out.println("Image can't be read ..."); + e.printStackTrace(); + } + } + + @Override + public void setAction() { + actionLockCounter++; + if (actionLockCounter == 120) { + Random random = new Random(); + int i = random.nextInt(100) + 1; // picks up a number from 1 to 100 + + switch (i / 25) { + case 0 -> direction = Directions.UP; + case 1 -> direction = Directions.DOWN; + case 2 -> direction = Directions.LEFT; + case 3 -> direction = Directions.RIGHT; + default -> { + // Display unexpected value and log. + System.err.println("Unexpected value: " + i); + } + } + actionLockCounter = 0; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/project/Main.java b/src/main/java/org/project/Main.java new file mode 100644 index 0000000..80af4c8 --- /dev/null +++ b/src/main/java/org/project/Main.java @@ -0,0 +1,17 @@ +package org.project; + +import java.io.IOException; +import org.project.ui.GameLoader; + + +/** + * Method that runs the project. + * + * @author Nathan Bartyuk + * @version 2023-04-04 + */ +public class Main { + public static void main(String[] args) throws IOException { + new GameLoader(); // Boots the game loader + } +} diff --git a/src/main/java/org/project/Map/Node.java b/src/main/java/org/project/Map/Node.java new file mode 100644 index 0000000..3a819df --- /dev/null +++ b/src/main/java/org/project/Map/Node.java @@ -0,0 +1,41 @@ +package org.project.Map; + +/** + * This class represents a node used in the Path Finder algorithm. + *

+ * It contains pointers to adjacent nodes and various variables for calculation. + * + * @author Amrit Jhatu + * @version 2023-04-04 + */ +public class Node { + // A reference to the parent node + public Node parent; + + // References to adjacent nodes + public Node up; + public Node down; + public Node left; + public Node right; + + // Variables for calculation + public int col; // Column index of the node + public int row; // Row index of the node + protected int gCost; // Cost from the starting node to this node + protected int hCost; // Heuristic cost from this node to the goal node + protected int fCost; // Total cost (gCost + hCost) of reaching the goal node through this node + protected boolean solid; // Indicates whether this node is an obstacle or not + protected boolean open; // Indicates whether this node is open or not (i.e., not yet visited) + protected boolean checked; // Indicates whether this node has been checked during the search process or not + + /** + * Creates a new Node object with the given column and row indices. + * + * @param col The column index of the node. + * @param row The row index of the node. + */ + public Node(int col, int row) { + this.col = col; + this.row = row; + } +} diff --git a/src/main/java/org/project/Map/PathFinder.java b/src/main/java/org/project/Map/PathFinder.java new file mode 100644 index 0000000..a50620f --- /dev/null +++ b/src/main/java/org/project/Map/PathFinder.java @@ -0,0 +1,329 @@ +package org.project.Map; + +import org.project.ui.GamePanel; + +import java.util.ArrayList; + +import static org.project.SystemVariables.*; + +/** + * The PathFinder class is responsible for finding the shortest path between two nodes in a grid. + * It uses the A* algorithm to determine the path from the start node to the goal node. + * The algorithm calculates the distance between nodes, and uses that to determine the shortest possible path. + * The class contains helper methods to open nodes, check surrounding nodes, and track the path. + * + * @author Amrit Jhatu + * @version 2023-04-04 + */ +public class PathFinder { + GamePanel gp; + Node[][] nodes; + ArrayList openList = new ArrayList<>(); + public ArrayList pathList = new ArrayList<>(); + + Node startNode, goalNode, currentNode; + boolean goalReached = false; + int step = 0; + + /** + * Constructs the new PathFinder object + * + * @param gp GamePanel it applies to + */ + public PathFinder(GamePanel gp) { + this.gp = gp; + instantiateNodes(); + connectNodes(); + } + + /** + * Sets up nodes for calculations. + * This method instantiates all the nodes in the game's map + */ + public void instantiateNodes() { + // Create a 2D array of nodes with dimensions MAP_COL by MAP_ROW + nodes = new Node[MAP_COL][MAP_ROW]; + + // Initialize row and column variables to 0 + int col = 0; + int row = 0; + + // Iterate through each row and column of the 2D array and create a new Node object at each position + while (col < MAP_COL && row < MAP_ROW) { + + // Create a new Node object at the current row and column position + nodes[col][row] = new Node(col, row); + + // Move to the next column position + col++; + + // If we have reached the end of the current row, move to the next row and reset the column position to 0 + if (col == MAP_COL) { + col = 0; + row++; + } + } + } + + /** + * this method here sets up our custom data structure + * A network of nodes, that were initially only being stored in a 2d array of nodes. + * Now that the nodes are connected, we can easily reference up, down, left and right + * to quickly access the adjacent nodes (storing tile int data) and speed up process. + */ + public void connectNodes() { +// nodes = new Node[numRows][numCols]; + for (int i = 0; i < MAP_ROW; i++) { + for (int j = 0; j < MAP_COL; j++) { +// nodes[i][j] = new Node(data[i][j]); + if (i > 0) { + nodes[i][j].up = nodes[i - 1][j]; + } + if (i < MAP_ROW - 1) { // one less than MapRow as index is from 0 + nodes[i][j].down = nodes[i + 1][j]; + } + if (j > 0) { + nodes[i][j].left = nodes[i][j - 1]; + } + if (j < MAP_ROW - 1) { // one less than MapRow as index is from 0 + nodes[i][j].right = nodes[i][j + 1]; + } + } + } + } + + /** + * Reset nodes to default value. + */ + public void resetNodes() { + // Initialize row and column variables to 0 + int col = 0; + int row = 0; + + // Iterate through each row and column of the 2D array of nodes + while (col < MAP_COL && row < MAP_ROW) { + // Reset the open, checked, and solid values for the current node to false + nodes[col][row].open = false; + nodes[col][row].checked = false; + nodes[col][row].solid = false; + + // Move to the next column position + col++; + + // If we have reached the end of the current row, move to the next row and reset the column position to 0 + if (col == MAP_COL) { + col = 0; + row++; + } + } + + // Clear the lists of open and path nodes + openList.clear(); + pathList.clear(); + + // Reset the goalReached flag and step counter to their default values + goalReached = false; + step = 0; + } + + + /** + * Set nodes to specific values. + * + * @param startCol Current node column + * @param startRow Current node row + * @param goalCol Goal node column + * @param goalRow Goal node row + */ + public void setNodes(int startCol, int startRow, int goalCol, int goalRow) { + // Reset all nodes in the map to their default values + resetNodes(); + + // Set the start, goal, and current nodes + startNode = nodes[startCol][startRow]; + currentNode = startNode; + goalNode = nodes[goalCol][goalRow]; + + // Add the current node to the list of open nodes + openList.add(currentNode); + + // Iterate through each row and column of the 2D array of nodes + int col = 0; + int row = 0; + while (col < MAP_COL && row < MAP_ROW) { + + // Get the tile number for the current position + int tileNum = gp.tManager.map[col][row]; + + // If the tile is solid, mark the corresponding node as solid + if (gp.tManager.tiles[tileNum].collision) { + nodes[col][row].solid = true; + } + + // Set the cost for the current node + getCost(nodes[col][row]); + + // Move to the next column position + col++; + + // If we have reached the end of the current row, move to the next row and reset the column position to 0 + if (col == MAP_COL) { + col = 0; + row++; + } + } + } + + /** + * Get the A-Star cost + * + * @param node Node to calculate + */ + public void getCost(Node node) { + + // Calculate the g cost (the cost of moving from the start node to this node) + int xDistance = Math.abs(node.col - startNode.col); + int yDistance = Math.abs(node.row - startNode.row); + node.gCost = xDistance + yDistance; + + // Calculate the h cost (the heuristic cost of moving from this node to the goal node) + xDistance = Math.abs(node.col - goalNode.col); + yDistance = Math.abs(node.row - goalNode.row); + node.hCost = xDistance + yDistance; + + // Calculate the f cost (the total cost of moving from the start node to this node, then to the goal node) + node.fCost = node.gCost + node.hCost; + } + + /** + * Searches for the shortest path from the start node to the goal node using the A* algorithm. + * + * @return true if the goal is reached, false otherwise + */ + public boolean search() { + // Continue searching until the goal is reached or the maximum number of steps is reached + while (!goalReached && step < 500) { + // Check the current node + checkCurrentNode(); + + // Check the surrounding nodes + checkSurroundingNodes(); + + // Find the node with the lowest fCost + int bestNodeIndex = findBestNodeIndex(); + + // If there is no node in the openList, end the loop + if (bestNodeIndex == -1) { + break; + } + + // Set the currentNode to the best node + currentNode = openList.get(bestNodeIndex); + + // If the currentNode is the goalNode, set goalReached to true and track the path + if (currentNode == goalNode) { + goalReached = true; + trackThePath(); + } + + // Increment the step count + step++; + } + + // Return whether the goal was reached or not + return goalReached; + } + + /** + * Marks the current node as checked and removes it from the openList. + */ + private void checkCurrentNode() { + currentNode.checked = true; + openList.remove(currentNode); + } + + /** + * Checks the surrounding nodes and adds them to the openList if they meet the necessary conditions. + */ + private void checkSurroundingNodes() { + int col = currentNode.col; + int row = currentNode.row; + + // Check the node above + if (row - 1 >= 0) { + openNode(currentNode.up); + } + + // Check the node to the left + if (col - 1 >= 0) { + openNode(currentNode.left); + } + + // Check the node below + if (row + 1 < MAP_ROW) { + openNode(currentNode.down); + } + + // Check the node to the right + if (col + 1 < MAP_COL) { + openNode(currentNode.right); + } + } + + /** + * Finds the node with the lowest fCost in the openList. + * + * @return the index of the best node in the openList, or -1 if the openList is empty + */ + private int findBestNodeIndex() { + int bestNodeIndex = -1; + int bestNodefCost = MAX_INDEX; + + // Iterate through the openList to find the node with the lowest fCost + for (int i = 0; i < openList.size(); i++) { + Node node = openList.get(i); + + // If the node has a lower fCost than the current best node, update the best node + if (node.fCost < bestNodefCost) { + bestNodeIndex = i; + bestNodefCost = node.fCost; + + // If the node has the same fCost as the current best node, use the gCost to break the tie + } else if (node.fCost == bestNodefCost) { + if (node.gCost < openList.get(bestNodeIndex).gCost) { + bestNodeIndex = i; + } + } + } + + // Return the index of the best node, or -1 if the openList is empty + return bestNodeIndex; + } + + /** + * Adds the specified node to the open list if it is not solid, has not been checked before, + * and has not already been added to the open list. + * + * @param node the node to be added to the open list + */ + public void openNode(Node node) { + if (!node.open && !node.checked && !node.solid) { + node.open = true; + node.parent = currentNode; + openList.add(node); + } + } + + /** + * Tracks the path from the start node to the goal node by following the chain of parent nodes + * starting at the goal node and working backwards to the start node. + * The resulting path is stored in the pathList in reverse order. + */ + public void trackThePath() { + Node current = goalNode; + while (current != startNode) { + pathList.add(0, current); // Add the node to the start of the list + current = current.parent; + } + } +} diff --git a/src/main/java/org/project/Map/Positionable.java b/src/main/java/org/project/Map/Positionable.java new file mode 100644 index 0000000..52f1964 --- /dev/null +++ b/src/main/java/org/project/Map/Positionable.java @@ -0,0 +1,17 @@ +package org.project.Map; + +/** + * Interface to unite all entities and savable elements + * + * @author Greg Song + * @version 2023-04-04 + */ +public interface Positionable { + int getWorldX(); + + int getWorldY(); + + void setWorldX(int worldX); + + void setWorldY(int worldY); +} \ No newline at end of file diff --git a/src/main/java/org/project/Map/Tile.java b/src/main/java/org/project/Map/Tile.java new file mode 100644 index 0000000..f16c8f3 --- /dev/null +++ b/src/main/java/org/project/Map/Tile.java @@ -0,0 +1,14 @@ +package org.project.Map; + +import java.awt.image.BufferedImage; + +/** + * Abstract class for a static element on the map + * + * @author Nathan Bartyuk + * @version 2023-03-28 + */ +public class Tile { + public BufferedImage sprite; + public boolean collision = false; +} diff --git a/src/main/java/org/project/Map/TileManager.java b/src/main/java/org/project/Map/TileManager.java new file mode 100644 index 0000000..35ebc9d --- /dev/null +++ b/src/main/java/org/project/Map/TileManager.java @@ -0,0 +1,121 @@ +package org.project.Map; + +import org.project.ui.GamePanel; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.io.*; + +import static org.project.SystemVariables.*; + +/** + * Manages the static elements on the map + * + * @author Nathan Bartyuk + * @version 2023-03-28 + */ +public class TileManager { + GamePanel gp; + public Tile[] tiles; + protected int[][] map; + + protected String mapPath = "assets/data/maps/map2.txt"; + protected String tilePath = "assets/data/tiles/"; + protected String[] tileName = {"grass.png", "wall.png", "water.png", "earth.png", "tree.png", "sand.png"}; + protected boolean[] tileCollide = {false, true, true, false, true, false}; + + /** + * Constructs a new TileManager object + * + * @param gp the GamePanel it belongs to + */ + public TileManager(GamePanel gp) { + this.gp = gp; + tiles = new Tile[10]; + map = new int[MAP_COL][MAP_ROW]; + getTileImage(); + loadMap(); + } + + /** + * Reads the map and gets assets for each tile + */ + public void getTileImage() { + try { + for (int i = 0; i < 6; i++) { + tiles[i] = new Tile(); + tiles[i].sprite = ImageIO.read(new FileInputStream(tilePath + tileName[i])); + tiles[i].collision = tileCollide[i]; + } + } catch (IOException e) { + System.out.println("Invalid tile image"); + e.printStackTrace(); + } + } + + /** + * Parses map assets from file + */ + public void loadMap() { + try { + InputStream is = new FileInputStream(mapPath); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + int col = 0; + int row = 0; + + while (col < MAP_COL && row < MAP_ROW) { + String line = br.readLine(); + String[] numbers = line.split(" "); + while (col < MAP_COL) { + int num = Integer.parseInt(numbers[col]); + map[col][row] = num; + col++; + } + if (col == MAP_COL) { + col = 0; + row++; + } + } + br.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Draws the map to screen + * + * @param g2 the GamePanel to draw to + */ + public void draw(Graphics2D g2) { + int worldCol = 0; + int worldRow = 0; + + // Only processes sprites in the parsed map + while (worldCol < MAP_COL && worldRow < MAP_ROW) { + int tileNum = map[worldCol][worldRow]; + int worldX = worldCol * TILE_SIZE; + int worldY = worldRow * TILE_SIZE; + int screenX = worldX - gp.player.getWorldX() + gp.player.getScreenX(); + int screenY = worldY - gp.player.getWorldY() + gp.player.getScreenY(); + + // Only draws sprites in the window view + if (worldX + TILE_SIZE > gp.player.getWorldX() - gp.player.getScreenX() && + worldX - TILE_SIZE < gp.player.getWorldX() + gp.player.getScreenX() && + worldY + TILE_SIZE > gp.player.getWorldY() - gp.player.getScreenY() && + worldY - TILE_SIZE < gp.player.getWorldY() + gp.player.getScreenY()) { + g2.drawImage(tiles[tileNum].sprite, screenX, screenY, TILE_SIZE, TILE_SIZE, null); + } + + worldCol++; + if (worldCol == MAP_COL) { + worldCol = 0; + worldRow++; + } + } + } + + public int[][] getMap() { + return map; + } +} diff --git a/src/main/java/org/project/Menu/Menu.java b/src/main/java/org/project/Menu/Menu.java new file mode 100644 index 0000000..854331a --- /dev/null +++ b/src/main/java/org/project/Menu/Menu.java @@ -0,0 +1,142 @@ +package org.project.Menu; + +import java.io.IOException; +import javax.swing.JPanel; +import java.awt.*; +import java.util.ArrayList; +import java.util.Collections; +import org.project.ui.GameLoader; +import org.project.ui.GamePanel; + + +/** + * The Menu class represents the Main menu screen of the game. It extends + * JPanel and is responsible for initializing the MenuComponents. + * + * @author Greg Song + * @version 2023-04-09 + */ +public class Menu extends JPanel { + /* Constants */ + private static final Dimension PANEL_SIZE = new Dimension(768, 576); + private static final Dimension BUTTON_SIZE = new Dimension(300, 100); + private static final Dimension LABEL_SIZE = new Dimension(100, 50); + private static final Dimension INPUT_SIZE = new Dimension(300, 30); + private static final Dimension TITLE_SIZE = new Dimension(700, 150); + private static final String TITLE_IMAGE = "assets/menu/title.png"; + private static final String BG_IMAGE = "assets/menu/jungle.jpeg"; + private static final int PANEL_WIDTH_HALF = 768 / 2; + private static final int BUTTON_X_OFFSET = PANEL_WIDTH_HALF - (BUTTON_SIZE.width / 2); + private static final int TITLE_X_OFFSET = PANEL_WIDTH_HALF - (TITLE_SIZE.width / 2); + private static final int[] VERTICAL_OFFSETS = {50, 200, 250, 290, 400}; + private static final Font FONT = new Font("Arial", Font.BOLD, 20); + private static final Color BUTTON_COLOR = Color.orange; + private static final Color CLEAR_COLOR = new Color(0, 0, 0, 0); + + /* Variables */ + private final ArrayList components; + private final ArrayList buttons; + private final ArrayList visibles; + + /** + * Constructs the Menu. + * + * @param gameLoader, The GameLoader object + * @param gamePanel, The GamePanel to switch to. + * @throws IOException If image cannot be opened. + */ + public Menu(GameLoader gameLoader, GamePanel gamePanel) throws IOException { + this.setPreferredSize(PANEL_SIZE); + this.setBackground(Color.black); + this.setDoubleBuffered(true); + this.setFocusable(true); + components = new ArrayList<>(); + this.buttons = new ArrayList<>(); + this.visibles = new ArrayList<>(); + initComponents(); + new MenuManager(this, gameLoader, gamePanel); + } + + /** + * Helper method to add MenuComponents to components list. + * + * @param components The MenuComponent objects to be added to the menu. + */ + private void addComponents(MenuComponent... components) { + Collections.addAll(this.components, components); + } + + /** + * Initializes all the MenuComponents of the menu. + * + * @throws IOException If there is an error loading required images. + */ + private void initComponents() throws IOException { + MenuImage background = new MenuImage(0, 0, PANEL_SIZE.width, PANEL_SIZE.height, BG_IMAGE); + MenuImage title = new MenuImage(TITLE_X_OFFSET, VERTICAL_OFFSETS[0], + TITLE_SIZE.width, TITLE_SIZE.height, TITLE_IMAGE); + MenuLabel label = new MenuLabel( + BUTTON_X_OFFSET, VERTICAL_OFFSETS[1], LABEL_SIZE.width, + LABEL_SIZE.height, CLEAR_COLOR, BUTTON_COLOR, + FONT, "Username:", false); + MenuTextField input = new MenuTextField( + BUTTON_X_OFFSET, VERTICAL_OFFSETS[2], INPUT_SIZE.width, + INPUT_SIZE.height, FONT, 30, false); + MenuButton button1 = new MenuButton( + BUTTON_X_OFFSET, VERTICAL_OFFSETS[3], BUTTON_SIZE.width, + BUTTON_SIZE.height, BUTTON_COLOR, Color.BLACK, + FONT, "New Game", MenuButton.ButtonName.NEW_GAME); + MenuButton button2 = new MenuButton( + BUTTON_X_OFFSET, VERTICAL_OFFSETS[4], BUTTON_SIZE.width, + BUTTON_SIZE.height, BUTTON_COLOR, Color.BLACK, + FONT, "Load Game", MenuButton.ButtonName.LOAD_GAME); + addComponents(background, title, label, input, button1, button2); + buttons.add(button1); + buttons.add(button2); + visibles.add(label); + visibles.add(input); + } + + /** + * Overrides the paintComponent method. Used to paint all MenuComponents. + * + * @param g the Graphics object used for painting. + */ + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + for (MenuComponent component : components) { + component.draw(g); + } + } + + /** + * Gets the MenuTextField of this menu. + * + * @return MenuTextField for username. + */ + public MenuTextField getTextField() { + return (MenuTextField) this.components.get(3); + } + + /** + * Gets the MenuButtons of this menu. + * + * @return buttons The list of MenuButtons. + */ + public ArrayList getButtons() { + return buttons; + } + + /** + * Sets the visibility of MenuComponents that implement Visible. + * + * @param isVisible true if MenuComponent should be set visible, false + * if they should be hidden. + */ + public void setVisibles(boolean isVisible) { + for (Visible visible : visibles) { + visible.setVisible(isVisible); + } + } +} diff --git a/src/main/java/org/project/Menu/MenuButton.java b/src/main/java/org/project/Menu/MenuButton.java new file mode 100644 index 0000000..7db7219 --- /dev/null +++ b/src/main/java/org/project/Menu/MenuButton.java @@ -0,0 +1,86 @@ +package org.project.Menu; + +import java.awt.*; + +/** + * MenuButton class represents a button component for a menu. It extends + * MenuTextBox. + * + * @author Greg Song + * @version 2023-04-09 + */ +public class MenuButton extends MenuTextBox { + /** + * ButtonName used to differentiate which button is active. + */ + public enum ButtonName { + NEW_GAME, + LOAD_GAME, + BACK, + SUBMIT + } + + private ButtonName buttonName; + + /** + * Constructs a new MenuButton. + * + * @param x The x-coordinate of top-left corner of the text box. + * @param y The y-coordinate of the top-left corner of the text box. + * @param width Width of the text box. + * @param height Height of the text box. + * @param bgColor Background color + * @param textColor Text color + * @param font Font of the text in the text box. + * @param text String of the text to display. + * @param buttonName the Type of the button. + */ + public MenuButton(int x, int y, int width, int height, Color bgColor, + Color textColor, Font font, String text, + ButtonName buttonName) { + super(x, y, width, height, bgColor, textColor, font, text); + this.buttonName = buttonName; + } + + /** + * Draws the button using Graphics. + * + * @param g a Graphics object. + */ + public void draw(Graphics g) { + super.draw(g); + } + + /** + * Checks if the given x and y are within the bounds of the button. Used to + * check if button is clicked. + * + * @param x The x-coordinate of the point. + * @param y The y-coordinate of the point. + * @return true if given x and y are within bound of this button. + */ + public boolean contains(int x, int y) { + return x >= getX() && x <= getX() + getWidth() + && y >= getY() && y <= getY() + getHeight(); + } + + /** + * Sets the buttonName and text. + * + * @param buttonName Name of the button. + * @param text The text to display on the button. + */ + public void setButton(ButtonName buttonName, String text) { + this.buttonName = buttonName; + this.text = text; + } + + /** + * Returns the current buttonName. + * + * @return The name of the Button. + */ + public ButtonName getButtonName() { + return this.buttonName; + } +} diff --git a/src/main/java/org/project/Menu/MenuComponent.java b/src/main/java/org/project/Menu/MenuComponent.java new file mode 100644 index 0000000..d423b05 --- /dev/null +++ b/src/main/java/org/project/Menu/MenuComponent.java @@ -0,0 +1,75 @@ +package org.project.Menu; + +import java.awt.*; + +/** + * Abstract class for components for Menu. It provides the most common properties + * of menu components. All other components found in Menu extend from this. + * + * @author Greg Song + * @version 2023-04-09 + */ +public abstract class MenuComponent { + protected int x; + protected int y; + protected int width; + protected int height; + + /** + * Constructs a new MenuComponent. Used for inheritance. + * + * @param x The x-coordinate of the MenuComponent. + * @param y The y-coordinate of the MenuComponent. + * @param width The width of the MenuComponent. + * @param height The height of the MenuComponent. + */ + public MenuComponent(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + /** + * Abstract method to draw MenuComponents using Graphics object. + * + * @param g The Graphics object used to draw. + */ + public abstract void draw(Graphics g); + + /** + * Gets the width of the MenuComponent. + * + * @return The width + */ + public int getWidth() { + return this.width; + } + + /** + * Gets the height of the MenuComponent. + * + * @return The height. + */ + public int getHeight() { + return this.height; + } + + /** + * Gets the x-coordinate of the MenuComponent. + * + * @return The x-coordinate. + */ + public int getX() { + return this.x; + } + + /** + * Gets the y-coordinate of the MenuComponent. + * + * @return The y-coordinate. + */ + public int getY() { + return this.y; + } +} diff --git a/src/main/java/org/project/Menu/MenuImage.java b/src/main/java/org/project/Menu/MenuImage.java new file mode 100644 index 0000000..0e240c0 --- /dev/null +++ b/src/main/java/org/project/Menu/MenuImage.java @@ -0,0 +1,56 @@ +package org.project.Menu; + +import java.awt.*; +import java.io.File; +import java.io.IOException; +import javax.imageio.ImageIO; + +/** + * Represents an image component in the Menu. It extends the MenuComponent class, + * loads and displays an image. + * + * @author Greg Song + * @version 2023-04-09 + */ +public class MenuImage extends MenuComponent { + private Image image; + + /** + * Constructs a MenuImage. + * + * @param x The x-coordinate of this MenuImage. + * @param y The y-coordinate of this MenuImage. + * @param width The width of this MenuImage. + * @param height The height of this MenuImage. + * @param imagePath The file path of the image to display. + * @throws IOException If there is an error loading the image at given file path. + */ + public MenuImage(int x, int y, int width, int height, String imagePath) throws IOException { + super(x, y, width, height); + loadImage(imagePath); + } + + /** + * Loads the image from given file path. + * + * @param imagePath The file path of the image file. + * @throws IOException If there is an error loading the image at given file path. + */ + private void loadImage(String imagePath) throws IOException { + try { + image = ImageIO.read(new File(imagePath)); + } catch (IOException e) { + throw new IOException(e); + } + } + + /** + * Overrides draw method. Used to draw this image. + * + * @param g The Graphics object used to draw. + */ + @Override + public void draw(Graphics g) { + g.drawImage(image, x, y, width, height, null); + } +} diff --git a/src/main/java/org/project/Menu/MenuLabel.java b/src/main/java/org/project/Menu/MenuLabel.java new file mode 100644 index 0000000..a4fe766 --- /dev/null +++ b/src/main/java/org/project/Menu/MenuLabel.java @@ -0,0 +1,64 @@ +package org.project.Menu; + +import java.awt.*; + +/** + * Represents a label component in the Menu. It extends MenuTextBox and + * implements the Visible interface, allowing it to be hidden. + * + * @author Greg Song + * @version 2023-04-09 + */ +public class MenuLabel extends MenuTextBox implements Visible { + private boolean isVisible; + + /** + * Constructs a MenuLabel. + * @param x The x-coordinate of the label. + * @param y The y-coordinate of the label. + * @param width The width of the label. + * @param height The height of the label. + * @param bgColor The background color of the label. + * @param textColor The text color of the label. + * @param font The font of the label. + * @param text The text to display in the label. + * @param isVisible Whether the label is currently visible or not. + */ + public MenuLabel(int x, int y, int width, int height, Color bgColor, + Color textColor, Font font, String text, Boolean isVisible) { + super(x, y, width, height, bgColor, textColor, font, text); + this.isVisible = isVisible; + } + + /** + * Overrides draw method, used to draw the label. + * + * @param g a Graphics object. + */ + @Override + public void draw(Graphics g) { + if (isVisible) { + super.draw(g); + } + } + + /** + * Sets the visibility Status of the label. + * + * @param visible true if object should be visible, false otherwise. + */ + @Override + public void setVisible(boolean visible) { + this.isVisible = visible; + } + + /** + * Gets the visibility Status of the label. + * + * @return True if object is currently visible, false otherwise. + */ + @Override + public boolean isVisible() { + return this.isVisible; + } +} diff --git a/src/main/java/org/project/Menu/MenuManager.java b/src/main/java/org/project/Menu/MenuManager.java new file mode 100644 index 0000000..0bb01d5 --- /dev/null +++ b/src/main/java/org/project/Menu/MenuManager.java @@ -0,0 +1,248 @@ +package org.project.Menu; + +import org.project.Datastate.Client; +import org.project.Datastate.SaveState; +import org.project.Datastate.SaveStateHandler; +import org.project.ui.GameLoader; +import org.project.ui.GamePanel; + +import java.awt.*; +import java.awt.event.*; +import java.io.FileNotFoundException; +import java.util.ArrayList; + +/** + * The MenuManger class handles the logic and event handling for the Menu. + * It implements KeyListener and MouseListener interfaces to handle keyboard + * and mouse events. + * + * @author Greg Song + * @version 2023-04-09 + */ +public class MenuManager implements KeyListener, MouseListener { + private enum MenuState { + MAIN_MENU, + NEW_GAME, + LOAD_GAME + } + + private MenuState menuState; + private final Menu menu; + private final MenuTextField textField; + private final ArrayList buttons; + SaveStateHandler saveStateHandler; + SaveState saveState; + GamePanel gamePanel; + GameLoader gameLoader; + Client client; + + /** + * Constructs the MenuManager class. + * + * @param menu The Menu object this manages. + * @param gameLoader The GameLoader object that loads and starts the game. + * @param gamePanel The GamePanel that displays the game. + */ + public MenuManager(Menu menu, GameLoader gameLoader, GamePanel gamePanel) { + this.menuState = MenuState.MAIN_MENU; + this.menu = menu; + this.textField = menu.getTextField(); + this.buttons = menu.getButtons(); + this.saveStateHandler = SaveStateHandler.getInstance(); + this.saveState = SaveState.getInstance(); + this.gamePanel = gamePanel; + this.gameLoader = gameLoader; + this.client = new Client(5000); + attachListeners(); + } + + /** + * Helper method for handling username input. Passes username to SaveStateHandler. + * + * @param textField, a MenuTextField + */ + private void handleTextInput(MenuTextField textField) { + String username = textField.getInput().trim(); + saveStateHandler.setUsername(username); + } + + /** + * Used to attach listeners and ensures Menu has focus. + */ + private void attachListeners() { + menu.addMouseListener(this); + menu.setFocusable(true); + menu.requestFocusInWindow(); + } + + /** + * Checks if character is a valid character for username input. + * + * @param c The character to be checked. + * @return true if character is valid, false otherwise. + */ + private boolean isValidCharacter(char c) { + return Character.isLetterOrDigit(c) || c == '_' || c == '-'; + } + + /** + * Overrides keyTyped method from the KeyListener interface. Handles input + * to MenuTextField. + * + * @param e KeyEvent object representing the key event. + */ + @Override + public void keyTyped(KeyEvent e) { + char keyChar = e.getKeyChar(); + if (isValidCharacter(keyChar)) { + textField.addChar(keyChar); + textField.setTextColor(Color.BLACK); + menu.repaint(); + } + } + + /** + * Overrides the mouseClicked method from MouseListener interface. Handles + * mouse click events on MenuButtons. Used to which Button with ButtonName + * is clicked, and handles accordingly. + * + * @param e MouseEvent to be processed. + */ + @Override + public void mouseClicked(MouseEvent e) { + int mouseX = e.getX(); + int mouseY = e.getY(); + for (MenuButton button : buttons) { + if (button.contains(mouseX, mouseY)) { + MenuButton.ButtonName buttonName = button.getButtonName(); + + switch (buttonName) { + case NEW_GAME -> { + this.menuState = MenuState.NEW_GAME; + handleMainButtonClick(); + menu.repaint(); + } + case LOAD_GAME -> { + this.menuState = MenuState.LOAD_GAME; + handleMainButtonClick(); + menu.repaint(); + } + case BACK -> { + handleBack(); + menu.repaint(); + this.menuState = MenuState.MAIN_MENU; + } + case SUBMIT -> { + handleSubmit(); + this.menuState = MenuState.MAIN_MENU; + menu.repaint(); + } + default -> System.err.println("Unknown ButtonName clicked."); + } + } + } + } + + /** + * Handles button clicks when menu in MAIN_MENU state. Changes text displayed + * in the buttons, visibility of visibles and enables KeyListening on the Menu. + */ + private void handleMainButtonClick() { + this.buttons.get(0).setButton(MenuButton.ButtonName.SUBMIT, "Submit"); + this.buttons.get(1).setButton(MenuButton.ButtonName.BACK, "Back"); + menu.setVisibles(true); + menu.addKeyListener(this); + } + + /** + * Handles submit action. If menu in NEW_GAME state, creates a new save file + * with username as fileName. If menu in LOAD_GAME state, loads the + * username.json file. + */ + private void handleSubmit() { + if (this.menuState == MenuState.NEW_GAME) { + handleTextInput(textField); + gamePanel.setUpGame(); + gameLoader.switchToGamePanel(); + } else if (this.menuState == MenuState.LOAD_GAME) { + handleLoadGameSubmit(gamePanel); + } + } + + /** + * Helper method to handle loading game on submit action. Loads previous save + * file at [username].json. If file not found changes text color to red to indicate + * error. + */ + private void handleLoadGameSubmit(GamePanel gamePanel) { + handleTextInput(textField); + SaveState saveState = new SaveState(); + try { + saveState = saveStateHandler.load(); + } catch (FileNotFoundException ex) { +// throw new RuntimeException(ex); + textField.setTextColor(Color.RED); + menu.repaint(); +// textField.setTextColor(Color.BLACK); + } + saveState.load(gamePanel.player, gamePanel); + gameLoader.switchToGamePanel(); + } + + /** + * Handles the back action in the menu. Resets buttons, textfield and label + * on menu to Main menu settings. + */ + private void handleBack() { + this.buttons.get(0).setButton(MenuButton.ButtonName.NEW_GAME, "New Game"); + this.buttons.get(1).setButton(MenuButton.ButtonName.LOAD_GAME, "Load Game"); + menu.setVisibles(false); + menu.removeKeyListener(this); + } + + /** + * Overrides keyPressed method in KeyListener interface. Enables Submit + * action on Enter key press, and removing characters from TextField on + * Backspace key press. + * + * @param e The KeyEvent to process. + */ + @Override + public void keyPressed(KeyEvent e) { + int keyCode = e.getKeyCode(); + if (keyCode == KeyEvent.VK_BACK_SPACE) { + textField.removeChar(); + textField.setTextColor(Color.BLACK); + menu.repaint(); + } else if (keyCode == KeyEvent.VK_ENTER && this.menuState != MenuState.MAIN_MENU) { + handleSubmit(); + } + } + + /** + * Default empty implementation of MouseLister and KeyListener interfaces. + * No action performed. + * + * @param e the event to be processed + */ + @Override + public void mousePressed(MouseEvent e) { + } + + @Override + public void mouseReleased(MouseEvent e) { + } + + @Override + public void mouseEntered(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + } + + @Override + public void keyReleased(KeyEvent e) { + } + +} diff --git a/src/main/java/org/project/Menu/MenuTextBox.java b/src/main/java/org/project/Menu/MenuTextBox.java new file mode 100644 index 0000000..60b566f --- /dev/null +++ b/src/main/java/org/project/Menu/MenuTextBox.java @@ -0,0 +1,59 @@ +package org.project.Menu; + +import java.awt.*; + +/** + * MenuTextBox is an abstract class that represents a Component that is + * colored rectangular box that can hold text to display. It extends MenuComponent. + * + * @author Greg Song + * @version 2023-04-09 + */ +public abstract class MenuTextBox extends MenuComponent { + protected Color bgColor; + protected Color textColor; + protected Font font; + protected String text; + + /** + * Constructs a new instance of MenuTextBox. + * + * @param x The x-coordinate of top-left corner of the text box. + * @param y The y-coordinate of the top-left corner of the text box. + * @param width Width of the text box. + * @param height Height of the text box. + * @param bgColor Background color + * @param textColor Text color + * @param font Font of the text in the text box. + * @param text String of the text to display. + */ + public MenuTextBox(int x, int y, int width, int height, Color bgColor, + Color textColor, Font font, String text) { + super(x, y, width, height); + this.bgColor = bgColor; + this.textColor = textColor; + this.font = font; + this.text = text; + } + + /** + * Used to draw the MenuTextBox. + * + * @param g a Graphics object. + */ + @Override + public void draw(Graphics g) { + g.setColor(bgColor); + g.fillRect(x, y, width, height); + + g.setFont(font); + FontMetrics fontMetrics = g.getFontMetrics(); + int textWidth = fontMetrics.stringWidth(text); + int textHeight = fontMetrics.getHeight(); + int textX = x + (width - textWidth) / 2; + int textY = y + (height + textHeight) / 2; + g.setColor(textColor); + + g.drawString(text, textX, textY); + } +} diff --git a/src/main/java/org/project/Menu/MenuTextField.java b/src/main/java/org/project/Menu/MenuTextField.java new file mode 100644 index 0000000..adb84ec --- /dev/null +++ b/src/main/java/org/project/Menu/MenuTextField.java @@ -0,0 +1,114 @@ +package org.project.Menu; + +import java.awt.*; + +/** + * Represents a text field component in a Menu. It extends the MenuComponent + * class and implements Visible, allowing it to be hidden. + * + * @author Greg Song + * @version 2023-04-09 + */ +public class MenuTextField extends MenuComponent implements Visible { + private static final int H_PADDING = 5; + private static final int V_PADDING = 20; + private String input; + private final int maxLength; + private final Font font; + private final Color bgColor = Color.white; + private Color textColor = Color.black; + private boolean isVisible; + + /** + * Constructs a MenuTextField. + * + * @param x The x-coordinate of the text field. + * @param y The y-coordinate of the text field. + * @param width The width of the text field. + * @param height The height of the text field. + * @param font The font of the text in the text field. + * @param maxLength The maximum length of the input in the text field. + * @param isVisible Whether the text field is visible or not. + */ + public MenuTextField(int x, int y, int width, int height, Font font, + int maxLength, boolean isVisible) { + super(x, y, width, height); + this.font = font; + this.maxLength = maxLength; + this.input = ""; + this.isVisible = isVisible; + } + + /** + * Overrides the draw method. Used to draw this text field. + * + * @param g The Graphics object used to draw. + */ + @Override + public void draw(Graphics g) { + if (isVisible) { + g.setColor(bgColor); + g.fillRect(x, y, width, height); + g.setColor(textColor); + g.drawRect(x, y, width, height); + g.setFont(font); + g.drawString(input, x + H_PADDING, y + V_PADDING); + } + } + + /** + * Adds a character to the input of the text field. + * + * @param c The character to be added. + */ + public void addChar(char c) { + if (input.length() < maxLength) { + input += c; + } + } + + /** + * Removes the last character of the input. + */ + public void removeChar() { + if (input.length() > 0) { + input = input.substring(0, input.length() - 1); + } + } + + /** + * Gets the current input of the text field. + * + * @return The current input, a String + */ + public String getInput() { + return input; + } + + /** + * Sets the visibility Status of the text field. + * + * @param visible true if object should be visible, false otherwise. + */ + public void setVisible(boolean visible) { + this.isVisible = visible; + } + + /** + * Returns the visibility Status of the text field. + * + * @return True if currently visible, false otherwise. + */ + public boolean isVisible() { + return this.isVisible; + } + + /** + * Sets the textColor of this MenuTextField. + * + * @param textColor a Color object + */ + public void setTextColor(Color textColor) { + this.textColor = textColor; + } +} diff --git a/src/main/java/org/project/Menu/Visible.java b/src/main/java/org/project/Menu/Visible.java new file mode 100644 index 0000000..a0c6538 --- /dev/null +++ b/src/main/java/org/project/Menu/Visible.java @@ -0,0 +1,23 @@ +package org.project.Menu; + +/** + * Visible interface represents an object that can be set visible or invisible. + * + * @author Greg Song + * @version 2023-04-09 + */ +public interface Visible { + /** + * Sets the visibility Status of the object. + * + * @param visible true if object should be visible, false otherwise. + */ + void setVisible(boolean visible); + + /** + * Returns the visibility Status of the object. + * + * @return true if object is visible, false otherwise. + */ + boolean isVisible(); +} diff --git a/src/main/java/org/project/Objects/CollisionDetector.java b/src/main/java/org/project/Objects/CollisionDetector.java new file mode 100644 index 0000000..8e80d1d --- /dev/null +++ b/src/main/java/org/project/Objects/CollisionDetector.java @@ -0,0 +1,282 @@ +package org.project.Objects; + +import org.project.Entities.Entity; +import org.project.Entities.Player; +import org.project.Map.TileManager; +import org.project.ui.GamePanel; + +import static org.project.SystemVariables.*; + +/** + * The CollisionDetector class provides methods for detecting + * collisions between the player, tiles, and intractable elements + * in the game. + * + * @author Nathan Bartyuk, Simrat Kaur, Abhishek Chouhan, Amrit Jhatu, Greg + * @version 2023-02-07 + */ +public class CollisionDetector { + + // instance of gamePanel in which collision is happening. + private static GamePanel gp; + + // a singleton instance of collision detector because each game needs only one + private static CollisionDetector instance; + + // stores the int-code data of adjacent tiles in above, below, left and right + // for checking if there is a solid tile adjacent current tile being checked + private int leftCol; + private int rightCol; + private int upRow; + private int downRow; + + /** + * Constructs a CollisionDetector object with a reference to + * the GamePanel it is being used in. + * + * @param gp The GamePanel object. + */ + private CollisionDetector(GamePanel gp) { + CollisionDetector.gp = gp; + } + + /** + * method that returns a single instance of Collision Detector class. + * Returns a new instance if called first time else returns previous instance. + * + * @param gp the gamePanel in which collision is happening. + * @return CollisionDetector instance + */ + public static CollisionDetector getCollisionDetector(GamePanel gp) { + if (instance == null) { + instance = new CollisionDetector(gp); + } + return instance; + } + + /** + * Checks for collision between the player and tiles in the game world. + * + * @param entity The Entity object representing the player + */ + public void checkTile(Entity entity) { + int leftX = entity.getWorldX() + entity.hitbox.x; + int rightX = leftX + entity.hitbox.width; + int upY = entity.getWorldY() + entity.hitbox.y; + int downY = upY + entity.hitbox.height; + + /* stores the int code of tiles adjacent to the tile the entity is on currently */ + leftCol = leftX / TILE_SIZE; + rightCol = rightX / TILE_SIZE; + upRow = upY / TILE_SIZE; + downRow = downY / TILE_SIZE; + + + /* checks performed below, if entity is deemed to running into a solid tile, + * entity collision boolean is set to be true + * movement is stopped in the entity's update method which has an if statement to only update + * entity position if collision is false. + */ + if (entity.peekDirection() == Directions.UP) { + upRow = (upY - entity.getSpeed()) / TILE_SIZE; + collide(entity); // sets entity collision to true + } else if (entity.peekDirection() == Directions.DOWN) { + downRow = (downY + entity.getSpeed()) / TILE_SIZE; + collide(entity); + } else if (entity.peekDirection() == Directions.LEFT) { + leftCol = (leftX - entity.getSpeed()) / TILE_SIZE; + collide(entity); + } else if (entity.peekDirection() == Directions.RIGHT) { + rightCol = (rightX + entity.getSpeed()) / TILE_SIZE; + collide(entity); + } + } + + /** + * method that checks if the tile Entity will next step on has collision property + * and sets entity collision to true, meaning entity is colliding. + * This prevents the entity's update position method from running, hence entity is stopped. + * + * @param entity The entity on which check is being performed. + */ + private void collide(Entity entity) { + int tileNum1, tileNum2; + + tileNum1 = gp.tManager.getMap()[leftCol][upRow]; + tileNum2 = gp.tManager.getMap()[rightCol][downRow]; + + // check if the tiles the entity is colliding with have the collision property + // the tiles have a collision boolean that is set to true for solid tiles and false for tiles + // that can be walked on + if (checkCollision(gp.tManager, tileNum1) || checkCollision(gp.tManager, tileNum2)) { + entity.setCollided(true); // set entity collision to true + } + } + + /** + * Helper method that checks if tiles the entity is colliding with has collision. + * + * @param tManager TileManager object. + * @param tileNum, Tile index number to check. + * @return True if tile at tileNum has collision set to true, false otherwise. + */ + private boolean checkCollision(TileManager tManager, int tileNum) { + return tManager.tiles[tileNum].collision; + } + + /** + * Checks for collision between the player and intractable elements in the game world, + * such as rubies, doors, and power-ups. + * + * @param p The Player object + * @return The index of the element which the player is colliding, or 999 if no collision occurs. + */ + public int checkObject(Player p, boolean player) { + int index = MAX_INDEX; + for (int i = 0; i < gp.elements.length; i++) { + if (gp.elements[i] != null) { + + // get entity/player's solid area position in the world map + p.hitbox.x = p.getWorldX() + p.hitbox.x; + p.hitbox.y = p.getWorldY() + p.hitbox.y; + + // get the object's solid area position in the world map, because the hitbox + // value is relative to object + // and we need its position on the world map to perform checks + // just add worldX to relative position of solid area to get the values. + gp.elements[i].getHitbox().x = gp.elements[i].getWorldX() + gp.elements[i].getHitbox().x; + gp.elements[i].getHitbox().y = gp.elements[i].getWorldY() + gp.elements[i].getHitbox().y; + + // check if player is in range of object to pick it up + if (p.peekDirection() == Directions.UP) { + p.hitbox.y -= p.getSpeed(); + index = handleCollision(p, gp, player, i, index); + } else if (p.peekDirection() == Directions.DOWN) { + p.hitbox.y += p.getSpeed(); + index = handleCollision(p, gp, player, i, index); + } else if (p.peekDirection() == Directions.LEFT) { + p.hitbox.x -= p.getSpeed(); + index = handleCollision(p, gp, player, i, index); + } else if (p.peekDirection() == Directions.RIGHT) { + p.hitbox.x += p.getSpeed(); + index = handleCollision(p, gp, player, i, index); + } + + // Reset hitbox's to default values because we momentarily + // set them to be relative to worldMap + p.hitbox.x = p.hitboxDefaultX; + p.hitbox.y = p.hitboxDefaultY; + gp.elements[i].getHitbox().x = 0; + gp.elements[i].getHitbox().y = 0; + } + } + return index; + } + + /** + * Helper function to handle collision between player and game object. + * + * @param p The Player object + * @param gp1 The gamePanel object + * @param player Boolean indicating if player collision should be tracked + * @param index Index of game object with which player is colliding + */ + private int handleCollision(Player p, GamePanel gp1, boolean player, int i, int index) { + if (p.hitbox.intersects(gp1.elements[i].getHitbox())) { + if (gp1.elements[i].getCollision()) { + p.setCollided(true); + } + if (player) { + index = i; + } + } + return index; + } + + /** + * Check collision of entity with entity. + * + * @param entity the entity on which the collision is being checked on + * @param target the entities against which collision is being checked + * @return the index of the target entity which collide with the entity which the check was done on. + */ + public int checkEntityCollide(Entity entity, Entity[] target) { + int index = MAX_INDEX; + for (int i = 0; i < target.length; i++) { + if (target[i] != null) { + + //get entity/player's solid area position + entity.hitbox.x = entity.getWorldX() + entity.hitbox.x; + entity.hitbox.y = entity.getWorldY() + entity.hitbox.y; + + //get teh object's solid area position + target[i].hitbox.x = target[i].getWorldX() + target[i].hitbox.x; + target[i].hitbox.y = target[i].getWorldY() + target[i].hitbox.y; + + int moveX = 0; + int moveY = 0; + + switch (entity.peekDirection()) { + case UP -> moveY = -entity.getSpeed(); + case DOWN -> moveY = entity.getSpeed(); + case LEFT -> moveX = -entity.getSpeed(); + case RIGHT -> moveX = entity.getSpeed(); + } + + entity.hitbox.x += moveX; + entity.hitbox.y += moveY; + + if (entity.hitbox.intersects(target[i].hitbox)) { + entity.setCollided(true); + index = i; + } + + entity.hitbox.x = entity.hitboxDefaultX; + entity.hitbox.y = entity.hitboxDefaultY; + target[i].hitbox.x = target[i].hitboxDefaultX; + target[i].hitbox.y = target[i].hitboxDefaultY; + } + } + return index; + } + + /** + * method to check if player is colliding with Entity. + * + * @param entity the Entities (all of them) + * @return contactPlayer a boolean to indicate that player had contact with Entity + */ + public boolean checkPlayerCollide(Entity entity) { + boolean contactPlayer = false; + + //get entity/player's solid area position + entity.hitbox.x = entity.getWorldX() + entity.hitbox.x; + entity.hitbox.y = entity.getWorldY() + entity.hitbox.y; + + //get the object's solid area position + gp.player.hitbox.x = gp.player.getWorldX() + gp.player.hitbox.x; + gp.player.hitbox.y = gp.player.getWorldY() + gp.player.hitbox.y; + + + switch (entity.peekDirection()) { + case UP -> entity.hitbox.y -= entity.getSpeed(); + case DOWN -> entity.hitbox.y += entity.getSpeed(); + case LEFT -> entity.hitbox.x -= entity.getSpeed(); + case RIGHT -> entity.hitbox.x += entity.getSpeed(); + } + + if (entity.hitbox.intersects(gp.player.hitbox)) { + entity.setCollided(true); + contactPlayer = true; + } + + entity.hitbox.x = entity.hitboxDefaultX; + entity.hitbox.y = entity.hitboxDefaultY; + + gp.player.hitbox.x = gp.player.hitboxDefaultX; + gp.player.hitbox.y = gp.player.hitboxDefaultY; + + return contactPlayer; + } + +} diff --git a/src/main/java/org/project/Objects/Door.java b/src/main/java/org/project/Objects/Door.java new file mode 100644 index 0000000..8e870fc --- /dev/null +++ b/src/main/java/org/project/Objects/Door.java @@ -0,0 +1,27 @@ +package org.project.Objects; + +import javax.imageio.ImageIO; +import java.io.FileInputStream; +import java.io.IOException; + +/** + * The Door class represents a door element in the game that can only be opened + * when the player has collected all the rubies. + * + * @author Simrat Kaur + * @version 2023-02-07 + */ +public class Door extends Element { + + /** + * Constructs a Door object and sets its name, image, and collision properties. + */ + public Door() { + try { + setImage(ImageIO.read(new FileInputStream("assets/data/objects/door1.png"))); + } catch (IOException e) { + e.printStackTrace(); + } + setCollision(true); + } +} diff --git a/src/main/java/org/project/Objects/Element.java b/src/main/java/org/project/Objects/Element.java new file mode 100644 index 0000000..8ed164c --- /dev/null +++ b/src/main/java/org/project/Objects/Element.java @@ -0,0 +1,103 @@ +package org.project.Objects; + +import org.project.ui.GamePanel; +import org.project.Map.Positionable; + +import java.awt.*; +import java.awt.image.BufferedImage; + +import static org.project.SystemVariables.*; + +/** + * The Element class represents intractable objects in the game + * that the player can interact with. Each element has an image, + * a name, and a collision property indicating whether the + * element can be collided with. + * + * @author Simrat Kaur + * @version 2023-02-07 + */ +public abstract class Element implements Positionable { + + // The hitbox of the element, used for collision detection + private final Rectangle hitbox = new Rectangle(0, 0, 46, 46); + + //setting instance variables. + private BufferedImage image; + private int currentFrame = 0; + private boolean collision = false; + private int worldX, worldY; + + /** + * draws the element on the game panel at its current position. + * + * @param g2 graphics object used to draw element + * @param gp game panel on which the element is drawn + */ + public void draw(Graphics2D g2, GamePanel gp) { + int screenX = worldX - gp.player.getWorldX() + gp.player.getScreenX(); + int screenY = worldY - gp.player.getWorldY() + gp.player.getScreenY(); + drawIfVisible(g2, gp, screenX, screenY); + } + + /** + * Draws the element on the game panel at its current position if it is visible. + * + * @param g2 graphics object used to draw element + * @param gp game panel on which the element is drawn + */ + public void drawIfVisible(Graphics2D g2, GamePanel gp, int screenX, int screenY) { + if (((worldX + TILE_SIZE) > (gp.player.getWorldX() - gp.player.getScreenX())) && + ((worldX - TILE_SIZE) < (gp.player.getWorldX() + gp.player.getScreenX())) && + ((worldY + TILE_SIZE) > (gp.player.getWorldY() - gp.player.getScreenY())) && + ((worldY - TILE_SIZE) < (gp.player.getWorldY() + gp.player.getScreenY()))) { + g2.drawImage(image, screenX, screenY, TILE_SIZE, TILE_SIZE, null); + } + } + + //Setters and getters for the instance variables. + + public BufferedImage getImage() { + return image; + } + + public void setImage(BufferedImage image) { + this.image = image; + } + + public int getCurrentFrame() { + return currentFrame; + } + + public void setCurrentFrame(int currentFrame) { + this.currentFrame = currentFrame; + } + + public boolean getCollision() { + return collision; + } + + public void setCollision(boolean collision) { + this.collision = collision; + } + + public int getWorldX() { + return worldX; + } + + public void setWorldX(int worldX) { + this.worldX = worldX; + } + + public int getWorldY() { + return worldY; + } + + public void setWorldY(int worldY) { + this.worldY = worldY; + } + + public Rectangle getHitbox() { + return hitbox; + } +} diff --git a/src/main/java/org/project/Objects/ElementHandler.java b/src/main/java/org/project/Objects/ElementHandler.java new file mode 100644 index 0000000..1c85522 --- /dev/null +++ b/src/main/java/org/project/Objects/ElementHandler.java @@ -0,0 +1,164 @@ +package org.project.Objects; + +import org.project.Entities.Monster; +import org.project.Entities.Villager; +import org.project.ui.GamePanel; + +import java.util.Random; + +import static org.project.SystemVariables.*; + +/** + * The ElementHandler class is responsible for handling the placement + * of elements in the map, including non-player characters (NPCs), + * monsters, and intractable objects such as doors, rubies, and + * power-ups. The class contains methods to set the positions of these + * elements within the game panel. + * + * @author Nathan Bartyuk, Simrat Kaur, Abhishek Chouhan, Amrit Jhatu, Greg + * @version 2023-02-07 + */ +public class ElementHandler { + + // Constants for map size and element spawning + private static final int ARRAY_SIZE = 20; + private static final int HALF_ARRAY_SIZE = ARRAY_SIZE / 2; + private static final int MAP_SIZE = 50; + private static final int SPAWN_INTERVAL = 30; // seconds + + private final Random random = new Random(); + + // Game panel that this element handler belongs to + private final GamePanel gp; + + /** + * Constructs an ElementHandler object with the specified GamePanel. + * + * @param gp game panel in which the elements are placed + */ + public ElementHandler(GamePanel gp) { + this.gp = gp; + } + + /** + * Sets the positions of the intractable objects within the game panel. + * This method is used during the initial setup of the game. + * The intractable objects include a door, power-up, and several rubies. + */ + public void setElement() { + gp.elements[0] = new Door(); + gp.elements[0].setWorldX(10 * TILE_SIZE); + gp.elements[0].setWorldY(11 * TILE_SIZE); + + gp.elements[1] = new PowerUp(); + gp.elements[1].setWorldX(23 * TILE_SIZE); + gp.elements[1].setWorldY(7 * TILE_SIZE); + + gp.elements[2] = new Ruby(); + gp.elements[2].setWorldX(23 * TILE_SIZE); + gp.elements[2].setWorldY(10 * TILE_SIZE); + + gp.elements[3] = new Ruby(); + gp.elements[3].setWorldX(12 * TILE_SIZE); + gp.elements[3].setWorldY(42 * TILE_SIZE); + + gp.elements[4] = new Ruby(); + gp.elements[4].setWorldX(20 * TILE_SIZE); + gp.elements[4].setWorldY(7 * TILE_SIZE); + + gp.elements[5] = new Ruby(); + gp.elements[5].setWorldX(36 * TILE_SIZE); + gp.elements[5].setWorldY(31 * TILE_SIZE); + + gp.elements[6] = new Ruby(); + gp.elements[6].setWorldX(38 * TILE_SIZE); + gp.elements[6].setWorldY(41 * TILE_SIZE); + + gp.elements[7] = new Fire(); + gp.elements[7].setWorldX(19 * TILE_SIZE); + gp.elements[7].setWorldY(37 * TILE_SIZE); + + gp.elements[8] = new Fire(); + gp.elements[8].setWorldX(20 * TILE_SIZE); + gp.elements[8].setWorldY(36 * TILE_SIZE); + + gp.elements[9] = new Fire(); + gp.elements[9].setWorldX(21 * TILE_SIZE); + gp.elements[9].setWorldY(39 * TILE_SIZE); + + gp.elements[10] = new Fire(); + gp.elements[10].setWorldX(22 * TILE_SIZE); + gp.elements[10].setWorldY(40 * TILE_SIZE); + } + + /** + * Sets the positions of the non-player characters (NPCs) within the game panel. + */ + public void setNPC() { + gp.npc[0] = new Villager(gp, 24, 10); + gp.npc[0].setWorldX((gp.npc[0]).getWorldX() * TILE_SIZE); + gp.npc[0].setWorldY((gp.npc[0]).getWorldY() * TILE_SIZE); + } + + /** + * Sets the positions of the monsters within the game panel. + */ + public void setMonster() { + gp.monster[0] = new Monster(gp, 24, 15); + gp.monster[0].setWorldX((gp.monster[0]).getWorldX() * TILE_SIZE); + gp.monster[0].setWorldY((gp.monster[0]).getWorldY() * TILE_SIZE); + + gp.monster[1] = new Monster(gp, 13, 9); + gp.monster[1].setWorldX((gp.monster[1]).getWorldX() * TILE_SIZE); + gp.monster[1].setWorldY((gp.monster[1]).getWorldY() * TILE_SIZE); + + gp.monster[2] = new Monster(gp, 24, 40); + gp.monster[2].setWorldX((gp.monster[2]).getWorldX() * TILE_SIZE); + gp.monster[2].setWorldY((gp.monster[2]).getWorldY() * TILE_SIZE); + + gp.monster[3] = new Monster(gp, 30, 38); + gp.monster[3].setWorldX((gp.monster[3]).getWorldX() * TILE_SIZE); + gp.monster[3].setWorldY((gp.monster[3]).getWorldY() * TILE_SIZE); + } + + /** + * Spawns new elements randomly within the game panel. + * This method is called at a fixed interval during the + * game and new elements such as rubies fire are added to + * the game panel's list of elements. + *

+ * This method also removes the previously spawned elements + * before adding new ones. + */ + public void spawnElements() { + // Remove all previous elements + for (int i = 2; i < ARRAY_SIZE; i++) { + gp.elements[i] = null; + } + + // Spawn new elements + for (int i = 2; i < HALF_ARRAY_SIZE; i++) { + int x = random.nextInt(MAP_SIZE); + int y = random.nextInt(MAP_SIZE); + // we gave non-collidable tiles int-codes that are multiples of 3. + // this check ensures that rubies don't spawn on objects that are collidable + if (gp.tManager.getMap()[x][y] % 3 == 0) { + gp.elements[i] = new Ruby(); + gp.elements[i].setWorldX(x * TILE_SIZE); + gp.elements[i].setWorldY(y * TILE_SIZE); + } + } + + // spawn fire at random locations + for (int i = HALF_ARRAY_SIZE; i < ARRAY_SIZE; i++) { + int x = random.nextInt(MAP_SIZE); + int y = random.nextInt(MAP_SIZE); + if (gp.tManager.getMap()[x][y] % 3 == 0) { + gp.elements[i] = new Fire(); + gp.elements[i].setWorldX(x * TILE_SIZE); + gp.elements[i].setWorldY(y * TILE_SIZE); + } + } + } + +} diff --git a/src/main/java/org/project/Objects/Fire.java b/src/main/java/org/project/Objects/Fire.java new file mode 100644 index 0000000..f811916 --- /dev/null +++ b/src/main/java/org/project/Objects/Fire.java @@ -0,0 +1,73 @@ +package org.project.Objects; + +import org.project.ui.GamePanel; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.FileInputStream; +import java.io.IOException; + +import static org.project.SystemVariables.*; + +/** + * Defines the Fire element that is not collidable and the player + * is supposed to die when they come in contact with fire. + * + * @author Simrat Kaur + * @version 2023-02-07 + */ +public class Fire extends Element { + private final BufferedImage[] fires; + private long lastFrameTime; + + /** + * Constructs a Fire object and sets its name, image, and collision properties. + * Also sets up the animation of the Fire element. + */ + public Fire() { + // creating an array of four frames for the animation + fires = new BufferedImage[4]; + try { + for (int i = 0; i < fires.length; i++) { + fires[i] = ImageIO.read(new FileInputStream("assets/data/objects/fire" + (i + 1) + ".png")); + } + } catch (IOException e) { + e.printStackTrace(); + } + setImage(fires[0]); + setCollision(false); + lastFrameTime = System.currentTimeMillis(); + } + + /** + * Draws the Fire element's current frame image on the GamePanel. + * Also updates the current frame of the Fire element's animation. + * + * @param g2 the Graphics2D object to be drawn on + * @param gp the GamePanel object where the Fire element is being drawn + */ + @Override + public void draw(Graphics2D g2, GamePanel gp) { + + //calculates the X and Y position of the Fire element on the screen. + int screenX = getWorldX() - gp.player.getWorldX() + gp.player.getScreenX(); + int screenY = getWorldY() - gp.player.getWorldY() + gp.player.getScreenY(); + + long currentTime = System.currentTimeMillis(); // gets the current time + int frameInterval = 150; // the time between each frame in milliseconds + + /* + Updates the current frame of the Fire element's animation if enough time has + passed since the last frame, and sets the Fire element's image to the current + frame of the animation. + */ + if (currentTime - lastFrameTime > frameInterval) { + setCurrentFrame((getCurrentFrame() + 1) % fires.length); + setImage(fires[getCurrentFrame()]); + lastFrameTime = currentTime; + } + int tileSize = TILE_SIZE; + g2.drawImage(getImage(), screenX, screenY, tileSize, tileSize, null); + } +} diff --git a/src/main/java/org/project/Objects/PowerUp.java b/src/main/java/org/project/Objects/PowerUp.java new file mode 100644 index 0000000..5d1b5eb --- /dev/null +++ b/src/main/java/org/project/Objects/PowerUp.java @@ -0,0 +1,27 @@ +package org.project.Objects; + +import javax.imageio.ImageIO; +import java.io.FileInputStream; +import java.io.IOException; + +/** + * Defines the Power up element that is to be collected by the player and + * makes the player faster. + * + * @author Nathan Bartyuk, Simrat Kaur, Abhishek Chouhan, Amrit Jhatu, Greg + * @version 2023-02-07 + */ +public class PowerUp extends Element { + + /** + * Constructs a Power-up object and sets its image, and collision properties. + */ + public PowerUp() { + try { + setImage(ImageIO.read(new FileInputStream("assets/data/objects/powerup.png"))); + } catch (IOException e) { + e.printStackTrace(); + } + setCollision(true); + } +} diff --git a/src/main/java/org/project/Objects/Ruby.java b/src/main/java/org/project/Objects/Ruby.java new file mode 100644 index 0000000..da39a03 --- /dev/null +++ b/src/main/java/org/project/Objects/Ruby.java @@ -0,0 +1,26 @@ +package org.project.Objects; + +import java.io.FileInputStream; +import java.io.IOException; +import javax.imageio.ImageIO; + +/** + * Defines the ruby element that is to be collected by the player throughout the game. + * + * @author Nathan Bartyuk, Simrat Kaur, Abhishek Chouhan, Amrit Jhatu, Greg + * @version 2023-02-07 + */ +public class Ruby extends Element { + + /** + * Constructs a Ruby object and sets its image, and collision properties. + */ + public Ruby() { + try { + setImage(ImageIO.read(new FileInputStream("assets/data/objects/ruby.png"))); + } catch (IOException e) { + e.printStackTrace(); + } + setCollision(true); + } +} diff --git a/src/main/java/org/project/SystemVariables.java b/src/main/java/org/project/SystemVariables.java new file mode 100644 index 0000000..7b8009a --- /dev/null +++ b/src/main/java/org/project/SystemVariables.java @@ -0,0 +1,49 @@ +package org.project; + +/** + * This class contains system-wide constants and enums used in the game. + */ +public record SystemVariables() { + + /** The size of each tile in pixels.*/ + public static final int TILE_SIZE = 48; + + /** The number of columns on the screen.*/ + public static final int SCREEN_COL = 16; + + /** The number of rows on the screen.*/ + public static final int SCREEN_ROW = 12; + + /** The number of columns on the game map.*/ + public static final int MAP_COL = 50; + + /** The number of rows on the game map.*/ + public static final int MAP_ROW = 50; + + /** The maximum value for an index in the game.*/ + public static final int MAX_INDEX = 999; + + /** The time interval, in nanoseconds, between each frame.*/ + public static final double DRAW_INTERVAL = 1000000000.0 / 60; + + /** An enum representing the four directions.*/ + public enum Directions { + /** Left direction.*/ + LEFT, + /** Right direction.*/ + RIGHT, + /** Up direction.*/ + UP, + /** Down direction.*/ + DOWN + } + + /**An enum representing the status of an entity.*/ + public enum Status { + /**The entity is alive.*/ + ALIVE, + /**The entity is dead.*/ + DEAD + } +} + diff --git a/src/main/java/org/project/ui/GameLoader.java b/src/main/java/org/project/ui/GameLoader.java new file mode 100644 index 0000000..ff01a17 --- /dev/null +++ b/src/main/java/org/project/ui/GameLoader.java @@ -0,0 +1,67 @@ +package org.project.ui; + +import org.project.Menu.Menu; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import java.io.IOException; + +/** + * GameLoader is the entry point for this game. + * It initializes SaveStateHandler, Window, game and menu panels and manages switching between them. + * + * @author Greg Song + * @version 2023-04-03 + */ +public class GameLoader { + private final JFrame window; + private final JPanel menuPanel; + private final GamePanel gamePanel; + + /** + * Constructs GameLoader. Instantiating GameLoader starts the game in JFrame, + * and initializes SaveStateHandler. + */ + public GameLoader() throws IOException { + this.window = new JFrame(); + window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + window.setResizable(false); + window.setTitle("Ruby Rush"); + window.requestFocus(); + + this.gamePanel = GamePanel.getGamePanel(); + + Menu menu = new Menu(this, gamePanel); + this.menuPanel = menu; + this.window.add(menuPanel); + window.pack(); + window.setLocationRelativeTo(null); + window.setVisible(true); + } + + /** + * Switches window's panel from Menu to GamePanel. + */ + public void switchToGamePanel() { + this.window.remove(menuPanel); + + gamePanel.startGameThread(); + gamePanel.setEnabled(true); + + this.window.add(gamePanel); + gamePanel.requestFocus(); + window.revalidate(); + window.repaint(); + } + + /** + * Switches window's panel from GamePanel to Menu. + */ + public void switchToMenuPanel() { + this.window.remove(gamePanel); + this.window.add(menuPanel); + menuPanel.requestFocus(); + window.revalidate(); + window.repaint(); + } +} diff --git a/src/main/java/org/project/ui/GamePanel.java b/src/main/java/org/project/ui/GamePanel.java new file mode 100644 index 0000000..981efb4 --- /dev/null +++ b/src/main/java/org/project/ui/GamePanel.java @@ -0,0 +1,252 @@ +package org.project.ui; + +import org.project.Entities.Entity; +import org.project.Entities.KeyHandler; +import org.project.Entities.Player; +import org.project.Map.PathFinder; +import org.project.Map.TileManager; +import org.project.Objects.CollisionDetector; +import org.project.Objects.Element; +import org.project.Objects.ElementHandler; + +import javax.swing.*; +import java.awt.*; + +import static org.project.Objects.CollisionDetector.*; +import static org.project.SystemVariables.*; + +/** + * The GamePanel class represents the main panel of the game. It extends JPanel and implements Runnable. + * This class contains the settings for the screen and manages the different managers, key handlers, entities, + * and ui of the game. It also contains methods to update and draw the game and start the game thread. + * + * @author Nathan Bartyuk, and Others + * @version April 8, 2023 + */ +public class GamePanel extends JPanel implements Runnable { + + // SCREEN SETTINGS + public final int screenWidth = TILE_SIZE * SCREEN_COL; + public final int screenHeight = TILE_SIZE * SCREEN_ROW; + + // INSTANTIATES OTHER MANAGERS + // These we believe should be stored here in the master-kind of class + // and be accessible to all components through the game panel + // these managers still manage their own selves, and enforce encapsulation, just putting them here, means + // these instances are accesible to all components through GamePanel, which in turn is made accessible explicitly + public static GamePanel instance; + public TileManager tManager = new TileManager(this); + public CollisionDetector cDetector = getCollisionDetector(this); + public KeyHandler kHandler = new KeyHandler(); + public Player player = Player.getInstance(this, kHandler); + public UserInterface ui = new UserInterface(this); + public ElementHandler aHandler = new ElementHandler(this); + public PathFinder pFinder = new PathFinder(this); + public Sound sound = new Sound(); + public Sound soundEffect = new Sound(); + public Thread gameThread; + public Element[] elements = new Element[20]; + public Entity[] npc = new Entity[10]; + public Entity[] monster = new Entity[10]; + + private int timer; // timer to check how many times frames have been drawn already, and then update rubies and monsters + + /** + * Creates a new GamePanel object with the specified dimensions and default background color. + * This constructor sets the preferred size of the panel to the specified dimensions, sets the + * background color to black, enables double buffering, and adds a key listener to the panel. + * Additionally, it sets the panel to be focusable. + */ + private GamePanel() { + this.setPreferredSize(new Dimension(screenWidth, screenHeight)); + this.setBackground(Color.black); + this.setDoubleBuffered(true); + this.addKeyListener(kHandler); + this.setFocusable(true); + } + + /** + * Returns the GamePanel instance, creating it if it doesn't exist. + * This method creates an instance of the GamePanel class if one does not already exist, and returns it. + * + * @return the GamePanel instance + */ + public static GamePanel getGamePanel() { + if (instance == null) { + instance = new GamePanel(); + } + return instance; + } + + /** + * Instantiates the game upon launch + */ + public void setUpGame() { + aHandler.setElement(); + aHandler.setNPC(); + aHandler.setMonster(); + playMusic(0); + } + + /** + * Updates the positions of the player, NPCs, and monsters in the game. + * This method updates the position of the player and calls the update() method + * of each NPC and monster in the game. + */ + public void update() { + player.update(this, this.kHandler); + + // NPC + for (Entity entity : npc) { + if (entity != null) { + entity.update(); + } + } + // MONSTER + for (Entity entity : monster) { + if (entity != null) { + entity.update(); + } + } + + // timer after which rubies and monsters will reshuffle + timer++; + // after intervals of 30 seconds + // 30 seconds times 60 frames (that is spawn after 1800 frames have been drawn) + int spawnInterval = (15) * 60; + if (timer >= spawnInterval) { + timer = 0; + aHandler.spawnElements(); + } + + } + + /** + * method to draw all components in the Game-J-Panel. + * Calls the draw methods of all entities (NPCs, player, Monster) + * elements, objects and ui in a single place as the master method. + * + * @param g the Graphics object attached to JPanel which helps in drawing + */ + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; + if (player.getCurrentStatus() == Status.ALIVE) { + tManager.draw(g2); + // Draw Elements - OBJECTS + for (Element element : elements) { + if (element != null) { + element.draw(g2, this); + } + } + // Draw docile characters - NPCs + for (Entity entity : npc) { + if (entity != null) { + entity.draw(g2); + } + } + // Draw hostile characters - MONSTERS + for (Entity entity : monster) { + if (entity != null) { + entity.draw(g2); + } + } + // DRAW PLAYER + player.draw(g2); + // Draw the ui + ui.draw(g2); + } else { + drawGameOverScreen(g2); + } + g2.dispose(); + } + + private void drawGameOverScreen(Graphics2D g2) { + String text = "Game Over"; + Font arial_40 = new Font("Arial", Font.PLAIN, 40); + g2.setFont(arial_40); + g2.setColor(Color.red); + int x = (screenWidth - TILE_SIZE) / 2; // centre of x-axis of window + int y = (screenWidth - TILE_SIZE) / 2; // centre of y-axis of window + // int length = (int) g2.getFontMetrics().getStringBounds(text, g2).getWidth(); + g2.drawString(text, x, y); +// sound.stop(); + } + + /** + * Starts the game thread + */ + public void startGameThread() { + gameThread = new Thread(this); + gameThread.start(); + } + + /** + * Runs the game thread. + * This method initializes variables used in the game loop, calculates the elapsed time since the last frame, + * and updates and redraws the game at a fixed frame rate. + */ + @Override + public void run() { + // Initialize variables + double delta = 0; // The amount of time passed since the last frame + long lastTime = System.nanoTime(); // The time at the start of the loop + long currentTime; // The current time + long timer = 0; // Keeps track of how long the loop has been running + + // Loop until gameThread is null + while (gameThread != null) { + // Get the current time and calculate delta + currentTime = System.nanoTime(); + delta += (currentTime - lastTime) / DRAW_INTERVAL; + + // Add the time elapsed since the last frame to the timer + timer += (currentTime - lastTime); + + // Set lastTime to the current time + lastTime = currentTime; + + // If enough time has passed to draw the next frame + if (delta >= 1) { + // Update the game state + update(); + + // Draw the game again, this is weird but this actually calls the paintComponent method again + // method from the java package (SWING library) + repaint(); + + // Decrement delta by 1 to account for the drawn frame + delta--; + } + + // If the loop has been running for longer than DRAW_INTERVAL + if (timer >= DRAW_INTERVAL) { + // Reset the timer + timer = 0; + } + } + } + + /** + * Plays a music file specified by the given index. + * + * @param i the index of the music file to play + */ + public void playMusic(int i) { + sound.setFile(i); + sound.play(); + sound.loop(); + } + + /** + * Plays a sound effect specified by the given index. + * + * @param i the index of the sound effect to play + */ + public void playSE(int i) { + soundEffect.setFile(i); + soundEffect.play(); + } + +} diff --git a/src/main/java/org/project/ui/Life.java b/src/main/java/org/project/ui/Life.java new file mode 100644 index 0000000..57e3435 --- /dev/null +++ b/src/main/java/org/project/ui/Life.java @@ -0,0 +1,33 @@ +package org.project.ui; + +import java.awt.image.BufferedImage; +import java.io.FileInputStream; +import java.io.IOException; +import javax.imageio.ImageIO; +import org.project.Objects.Element; + +/** + * Defines the heart element that is displayed on top of the overlay ui. + * + * @author Abhishek Chouhan + * @version 2023-03-31 + */ +public class Life extends Element { + public BufferedImage halfLife; + public BufferedImage emptyLife; + + /** + * Constructs a heart object and sets its name, image, and collision properties. + */ + public Life() { + try { + setImage(ImageIO.read(new FileInputStream("assets/player/fullHeart.png"))); + halfLife = ImageIO.read(new FileInputStream("assets/player/halfHeart.png")); + emptyLife = ImageIO.read(new FileInputStream("assets/player/emptyHeart.png")); + + } catch (IOException e) { + e.printStackTrace(); + } + setCollision(true); + } +} diff --git a/src/main/java/org/project/ui/Sound.java b/src/main/java/org/project/ui/Sound.java new file mode 100644 index 0000000..fcef5b0 --- /dev/null +++ b/src/main/java/org/project/ui/Sound.java @@ -0,0 +1,96 @@ +package org.project.ui; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.Clip; + +/** + * The Sound class represents a sound object that is responsible + * for playing different sound effects and background music in the game. + * + * @author Simrat Kaur + * @version April 09, 2023 + */ +public class Sound { + + /** + * int codes for Sounds being used in this project. + */ + public static final int backgroundMusic = 0; + public static final int rubyGetSound = 1; + public static final int doorOpenSound = 2; + public static final int powerUpSound = 3; + public static final int runningSound = 4; + + /** + * Clip object from the java package through which sound is being output in AudioStream. + */ + public Clip clip; + + /** + * stores the file in-system URLs of all sound files to be set to clip whenever + * a particular sound is to be played. + */ + private final URL[] soundURL = new URL[30]; + + /** + * constructor to initialize the sound object's array with URL + * of all sound files to be run in the game. + */ + public Sound() { + try { + soundURL[backgroundMusic] = new File("assets/sound/background.wav").toURI().toURL(); + soundURL[rubyGetSound] = new File("assets/sound/rubycollection.wav").toURI().toURL(); + soundURL[doorOpenSound] = new File("assets/sound/doorOpening.wav").toURI().toURL(); + soundURL[powerUpSound] = new File("assets/sound/powerup.wav").toURI().toURL(); + soundURL[runningSound] = new File("assets/sound/running.wav").toURI().toURL(); + } catch (MalformedURLException e) { + System.err.println("Unable to get sound files"); + } + } + + /** + * function to setup the clip and which sound to be played. + * + * @param i the int code of the file to be used (defined as static variables in class above) + */ + public void setFile(int i) { + try { + AudioInputStream ais = AudioSystem.getAudioInputStream(soundURL[i]); + clip = AudioSystem.getClip(); + clip.open(ais); + if (clip == null) { + System.err.println("Unable to setup sound files"); + } + } catch (Exception e) { + System.err.println("unable to open file: " + e + i); + } + } + + /** + * method to play the sound. + * Calls the java library start method on the Sound Clip object. + */ + public void play() { + clip.start(); + } + + /** + * method to loop a sound clip continuosly. + * Being used to loop the BGM music. + */ + public void loop() { + clip.setFramePosition(0); + clip.loop(Clip.LOOP_CONTINUOUSLY); + } + + /** + * method to stop a clip or stop sound. + */ + public void stop() { + clip.stop(); + } +} diff --git a/src/main/java/org/project/ui/UserInterface.java b/src/main/java/org/project/ui/UserInterface.java new file mode 100644 index 0000000..6456f1e --- /dev/null +++ b/src/main/java/org/project/ui/UserInterface.java @@ -0,0 +1,153 @@ +package org.project.ui; + +import static org.project.SystemVariables.TILE_SIZE; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import org.project.Objects.Ruby; + +/** + * ui class that is responsible for drawing/displaying + * the player stats and other important messages. + * This class is instantiable and manages ui components as a whole. + * + * @author Abhishek Chouhan + */ +public class UserInterface { + + /** + * The JPanel attached to the game window. + */ + private static GamePanel gp; + + /** + * The Arial font with a size of 40. + */ + private static Font font; + + /** + * The image component for the key. + */ + BufferedImage keyImage; + + /** + * The image component for the full heart. + */ + BufferedImage fullHeart; + + /** + * The image component for the half heart. + */ + BufferedImage halfHeart; + + /** + * The image component for the empty heart. + */ + BufferedImage emptyHeart; + + /** + * A boolean flag indicating whether a message should be displayed. + */ + private boolean displayMessage = false; + + /** + * The message to display. + */ + private String message = ""; + + /** + * The frame counter for the message display. + */ + private int messageFrameCounter; + + + /** + * Constructor for the ui class object that initializes all variables. + * This object is used to draw over the game and display important stats. + * + * @param gp the panel attached to game window in which stats are drawn. + */ + public UserInterface(GamePanel gp) { + UserInterface.gp = gp; + UserInterface.font = new Font("Arial", Font.PLAIN, 40); + Ruby key = new Ruby(); + Life life = new Life(); + + // Set all imageVariables to respective PNGs + this.keyImage = key.getImage(); + this.fullHeart = life.getImage(); + this.halfHeart = life.halfLife; + this.emptyHeart = life.emptyLife; + + } + + /** + * this method is used to display message when an action is done by the player. + * + * @param text specialized text for each action giving feedback/updating player on info. + */ + public void showMessage(String text) { + message = text; + displayMessage = true; + } + + /** + * method to draw stats for the game. + * + * @param g2 the panel attached to game window. + */ + public void draw(Graphics2D g2) { + g2.setFont(font); + g2.setColor(Color.white); + g2.drawImage(keyImage, TILE_SIZE / 2, TILE_SIZE / 2, TILE_SIZE, TILE_SIZE, null); + g2.drawString("x " + gp.player.getCurrentRubies(), 74, 65); + drawPlayerLife(g2); + + // popping message on window + if (displayMessage) { + g2.setFont(g2.getFont().deriveFont(30F)); + g2.drawString(message, TILE_SIZE / 2, TILE_SIZE * 5); + messageFrameCounter++; + // Amount of time a message displays + int messageLength = 120; + if (messageFrameCounter > messageLength) { + messageFrameCounter = 0; + displayMessage = false; + } + } + } + + /** + * used to draw the heart images, showing the Status of current amount of lives. + */ + public void drawPlayerLife(Graphics2D g2) { + int x = TILE_SIZE * 10; // x coordinate of the lives display on screen + int y = TILE_SIZE / 3; // y coordinate of the lives display on screen + int i = 0; // counts the number of heart displayed already on screen respect to max lives + + // DRAW MAX LIFE + // display half the max lives because each heart is 2 sub-lives + while (i < gp.player.getLives() / 2) { + g2.drawImage(emptyHeart, x, y, TILE_SIZE, TILE_SIZE, null); + i++; + x += TILE_SIZE; + } + + // RESET after drawing emptyHearts (displaying full life but BLANK) + x = TILE_SIZE * 10; + i = 0; + + // DRAW CURRENT LIFE + while (i < gp.player.getLives()) { + g2.drawImage(halfHeart, x, y, TILE_SIZE, TILE_SIZE, null); + i++; + if (i < gp.player.getLives()) { + g2.drawImage(fullHeart, x, y, TILE_SIZE, TILE_SIZE, null); + } + i++; + x += TILE_SIZE; + } + } +} diff --git a/src/test/java/org/project/DoorTest.java b/src/test/java/org/project/DoorTest.java new file mode 100644 index 0000000..7e7bec6 --- /dev/null +++ b/src/test/java/org/project/DoorTest.java @@ -0,0 +1,45 @@ +package org.project; + +import org.junit.jupiter.api.Test; +import org.project.Objects.Door; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +public class DoorTest { + + // Test for the constructor + @Test + public void testConstructor() { + Door door = new Door(); + assertNotNull(door.getImage()); + assertTrue(door.getCollision()); + } + + // Test for setImage() and getImage() + @Test + public void testSetGetImage() { + Door door = new Door(); + BufferedImage image = null; + try { + image = ImageIO.read(new File("assets/test/test_door.png")); + } catch (IOException e) { + e.printStackTrace(); + } + door.setImage(image); + assertEquals(image, door.getImage()); + } + + // Test for setCollision() and getCollision() + @Test + public void testSetGetCollision() { + Door door = new Door(); + door.setCollision(false); + assertFalse(door.getCollision()); + } + +} diff --git a/src/test/java/org/project/ElementHandlerTest.java b/src/test/java/org/project/ElementHandlerTest.java new file mode 100644 index 0000000..5788833 --- /dev/null +++ b/src/test/java/org/project/ElementHandlerTest.java @@ -0,0 +1,48 @@ +package org.project; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.project.Entities.Monster; +import org.project.Entities.Villager; +import org.project.Objects.ElementHandler; +import org.project.Objects.Ruby; +import org.project.ui.GamePanel; + +import static org.project.SystemVariables.*; + +public class ElementHandlerTest { + private GamePanel gp; + private ElementHandler elementHandler; + + @BeforeEach + public void setup() { + gp = GamePanel.getGamePanel(); + elementHandler = new ElementHandler(gp); + } + + @Test + public void testSetElement() { + elementHandler.setElement(); + Assertions.assertTrue(gp.elements[0] instanceof Ruby); + Assertions.assertEquals(17 * TILE_SIZE, gp.elements[0].getWorldX()); + Assertions.assertEquals(38 * TILE_SIZE, gp.elements[0].getWorldY()); + } + + @Test + public void testSetNPC() { + elementHandler.setNPC(); + Assertions.assertTrue(gp.npc[0] instanceof Villager); + Assertions.assertEquals(24 * TILE_SIZE, gp.npc[0].getWorldX()); + Assertions.assertEquals(10 * TILE_SIZE, gp.npc[0].getWorldY()); + } + + @Test + public void testSetMonster() { + elementHandler.setMonster(); + Assertions.assertTrue(gp.monster[0] instanceof Monster); + Assertions.assertEquals(24 * TILE_SIZE, gp.monster[0].getWorldX()); + Assertions.assertEquals(15 * TILE_SIZE, gp.monster[0].getWorldY()); + } + // Add more tests here for the BOSS if required. +} diff --git a/src/test/java/org/project/ElementTest.java b/src/test/java/org/project/ElementTest.java new file mode 100644 index 0000000..73371d4 --- /dev/null +++ b/src/test/java/org/project/ElementTest.java @@ -0,0 +1,53 @@ +package org.project; + +import org.junit.jupiter.api.Test; +import org.project.Objects.Element; + +import java.awt.*; + +import static org.junit.jupiter.api.Assertions.*; + +public class ElementTest { + + // Test for getCurrentFrame() and setCurrentFrame() + @Test + public void testGetSetCurrentFrame() { + Element element = new ConcreteElement(); + element.setCurrentFrame(5); + assertEquals(5, element.getCurrentFrame()); + } + + // Test for getCollision() and setCollision() + @Test + public void testGetSetCollision() { + Element element = new ConcreteElement(); + element.setCollision(true); + assertTrue(element.getCollision()); + } + + // Test for getWorldX() and setWorldX() + @Test + public void testGetSetWorldX() { + Element element = new ConcreteElement(); + element.setWorldX(10); + assertEquals(10, element.getWorldX()); + } + + // Test for getWorldY() and setWorldY() + @Test + public void testGetSetWorldY() { + Element element = new ConcreteElement(); + element.setWorldY(20); + assertEquals(20, element.getWorldY()); + } + + // Test for getSolidArea() + @Test + public void testGetSolidArea() { + Element element = new ConcreteElement(); + assertEquals(new Rectangle(0, 0, 46, 46), element.getHitbox()); + } + + // Concrete subclass of Element for testing + private static class ConcreteElement extends Element {} +} diff --git a/src/test/java/org/project/EntityTest.java b/src/test/java/org/project/EntityTest.java new file mode 100644 index 0000000..afbe1d2 --- /dev/null +++ b/src/test/java/org/project/EntityTest.java @@ -0,0 +1,79 @@ +package org.project; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.project.Entities.Entity; +import org.project.ui.GamePanel; + +import static org.junit.jupiter.api.Assertions.*; +import static org.project.SystemVariables.*; + +public class EntityTest { + + private TestEntity testEntity; + + @BeforeEach + public void setUp() { + GamePanel testGamePanel = GamePanel.getGamePanel(); + testEntity = new TestEntity(testGamePanel); + } + + @Test + public void testSetAction() { + // Check the initial direction and speed below. + assertEquals(Directions.LEFT, testEntity.peekDirection()); + assertEquals(2, testEntity.getSpeed()); + + // Another change to direction and speed, and test again. + testEntity.changeDirection(Directions.UP); + testEntity.setSpeed(3); + testEntity.setAction(); + assertEquals(Directions.UP, testEntity.peekDirection()); + assertEquals(3, testEntity.getSpeed()); + } + + @Test + public void testUpdate() { + // Store initial value of ... worldX and worldY + int initialWorldX = testEntity.getWorldX(); + int initialWorldY = testEntity.getWorldY(); + + // Update to check if worldX and worldY have changed with speed and direction in mind. + testEntity.update(); + assertEquals(initialWorldX - testEntity.getSpeed(), testEntity.getWorldX()); + assertEquals(initialWorldY, testEntity.getWorldY()); + + // Another test for a differing direction. + testEntity.changeDirection(Directions.RIGHT); + testEntity.update(); + assertEquals(initialWorldX, testEntity.getWorldX()); + assertEquals(initialWorldY, testEntity.getWorldY()); + } + + @Test + public void testWorldXWorldYGetterSetter() { + // Set another grouping worldX and worldY values + testEntity.setWorldX(100); + testEntity.setWorldY(200); + + // Check if the new values are set correctly + assertEquals(100, testEntity.getWorldX()); + assertEquals(200, testEntity.getWorldY()); + } + + private static class TestEntity extends Entity { + + public TestEntity(GamePanel gp) { + super(gp); + direction = Directions.LEFT; + speed = 2; + } + + @Override + public void setAction() { + // Sample for setAction() method. + direction = Directions.UP; + speed = 3; + } + } +} diff --git a/src/test/java/org/project/FireTest.java b/src/test/java/org/project/FireTest.java new file mode 100644 index 0000000..2d933db --- /dev/null +++ b/src/test/java/org/project/FireTest.java @@ -0,0 +1,82 @@ +package org.project; + +import org.junit.jupiter.api.Test; +import org.project.Objects.Fire; +import org.project.ui.GamePanel; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +public class FireTest { + + // Test for setImage() and getImage() + @Test + public void testSetGetImage() { + Fire fire = new Fire(); + BufferedImage image = null; + try { + image = ImageIO.read(new File("assets/test/test_fire.png")); + } catch (IOException e) { + e.printStackTrace(); + } + fire.setImage(image); + assertEquals(image, fire.getImage()); + } + + // Test for getCurrentFrame() and setCurrentFrame() + @Test + public void testGetSetCurrentFrame() { + Fire fire = new Fire(); + fire.setCurrentFrame(3); + assertEquals(3, fire.getCurrentFrame()); + } + + // Test for getCollision() and setCollision() + @Test + public void testGetSetCollision() { + Fire fire = new Fire(); + fire.setCollision(false); + assertFalse(fire.getCollision()); + } + + // Test for getWorldX() and setWorldX() + @Test + public void testGetSetWorldX() { + Fire fire = new Fire(); + fire.setWorldX(10); + assertEquals(10, fire.getWorldX()); + } + + // Test for getWorldY() and setWorldY() + @Test + public void testGetSetWorldY() { + Fire fire = new Fire(); + fire.setWorldY(20); + assertEquals(20, fire.getWorldY()); + } + + // Test for getSolidArea() + @Test + public void testGetSolidArea() { + Fire fire = new Fire(); + assertEquals(new Rectangle(0, 0, 46, 46), fire.getHitbox()); + } + + // Test for draw() + @Test + public void testDraw() { + Fire fire = new Fire(); + GamePanel gamePanel = GamePanel.getGamePanel(); + BufferedImage image = new BufferedImage(48, 48, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2 = image.createGraphics(); + g2.setBackground(Color.BLACK); + g2.clearRect(0, 0, 48, 48); + fire.draw(g2, gamePanel); + assertNotNull(fire.getImage()); + } +} diff --git a/src/test/java/org/project/PowerUpTest.java b/src/test/java/org/project/PowerUpTest.java new file mode 100644 index 0000000..9a846bf --- /dev/null +++ b/src/test/java/org/project/PowerUpTest.java @@ -0,0 +1,45 @@ +package org.project; + +import org.junit.jupiter.api.Test; +import org.project.Objects.PowerUp; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +public class PowerUpTest { + + // Test for the constructor + @Test + public void testConstructor() { + PowerUp powerup = new PowerUp(); + assertNotNull(powerup.getImage()); + assertTrue(powerup.getCollision()); + } + + // Test for setImage() and getImage() + @Test + public void testSetGetImage() { + PowerUp powerup = new PowerUp(); + BufferedImage image = null; + try { + image = ImageIO.read(new File("assets/test/test_powerup.png")); + } catch (IOException e) { + e.printStackTrace(); + } + powerup.setImage(image); + assertEquals(image, powerup.getImage()); + } + + // Test for setCollision() and getCollision() + @Test + public void testSetGetCollision() { + PowerUp powerup = new PowerUp(); + powerup.setCollision(false); + assertFalse(powerup.getCollision()); + } + +} diff --git a/src/test/java/org/project/RubyTest.java b/src/test/java/org/project/RubyTest.java new file mode 100644 index 0000000..d32effa --- /dev/null +++ b/src/test/java/org/project/RubyTest.java @@ -0,0 +1,45 @@ +package org.project; + +import org.junit.jupiter.api.Test; +import org.project.Objects.Ruby; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +public class RubyTest { + + // Test for the constructor + @Test + public void testConstructor() { + Ruby ruby = new Ruby(); + assertNotNull(ruby.getImage()); + assertTrue(ruby.getCollision()); + } + + // Test for setImage() and getImage() + @Test + public void testSetGetImage() { + Ruby ruby = new Ruby(); + BufferedImage image = null; + try { + image = ImageIO.read(new File("assets/test/test_ruby.png")); + } catch (IOException e) { + e.printStackTrace(); + } + ruby.setImage(image); + assertEquals(image, ruby.getImage()); + } + + // Test for setCollision() and getCollision() + @Test + public void testSetGetCollision() { + Ruby ruby = new Ruby(); + ruby.setCollision(false); + assertFalse(ruby.getCollision()); + } + +} diff --git a/src/test/java/org/project/SaveStateHandlerTest.java b/src/test/java/org/project/SaveStateHandlerTest.java new file mode 100644 index 0000000..87051af --- /dev/null +++ b/src/test/java/org/project/SaveStateHandlerTest.java @@ -0,0 +1,23 @@ +package org.project; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.project.Datastate.SaveState; +import org.project.ui.GamePanel; + +public class SaveStateHandlerTest { + private GamePanel gamePanel; + + @BeforeEach + public void setUp() { + gamePanel = GamePanel.getGamePanel(); + gamePanel.setUpGame(); + gamePanel.startGameThread(); + } + + @Test + public void testSaveStateHandler() { + SaveState saveState = new SaveState(); + saveState.setSaveState(gamePanel); + } +} diff --git a/src/test/java/org/project/SoundTest.java b/src/test/java/org/project/SoundTest.java new file mode 100644 index 0000000..712f430 --- /dev/null +++ b/src/test/java/org/project/SoundTest.java @@ -0,0 +1,82 @@ +package org.project; + +import org.junit.jupiter.api.Test; +import org.project.ui.Sound; + +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.Clip; +import javax.sound.sampled.LineUnavailableException; +import java.io.File; +import java.net.URL; + +import static org.junit.jupiter.api.Assertions.*; + +public class SoundTest { + + // Test for constructor + @Test + public void testConstructor() { + Sound sound = new Sound(); + assertNotNull(sound); + } + + // Test for setFile() method + @Test + public void testSetFile() { + Sound sound = new Sound(); + URL soundURL = null; + try { + soundURL = new File("assets/sound/background.wav").toURI().toURL(); + } catch (Exception e) { + e.printStackTrace(); + } + AudioInputStream audioInputStream = null; + try { + audioInputStream = AudioSystem.getAudioInputStream(soundURL); + } catch (Exception e) { + e.printStackTrace(); + } + Clip clip = null; + try { + clip = AudioSystem.getClip(); + } catch (Exception e) { + e.printStackTrace(); + } + try { + clip.open(audioInputStream); + } catch (Exception e) { + e.printStackTrace(); + } + sound.setFile(0); + assertEquals(clip.getFrameLength(), sound.clip.getFrameLength()); + } + + // Test for stop() method + @Test + public void testStop() { + Sound sound = new Sound(); + sound.setFile(0); + sound.play(); + sound.stop(); + assertFalse(sound.clip.isRunning()); + } + + // Test for play() method when clip is not null but not open + @Test + public void testPlayClipNotOpen() throws LineUnavailableException { + Sound sound = new Sound(); + sound.clip = AudioSystem.getClip(); + sound.play(); + assertFalse(sound.clip.isRunning()); + } + + // Test for loop() method when clip is not null but not open + @Test + public void testLoopClipNotOpen() throws LineUnavailableException { + Sound sound = new Sound(); + sound.clip = AudioSystem.getClip(); + sound.loop(); + assertFalse(sound.clip.isRunning()); + } + } diff --git a/src/test/java/org/project/VillagerTest.java b/src/test/java/org/project/VillagerTest.java new file mode 100644 index 0000000..32a1f1a --- /dev/null +++ b/src/test/java/org/project/VillagerTest.java @@ -0,0 +1,66 @@ +package org.project; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.project.Entities.Villager; +import org.project.ui.GamePanel; + +import static org.project.SystemVariables.*; +import java.awt.*; + +import static org.junit.jupiter.api.Assertions.*; + +public class VillagerTest { + + private Villager testVillager; + + @BeforeEach + public void setUp() { + GamePanel testGamePanel = GamePanel.getGamePanel(); + testVillager = new Villager(testGamePanel,10,10); + } + + @Test + public void testSetAction() { + // Check direction and speed. Similar testing from Entity. + assertEquals(Directions.DOWN, testVillager.peekDirection()); + assertEquals(2, testVillager.getSpeed()); + + // Invoke setAction a bunch of times to ensure direction change randomly. + Directions initialDirection = testVillager.peekDirection(); + for (int i = 0; i < 10; i++) { + testVillager.setAction(); + if (testVillager.peekDirection() != initialDirection) { + break; + } + } + assertNotEquals(initialDirection, testVillager.peekDirection()); + } + + @Test + public void testUpdate() { + int initialWorldX = testVillager.getWorldX(); + int initialWorldY = testVillager.getWorldY(); + + // Update and check worldX and worldY for change in line with speed and direction + testVillager.update(); + assertEquals(initialWorldX, testVillager.getWorldX()); + assertEquals(initialWorldY + testVillager.getSpeed(), testVillager.getWorldY()); + + // Change direction to test again ... + testVillager.changeDirection(Directions.LEFT); + testVillager.update(); + assertEquals(initialWorldX - testVillager.getSpeed(), testVillager.getWorldX()); + assertEquals(initialWorldY + testVillager.getSpeed(), testVillager.getWorldY()); + } + + @Test + public void testSolidArea() { + // Check the box ! + Rectangle solidArea = testVillager.hitbox; + assertEquals(8, solidArea.x); + assertEquals(8, solidArea.y); + assertEquals(32, solidArea.width); + assertEquals(32, solidArea.height); + } +}