diff --git a/cheats.txt b/cheats.txt new file mode 100644 index 0000000..fecc29e --- /dev/null +++ b/cheats.txt @@ -0,0 +1 @@ +Press P to skip to the next level. \ No newline at end of file diff --git a/src/DialogueMenu.java b/src/DialogueMenu.java index c26a6bd..d8f6ac9 100644 --- a/src/DialogueMenu.java +++ b/src/DialogueMenu.java @@ -1,8 +1,10 @@ +// Eric Li, Charlie Zhao, ICS4U, Finished 6/17/2022 +// displays dialogue, animates dialogue, and renders box that contains dialogue as well as the portraits of the characters +// that speak to the players + import java.awt.*; import java.io.Serializable; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; public class DialogueMenu extends TextBox implements Serializable { public static final int PORTRAIT_WIDTH = 200; @@ -10,18 +12,19 @@ public class DialogueMenu extends TextBox implements Serializable { public static final int TOP_PADDING = 10; public static final double LINE_SPACING = 1.5; public static final int FREQUENCY = 2; - public BufferedImageWrapper PORTRAIT; + public BufferedImageWrapper portrait; public int currentFrame = 0; public int frameCounter = 0; public boolean isNarrator; public DialogueMenu(int y, int yHeight, Font font, BufferedImageWrapper portrait, boolean isNarrator) { super(y, GamePanel.GAME_WIDTH - PORTRAIT_WIDTH - PADDING*3, yHeight, 0, font, null, null); - PORTRAIT = portrait; + this.portrait = portrait; checkForNarrator(); this.isNarrator = isNarrator; } + // check if the person speaking is currently the narrator, and adjust the newX value accordingly public void checkForNarrator() { if (isNarrator) { newX = PORTRAIT_WIDTH + PADDING*2; @@ -30,64 +33,85 @@ public class DialogueMenu extends TextBox implements Serializable { } } + // draw a centered text box public void drawCenteredTextBox(Graphics g, String text, Color backgroundColor, Color textColor) { + // only draw currentFrame number of text characters; this animates the process of drawing the text text = text.substring(0, currentFrame); + // only set the background color if it is not null if (backgroundColor != null) { g.setColor(textColor); + // if the person speaking is the narrator, draw the textbox to the right of the portrait if (isNarrator) { - g.drawImage(PORTRAIT.image, newX - PORTRAIT_WIDTH - PADDING, newY, PORTRAIT_WIDTH, yHeight, null); - } else { - g.drawImage(PORTRAIT.image, GamePanel.GAME_WIDTH - PORTRAIT_WIDTH - PADDING, newY, PORTRAIT_WIDTH, yHeight, null); + g.drawImage(portrait.image, newX - PORTRAIT_WIDTH - PADDING, newY, PORTRAIT_WIDTH, yHeight, null); + } else { // otherwise, draw the textbox to the left of the portrait + g.drawImage(portrait.image, GamePanel.GAME_WIDTH - PORTRAIT_WIDTH - PADDING, newY, PORTRAIT_WIDTH, yHeight, null); } + // create border and set it to a width of 4.0 ((Graphics2D)g).setStroke(new BasicStroke(4f)); + // draw the border g.drawRect(newX, newY, xWidth, yHeight - 4); + // set color of the rectangle inside the border, and draw it g.setColor(backgroundColor); g.fillRect(newX, newY, xWidth, yHeight - 4); } + // set text color and draw it g.setColor(textColor); drawCenteredString(g, newY, newX, text); } + // draw a centered string public static void drawCenteredString(Graphics g, int y, int x, String text) { - // split text by spaces + // split text by spaces (into individual words) String[] newText = text.split(" "); - ArrayList lines = new ArrayList(); + // create new ArrayList that will compose of every line in the new text + ArrayList lines = new ArrayList<>(); + // add an empty line; this line will eventually have more text added to it lines.add(""); // get font size FontMetrics metrics = g.getFontMetrics(); + // declare temporary variables used in the for loop, and initialize them int currentLineWidth = 0, lastLineIndex = 0; for (String s: newText) { + // add width of new word to current line width currentLineWidth += metrics.stringWidth(s + " "); + // if the newLineWidth still fits in the dialogue box, add it to the current line if (currentLineWidth - metrics.stringWidth(" ") < (GamePanel.GAME_WIDTH - PORTRAIT_WIDTH - PADDING*5)) { lines.set(lastLineIndex, lines.get(lastLineIndex) + s + " "); - } else { + } else { // otherwise, create a new line, set the current line width to the current width, and increment the current line index currentLineWidth = metrics.stringWidth(s); lines.add(s + " "); lastLineIndex ++; } } + // leave TOP_PADDING + (metrics.getAscent() - metrics.getDescent()) * LINE_SPACING) + // space between the top of the dialogue box and the text y += TOP_PADDING; - // center y (half is above y value, half is below y value) for (String s: lines) { + // add spacing since last line y += (metrics.getAscent() - metrics.getDescent()) * LINE_SPACING; - // draw string + // draw actual string g.drawString(s + "\n", x + PADDING, y); } } public boolean draw(Graphics g, String text, Color backgroundColor, Color textColor) { + // before every draw, check if the status of the current person speaking changed checkForNarrator(); + // if more than FREQUENCY ticks has passed since the last text animation, animate by drawing one more char if (frameCounter >= FREQUENCY) { frameCounter -= FREQUENCY; currentFrame += 1; } + // set font of string to be drawn g.setFont(font); drawCenteredTextBox(g, text, backgroundColor, textColor); + // increment the frame counter frameCounter++; + // if the text has been completely drawn (nothing left to animate), return true if (currentFrame >= text.length()) { currentFrame = 0; return true; - } else { + } else { // otherwise, return false return false; } } diff --git a/src/FireBall.java b/src/FireBall.java index a589ff3..b0213f9 100644 --- a/src/FireBall.java +++ b/src/FireBall.java @@ -1,3 +1,6 @@ +// Eric Li, Charlie Zhao, ICS4U, Finished 6/19/2022 +// fireball object that is shot by fireball tiles; this kills players and forces them to dodge + import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.UnsupportedAudioFileException; import java.awt.*; @@ -11,7 +14,7 @@ public class FireBall extends GenericSprite{ private int lifeSpan; - + // called every time GamePanel.updateShootingBlock is called public FireBall(int x, int y, int xv, int yv, String dir,int height, int width) { super(x, y, height, width); xVelocity = xv; @@ -30,16 +33,21 @@ public class FireBall extends GenericSprite{ lifeSpan = 1000; } + // update realX position of fireball (is affected by camera.x instead of being pure x) public void update(){ realX = x-GameFrame.game.camera.x; } + // check if fireball will collide with the player public boolean collidePlayer(Player p){ if(realX+width>p.x&&realXGameFrame.game.HEIGHT){ -// dead = true; -// } } public void draw(Graphics g) throws IOException { g.drawImage(GamePanel.getImage(spritePath),x-GameFrame.game.camera.x,y,null); diff --git a/src/GameFrame.java b/src/GameFrame.java index f421a33..1566cd3 100644 --- a/src/GameFrame.java +++ b/src/GameFrame.java @@ -1,20 +1,16 @@ +// Eric Li, Charlie Zhao, ICS4U, Completed 6/20/2022 /* GameFrame class establishes the frame (window) for the game It is a child of JFrame because JFrame manages frames -Runs the constructor in GamePanel class +Creates new JPanel child, and adds GamePanel, MenuPanel, and SettingPanel classes to it, allowing for navigation between them +Also initializes and deserializes them as necessary +*/ -*/ -import java.awt.*; -import java.io.IOException; -import java.nio.file.FileSystemException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Set; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.UnsupportedAudioFileException; import javax.swing.*; +import java.awt.*; +import java.io.IOException; +import java.util.Arrays; public class GameFrame extends JFrame{ @@ -25,50 +21,55 @@ public class GameFrame extends JFrame{ public GameFrame(){ try { + // CameraPanel is child of JPanel with camera object main = new CameraPanel(); + // CardLayout is used to allow navigating between the different "cards" in the menu main.setLayout(new CardLayout()); try { - game = (GamePanel)FileManager.readObjectFromFile("local/game_state.dat", Arrays.asList("Any")); + // attempts to read GamePanel object from file + // if it succeeds, this becomes the main game + // the second argument includes all the classes that are used by the GamePanel object + // this ensures that attempting to execute malicious code by tampering with saves will be more difficult, as payloads would have to stick to these classes + // please note that it is not a perfect mitigation though + game = (GamePanel)FileManager.readObjectFromFile("local/game_state.dat", + Arrays.asList("GamePanel", "javax.swing.JPanel", "javax.swing.JComponent", "java.awt.Container", + "java.awt.Component", "javax.swing.plaf.ColorUIResource", "java.awt.Color", + "javax.swing.plaf.FontUIResource", "java.awt.Font", "java.util.Locale", "java.awt.Dimension", + "java.awt.ComponentOrientation", "[Ljava.awt.Component;", "java.awt.FlowLayout", + "javax.swing.event.EventListenerList", "BackgroundImage", "BufferedImageWrapper", + "java.lang.Boolean", "Camera", "BombDirectionShow", "StickyBomb", "GenericSprite", + "java.awt.Rectangle", "java.util.ArrayList", "DialogueMenu", "TextBox", "NonPlayer", + "[[[LBufferedImageWrapper;", "[[LBufferedImageWrapper;", "[LBufferedImageWrapper;", "PauseMenu", + "WallSign", "[[LTile;", "[LTile;", "SingleTile", "Tile", "Particle", "Player")); game.gameFrame = main; + // shows that the game can be continued, as it was loaded from the file game.isContinue = true; + // requests focus from OS; this is needed because otherwise Windows will not pass keystrokes to game game.requestFocusable(); + // add mouse listener to game game.addMouseListener(); } catch (IOException | ClassNotFoundException | ClassCastException | SecurityException e) { - System.out.println(e); + // if an exception occurs during serialization, it is not a game-breaking exception; it is logged in the console and a new game is created + System.out.println("[LOG] " + e.toString().replace("Exception", "NotAnError")); game = new GamePanel(main); //run GamePanel constructor } - // delete saves to prevent unexpected behaviour - try { - Files.deleteIfExists(Path.of("local/game_state.dat")); - Files.deleteIfExists(Path.of("local/temp_state.dat")); - } catch (FileSystemException e) { - System.out.println(e); - } + // start game thread to allow the game to run game.startThread(); // save game after load to prevent lag spikes during the game FileManager.writeObjectToFile("local\\temp_state.dat", game); - /* - try { - // read previously saved controls - // SafeObjectInputStream was implemented to prevent arbitrary code execution from occurring if the save files were modified - game.middlewareArray = (ArrayList)FileManager. - readObjectFromFile("local/controls", Arrays.asList("java.util.ArrayList", "Middleware")); - } catch (IOException | ClassNotFoundException | ClassCastException | SecurityException e) { - game.middlewareArray = new ArrayList(); - } - */ + // create menu screen and settings screen menu = new MenuPanel(main); settings = new SettingPanel(main); + // adds all three panels to the CardLayout CameraPanel main, to allow for navigation between them + // the menu screen is added first because it is the first screen to show upon loading the game main.add(menu, "menu"); main.add(settings, "settings"); main.add(game, "game"); } catch (IOException | SpriteException | UnsupportedAudioFileException | LineUnavailableException e) { - // TODO: handle IO errors gracefully - // exceptions are raised when tiles are not found or are of incorrect dimensions throw new RuntimeException(e); } this.add(main); - this.setTitle("GUI is cool!"); //set title for frame + this.setTitle("Kenney"); //set title for frame // set game icon and ignore exception (failing to set icon doesn't otherwise break program) try { this.setIconImage(GamePanel.getImage("img/misc/favicon.png")); diff --git a/src/GamePanel.java b/src/GamePanel.java index 2f3dc54..4081a3a 100644 --- a/src/GamePanel.java +++ b/src/GamePanel.java @@ -9,21 +9,21 @@ Implements KeyListener interface to listen for keyboard input Implements Runnable interface to use "threading" - let the game do two things at once */ + +import javax.imageio.ImageIO; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.UnsupportedAudioFileException; +import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; -import javax.imageio.ImageIO; import java.io.Serializable; -import java.lang.reflect.Array; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.ConcurrentModificationException; -import javax.sound.sampled.LineUnavailableException; -import javax.sound.sampled.UnsupportedAudioFileException; -import javax.swing.*; import static java.nio.file.StandardCopyOption.ATOMIC_MOVE; @@ -316,8 +316,6 @@ public class GamePanel extends JPanel implements Runnable, KeyListener, Serializ if (particles.get(i).lifeSpan <= 0) { particles.remove(i); } - } else { - throw new RuntimeException(); // TODO: remove stack trace } } // show bomb trajectory preview if player is able to throw a bomb diff --git a/src/GenericSprite.java b/src/GenericSprite.java index 587aa5b..bbbdc14 100644 --- a/src/GenericSprite.java +++ b/src/GenericSprite.java @@ -1,13 +1,16 @@ -/* PlayerBall class defines behaviours for the player-controlled ball +// Eric Li, Charlie Zhao, ICS4U, completed 6/10/2022 +/* GenericSprite class defines behaviours for all objects that move child of Rectangle because that makes it easy to draw and check for collision In 2D GUI, basically everything is a rectangle even if it doesn't look like it! -*/ +*/ + import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.UnsupportedAudioFileException; import java.awt.*; -import java.awt.event.*; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; import java.io.IOException; import java.io.Serializable; @@ -15,7 +18,6 @@ public class GenericSprite extends Rectangle implements Serializable { public double yVelocity; public double xVelocity; - public final double SPEED = 20; //movement speed of ball public final double speedCapx = 50; public final double speedCapy = 20; @@ -29,7 +31,6 @@ public class GenericSprite extends Rectangle implements Serializable { public boolean isPlayer = false; //constructor creates ball at given location with given dimensions - // TODO: reverse order of height and width public GenericSprite(int x, int y, int height, int width){ super(x, y, width, height); WIDTH = width; @@ -63,6 +64,7 @@ public class GenericSprite extends Rectangle implements Serializable { } + // caps x and y velocity at speedCapx and speedCapy respectively public void capSpeed(){ if(xVelocity>speedCapx){ xVelocity = speedCapx; @@ -76,6 +78,7 @@ public class GenericSprite extends Rectangle implements Serializable { } } + // checks if the sprite is colliding with a tile public boolean collide(Tile tile, double x, double y){ if(tile==null){return false;} if(!tile.collision){ @@ -87,6 +90,7 @@ public class GenericSprite extends Rectangle implements Serializable { return false; } + // checks if the sprite can move x in the x plane and y in the y plane public boolean canUpdate(double x, double y) throws UnsupportedAudioFileException, LineUnavailableException, IOException { boolean canUpdate = true; int lowX = Math.max(0, (this.x+GamePanel.GAME_WIDTH/2)/Tile.length-4); @@ -109,8 +113,8 @@ public class GenericSprite extends Rectangle implements Serializable { } return canUpdate; } - //called frequently from the GamePanel class - //draws the current location of the ball to the screen + + //draws the current location of the sprite to the screen public void draw(Graphics g) throws IOException, UnsupportedAudioFileException, LineUnavailableException { diff --git a/src/GlobalState.java b/src/GlobalState.java index e60f2be..734d2b1 100644 --- a/src/GlobalState.java +++ b/src/GlobalState.java @@ -1,5 +1,10 @@ +// Eric Li, Charlie Zhao, ICS4U, Finished 6/19/2022 +// determines the standard time unit for exploding objects (i.e., bombs) +// while not very useful now, GlobalState was added to increase extensibility in the future + import java.io.Serializable; public class GlobalState implements Serializable { + // each object takes GlobalState.second * 5 ticks to explode public static final int second = 10; } diff --git a/src/LevelManager.java b/src/LevelManager.java index 188afcb..51c61eb 100644 --- a/src/LevelManager.java +++ b/src/LevelManager.java @@ -12,26 +12,30 @@ public class LevelManager implements Serializable { public static String filePath; public static int bombs; + // set current level, then load the map, enemies, dialogue, signs, and bomb count for that level public static void setLevel(int level, boolean hasDied){ + // remove all current bombs and fireballs GameFrame.game.bombs.clear(); GameFrame.game.fireballs.clear(); + // set the player velocity to zero GameFrame.game.player.yVelocity = 0; GameFrame.game.player.xVelocity = 0; GameFrame.game.level = level; + // change spawn coordinates, bomb count, and save file path based on level inputted if(level == 1){ - //-400/450 + // spawn coordinates: -400/450 xSpawn = -400; ySpawn = 450; filePath = "saves/Level1.txt"; bombs = 8; } else if(level == 2){ - //-400/400 + // spawn coordinates: -400/400 xSpawn = -400; ySpawn = 400; filePath = "saves/Level2.txt"; bombs = 3; } else if(level == 3){ - //-800/100 + // spawn coordinates: -800/100 xSpawn = -800; ySpawn = 100; filePath = "saves/Level3.txt"; @@ -43,24 +47,34 @@ public class LevelManager implements Serializable { filePath = "saves/Level4.txt"; bombs = 5; } else if(level == 5){ - //-1100/350 - xSpawn = 1800; - ySpawn = 150; + //-1100/460 + xSpawn = -1100; + ySpawn = 350; filePath = "saves/Level5.txt"; bombs = 1; } try { + // load map into GamePanel MapReader.inputMap(filePath); + // if the player has not died yet (i.e., first time seeing the dialogue), load it if (!hasDied) { + // do not load dialogue if there is no dialogue to load if (!(MapReader.inputDialogue(filePath)[0].equals("$Empty"))) { - GameFrame.game.dialogueArray = new ArrayList(Arrays.asList(MapReader.inputDialogue(filePath))); + // convert dialogue from String[] to ArrayList, and load it into GamePanel + GameFrame.game.dialogueArray = new ArrayList<>(Arrays.asList(MapReader.inputDialogue(filePath))); + // if the dialogue file starts with $Villain, have the portrait display on the right side of the screen, and display the alternate portrait if (GameFrame.game.dialogueArray.get(0).contains("$Villain")) { + // delete the first item in the array so the villain does not say "$Villain" GameFrame.game.dialogueArray.remove(0); + // display portrait on right side of screen GameFrame.game.dialogueMenu.isNarrator = false; - GameFrame.game.dialogueMenu.PORTRAIT = GameFrame.game.villainPortrait; + // change portrait + GameFrame.game.dialogueMenu.portrait = GameFrame.game.villainPortrait; } + // reset dialogue frame to zero, so it restarts the animation every time the dialogue is loaded GameFrame.game.dialogueMenu.currentFrame = 0; GameFrame.game.dialogueMenu.frameCounter = 0; + // tell the GameFrame to load the DialogueMenu GameFrame.game.isDialogue = true; } } @@ -70,12 +84,15 @@ public class LevelManager implements Serializable { // temporary boolean, so only declared here boolean stillTutorial = true; for (String[] sA: MapReader.inputSign(filePath)) { + // if the line contains "/", skip the line, stop adding signs to tutorialSign and instead add signs to loreSign + // this is important because loreSigns and tutorialSigns have different colours and font types if (sA[0].contains("/")) { stillTutorial = false; } else if (stillTutorial) { - //System.out.println("" + sA[0] + sA[1]); + // add sign to tutorialSign GameFrame.game.tutorialSign.add(new WallSign(Integer.parseInt(sA[0]), Integer.parseInt(sA[1]), GamePanel.tutorialFont, sA[2])); } else { + // add sign to loreSign if stillTutorial is false GameFrame.game.loreSign.add(new WallSign(Integer.parseInt(sA[0]), Integer.parseInt(sA[1]), GamePanel.loreFont, sA[2])); } } @@ -87,6 +104,7 @@ public class LevelManager implements Serializable { } + //Gives the player bombs public static void setBombs(){ if(GameFrame.game.level == 1){ bombs = 8; @@ -100,9 +118,11 @@ public class LevelManager implements Serializable { bombs = 1; } } + // overloaded setLevel that accepts only a level argument public static void setLevel(int level) { setLevel(level, false); } + // go to the next level public static void nextLevel(){ setLevel(GameFrame.game.level+1); } diff --git a/src/Main.java b/src/Main.java index cac4b9a..bc79c25 100644 --- a/src/Main.java +++ b/src/Main.java @@ -1,3 +1,4 @@ +// Eric Li, Charlie Zhao, ICS4U, Finished 5/30/2022 /* Main class starts the game All it does is run the constructor in GameFrame class diff --git a/src/MapReader.java b/src/MapReader.java index 7dc3566..056023c 100644 --- a/src/MapReader.java +++ b/src/MapReader.java @@ -1,11 +1,15 @@ +// Eric Li, Charlie Zhao, ICS4U, Finished 6/16/2022 +// reads map, dialogue, and signs from files and loads them into GamePanel + import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.UnsupportedAudioFileException; -import java.io.*; +import java.io.IOException; +import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; public class MapReader implements Serializable { - //Input game map + // loads game map into GamePanel /* 1: Normal Grass 2: Left Grass: @@ -23,28 +27,36 @@ public class MapReader implements Serializable { // h k Shooting // m public static void inputMap(String filePath) throws IOException, SpriteException, UnsupportedAudioFileException, LineUnavailableException { + int TileX, TileY; int x = 0; int y = 0; - int TileX = 0; - int TileY = 0; + // clears existing enemies, particles, fireball shooters, and fireballs GameFrame.game.enemy.clear(); GameFrame.game.particleTiles.clear(); GameFrame.game.shootingTiles.clear(); GameFrame.game.fireballs.clear(); + // clears current map for(int i=0; i0&&GameFrame.game.map[x][y-1]==null) { GameFrame.game.particleTiles.add(GameFrame.game.map[x][y]); } - } else if(file.charAt(i)=='o'){ + } else if(file.charAt(i)=='o'){ // steel tiles can be picked up and moved newTile("img/tiles/boxes/steel.png", x, y, TileX, TileY); GameFrame.game.map[x][y].movable = true; - } else if(file.charAt(i)=='h'){ + } else if(file.charAt(i)=='h'){ // the following tiles shoot fireballs in the directions indicated; these fireballs kill players on contact newTile("img/tiles/boxes/boxShootLeft.png", x, y, TileX, TileY); GameFrame.game.map[x][y].shootingDir = "left"; GameFrame.game.shootingTiles.add(GameFrame.game.map[x][y]); @@ -118,25 +129,28 @@ public class MapReader implements Serializable { GameFrame.game.map[x][y].shootingDir = "down"; GameFrame.game.shootingTiles.add(GameFrame.game.map[x][y]); } - x+=1; + x+=1; // increment x value by one after every character read } } + // return dialogue array given the filePath inputted in inputMath for further processing in LevelManager public static String[] inputDialogue(String mapFilePath) throws IOException { - String filePath = mapFilePath.replace(".txt", "-dialogue.txt"); - return FileManager.readFile(filePath).split("\n"); + String filePath = mapFilePath.replace(".txt", "-dialogue.txt"); // format path to open the dialogue file instead + return FileManager.readFile(filePath).split("\n"); // read and split file, before returning } - public static ArrayList inputSign(String signFilePath) throws IOException { - String filePath = signFilePath.replace(".txt", "-signs.txt"); + // return sign array given the filePath inputted in inputMath for further processing in LevelManager + public static ArrayList inputSign(String mapFilePath) throws IOException { + String filePath = mapFilePath.replace(".txt", "-signs.txt"); // format path to open the sign file instead String[] temporaryStringArray = FileManager.readFile(filePath).split("\n"); - ArrayList returnArray = new ArrayList(); + ArrayList returnArray = new ArrayList<>(); // create new ArrayList and populate it with details about the text for (String s: temporaryStringArray) { - returnArray.add(s.split(" ", 3)); + returnArray.add(s.split(" ", 3)); // s[0] = x, s[1] = y, s[2] = text; given that the text often has spaces, the amount of items resulting from the split was limited to three } return returnArray; } + // populate GamePanel tile array with new tile with TileX and TileY positions public static void newTile(String filePath, int x, int y, int TileX, int TileY) throws IOException, SpriteException { GameFrame.game.map[x][y]=(new SingleTile(TileX,TileY, new BufferedImageWrapper((filePath)))); } diff --git a/src/MenuPanel.java b/src/MenuPanel.java index 3784875..b97e790 100644 --- a/src/MenuPanel.java +++ b/src/MenuPanel.java @@ -1,4 +1,5 @@ -/* GamePanel class acts as the main "game loop" - continuously runs the game and calls whatever needs to be called +// Eric Li, Charlie Zhao, ICS4U, Finished 6/17/2022 +/* MenuPanel class acts as the menu - launches the other panels that need to be called Child of JPanel because JPanel contains methods for drawing to the screen @@ -7,15 +8,15 @@ Implements KeyListener interface to listen for keyboard input Implements Runnable interface to use "threading" - let the game do two things at once */ -import java.awt.*; -import java.awt.event.*; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; + import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.UnsupportedAudioFileException; import javax.swing.*; +import java.awt.*; +import java.awt.event.*; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; public class MenuPanel extends JPanel implements Runnable, KeyListener{ @@ -31,7 +32,7 @@ public class MenuPanel extends JPanel implements Runnable, KeyListener{ public Graphics graphics; public BackgroundImage background; public TextBox title, enter, settings, continueGame; - public ArrayList textBoxArray = new ArrayList(); + public ArrayList textBoxArray = new ArrayList<>(); public Font standardFont = new Font(Font.MONOSPACED, Font.BOLD, 60); public int playerFrame, enemyFrame; // keeps track of how many ticks has elapsed since last frame change @@ -39,28 +40,30 @@ public class MenuPanel extends JPanel implements Runnable, KeyListener{ public static boolean gameStart = false; - // image imports begin here public BufferedImageWrapper backgroundImage = new BufferedImageWrapper(("img/backgrounds/pointyMountains.png")); public MenuPanel(CameraPanel gameFrame) throws IOException, SpriteException, UnsupportedAudioFileException, LineUnavailableException { this.gameFrame = gameFrame; + // initialize shared camera to ensure that the background stays consistent between MenuPanel and SettingPanel camera = gameFrame.camera; - + // create title textbox title = new TextBox(100, 400, 100, GAME_WIDTH, standardFont, "Platformer", null); + // initialize selectable menu options and add the text boxes to the textBoxArray, which controls which text box is currently highlighted continueGame = new TextBox(300, 600, 100, GAME_WIDTH, standardFont, "Continue", "game"); enter = new TextBox(400, 600, 100, GAME_WIDTH, standardFont, "Start Game", "game-start"); settings = new TextBox(500, 600, 100, GAME_WIDTH, standardFont, "Settings", "settings"); textBoxArray.add(enter); textBoxArray.add(settings); + // if there is a loadable save, isContinue is set to true and the Continue textbox is selectable if (GameFrame.game.isContinue) { textBoxArray.add(0, continueGame); } background = new BackgroundImage(0, 0, backgroundImage, GAME_WIDTH, GAME_HEIGHT, 10, camera); - // the height of 35 is set because it is half of the original tile height (i.e., 70px) this.setFocusable(true); //make everything in this class appear on the screen this.addKeyListener(this); //start listening for keyboard input // request focus when the CardLayout selects this game + // this allows the OS to forward HID input to this panel this.addComponentListener(new ComponentAdapter() { @Override @@ -73,6 +76,7 @@ public class MenuPanel extends JPanel implements Runnable, KeyListener{ //add the MousePressed method from the MouseAdapter - by doing this we can listen for mouse input. We do this differently from the KeyListener because MouseAdapter has SEVEN mandatory methods - we only need one of them, and we don't want to make 6 empty methods addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { + // check if the mouse is hovering over a textbox; if it is, run the action connected to the textbox if (hoverCheck(e)) { keyPressed(new KeyEvent(new Component() { }, 0, -1, 0, KeyEvent.VK_ENTER, (char)KeyEvent.VK_ENTER)); @@ -81,6 +85,7 @@ public class MenuPanel extends JPanel implements Runnable, KeyListener{ }); addMouseMotionListener(new MouseMotionAdapter() { public void mouseMoved(MouseEvent e) { + // check if the mouse is now hovering over a text box; if it is, select that textbox hoverCheck(e); } }); @@ -103,20 +108,25 @@ public class MenuPanel extends JPanel implements Runnable, KeyListener{ //call the draw methods in each class to update positions as things move public void draw(Graphics g, int playerFrame, int enemyFrame){ + // draw background background.draw(g); + // draw title title.draw(g,null, Color.black); + // if there is no loadable save, gray out the Continue textbox if (!GameFrame.game.isContinue) { continueGame.draw(g, null, Color.gray); } + // draw each selectable textBox in the textbox array for (TextBox t: textBoxArray) { t.draw(g, null, Color.cyan); } + // overwrite the selectable text box with a background color and a different text color (blue instead of cyan) textBoxArray.get(currentBox).draw(g, Color.gray, Color.blue); } - //call the move methods in other classes to update positions - //this method is constantly called from run(). By doing this, movements appear fluid and natural. If we take this out the movements appear sluggish and laggy - public void move(){ + // this function does nothing, but was retained to allow the children of this class to implement their own functions without also having to reimplement run() + // e.x., a child could override doAction() to launch someFunction() + public void doAction(){ } //run() method is what makes the game continue running without end. It calls other methods to move objects, check for collision, and update the screen @@ -135,7 +145,8 @@ public class MenuPanel extends JPanel implements Runnable, KeyListener{ //only move objects around and update screen if enough time has passed if(delta >= 1){ - move(); + doAction(); + // shift the camera to cause the parallax effect camera.x += 10; repaint(); delta--; @@ -143,8 +154,10 @@ public class MenuPanel extends JPanel implements Runnable, KeyListener{ } } + // check if the mouse is hovering over a textbox; if the mouse is hovering over a textbox, select the textbox hovered over public boolean hoverCheck(MouseEvent e) { for (TextBox t: textBoxArray) { + // select the textbox hovered over if it is hovering over a textbox if (t.isHover(e.getX(), e.getY())) { currentBox = textBoxArray.indexOf(t); return true; @@ -155,35 +168,48 @@ public class MenuPanel extends JPanel implements Runnable, KeyListener{ //if a key is pressed, we'll send it over to the Player class for processing public void keyPressed(KeyEvent e) { - + // intercept keypresses and replace them with previously defined keypresses through the Middleware class e = UtilityFunction.intercept(e, GameFrame.game.middlewareArray); + // if the keypress is ENTER, run the action indicated by the connected textbox's id if (e.getKeyCode() == KeyEvent.VK_ENTER) { + // indicate that the game is starting if the player presses "Continue" or "New Game" if(textBoxArray.get(currentBox).id.contains("game")){ gameStart = true; } // always unpause game, no matter what screen is navigated to GameFrame.game.isPaused = false; // logic for different screens starts here + // if the user presses "New Game", reset GamePanel and navigate to it if (textBoxArray.get(currentBox).id.equals("game-start")) { try { + // remove the old GamePanel from the CardLayout CameraPanel GameFrame.main.remove(GameFrame.game); + // stop the run() while loop, effectively killing the thread GameFrame.game.isRunning = false; + // reset player velocities to prevent race conditions GameFrame.game.player.xVelocity = 0; GameFrame.game.player.yVelocity = 0; + // reset the tile map to prevent race conditions GameFrame.game.map = new Tile[1000][18]; GameFrame.game = new GamePanel(GameFrame.main); //run GamePanel constructor + // make it so that the game can be resumed if the player leaves GameFrame.game.isContinue = true; textBoxArray.add(continueGame); + // start the game GameFrame.game.startThread(); - GameFrame.main.add(GameFrame.game, "game", 0); + // add the game to the CardLayout CameraPanel, enabling navigation + GameFrame.main.add(GameFrame.game, "game"); } catch (IOException | SpriteException | UnsupportedAudioFileException | LineUnavailableException ex) { ex.printStackTrace(); } + // switch to the game panel ((CardLayout)gameFrame.getLayout()).show(gameFrame, "game"); } else { + // switch to the panel indicated by the id of the textbox ((CardLayout) gameFrame.getLayout()).show(gameFrame, textBoxArray.get(currentBox).id); } } else { + // if the keypress is not ENTER, either select the textbox above or below the currently selected textbox currentBox = UtilityFunction.processBox(e, currentBox, textBoxArray); } } diff --git a/src/Middleware.java b/src/Middleware.java index b88aaf8..77f3f49 100644 --- a/src/Middleware.java +++ b/src/Middleware.java @@ -1,47 +1,38 @@ -import com.sun.jdi.request.DuplicateRequestException; - +// Eric Li, Charlie Zhao, ICS4U, Finished 6/16/2022 +// intercepts keystrokes and substitutes new keystrokes in the place of the old keystrokes +// allows for arbitrary replacement of keystrokes, including chained replacement of keystrokes if necessary +// although those features have not yet been implemented import java.awt.event.KeyEvent; import java.io.Serializable; import java.util.ArrayList; public class Middleware implements Serializable { - public static ArrayList allOldCode = new ArrayList(); - public static ArrayList allNewCode = new ArrayList(); + public static ArrayList allOldCode = new ArrayList<>(); + public static ArrayList allNewCode = new ArrayList<>(); public final int oldCode; public final int newCode; public boolean isDestroyed = false; + // create new Middleware which intercepts newCode and replaces it with oldCode Middleware(int oldCode, int newCode) { - // if (!canCreate(oldCode, newCode)) { - // TODO: replace with more appropriate exception - // throw new DuplicateRequestException(); - // } allOldCode.add(oldCode); allNewCode.add(newCode); this.oldCode = oldCode; this.newCode = newCode; } + // checks if the keypress can be intercepted (i.e., if it is the same keyCode as newCode) public boolean canIntercept(KeyEvent e) { return e.getKeyCode() == newCode && !isDestroyed; } + // intercepts the key if it has been found to be interceptable public KeyEvent interceptKey(KeyEvent e) { e.setKeyCode(oldCode); return e; } - public void destroy() { - allOldCode.remove(oldCode); - allNewCode.remove(newCode); - isDestroyed = true; - } - - public static boolean canCreate(int oldCode, int newCode) { - return (!allOldCode.contains(oldCode) && !allNewCode.contains(newCode)); - } - @Override public boolean equals(Object o) { try { diff --git a/src/NonPlayer.java b/src/NonPlayer.java index c685f76..70f6714 100644 --- a/src/NonPlayer.java +++ b/src/NonPlayer.java @@ -1,16 +1,14 @@ -/* Eric Li, ICS4U, Completed 5/29/2022 +/* Eric Li, ICS4U, Completed 6/19/2022 -Paddle class defines behaviours for the left and right player-controlled paddles */ +NonPlayer class defines behaviour for enemies and characters that are not controlled by the player */ import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.UnsupportedAudioFileException; import java.awt.*; -import java.awt.image.BufferedImage; import java.io.IOException; import java.io.Serializable; public class NonPlayer extends GenericSprite implements Serializable { - public final int SPEED = 3; // please note that these are not static, in contrast to the player class, as different enemies will have different heights public int npcWidth; public int npcHeight; @@ -22,7 +20,6 @@ public class NonPlayer extends GenericSprite implements Serializable { public int health; public double fadeCounter; - // private final Sound bump; public BufferedImageWrapper[][][] spriteArray; public NonPlayer(int x, int y, BufferedImageWrapper[][][] sprites, int npcWidth, int npcHeight, int health) throws UnsupportedAudioFileException, LineUnavailableException, IOException { diff --git a/src/Particle.java b/src/Particle.java index e1eda69..c0734b0 100644 --- a/src/Particle.java +++ b/src/Particle.java @@ -1,5 +1,4 @@ import java.awt.*; -import java.awt.image.BufferedImage; import java.io.IOException; import java.io.Serializable; diff --git a/src/SafeObjectInputStream.java b/src/SafeObjectInputStream.java index 3230005..d0e4715 100644 --- a/src/SafeObjectInputStream.java +++ b/src/SafeObjectInputStream.java @@ -2,7 +2,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectStreamClass; -import java.util.ArrayList; import java.util.List; public class SafeObjectInputStream extends ObjectInputStream { diff --git a/src/SettingPanel.java b/src/SettingPanel.java index de31668..5198958 100644 --- a/src/SettingPanel.java +++ b/src/SettingPanel.java @@ -10,14 +10,11 @@ Implements Runnable interface to use "threading" - let the game do two things at import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.UnsupportedAudioFileException; -import javax.swing.*; import java.awt.*; -import java.awt.event.*; -import java.awt.image.BufferedImage; +import java.awt.event.KeyEvent; +import java.awt.event.MouseEvent; import java.io.IOException; -import java.security.Key; import java.util.ArrayList; -import java.util.Arrays; public class SettingPanel extends MenuPanel { @@ -87,7 +84,7 @@ public class SettingPanel extends MenuPanel { } // move is repurposed to change key bind - public void move() { + public void doAction() { changeKeyBind(); } diff --git a/src/SingleTile.java b/src/SingleTile.java index 445dc4c..70ce65a 100644 --- a/src/SingleTile.java +++ b/src/SingleTile.java @@ -1,5 +1,4 @@ import java.awt.*; -import java.awt.image.BufferedImage; import java.io.Serializable; public class SingleTile extends Tile implements Serializable { diff --git a/src/SoundWrapper.java b/src/SoundWrapper.java index ac1eed8..f1e0c26 100644 --- a/src/SoundWrapper.java +++ b/src/SoundWrapper.java @@ -1,10 +1,6 @@ import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.UnsupportedAudioFileException; import java.io.*; -import javax.sound.sampled.AudioInputStream; -import javax.sound.sampled.AudioSystem; -import javax.sound.sampled.Clip; -import javax.swing.*; public class SoundWrapper implements Serializable { transient public Sound sound; diff --git a/src/StickyBomb.java b/src/StickyBomb.java index 4b673c7..b5ee74e 100644 --- a/src/StickyBomb.java +++ b/src/StickyBomb.java @@ -1,9 +1,6 @@ import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.UnsupportedAudioFileException; import java.awt.*; -import java.awt.event.KeyEvent; -import java.awt.event.MouseEvent; -import java.awt.image.BufferedImage; import java.io.IOException; import java.io.Serializable; diff --git a/src/UtilityFunction.java b/src/UtilityFunction.java index 5bb6c46..e1edcf3 100644 --- a/src/UtilityFunction.java +++ b/src/UtilityFunction.java @@ -1,7 +1,4 @@ import java.awt.event.KeyEvent; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.lang.reflect.Array; import java.util.ArrayList; public final class UtilityFunction {