Compare commits

...

2 Commits

Author SHA1 Message Date
Martin Prokoph
dfba8b397e feat: add translation support 2025-08-02 11:15:53 +02:00
Martin Prokoph
ec1841a592 ui: improve highscore list style 2025-08-01 20:37:32 +02:00
14 changed files with 178 additions and 43 deletions

View File

@@ -1,6 +1,7 @@
package eu.midnightdust.yaytris;
import eu.midnightdust.yaytris.util.Difficulty;
import eu.midnightdust.yaytris.util.Language;
import eu.midnightdust.yaytris.util.NightJson;
public class Settings {
@@ -11,6 +12,7 @@ public class Settings {
public static float guiScale = 3.f;
public static boolean shouldScaleSpeed = true;
public static Difficulty difficulty = Difficulty.NORMAL;
public static Language language = Language.ENGLISH;
public static void load() {
json.readJson();

View File

@@ -25,6 +25,7 @@ public class Tetris {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception | Error e) { System.out.printf("%s: %s\n", "Error setting system look and feel", e); }
Settings.load();
Translation.load(Settings.language.locale);
HighScores.load();
timer = new Timer("Tetris falling pieces");
space = new Space();

View File

@@ -0,0 +1,19 @@
package eu.midnightdust.yaytris;
import eu.midnightdust.yaytris.util.NightJson;
import java.util.HashMap;
import java.util.Map;
public class Translation {
public static Map<String, String> translationMap = new HashMap<>();
public static void load(String locale) {
NightJson json = new NightJson(translationMap, String.format("translation/%s.json5", locale));
json.readJson();
}
public static String t(String key, Object... args) {
return String.format(translationMap.getOrDefault(key, key), args);
}
}

View File

@@ -1,12 +1,15 @@
package eu.midnightdust.yaytris.ui;
import eu.midnightdust.yaytris.HighScores;
import eu.midnightdust.yaytris.util.CatppuccinColor;
import javax.swing.*;
import javax.swing.border.LineBorder;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import static eu.midnightdust.yaytris.Translation.t;
import static eu.midnightdust.yaytris.ui.TetrisUI.scale;
public class HighScoreMenu extends JPanel {
@@ -17,7 +20,7 @@ public class HighScoreMenu extends JPanel {
this.setBounds(x, y, width, height);
this.setLayout(null);
this.add(new JLabel("Highscores:"));
this.add(new JLabel(t("highscores.title")));
List<String> highscores = new ArrayList<>();
for (String key : HighScores.scores.keySet()) {
@@ -25,11 +28,14 @@ public class HighScoreMenu extends JPanel {
}
highscores.sort((s1, s2) -> Integer.compare(Integer.parseInt(s2.split("")[0].replace(" ", "")), Integer.parseInt(s1.split("")[0].replace(" ", ""))));
JList<String> highscoreList = new JList<>(highscores.toArray(String[]::new));
highscoreList.setBackground(CatppuccinColor.BASE.getColor());
highscoreList.setSelectionForeground(CatppuccinColor.CRUST.getColor());
JScrollPane highscoreScrollPane = new JScrollPane(highscoreList);
highscoreScrollPane.setBorder(new LineBorder(CatppuccinColor.SURFACE0.getColor(), 3, true));
this.add(highscoreScrollPane);
highscoreScrollPane.setBounds(scale(60), scale(43), scale(100), scale(80));
JButton backButton = new JButton("Zurück");
JButton backButton = new JButton(t("ui.back"));
backButton.addActionListener(ui::openMainMenu);
backButton.setBounds(scale(60), scale(140), scale(100), scale(20));
this.add(backButton);

View File

@@ -2,6 +2,8 @@ package eu.midnightdust.yaytris.ui;
import javax.swing.*;
import static eu.midnightdust.yaytris.Translation.t;
public class MainMenu extends AbstractMenu {
final TetrisUI ui;
@@ -10,19 +12,19 @@ public class MainMenu extends AbstractMenu {
this.setBounds(x, y, width, height);
this.setLayout(null);
JButton startButton = new JButton("Start");
JButton startButton = new JButton(t("menu.start"));
startButton.addActionListener(ui::startGame);
this.add(startButton);
JButton settingsButton = new JButton("Einstellungen");
JButton settingsButton = new JButton(t("menu.settings"));
settingsButton.addActionListener(ui::openSettings);
this.add(settingsButton);
JButton highscoreButton = new JButton("Highscores");
JButton highscoreButton = new JButton(t("menu.highscores"));
highscoreButton.addActionListener(ui::openHighscores);
this.add(highscoreButton);
JButton leaveButton = new JButton("Spiel verlassen");
JButton leaveButton = new JButton(t("menu.exit"));
leaveButton.addActionListener(e -> System.exit(0));
this.add(leaveButton);
}

View File

@@ -8,6 +8,7 @@ import java.awt.*;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static eu.midnightdust.yaytris.Translation.t;
import static eu.midnightdust.yaytris.ui.TetrisUI.scale;
public class ScoreMenu extends AbstractMenu {
@@ -32,22 +33,22 @@ public class ScoreMenu extends AbstractMenu {
this.gameOverLabel = new JLabel();
this.add(gameOverLabel);
this.currentScoreLabel = new JLabel("Score: 0");
this.currentScoreLabel = new JLabel(t("game.score", "0"));
this.add(currentScoreLabel);
this.currentLevelLabel = new JLabel("Level: 0");
this.currentLevelLabel = new JLabel(t("game.level", "0"));
this.add(currentLevelLabel);
this.currentTimeLabel = new JLabel("Zeit: 00:00");
this.currentTimeLabel = new JLabel(t("game.time", "00:00"));
this.add(currentTimeLabel);
JButton backButton = new JButton("Zurück");
JButton backButton = new JButton(t("ui.back"));
backButton.addActionListener(ui::openMainMenu);
this.add(backButton);
}
public void updateScore(int score) {
this.currentScoreLabel.setText(String.format("Score: %s", score));
this.currentScoreLabel.setText(t("game.score", score));
this.repaint();
}
@@ -55,16 +56,16 @@ public class ScoreMenu extends AbstractMenu {
LocalTime timeElapsed = LocalTime.ofNanoOfDay(LocalTime.now().toNanoOfDay() - startingTime.toNanoOfDay());
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern(timeElapsed.getHour() > 0 ? "HH:mm:ss" : "mm:ss");
this.currentTimeLabel.setText(String.format("Zeit: %s", timeFormatter.format(timeElapsed)));
this.currentTimeLabel.setText(t("game.time", timeFormatter.format(timeElapsed)));
this.repaint();
}
public void gameOver() {
this.gameOverLabel.setText("Game over :(");
this.gameOverLabel.setText(t("game.over"));
}
public void updateLevel(int level) {
this.currentLevelLabel.setText(String.format("Level: %s", level));
this.currentLevelLabel.setText(t("game.level", level));
this.repaint();
}

View File

@@ -1,11 +1,14 @@
package eu.midnightdust.yaytris.ui;
import eu.midnightdust.yaytris.Settings;
import eu.midnightdust.yaytris.Translation;
import eu.midnightdust.yaytris.util.Difficulty;
import eu.midnightdust.yaytris.util.Language;
import javax.swing.*;
import java.awt.*;
import static eu.midnightdust.yaytris.Translation.t;
import static eu.midnightdust.yaytris.ui.TetrisUI.scale;
import static eu.midnightdust.yaytris.ui.TetrisUI.setFontScale;
@@ -17,7 +20,7 @@ public class SettingsMenu extends JPanel {
this.setBounds(x, y, width, height);
this.setLayout(null);
this.add(new JLabel("Musik-Lautstärke:"));
this.add(new JLabel(t("settings.volume.music")));
JSlider musicVolumeSlider = new JSlider(0, 100, Settings.musicVolume);
musicVolumeSlider.addChangeListener(change -> {
Settings.musicVolume = musicVolumeSlider.getValue();
@@ -25,7 +28,7 @@ public class SettingsMenu extends JPanel {
});
this.add(musicVolumeSlider);
this.add(new JLabel("Effekt-Lautstärke:"));
this.add(new JLabel(t("settings.volume.sound")));
JSlider soundVolumeSlider = new JSlider(0, 100, Settings.soundVolume);
soundVolumeSlider.addChangeListener(change -> {
Settings.soundVolume = soundVolumeSlider.getValue();
@@ -33,7 +36,7 @@ public class SettingsMenu extends JPanel {
});
this.add(soundVolumeSlider);
this.add(new JLabel("Fenster-Skalierung:"));
this.add(new JLabel(t("settings.window_scaling")));
JSlider scaleSlider = new JSlider(100, 600, (int) (Settings.guiScale * 100));
scaleSlider.addChangeListener(change -> {
Settings.guiScale = scaleSlider.getValue() / 100f;
@@ -41,7 +44,7 @@ public class SettingsMenu extends JPanel {
});
this.add(scaleSlider);
this.add(new JLabel("Geschwindigkeitszunahme:"));
this.add(new JLabel(t("settings.scale_speed")));
JCheckBox speedCheckbox = new JCheckBox("Aktiviert", Settings.shouldScaleSpeed);
speedCheckbox.addChangeListener(change -> {
Settings.shouldScaleSpeed = speedCheckbox.isSelected();
@@ -49,7 +52,7 @@ public class SettingsMenu extends JPanel {
});
this.add(speedCheckbox);
this.add(new JLabel("Schwierigkeit:"));
this.add(new JLabel(t("settings.difficulty")));
JComboBox<Difficulty> difficultySelector = new JComboBox<>(Difficulty.values());
difficultySelector.setSelectedItem(Settings.difficulty);
@@ -60,7 +63,20 @@ public class SettingsMenu extends JPanel {
this.add(difficultySelector);
difficultySelector.setSize(difficultySelector.getBounds().width,scale(10));
JButton backButton = new JButton("Zurück");
this.add(new JLabel(t("settings.language")));
JComboBox<Language> languageSelector = new JComboBox<>(Language.values());
languageSelector.setSelectedItem(Settings.language);
languageSelector.addActionListener(action -> {
Settings.language = (Language) languageSelector.getSelectedItem();
Settings.write();
Translation.load(Settings.language.locale);
ui.openSettings(action);
});
this.add(languageSelector);
languageSelector.setSize(languageSelector.getBounds().width,scale(10));
JButton backButton = new JButton(t("ui.back"));
backButton.addActionListener(ui::openMainMenu);
this.add(backButton);
}
@@ -68,9 +84,9 @@ public class SettingsMenu extends JPanel {
@Override
public Component add(Component comp) {
comp.setBounds(scale(60), scale(20+23*this.getComponentCount()-labelAmount*10), scale(100), scale(20));
comp.setBounds(scale(60), scale(20+17*this.getComponentCount()-labelAmount*10), scale(100), scale(20));
if (comp instanceof JLabel) {
comp.setBounds(scale(60), scale(50+23*(this.getComponentCount()-1)-labelAmount*10), scale(100), scale(7));
comp.setBounds(scale(60), scale(20+17*(this.getComponentCount())-labelAmount*10), scale(100), scale(7));
labelAmount++;
}
if (comp instanceof JComponent) setFontScale((JComponent) comp);

View File

@@ -14,6 +14,7 @@ import java.awt.event.KeyListener;
import java.io.IOException;
import static eu.midnightdust.yaytris.Settings.guiScale;
import static eu.midnightdust.yaytris.Translation.t;
public class TetrisUI extends JFrame implements KeyListener {
JLabel titleLabel;
@@ -125,7 +126,7 @@ public class TetrisUI extends JFrame implements KeyListener {
}
public void showHighscoreDialog(int score) {
String playerName = JOptionPane.showInputDialog(null, "Gib deinen Namen ein:", "Neuer Highscore!", JOptionPane.PLAIN_MESSAGE);
String playerName = JOptionPane.showInputDialog(null, t("dialog.highscore.action"), t("dialog.highscore.title"), JOptionPane.PLAIN_MESSAGE);
HighScores.addScore(playerName, score);
}

View File

@@ -0,0 +1,17 @@
package eu.midnightdust.yaytris.util;
public enum Language {
ENGLISH("en_gb"), DEUTSCH("de_de");
public final String locale;
private final String name;
Language(String locale) {
this.locale = locale;
this.name = this.name().charAt(0) + this.name().substring(1).toLowerCase();
}
@Override
public String toString() {
return this.name;
}
}

View File

@@ -9,19 +9,24 @@ import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
/*
NightJson v0.1 by Martin Prokoph
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 = "\"(-?[0-9a-zA-Z]*)\":";
private static final String KEY_PATTERN = "\"(-?.*)\":";
Class<?> jsonClass;
Map<String, ?> jsonMap;
String fileLocation;
public NightJson(Class<?> jsonClass, String fileLocation) {
this.jsonClass = jsonClass;
this.fileLocation = fileLocation;
}
public NightJson(Map<String, ?> jsonMap, String fileLocation) {
this.jsonMap = jsonMap;
this.fileLocation = fileLocation;
}
public void writeJson() {
if (fileLocation == null) return;
@@ -29,17 +34,22 @@ public class NightJson {
FileWriter jsonFile = new FileWriter(fileLocation);
jsonFile.write("{\n");
Iterator<Field> it = Arrays.stream(jsonClass.getFields()).iterator();
while (it.hasNext()) {
Field field = it.next();
jsonFile.write("\t");
if (field.getType() == Comment.class) {
jsonFile.write(String.format("// %s\n", ((Comment) field.get(null)).commentString));
continue;
if (jsonClass != null) {
Iterator<Field> it = Arrays.stream(jsonClass.getFields()).iterator();
while (it.hasNext()) {
Field field = it.next();
writeElement(jsonFile, field.get(null), field.getType(), field.getName());
jsonFile.write(it.hasNext() ? ",\n" : "\n");
}
}
else if (jsonMap != null) {
Iterator<String> it = jsonMap.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
Object value = jsonMap.get(key);
writeElement(jsonFile, jsonMap.get(key), value.getClass(), key);
jsonFile.write(it.hasNext() ? ",\n" : "\n");
}
jsonFile.write(String.format("\"%s\": ", field.getName()));
jsonFile.write(objToString(field.get(null), field.getType()));
jsonFile.write(it.hasNext() ? ",\n" : "\n");
}
jsonFile.write("}");
jsonFile.close();
@@ -49,6 +59,16 @@ public class NightJson {
}
}
private void writeElement(FileWriter jsonFile, Object value, Class<?> type, String name) throws IOException, IllegalAccessException {
jsonFile.write("\t");
if (type == Comment.class) {
jsonFile.write(String.format("// %s\n", ((Comment) value).commentString));
return;
}
jsonFile.write(String.format("\"%s\": ", name));
jsonFile.write(objToString(value, type));
}
public void readJson() {
if (fileLocation == null) return;
try {
@@ -80,11 +100,20 @@ public class NightJson {
for (String key : jsonKeyValuePairs.keySet()) {
String currentString = jsonKeyValuePairs.get(key);
//System.out.printf("Key: %s Value: %s%n", key, currentString);
Field field;
try { field = jsonClass.getField(key);
} catch (NoSuchFieldException e) {continue;}
if (jsonClass != null) {
Field field;
try {
field = jsonClass.getField(key);
} catch (NoSuchFieldException e) {
continue;
}
field.set(field, stringToFieldObj(currentString, field));
field.set(field, stringToFieldObj(currentString, field));
}
else if (jsonMap != null) {
//noinspection unchecked
((Map<String, Object>)jsonMap).put(key, stringToObj(currentString, String.class)); // TODO: Un-hardcode string type
}
}
jsonFile.close();
} catch (IOException | IllegalAccessException | NoSuchElementException e) {
@@ -93,7 +122,7 @@ public class NightJson {
}
}
private String objToString(Object value, Class<?> type) throws IllegalAccessException {
private String objToString(Object value, Class<?> type) {
if (type == Map.class) {
StringBuilder mapPairs = new StringBuilder();
Map<?, ?> map = (Map<?, ?>) value;

View File

@@ -1,3 +1,3 @@
{
"scores": {"Martin": 5703}
"scores": {"Martin": 6401}
}

View File

@@ -1,7 +1,8 @@
{
"musicVolume": 19,
"musicVolume": 100,
"soundVolume": 100,
"guiScale": 6.0,
"shouldScaleSpeed": true,
"difficulty": "Normal"
"difficulty": "Normal",
"language": "Deutsch"
}

20
translation/de_de.json5 Normal file
View File

@@ -0,0 +1,20 @@
{
"dialog.highscore.action": "Gib deinen Namen ein:",
"dialog.highscore.title": "Neuer Highscore!",
"game.level": "Level: %s",
"game.over": "Game over :(",
"game.score": "Score: %s",
"game.time": "Zeit: %s",
"highscores.title": "Highscores:",
"menu.exit": "Spiel verlassen",
"menu.highscores": "Highscores",
"menu.settings": "Einstellungen",
"menu.start": "Start",
"settings.difficulty": "Schwierigkeit:",
"settings.language": "Sprache:",
"settings.scale_speed": "Geschwindigkeitszunahme:",
"settings.volume.music": "Musik-Lautstärke:",
"settings.volume.sound": "Effekt-Lautstärke:",
"settings.window_scaling": "Fenster-Skalierung:",
"ui.back": "Zurück"
}

20
translation/en_gb.json5 Normal file
View File

@@ -0,0 +1,20 @@
{
"dialog.highscore.action": "Enter your name:",
"dialog.highscore.title": "New Highscore!",
"game.level": "Level: %s",
"game.over": "Game over :(",
"game.score": "Score: %s",
"game.time": "Time: %s",
"highscores.title": "Highscores:",
"menu.exit": "Exit Game",
"menu.highscores": "Highscores",
"menu.settings": "Settings",
"menu.start": "Start",
"settings.difficulty": "Difficulty:",
"settings.language": "Language:",
"settings.scale_speed": "Speed Increase:",
"settings.volume.music": "Music Volume:",
"settings.volume.sound": "Sound Volume:",
"settings.window_scaling": "Window Scaling:",
"ui.back": "Back"
}