Compare commits

...

3 Commits

Author SHA1 Message Date
Martin Prokoph
67a12899c6 docs: add javadocs for nearly everything 2025-09-02 23:48:49 +02:00
Martin Prokoph
504018b9f8 NightJson: expand maps 2025-08-02 23:30:59 +02:00
Martin Prokoph
fcf7f2a4f1 NightJson: remove empty line before comment 2025-08-02 23:06:02 +02:00
16 changed files with 349 additions and 44 deletions

View File

@@ -9,6 +9,12 @@ public class HighScores {
private static final NightJson json = new NightJson(HighScores.class, "tetris_scores.json5"); private static final NightJson json = new NightJson(HighScores.class, "tetris_scores.json5");
public static Map<String, Integer> scores = new HashMap<>(); public static Map<String, Integer> scores = new HashMap<>();
/**
* Saves a new high score for the specified player
*
* @param playerName the player's name, duh
* @param score the score to save
*/
public static void addScore(String playerName, int score) { public static void addScore(String playerName, int score) {
scores.put(playerName, score); scores.put(playerName, score);
write(); write();

View File

@@ -19,6 +19,13 @@ public class Tetris {
private static TimerTask gravityTask; private static TimerTask gravityTask;
private static LocalTime startTime; private static LocalTime startTime;
/**
* The application's entry point.
* Initializes the UI library to match the system style.
* Also loads saved settings, translations, and highscores from JSON.
*
* @param args command line arguments will be ignored
*/
public static void main(String[] args) { public static void main(String[] args) {
try { try {
System.setProperty("java.awt.headless", "false"); System.setProperty("java.awt.headless", "false");
@@ -32,10 +39,20 @@ public class Tetris {
ui = new TetrisUI(); ui = new TetrisUI();
} }
/**
* Get the active game space
*
* @see Space
*/
public static Space getSpace() { public static Space getSpace() {
return space; return space;
} }
/**
* Resets the game space, preparing it for a new game.
*
* @see Space
*/
public static void resetSpace() { public static void resetSpace() {
SoundUtil.stopMusic("/music/theme.wav"); SoundUtil.stopMusic("/music/theme.wav");
if (gravityTask != null) gravityTask.cancel(); if (gravityTask != null) gravityTask.cancel();
@@ -43,6 +60,12 @@ public class Tetris {
space = new Space(); space = new Space();
} }
/**
* Starts a new game of Tetris :D
* This involves starting our gravity task, playing music and spawning the first {@link eu.midnightdust.yaytris.game.Tetromino}
*
* @see Space#spawnTetromino()
*/
public static void startGame() { public static void startGame() {
SoundUtil.playMusic("/music/theme.wav", true); SoundUtil.playMusic("/music/theme.wav", true);
space.spawnTetromino(); space.spawnTetromino();
@@ -51,6 +74,13 @@ public class Tetris {
timer.scheduleAtFixedRate(gravityTask, 1, Settings.difficulty.getTimerPeriod()); timer.scheduleAtFixedRate(gravityTask, 1, Settings.difficulty.getTimerPeriod());
} }
/**
* Stops the current game.
* Disables falling, fades out music and handles saving of high scores.
*
* @see ScoreMenu
* @see HighScores
*/
public static void stopGame() { public static void stopGame() {
SoundUtil.stopMusic("/music/theme.wav"); SoundUtil.stopMusic("/music/theme.wav");
if (gravityTask != null) gravityTask.cancel(); if (gravityTask != null) gravityTask.cancel();
@@ -60,13 +90,30 @@ public class Tetris {
if (HighScores.scores.values().stream().noneMatch(hs -> hs > space.getScore())) ui.showHighscoreDialog(space.getScore()); if (HighScores.scores.values().stream().noneMatch(hs -> hs > space.getScore())) ui.showHighscoreDialog(space.getScore());
} }
/**
* Updates the displayed score
*
* @param score the new score
* @see ScoreMenu
*/
public static void updateScore(int score) { public static void updateScore(int score) {
if (ui.getMenuPanel() instanceof ScoreMenu) ((ScoreMenu) ui.getMenuPanel()).updateScore(score); if (ui.getMenuPanel() instanceof ScoreMenu) ((ScoreMenu) ui.getMenuPanel()).updateScore(score);
updateLevel(score); updateLevel(score);
} }
/**
* Updates the elapsed time
*
* @see ScoreMenu
*/
public static void updateTime() { public static void updateTime() {
if (ui.getMenuPanel() instanceof ScoreMenu) ((ScoreMenu) ui.getMenuPanel()).updateTime(startTime); if (ui.getMenuPanel() instanceof ScoreMenu) ((ScoreMenu) ui.getMenuPanel()).updateTime(startTime);
} }
/**
* Updates the displayed level
*
* @param score the new score, from which the level will be calculated
* @see ScoreMenu
*/
public static void updateLevel(int score) { public static void updateLevel(int score) {
int newLevel = Math.max(0, (int) (score / 1000f)); int newLevel = Math.max(0, (int) (score / 1000f));
if (newLevel != space.level) { if (newLevel != space.level) {
@@ -80,6 +127,9 @@ public class Tetris {
} }
} }
/**
* Defines our custom timer task that handles falling pieces.
*/
public static class GravityTimerTask extends TimerTask { public static class GravityTimerTask extends TimerTask {
@Override @Override
public void run() { public void run() {

View File

@@ -8,11 +8,23 @@ import java.util.Map;
public class Translation { public class Translation {
public static Map<String, String> jsonMap = new HashMap<>(); public static Map<String, String> jsonMap = new HashMap<>();
/**
* Loads translation strings from the specified language via NightJSON.
*
* @param locale the language code (i.e. de_de, en_us)
* @see NightJson
*/
public static void load(String locale) { public static void load(String locale) {
NightJson json = new NightJson(Translation.class, String.format("translation/%s.json5", locale)); NightJson json = new NightJson(Translation.class, String.format("translation/%s.json5", locale));
json.readJson(); json.readJson();
} }
/**
* Translates the given key to the active language.
*
* @param key translation key
* @return the resolved translation in the current locale, if present. Otherwise, the key is returned.
*/
public static String t(String key, Object... args) { public static String t(String key, Object... args) {
return String.format(jsonMap.getOrDefault(key, key), args); return String.format(jsonMap.getOrDefault(key, key), args);
} }

View File

@@ -15,25 +15,40 @@ public class Space {
private int score; private int score;
public int level; public int level;
/**
* Space saves the current state of the game map
*/
public Space() { public Space() {
gameMap = new Color[14][8]; gameMap = new Color[20][12];
nextShape = generateNextShape(); nextShape = generateNextShape();
score = 0; score = 0;
} }
/**
* Spawn the queued tetromino piece and generate the next one
*/
public void spawnTetromino() { public void spawnTetromino() {
currentTetromino = new Tetromino(nextShape); currentTetromino = new Tetromino(nextShape);
nextShape = generateNextShape(); nextShape = generateNextShape();
} }
/**
* @return the currently controlled tetromino piece
*/
public Tetromino getCurrentTetromino() { public Tetromino getCurrentTetromino() {
return currentTetromino; return currentTetromino;
} }
/**
* @return a randomly selected tetromino shape
*/
public TetrominoShape generateNextShape() { public TetrominoShape generateNextShape() {
return TetrominoShape.values()[random.nextInt(TetrominoShape.values().length)]; return TetrominoShape.values()[random.nextInt(TetrominoShape.values().length)];
} }
/**
* @return the queued tetromino shape
*/
public TetrominoShape getNextShape() { public TetrominoShape getNextShape() {
return nextShape; return nextShape;
} }
@@ -46,6 +61,11 @@ public class Space {
return gameMap.length; return gameMap.length;
} }
/**
* Converts the current state of the space to an array and includes the falling tetromino.
*
* @return a representation of the space with the falling tetromino baked in
*/
public Color[][] getGameMapWithTetromino() { public Color[][] getGameMapWithTetromino() {
Color[][] tempGameMap = new Color[gameMap.length][gameMap[0].length]; Color[][] tempGameMap = new Color[gameMap.length][gameMap[0].length];
for (int y = 0; y < tempGameMap.length; y++) { for (int y = 0; y < tempGameMap.length; y++) {
@@ -61,6 +81,11 @@ public class Space {
return tempGameMap; return tempGameMap;
} }
/**
* Converts the current state of the space to an array.
*
* @return a representation of the space without the falling tetromino baked in
*/
public Color[][] getGameMap() { public Color[][] getGameMap() {
return gameMap; return gameMap;
} }
@@ -69,6 +94,11 @@ public class Space {
return score; return score;
} }
/**
* Handle line changes.
* Once a line is fully completed, it is removed from the space, the lines above are moved down and the score is increased.
* More lines completed at once increase the combo and therefore the score.
*/
public void onLinesChanged(Tetromino tetromino, int... lines) { public void onLinesChanged(Tetromino tetromino, int... lines) {
int combo = 0; int combo = 0;
Set<Integer> completedLines = new TreeSet<>(); Set<Integer> completedLines = new TreeSet<>();

View File

@@ -12,12 +12,20 @@ public class Tetromino {
private Vec2i centerPos; private Vec2i centerPos;
private int fallLength = 0; private int fallLength = 0;
/**
* This class handles the falling tetromino.
* @param shape the tetromino's shape
*/
public Tetromino(TetrominoShape shape) { public Tetromino(TetrominoShape shape) {
this.shape = shape; this.shape = shape;
this.collision = shape.boundary; this.collision = shape.boundary;
this.centerPos = Vec2i.of(Tetris.getSpace().getMapWidth()/2-1, -1); this.centerPos = Vec2i.of(Tetris.getSpace().getMapWidth()/2-1, -1);
} }
/**
* Moves the tetromino vertically.
* Upon hitting solid ground, the piece is released and {@link Space#onLinesChanged(Tetromino, int...)} is triggered.
*/
public boolean fall() { public boolean fall() {
Vec2i newPos = centerPos.offset(Vec2i.of(0, 1)); Vec2i newPos = centerPos.offset(Vec2i.of(0, 1));
if (collidesVertically(newPos)) { if (collidesVertically(newPos)) {
@@ -37,6 +45,10 @@ public class Tetromino {
return true; return true;
} }
/**
* Moves the tetromino horizontally.
* Accounts for collision and may prevent movement.
*/
public void move(int xOffset) { public void move(int xOffset) {
Vec2i newPos = centerPos.offset(Vec2i.of(xOffset, 0)); Vec2i newPos = centerPos.offset(Vec2i.of(xOffset, 0));
if (collidesHorizontally(newPos, xOffset)) { if (collidesHorizontally(newPos, xOffset)) {
@@ -45,6 +57,11 @@ public class Tetromino {
centerPos = newPos; centerPos = newPos;
} }
/**
* Checks, whether the piece would overlap with a block that's already present on the map or would be out-of-bounds.
* @param newPos the new position
* @return true if collision is detected, false otherwise
*/
private boolean collidesVertically(Vec2i newPos) { private boolean collidesVertically(Vec2i newPos) {
if (Tetris.getSpace() == null) return false; if (Tetris.getSpace() == null) return false;
@@ -62,6 +79,12 @@ public class Tetromino {
return collides; return collides;
} }
/**
* Checks, whether the piece would overlap with a block that's already present on the map or would be out-of-bounds.
* @param newPos the new position
* @param xOffset the direction of movement (-1 == left; 1 == right)
* @return true if collision is detected, false otherwise
*/
private boolean collidesHorizontally(Vec2i newPos, int xOffset) { private boolean collidesHorizontally(Vec2i newPos, int xOffset) {
if (Tetris.getSpace() == null) return false; if (Tetris.getSpace() == null) return false;
@@ -78,6 +101,10 @@ public class Tetromino {
return collides; return collides;
} }
/**
* Rotate the tetromino 90 degrees clockwise.
* Fails if the piece would collide vertically or horizontally.
*/
public void rotate() { public void rotate() {
int M = collision.length; int M = collision.length;
int N = collision[0].length; int N = collision[0].length;
@@ -98,6 +125,10 @@ public class Tetromino {
else this.centerPos = centerPos.offset(Vec2i.of(offset, 0)); else this.centerPos = centerPos.offset(Vec2i.of(offset, 0));
} }
/**
* Moves the tetromino piece to its world-view position.
* @return an array representing the line
*/
public Color[] getLine(int line) { public Color[] getLine(int line) {
Color[] l = new Color[Tetris.getSpace().getMapWidth()]; Color[] l = new Color[Tetris.getSpace().getMapWidth()];
for (int i = 0; i < l.length; i++) { for (int i = 0; i < l.length; i++) {
@@ -111,6 +142,9 @@ public class Tetromino {
return l; return l;
} }
/**
* Moves the tetromino down until it collides.
*/
public void fallToBottom() { public void fallToBottom() {
while (true) { while (true) {
if (!fall()) break; if (!fall()) break;

View File

@@ -5,39 +5,38 @@ import java.awt.Color;
public enum TetrominoShape { public enum TetrominoShape {
SQUARE(new int[][]{ SQUARE(new int[][]{
{1, 1}, {1, 1},
{1, 2} {1, 1}
}, Color.YELLOW), }, Color.YELLOW),
LINE(new int[][]{ LINE(new int[][]{
{1}, {1},
{2}, {1},
{1}, {1},
{1} {1}
}, Color.BLUE), }, Color.BLUE),
T(new int[][]{ T(new int[][]{
{0, 1, 0}, {0, 1, 0},
{1, 2, 1} {1, 1, 1}
}, Color.RED), }, Color.RED),
L_LEFT(new int[][]{ L_LEFT(new int[][]{
{0, 1}, {0, 1},
{0, 2}, {0, 1},
{1, 1} {1, 1}
}, Color.MAGENTA), }, Color.MAGENTA),
L_RIGHT(new int[][]{ L_RIGHT(new int[][]{
{1, 0}, {1, 0},
{2, 0}, {1, 0},
{1, 1} {1, 1}
}, Color.GREEN), }, Color.GREEN),
ZAP_LEFT(new int[][]{ ZAP_LEFT(new int[][]{
{0, 1}, {0, 1},
{1, 2}, {1, 1},
{1, 0} {1, 0}
}, Color.CYAN), }, Color.CYAN),
ZAP_RIGHT(new int[][]{ ZAP_RIGHT(new int[][]{
{1, 0}, {1, 0},
{2, 1}, {1, 1},
{0, 1} {0, 1}
}, Color.ORANGE); }, Color.ORANGE);
;
final int[][] boundary; final int[][] boundary;
final Color color; final Color color;

View File

@@ -16,6 +16,12 @@ public class GameCanvas extends JPanel {
this.texture = FileUtil.loadImage("/textures/tetromino.png"); this.texture = FileUtil.loadImage("/textures/tetromino.png");
} }
/**
* Draw the current state of the game space.
*
* @param graphics the swing graphics instance
*/
@Override
public void paintComponent(Graphics graphics) { public void paintComponent(Graphics graphics) {
super.paintComponent(graphics); super.paintComponent(graphics);
if (graphics == null) return; if (graphics == null) return;
@@ -32,6 +38,13 @@ public class GameCanvas extends JPanel {
} }
} }
/**
* Replace a color's alpha channel.
*
* @param color the base color
* @param alpha the opacity (0-255)
* @return the color with the specified alpha channel value
*/
public static Color withAlpha(Color color, int alpha) { public static Color withAlpha(Color color, int alpha) {
return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha);
} }

View File

@@ -1,7 +1,6 @@
package eu.midnightdust.yaytris.ui; package eu.midnightdust.yaytris.ui;
import eu.midnightdust.yaytris.Tetris; import eu.midnightdust.yaytris.Tetris;
import eu.midnightdust.yaytris.game.Tetromino;
import eu.midnightdust.yaytris.game.TetrominoShape; import eu.midnightdust.yaytris.game.TetrominoShape;
import eu.midnightdust.yaytris.util.FileUtil; import eu.midnightdust.yaytris.util.FileUtil;
@@ -9,8 +8,6 @@ import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import static eu.midnightdust.yaytris.game.TetrominoShape.T;
public class PreviewCanvas extends JPanel { public class PreviewCanvas extends JPanel {
final TetrisUI ui; final TetrisUI ui;
final BufferedImage texture; final BufferedImage texture;
@@ -20,6 +17,12 @@ public class PreviewCanvas extends JPanel {
this.texture = FileUtil.loadImage("/textures/tetromino.png"); this.texture = FileUtil.loadImage("/textures/tetromino.png");
} }
/**
* Draw a preview of the next tetromino piece.
*
* @param graphics the swing graphics instance
*/
@Override
public void paintComponent(Graphics graphics) { public void paintComponent(Graphics graphics) {
super.paintComponent(graphics); super.paintComponent(graphics);
if (graphics == null) return; if (graphics == null) return;

View File

@@ -21,6 +21,9 @@ public class TetrisUI extends JFrame implements KeyListener {
GameCanvas gamePanel; GameCanvas gamePanel;
JPanel menuPanel; JPanel menuPanel;
/**
* Initialize the main Tetris GUI based on Swing
*/
public TetrisUI() { public TetrisUI() {
this.setLayout(null); this.setLayout(null);
this.setTitle("Tetris"); this.setTitle("Tetris");
@@ -54,6 +57,9 @@ public class TetrisUI extends JFrame implements KeyListener {
this.setVisible(true); this.setVisible(true);
} }
/**
* Resize all elements to match the current GUI scale.
*/
private void rescale() { private void rescale() {
this.setSize((int) (400 * guiScale), (int) (300 * guiScale)); this.setSize((int) (400 * guiScale), (int) (300 * guiScale));
titleLabel.setBounds(scale(225), scale(7), scale(110), scale(30)); titleLabel.setBounds(scale(225), scale(7), scale(110), scale(30));
@@ -63,6 +69,11 @@ public class TetrisUI extends JFrame implements KeyListener {
} }
} }
/**
* Calculate a size/coordinate in relation to the GUI size multiplicator.
*
* @param bound the regular size
*/
public static int scale(int bound) { public static int scale(int bound) {
return (int) (bound * guiScale); return (int) (bound * guiScale);
} }
@@ -78,6 +89,11 @@ public class TetrisUI extends JFrame implements KeyListener {
return menuPanel; return menuPanel;
} }
/**
* Start the game let's go!
*
* @param actionEvent unnecessary, but allows for more elegant lambda statements :)
*/
public void startGame(ActionEvent actionEvent) { public void startGame(ActionEvent actionEvent) {
Tetris.startGame(); Tetris.startGame();
this.openScoreMenu(actionEvent); this.openScoreMenu(actionEvent);
@@ -85,6 +101,11 @@ public class TetrisUI extends JFrame implements KeyListener {
this.repaint(); this.repaint();
} }
/**
* Open the main menu.
*
* @param actionEvent unnecessary, but allows for more elegant lambda statements :)
*/
public void openMainMenu(ActionEvent actionEvent) { public void openMainMenu(ActionEvent actionEvent) {
if (this.menuPanel != null) this.remove(menuPanel); if (this.menuPanel != null) this.remove(menuPanel);
Tetris.resetSpace(); Tetris.resetSpace();
@@ -98,6 +119,11 @@ public class TetrisUI extends JFrame implements KeyListener {
this.repaint(); this.repaint();
} }
/**
* Open the settings panel.
*
* @param actionEvent unnecessary, but allows for more elegant lambda statements :)
*/
public void openSettings(ActionEvent actionEvent) { public void openSettings(ActionEvent actionEvent) {
if (this.menuPanel != null) this.remove(menuPanel); if (this.menuPanel != null) this.remove(menuPanel);
menuPanel = new SettingsMenu(scale(170), scale(40), scale(220), scale(226), this); menuPanel = new SettingsMenu(scale(170), scale(40), scale(220), scale(226), this);
@@ -107,6 +133,11 @@ public class TetrisUI extends JFrame implements KeyListener {
this.repaint(); this.repaint();
} }
/**
* Open a panel showing information about the current game session.
*
* @param actionEvent unnecessary, but allows for more elegant lambda statements :)
*/
public void openScoreMenu(ActionEvent actionEvent) { public void openScoreMenu(ActionEvent actionEvent) {
if (this.menuPanel != null) this.remove(menuPanel); if (this.menuPanel != null) this.remove(menuPanel);
menuPanel = new ScoreMenu(scale(170), scale(40), scale(220), scale(226), this); menuPanel = new ScoreMenu(scale(170), scale(40), scale(220), scale(226), this);
@@ -116,6 +147,11 @@ public class TetrisUI extends JFrame implements KeyListener {
this.repaint(); this.repaint();
} }
/**
* Open a panel showing a list of highscores.
*
* @param actionEvent unnecessary, but allows for more elegant lambda statements :)
*/
public void openHighscores(ActionEvent actionEvent) { public void openHighscores(ActionEvent actionEvent) {
if (this.menuPanel != null) this.remove(menuPanel); if (this.menuPanel != null) this.remove(menuPanel);
menuPanel = new HighScoreMenu(scale(170), scale(40), scale(220), scale(226), this); menuPanel = new HighScoreMenu(scale(170), scale(40), scale(220), scale(226), this);
@@ -125,12 +161,24 @@ public class TetrisUI extends JFrame implements KeyListener {
this.repaint(); this.repaint();
} }
/**
* Open a popup asking for the player's name to save a new highscore.
*
* @param score the new highscore
*/
public void showHighscoreDialog(int score) { public void showHighscoreDialog(int score) {
String playerName = JOptionPane.showInputDialog(null, t("dialog.highscore.action"), t("dialog.highscore.title"), JOptionPane.PLAIN_MESSAGE); String playerName = JOptionPane.showInputDialog(null, t("dialog.highscore.action"), t("dialog.highscore.title"), JOptionPane.PLAIN_MESSAGE);
HighScores.addScore(playerName, score); HighScores.addScore(playerName, score);
} }
// Source: https://stackoverflow.com/a/19746437
/**
* Centers the game window on the given screen.
* Source: <a href="https://stackoverflow.com/a/19746437">Miss Chanandler Bong & Peter Szabo on StackOverflow</a>
*
* @param window the window to center
* @param screen the screen to center it on
*/
private void setWindowPosition(JFrame window, int screen) { private void setWindowPosition(JFrame window, int screen) {
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] allDevices = env.getScreenDevices(); GraphicsDevice[] allDevices = env.getScreenDevices();
@@ -157,12 +205,15 @@ public class TetrisUI extends JFrame implements KeyListener {
window.setLocation(windowPosX, windowPosY); window.setLocation(windowPosX, windowPosY);
} }
/**
* Capture keyboard inputs during a game session.
*
* @param e the key event
* @see eu.midnightdust.yaytris.game.Space
*/
@Override @Override
public void keyPressed(KeyEvent e) { public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) { switch (e.getKeyCode()) {
case KeyEvent.VK_E:
Tetris.getSpace().spawnTetromino();
break;
case KeyEvent.VK_UP: case KeyEvent.VK_UP:
case KeyEvent.VK_W: case KeyEvent.VK_W:
Tetris.getSpace().getCurrentTetromino().rotate(); Tetris.getSpace().getCurrentTetromino().rotate();

View File

@@ -2,8 +2,8 @@ package eu.midnightdust.yaytris.util;
import java.awt.*; import java.awt.*;
/* /**
Colors based on the [Catppuccin Mocha](https://github.com/catppuccin/catppuccin) color palette * Color scheme based on the <a href="https://github.com/catppuccin/catppuccin">Catppuccin Mocha</a> color palette
*/ */
public enum CatppuccinColor { public enum CatppuccinColor {
CRUST(0x11111b), MANTLE(0x181825), BASE(0x1e1e2e), SURFACE0(0x313244); CRUST(0x11111b), MANTLE(0x181825), BASE(0x1e1e2e), SURFACE0(0x313244);

View File

@@ -11,10 +11,10 @@ import java.util.function.Function;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/* /**
NightJson v0.2 by Martin Prokoph * NightJson v0.2 by Martin Prokoph
Extremely lightweight (and incomplete) JSON library * Extremely lightweight (and incomplete) JSON library
Concept inspired by GSON * Concept inspired by GSON
*/ */
public class NightJson { public class NightJson {
private static final String KEY_PATTERN = "\"(-?[A-Za-z-_.]*)\":"; private static final String KEY_PATTERN = "\"(-?[A-Za-z-_.]*)\":";
@@ -22,6 +22,12 @@ public class NightJson {
Field jsonMap; Field jsonMap;
String fileLocation; String fileLocation;
/**
* Initialize the NightJSON json reader & writer.
*
* @param jsonClass the java class that should be linked to the json file
* @param fileLocation the location the json file is read from and written to
*/
public NightJson(Class<?> jsonClass, String fileLocation) { public NightJson(Class<?> jsonClass, String fileLocation) {
this.jsonClass = jsonClass; this.jsonClass = jsonClass;
this.fileLocation = fileLocation; this.fileLocation = fileLocation;
@@ -30,6 +36,9 @@ public class NightJson {
}); });
} }
/**
* Convert the current state of the java json class to actual json and save it to disk.
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void writeJson() { public void writeJson() {
if (fileLocation == null) return; if (fileLocation == null) return;
@@ -54,14 +63,18 @@ public class NightJson {
jsonFile.close(); jsonFile.close();
} catch (IOException | IllegalAccessException e) { } catch (IOException | IllegalAccessException e) {
System.out.println("Oh no! An Error occurred whilst writing the JSON file :("); System.out.println("Oh no! An Error occurred whilst writing the JSON file :(");
//noinspection CallToPrintStackTrace
e.printStackTrace(); e.printStackTrace();
} }
} }
/**
* Write the desired element into the file.
*/
private void writeElement(FileWriter jsonFile, Object value, Class<?> type, String name, boolean hasNext) throws IOException, IllegalAccessException { private void writeElement(FileWriter jsonFile, Object value, Class<?> type, String name, boolean hasNext) throws IOException, IllegalAccessException {
jsonFile.write("\t"); jsonFile.write("\t");
if (type == Comment.class) { if (type == Comment.class) {
jsonFile.write(String.format("\n\t// %s\n", ((Comment) value).commentString)); jsonFile.write(String.format("// %s\n", ((Comment) value).commentString));
return; return;
} }
jsonFile.write(String.format("\"%s\": ", name)); jsonFile.write(String.format("\"%s\": ", name));
@@ -69,6 +82,9 @@ public class NightJson {
jsonFile.write(hasNext ? ",\n" : "\n"); jsonFile.write(hasNext ? ",\n" : "\n");
} }
/**
* Converts the specified value object to its json representation.
*/
private String objToString(Object value, Class<?> type) { private String objToString(Object value, Class<?> type) {
if (type == Map.class) { if (type == Map.class) {
StringBuilder mapPairs = new StringBuilder(); StringBuilder mapPairs = new StringBuilder();
@@ -78,14 +94,18 @@ public class NightJson {
while (it.hasNext()) { while (it.hasNext()) {
Object key = it.next(); Object key = it.next();
Object val = map.get(key); Object val = map.get(key);
mapPairs.append("\n\t\t");
mapPairs.append(String.format("%s: %s", objToString(key, key.getClass()), objToString(val, val.getClass()))); mapPairs.append(String.format("%s: %s", objToString(key, key.getClass()), objToString(val, val.getClass())));
mapPairs.append(it.hasNext() ? "," : "}"); mapPairs.append(it.hasNext() ? "," : "\n\t}");
} }
return mapPairs.toString(); return mapPairs.toString();
} }
return String.format(type == String.class || type.isEnum() ? "\"%s\"" : "%s", value); return String.format(type == String.class || type.isEnum() ? "\"%s\"" : "%s", value);
} }
/**
* Read the json file from disk and overwrite the json class's field values.
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void readJson() { public void readJson() {
if (fileLocation == null) return; if (fileLocation == null) return;
@@ -112,10 +132,14 @@ public class NightJson {
} }
} catch (IOException | IllegalAccessException | NoSuchElementException | ClassCastException e) { } catch (IOException | IllegalAccessException | NoSuchElementException | ClassCastException e) {
System.out.println("Oh no! An Error occurred whilst reading the JSON file :("); System.out.println("Oh no! An Error occurred whilst reading the JSON file :(");
//noinspection CallToPrintStackTrace
e.printStackTrace(); e.printStackTrace();
} }
} }
/**
* Read the json file as key-value pairs and save it as a map.
*/
private Map<String, Object> jsonToMap(String jsonString, Function<String, Class<?>> keyToType) { private Map<String, Object> jsonToMap(String jsonString, Function<String, Class<?>> keyToType) {
Map<String, Object> map = new HashMap<>(); Map<String, Object> map = new HashMap<>();
Iterator<String> pairIterator = Arrays.stream(jsonString.replaceAll("(//)+.*\n", "").replaceFirst("[{]", "").split(",")).iterator(); Iterator<String> pairIterator = Arrays.stream(jsonString.replaceAll("(//)+.*\n", "").replaceFirst("[{]", "").split(",")).iterator();
@@ -131,10 +155,13 @@ public class NightJson {
return map; return map;
} }
/**
* Read the current key in the json file.
*/
private Object getValue(String s, String key, Function<String, Class<?>> keyToType, Iterator<String> pairIterator) { private Object getValue(String s, String key, Function<String, Class<?>> keyToType, Iterator<String> pairIterator) {
String val = s.split(KEY_PATTERN, 2)[1]; String val = s.split(KEY_PATTERN, 2)[1];
if (s.contains("{")) { if (s.contains("{")) { // Handle maps recursively
StringBuilder submapString = new StringBuilder(); StringBuilder submapString = new StringBuilder();
int level = charAmount(s, '{'); int level = charAmount(s, '{');
submapString.append(val); submapString.append(val);
@@ -151,18 +178,27 @@ public class NightJson {
return jsonToMap(String.valueOf(submapString), k -> field.isPresent() ? getTypeArgument(field.get(), 1) : String.class); return jsonToMap(String.valueOf(submapString), k -> field.isPresent() ? getTypeArgument(field.get(), 1) : String.class);
} }
else { else {
if (val.startsWith(" ")) val = val.substring(1); while (val.startsWith(" ")) val = val.substring(1);
val = val.replaceAll("[\"}\n]", ""); val = val.replaceAll("[\"}\n]", "");
if (val.endsWith(",")) val = val.substring(0, val.length() - 1); while (val.endsWith(",") || val.endsWith("\n") || val.endsWith("\t")) val = val.substring(0, val.length() - 1);
return stringToObj(val, keyToType.apply(key)); return stringToObj(val, keyToType.apply(key));
} }
} }
/**
* Count the amount of appearances of a char in a string.
*
* @param input the string to search in
* @param c the char to count
*/
private int charAmount(String input, char c) { private int charAmount(String input, char c) {
return (int) input.chars().filter(ch -> ch == c).count(); return (int) input.chars().filter(ch -> ch == c).count();
} }
/**
* Converts the value from a json string to the actual field type.
*/
private Object stringToObj(String value, Class<?> type) { private Object stringToObj(String value, Class<?> type) {
switch (type.getName()) { switch (type.getName()) {
case "byte": return Byte.parseByte(value); case "byte": return Byte.parseByte(value);
@@ -177,15 +213,28 @@ public class NightJson {
else return value; else return value;
} }
/**
* Gets the type arguments of typed data structures, such as lists or maps.
* @param field the associated field
* @param index the type index (relevant for maps)
*/
private static Class<?> getTypeArgument(Field field, int index) { private static Class<?> getTypeArgument(Field field, int index) {
return getPrimitiveType((Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[index]); return getPrimitiveType((Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[index]);
} }
/**
* Tries to get primitive types from non-primitives (e.g. Boolean -> boolean)
*/
public static Class<?> getPrimitiveType(Class<?> rawType) { public static Class<?> getPrimitiveType(Class<?> rawType) {
try { return (Class<?>) rawType.getField("TYPE").get(null); // Tries to get primitive types from non-primitives (e.g. Boolean -> boolean) try { return (Class<?>) rawType.getField("TYPE").get(null);
} catch (NoSuchFieldException | IllegalAccessException ignored) { return rawType; } } catch (NoSuchFieldException | IllegalAccessException ignored) { return rawType; }
} }
/**
* Get a field from a class by its name.
* @param name the field specifier
* @return an optional representation of the field
*/
private Optional<Field> getField(String name) { private Optional<Field> getField(String name) {
try { try {
return Optional.of(jsonClass.getField(name)); return Optional.of(jsonClass.getField(name));
@@ -194,8 +243,17 @@ public class NightJson {
} }
} }
/**
* Add comments to your json files.
* If you decide to use this, it's best to save with the .json5 extension, as regular json does not officially support comments.
*/
public static class Comment { public static class Comment {
final String commentString; final String commentString;
/**
* Add a comment to spice-up the json file :)
* @param commentString the string you want to write as a comment
* @param args optional formatting arguments, calls {@link String#format(String, Object...)}
*/
public Comment(String commentString, Object... args) { public Comment(String commentString, Object... args) {
this.commentString = String.format(commentString, args); this.commentString = String.format(commentString, args);
} }

View File

@@ -8,6 +8,10 @@ public enum SoundEffect {
this.location = location; this.location = location;
} }
/**
* Play this sound effect aloud
* @see SoundUtil#playSoundClip(String)
*/
public void play() { public void play() {
SoundUtil.playSoundClip(location); SoundUtil.playSoundClip(location);
} }

View File

@@ -11,6 +11,12 @@ import java.util.Map;
public class SoundUtil { public class SoundUtil {
private static final Map<String, MusicThread> musicThreads = new HashMap<>(); private static final Map<String, MusicThread> musicThreads = new HashMap<>();
/**
* Play the long audio file found at the specified location.
*
* @param fileLocation the URI of the desired audio file (should be a .wav file)
* @param looped whether to repeat the song indefinitely after it finishes
*/
public static void playMusic(String fileLocation, boolean looped) { public static void playMusic(String fileLocation, boolean looped) {
if (musicThreads.containsKey(fileLocation)) stopMusic(fileLocation); if (musicThreads.containsKey(fileLocation)) stopMusic(fileLocation);
MusicThread musicThread = new MusicThread(fileLocation, looped); MusicThread musicThread = new MusicThread(fileLocation, looped);
@@ -18,11 +24,22 @@ public class SoundUtil {
musicThreads.put(fileLocation, musicThread); musicThreads.put(fileLocation, musicThread);
} }
/**
* Stop the long audio file found at the specified location, if it is playing.
* Also has a nice smooth fade-out effect.
*
* @param fileLocation the URI of the desired audio file (should be a .wav file)
*/
public static void stopMusic(String fileLocation) { public static void stopMusic(String fileLocation) {
if (musicThreads.containsKey(fileLocation)) musicThreads.get(fileLocation).stopMusic(); if (musicThreads.containsKey(fileLocation)) musicThreads.get(fileLocation).stopMusic();
} }
// Adapted from: https://www.baeldung.com/java-play-sound /**
* Play a short audio clip found at the specified location.
* Adapted from: <a href="https://www.baeldung.com/java-play-sound">Baeldung</a>
*
* @param fileLocation the URI of the desired audio file (should be a .wav file)
*/
public static void playSoundClip(String fileLocation) { public static void playSoundClip(String fileLocation) {
try (AudioInputStream stream = AudioSystem.getAudioInputStream(getResource(fileLocation))) { // FIXME: Support audio files from JAR. File streams won't work here for some reason. try (AudioInputStream stream = AudioSystem.getAudioInputStream(getResource(fileLocation))) { // FIXME: Support audio files from JAR. File streams won't work here for some reason.
AudioFormat format = stream.getFormat(); AudioFormat format = stream.getFormat();
@@ -42,7 +59,13 @@ public class SoundUtil {
return SoundUtil.class.getResource(fileLocation); return SoundUtil.class.getResource(fileLocation);
} }
// Adapted from: https://stackoverflow.com/a/40698149 /**
* Set the volume for the audio player.
* Adapted from: <a href="https://stackoverflow.com/a/40698149">Steve Eynon on StackOverflow</a>
*
* @param line the DataLine responsible for audio playback
* @param volume the requested volume in percent (0-100%)
*/
private static void setVolume(Line line, int volume) { private static void setVolume(Line line, int volume) {
if (volume < 0 || volume > 100) if (volume < 0 || volume > 100)
throw new IllegalArgumentException("Volume not valid: " + volume); throw new IllegalArgumentException("Volume not valid: " + volume);
@@ -50,6 +73,9 @@ public class SoundUtil {
gainControl.setValue(20f * (float) Math.log10(volume/100f)); gainControl.setValue(20f * (float) Math.log10(volume/100f));
} }
/**
* Handle music in separate threads to not interrupt the main game
*/
public static class MusicThread extends Thread { public static class MusicThread extends Thread {
private static final int BUFFER_SIZE = 8192; private static final int BUFFER_SIZE = 8192;
private final boolean looped; private final boolean looped;
@@ -71,7 +97,13 @@ public class SoundUtil {
do {playMusic(fileLocation);} while (looped && playing); do {playMusic(fileLocation);} while (looped && playing);
} }
// Adapted from: https://www.baeldung.com/java-play-sound /**
* INTERNAL!
* Play the long audio file found at the specified location.
* Fades out after calling {@link #stopMusic()}.
* Adapted from: <a href="https://www.baeldung.com/java-play-sound">Baeldung</a>
* @see SoundUtil#playMusic(String, boolean)
*/
private void playMusic(String fileLocation) { private void playMusic(String fileLocation) {
try (AudioInputStream stream = AudioSystem.getAudioInputStream(getResource(fileLocation))) { try (AudioInputStream stream = AudioSystem.getAudioInputStream(getResource(fileLocation))) {
AudioFormat format = stream.getFormat(); AudioFormat format = stream.getFormat();

View File

@@ -9,22 +9,36 @@ public class Vec2i {
this.y = y; this.y = y;
} }
public static Vec2i of(int x) { /**
//noinspection SuspiciousNameCombination * Creates a vector holding two signed ints.
return new Vec2i(x, x); *
} * @param x the x value
* @param y the y value
*/
public static Vec2i of(int x, int y) { public static Vec2i of(int x, int y) {
return new Vec2i(x, y); return new Vec2i(x, y);
} }
/**
* @return the saved x value
*/
public int getX() { public int getX() {
return x; return x;
} }
/**
* @return the saved y value
*/
public int getY() { public int getY() {
return y; return y;
} }
/**
* Offset each coordinate of this vector by the same coordinate of another vector
*
* @param other the vector to add
* @return the sum of both vectors
*/
public Vec2i offset(Vec2i other) { public Vec2i offset(Vec2i other) {
return new Vec2i(x + other.getX(), y + other.getY()); return new Vec2i(x + other.getX(), y + other.getY());
} }

View File

@@ -1,3 +1,8 @@
{ {
"scores": {"Martin": 6401} "scores": {
"Marrrtin": 2648,
"Marrtin": 623,
"Martin": 147,
"Mark": 84
}
} }

View File

@@ -1,20 +1,14 @@
{ {
// Volume of theme music (0-100) // Volume of theme music (0-100)
"musicVolume": 100, "musicVolume": 30,
// Volume of sound effects (0-100) // Volume of sound effects (0-100)
"soundVolume": 100, "soundVolume": 100,
// Amount the user interface should be scaled // Amount the user interface should be scaled
"guiScale": 4.49, "guiScale": 5.7,
// Whether speed should scale with level (true/false) // Whether speed should scale with level (true/false)
"shouldScaleSpeed": true, "shouldScaleSpeed": true,
// One of [Noob, Easy, Normal, Hard, Extreme, WTF] // One of [Noob, Easy, Normal, Hard, Extreme, WTF]
"difficulty": "Normal", "difficulty": "Normal",
// One of [English, Deutsch] // One of [English, Deutsch]
"language": "Deutsch" "language": "Deutsch"
} }