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");
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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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)
"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"
}