Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/LevelManager.java
master
Chara1236 2022-06-20 19:28:11 -04:00
commit 045c1e3afd
20 changed files with 223 additions and 152 deletions

1
cheats.txt Normal file
View File

@ -0,0 +1 @@
Press P to skip to the next level.

View File

@ -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<String> lines = new ArrayList<String>();
// create new ArrayList that will compose of every line in the new text
ArrayList<String> 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;
}
}

View File

@ -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&&realX<p.x+Player.PLAYER_WIDTH&&y-p.y<Player.PLAYER_HEIGHT&&p.y-y<height){
return true;
}
return false;
}
// kills fireball if fireball has existed for longer than lifeSpan ticks
// moves fireball and kills it if it hits a block or a player
// kills player and resets level if it hits a player
public void move() throws UnsupportedAudioFileException, LineUnavailableException, IOException {
lifeSpan--;
if(lifeSpan<=0){
@ -56,9 +64,6 @@ public class FireBall extends GenericSprite{
dead = true;
GameFrame.game.player.reset();
}
// if(y<0||y>GameFrame.game.HEIGHT){
// dead = true;
// }
}
public void draw(Graphics g) throws IOException {
g.drawImage(GamePanel.getImage(spritePath),x-GameFrame.game.camera.x,y,null);

View File

@ -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<Middleware>)FileManager.
readObjectFromFile("local/controls", Arrays.asList("java.util.ArrayList", "Middleware"));
} catch (IOException | ClassNotFoundException | ClassCastException | SecurityException e) {
game.middlewareArray = new ArrayList<Middleware>();
}
*/
// 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"));

View File

@ -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

View File

@ -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 {

View File

@ -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;
}

View File

@ -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<String>(Arrays.asList(MapReader.inputDialogue(filePath)));
// convert dialogue from String[] to ArrayList<String>, 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);
}

View File

@ -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

View File

@ -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; i<GameFrame.game.map.length; i++){
Arrays.fill(GameFrame.game.map[i], null);
}
// read new map file
String file = FileManager.readFile(filePath);
// converts characters in map file into Tiles
for(int i=0; i<file.length(); i++){
// if a newline is reached, reset x position to 0 and increase y position by 1
if(file.charAt(i)=='\n'){
y+=1;
x=0;
}
// each x position translates into an actual pixel position of x*Tile.length - (GamePanel.GAME_WIDTH/2) on the screen
TileX = x*Tile.length - (GamePanel.GAME_WIDTH/2);
if(y==0){
if(y==0){ // if the tile is on the first row, it's shifted to the right to adjust for the start of the file
TileX += Tile.length;
}
// each y position translates into an actual pixel position of y*Tile.length on the screen; please note that Tile.length was used
// instead of something like Tile.height because Tiles are rectangles
TileY = y*Tile.length;
// if the char at the specific position in the file matches one of the options below, a tile with an image located at filePath is inserted at that position with newTile()
if(file.charAt(i)=='1'){
newTile("img/tiles/terrain/grass.png", x, y, TileX, TileY);
} else if(file.charAt(i)=='2'){
@ -77,31 +89,30 @@ public class MapReader implements Serializable {
newTile("img/tiles/terrain/cornerBottomLeft.png", x, y, TileX, TileY);
} else if(file.charAt(i)=='g'){
newTile("img/tiles/terrain/cornerBottomRight.png", x, y, TileX, TileY);
} else if(file.charAt(i)=='b'){
} else if(file.charAt(i)=='b'){ // this tile has its breakable variable set to true after being created by newTile() to let it be blown up by explosions
newTile("img/tiles/boxes/box.png", x, y, TileX, TileY);
GameFrame.game.map[x][y].breakable = true;
} else if(file.charAt(i)=='!'){
} else if(file.charAt(i)=='!'){ // instead of adding a tile, add an enemy slime at this location
GameFrame.game.enemy.add(new NonPlayer(TileX, TileY, GameFrame.game.slimeSpriteArray, 50, 28, 100));
} else if(file.charAt(i)=='+') {
} else if(file.charAt(i)=='+') { // this tile will send the player to the next level when the player steps on it
newTile("img/tiles/boxes/finish.png", x, y, TileX, TileY);
GameFrame.game.map[x][y].isFinish = true;
GameFrame.game.map[x][y].nonBombCollide = true;
} else if(file.charAt(i)=='v'){
} else if(file.charAt(i)=='v'){ // this tile is a tile that is part of the background; it is not subject to collisions or bomb explosions
newTile("img/tiles/background/wall.png", x, y, TileX, TileY);
GameFrame.game.map[x][y].collision = false;
GameFrame.game.map[x][y].replaceAble = true;
} else if(file.charAt(i)=='l'){
} else if(file.charAt(i)=='l'){ // this tile will kill you on contact; lava tiles also generate particles
newTile("img/tiles/terrain/lava.png", x, y, TileX, TileY);
GameFrame.game.map[x][y].nonBombCollide = true;
GameFrame.game.map[x][y].kills = true;
if(y>0&&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<String[]> 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<String[]> 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<String[]> returnArray = new ArrayList<String[]>();
ArrayList<String[]> returnArray = new ArrayList<>(); // create new ArrayList<String[]> 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))));
}

View File

@ -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<TextBox> textBoxArray = new ArrayList<TextBox>();
public ArrayList<TextBox> 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);
}
}

View File

@ -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<Integer> allOldCode = new ArrayList<Integer>();
public static ArrayList<Integer> allNewCode = new ArrayList<Integer>();
public static ArrayList<Integer> allOldCode = new ArrayList<>();
public static ArrayList<Integer> 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 {

View File

@ -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 {

View File

@ -1,5 +1,4 @@
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.Serializable;

View File

@ -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 {

View File

@ -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();
}

View File

@ -1,5 +1,4 @@
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.Serializable;
public class SingleTile extends Tile implements Serializable {

View File

@ -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;

View File

@ -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;

View File

@ -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 {