diff --git a/SuperPangWorld/src/resources/Herz.png b/SuperPangWorld/src/resources/Herz.png new file mode 100644 index 0000000..dc04122 Binary files /dev/null and b/SuperPangWorld/src/resources/Herz.png differ diff --git a/SuperPangWorld/src/resources/Player.png b/SuperPangWorld/src/resources/Player.png new file mode 100644 index 0000000..0f0154c Binary files /dev/null and b/SuperPangWorld/src/resources/Player.png differ diff --git a/SuperPangWorld/src/resources/Target.png b/SuperPangWorld/src/resources/Target.png new file mode 100644 index 0000000..b8d072e Binary files /dev/null and b/SuperPangWorld/src/resources/Target.png differ diff --git a/SuperPangWorld/src/superpangworld/GameLoopManager.java b/SuperPangWorld/src/superpangworld/GameLoopManager.java new file mode 100644 index 0000000..4883324 --- /dev/null +++ b/SuperPangWorld/src/superpangworld/GameLoopManager.java @@ -0,0 +1,2 @@ +package superpangworld;public class GameLoopManager { +} diff --git a/SuperPangWorld/src/superpangworld/GameView.java b/SuperPangWorld/src/superpangworld/GameView.java new file mode 100644 index 0000000..97656ce --- /dev/null +++ b/SuperPangWorld/src/superpangworld/GameView.java @@ -0,0 +1,2243 @@ +package view; + +import javax.imageio.ImageIO; +import javax.sound.sampled.*; +import javax.swing.Timer; +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; +import java.awt.font.TextAttribute; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.time.LocalDate; +import java.util.*; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Ein Fenster, welches das einfache Gestalten von Spielen erlaubt. Es wird eine Leinwand mit einer 16:9 Auflösung von + * {@value WIDTH} * {@value HEIGHT} Pixeln erzeugt, es kann Ton ausgegeben werden und Tastatur- und Mausereignisse + * werden zurückliefert. + * + * @author Andreas Berl + */ +public class GameView { + + private static class Version { + private final static String VERSION = "2021.05"; + private final static String VERSION_SHORT = VERSION.substring(0, 6); + private final static LocalDate DATE = LocalDate.parse("2021-02-28"); + private final static String STANDARD_TITLE = "GameView"; + private final static String SIGNATURE = "Prof. Dr. Andreas Berl - TH Deggendorf"; + + private static String getStatusSignature() { + return " " + STANDARD_TITLE + " " + VERSION_SHORT + " - " + SIGNATURE + " "; + } + + private static String getStandardTitle() { + return STANDARD_TITLE + " " + DATE.getYear(); + } + } + + // Auflösung + /** + * Breite der Leinwand in Pixeln. + */ + public static final int WIDTH = 960; + /** + * Höhe der Leinwand in Pixeln. + */ + public static final int HEIGHT = 540; + public static final java.awt.Rectangle BOUNDS = new java.awt.Rectangle(0, 0, GameView.WIDTH, GameView.HEIGHT); + + // Klassen + private final GameTime gameTime; + private final Canvas canvas; + private final Window window; + private final Mouse mouse; + private final Keyboard keyboard; + private final Sound sound; + private final SwingAdapter swingAdapter; + + /** + * Es wird eine Leinwand mit einer 16:9 Auflösung von {@value WIDTH} * {@value HEIGHT} Pixeln erzeugt (Breite = + * {@value WIDTH} Pixel, Höhe = {@value HEIGHT} + * Pixel). + *
+     * 
+     * 
+ * 0/0 . . . . . {@value WIDTH}/0
+ * . . . . . . . . . .
+ * . . . . . . . . . .
+ * . . . . . . . . . .
+ * 0/{@value HEIGHT} . . . {@value WIDTH}/{@value HEIGHT}
+ *
+ *
+ *
+ */ + public GameView() { + this.gameTime = new GameTime(); + this.swingAdapter = new SwingAdapter(); + this.window = new Window(swingAdapter); + this.mouse = new Mouse(swingAdapter); + this.keyboard = new Keyboard(); + this.sound = new Sound(); + this.canvas = new Canvas(); + + this.swingAdapter.registerListeners(mouse, keyboard, sound); + } + + /** + * Setzt den Fenstertitel. + * + * @param title Der Fenstertitel + */ + public void setWindowTitle(String title) { + window.setTitle(title); + } + + /** + * Legt ein Symbol für die Titelleiste fest. Das Symbolfile muss in einem Verzeichnis "src/resources" liegen. Bitte + * den Namen des Files ohne Verzeichnisnamen angeben, z.B.setWindowIcon("Symbol.png"). + * + * @param iconFileName Der Dateiname des Symbols. + */ + public void setWindowIcon(String iconFileName) { + window.setWindowIcon(iconFileName); + } + + /** + * Text, der in der Statuszeile angezeigt wird. + * + * @param statusText Text der Statuszeile. + */ + public void setStatusText(String statusText) { + window.setStatusText(statusText); + } + + /** + * Setzt die Hintergrundfarbe. + * + * @param backgroundColor Hintergrundfarbe + */ + public void setBackgroundColor(Color backgroundColor) { + canvas.setBackgroundColor(backgroundColor); + } + + + /** + * Fügt eine neue Farbe zur Farbtabelle für Block-Grafiken hinzu oder Überschreibt eine vorhandene Farbe mit neuen + * Werten. + *
+     * 
+     * 
+ * Die bereits vordefinierte Farbtabelle: + * 'R' = Color.RED + * 'r' = Color.RED.brighter() + * 'G' = Color.GREEN + * 'g' = Color.GREEN.brighter() + * 'B' = Color.BLUE + * 'b' = Color.BLUE.brighter() + * 'Y' = Color.YELLOW + * 'y' = Color.YELLOW.brighter() + * 'P' = Color.PINK + * 'p' = Color.PINK.brighter() + * 'C' = Color.CYAN + * 'c' = Color.CYAN.brighter() + * 'M' = Color.MAGENTA + * 'm' = Color.MAGENTA.brighter() + * 'O' = Color.ORANGE + * 'o' = Color.ORANGE.brighter() + * 'W' = Color.WHITE + * 'L' = Color.BLACK + *
+ *
+ * + * @param character Buchstabe, der der Farbe zugeordnet ist. + * @param color Die Farbe, die dem Buchstaben zugeordnet ist. + */ + public void setColorForBlockImage(char character, Color color) { + swingAdapter.setColorForBlockImage(character, color); + } + + /** + * Es wird ein Startbildschirm mit einem Auswahlmenü angezeigt. Die Auswahl des Benutzers wird zurückgegeben. + * + * @param title Titel des Programms. + * @param description Beschreibung des Programms. Achtung, es steht nicht viel Platz zur Verfügung. An den + * passenden Stellen müssen Zeilenumbrüche eingefügt werden. + * @param selectionTitle Titel des Auswahlmenüs. + * @param selectionItems Einträge des Auswahlmenüs. + * @param selectedItem Gibt an, welcher Eintrag vorselektiert sein soll. Der erste Eintrag hat den Wert 0. + * @return Der vom Benutzer gewählte Eintrag. Der erste Eintrag hat den Wert 0. + */ + public int showStartScreenWithChooseBox(String title, String description, String selectionTitle, + String[] selectionItems, int selectedItem) { + StartScreenWithChooseBox startScreenWithChooseBox = new StartScreenWithChooseBox(this, title, description, + selectionTitle, + selectionItems, selectedItem); + pollKeyEvents(); + startScreenWithChooseBox.printStartScreen(); + return startScreenWithChooseBox.getSelectedItem(); + } + + /** + * Es wird ein Startbildschirm mit einem einfachen Auswahlmenü angezeigt: "Easy", "Standard" und "Close Game". Die + * Auswahl des Benutzers wird zurückgegeben. Falls der Benutzer "Beenden" wählt, wird das Programm sofort beendet. + * + * @param title Titel des Programms. + * @param description Beschreibung des Programms. Achtung, es steht nicht viel Platz zur Verfügung. An den passenden + * Stellen müssen Zeilenumbrüche eingefügt werden. + * @return true falls "Easy" gewählt wurde, ansonsten false. + */ + public boolean showSimpleStartScreen(String title, String description) { + SimpleStartScreen simpleStartScreen = new SimpleStartScreen(this, title, description); + pollKeyEvents(); + simpleStartScreen.printStartScreen(); + String result = simpleStartScreen.getSelectedItem(); + if (result.equals("Close")) { + closeGameView(true); + } + return simpleStartScreen.getSelectedItem().equals("Easy"); + } + + /** + * Es wird ein Endbildschirm mit einem einfachen Auswahlmenü angezeigt: "Neu starten" und "Beenden". Falls der + * Benutzer "Beenden" wählt, wird das Programm sofort beendet. + * + * @param message Nachricht, die der Benutzer angezeigt bekommt. + */ + public void showEndScreen(String message) { + EndScreen endScreen = new EndScreen(this, message); + pollKeyEvents(); + endScreen.printEndScreen(); + if (!endScreen.playAgain()) { + closeGameView(true); + } + } + + /** + * Diese Methode kann bunte Block-Grafiken anzeigen. Dazu muss ein farbcodierter String übergeben + * werden, der dann auf die Leinwand (Canvas) übertragen wird, ohne die bisherigen Inhalte zu löschen. Die im + * String enthaltenen Buchstaben werden als Farben interpretiert. Jeder Buchstabe repräsentiert einen + * Block mit der Größe blockSize * blockSize. Beispiel: Ein rotes Dreieck mit grüner Füllung. + *
+ *
+     * 
+     * 
+ * String dreieck = + * " R \n" + + * " RGR \n" + + * " RGGGR \n" + + * "RRRRRRR\n"; + *
+ *
+ *
+ * Um die Farbcodes zu interpretieren, wird eine Farbpalette ausgewertet. Die Farben der Farbpalette lassen sich + * über die Methode {@link #setColorForBlockImage(char, Color)} anpassen. + *

+ * Es sind nur Zeichen erlaubt, die in der Farbpalette vorkommen, das Leerzeichen (Space) und Zeilenumbrüche. Das + * Leerzeichen ist transparent, man kann den Hintergrund sehen. Zusätzlich werden Koordinaten ausgewertet: (0, 0) + * ist links oben {@link #GameView()}. + * Negative Koordinaten können verwendet werden um Grafiken teilweise anzuzeigen. + *

+ * Die Größe der Blöcke muss mit dem Parameter blockSize festgelegt werden. Beispielsweise bedeutet + * blockSize = 10, dass ein Block die Fläche von 10 * 10 Pixeln belegt. + *

+ * Die Grafik kann mit einer Rotation dargestellt werden, dabei wird um den Mittelpunkt der Grafik rotiert. Eine + * Rotation um 0° stellt das Bild ohne Rotation dar, bei 180° steht das Bild auf dem Kopf. + * + * @param blockImage Das Bild als farbcodierter String. + * @param x x-Koordinate, bei welcher der Text angezeigt werden soll. 0 ist links. + * @param y y-Koordinate, bei welcher der Text angezeigt werden soll. 0 ist oben. + * @param blockSize Die Größe eines einzelnen Farbblocks. + * @param rotation Die Rotation des Bildes in Grad um den Mittelpunkt. + * @see #setColorForBlockImage(char, Color) + */ + public void addBlockImageToCanvas(String blockImage, double x, double y, double blockSize, double rotation) { + BufferedImage image = swingAdapter.createImageFromColorString(blockImage); + addImageToCanvasIfVisible(image, x, y, blockSize, rotation); + } + + /** + * Schreibt den übergebenen Text auf die Leinwand (Canvas), ohne die bisherigen Inhalte zu löschen. Zusätzlich + * werden Koordinaten ausgewertet: (0, 0) entspricht links oben {@link #GameView()}. + * Negative Koordinaten können verwendet werden um Texte teilweise anzuzeigen. Leerzeichen sind durchsichtig + * (Objekte im Hintergrund sind zu sehen). + *

+ * In dieser Methode muss die Schriftgröße angegeben werden, dabei bedeutet z.B. fontSize = 20, dass + * ein Buchstabe eine Fläche von 20 * 20 Pixeln belegt. + *

+ * Die Schrift kann mit einer Rotation dargestellt werden, dabei wird um den Mittelpunkt der Grafik rotiert. Eine + * Rotation um 0° stellt die Schrift ohne Rotation dar, bei 180° steht die Schrift auf dem Kopf. + * + * @param text Der anzuzeigende Text. + * @param x x-Koordinate, bei welcher der Text angezeigt werden soll. 0 ist links. + * @param y y-Koordinate, bei welcher der Text angezeigt werden soll. 0 ist oben. + * @param fontSize Die Schriftgröße. + * @param color Die Farbe, in der der Text angezeigt werden soll. + * @param rotation Die Rotation der Schrift in Grad um den Mittelpunkt. + */ + public void addTextToCanvas(String text, double x, double y, double fontSize, Color color, double rotation) { + BufferedImage image = swingAdapter.createImageFromText(text, color, (int) Math.round(fontSize)); + addImageToCanvasIfVisible(image, x, y, 1, rotation); + } + + /** + * Erzeugt eine Grafik aus einer Datei. Die Datei muss im Verzeichnis "src/resources" liegen. Bitte den Namen des + * Files ohne Verzeichnisnamen angeben, z.B."Raumschiff.png". + *

+ * Die Grafik wird auf die Leinwand (Canvas) übertragen, ohne die bisherigen Inhalte zu löschen. + *

+ * Koordinaten werden ausgewertet: (0, 0) ist links oben {@link #GameView()}. + * Negative Koordinaten können verwendet werden um Grafiken teilweise anzuzeigen. + *

+ * Die Größe der Grafik mit dem Parameter scaleFactor festgelegt werden. Beispielsweise bedeutet + * scaleFactor = 1, dass das Bild in Originalgröße angezeigt wird. + *

+ * Die Grafik kann mit einer Rotation dargestellt werden, dabei wird um den Mittelpunkt der Grafik rotiert. Eine + * Rotation um 0° stellt das Bild ohne Rotation dar, bei 180° steht das Bild auf dem Kopf. + * + * @param imageFile Das File mit dem Bild. + * @param x x-Koordinate, bei welcher das Bild angezeigt werden soll. 0 ist links. + * @param y y-Koordinate, bei welcher das Bild angezeigt werden soll. 0 ist oben. + * @param scaleFactor Skalierungsfaktor des Bildes. + * @param rotation Die Rotation des Bildes in Grad um den Mittelpunkt. + */ + public void addImageToCanvas(String imageFile, double x, double y, double scaleFactor, double rotation) { + BufferedImage image = swingAdapter.createImageFromFile(imageFile); + addImageToCanvasIfVisible(image, x, y, scaleFactor, rotation); + } + + private void addImageToCanvasIfVisible(BufferedImage image, double x, double y, double scaleFactor, + double rotation) { + int xInt = (int) Math.round(x); + int yInt = (int) Math.round(y); + int CenterX = xInt + image.getWidth() / 2; + int CenterY = yInt + image.getHeight() / 2; + int size = (int) Math.round((image.getWidth() * scaleFactor + image.getHeight() * scaleFactor) / 2); + if (intersectsGameViewBounds(CenterX - size, CenterY - size, 2 * size, 2 * size, 0)) { + canvas.addImageToCanvas(image, xInt, yInt, scaleFactor, rotation); + } + } + + /** + * Diese Methode kann ein farbiges Oval auf die Leinwand (Canvas) zeichnen, ohne die bisherigen Inhalte zu löschen. + *

+ * Die Koordinaten werden wie folgt ausgewertet: (0, 0) ist links oben {@link #GameView()}. + * Negative Koordinaten können verwendet werden um Ovale teilweise anzuzeigen. + * + * @param xCenter x-Koordinate des Mittelpunkts des Ovals. 0 ist links. + * @param yCenter y-Koordinate des Mittelpunkts des Ovals. 0 ist oben. + * @param width Breite des Ovals. + * @param height Höhe des Ovals. + * @param lineWeight Die Linienstärke des Ovals. + * @param filled Legt fest, ob das Oval gefüllt werden soll oder nicht. + * @param color Die Farbe des Ovals. + */ + public void addOvalToCanvas(double xCenter, double yCenter, double width, double height, double lineWeight, + boolean filled, Color color) { + int xInt = (int) Math.round(xCenter - width / 2); + int yInt = (int) Math.round(yCenter - height / 2); + int widthInt = (int) Math.round(width); + int heightInt = (int) Math.round(height); + if (intersectsGameViewBounds(xInt, yInt, widthInt, heightInt, lineWeight)) { + canvas.addOvalToCanvas((int) Math.round(xCenter), (int) Math.round(yCenter), widthInt, + heightInt, (int) Math.round(lineWeight), filled, color); + } + } + + /** + * Diese Methode kann ein farbiges Rechteck auf die Leinwand (Canvas) zeichnen, ohne die bisherigen Inhalte zu + * löschen. + *

+ * Die Koordinaten werden wie folgt ausgewertet: (0, 0) ist links oben {@link #GameView()}. + * Negative Koordinaten können verwendet werden um Rechtecke teilweise anzuzeigen. + * + * @param x x-Koordinate des linken oberen Ecks des Rechtecks. 0 ist links. + * @param y y-Koordinate des linken oberen Ecks des Rechtecks. 0 ist oben. + * @param width Breite des Rechtecks. + * @param height Höhe des Rechtecks. + * @param lineWeight Die Linienstärke. + * @param filled Legt fest, ob das Rechteck gefüllt werden soll oder nicht. + * @param color Die Farbe des Rechtecks. + */ + public void addRectangleToCanvas(double x, double y, double width, double height, double lineWeight, + boolean filled, Color color) { + int xInt = (int) Math.round(x); + int yInt = (int) Math.round(y); + int widthInt = (int) Math.round(width); + int heightInt = (int) Math.round(height); + if (intersectsGameViewBounds(xInt, yInt, widthInt, heightInt, lineWeight)) { + canvas.addRectangleToCanvas(xInt, yInt, widthInt, heightInt, (int) Math.round(lineWeight), filled, color); + } + } + + + /** + * Diese Methode kann eine farbige Linie auf die Leinwand (Canvas) zeichnen, ohne die bisherigen Inhalte zu + * löschen. + *

+ * Die Koordinaten werden wie folgt ausgewertet: (0, 0) ist links oben {@link #GameView()}. + * Negative Koordinaten können verwendet werden um Linien teilweise anzuzeigen. + * + * @param xStart x-Koordinate des Startpunkts der Linie. 0 ist links. + * @param yStart y-Koordinate des Startpunkts der Linie. 0 ist oben. + * @param xEnd x-Koordinate des Endpunkts der Linie. 0 ist links. + * @param yEnd y-Koordinate des Endpunkts der Linie. 0 ist oben. + * @param lineWeight Die Linienstärke. + * @param color Die Farbe der Linie. + */ + public void addLineToCanvas(double xStart, double yStart, double xEnd, double yEnd, double lineWeight, + Color color) { + int xStartInt = (int) Math.round(xStart); + int yStartInt = (int) Math.round(yStart); + int xEndInt = (int) Math.round(xEnd); + int yEndInt = (int) Math.round(yEnd); + int[] xs = new int[]{xStartInt, xEndInt}; + int[] ys = new int[]{yStartInt, yEndInt}; + if (intersectsGameViewBounds(xs, ys, lineWeight)) { + canvas.addLineToCanvas(xStartInt, yStartInt, xEndInt, yEndInt, (int) Math.round(lineWeight), color); + } + } + + /** + * Diese Methode kann eine farbige Poly-Linie (eine Linie zwischen mehreren Punkten) auf die Leinwand (Canvas) + * zeichnen, ohne die bisherigen Inhalte zu löschen. Dazu müssen alle Punkte der Poly-Linie angegeben werden. + *

+ * Die Koordinaten werden wie folgt ausgewertet: (0, 0) ist links oben {@link #GameView()}. + * Negative Koordinaten können verwendet werden um die Linien teilweise anzuzeigen. + * + * @param xCoordinates Die x-Koordinaten der Punkte der Poly-Linie. + * @param yCoordinates Die y-Koordinaten der Punkte der Poly-Linie. + * @param lineWeight Die Linienstärke. + * @param color Die Farbe der Poly-Linie. + */ + public void addPolyLineToCanvas(double[] xCoordinates, double[] yCoordinates, double lineWeight, Color color) { + int[] xs = convertDoubleToIntArray(xCoordinates); + int[] ys = convertDoubleToIntArray(yCoordinates); + if (intersectsGameViewBounds(xs, ys, lineWeight)) { + canvas.addPolyLineToCanvas(xs, ys, (int) Math.round(lineWeight), color); + } + } + + + /** + * Diese Methode kann ein farbiges Polygon auf die Leinwand (Canvas) zeichnen, ohne die bisherigen Inhalte zu + * löschen. Dazu müssen alle Punkte des Polygons angegeben werden. Der Letzte angegebene Punkt wird mit dem ersten + * Punkt des Polygons verbunden. + *

+ * Die Koordinaten werden wie folgt ausgewertet: (0, 0) ist links oben {@link #GameView()}. + * Negative Koordinaten können verwendet werden um das Polygon teilweise anzuzeigen. + * + * @param xCoordinates Die x-Koordinaten der Punkte des Polygons. + * @param yCoordinates Die y-Koordinaten der Punkte des Polygons. + * @param lineWeight Die Linienstärke. + * @param filled Legt fest, ob das Polygon gefüllt werden soll oder nicht. + * @param color Die Farbe des Polygons. + */ + public void addPolygonToCanvas(double[] xCoordinates, double[] yCoordinates, double lineWeight, boolean filled, + Color color) { + int[] xs = convertDoubleToIntArray(xCoordinates); + int[] ys = convertDoubleToIntArray(yCoordinates); + if (intersectsGameViewBounds(xs, ys, lineWeight)) { + canvas.addPolygonToCanvas(xs, ys, (int) Math.round(lineWeight), filled, color); + } + } + + private boolean intersectsGameViewBounds(int[] xs, int[] ys, double lineWeight) { + IntSummaryStatistics statX = Arrays.stream(xs).summaryStatistics(); + IntSummaryStatistics statY = Arrays.stream(ys).summaryStatistics(); + return intersectsGameViewBounds(statX.getMin(), statY.getMin(), statX.getMax() - statX.getMin(), + statY.getMax() - statY.getMin(), lineWeight); + + } + + private boolean intersectsGameViewBounds(int x, int y, int width, int height, double lineWeight) { + int halfLineWeight = (int) Math.round(lineWeight / 2); + java.awt.Rectangle rect = new java.awt.Rectangle(x - halfLineWeight, + y - halfLineWeight, + width + halfLineWeight, + height + halfLineWeight); + return rect.intersects(BOUNDS); + } + + private int[] convertDoubleToIntArray(double[] original) { + int[] converted = new int[original.length]; + for (int i = 0; i < converted.length; i++) { + converted[i] = (int) Math.round(original[i]); + } + return converted; + } + + + /** + * Zeigt den aktuellen Inhalt der Leinwand (Canvas) im Fenster an. Nach der Ausgabe wird der Inhalt der Leinwand + * gelöscht. Zwischen zwei Aufrufen dieser Methode werden automatisch immer mindestens 8 Millisekunden eingefügt. + * Das führt zu einer Darstellung von höchstens 120 Bildern pro Sekunde. + */ + public void printCanvas() { + window.printCanvas(canvas); + } + + /** + * Gibt den übergebenen Text direkt im Fenster aus. Es muss eine Darstellungsgröße gewählt werden: + *

+     * 
+ * 1: 160 Textspalten * 90 Textzeilen + * 2: 96 Textspalten * 54 Textzeilen + * 3: 80 Textspalten * 45 Textzeilen + * 4: 64 Textspalten * 36 Textzeilen + * 5: 48 Textspalten * 27 Textzeilen + * 6: 32 Textspalten * 18 Textzeilen + * 7: 16 Textspalten * 9 Textzeilen + *
+ *
+ * Zwischen zwei Aufrufen dieser Methode werden automatisch immer mindestens 8 Millisekunden eingefügt. Das führt zu + * einer Darstellung von höchstens 120 Bildern pro Sekunde. + * + * @param string Der anzuzeigende String. + * @param textSize Die Darstellungsgröße des Textes. + */ + public void print(String string, int textSize) { + int fontSize; + switch (textSize) { + case 7: + fontSize = 60; + break; + case 6: + fontSize = 30; + break; + case 5: + fontSize = 20; + break; + case 4: + fontSize = 15; + break; + case 3: + fontSize = 12; + break; + case 2: + fontSize = 10; + break; + case 1: + fontSize = 6; + break; + default: + throw new InputMismatchException("Falsche Darstellungsgröße (1 - 7): " + textSize); + } + setBackgroundColor(Color.BLACK); + addTextToCanvas(string, 0, 0, fontSize, Color.WHITE, 0); + printCanvas(); + } + + /** + * Liefert die Zeit in Millisekunden, die seit der Erzeugung des GameView-Fensters verstrichen sind. + * + * @return Zeit in Millisekunden. + */ + public int getGameTimeInMilliseconds() { + return gameTime.getTimeInMilliseconds(); + } + + /** + * Sets a new timer with the given duration. After the duration, the timer will expire. The timer starts + * immediately. + * + * @param name The identifier of the timer to be checked. + * @param objectID The id of the object that issued this request. This id should be unique for this object. + * @param duration The duration of the timer. After this duration the timer will expire. + */ + public void setTimer(String name, String objectID, long duration) { + gameTime.setTimer(name, objectID, duration); + } + + /** + * Checks if the timer that belongs to the given true already has expired. This method returns + * true, if the timer has never been set. This means this method can be called, even before the + * timer has been set. + * + * @param name The identifier of the timer to be checked. + * @param objectID The id of the object that issued this request. This id should be unique for this object. + * @return true, if timer has expired. It also is true, if timer has not never been set. + */ + public boolean timerExpired(String name, String objectID) { + return gameTime.timerExpired(name, objectID); + } + + /** + * Liefert alle Tastenereignisse die seit dem letzten Aufruf dieser Methode aufgelaufen sind als Array zurück. Es + * werden maximal die neuesten 25 Ereignisse zurückgegeben, alte Ereignisse werden gelöscht. + *

+ * Das Array enthält Ereignisse vom Typ {@link KeyEvent}. Der Typ des Events ist entweder
+ * KeyEvent.KEY_PRESSED (Taste wurde gedrückt),
+ * KeyEvent.KEY_RELEASED (Taste wurde losgelassen)
+ * oder KeyEvent.KEY_TYPED(Taste wurde getippt, funktioniert nur für sichtbare Zeichen). + *

+ * Sichtbare Zeichen lassen sich mit der Methode {@link KeyEvent#getKeyChar()} direkt auslesen. + *

+ * Bei Tastenereignissen gibt es die sogenannte Anschlagverzögerung. Das bedeutet, dass wenn man eine Taste gedrückt + * hält, dann wird die Taste einmal ausgelöst, dann folgt eine kurze Pause, dann folgt eine schnelle Wiederholung + * des Tastendrucks. Falls dieses Verhalten nicht gewünscht ist (z.B. bei der Steuerung von Spielfiguren), sollte + * statt dessen die Methode {@link #getKeyCodesOfCurrentlyPressedKeys()} verwendet werden. + * + *

+     * 
+     * 
+ * package demo; + * + * import java.awt.event.KeyEvent; + * + * public class KeyEventTest { + * GameView gameView; + * + * public KeyEventTest() { + * gameView = new GameView(); + * loop(); + * } + * + * public void loop() { + * while (true) { + * KeyEvent[] keyEvents = gameView.pollKeyEvents(); + * for (KeyEvent keyEvent : keyEvents) { + * if (keyEvent.getID() == KeyEvent.KEY_TYPED) { + * gameView.print("Taste: " + keyEvent.getKeyChar(), 6); + * } + * } + * } + * } + * } + *
+ *
+ *
+ * + * @return Alle KeyEvent Ereignisse seit dem letzten Aufruf dieser Methode. + * @see KeyEvent + * @see #getKeyCodesOfCurrentlyPressedKeys() + */ + public KeyEvent[] pollKeyEvents() { + return keyboard.pollKeyEvents(); + } + + /** + * Legt fest, ob die Maus im Fenster benutzt werden soll. Falls sie nicht benutzt wird, wird der Cursor der Maus auf + * den Default-Ansicht zurückgesetzt und die Maus wird ausgeblendet. Falls sie benutzt wird, werden Maus-Ereignisse + * erzeugt, die verwendet werden können. Die Standardeinstellung ist false. + * + * @param useMouse Legt fest, ob die Maus im Fenster benutzt werden soll. + */ + public void useMouse(boolean useMouse) { + mouse.useMouse(useMouse); + } + + /** + * Gibt zurück, ob die Maus eingeschaltet ist. + * + * @return true, falls die Maus eingeschaltet ist. + */ + public boolean isMouseEnabled() { + return mouse.useMouse; + } + + /** + * Legt ein neues Symbol für den Maus-Cursor fest. Die Bild-Datei muss im Verzeichnis "src/resources" liegen. Bitte + * den Namen der Datei ohne Verzeichnisnamen angeben, z.B. setMouseCursor("Cursor.png", false). + * + * @param fileName Name der Bild-Datei. Die Bild-Datei muss in einem Verzeichnis "src/resources" liegen. + * @param centered Gibt an, ob der Hotspot des Cursors in der Mitte des Symbols oder oben links liegen soll. + */ + public void setMouseCursor(String fileName, boolean centered) { + mouse.setMouseCursor(fileName, centered); + } + + /** + * Der Maus-Cursor wird auf das Standard-Icon zurückgesetzt. + */ + public void resetMouseCursor() { + mouse.setStandardMouseCursor(); + } + + /** + * Falls die Maus mit {@link #useMouse(boolean)} aktiviert wurde, liefert diese Methode alle gerade im Moment + * gedrückten Tasten als KeyCode der Klasse {@link KeyEvent} als Array zurück. Es handelt sich dabei + * um Ganzzahlen vom Typ Integer. Die Tasten sind in der Reihenfolge enthalten, in der sie gedrückt + * wurden. Diese + * Methode ist geeignet um die Steuerung von Spielfiguren zu realisieren. + *

+ * Ein Abgleich der KeyCodes kann über Konstanten der Klasse {@link KeyEvent} erfolgen. Beispielsweise kann die + * Leertaste mit Hilfe der Konstante {@link KeyEvent#VK_SPACE} abgeglichen werden. + *

+     * 
+     * 
+ * package demo; + * + * import java.awt.event.KeyEvent; + * + * public class PressedKeys { + * GameView gameView; + * + * public PressedKeys() { + * gameView = new GameView(); + * loop(); + * } + * + * private void loop() { + * while (true) { + * Integer[] pressedKeys = gameView.getKeyCodesOfCurrentlyPressedKeys(); + * String result = ""; + * for (int keyCode : pressedKeys) { + * if (keyCode == KeyEvent.VK_UP) { + * result += "UP\n"; + * } else if (keyCode == KeyEvent.VK_DOWN) { + * result += "Down\n"; + * } else if (keyCode == KeyEvent.VK_LEFT) { + * result += "Left\n"; + * } else if (keyCode == KeyEvent.VK_RIGHT) { + * result += "Right\n"; + * } else if (keyCode == KeyEvent.VK_SPACE) { + * result += "Space\n"; + * } + * } + * gameView.print(result, 6); + * } + * } + * } + * + *
+ *
+ * + * @return Alle gerade gedrückten Tasten als KeyCode in einem Array. + * @see KeyEvent + */ + public Integer[] getKeyCodesOfCurrentlyPressedKeys() { + return keyboard.getKeyCodesOfCurrentlyPressedKeys(); + } + + /** + * Falls die Maus mit {@link #useMouse(boolean)} aktiviert wurde, liefert diese Methode alle Mausereignisse die seit + * dem letzten Aufruf dieser Methode aufgelaufen sind als Array zurück. Es werden maximal die neuesten 25 Ereignisse + * zurückgegeben, alte Ereignisse werden gelöscht. Diese Methode ist geeignet um die Texteingaben vom Benutzer zu + * realisieren. + *

+ * Das Array enthält Ereignisse vom Typ {@link MouseEvent}. Das Ereignis enthält Koordinaten auf der Leinwand + * (Canvas) und die Information ob die Maus gedrückt, losgelassen, geklickt oder nur bewegt wurde. Um festzustellen, + * wie die Maus betätigt wurde, kann der Typ des Ereignisses mit {@link MouseEvent#getID()} abgefragt werden. + * Folgende Konstanten werden weitergeleitet: + *
+ * MouseEvent.MOUSE_PRESSED
+ * MouseEvent.MOUSE_RELEASED
+ * MouseEvent.MOUSE_CLICKED
+ * MouseEvent.MOUSE_MOVED
+ *
+ * Die Fensterkoordinaten können mit den Methoden
{@link MouseEvent#getX()} = X-Koordinate
{@link + * MouseEvent#getY()} = Y-Koordinate
abgerufen werden, um X und Y-Koordinate des Ereignisses zu bestimmen.
+ *
+ * Beispiel zur Erkennung einer gedrückten Maustaste:
+ * + *

+     * 
+     * 
+ * package demo; + * + * import java.awt.event.MouseEvent; + * + * public class MouseEventTest { + * GameView gameView; + * + * public MouseEventTest() { + * gameView = new GameView(); + * gameView.useMouse(true); + * loop(); + * } + * + * public void loop() { + * int x = 0; + * int y = 0; + * while(true) { + * MouseEvent[] mouseEvents = gameView.pollMouseEvents(); + * for (MouseEvent mouseEvent : mouseEvents) { + * if (mouseEvent.getID() == MouseEvent.MOUSE_PRESSED) { + * x = mouseEvent.getX(); + * y = mouseEvent.getY(); + * } + * } + * gameView.addTextToCanvas("X=" + x + " Y=" + y, x, y, 12, Color.WHITE); + * gameView.printCanvas(); + * } + * } + * } + *
+ *
+ *

+ * Mit {@link MouseEvent#getButton()} ()} lässt sich ermitteln, welche Maustaste betätigt wurde (links, rechts oder + * die Mitte). + * + * @return Alle Mausereignisse seit dem letzten Aufruf dieser Methode. + * @see MouseEvent + */ + public MouseEvent[] pollMouseEvents() { + return mouse.pollMouseEvents(); + } + + /** + * Spielt einen Sound ab (z.B. eine wav.-Datei). Das Soundfile muss in einem Verzeichnis "src/resources" liegen. + * Bitte den Namen des Files ohne Verzeichnisnamen angeben, z.B. playSound("sound.wav", false). Der + * Sound beendet sich selbst, sobald er fertig abgespielt wurde. Der Parameter "replay" kann genutzt werden um den + * Sound endlos zu wiederholen. Mit der Methode {@link #stopSound(int)} kann ein Sound frühzeitig beendet werden. + * Mit der Methode {@link #stopAllSounds()} können alle laufenden Sounds beendet werden. + * + * @param sound Name des Soundfiles. Das Soundfile muss in einem Verzeichnis "src/resources" liegen. + * @param replay Legt fest, ob der Sound endlos wiederholt werden soll. + * @return Die eindeutige Nummer des Soundfiles wird zurückgegeben. Diese Nummer kann genutzt werden um mit der + * Methode {@link #stopSound(int)} das Abspielen des Sounds zu beenden. + */ + public int playSound(String sound, boolean replay) { + return this.sound.playSound(sound, replay); + } + + /** + * Stoppt den Sound mit der angegebenen Nummer. Falls der Sound schon gestoppt wurde, passiert nichts. + * + * @param number Der eindeutige Nummer des Soundfiles, das gestoppt werden soll. + */ + public void stopSound(int number) { + sound.stopSound(number); + } + + /** + * Stoppt alle gerade spielenden Sounds. + */ + public void stopAllSounds() { + sound.stopAllSounds(); + } + + /** + * Schließt entweder nur das GameView-Fenster oder die ganze Anwendung. + * + * @param terminateEverything Wenn true ausgewählt wird, wird die komplette Anwendung mit + * System.exit(0)beendet. Ansonsten wird nur das Fenster geschlossen. + */ + public void closeGameView(boolean terminateEverything) { + window.closeWindow(terminateEverything); + } + + private static class GameTime { + + private final long startTimeInMilliseconds; + private final HashMap timers; + + private GameTime() { + this.startTimeInMilliseconds = System.currentTimeMillis(); + this.timers = new HashMap<>(200); + } + + private int getCurrentTime() { + return (int) (System.currentTimeMillis() - startTimeInMilliseconds); + } + + private int getTimeInMilliseconds() { + return getCurrentTime(); + } + + private void setTimer(String name, String objectID, long duration) { + timers.put(name + objectID, getCurrentTime() + duration); + } + + private boolean timerExpired(String name, String objectID) { + boolean expired = true; + try { + expired = timers.get(name + objectID) - getCurrentTime() <= 0; + } catch (NullPointerException ignored) { + } + return expired; + } + } + + private static class PrintObject { + int x; + int y; + Color color; + + public PrintObject(int x, int y, Color color) { + this.x = x; + this.y = y; + this.color = color; + } + } + + private static class Oval extends PrintObject { + int width; + int height; + int lineWeight; + boolean filled; + + public Oval(int xCenter, int yCenter, int width, int height, int lineWeight, boolean filled, Color color) { + super(xCenter, yCenter, color); + this.width = width; + this.height = height; + this.lineWeight = lineWeight; + this.filled = filled; + } + } + + private static class Rectangle extends PrintObject { + int width; + int height; + int lineWeight; + boolean filled; + + public Rectangle(int x, int y, int width, int height, int lineWeight, boolean filled, Color color) { + super(x, y, color); + this.width = width; + this.height = height; + this.lineWeight = lineWeight; + this.filled = filled; + } + } + + private static class Line extends PrintObject { + int xEnd; + int yEnd; + int lineWeight; + + public Line(int xStart, int yStart, int xEnd, int yEnd, int lineWeight, Color color) { + super(xStart, yStart, color); + this.xEnd = xEnd; + this.yEnd = yEnd; + this.lineWeight = lineWeight; + } + } + + private static class Polygon extends PrintObject { + int[] xCoordinates; + int[] yCoordinates; + int lineWeight; + boolean filled; + + public Polygon(int[] xCoordinates, int[] yCoordinates, int lineWeight, boolean filled, Color color) { + super(xCoordinates[0], yCoordinates[0], color); + this.xCoordinates = xCoordinates; + this.yCoordinates = yCoordinates; + this.lineWeight = lineWeight; + this.filled = filled; + } + } + + private static class PolyLine extends PrintObject { + int[] xCoordinates; + int[] yCoordinates; + int lineWeight; + + public PolyLine(int[] xCoordinates, int[] yCoordinates, int lineWeight, Color color) { + super(xCoordinates[0], yCoordinates[0], color); + this.xCoordinates = xCoordinates; + this.yCoordinates = yCoordinates; + this.lineWeight = lineWeight; + } + } + + private static class ImageObject extends PrintObject { + BufferedImage image; + double scaleFactor; + double rotation; + + public ImageObject(int x, int y, BufferedImage image, double scaleFactor, double rotation) { + super(x, y, Color.BLACK); + this.scaleFactor = scaleFactor; + this.image = image; + this.rotation = rotation; + } + } + + private static class Canvas implements Cloneable { + private Color backgroundColor; + private final ArrayList printObjects; + + Canvas() { + this.backgroundColor = Color.black; + this.printObjects = new ArrayList<>(30000); + } + + void setBackgroundColor(Color backgroundColor) { + this.backgroundColor = backgroundColor; + } + + Color getBackgroundColor() { + return backgroundColor; + } + + ArrayList getPrintObjects() { + return printObjects; + } + + public void addImageToCanvas(BufferedImage image, int x, int y, double scaleFactor, double rotation) { + printObjects.add(new ImageObject(x, y, image, scaleFactor, rotation)); + } + + void addRectangleToCanvas(int x, int y, int width, int height, int lineWeight, boolean filled, Color color) { + printObjects.add(new Rectangle(x, y, width, height, lineWeight, filled, color)); + } + + void addLineToCanvas(int xStart, int yStart, int xEnd, int yEnd, int lineWeight, Color color) { + printObjects.add(new Line(xStart, yStart, xEnd, yEnd, lineWeight, color)); + } + + void addOvalToCanvas(int xCenter, int yCenter, int width, int height, int lineWeight, boolean filled, + Color color) { + printObjects.add(new Oval(xCenter, yCenter, width, height, lineWeight, filled, color)); + } + + void addPolygonToCanvas(int[] xCoordinates, int[] yCoordinates, int lineWeight, boolean filled, Color color) { + if (xCoordinates.length != yCoordinates.length) { + throw new InputMismatchException("Die Anzahl der X- und Y-Koordinaten ist nicht gleich!"); + } + printObjects.add(new Polygon(xCoordinates, yCoordinates, lineWeight, filled, color)); + } + + void addPolyLineToCanvas(int[] xCoordinates, int[] yCoordinates, int lineWeight, Color color) { + if (xCoordinates.length != yCoordinates.length) { + throw new InputMismatchException("Die Anzahl der X- und Y-Koordinaten ist nicht gleich!"); + } + printObjects.add(new PolyLine(xCoordinates, yCoordinates, lineWeight, color)); + } + } + + private static class Frame extends JFrame { + + private Mouse mouse; + private Keyboard keyboard; + + private final JPanel statusBar; + private JLabel statusLabelLinks; + + void registerListeners(Mouse mouse, Keyboard keyboard) { + // Klassen + this.mouse = mouse; + this.keyboard = keyboard; + } + + Frame(PaintingPanel paintingPanel) { + + statusBar = new JPanel() { + { + setLayout(new BorderLayout()); + setBorder(BorderFactory.createRaisedBevelBorder()); + setBackground(Color.WHITE); + setForeground(Color.BLACK); + statusLabelLinks = new JLabel(); + statusLabelLinks.setBackground(Color.WHITE); + statusLabelLinks.setForeground(Color.BLACK); + statusLabelLinks.setHorizontalAlignment(JLabel.LEFT); + + JLabel statusLabelRechts = new JLabel(Version.getStatusSignature()); + statusLabelRechts.setBackground(Color.WHITE); + statusLabelRechts.setForeground(Color.BLACK); + statusLabelRechts.setHorizontalAlignment(JLabel.RIGHT); + add(statusLabelLinks, BorderLayout.WEST); + add(statusLabelRechts, BorderLayout.EAST); + } + }; + + JPanel center = new JPanel(new GridBagLayout()); + center.setBackground(Color.BLACK); + center.add(paintingPanel); + + + // Struktur + paintingPanel.setPreferredSize(new Dimension(GameView.WIDTH, GameView.HEIGHT)); + JPanel textPanelAndStatusBar = new JPanel(new BorderLayout(5, 5)); + textPanelAndStatusBar.setBackground(Color.BLACK); + textPanelAndStatusBar.add(new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)), BorderLayout.NORTH); + textPanelAndStatusBar.add(new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)), BorderLayout.EAST); + textPanelAndStatusBar.add(new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)), BorderLayout.WEST); + textPanelAndStatusBar.add(center, BorderLayout.CENTER); + textPanelAndStatusBar.add(statusBar, BorderLayout.SOUTH); + add(textPanelAndStatusBar); + + // Eigenschaften + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + setTitle(Version.getStandardTitle()); + paintingPanel.requestFocus(); + setResizable(true); + + + // Listeners + addKeyListener(new KeyListener() { + + @Override + public void keyTyped(KeyEvent keyEvent) { + all(keyEvent); + } + + @Override + public void keyReleased(KeyEvent keyEvent) { + all(keyEvent); + } + + @Override + public void keyPressed(KeyEvent keyEvent) { + all(keyEvent); + } + + private void all(KeyEvent keyEvent) { + if (keyboard != null) { + keyboard.update(keyEvent); + } + } + }); + MouseAdapter mouseAdapter = new MouseAdapter() { + @Override + public void mouseReleased(MouseEvent mouseEvent) { + all(mouseEvent); + } + + @Override + public void mousePressed(MouseEvent mouseEvent) { + all(mouseEvent); + } + + @Override + public void mouseMoved(MouseEvent mouseEvent) { + all(mouseEvent); + } + + @Override + public void mouseClicked(MouseEvent mouseEvent) { + all(mouseEvent); + } + + private void all(MouseEvent mouseEvent) { + if (mouse != null) { + mouse.update(mouseEvent); + } + } + }; + paintingPanel.addMouseMotionListener(mouseAdapter); + paintingPanel.addMouseListener(mouseAdapter); + + final Timer packTimer = new Timer(500, actionEvent -> { + if (getExtendedState() != MAXIMIZED_BOTH) { + Point location = getLocation(); + pack(); + setLocation(location); + } + }); + packTimer.setRepeats(false); + + addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + super.componentResized(e); + double scalingFactor = Math.min(paintingPanel.getParent().getWidth() * 1d / GameView.WIDTH, + paintingPanel.getParent().getHeight() * 1d / GameView.HEIGHT); + int newWidth = (int) Math.round(GameView.WIDTH * scalingFactor); + int newHeight = (int) Math.round(GameView.HEIGHT * scalingFactor); + paintingPanel.setPreferredSize(new Dimension(newWidth, newHeight)); + paintingPanel.setMinimumSize(new Dimension(newWidth, newHeight)); + paintingPanel.setSize(new Dimension(newWidth, newHeight)); + paintingPanel.setMaximumSize(new Dimension(newWidth, newHeight)); + if (packTimer.isRunning()) { + packTimer.restart(); + } else { + packTimer.start(); + } + revalidate(); + } + }); + + // Location und Ausgeben + int newWidth = 1280; + int newHeight = 720; + paintingPanel.setPreferredSize(new Dimension(newWidth, newHeight)); + + pack(); + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + if (getHeight() > screenSize.height || getWidth() > screenSize.width) { + newWidth = GameView.WIDTH * 8 / 10; + newHeight = GameView.HEIGHT * 8 / 10; + paintingPanel.setPreferredSize(new Dimension(newWidth, newHeight)); + pack(); + } + setLocationRelativeTo(null); + setVisible(true); + } + + JLabel getStatusLabelLinks() { + return statusLabelLinks; + } + + JPanel getStatusBar() { + return statusBar; + } + } + + private static class Keyboard { + private final ArrayBlockingQueue keyboardEvents; + private final ArrayBlockingQueue keyCodesOfCurrentlyPressedKeys; + + private final static int KEY_EVENT_BUFFER_SIZE = 25; + + Keyboard() { + keyboardEvents = new ArrayBlockingQueue<>(KEY_EVENT_BUFFER_SIZE, true); + keyCodesOfCurrentlyPressedKeys = new ArrayBlockingQueue<>(10, true); + } + + void update(KeyEvent keyEvent) { + if (keyboardEvents.size() == KEY_EVENT_BUFFER_SIZE) { + keyboardEvents.remove(); + } + keyboardEvents.add(keyEvent); + if (keyEvent.getID() == KeyEvent.KEY_PRESSED) { + if (!keyCodesOfCurrentlyPressedKeys.contains(keyEvent.getKeyCode())) + keyCodesOfCurrentlyPressedKeys.add(keyEvent.getKeyCode()); + } else if (keyEvent.getID() == KeyEvent.KEY_RELEASED) { + keyCodesOfCurrentlyPressedKeys.remove(keyEvent.getKeyCode()); + } + } + + KeyEvent[] pollKeyEvents() { + KeyEvent[] events = new KeyEvent[0]; + if (keyboardEvents.size() > 0) { + events = keyboardEvents.toArray(events); + keyboardEvents.clear(); + } + return events; + } + + Integer[] getKeyCodesOfCurrentlyPressedKeys() { + Integer[] keyCodes = new Integer[0]; + if (keyCodesOfCurrentlyPressedKeys.size() > 0) { + keyCodes = keyCodesOfCurrentlyPressedKeys.toArray(keyCodes); + } + return keyCodes; + } + } + + private static class Mouse implements ActionListener { + private final SwingAdapter swingAdapter; + + private boolean invisibleMouseCursor; + private boolean invisibleMouseCursorMoved; + private final Timer invisibleMouseTimer; + + private final static int MOUSE_EVENT_BUFFER_SIZE = 25; + private final ArrayBlockingQueue mousePointerEvents; + + private boolean useMouse; + + Mouse(SwingAdapter swingAdapter) { + this.swingAdapter = swingAdapter; + this.invisibleMouseCursor = false; + this.invisibleMouseCursorMoved = true; + this.mousePointerEvents = new ArrayBlockingQueue<>(MOUSE_EVENT_BUFFER_SIZE, true); + this.invisibleMouseTimer = new Timer(500, this); + setMouseInvisible(); + } + + private void setMouseInvisible() { + this.useMouse = false; + setInvisibleMouseCursor(); + if (!invisibleMouseTimer.isRunning()) { + invisibleMouseTimer.start(); + } + } + + @Override + public void actionPerformed(ActionEvent actionEvent) { + if (invisibleMouseCursorMoved) { + if (invisibleMouseCursor) { + setStandardMouseCursor(); + } + invisibleMouseCursorMoved = false; + } else { + if (!invisibleMouseCursor) { + setInvisibleMouseCursor(); + } + } + } + + void useMouse(boolean useMouse) { + if (useMouse == this.useMouse) { + return; + } + if (useMouse) { + this.useMouse = true; + setStandardMouseCursor(); + invisibleMouseTimer.stop(); + } else { + setMouseInvisible(); + } + } + + void setStandardMouseCursor() { + this.invisibleMouseCursor = false; + swingAdapter.setStandardMouseCursor(); + } + + void setMouseCursor(String cursorImageFile, boolean centered) { + this.invisibleMouseCursor = false; + swingAdapter.setMouseCursor(cursorImageFile, centered); + } + + private void setInvisibleMouseCursor() { + invisibleMouseCursor = true; + swingAdapter.setInvisibleMouseCursor(); + } + + void update(MouseEvent mouseEvent) { + if (useMouse) { + int mouseEventY = GameView.HEIGHT * mouseEvent.getY() / swingAdapter.getTextDisplaySize().height; + int mouseEventX = GameView.WIDTH * mouseEvent.getX() / swingAdapter.getTextDisplaySize().width; + MouseEvent fixedMouseEvent = new MouseEvent(mouseEvent.getComponent(), mouseEvent.getID(), + mouseEvent.getWhen(), mouseEvent.getModifiersEx(), + mouseEventX, mouseEventY, mouseEvent.getClickCount(), + mouseEvent.isPopupTrigger(), mouseEvent.getButton()); + if (mousePointerEvents.size() == MOUSE_EVENT_BUFFER_SIZE) { + mousePointerEvents.remove(); + } + mousePointerEvents.add(fixedMouseEvent); + } else { + invisibleMouseCursorMoved = true; + } + } + + MouseEvent[] pollMouseEvents() { + MouseEvent[] events = new MouseEvent[0]; + if (mousePointerEvents.size() > 0) { + events = mousePointerEvents.toArray(events); + mousePointerEvents.clear(); + } + return events; + } + } + + private static class Sound { + private final ConcurrentHashMap> clips; + private static int soundCounter; + + Sound() { + this.clips = new ConcurrentHashMap<>(); + soundCounter = 0; + } + + int playSound(String sound, boolean replay) { + final int number = ++soundCounter; + clips.put(number, Optional.empty()); + new Thread(() -> { + try { + AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(GameView.class.getResource( + "/resources/" + sound)); + Clip clip = AudioSystem.getClip(); + clip.open(audioInputStream); + clip.addLineListener(event -> { + if (event.getType().equals(LineEvent.Type.STOP)) { + event.getLine().close(); + } + }); + if (replay) { + clip.loop(Clip.LOOP_CONTINUOUSLY); + } else { + clip.start(); + } + clips.put(number, Optional.of(clip)); + } catch (Exception e) { + System.err.println("Soundfile \"" + sound + "\" konnte nicht abgespielt werden!"); + e.printStackTrace(); + System.exit(1); + } + }).start(); + return number; + } + + void stopSound(int number) { + new Thread(() -> { + synchronized (clips) { + if (clips.containsKey(number)) { + while (clips.get(number).isEmpty()) { + Thread.onSpinWait(); + } + Clip clip = clips.get(number).get(); + // Mute the clip + BooleanControl muteControl = (BooleanControl) clip.getControl(BooleanControl.Type.MUTE); + if (muteControl != null) { + muteControl.setValue(true); // True to mute the line + } + clip.stop(); + clips.remove(number); + } + } + }).start(); + } + + void stopAllSounds() { + Integer[] keys = clips.keySet().toArray(new Integer[0]); + for (Integer number : keys) { + stopSound(number); + } + } + } + + private static class SwingAdapter { + + private final PaintingPanel paintingPanel; + private final Frame frame; + private Sound sound; + private Mouse mouse; + private final Font font; + private BufferedImage bufferedImage; + private final BufferedImage[] bufferedImages; + private int currentBufferedImage; + private Graphics2D g2D; + private HashMap colorMap; + private final HashMap imageMap; + private double sizeOfImageMapInMB; + private final static int IMAGE_MAP_LIMIT_IN_MB = 1000; + + SwingAdapter() { + this.paintingPanel = new PaintingPanel(); + this.frame = new Frame(paintingPanel); + this.bufferedImages = new BufferedImage[5]; + this.currentBufferedImage = 0; + for (int i = 0; i < bufferedImages.length; i++) { + bufferedImages[i] = new BufferedImage(GameView.WIDTH, GameView.HEIGHT, BufferedImage.TYPE_INT_RGB); + } + this.bufferedImage = bufferedImages[currentBufferedImage]; + this.g2D = bufferedImage.createGraphics(); + Map fontMap = new HashMap<>(); + fontMap.put(TextAttribute.FAMILY, "Monospaced"); + fontMap.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD); + this.font = new Font(fontMap); + initColorMap(); + this.imageMap = new HashMap<>(); + } + + public void setColorForBlockImage(char character, Color color) { + colorMap.put(character, color); + } + + void registerListeners(Mouse mouse, Keyboard keyboard, Sound sound) { + frame.registerListeners(mouse, keyboard); + this.sound = sound; + this.mouse = mouse; + } + + private void initColorMap() { + colorMap = new HashMap<>(); + colorMap.put('R', Color.RED); + colorMap.put('r', Color.RED.darker()); + colorMap.put('G', Color.GREEN); + colorMap.put('g', Color.GREEN.darker()); + colorMap.put('B', Color.BLUE); + colorMap.put('b', Color.BLUE.darker()); + colorMap.put('Y', Color.YELLOW); + colorMap.put('y', Color.YELLOW.darker()); + colorMap.put('P', Color.PINK); + colorMap.put('p', Color.PINK.darker()); + colorMap.put('C', Color.CYAN); + colorMap.put('c', Color.CYAN.darker()); + colorMap.put('M', Color.MAGENTA); + colorMap.put('m', Color.MAGENTA.darker()); + colorMap.put('O', Color.ORANGE); + colorMap.put('o', Color.ORANGE.darker()); + colorMap.put('W', Color.WHITE); + colorMap.put('L', Color.BLACK); + } + + // Anzeige + void setStatusText(String statusText) { + SwingUtilities.invokeLater(() -> { + frame.getStatusLabelLinks().setText(statusText); + int minWidth = frame.getStatusBar().getPreferredSize().width + 50; + frame.setMinimumSize(new Dimension(minWidth, minWidth / 16 * 9)); + }); + } + + void printToDisplay(ArrayList printObjects, Color backgroundColor) { + currentBufferedImage = currentBufferedImage < bufferedImages.length - 1 ? ++currentBufferedImage : 0; + this.bufferedImage = bufferedImages[currentBufferedImage]; + createImageFromPrintObjects(printObjects, backgroundColor); + paintingPanel.bufferedImage = bufferedImage; + paintingPanel.repaint(); + } + + private void createImageFromPrintObjects(ArrayList printObjects, Color backgroundColor) { + g2D = bufferedImage.createGraphics(); + g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2D.setColor(backgroundColor); + g2D.fillRect(0, 0, GameView.WIDTH, GameView.HEIGHT); + char[] chars; + for (PrintObject p : printObjects) { + if (p.color != null) { + g2D.setColor(p.color); + if (p.getClass() == Oval.class) { + Oval oval = (Oval) p; + int x = oval.x - oval.width / 2; + int y = oval.y - oval.height / 2; + if (oval.filled) { + g2D.fillOval(x, y, oval.width + oval.lineWeight, oval.height + oval.lineWeight); + } else { + g2D.setStroke(new BasicStroke(oval.lineWeight)); + g2D.drawOval(x + oval.lineWeight / 2, y + oval.lineWeight / 2, oval.width, oval.height); + } + } else if (p.getClass() == Line.class) { + Line line = (Line) p; + g2D.setStroke(new BasicStroke(line.lineWeight)); + g2D.drawLine(line.x, line.y, line.xEnd, line.yEnd); + } else if (p.getClass() == Rectangle.class) { + Rectangle rectangle = (Rectangle) p; + if (rectangle.filled) { + g2D.fillRect(rectangle.x, rectangle.y, rectangle.width + rectangle.lineWeight, + rectangle.height + rectangle.lineWeight); + } else { + g2D.setStroke(new BasicStroke(rectangle.lineWeight)); + g2D.drawRect(rectangle.x + rectangle.lineWeight / 2, + rectangle.y + rectangle.lineWeight / 2, rectangle.width, rectangle.height); + } + } else if (p.getClass() == Polygon.class) { + Polygon polygon = (Polygon) p; + if (polygon.filled) { + g2D.fillPolygon(polygon.xCoordinates, polygon.yCoordinates, polygon.xCoordinates.length); + } else { + g2D.setStroke(new BasicStroke(polygon.lineWeight)); + g2D.drawPolygon(polygon.xCoordinates, polygon.yCoordinates, polygon.xCoordinates.length); + } + } else if (p.getClass() == PolyLine.class) { + PolyLine polyLine = (PolyLine) p; + g2D.setStroke(new BasicStroke(polyLine.lineWeight)); + g2D.drawPolyline(polyLine.xCoordinates, polyLine.yCoordinates, polyLine.xCoordinates.length); + } else if (p.getClass() == ImageObject.class) { + ImageObject imageObject = (ImageObject) p; + AffineTransform trans = g2D.getTransform(); + trans.translate(imageObject.x, imageObject.y); + trans.scale(imageObject.scaleFactor, imageObject.scaleFactor); + trans.rotate(Math.toRadians(imageObject.rotation), imageObject.image.getWidth() / 2.0, + imageObject.image.getHeight() / 2.0); + g2D.drawImage(imageObject.image, trans, null); + } + } + } + g2D.dispose(); + } + + BufferedImage createImageFromFile(String imageFileName) { + int hash = imageFileName.hashCode(); + BufferedImage image = imageMap.get(hash); + if (image == null) { + try { + image = ImageIO.read(GameView.class.getResource("/resources/" + imageFileName)); + } catch (Exception e) { + e.printStackTrace(); + System.err.println("Symbolfile \"" + imageFileName + "\" konnte nicht gefunden werden!"); + System.exit(1); + } + addImageToMapOrClearMap(hash, image); + } + return image; + } + + BufferedImage createImageFromColorString(String colorString) { + int hash = colorString.hashCode(); + BufferedImage image = imageMap.get(hash); + if (image == null) { + String[] lines = colorString.split("\\R"); + int height = lines.length; + int width = Arrays.stream(lines).mapToInt(String::length).max().orElse(1); + image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE); + Graphics2D g2D = image.createGraphics(); + for (int i = 0; i < lines.length; i++) { + char[] blocks = lines[i].toCharArray(); + for (int j = 0; j < blocks.length; j++) { + Color color = colorMap.get(blocks[j]); + if (color != null) { + g2D.setColor(color); + g2D.fillRect(j, i, 1, 1); + } + } + } + g2D.dispose(); + addImageToMapOrClearMap(hash, image); + } + return image; + } + + BufferedImage createImageFromText(String text, Color color, int fontSize) { + int hash = Objects.hash(text, color, fontSize); + BufferedImage image = imageMap.get(hash); + if (image == null) { + String[] lines = text.split("\\R"); + int height = lines.length * fontSize; + int width = Arrays.stream(lines).mapToInt(String::length).max().orElse(1) * fontSize; + image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE); + Graphics2D imageG2D = image.createGraphics(); + Font imageFont = this.font.deriveFont((float) fontSize); + imageG2D.setFont(imageFont); + FontMetrics imageFontMetrics = imageG2D.getFontMetrics(imageFont); + char[] chars; + imageG2D.setColor(color); + for (int i = 0; i < lines.length; i++) { + chars = lines[i].toCharArray(); + for (int c = 0; c < chars.length; c++) { + imageG2D.drawChars(chars, c, 1, + (fontSize * c) + (fontSize - imageFontMetrics.charWidth('W')) / 2, + (i * fontSize) + (fontSize + imageFontMetrics.getAscent() - imageFontMetrics.getDescent() - fontSize / 10) / 2); + } + } + g2D.dispose(); + addImageToMapOrClearMap(hash, image); + } + return image; + } + + private void addImageToMapOrClearMap(int hash, BufferedImage image) { + if (sizeOfImageMapInMB > IMAGE_MAP_LIMIT_IN_MB) { + imageMap.clear(); + sizeOfImageMapInMB = 0; + } + imageMap.put(hash, image); + sizeOfImageMapInMB += image.getHeight() * image.getWidth() * 0.000004; + } + + // Fenster-Dekorationen + void setTitle(String title) { + frame.setTitle(title); + } + + void setWindowIcon(String windowIcon) { + Image fensterSymbol = null; + try { + fensterSymbol = new ImageIcon(GameView.class.getResource("/resources/" + windowIcon)).getImage(); + } catch (Exception e) { + e.printStackTrace(); + System.err.println("Symbolfile \"" + windowIcon + "\" konnte nicht gefunden werden!"); + } + frame.setIconImage(fensterSymbol); + } + + // Maus Cursor + void setMouseCursor(String cursor, boolean centered) { + try { + Image im = new ImageIcon(GameView.class.getResource("/resources/" + cursor)).getImage(); + SwingUtilities.invokeLater(() -> paintingPanel.setCursor(createCursor(im, centered))); + } catch (Exception e) { + System.out.println("Cursor-Datei konnte nicht gefunden werden!"); + System.exit(1); + } + } + + private Cursor createCursor(Image im, boolean centered) { + Toolkit toolkit = paintingPanel.getToolkit(); + Dimension cursorSize = Toolkit.getDefaultToolkit().getBestCursorSize(64, 64); + Point cursorHotSpot = new Point(0, 0); + if (centered) { + cursorHotSpot = new Point(cursorSize.width / 2, cursorSize.height / 2); + } + return toolkit.createCustomCursor(im, cursorHotSpot, "Cross"); + } + + void setStandardMouseCursor() { + SwingUtilities.invokeLater(() -> paintingPanel.setCursor(Cursor.getDefaultCursor())); + } + + void setInvisibleMouseCursor() { + Image im = new ImageIcon("").getImage(); + SwingUtilities.invokeLater(() -> paintingPanel.setCursor(createCursor(im, false))); + } + + // Beenden + void closeWindow(boolean terminateEverything) { + frame.dispose(); + sound.stopAllSounds(); + mouse.invisibleMouseTimer.stop(); + if (terminateEverything) { + System.exit(0); + } + } + + Dimension getTextDisplaySize() { + return paintingPanel.getSize(); + } + } + + private static class PaintingPanel extends JPanel { + + volatile BufferedImage bufferedImage; + + PaintingPanel() { + setBackground(Color.BLACK); + setForeground(Color.WHITE); + bufferedImage = new BufferedImage(GameView.WIDTH, GameView.HEIGHT, BufferedImage.TYPE_INT_RGB); + setDoubleBuffered(false); + setIgnoreRepaint(true); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + double scalingFactor = Math.min(getWidth() * 1d / GameView.WIDTH, getHeight() * 1d / GameView.HEIGHT); + Graphics2D g2D = (Graphics2D) g; + AffineTransform trans = g2D.getTransform(); + trans.scale(scalingFactor, scalingFactor); + g2D.setTransform(trans); + g2D.drawImage(bufferedImage, 0, 0, null); + g2D.dispose(); + } + } + + + private static class Window { + + private final SwingAdapter swingAdapter; + private long lastPrintTimeInNanos; + private final static int FRAMES_PER_SECOND = 120; + private final static int NANOS_PER_FRAME = 1_000_000_000 / FRAMES_PER_SECOND; + + Window(SwingAdapter swingAdapter) { + this.swingAdapter = swingAdapter; + this.lastPrintTimeInNanos = System.nanoTime(); + } + + void printCanvas(Canvas canvas) { + swingAdapter.printToDisplay(canvas.getPrintObjects(), canvas.getBackgroundColor()); + canvas.getPrintObjects().clear(); + int elapsedNanosSinceLastPrint = (int) (System.nanoTime() - lastPrintTimeInNanos); + sleep(NANOS_PER_FRAME - elapsedNanosSinceLastPrint); + lastPrintTimeInNanos = System.nanoTime(); + } + + private void sleep(int nanos) { + try { + Thread.sleep(Math.max(0, nanos / 1_000_000)); + } catch (InterruptedException ignored) { + } + } + + void setStatusText(String statusText) { + swingAdapter.setStatusText(statusText); + } + + void setWindowIcon(String windowIcon) { + swingAdapter.setWindowIcon(windowIcon); + } + + void setTitle(String title) { + swingAdapter.setTitle(title); + } + + void closeWindow(boolean terminateEverything) { + swingAdapter.closeWindow(terminateEverything); + } + } + + private static class StartScreenWithChooseBox { + private final GameView gameView; + + private final int lineWeight; + private final int titleFontSize; + private final String title; + private final int titleHeight; + private final Color font; + private final Color frameAndTitle; + + private final String description; + private final int descriptionFontSize; + private final int yDescription; + + private final SelectionBox selectionBox; + private final int selectionBoxLineWeight; + private final int xSelectionBox; + + private final int enterBoxWidth; + private final int enterBoxHeight; + private final int ySelectionBox; + private final java.awt.Rectangle enterBox; + + private final int yLowerLine; + + private boolean startScreenClosed; + private final boolean useMouseBackup; + + + StartScreenWithChooseBox(GameView gameView, String title, String description, String selectionTitle, + String[] selectionItems, int selectedItem) { + this.gameView = gameView; + + this.lineWeight = 5; + this.title = title; + this.titleFontSize = 45; + this.titleHeight = (int) Math.rint(titleFontSize * 1.5); + this.font = Color.GRAY; + this.frameAndTitle = Color.WHITE; + + this.description = description; + this.descriptionFontSize = 16; + this.yDescription = titleHeight + 2 * lineWeight; + + int gap = 20; + int selectionFontSize = 20; + this.selectionBoxLineWeight = (int) Math.rint(selectionFontSize / 8d); + this.selectionBox = new SelectionBox(gameView, selectionTitle, selectionItems, selectedItem, + selectionFontSize, selectionBoxLineWeight, font, Color.YELLOW, + Color.BLACK, frameAndTitle); + this.xSelectionBox = gap; + this.ySelectionBox = HEIGHT - selectionBox.getHeight() - gap; + + this.enterBoxWidth = WIDTH / 3 - 2 * gap; + this.enterBoxHeight = 4 * descriptionFontSize; + int yEnterBox = HEIGHT - enterBoxHeight - gap; + this.enterBox = new java.awt.Rectangle(WIDTH - enterBoxWidth - gap, yEnterBox, enterBoxWidth, + enterBoxHeight); + + this.yLowerLine = Math.min(ySelectionBox - gap, yEnterBox - gap); + this.startScreenClosed = false; + useMouseBackup = gameView.isMouseEnabled(); + gameView.useMouse(true); + } + + + void printStartScreen() { + while (!startScreenClosed) { + checkUserInput(); + addRectangles(); + addTitle(); + addDescription(); + selectionBox.addSelectionBox(xSelectionBox, ySelectionBox); + addEnterField(); + gameView.printCanvas(); + } + gameView.useMouse(useMouseBackup); + } + + private void checkUserInput() { + // Tastendruck + KeyEvent[] keyEvents = gameView.pollKeyEvents(); + for (KeyEvent keyEvent : keyEvents) { + if (keyEvent.getID() == KeyEvent.KEY_PRESSED) { + if (keyEvent.getKeyCode() == KeyEvent.VK_UP) { + selectionBox.up(); + } else if (keyEvent.getKeyCode() == KeyEvent.VK_DOWN) { + selectionBox.down(); + } else if (keyEvent.getKeyCode() == KeyEvent.VK_ENTER) { + startScreenClosed = true; + } + } + } + // Mausklick + MouseEvent[] mouseEvents = gameView.pollMouseEvents(); + for (MouseEvent mouseEvent : mouseEvents) { + if (mouseEvent.getID() == MouseEvent.MOUSE_PRESSED) { + selectionBox.processMouseClick(mouseEvent.getX(), mouseEvent.getY()); + if (enterBox.contains(mouseEvent.getX(), mouseEvent.getY())) { + startScreenClosed = true; + } + } + } + } + + private void addRectangles() { + gameView.addRectangleToCanvas(lineWeight / 2d, lineWeight / 2d, WIDTH - 1 - lineWeight, + HEIGHT - 1 - lineWeight, lineWeight, false, font); + gameView.addRectangleToCanvas(lineWeight / 2d, lineWeight / 2d, WIDTH - 1 - lineWeight, + titleHeight - lineWeight, lineWeight, false, font); + gameView.addRectangleToCanvas(lineWeight / 2d, yLowerLine + lineWeight / 2d, WIDTH - 1 - lineWeight, + HEIGHT - yLowerLine - lineWeight, lineWeight, false, font); + } + + private void addTitle() { + gameView.addTextToCanvas(title, (WIDTH - title.length() * titleFontSize) / 2d, + ((int) (titleFontSize * 1.5) - titleFontSize) / 2d, titleFontSize, frameAndTitle + , 0); + } + + private void addDescription() { + gameView.addTextToCanvas(description, 2 * lineWeight, yDescription, descriptionFontSize, font, 0); + } + + private void addEnterField() { + gameView.addRectangleToCanvas(enterBox.x, enterBox.y, enterBox.width, enterBox.height, + selectionBoxLineWeight, false, frameAndTitle); + int gap = 2 * selectionBoxLineWeight; + gameView.addRectangleToCanvas(enterBox.x + gap, enterBox.y + gap, enterBox.width - 2 * gap, + enterBox.height - 2 * gap, selectionBoxLineWeight, false, frameAndTitle); + String text = "Press ENTER or\n" + "click to start"; + int titleWidth = 14 * descriptionFontSize; + int titleHeight = 2 * descriptionFontSize; + gameView.addTextToCanvas(text, enterBox.x + (enterBoxWidth - titleWidth) / 2d, + enterBox.y + (enterBoxHeight - titleHeight) / 2d, descriptionFontSize, + frameAndTitle, 0); + } + + int getSelectedItem() { + return selectionBox.getSelectedItem(); + } + + private static class SelectionBox { + private final GameView gameView; + private final String title; + private final String[] items; + private final int fontSize; + private int selectedItem; + private final Color markerFont; + private final Color markerHighlight; + private final Color markerRectangle; + private final Color frameAndTitle; + + private final int lineWeight; + private final int titleHeight; + private final int heightOfMarkerField; + private final int heightOfMarkerBox; + private final int height; + private int widthOfMarkerField; + private final int width; + + private final java.awt.Rectangle[] markerBounds; + private final java.awt.Rectangle upBounds; + private final java.awt.Rectangle downBounds; + + private int x; + private int xLine; + private int y; + private int yMarkerBox; + + + private SelectionBox(GameView gameView, String title, String[] items, int selectedItem, int fontSize, + int lineWeight, Color markerFont, Color markerHighlight, Color markerRectangle, + Color frameAndTitle) { + this.gameView = gameView; + this.title = title; + this.items = items; + this.fontSize = fontSize; + this.selectedItem = selectedItem; + this.markerFont = markerFont; + this.markerHighlight = markerHighlight; + this.markerRectangle = markerRectangle; + this.frameAndTitle = frameAndTitle; + + this.lineWeight = lineWeight; + this.titleHeight = (int) Math.rint(fontSize * 1.6); + this.heightOfMarkerField = (int) Math.rint(fontSize * 1.25); + this.heightOfMarkerBox = items.length * heightOfMarkerField + 2 * lineWeight; + this.height = titleHeight + heightOfMarkerBox - lineWeight; + calculateWidthOfMarkerField(); + this.width = widthOfMarkerField + 6 * lineWeight; + + this.markerBounds = new java.awt.Rectangle[items.length]; + for (int i = 0; i < items.length; i++) { + markerBounds[i] = new java.awt.Rectangle(0, 0, widthOfMarkerField, heightOfMarkerField); + } + this.upBounds = new java.awt.Rectangle(0, 0, 5 * lineWeight, markerBounds[0].height); + this.downBounds = new java.awt.Rectangle(0, 0, 5 * lineWeight, markerBounds[items.length - 1].height); + } + + private void calculateWidthOfMarkerField() { + int letters = title.strip().length(); + for (String name : items) { + if (name.strip().length() > letters) { + letters = name.strip().length(); + } + } + this.widthOfMarkerField = letters * fontSize + 2 * lineWeight; + } + + void addSelectionBox(int x, int y) { + this.x = x + lineWeight / 2; + this.xLine = x + lineWeight; + this.y = y + lineWeight / 2; + this.yMarkerBox = y + titleHeight - lineWeight + lineWeight / 2; + addTitleBox(); + addMarkerFields(); + addNavigationBox(); + } + + private void addTitleBox() { + gameView.addRectangleToCanvas(x, y, width - lineWeight, titleHeight - lineWeight, lineWeight, false, + frameAndTitle); + gameView.addTextToCanvas(title, xLine + (widthOfMarkerField - title.length() * fontSize) / 2d, + y + (titleHeight - fontSize) / 2d, fontSize, frameAndTitle, 0); + } + + private void addMarkerFields() { + gameView.addRectangleToCanvas(x, yMarkerBox, widthOfMarkerField + 2 * lineWeight - lineWeight, + heightOfMarkerBox - lineWeight, lineWeight, false, frameAndTitle); + int yMarkerField = yMarkerBox + lineWeight / 2; + for (int i = 0; i < items.length; i++) { + boolean isSelected = (i == selectedItem); + markerBounds[i].x = xLine; + markerBounds[i].y = yMarkerField + i * heightOfMarkerField; + addMarkerField(markerBounds[i], items[i], isSelected); + } + upBounds.x = markerBounds[0].x + markerBounds[0].width; + upBounds.y = markerBounds[0].y; + downBounds.x = markerBounds[items.length - 1].x + markerBounds[items.length - 1].width; + downBounds.y = markerBounds[items.length - 1].y; + } + + private void addMarkerField(java.awt.Rectangle bounds, String name, boolean isMarked) { + if (isMarked) { + gameView.addRectangleToCanvas(bounds.x + lineWeight / 2d, bounds.y + lineWeight / 2d, + bounds.width - lineWeight, bounds.height - lineWeight, lineWeight, + true, markerHighlight); + gameView.addRectangleToCanvas(bounds.x + lineWeight / 2d, bounds.y + lineWeight / 2d, + bounds.width - lineWeight, bounds.height - lineWeight, 1, false, + markerRectangle); + } else { + gameView.addRectangleToCanvas(bounds.x, bounds.y, bounds.width, bounds.height, 1, false, + markerFont); + } + gameView.addTextToCanvas(name, bounds.x + lineWeight, bounds.y + (bounds.height - fontSize) / 2d, + fontSize, markerFont, 0); + } + + private void addNavigationBox() { + int xUpDown = x + widthOfMarkerField + lineWeight; + int yUp = yMarkerBox + lineWeight + fontSize / 2 + 2 * lineWeight; + int yDown = yMarkerBox + items.length * heightOfMarkerField - fontSize / 2 - 2 * lineWeight; + gameView.addRectangleToCanvas(xUpDown, yMarkerBox, 4 * lineWeight, + items.length * heightOfMarkerField + lineWeight, lineWeight, false, + frameAndTitle); + gameView.addPolygonToCanvas(new double[]{xUpDown + lineWeight, xUpDown + 3 * lineWeight, + xUpDown + 2 * lineWeight}, new double[]{yUp, yUp, yUp - fontSize / 2d}, 1, true, frameAndTitle); + gameView.addPolygonToCanvas(new double[]{xUpDown + lineWeight, xUpDown + 3 * lineWeight, + xUpDown + 2 * lineWeight}, new double[]{yDown, yDown, + yDown + fontSize / 2d}, 1, true, + frameAndTitle); + } + + void processMouseClick(int x, int y) { + for (int i = 0; i < markerBounds.length; i++) { + if (markerBounds[i].contains(x, y)) { + selectedItem = i; + } + } + if (upBounds.contains(x, y)) { + up(); + } else if (downBounds.contains(x, y)) { + down(); + } + } + + void up() { + if (selectedItem > 0) { + selectedItem--; + } + } + + void down() { + if (selectedItem < items.length - 1) { + selectedItem++; + } + } + + int getHeight() { + return height; + } + + int getSelectedItem() { + return selectedItem; + } + } + } + + private static class Screen { + protected final GameView gameView; + protected final int gap; + protected final int fontSize; + protected final boolean useMouseBackup; + protected boolean screenClosed; + protected SelectionManager selectionManager; + protected ArrayList simpleBoxes; + + protected Screen(GameView gameView, int gap, int fontSize) { + this.gameView = gameView; + this.gap = gap; + this.fontSize = fontSize; + this.useMouseBackup = gameView.isMouseEnabled(); + this.gameView.useMouse(true); + } + + protected void setSimpleBoxes(ArrayList simpleBoxes, int highLighted) { + this.simpleBoxes = simpleBoxes; + this.selectionManager = new SelectionManager(simpleBoxes, highLighted); + } + + protected void checkUserInput() { + for (KeyEvent keyEvent : gameView.pollKeyEvents()) { + if (keyEvent.getID() == KeyEvent.KEY_PRESSED) { + selectionManager.processKeyEvent(keyEvent); + if (keyEvent.getKeyCode() == KeyEvent.VK_ENTER) { + screenClosed = true; + } + } + } + for (MouseEvent mouseEvent : gameView.pollMouseEvents()) { + if (mouseEvent.getID() == MouseEvent.MOUSE_PRESSED) { + if (selectionManager.processMouseEvent(mouseEvent.getX(), mouseEvent.getY())) { + screenClosed = true; + } + } + } + } + + protected Dimension calculateBounds(String text) { + String[] lines = text.split("\\R"); + int longestLine = Arrays.stream(lines).mapToInt(String::length).max().orElse(1); + return new Dimension(longestLine, Math.max(1, lines.length)); + } + + protected int calculateFontSizeForBounds(Dimension textBounds, int height) { + return Math.min(WIDTH / textBounds.width, height / textBounds.height) - 1; + } + } + + private static class EndScreen extends Screen { + + private final String message; + + private EndScreen(GameView gameView, String message) { + super(gameView, 20, 28); + this.message = message; + int height = 40; + int width = 250; + int x = (WIDTH - 3 * width - 2 * gap) / 2; + int y = HEIGHT - height - gap; + ArrayList simpleBoxes = new ArrayList<>(3); + simpleBoxes.add(new SimpleBox("New Game", x, y, width, height, false)); + simpleBoxes.add(new SimpleBox("Close", x + 2 * width + 2 * gap, y, width, height, true)); + setSimpleBoxes(simpleBoxes, 0); + } + + void printEndScreen() { + while (!screenClosed) { + checkUserInput(); + addMessageToCanvas(); + simpleBoxes.forEach(s -> s.addToCanvas(gameView)); + gameView.printCanvas(); + } + gameView.useMouse(useMouseBackup); + } + + private void addMessageToCanvas() { + Dimension messageBounds = calculateBounds(message); + int x = (GameView.WIDTH - messageBounds.width * fontSize) / 2; + int y = (GameView.HEIGHT - messageBounds.height * fontSize - 200) / 2; + gameView.addTextToCanvas(message, x, y, fontSize, Color.WHITE, 0); + } + + boolean playAgain() { + return simpleBoxes.get(0).isHighlighted; + } + } + + + private static class SimpleStartScreen extends Screen { + private final int titleHeight; + private final String title; + private final Color titleColor; + private final String description; + + private SimpleStartScreen(GameView gameView, String title, String description) { + super(gameView, 20, 16); + this.title = title; + this.titleHeight = HEIGHT / 4; + this.titleColor = Color.RED.brighter(); + this.description = description; + int height = 40; + int width = 200; + int x = (WIDTH - 3 * width - 2 * gap) / 2; + int y = HEIGHT - height - gap; + ArrayList simpleBoxes = new ArrayList<>(3); + simpleBoxes.add(new SimpleBox("Easy", x, y, width, height, false)); + simpleBoxes.add(new SimpleBox("Standard", x + width + gap, y, width, height, false)); + simpleBoxes.add(new SimpleBox("Close", x + 2 * width + 2 * gap, y, width, height, true)); + setSimpleBoxes(simpleBoxes, 1); + } + + String getSelectedItem() { + return selectionManager.getSelectedItem().text; + } + + void printStartScreen() { + while (!screenClosed) { + checkUserInput(); + addTitle(); + gameView.addTextToCanvas(description, gap, titleHeight + gap, fontSize, Color.WHITE, 0); + simpleBoxes.forEach(s -> s.addToCanvas(gameView)); + gameView.printCanvas(); + } + gameView.useMouse(useMouseBackup); + } + + private void addTitle() { + Dimension textBounds = calculateBounds(title); + int fontSize = calculateFontSizeForBounds(textBounds, titleHeight); + gameView.addTextToCanvas(title, (WIDTH - (textBounds.width * fontSize)) / 2d, + (titleHeight - (textBounds.height * fontSize)) / 2d, fontSize, titleColor, 0); + } + } + + private static class SimpleBox extends java.awt.Rectangle { + public final String text; + public boolean isHighlighted; + public boolean isQuitBox; + private final int fontSize; + + private SimpleBox(String text, int x, int y, int width, int height, boolean isQuitBox) { + super(x, y, width, height); + this.text = text; + this.isQuitBox = isQuitBox; + this.fontSize = height / 2; + } + + void addToCanvas(GameView gameView) { + if (isHighlighted) { + gameView.addRectangleToCanvas(x, y, width, height, 3, true, Color.DARK_GRAY); + gameView.addRectangleToCanvas(x, y, width, height, 3, false, Color.YELLOW); + } else { + gameView.addRectangleToCanvas(x, y, width, height, 3, false, Color.WHITE); + } + gameView.addTextToCanvas(text, x + (width - text.length() * fontSize) / 2d, y + (height - fontSize) / 2d, + fontSize, Color.WHITE, 0); + } + } + + private static class SelectionManager { + private final ArrayList simpleBoxes; + private int highlightedBox; + + private SelectionManager(ArrayList simpleBoxes, int highlightedBox) { + this.simpleBoxes = simpleBoxes; + this.highlightedBox = highlightedBox; + this.simpleBoxes.get(highlightedBox).isHighlighted = true; + } + + public SimpleBox getSelectedItem() { + return simpleBoxes.get(highlightedBox); + } + + void processKeyEvent(KeyEvent keyEvent) { + if (keyEvent.getKeyCode() == KeyEvent.VK_RIGHT || keyEvent.getKeyCode() == KeyEvent.VK_DOWN) { + highlight(highlightedBox + 1); + } else if (keyEvent.getKeyCode() == KeyEvent.VK_LEFT || keyEvent.getKeyCode() == KeyEvent.VK_UP) { + highlight(highlightedBox - 1); + } + } + + boolean processMouseEvent(int x, int y) { + for (int i = 0; i < simpleBoxes.size(); i++) { + SimpleBox simpleBox = simpleBoxes.get(i); + if (simpleBox.contains(x, y)) { + highlight(i); + return true; + } + } + return false; + } + + private void highlight(int boxToHighlight) { + if (boxToHighlight >= 0 && boxToHighlight < simpleBoxes.size()) { + simpleBoxes.forEach(s -> s.isHighlighted = false); + simpleBoxes.get(boxToHighlight).isHighlighted = true; + highlightedBox = boxToHighlight; + } + } + } +} diff --git a/SuperPangWorld/src/superpangworld/Overlay.java b/SuperPangWorld/src/superpangworld/Overlay.java new file mode 100644 index 0000000..d0f30b9 --- /dev/null +++ b/SuperPangWorld/src/superpangworld/Overlay.java @@ -0,0 +1,2 @@ +package superpangworld;public class Overlay { +}