+
+
+
-## 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.
-- `
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 extends Element> 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
+ * 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);
+ }
+}