commit 9e45ea3f374c9769d112c19bd2d06ee9cd513189 Author: Martin Prokoph Date: Fri Jun 27 19:58:59 2025 +0200 feat: day 1 progress diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..c586672 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +aop \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..cabbbc1 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..2a65317 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..f16dea7 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..6dec271 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("java") +} + +group = "org.example" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..712cb1c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Apr 16 11:14:58 CEST 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..4e38ff4 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "aop" + diff --git a/src/main/java/eu/midnightdust/yaytris/Settings.java b/src/main/java/eu/midnightdust/yaytris/Settings.java new file mode 100644 index 0000000..fe7c9d7 --- /dev/null +++ b/src/main/java/eu/midnightdust/yaytris/Settings.java @@ -0,0 +1,23 @@ +package eu.midnightdust.yaytris; + +import eu.midnightdust.yaytris.util.NightJson; + +import java.util.HashMap; +import java.util.Map; + +public class Settings { + private static final NightJson json = new NightJson(Settings.class, "tetris_settings.json5"); + + public static int musicVolume = 100; + public static int soundVolume = 100; + public static float guiScale = 3.f; + //public static Map highScores = new HashMap<>(); + + public static void load() { + json.readJson(); + } + + public static void write() { + json.writeJson(); + } +} diff --git a/src/main/java/eu/midnightdust/yaytris/Tetris.java b/src/main/java/eu/midnightdust/yaytris/Tetris.java new file mode 100644 index 0000000..d246f71 --- /dev/null +++ b/src/main/java/eu/midnightdust/yaytris/Tetris.java @@ -0,0 +1,21 @@ +package eu.midnightdust.yaytris; + +import eu.midnightdust.yaytris.game.Space; +import eu.midnightdust.yaytris.ui.TetrisUI; + +import javax.swing.*; + +public class Tetris { + public static Space space; + static TetrisUI ui; + + public static void main(String[] args) { + try { + System.setProperty("java.awt.headless", "false"); + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception | Error e) { System.out.printf("%s: %s\n", "Error setting system look and feel", e); } + Settings.load(); + space = new Space(); + ui = new TetrisUI(); + } +} diff --git a/src/main/java/eu/midnightdust/yaytris/game/Space.java b/src/main/java/eu/midnightdust/yaytris/game/Space.java new file mode 100644 index 0000000..ca5fbb5 --- /dev/null +++ b/src/main/java/eu/midnightdust/yaytris/game/Space.java @@ -0,0 +1,47 @@ +package eu.midnightdust.yaytris.game; + +import java.awt.Color; +import java.util.*; + +public class Space { + private final Color[][] gameMap; // Bereits abgesetzte Tetrominos werden nur noch als einzelne Farben ('Blobs') auf der Karte abgespeichert + + public Space() { + gameMap = new Color[7][12]; + for (int x = 0; x < gameMap.length; x++) { + for (int y = 0; y < gameMap[x].length; y++) { + if (Math.random() < 0.5f) { + gameMap[x][y] = Color.getHSBColor((float) Math.random(), 1.f, 1.f); + } + } + } + } + + public Color[][] getGameMap() { + return gameMap; + } + + public int onLinesChanged(Tetromino tetromino, int... lines) { + int combo = 0; + Set completedLines = new TreeSet<>(); + for (int line : lines) { + Color[] newBlobs = tetromino.getLine(line); + for (int i = 0; i < newBlobs.length; i++) { + if (newBlobs[i] == null) continue; + gameMap[line][i] = newBlobs[i]; + } + if (Arrays.stream(gameMap[line]).noneMatch(Objects::isNull)) { // Line completed + combo += 1; + completedLines.add(line); + combo *= completedLines.size(); + } + } + for (int completedIndex = 0; completedIndex < completedLines.size(); completedIndex++) { // Remove completed lines + int line = completedLines.toArray(new Integer[0])[completedIndex]; + for (int i = line+completedIndex; i >= 0; i--) { + gameMap[i] = gameMap[i-1]; + } + } + return combo; + } +} diff --git a/src/main/java/eu/midnightdust/yaytris/game/Tetromino.java b/src/main/java/eu/midnightdust/yaytris/game/Tetromino.java new file mode 100644 index 0000000..f88bace --- /dev/null +++ b/src/main/java/eu/midnightdust/yaytris/game/Tetromino.java @@ -0,0 +1,40 @@ +package eu.midnightdust.yaytris.game; + +import eu.midnightdust.yaytris.util.Vec2i; + +import java.awt.*; + +public class Tetromino { + private final TetrominoShape shape; + private int[][] collision; + private Vec2i centerPos; + + public Tetromino(TetrominoShape shape) { + this.shape = shape; + this.collision = shape.boundary; + this.centerPos = Vec2i.of(0, 0); + } + + public void fall(int length) { + centerPos = centerPos.offset(Vec2i.of(0, length)); + } + + public void rotate() { + int[][] newCollision = new int[collision[0].length][collision.length]; + for (int i = 0; i < collision.length; i++) { + for (int j = 0; j < collision[i].length; j++) { + newCollision[j][i] = collision[i][j]; + } + } + this.collision = newCollision; + } + + public Color[] getLine(int line) { + Color[] l = new Color[7]; + for (int i = 0; i < l.length; i++) { + if (collision.length < line-centerPos.getX() && collision[line-centerPos.getX()][i] != 0) + l[i] = shape.color; + } + return l; + } +} diff --git a/src/main/java/eu/midnightdust/yaytris/game/TetrominoShape.java b/src/main/java/eu/midnightdust/yaytris/game/TetrominoShape.java new file mode 100644 index 0000000..2efacc3 --- /dev/null +++ b/src/main/java/eu/midnightdust/yaytris/game/TetrominoShape.java @@ -0,0 +1,48 @@ +package eu.midnightdust.yaytris.game; + +import java.awt.Color; + +public enum TetrominoShape { + SQUARE(new int[][]{ + {1, 1}, + {1, 2} + }, Color.YELLOW), + LINE(new int[][]{ + {1}, + {2}, + {1}, + {1} + }, Color.BLUE), + T(new int[][]{ + {0, 1, 0}, + {1, 2, 1} + }, Color.RED), + L_LEFT(new int[][]{ + {0, 1}, + {0, 2}, + {1, 1} + }, Color.MAGENTA), + L_RIGHT(new int[][]{ + {1, 0}, + {2, 0}, + {1, 1} + }, Color.GREEN), + ZAP_LEFT(new int[][]{ + {0, 1}, + {1, 2}, + {1, 0} + }, Color.CYAN), + ZAP_RIGHT(new int[][]{ + {1, 0}, + {2, 1}, + {0, 1} + }, Color.PINK); + ; + + final int[][] boundary; + final Color color; + TetrominoShape(int[][] boundary, Color color) { + this.boundary = boundary; + this.color = color; + } +} diff --git a/src/main/java/eu/midnightdust/yaytris/ui/GameCanvas.java b/src/main/java/eu/midnightdust/yaytris/ui/GameCanvas.java new file mode 100644 index 0000000..9e69be0 --- /dev/null +++ b/src/main/java/eu/midnightdust/yaytris/ui/GameCanvas.java @@ -0,0 +1,46 @@ +package eu.midnightdust.yaytris.ui; + +import eu.midnightdust.yaytris.Tetris; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; + +public class GameCanvas extends JPanel { + final TetrisUI ui; + final BufferedImage texture; + + GameCanvas(TetrisUI ui) { + this.ui = ui; + try { + this.texture = ImageIO.read(this.getClass().getResourceAsStream("/textures/tetromino.png")); + } catch (IOException | NullPointerException ex) { + throw new RuntimeException(ex); + } + } + + public void paintComponent(Graphics graphics) { + super.paintComponent(graphics); + if (graphics == null) return; + + //graphics.clearRect(this.getX(), this.getY(), this.getWidth(), this.getHeight()); + for (int x = 0; x < Tetris.space.getGameMap().length; x++) { + for (int y = 0; y < Tetris.space.getGameMap()[x].length; y++) { + Color color = Tetris.space.getGameMap()[x][y]; + if (color == null) continue; + int blockSize = (this.getWidth()-this.getInsets().right)/Tetris.space.getGameMap().length; + //graphics.setXORMode(color); + graphics.drawImage(texture, x*blockSize +getInsets().left, y*blockSize + getInsets().top, blockSize, blockSize, color, this); + graphics.setColor(withAlpha(color, 100)); + graphics.fillRect(x*blockSize +getInsets().left, y*blockSize + getInsets().top, blockSize, blockSize); + } + } + //this.paint(graphics); + //super.paintComponent(graphics); + } + public static Color withAlpha(Color color, int alpha) { + return new Color(color.getRed(), color.getGreen(), color.getBlue(), alpha); + } +} diff --git a/src/main/java/eu/midnightdust/yaytris/ui/MainMenu.java b/src/main/java/eu/midnightdust/yaytris/ui/MainMenu.java new file mode 100644 index 0000000..c90ec9c --- /dev/null +++ b/src/main/java/eu/midnightdust/yaytris/ui/MainMenu.java @@ -0,0 +1,25 @@ +package eu.midnightdust.yaytris.ui; + +import javax.swing.*; +import javax.swing.plaf.basic.BasicButtonUI; +import javax.swing.plaf.metal.MetalButtonUI; +import javax.swing.plaf.synth.SynthButtonUI; + +import static eu.midnightdust.yaytris.ui.TetrisUI.scale; +import static eu.midnightdust.yaytris.ui.TetrisUI.setFontScale; + +public class MainMenu extends JPanel { + final TetrisUI ui; + + MainMenu(int x, int y, int width, int height, TetrisUI ui) { + this.ui = ui; + this.setBounds(x, y, width, height); + this.setLayout(null); + + JButton settingsButton = new JButton("Settings"); + settingsButton.addActionListener(ui::openSettings); + settingsButton.setBounds(scale(60), scale(20), scale(100), scale(20)); + setFontScale(settingsButton); + this.add(settingsButton); + } +} diff --git a/src/main/java/eu/midnightdust/yaytris/ui/SettingsMenu.java b/src/main/java/eu/midnightdust/yaytris/ui/SettingsMenu.java new file mode 100644 index 0000000..cebfec0 --- /dev/null +++ b/src/main/java/eu/midnightdust/yaytris/ui/SettingsMenu.java @@ -0,0 +1,38 @@ +package eu.midnightdust.yaytris.ui; + +import eu.midnightdust.yaytris.Settings; +import eu.midnightdust.yaytris.Tetris; + +import javax.swing.*; +import javax.swing.plaf.basic.BasicSliderUI; + +import java.awt.*; + +import static eu.midnightdust.yaytris.ui.TetrisUI.scale; +import static eu.midnightdust.yaytris.ui.TetrisUI.setFontScale; + +public class SettingsMenu extends JPanel { + final TetrisUI ui; + + SettingsMenu(int x, int y, int width, int height, TetrisUI ui) { + this.ui = ui; + this.setBounds(x, y, width, height); + this.setLayout(null); + + JSlider scaleSlider = new JSlider(100, 500, (int) (Settings.guiScale * 100)); + scaleSlider.setBounds(scale(10), scale(20), scale(200), scale(20)); + scaleSlider.setBackground(Color.DARK_GRAY); + scaleSlider.addChangeListener(change -> { + Settings.guiScale = scaleSlider.getValue() / 100f; + Settings.write(); + }); + setFontScale(scaleSlider); + this.add(scaleSlider); + + JButton backButton = new JButton("Back"); + backButton.addActionListener(ui::openMainMenu); + backButton.setBounds(scale(60), scale(140), scale(100), scale(20)); + setFontScale(backButton); + this.add(backButton); + } +} diff --git a/src/main/java/eu/midnightdust/yaytris/ui/TetrisUI.java b/src/main/java/eu/midnightdust/yaytris/ui/TetrisUI.java new file mode 100644 index 0000000..6a7052c --- /dev/null +++ b/src/main/java/eu/midnightdust/yaytris/ui/TetrisUI.java @@ -0,0 +1,116 @@ +package eu.midnightdust.yaytris.ui; + +import eu.midnightdust.yaytris.game.Space; + +import javax.imageio.ImageIO; +import javax.swing.*; +import javax.swing.border.LineBorder; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.io.IOException; + +import static eu.midnightdust.yaytris.Settings.guiScale; + +public class TetrisUI extends JFrame { + JLabel titleLabel; + GameCanvas gamePanel; + JPanel menuPanel; + + public TetrisUI() { + Space space = new Space(); + this.setLayout(null); + this.setTitle("Tetris"); + this.setSize((int) (400 * guiScale), (int) (300 * guiScale)); + this.setResizable(false); + this.getContentPane().setBackground(Color.DARK_GRAY); + this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setWindowPosition(this, 0); + + titleLabel = new JLabel("Tetris"); + titleLabel.setForeground(Color.WHITE); + Image titleImage; + try { + titleImage = ImageIO.read(this.getClass().getResourceAsStream("/textures/logo.png")); + } catch (IOException | NullPointerException ex) { + throw new RuntimeException(ex); + } + titleLabel = new JLabel(); + titleLabel.setIcon(new ImageIcon(new ImageIcon(titleImage).getImage().getScaledInstance(scale(110), scale(30), Image.SCALE_DEFAULT))); + this.add(titleLabel); + + gamePanel = new GameCanvas(this); + gamePanel.setBackground(Color.BLACK); + gamePanel.setBorder(new LineBorder(Color.GRAY, scale(2))); + this.add(gamePanel); + + rescale(); + openMainMenu(null); + + this.setVisible(true); + } + + private void rescale() { + this.setSize((int) (400 * guiScale), (int) (300 * guiScale)); + titleLabel.setBounds(scale(225), scale(7), scale(110), scale(30)); + gamePanel.setBounds(scale(10), scale(10), scale(150), scale(260)); + for (Component comp : this.getComponents()){ + if (comp instanceof JComponent) setFontScale((JComponent) comp); + } + } + + public static int scale(int bound) { + return (int) (bound * guiScale); + } + public static void setFontScale(JComponent label) { + //if (label.getFont() != null) label.setFont(label.getFont().deriveFont((float) label.getFont().getSize() * guiScale)); + } + + public void openMainMenu(ActionEvent actionEvent) { + if (this.menuPanel != null) this.remove(menuPanel); + rescale(); + menuPanel = new MainMenu(scale(170), scale(40), scale(220), scale(230), this); + menuPanel.setBackground(Color.DARK_GRAY); + menuPanel.setBorder(new LineBorder(Color.GRAY, scale(2))); + this.add(menuPanel); + this.repaint(); + } + + public void openSettings(ActionEvent actionEvent) { + if (this.menuPanel != null) this.remove(menuPanel); + menuPanel = new SettingsMenu(scale(170), scale(40), scale(220), scale(230), this); + menuPanel.setBackground(Color.DARK_GRAY); + menuPanel.setBorder(new LineBorder(Color.GRAY, scale(2))); + this.add(menuPanel); + this.repaint(); + } + + // Source: https://stackoverflow.com/a/19746437 + private void setWindowPosition(JFrame window, int screen) + { + GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice[] allDevices = env.getScreenDevices(); + int topLeftX, topLeftY, screenX, screenY, windowPosX, windowPosY; + + if (screen < allDevices.length && screen > -1) + { + topLeftX = allDevices[screen].getDefaultConfiguration().getBounds().x; + topLeftY = allDevices[screen].getDefaultConfiguration().getBounds().y; + + screenX = allDevices[screen].getDefaultConfiguration().getBounds().width; + screenY = allDevices[screen].getDefaultConfiguration().getBounds().height; + } + else + { + topLeftX = allDevices[0].getDefaultConfiguration().getBounds().x; + topLeftY = allDevices[0].getDefaultConfiguration().getBounds().y; + + screenX = allDevices[0].getDefaultConfiguration().getBounds().width; + screenY = allDevices[0].getDefaultConfiguration().getBounds().height; + } + + windowPosX = ((screenX - window.getWidth()) / 2) + topLeftX; + windowPosY = ((screenY - window.getHeight()) / 2) + topLeftY; + + window.setLocation(windowPosX, windowPosY); + } +} diff --git a/src/main/java/eu/midnightdust/yaytris/util/NightJson.java b/src/main/java/eu/midnightdust/yaytris/util/NightJson.java new file mode 100644 index 0000000..fad0a94 --- /dev/null +++ b/src/main/java/eu/midnightdust/yaytris/util/NightJson.java @@ -0,0 +1,113 @@ +package eu.midnightdust.yaytris.util; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +/* + NightJson v0.1 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]*)\":"; + Class jsonClass; + String fileLocation; + + public NightJson(Class jsonClass) { + this.jsonClass = jsonClass; + } + public NightJson(Class jsonClass, String fileLocation) { + this(jsonClass); + this.fileLocation = fileLocation; + } + + public void setFileLocation(String fileLocation) { + this.fileLocation = fileLocation; + } + + public void writeJson() { + if (fileLocation == null) return; + try { + FileWriter jsonFile = new FileWriter(fileLocation); + + jsonFile.write("{\n"); + Iterator it = Arrays.stream(jsonClass.getFields()).iterator(); + while (it.hasNext()) { + Field field = it.next(); + jsonFile.write("\t"); + if (field.getType() == Comment.class) { + jsonFile.write("// %s\n".formatted(((Comment) field.get(null)).commentString)); + continue; + } + jsonFile.write((field.getType() == String.class || field.getType().isEnum() ? "\"%s\": \"%s\"" : "\"%s\": %s").formatted(field.getName(), field.get(null))); + jsonFile.write(it.hasNext() ? ",\n" : "\n"); + } + jsonFile.write("}"); + jsonFile.close(); + } catch (IOException | IllegalAccessException e) { + System.out.println("Oh no! An Error occurred whilst writing the JSON file :("); + e.fillInStackTrace(); + } + } + + public void readJson() { + if (fileLocation == null) return; + try { + File file = new File(fileLocation); + if (!file.exists()) { + writeJson(); + return; + } + + Scanner jsonFile = new Scanner(file); + Map jsonKeyValuePairs = new HashMap<>(); + AtomicReference lastKey = new AtomicReference<>(); + jsonFile.forEachRemaining(s -> { + if (!s.matches("[{}]") && !s.matches("//+")) { + if (s.matches(KEY_PATTERN)) { + lastKey.set(s.replaceAll("([\":])", "")); + jsonKeyValuePairs.put(lastKey.get(), ""); + } + else jsonKeyValuePairs.put(lastKey.get(), (jsonKeyValuePairs.get( + lastKey.get()).isEmpty() ? "" : jsonKeyValuePairs.get(lastKey.get()) + " " + ) + s.replaceAll("([\",])", "")); + } + }); + + 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;} + + Object value = switch (field.getType().getName()) { + case "byte" -> Byte.parseByte(currentString); + case "int" -> Integer.parseInt(currentString); + case "long" -> Long.parseLong(currentString); + case "float" -> Float.parseFloat(currentString); + case "double" -> Double.parseDouble(currentString); + default -> currentString; + }; + if (field.getType().isEnum()) value = Arrays.stream(field.getType().getEnumConstants()) + .filter(enumConstant -> Objects.equals(enumConstant.toString(), currentString)).findFirst().orElseThrow(); + field.set(field, value); + } + jsonFile.close(); + } catch (IOException | IllegalAccessException | NoSuchElementException e) { + System.out.println("Oh no! An Error occurred whilst reading the JSON file :("); + e.fillInStackTrace(); + } + } + + public static class Comment { + final String commentString; + public Comment(String commentString) { + this.commentString = commentString; + } + } +} diff --git a/src/main/java/eu/midnightdust/yaytris/util/Vec2i.java b/src/main/java/eu/midnightdust/yaytris/util/Vec2i.java new file mode 100644 index 0000000..5068204 --- /dev/null +++ b/src/main/java/eu/midnightdust/yaytris/util/Vec2i.java @@ -0,0 +1,31 @@ +package eu.midnightdust.yaytris.util; + +public class Vec2i { + private final int x; + private final int y; + + Vec2i(int x, int y) { + this.x = x; + this.y = y; + } + + public static Vec2i of(int x) { + //noinspection SuspiciousNameCombination + return new Vec2i(x, x); + } + public static Vec2i of(int x, int y) { + return new Vec2i(x, y); + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public Vec2i offset(Vec2i other) { + return new Vec2i(x + other.getX(), y + other.getY()); + } +} diff --git a/src/main/resources/textures/logo.png b/src/main/resources/textures/logo.png new file mode 100644 index 0000000..df0d4d0 Binary files /dev/null and b/src/main/resources/textures/logo.png differ diff --git a/src/main/resources/textures/logo.pxo b/src/main/resources/textures/logo.pxo new file mode 100644 index 0000000..4863d3e Binary files /dev/null and b/src/main/resources/textures/logo.pxo differ diff --git a/src/main/resources/textures/tetromino.png b/src/main/resources/textures/tetromino.png new file mode 100644 index 0000000..502c733 Binary files /dev/null and b/src/main/resources/textures/tetromino.png differ diff --git a/src/main/resources/textures/tetromino.pxo b/src/main/resources/textures/tetromino.pxo new file mode 100644 index 0000000..fe6c170 Binary files /dev/null and b/src/main/resources/textures/tetromino.pxo differ diff --git a/tetris_settings.json5 b/tetris_settings.json5 new file mode 100644 index 0000000..396dd38 --- /dev/null +++ b/tetris_settings.json5 @@ -0,0 +1,5 @@ +{ + "musicVolume": 100, + "soundVolume": 100, + "guiScale": 4.95 +} \ No newline at end of file