Compare commits
3 Commits
f3c211f0a6
...
67a12899c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67a12899c6 | ||
|
|
504018b9f8 | ||
|
|
fcf7f2a4f1 |
@@ -9,6 +9,12 @@ public class HighScores {
|
||||
private static final NightJson json = new NightJson(HighScores.class, "tetris_scores.json5");
|
||||
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) {
|
||||
scores.put(playerName, score);
|
||||
write();
|
||||
|
||||
@@ -19,6 +19,13 @@ public class Tetris {
|
||||
private static TimerTask gravityTask;
|
||||
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) {
|
||||
try {
|
||||
System.setProperty("java.awt.headless", "false");
|
||||
@@ -32,10 +39,20 @@ public class Tetris {
|
||||
ui = new TetrisUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active game space
|
||||
*
|
||||
* @see Space
|
||||
*/
|
||||
public static Space getSpace() {
|
||||
return space;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the game space, preparing it for a new game.
|
||||
*
|
||||
* @see Space
|
||||
*/
|
||||
public static void resetSpace() {
|
||||
SoundUtil.stopMusic("/music/theme.wav");
|
||||
if (gravityTask != null) gravityTask.cancel();
|
||||
@@ -43,6 +60,12 @@ public class Tetris {
|
||||
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() {
|
||||
SoundUtil.playMusic("/music/theme.wav", true);
|
||||
space.spawnTetromino();
|
||||
@@ -51,6 +74,13 @@ public class Tetris {
|
||||
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() {
|
||||
SoundUtil.stopMusic("/music/theme.wav");
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the displayed score
|
||||
*
|
||||
* @param score the new score
|
||||
* @see ScoreMenu
|
||||
*/
|
||||
public static void updateScore(int score) {
|
||||
if (ui.getMenuPanel() instanceof ScoreMenu) ((ScoreMenu) ui.getMenuPanel()).updateScore(score);
|
||||
updateLevel(score);
|
||||
}
|
||||
/**
|
||||
* Updates the elapsed time
|
||||
*
|
||||
* @see ScoreMenu
|
||||
*/
|
||||
public static void updateTime() {
|
||||
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) {
|
||||
int newLevel = Math.max(0, (int) (score / 1000f));
|
||||
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 {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
@@ -8,11 +8,23 @@ import java.util.Map;
|
||||
public class Translation {
|
||||
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) {
|
||||
NightJson json = new NightJson(Translation.class, String.format("translation/%s.json5", locale));
|
||||
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) {
|
||||
return String.format(jsonMap.getOrDefault(key, key), args);
|
||||
}
|
||||
|
||||
@@ -15,25 +15,40 @@ public class Space {
|
||||
private int score;
|
||||
public int level;
|
||||
|
||||
/**
|
||||
* Space saves the current state of the game map
|
||||
*/
|
||||
public Space() {
|
||||
gameMap = new Color[14][8];
|
||||
gameMap = new Color[20][12];
|
||||
nextShape = generateNextShape();
|
||||
score = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn the queued tetromino piece and generate the next one
|
||||
*/
|
||||
public void spawnTetromino() {
|
||||
currentTetromino = new Tetromino(nextShape);
|
||||
nextShape = generateNextShape();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the currently controlled tetromino piece
|
||||
*/
|
||||
public Tetromino getCurrentTetromino() {
|
||||
return currentTetromino;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a randomly selected tetromino shape
|
||||
*/
|
||||
public TetrominoShape generateNextShape() {
|
||||
return TetrominoShape.values()[random.nextInt(TetrominoShape.values().length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the queued tetromino shape
|
||||
*/
|
||||
public TetrominoShape getNextShape() {
|
||||
return nextShape;
|
||||
}
|
||||
@@ -46,6 +61,11 @@ public class Space {
|
||||
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() {
|
||||
Color[][] tempGameMap = new Color[gameMap.length][gameMap[0].length];
|
||||
for (int y = 0; y < tempGameMap.length; y++) {
|
||||
@@ -61,6 +81,11 @@ public class Space {
|
||||
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() {
|
||||
return gameMap;
|
||||
}
|
||||
@@ -69,6 +94,11 @@ public class Space {
|
||||
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) {
|
||||
int combo = 0;
|
||||
Set<Integer> completedLines = new TreeSet<>();
|
||||
|
||||
@@ -12,12 +12,20 @@ public class Tetromino {
|
||||
private Vec2i centerPos;
|
||||
private int fallLength = 0;
|
||||
|
||||
/**
|
||||
* This class handles the falling tetromino.
|
||||
* @param shape the tetromino's shape
|
||||
*/
|
||||
public Tetromino(TetrominoShape shape) {
|
||||
this.shape = shape;
|
||||
this.collision = shape.boundary;
|
||||
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() {
|
||||
Vec2i newPos = centerPos.offset(Vec2i.of(0, 1));
|
||||
if (collidesVertically(newPos)) {
|
||||
@@ -37,6 +45,10 @@ public class Tetromino {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the tetromino horizontally.
|
||||
* Accounts for collision and may prevent movement.
|
||||
*/
|
||||
public void move(int xOffset) {
|
||||
Vec2i newPos = centerPos.offset(Vec2i.of(xOffset, 0));
|
||||
if (collidesHorizontally(newPos, xOffset)) {
|
||||
@@ -45,6 +57,11 @@ public class Tetromino {
|
||||
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) {
|
||||
if (Tetris.getSpace() == null) return false;
|
||||
|
||||
@@ -62,6 +79,12 @@ public class Tetromino {
|
||||
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) {
|
||||
if (Tetris.getSpace() == null) return false;
|
||||
|
||||
@@ -78,6 +101,10 @@ public class Tetromino {
|
||||
return collides;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate the tetromino 90 degrees clockwise.
|
||||
* Fails if the piece would collide vertically or horizontally.
|
||||
*/
|
||||
public void rotate() {
|
||||
int M = collision.length;
|
||||
int N = collision[0].length;
|
||||
@@ -98,6 +125,10 @@ public class Tetromino {
|
||||
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) {
|
||||
Color[] l = new Color[Tetris.getSpace().getMapWidth()];
|
||||
for (int i = 0; i < l.length; i++) {
|
||||
@@ -111,6 +142,9 @@ public class Tetromino {
|
||||
return l;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the tetromino down until it collides.
|
||||
*/
|
||||
public void fallToBottom() {
|
||||
while (true) {
|
||||
if (!fall()) break;
|
||||
|
||||
@@ -5,39 +5,38 @@ import java.awt.Color;
|
||||
public enum TetrominoShape {
|
||||
SQUARE(new int[][]{
|
||||
{1, 1},
|
||||
{1, 2}
|
||||
{1, 1}
|
||||
}, Color.YELLOW),
|
||||
LINE(new int[][]{
|
||||
{1},
|
||||
{2},
|
||||
{1},
|
||||
{1},
|
||||
{1}
|
||||
}, Color.BLUE),
|
||||
T(new int[][]{
|
||||
{0, 1, 0},
|
||||
{1, 2, 1}
|
||||
{1, 1, 1}
|
||||
}, Color.RED),
|
||||
L_LEFT(new int[][]{
|
||||
{0, 1},
|
||||
{0, 2},
|
||||
{0, 1},
|
||||
{1, 1}
|
||||
}, Color.MAGENTA),
|
||||
L_RIGHT(new int[][]{
|
||||
{1, 0},
|
||||
{2, 0},
|
||||
{1, 0},
|
||||
{1, 1}
|
||||
}, Color.GREEN),
|
||||
ZAP_LEFT(new int[][]{
|
||||
{0, 1},
|
||||
{1, 2},
|
||||
{1, 1},
|
||||
{1, 0}
|
||||
}, Color.CYAN),
|
||||
ZAP_RIGHT(new int[][]{
|
||||
{1, 0},
|
||||
{2, 1},
|
||||
{1, 1},
|
||||
{0, 1}
|
||||
}, Color.ORANGE);
|
||||
;
|
||||
|
||||
final int[][] boundary;
|
||||
final Color color;
|
||||
|
||||
@@ -16,6 +16,12 @@ public class GameCanvas extends JPanel {
|
||||
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) {
|
||||
super.paintComponent(graphics);
|
||||
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) {
|
||||
return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eu.midnightdust.yaytris.ui;
|
||||
|
||||
import eu.midnightdust.yaytris.Tetris;
|
||||
import eu.midnightdust.yaytris.game.Tetromino;
|
||||
import eu.midnightdust.yaytris.game.TetrominoShape;
|
||||
import eu.midnightdust.yaytris.util.FileUtil;
|
||||
|
||||
@@ -9,8 +8,6 @@ import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import static eu.midnightdust.yaytris.game.TetrominoShape.T;
|
||||
|
||||
public class PreviewCanvas extends JPanel {
|
||||
final TetrisUI ui;
|
||||
final BufferedImage texture;
|
||||
@@ -20,6 +17,12 @@ public class PreviewCanvas extends JPanel {
|
||||
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) {
|
||||
super.paintComponent(graphics);
|
||||
if (graphics == null) return;
|
||||
|
||||
@@ -21,6 +21,9 @@ public class TetrisUI extends JFrame implements KeyListener {
|
||||
GameCanvas gamePanel;
|
||||
JPanel menuPanel;
|
||||
|
||||
/**
|
||||
* Initialize the main Tetris GUI based on Swing
|
||||
*/
|
||||
public TetrisUI() {
|
||||
this.setLayout(null);
|
||||
this.setTitle("Tetris");
|
||||
@@ -54,6 +57,9 @@ public class TetrisUI extends JFrame implements KeyListener {
|
||||
this.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize all elements to match the current GUI scale.
|
||||
*/
|
||||
private void rescale() {
|
||||
this.setSize((int) (400 * guiScale), (int) (300 * guiScale));
|
||||
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) {
|
||||
return (int) (bound * guiScale);
|
||||
}
|
||||
@@ -78,6 +89,11 @@ public class TetrisUI extends JFrame implements KeyListener {
|
||||
return menuPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the game – let's go!
|
||||
*
|
||||
* @param actionEvent unnecessary, but allows for more elegant lambda statements :)
|
||||
*/
|
||||
public void startGame(ActionEvent actionEvent) {
|
||||
Tetris.startGame();
|
||||
this.openScoreMenu(actionEvent);
|
||||
@@ -85,6 +101,11 @@ public class TetrisUI extends JFrame implements KeyListener {
|
||||
this.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the main menu.
|
||||
*
|
||||
* @param actionEvent unnecessary, but allows for more elegant lambda statements :)
|
||||
*/
|
||||
public void openMainMenu(ActionEvent actionEvent) {
|
||||
if (this.menuPanel != null) this.remove(menuPanel);
|
||||
Tetris.resetSpace();
|
||||
@@ -98,6 +119,11 @@ public class TetrisUI extends JFrame implements KeyListener {
|
||||
this.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the settings panel.
|
||||
*
|
||||
* @param actionEvent unnecessary, but allows for more elegant lambda statements :)
|
||||
*/
|
||||
public void openSettings(ActionEvent actionEvent) {
|
||||
if (this.menuPanel != null) this.remove(menuPanel);
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (this.menuPanel != null) this.remove(menuPanel);
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a panel showing a list of highscores.
|
||||
*
|
||||
* @param actionEvent unnecessary, but allows for more elegant lambda statements :)
|
||||
*/
|
||||
public void openHighscores(ActionEvent actionEvent) {
|
||||
if (this.menuPanel != null) this.remove(menuPanel);
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a popup asking for the player's name to save a new highscore.
|
||||
*
|
||||
* @param score the new highscore
|
||||
*/
|
||||
public void showHighscoreDialog(int score) {
|
||||
String playerName = JOptionPane.showInputDialog(null, t("dialog.highscore.action"), t("dialog.highscore.title"), JOptionPane.PLAIN_MESSAGE);
|
||||
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) {
|
||||
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
|
||||
GraphicsDevice[] allDevices = env.getScreenDevices();
|
||||
@@ -157,12 +205,15 @@ public class TetrisUI extends JFrame implements KeyListener {
|
||||
window.setLocation(windowPosX, windowPosY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture keyboard inputs during a game session.
|
||||
*
|
||||
* @param e the key event
|
||||
* @see eu.midnightdust.yaytris.game.Space
|
||||
*/
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
switch (e.getKeyCode()) {
|
||||
case KeyEvent.VK_E:
|
||||
Tetris.getSpace().spawnTetromino();
|
||||
break;
|
||||
case KeyEvent.VK_UP:
|
||||
case KeyEvent.VK_W:
|
||||
Tetris.getSpace().getCurrentTetromino().rotate();
|
||||
|
||||
@@ -2,8 +2,8 @@ package eu.midnightdust.yaytris.util;
|
||||
|
||||
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 {
|
||||
CRUST(0x11111b), MANTLE(0x181825), BASE(0x1e1e2e), SURFACE0(0x313244);
|
||||
|
||||
@@ -11,10 +11,10 @@ import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/*
|
||||
NightJson v0.2 by Martin Prokoph
|
||||
Extremely lightweight (and incomplete) JSON library
|
||||
Concept inspired by GSON
|
||||
/**
|
||||
* NightJson v0.2 by Martin Prokoph
|
||||
* Extremely lightweight (and incomplete) JSON library
|
||||
* Concept inspired by GSON
|
||||
*/
|
||||
public class NightJson {
|
||||
private static final String KEY_PATTERN = "\"(-?[A-Za-z-_.]*)\":";
|
||||
@@ -22,6 +22,12 @@ public class NightJson {
|
||||
Field jsonMap;
|
||||
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) {
|
||||
this.jsonClass = jsonClass;
|
||||
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")
|
||||
public void writeJson() {
|
||||
if (fileLocation == null) return;
|
||||
@@ -54,14 +63,18 @@ public class NightJson {
|
||||
jsonFile.close();
|
||||
} catch (IOException | IllegalAccessException e) {
|
||||
System.out.println("Oh no! An Error occurred whilst writing the JSON file :(");
|
||||
//noinspection CallToPrintStackTrace
|
||||
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 {
|
||||
jsonFile.write("\t");
|
||||
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;
|
||||
}
|
||||
jsonFile.write(String.format("\"%s\": ", name));
|
||||
@@ -69,6 +82,9 @@ public class NightJson {
|
||||
jsonFile.write(hasNext ? ",\n" : "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the specified value object to its json representation.
|
||||
*/
|
||||
private String objToString(Object value, Class<?> type) {
|
||||
if (type == Map.class) {
|
||||
StringBuilder mapPairs = new StringBuilder();
|
||||
@@ -78,14 +94,18 @@ public class NightJson {
|
||||
while (it.hasNext()) {
|
||||
Object key = it.next();
|
||||
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(it.hasNext() ? "," : "}");
|
||||
mapPairs.append(it.hasNext() ? "," : "\n\t}");
|
||||
}
|
||||
return mapPairs.toString();
|
||||
}
|
||||
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")
|
||||
public void readJson() {
|
||||
if (fileLocation == null) return;
|
||||
@@ -112,10 +132,14 @@ public class NightJson {
|
||||
}
|
||||
} catch (IOException | IllegalAccessException | NoSuchElementException | ClassCastException e) {
|
||||
System.out.println("Oh no! An Error occurred whilst reading the JSON file :(");
|
||||
//noinspection CallToPrintStackTrace
|
||||
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) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
Iterator<String> pairIterator = Arrays.stream(jsonString.replaceAll("(//)+.*\n", "").replaceFirst("[{]", "").split(",")).iterator();
|
||||
@@ -131,10 +155,13 @@ public class NightJson {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current key in the json file.
|
||||
*/
|
||||
private Object getValue(String s, String key, Function<String, Class<?>> keyToType, Iterator<String> pairIterator) {
|
||||
String val = s.split(KEY_PATTERN, 2)[1];
|
||||
|
||||
if (s.contains("{")) {
|
||||
if (s.contains("{")) { // Handle maps recursively
|
||||
StringBuilder submapString = new StringBuilder();
|
||||
int level = charAmount(s, '{');
|
||||
submapString.append(val);
|
||||
@@ -151,18 +178,27 @@ public class NightJson {
|
||||
return jsonToMap(String.valueOf(submapString), k -> field.isPresent() ? getTypeArgument(field.get(), 1) : String.class);
|
||||
}
|
||||
else {
|
||||
if (val.startsWith(" ")) val = val.substring(1);
|
||||
while (val.startsWith(" ")) val = val.substring(1);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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) {
|
||||
switch (type.getName()) {
|
||||
case "byte": return Byte.parseByte(value);
|
||||
@@ -177,15 +213,28 @@ public class NightJson {
|
||||
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) {
|
||||
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) {
|
||||
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; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
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 {
|
||||
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) {
|
||||
this.commentString = String.format(commentString, args);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ public enum SoundEffect {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play this sound effect aloud
|
||||
* @see SoundUtil#playSoundClip(String)
|
||||
*/
|
||||
public void play() {
|
||||
SoundUtil.playSoundClip(location);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@ import java.util.Map;
|
||||
public class SoundUtil {
|
||||
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) {
|
||||
if (musicThreads.containsKey(fileLocation)) stopMusic(fileLocation);
|
||||
MusicThread musicThread = new MusicThread(fileLocation, looped);
|
||||
@@ -18,11 +24,22 @@ public class SoundUtil {
|
||||
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) {
|
||||
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) {
|
||||
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();
|
||||
@@ -42,7 +59,13 @@ public class SoundUtil {
|
||||
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) {
|
||||
if (volume < 0 || volume > 100)
|
||||
throw new IllegalArgumentException("Volume not valid: " + volume);
|
||||
@@ -50,6 +73,9 @@ public class SoundUtil {
|
||||
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 {
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
private final boolean looped;
|
||||
@@ -71,7 +97,13 @@ public class SoundUtil {
|
||||
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) {
|
||||
try (AudioInputStream stream = AudioSystem.getAudioInputStream(getResource(fileLocation))) {
|
||||
AudioFormat format = stream.getFormat();
|
||||
|
||||
@@ -9,22 +9,36 @@ public class Vec2i {
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public static Vec2i of(int x) {
|
||||
//noinspection SuspiciousNameCombination
|
||||
return new Vec2i(x, x);
|
||||
}
|
||||
/**
|
||||
* Creates a vector holding two signed ints.
|
||||
*
|
||||
* @param x the x value
|
||||
* @param y the y value
|
||||
*/
|
||||
public static Vec2i of(int x, int y) {
|
||||
return new Vec2i(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the saved x value
|
||||
*/
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the saved y value
|
||||
*/
|
||||
public int getY() {
|
||||
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) {
|
||||
return new Vec2i(x + other.getX(), y + other.getY());
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
{
|
||||
"scores": {"Martin": 6401}
|
||||
"scores": {
|
||||
"Marrrtin": 2648,
|
||||
"Marrtin": 623,
|
||||
"Martin": 147,
|
||||
"Mark": 84
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,14 @@
|
||||
{
|
||||
|
||||
// Volume of theme music (0-100)
|
||||
"musicVolume": 100,
|
||||
|
||||
"musicVolume": 30,
|
||||
// Volume of sound effects (0-100)
|
||||
"soundVolume": 100,
|
||||
|
||||
// Amount the user interface should be scaled
|
||||
"guiScale": 4.49,
|
||||
|
||||
"guiScale": 5.7,
|
||||
// Whether speed should scale with level (true/false)
|
||||
"shouldScaleSpeed": true,
|
||||
|
||||
// One of [Noob, Easy, Normal, Hard, Extreme, WTF]
|
||||
"difficulty": "Normal",
|
||||
|
||||
// One of [English, Deutsch]
|
||||
"language": "Deutsch"
|
||||
}
|
||||
Reference in New Issue
Block a user