diff --git a/.gitignore b/.gitignore index 6e117ff..8d0f0a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,120 @@ +# Created by https://www.toptal.com/developers/gitignore/api/gradle,intellij,java +# Edit at https://www.toptal.com/developers/gitignore?templates=gradle,intellij,java + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### Java ### # Compiled class file *.class @@ -23,8 +140,35 @@ hs_err_pid* replay_pid* -.idea +### Gradle ### .gradle -build/* +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Gradle Patch ### +# Java heap dump +*.hprof + +# End of https://www.toptal.com/developers/gitignore/api/gradle,intellij,java /resources/ +/build/ +/bin/ \ 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/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..3ff5d35 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..7b18ee8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..95c6c66 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..70c5a94 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0a6f7f2 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..9522e33 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/Minigames.main.iml b/.idea/modules/Minigames.main.iml new file mode 100644 index 0000000..33db1bc --- /dev/null +++ b/.idea/modules/Minigames.main.iml @@ -0,0 +1,17 @@ + + + + + + + ADVENTURE + + 1 + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/Minigames.test.iml b/.idea/modules/Minigames.test.iml new file mode 100644 index 0000000..a376b96 --- /dev/null +++ b/.idea/modules/Minigames.test.iml @@ -0,0 +1,13 @@ + + + + + + + ADVENTURE + + 1 + + + + \ No newline at end of file diff --git a/.idea/modules/eu.mhsl.minenet.Minigames.main.iml b/.idea/modules/eu.mhsl.minenet.Minigames.main.iml new file mode 100644 index 0000000..a376b96 --- /dev/null +++ b/.idea/modules/eu.mhsl.minenet.Minigames.main.iml @@ -0,0 +1,13 @@ + + + + + + + ADVENTURE + + 1 + + + + \ No newline at end of file diff --git a/.idea/modules/eu.mhsl.minenet.Minigames.test.iml b/.idea/modules/eu.mhsl.minenet.Minigames.test.iml new file mode 100644 index 0000000..a376b96 --- /dev/null +++ b/.idea/modules/eu.mhsl.minenet.Minigames.test.iml @@ -0,0 +1,13 @@ + + + + + + + ADVENTURE + + 1 + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3e1ac02..9a890d3 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { id 'java' - id "com.github.johnrengelman.shadow" version "7.1.0" + id "com.gradleup.shadow" version "9.6.0" } group 'eu.mhsl.minenet' @@ -11,58 +11,37 @@ version '1.0-SNAPSHOT' repositories { mavenCentral() - google() - - maven { - url 'https://jitpack.io' - } - - maven { - url 'https://oss.sonatype.org/content/repositories/snapshots' - } - - maven { - url 'https://repo.papermc.io/repository/maven-public/' - } + maven { url = 'https://mvn.everbuild.org/public' } } -allprojects { - repositories { - maven { url "https://jitpack.io" } - } -} - - java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(25) } } dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' + testImplementation 'org.junit.jupiter:junit-jupiter:6.1.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - //https://jitpack.io/#Minestom/Minestom - implementation 'net.minestom:minestom-snapshots:fd51c8d17a' + //https://mvnrepository.com/artifact/net.minestom/minestom + implementation 'net.minestom:minestom:2026.07.12-26.2' //Tools - implementation 'de.articdive:jnoise:3.0.2' - implementation 'net.md-5:bungeecord-config:1.20-R0.1' - implementation 'org.apache.commons:commons-text:1.10.0' - implementation 'org.spongepowered:configurate-yaml:4.1.2' + implementation 'de.articdive:jnoise-pipeline:4.1.0' + compileOnly 'it.unimi.dsi:fastutil:8.5.18' // von Minestom nur zur Laufzeit bereitgestellt, fuer BatchUtil-Reflection benoetigt + implementation 'net.md-5:bungeecord-config:1.21-R0.3' + implementation 'org.apache.commons:commons-text:1.15.0' + implementation 'org.spongepowered:configurate-yaml:4.2.0' implementation 'com.sparkjava:spark-core:2.9.4' - implementation 'com.google.code.gson:gson:2.10.1' - implementation 'com.google.guava:guava:32.0.1-android' - + implementation 'com.google.code.gson:gson:2.14.0' + implementation 'com.google.guava:guava:33.4.8-jre' //PvP - implementation 'io.github.TogAr2:MinestomPvP:PR62-SNAPSHOT' + implementation 'io.github.togar2:MinestomPvP:2026.05.30-26.1.1' - // Hephaestus engine (repo.unnamed.team currently unreachable, unused in sources) -// implementation("team.unnamed:hephaestus-api:0.2.1-SNAPSHOT") -// implementation("team.unnamed:hephaestus-reader-blockbench:0.2.1-SNAPSHOT") -// implementation("team.unnamed:hephaestus-runtime-minestom:0.2.1-SNAPSHOT") + //Block placement + implementation 'org.everbuild.blocksandstuff:blocksandstuff-blocks:1.10.2-SNAPSHOT' } tasks { @@ -71,8 +50,6 @@ tasks { attributes 'Main-Class': 'eu.mhsl.minenet.minigames.Main' attributes 'Multi-Release': true } - duplicatesStrategy = 'exclude' - from configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) } } build { dependsOn(shadowJar) @@ -89,3 +66,7 @@ tasks.register('copyJarToServer', Exec) { commandLine 'scp', 'build/libs/Minigames-1.0-SNAPSHOT.jar', 'root@10.20.6.5:/root/minigames' } + +tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:deprecation" +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..b1b8ef5 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 index 9355b41..a9db115 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index f5feea6..249efbb 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 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. @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -115,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -173,7 +171,6 @@ fi # 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" ) @@ -206,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a21..a51ec4f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,12 +19,12 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,30 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :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 %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 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! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/src/main/java/eu/mhsl/minenet/minigames/Main.java b/src/main/java/eu/mhsl/minenet/minigames/Main.java index a04111a..5d147fc 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/Main.java +++ b/src/main/java/eu/mhsl/minenet/minigames/Main.java @@ -3,12 +3,12 @@ package eu.mhsl.minenet.minigames; import eu.mhsl.minenet.minigames.api.HttpServer; import eu.mhsl.minenet.minigames.command.Commands; import eu.mhsl.minenet.minigames.handler.Listeners; +import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.lang.Languages; import eu.mhsl.minenet.minigames.server.tasks.TablistUpdateTask; +import net.minestom.server.Auth; import net.minestom.server.MinecraftServer; -import net.minestom.server.extras.bungee.BungeeCordProxy; import net.minestom.server.extras.lan.OpenToLAN; -import net.minestom.server.extras.velocity.VelocityProxy; import net.minestom.server.timer.TaskSchedule; import org.spongepowered.configurate.ConfigurateException; import org.spongepowered.configurate.ConfigurationNode; @@ -24,27 +24,45 @@ public class Main { private final static Logger logger = Logger.getGlobal(); public static ConfigurationNode globalConfig; - public static void main(String[] args) throws ConfigurateException { + @SuppressWarnings("ResultOfMethodCallIgnored") + static void main(String[] args) throws ConfigurateException { //noinspection ResultOfMethodCallIgnored Resource.values(); // This initializes and preloads the enum and extracts the resources Languages.getInstance(); //Preload languages into the jvm globalConfig = YamlConfigurationLoader - .builder() - .path(Resource.CONFIG.getPath()) - .build() - .load(); + .builder() + .path(Resource.CONFIG.getPath()) + .build() + .load(); ConfigurationNode serverConfig = globalConfig.node("server"); + // muss vor MinecraftServer.init() gesetzt werden: ServerFlag liest die Properties statisch beim Klassenladen; + // bei fehlendem/ungueltigem Config-Wert den Minestom-Default behalten (0 wuerde keine Chunks senden) + int viewDistance = serverConfig.node("view-distance").getInt(0); + if(viewDistance > 0) { + System.setProperty("minestom.chunk-view-distance", String.valueOf(viewDistance)); + } + logger.info("Initialize Minecraft server..."); - MinecraftServer server = MinecraftServer.init(); + Auth auth = new Auth.Offline(); + if(globalConfig.node("bungeecord", "enabled").getBoolean()) { + auth = new Auth.Bungee(); + } else if(globalConfig.node("velocity", "enabled").getBoolean()) { + auth = new Auth.Velocity(Objects.requireNonNull(globalConfig.node("velocity", "secret").getString())); + } + + MinecraftServer server = MinecraftServer.init(auth); // MinestomPvP.init(); + // Dimensionen muessen vor server.start() registriert werden: + // seit Minestom 26.x sind die dynamischen Registries nach dem Start eingefroren + Dimension.values(); + MinecraftServer.setBrandName("mhsl.eu - minenet - credits to minestom"); MinecraftServer.setCompressionThreshold(serverConfig.node("compression-threshold").getInt(0)); - System.setProperty("minestom.chunk-view-distance", String.valueOf(serverConfig.node("view-distance").getInt())); Commands.values(); Listeners.values(); @@ -56,12 +74,6 @@ public class Main { if(serverConfig.node("open-to-lan").getBoolean()) OpenToLAN.open(); - if(globalConfig.node("bungeecord", "enabled").getBoolean()) BungeeCordProxy.enable(); - - if(globalConfig.node("velocity", "enabled").getBoolean()) { - VelocityProxy.enable(Objects.requireNonNull(globalConfig.node("velocity", "secret").getString())); - } - server.start("0.0.0.0", serverConfig.node("port").getInt(25565)); System.gc(); diff --git a/src/main/java/eu/mhsl/minenet/minigames/Resource.java b/src/main/java/eu/mhsl/minenet/minigames/Resource.java index 775748d..40bebd2 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/Resource.java +++ b/src/main/java/eu/mhsl/minenet/minigames/Resource.java @@ -21,6 +21,7 @@ public enum Resource { private final Path path; private final String name; + Resource(String name, boolean keepOutdated) { this.name = name; this.path = Path.of("resources/" + name); @@ -28,7 +29,7 @@ public enum Resource { try { Logger.getLogger("ressource").info("extracting resource " + name + " ... "); ResourceUtils.extractResource(name, keepOutdated); - } catch (URISyntaxException | IOException e) { + } catch(URISyntaxException | IOException e) { throw new RuntimeException(e); } } @@ -38,10 +39,10 @@ public enum Resource { } public Path getPath() { - return path; + return this.path; } public String getName() { - return name; + return this.name; } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/api/Controller.java b/src/main/java/eu/mhsl/minenet/minigames/api/Controller.java index 40afb34..cadc112 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/api/Controller.java +++ b/src/main/java/eu/mhsl/minenet/minigames/api/Controller.java @@ -12,11 +12,11 @@ public abstract class Controller implements Route { @SuppressWarnings("unchecked") public Controller() { - this.requestType = ((Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]); + this.requestType = ((Class) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0]); } @Override - public Object handle(Request request, Response response) throws Exception { + public Object handle(Request request, Response response) { response.header("Access-Control-Allow-Origin", "*"); response.header("Access-Control-Allow-Methods", "*"); @@ -28,7 +28,7 @@ public abstract class Controller implements Route { req = new Gson().fromJson(request.body(), this.requestType); } - return new Gson().toJson(handle(req, response)); + return new Gson().toJson(this.handle(req, response)); } public abstract R handle(Q request, Response response); diff --git a/src/main/java/eu/mhsl/minenet/minigames/api/HttpServer.java b/src/main/java/eu/mhsl/minenet/minigames/api/HttpServer.java index 26af5a7..a033a62 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/api/HttpServer.java +++ b/src/main/java/eu/mhsl/minenet/minigames/api/HttpServer.java @@ -10,6 +10,7 @@ import static spark.Spark.*; public class HttpServer { private static final ConfigurationNode apiConfig = Main.globalConfig.node("api"); + public HttpServer() { if(!apiConfig.node("enabled").getBoolean()) return; diff --git a/src/main/java/eu/mhsl/minenet/minigames/api/routes/closeRoom/CloseRoom.java b/src/main/java/eu/mhsl/minenet/minigames/api/routes/closeRoom/CloseRoom.java index 511723b..42481a4 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/api/routes/closeRoom/CloseRoom.java +++ b/src/main/java/eu/mhsl/minenet/minigames/api/routes/closeRoom/CloseRoom.java @@ -6,8 +6,12 @@ import spark.Response; import java.util.UUID; -record Req(UUID room) {} -record Resp() {} +record Req(UUID room) { +} + +record Resp() { +} + public class CloseRoom extends Controller { @Override public Resp handle(Req request, Response response) { diff --git a/src/main/java/eu/mhsl/minenet/minigames/api/routes/createRoom/CreateRoom.java b/src/main/java/eu/mhsl/minenet/minigames/api/routes/createRoom/CreateRoom.java index aefe365..0af1cc7 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/api/routes/createRoom/CreateRoom.java +++ b/src/main/java/eu/mhsl/minenet/minigames/api/routes/createRoom/CreateRoom.java @@ -6,8 +6,12 @@ import spark.Response; import java.util.UUID; -record Req() {} -record Resp(UUID uuid) {} +record Req() { +} + +record Resp(UUID uuid) { +} + public class CreateRoom extends Controller { @Override public Resp handle(Req request, Response response) { diff --git a/src/main/java/eu/mhsl/minenet/minigames/api/routes/queueRoom/QueueRoom.java b/src/main/java/eu/mhsl/minenet/minigames/api/routes/queueRoom/QueueRoom.java index a5cf128..fc584ba 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/api/routes/queueRoom/QueueRoom.java +++ b/src/main/java/eu/mhsl/minenet/minigames/api/routes/queueRoom/QueueRoom.java @@ -8,8 +8,12 @@ import spark.Response; import java.util.UUID; -record Req(UUID player, UUID room) {} -record Resp(String error) {} +record Req(UUID player, UUID room) { +} + +record Resp(String error) { +} + public class QueueRoom extends Controller { @Override public Resp handle(Req request, Response response) { diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/Commands.java b/src/main/java/eu/mhsl/minenet/minigames/command/Commands.java index a9a6a90..7880e78 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/Commands.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/Commands.java @@ -1,9 +1,8 @@ package eu.mhsl.minenet.minigames.command; -import eu.mhsl.minenet.minigames.command.privileged.SkinCommand; -import eu.mhsl.minenet.minigames.command.privileged.*; import eu.mhsl.minenet.minigames.command.anonymous.HubCommand; import eu.mhsl.minenet.minigames.command.anonymous.LeaveCommand; +import eu.mhsl.minenet.minigames.command.privileged.*; import eu.mhsl.minenet.minigames.message.Icon; import eu.mhsl.minenet.minigames.message.type.ChatMessage; import net.minestom.server.MinecraftServer; @@ -21,7 +20,6 @@ public enum Commands { ROOM(new RoomCommand()), UPDATE(new RefreshCommandsCommand()), OP(new OpCommand()), - FAKEPLAYER(new FakeplayerCommand()), KICK(new KickCommand()), SKIN(new SkinCommand()), SETOWNER(new SetRoomOwnerCommand()), @@ -34,14 +32,18 @@ public enum Commands { PLAYERLIMIT(new PlayerLimitCommand()), SETMEMORIAL(new SetMemorialCommand()); - Commands(Command handler) { - MinecraftServer.getCommandManager().register(handler); - } - static { MinecraftServer.getCommandManager().setUnknownCommandCallback((sender, command) -> { if(command.isBlank()) return; - new ChatMessage(Icon.ERROR).appendStatic("Unknown command: ").quote(command).send(sender); + new ChatMessage(Icon.ERROR) + .appendTranslated("common#unknownCommand") + .appendSpace() + .quote(command) + .send(sender); }); } + + Commands(Command handler) { + MinecraftServer.getCommandManager().register(handler); + } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/PrivilegedCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/PrivilegedCommand.java index c4aa335..1316da1 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/PrivilegedCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/PrivilegedCommand.java @@ -11,20 +11,21 @@ import java.util.List; public class PrivilegedCommand extends Command { private final List conditions = new ArrayList<>(); + public PrivilegedCommand(@NotNull String name, @Nullable String... aliases) { super(name, aliases); - construct(); + this.construct(); } public PrivilegedCommand(@NotNull String name) { super(name); - construct(); + this.construct(); } private void construct() { - addCondition(isPrivileged()); + this.addCondition(this.isPrivileged()); - setCondition((sender, commandString) -> conditions.parallelStream().allMatch(condition -> condition.canUse(sender, commandString))); + this.setCondition((sender, commandString) -> this.conditions.parallelStream().allMatch(condition -> condition.canUse(sender, commandString))); } protected CommandCondition isPrivileged() { @@ -32,6 +33,6 @@ public class PrivilegedCommand extends Command { } protected void addCondition(CommandCondition condition) { - conditions.add(condition); + this.conditions.add(condition); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/anonymous/HubCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/anonymous/HubCommand.java index 9936895..7daa110 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/anonymous/HubCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/anonymous/HubCommand.java @@ -1,8 +1,8 @@ package eu.mhsl.minenet.minigames.command.anonymous; -import eu.mhsl.minenet.minigames.util.MoveInstance; import eu.mhsl.minenet.minigames.instance.hub.Hub; import eu.mhsl.minenet.minigames.instance.room.Room; +import eu.mhsl.minenet.minigames.util.MoveInstance; import net.minestom.server.command.builder.Command; import net.minestom.server.entity.Player; @@ -10,12 +10,12 @@ public class HubCommand extends Command { public HubCommand() { super("hub"); - setCondition( + this.setCondition( (sender, commandString) -> ((Player) sender).getInstance() instanceof Room room && !room.apiDriven ); - setDefaultExecutor((sender, context) -> { + this.setDefaultExecutor((sender, context) -> { if(Room.getRoom((Player) sender).orElseThrow().apiDriven) return; Room.unsetRoom((Player) sender); MoveInstance.move((Player) sender, Hub.INSTANCE); diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/anonymous/LeaveCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/anonymous/LeaveCommand.java index ff36186..a30c2c6 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/anonymous/LeaveCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/anonymous/LeaveCommand.java @@ -9,8 +9,8 @@ public class LeaveCommand extends Command { public LeaveCommand() { super("leave"); - setCondition((sender, commandString) -> ((Player) sender).getInstance() instanceof Game); + this.setCondition((sender, commandString) -> ((Player) sender).getInstance() instanceof Game); - setDefaultExecutor((sender, context) -> Room.setOwnRoom((Player) sender)); + this.setDefaultExecutor((sender, context) -> Room.setOwnRoom((Player) sender)); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/DebugCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/DebugCommand.java index 4298529..a6980a2 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/DebugCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/DebugCommand.java @@ -14,26 +14,26 @@ public class DebugCommand extends PrivilegedCommand { public DebugCommand() { super("debug"); - setDefaultExecutor((sender, args) -> { + this.setDefaultExecutor((sender, args) -> { new ChatMessage(Icon.CHAT).appendTranslated("sample").send(sender); new ActionBarMessage().appendTranslated("sample").send(sender); new TitleMessage().subtitle(subtitleMessage -> subtitleMessage.appendTranslated("sample")).appendTranslated("sample").send(sender); List testplayers = new ArrayList<>() { { - add("MineTec"); - add("Goldi187"); - add("Test"); + this.add("MineTec"); + this.add("Goldi187"); + this.add("Test"); } }; new ChatMessage(Icon.STAR, true) - .appendTranslated("score#result") - .newLine() - .indent() - .numberedList(testplayers) - .undent() - .newLine() - .appendTranslated("score#thanks") - .send(sender); + .appendTranslated("score#result") + .newLine() + .indent() + .numberedList(testplayers) + .undent() + .newLine() + .appendTranslated("score#thanks") + .send(sender); new ChatMessage(Icon.SCIENCE).appendStatic(((Player) sender).getUuid().toString()).send(sender); }); diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/FakeplayerCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/FakeplayerCommand.java deleted file mode 100644 index 69c31c5..0000000 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/FakeplayerCommand.java +++ /dev/null @@ -1,31 +0,0 @@ -package eu.mhsl.minenet.minigames.command.privileged; - -import eu.mhsl.minenet.minigames.command.PrivilegedCommand; -import eu.mhsl.minenet.minigames.instance.room.Room; -import eu.mhsl.minenet.minigames.message.Icon; -import eu.mhsl.minenet.minigames.message.type.ChatMessage; -import net.minestom.server.command.builder.arguments.ArgumentType; -import net.minestom.server.entity.Player; - -import java.util.UUID; - -public class FakeplayerCommand extends PrivilegedCommand { - public FakeplayerCommand() { - super("fakeplayer"); - - addSyntax((sender, context) -> { - if(sender instanceof Player p) { - if(p.getInstance() instanceof Room room) { -// FakePlayer.initPlayer( // TODO FakePlayer does no longer exists -// UUID.randomUUID(), -// context.getRaw("name"), -// new FakePlayerOption().setInTabList(true).setRegistered(true), -// fakePlayer -> Room.setRoom(fakePlayer, room) -// ); - } else { - new ChatMessage(Icon.ERROR).appendStatic("Du musst dich in einer Raumlobby befinden!").send(sender); - } - } - }, ArgumentType.String("name")); - } -} diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/FlyCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/FlyCommand.java index f5e3dac..2e3385b 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/FlyCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/FlyCommand.java @@ -8,7 +8,7 @@ public class FlyCommand extends PrivilegedCommand { public FlyCommand() { super("fly"); - setDefaultExecutor((sender, context) -> { + this.setDefaultExecutor((sender, context) -> { Player p = (Player) sender; p.setVelocity(new Vec(0, 5, 0)); p.setFlying(!p.isFlying()); diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GameStartCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GameStartCommand.java index 1e591a8..0f1e813 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GameStartCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GameStartCommand.java @@ -7,11 +7,11 @@ import net.minestom.server.entity.Player; public class GameStartCommand extends PrivilegedCommand { public GameStartCommand() { super("gameStart"); - setDefaultExecutor((sender, context) -> { + this.setDefaultExecutor((sender, context) -> { Player player = (Player) sender; if(player.getInstance() instanceof StatelessGame game) { game.startAccessor(); } - }); + }); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GameStopCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GameStopCommand.java index addbcd3..8bfd2ee 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GameStopCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GameStopCommand.java @@ -7,7 +7,7 @@ import net.minestom.server.entity.Player; public class GameStopCommand extends PrivilegedCommand { public GameStopCommand() { super("gameStop"); - setDefaultExecutor((sender, context) -> { + this.setDefaultExecutor((sender, context) -> { Player player = (Player) sender; if(player.getInstance() instanceof StatelessGame game) { game.stop(); diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GameTimeoutCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GameTimeoutCommand.java index 8e3c1b8..ee4e7ba 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GameTimeoutCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GameTimeoutCommand.java @@ -12,7 +12,7 @@ public class GameTimeoutCommand extends PrivilegedCommand { ArgumentInteger timeout = ArgumentType.Integer("timeout"); - addSyntax((sender, context) -> { + this.addSyntax((sender, context) -> { Player player = (Player) sender; if(player.getInstance() instanceof StatelessGame game) { game.setTimeLimit(context.get(timeout)); diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GamemodeCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GamemodeCommand.java index 68fe523..b0d5ce8 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GamemodeCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GamemodeCommand.java @@ -11,9 +11,9 @@ public class GamemodeCommand extends PrivilegedCommand { super("gamemode", "gm"); - addSyntax((sender, context) -> ((Player) sender).setGameMode( + this.addSyntax((sender, context) -> ((Player) sender).setGameMode( context.get("target")), - ArgumentType.Enum("target", GameMode.class).setFormat(ArgumentEnum.Format.LOWER_CASED) + ArgumentType.Enum("target", GameMode.class).setFormat(ArgumentEnum.Format.LOWER_CASED) ); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GcCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GcCommand.java index e332b8e..cbc5e4e 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GcCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/GcCommand.java @@ -1,17 +1,18 @@ package eu.mhsl.minenet.minigames.command.privileged; import eu.mhsl.minenet.minigames.command.PrivilegedCommand; -import eu.mhsl.minenet.minigames.message.type.ChatMessage; import eu.mhsl.minenet.minigames.message.Icon; +import eu.mhsl.minenet.minigames.message.type.ChatMessage; import eu.mhsl.minenet.minigames.util.Monitoring; public class GcCommand extends PrivilegedCommand { private static long lastRun = System.currentTimeMillis(); + public GcCommand() { super("gc"); - setDefaultExecutor((sender, context) -> { - long nextRun = (lastRun - (System.currentTimeMillis() - 30*1000)) / 1000; + this.setDefaultExecutor((sender, context) -> { + long nextRun = (lastRun - (System.currentTimeMillis() - 30 * 1000)) / 1000; if(nextRun > 0) { new ChatMessage(Icon.ERROR).appendStatic("Please wait ").appendStatic(String.valueOf(nextRun)).appendStatic(" seconds before running GC again!").send(sender); return; @@ -24,10 +25,10 @@ public class GcCommand extends PrivilegedCommand { long after = Monitoring.getRamUsage(); new ChatMessage(Icon.SUCCESS).appendStatic("Garbage collector ran successfully!").newLine() - .appendStatic("before: ").appendStatic(String.valueOf(before)).appendStatic("MB").newLine() - .appendStatic("now: ").appendStatic(String.valueOf(after)).appendStatic("MB").newLine() - .appendStatic("difference: ").appendStatic(String.valueOf(before-after)).appendStatic("MB") - .send(sender); + .appendStatic("before: ").appendStatic(String.valueOf(before)).appendStatic("MB").newLine() + .appendStatic("now: ").appendStatic(String.valueOf(after)).appendStatic("MB").newLine() + .appendStatic("difference: ").appendStatic(String.valueOf(before - after)).appendStatic("MB") + .send(sender); }); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/InstanceProxyMoveCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/InstanceProxyMoveCommand.java index 084760f..e403bf1 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/InstanceProxyMoveCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/InstanceProxyMoveCommand.java @@ -13,7 +13,7 @@ public class InstanceProxyMoveCommand extends PrivilegedCommand { ArgumentWord serverArgument = new ArgumentWord("server"); - addSyntax((sender, context) -> { + this.addSyntax((sender, context) -> { Instance room = ((Player) sender).getInstance(); room.getPlayers().forEach(player -> { Room.unsetRoom(player); diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/KickCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/KickCommand.java index 02f292c..3e9b7fb 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/KickCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/KickCommand.java @@ -11,17 +11,17 @@ public class KickCommand extends PrivilegedCommand { public KickCommand() { super("kick"); - addSyntax( + this.addSyntax( (sender, context) -> - kick(context.getRaw("player"), ""), - ArgumentType.Entity("player").onlyPlayers(true) + this.kick(context.getRaw("player"), ""), + ArgumentType.Entity("player").onlyPlayers(true) ); - addSyntax( + this.addSyntax( (sender, context) -> - kick(context.getRaw("player"), context.getRaw("reason")), - ArgumentType.Entity("player").onlyPlayers(true), - ArgumentType.String("reason") + this.kick(context.getRaw("player"), context.getRaw("reason")), + ArgumentType.Entity("player").onlyPlayers(true), + ArgumentType.String("reason") ); } diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/LangTestCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/LangTestCommand.java index e9f98ae..cf68687 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/LangTestCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/LangTestCommand.java @@ -1,8 +1,8 @@ package eu.mhsl.minenet.minigames.command.privileged; import eu.mhsl.minenet.minigames.command.PrivilegedCommand; -import eu.mhsl.minenet.minigames.lang.Languages; import eu.mhsl.minenet.minigames.lang.Lang; +import eu.mhsl.minenet.minigames.lang.Languages; import eu.mhsl.minenet.minigames.message.Icon; import eu.mhsl.minenet.minigames.message.TranslatableMessage; import eu.mhsl.minenet.minigames.message.type.ChatMessage; @@ -13,11 +13,11 @@ public class LangTestCommand extends PrivilegedCommand { public LangTestCommand() { super("langtest"); - setDefaultExecutor((sender, context) -> sendMessage(Languages.getInstance().getLanguage((Player) sender), "sample").send(sender)); + this.setDefaultExecutor((sender, context) -> this.sendMessage(Languages.getInstance().getLanguage((Player) sender), "sample").send(sender)); var targetString = ArgumentType.String("mapId"); - addSyntax((sender, context) -> sendMessage(Languages.getInstance().getLanguage((Player) sender), context.get("mapId")).send(sender), targetString); + this.addSyntax((sender, context) -> this.sendMessage(Languages.getInstance().getLanguage((Player) sender), context.get("mapId")).send(sender), targetString); } private TranslatableMessage sendMessage(Lang lang, String mapId) { diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/OpCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/OpCommand.java index aebc0d7..1f1b1e9 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/OpCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/OpCommand.java @@ -11,7 +11,7 @@ public class OpCommand extends PrivilegedCommand { public OpCommand() { super("op"); - addSyntax((sender, context) -> { + this.addSyntax((sender, context) -> { Player target = MinecraftServer.getConnectionManager().getOnlinePlayerByUsername(context.getRaw("target")); if(target != null) { target.setPermissionLevel(4); diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/PlayerLimitCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/PlayerLimitCommand.java index 7b50a72..ef93043 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/PlayerLimitCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/PlayerLimitCommand.java @@ -10,8 +10,9 @@ public class PlayerLimitCommand extends PrivilegedCommand { ArgumentInteger count = ArgumentType.Integer("count"); - addSyntax((sender, context) -> { - System.setProperty("minenet.playerlimit", String.valueOf(context.get(count))); - }, count); + this.addSyntax((sender, context) -> System.setProperty( + "minenet.playerlimit", + String.valueOf(context.get(count)) + ), count); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/PublishRewardCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/PublishRewardCommand.java index 3313581..75b7604 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/PublishRewardCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/PublishRewardCommand.java @@ -13,10 +13,11 @@ import java.net.http.HttpResponse; public class PublishRewardCommand extends PrivilegedCommand { private final HttpClient rewardPublishClient = HttpClient.newHttpClient(); + public PublishRewardCommand() { super("publishReward"); - setDefaultExecutor((sender, context) -> { + this.setDefaultExecutor((sender, context) -> { try { Room room = Room.getRoom((Player) sender).orElseThrow(); TournamentDisplay world = new TournamentDisplay(room.getTournament()); @@ -24,14 +25,14 @@ public class PublishRewardCommand extends PrivilegedCommand { String rewardRequestJson = new Gson().toJson(room.getTournament().getRewards()); HttpRequest giveRewardsRequest = HttpRequest.newBuilder() - .uri(new URI("http://10.20.7.1:8080/api/event/reward")) - .POST(HttpRequest.BodyPublishers.ofString(rewardRequestJson)) - .build(); + .uri(new URI("http://10.20.9.3:8080/api/event/reward")) + .POST(HttpRequest.BodyPublishers.ofString(rewardRequestJson)) + .build(); room.getTournament().getRewards(); - HttpResponse rawResponse = rewardPublishClient.send(giveRewardsRequest, HttpResponse.BodyHandlers.discarding()); + HttpResponse rawResponse = this.rewardPublishClient.send(giveRewardsRequest, HttpResponse.BodyHandlers.discarding()); sender.sendMessage(String.format("Rewards published: HTTP %s", rawResponse.statusCode())); - } catch (Exception e) { + } catch(Exception e) { sender.sendMessage(e.getMessage()); } }); diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/RefreshCommandsCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/RefreshCommandsCommand.java index d0e5b36..4d9a8b8 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/RefreshCommandsCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/RefreshCommandsCommand.java @@ -1,8 +1,8 @@ package eu.mhsl.minenet.minigames.command.privileged; import eu.mhsl.minenet.minigames.command.PrivilegedCommand; -import eu.mhsl.minenet.minigames.message.type.ChatMessage; import eu.mhsl.minenet.minigames.message.Icon; +import eu.mhsl.minenet.minigames.message.type.ChatMessage; import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Player; @@ -10,7 +10,7 @@ public class RefreshCommandsCommand extends PrivilegedCommand { public RefreshCommandsCommand() { super("refreshCommands"); - setDefaultExecutor((sender, context) -> { + this.setDefaultExecutor((sender, context) -> { MinecraftServer.getConnectionManager().getOnlinePlayers().forEach(Player::refreshCommands); new ChatMessage(Icon.SUCCESS).appendStatic("Updated command syntax!").send(sender); }); diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/RoomCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/RoomCommand.java index e9a5cfe..242fcc1 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/RoomCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/RoomCommand.java @@ -1,10 +1,10 @@ package eu.mhsl.minenet.minigames.command.privileged; import eu.mhsl.minenet.minigames.command.PrivilegedCommand; +import eu.mhsl.minenet.minigames.instance.room.Room; import eu.mhsl.minenet.minigames.message.Icon; import eu.mhsl.minenet.minigames.message.TranslatableMessage; import eu.mhsl.minenet.minigames.message.type.ChatMessage; -import eu.mhsl.minenet.minigames.instance.room.Room; import net.minestom.server.entity.Player; import java.util.stream.Collectors; @@ -13,14 +13,14 @@ public class RoomCommand extends PrivilegedCommand { public RoomCommand() { super("room"); - setDefaultExecutor((sender, context) -> { + this.setDefaultExecutor((sender, context) -> { TranslatableMessage out = new ChatMessage(Icon.SCIENCE).appendStatic("Rooms:").newLine(); Room.getAllRooms().forEach((roomInstance) -> out - .newLine() - .appendStatic("Owner: ").appendStatic(roomInstance.getOwner() != null ? roomInstance.getOwner().getUsername() : "none").newLine() - .appendStatic("Players: ").appendStatic(String.valueOf(roomInstance.getAllMembers().size())).newLine() - .list(roomInstance.getAllMembers().stream().map(Player::getUsername).collect(Collectors.toList())).newLine()); + .newLine() + .appendStatic("Owner: ").appendStatic(roomInstance.getOwner() != null ? roomInstance.getOwner().getUsername() : "none").newLine() + .appendStatic("Players: ").appendStatic(String.valueOf(roomInstance.getAllMembers().size())).newLine() + .list(roomInstance.getAllMembers().stream().map(Player::getUsername).collect(Collectors.toList())).newLine()); out.send(sender); }); diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetMemorialCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetMemorialCommand.java index c18dd63..2592123 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetMemorialCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetMemorialCommand.java @@ -20,37 +20,37 @@ public class SetMemorialCommand extends PrivilegedCommand { ArgumentString titleArgument = ArgumentType.String("title"); ArgumentString loreArgument = ArgumentType.String("lore"); - materialArgument.setSuggestionCallback((sender, context, suggestion) -> { - Material - .values() - .stream() - .map(material -> new SuggestionEntry(material.name(), Component.text(material.name()))) - .forEach(suggestion::addEntry); - }); + materialArgument.setSuggestionCallback( + (sender, context, suggestion) -> Material + .values() + .stream() + .map(material -> new SuggestionEntry(material.name(), Component.text(material.name()))) + .forEach(suggestion::addEntry) + ); - addSyntax((sender, context) -> { + this.addSyntax((sender, context) -> { Room - .getRoom((Player) sender) - .orElseThrow() - .getTournament() - .setMemorialConfiguration( - new MemorialConfiguration( - Material.fromNamespaceId(context.get(materialArgument)), - context.get(titleArgument), - context.get(loreArgument) - ) - ); + .getRoom((Player) sender) + .orElseThrow() + .getTournament() + .setMemorialConfiguration( + new MemorialConfiguration( + Material.fromKey(context.get(materialArgument)), + context.get(titleArgument), + context.get(loreArgument) + ) + ); sender.sendMessage( - Component.text() - .append(Component.text("Memorial gesetzt:", NamedTextColor.GOLD)) - .appendNewline() - .append(Component.text(context.get(materialArgument), NamedTextColor.AQUA)) - .appendNewline() - .append(Component.text(context.get(titleArgument), NamedTextColor.RED)) - .appendNewline() - .append(Component.text(context.get(loreArgument), NamedTextColor.DARK_RED)) - .build() + Component.text() + .append(Component.text("Memorial gesetzt:", NamedTextColor.GOLD)) + .appendNewline() + .append(Component.text(context.get(materialArgument), NamedTextColor.AQUA)) + .appendNewline() + .append(Component.text(context.get(titleArgument), NamedTextColor.RED)) + .appendNewline() + .append(Component.text(context.get(loreArgument), NamedTextColor.DARK_RED)) + .build() ); }, materialArgument, titleArgument, loreArgument); } diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetRewardCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetRewardCommand.java index 659a610..52e2396 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetRewardCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetRewardCommand.java @@ -22,34 +22,34 @@ public class SetRewardCommand extends PrivilegedCommand { ArgumentWord materialArgument = ArgumentType.Word("material"); ArgumentStringArray amountsArgument = ArgumentType.StringArray("amount"); - materialArgument.setSuggestionCallback((sender, context, suggestion) -> { - Material - .values() - .stream() - .map(material -> new SuggestionEntry(material.name(), Component.text(material.name()))) - .forEach(suggestion::addEntry); - }); + materialArgument.setSuggestionCallback( + (sender, context, suggestion) -> Material + .values() + .stream() + .map(material -> new SuggestionEntry(material.name(), Component.text(material.name()))) + .forEach(suggestion::addEntry) + ); - addSyntax((sender, context) -> { + this.addSyntax((sender, context) -> { Room - .getRoom((Player) sender) - .orElseThrow() - .getTournament() - .setRewardConfiguration( - new RewardConfiguration( - Material.fromNamespaceId(context.get(materialArgument)), - Arrays.stream(context.get(amountsArgument)).map(Integer::valueOf).collect(Collectors.toList()) - ) - ); + .getRoom((Player) sender) + .orElseThrow() + .getTournament() + .setRewardConfiguration( + new RewardConfiguration( + Material.fromKey(context.get(materialArgument)), + Arrays.stream(context.get(amountsArgument)).map(Integer::valueOf).collect(Collectors.toList()) + ) + ); sender.sendMessage( - Component.text() - .append(Component.text("Belohnung gesetzt:", NamedTextColor.GOLD)) - .appendNewline() - .append(Component.text(context.get(materialArgument), NamedTextColor.AQUA)) - .appendNewline() - .append(Component.text(String.join(", ", context.get(amountsArgument)), NamedTextColor.RED)) - .build() + Component.text() + .append(Component.text("Belohnung gesetzt:", NamedTextColor.GOLD)) + .appendNewline() + .append(Component.text(context.get(materialArgument), NamedTextColor.AQUA)) + .appendNewline() + .append(Component.text(String.join(", ", context.get(amountsArgument)), NamedTextColor.RED)) + .build() ); }, materialArgument, amountsArgument); } diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetRoomOwnerCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetRoomOwnerCommand.java index 6a83853..b95d0ff 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetRoomOwnerCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SetRoomOwnerCommand.java @@ -15,21 +15,21 @@ public class SetRoomOwnerCommand extends PrivilegedCommand { public SetRoomOwnerCommand() { super("setRoomOwner"); - addCondition((sender, commandString) -> ((Player) sender).getInstance() instanceof Room); + this.addCondition((sender, commandString) -> ((Player) sender).getInstance() instanceof Room); - setDefaultExecutor((sender, context) -> { + this.setDefaultExecutor((sender, context) -> { if(sender instanceof Player p) { Room.getRoom(p).orElseThrow().setOwner(p); - new ChatMessage(Icon.SUCCESS).appendStatic("You are now the owner of this room!").send(sender); + new ChatMessage(Icon.SUCCESS).appendTranslated("room#ownerSelf").send(sender); } }); - addSyntax((sender, context) -> { + this.addSyntax((sender, context) -> { System.out.println("Test"); if(sender instanceof Player p) { Player newOwner = MinecraftServer.getConnectionManager().getOnlinePlayerByUsername(context.getRaw("player")); Room.getRoom(p).orElseThrow().setOwner(Objects.requireNonNull(newOwner)); - new ChatMessage(Icon.SUCCESS).appendStatic("The new owner has been set!").send(sender); + new ChatMessage(Icon.SUCCESS).appendTranslated("room#ownerSet").send(sender); } }, ArgumentType.Entity("player").onlyPlayers(true)); } diff --git a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SkinCommand.java b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SkinCommand.java index 4de09dc..feca11f 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SkinCommand.java +++ b/src/main/java/eu/mhsl/minenet/minigames/command/privileged/SkinCommand.java @@ -9,7 +9,7 @@ public class SkinCommand extends PrivilegedCommand { public SkinCommand() { super("skin"); - addSyntax((sender, context) -> { + this.addSyntax((sender, context) -> { if(sender instanceof Player p) { p.setSkin(PlayerSkin.fromUsername(context.getRaw("target"))); } diff --git a/src/main/java/eu/mhsl/minenet/minigames/handler/Listeners.java b/src/main/java/eu/mhsl/minenet/minigames/handler/Listeners.java index 72b17c0..5590cc2 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/handler/Listeners.java +++ b/src/main/java/eu/mhsl/minenet/minigames/handler/Listeners.java @@ -1,13 +1,19 @@ package eu.mhsl.minenet.minigames.handler; -import eu.mhsl.minenet.minigames.handler.global.*; +import eu.mhsl.minenet.minigames.handler.global.AddEntityToInstanceEventListener; +import eu.mhsl.minenet.minigames.handler.global.ChatFormatHandler; +import eu.mhsl.minenet.minigames.handler.global.PlayerFreezeHandler; +import eu.mhsl.minenet.minigames.handler.global.PlayerLeaveHandler; +import eu.mhsl.minenet.minigames.handler.global.PlayerLoginHandler; import net.minestom.server.MinecraftServer; import net.minestom.server.event.EventListener; public enum Listeners { SPAWN(new AddEntityToInstanceEventListener()), LOGIN(new PlayerLoginHandler()), - LEAVE(new PlayerLeaveHandler()); + LEAVE(new PlayerLeaveHandler()), + FREEZE(new PlayerFreezeHandler()), + CHAT(new ChatFormatHandler()); Listeners(EventListener event) { MinecraftServer.getGlobalEventHandler().addListener(event); diff --git a/src/main/java/eu/mhsl/minenet/minigames/handler/global/ChatFormatHandler.java b/src/main/java/eu/mhsl/minenet/minigames/handler/global/ChatFormatHandler.java new file mode 100644 index 0000000..72c251a --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/handler/global/ChatFormatHandler.java @@ -0,0 +1,37 @@ +package eu.mhsl.minenet.minigames.handler.global; + +import eu.mhsl.minenet.minigames.Main; +import eu.mhsl.minenet.minigames.message.Icon; +import eu.mhsl.minenet.minigames.message.type.ChatMessage; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.event.EventListener; +import net.minestom.server.event.player.PlayerChatEvent; +import org.jetbrains.annotations.NotNull; + +public class ChatFormatHandler implements EventListener { + // Verteilt der Proxy den Chat (chat-bridge bei CraftAttack), darf er hier nicht noch einmal erscheinen + private final boolean chatViaProxy = Main.globalConfig.node("velocity", "chat-via-proxy").getBoolean(false); + + @Override + public @NotNull Class eventType() { + return PlayerChatEvent.class; + } + + @Override + public @NotNull Result run(@NotNull PlayerChatEvent event) { + if(this.chatViaProxy) { + event.setCancelled(true); + return Result.SUCCESS; + } + NamedTextColor color = event.getPlayer().getPermissionLevel() > 0 ? NamedTextColor.AQUA : NamedTextColor.GRAY; + event.setFormattedMessage( + new ChatMessage(Icon.CHAT) + .appendStatic(Component.text(event.getPlayer().getUsername(), color)) + .appendStatic(Component.text(" > ", NamedTextColor.DARK_GRAY)) + .appendStatic(event.getRawMessage()) + .build(event.getPlayer()) + ); + return Result.SUCCESS; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerFreezeHandler.java b/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerFreezeHandler.java new file mode 100644 index 0000000..8845d89 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerFreezeHandler.java @@ -0,0 +1,43 @@ +package eu.mhsl.minenet.minigames.handler.global; + +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Player; +import net.minestom.server.event.EventListener; +import net.minestom.server.event.player.PlayerTickEvent; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; + +/** + * Weder Minestom noch MinestomPvP verwalten die TICKS_FROZEN-Metadata. Der 26.x-Client baut das + * Freeze-Overlay bei Powder-Snow-Kontakt zwar selbst auf, der Abbau ist aber server-authoritative — + * ohne diese Vanilla-Logik bleibt das Overlay dauerhaft haengen (auch ueber Instanzwechsel hinweg). + */ +public class PlayerFreezeHandler implements EventListener { + private static final int TICKS_REQUIRED_TO_FREEZE = 140; + + @Override + public @NotNull Class eventType() { + return PlayerTickEvent.class; + } + + @Override + public @NotNull Result run(@NotNull PlayerTickEvent event) { + Player p = event.getPlayer(); + Instance instance = p.getInstance(); + if(instance == null) return Result.SUCCESS; + + Pos pos = p.getPosition(); + boolean inPowderSnow = instance.getBlock(pos).compare(Block.POWDER_SNOW) + || instance.getBlock(pos.add(0, 1, 0)).compare(Block.POWDER_SNOW); + + int ticksFrozen = p.getEntityMeta().getTickFrozen(); + if(inPowderSnow) { + p.getEntityMeta().setTickFrozen(Math.min(TICKS_REQUIRED_TO_FREEZE, ticksFrozen + 1)); + } else if(ticksFrozen > 0) { + p.getEntityMeta().setTickFrozen(Math.max(0, ticksFrozen - 2)); + } + + return Result.SUCCESS; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLeaveHandler.java b/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLeaveHandler.java index fcfc4bb..afa771e 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLeaveHandler.java +++ b/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLeaveHandler.java @@ -1,5 +1,9 @@ package eu.mhsl.minenet.minigames.handler.global; +import eu.mhsl.minenet.minigames.message.Icon; +import eu.mhsl.minenet.minigames.message.type.ChatMessage; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Player; import net.minestom.server.event.EventListener; import net.minestom.server.event.player.PlayerDisconnectEvent; @@ -14,7 +18,11 @@ public class PlayerLeaveHandler implements EventListener @Override public @NotNull Result run(@NotNull PlayerDisconnectEvent event) { Player p = event.getPlayer(); -// new ChatMessage(Icon.SCIENCE).appendStatic("unübersetzter Leavetext: ").appendStatic(p.getDisplayName()).send(MinecraftServer.getConnectionManager().getOnlinePlayers()); + new ChatMessage(Icon.LEAVE) + .appendStatic(p.getName().color(NamedTextColor.GRAY)) + .appendSpace() + .appendTranslated("common#leave", NamedTextColor.DARK_GRAY) + .send(MinecraftServer.getConnectionManager().getOnlinePlayers()); return Result.SUCCESS; } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLoginHandler.java b/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLoginHandler.java index b694668..02d7725 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLoginHandler.java +++ b/src/main/java/eu/mhsl/minenet/minigames/handler/global/PlayerLoginHandler.java @@ -2,13 +2,13 @@ package eu.mhsl.minenet.minigames.handler.global; import eu.mhsl.minenet.minigames.Main; import eu.mhsl.minenet.minigames.api.QueuedPlayerRooms; +import eu.mhsl.minenet.minigames.instance.hub.Hub; import eu.mhsl.minenet.minigames.instance.room.Room; import eu.mhsl.minenet.minigames.instance.transfer.Transfer; import eu.mhsl.minenet.minigames.skin.SkinCache; import eu.mhsl.minenet.minigames.util.MoveInstance; import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Player; -import eu.mhsl.minenet.minigames.instance.hub.Hub; import net.minestom.server.event.EventListener; import net.minestom.server.event.player.AsyncPlayerConfigurationEvent; import net.minestom.server.network.packet.server.play.TeamsPacket; @@ -61,12 +61,11 @@ public class PlayerLoginHandler implements EventListener s.equalsIgnoreCase(p.getUsername()))) { p.setPermissionLevel(4); } - } catch (SerializationException | NullPointerException ignored) {} + } catch(SerializationException | NullPointerException ignored) { + } Logger.getLogger("user").info(p.getUsername() + " joined"); -// new ChatMessage(Icon.SCIENCE).appendStatic("unübersetzter Jointext: ").appendStatic(p.getUsername()).send(MinecraftServer.getConnectionManager().getOnlinePlayers()); - return Result.SUCCESS; } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/Dimension.java b/src/main/java/eu/mhsl/minenet/minigames/instance/Dimension.java index f1d7825..b755bdc 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/Dimension.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/Dimension.java @@ -1,48 +1,85 @@ package eu.mhsl.minenet.minigames.instance; +import net.kyori.adventure.key.Key; import net.minestom.server.MinecraftServer; -import net.minestom.server.registry.DynamicRegistry; -import net.minestom.server.utils.NamespaceID; +import net.minestom.server.registry.RegistryKey; +import net.minestom.server.registry.RegistryTag; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; import net.minestom.server.world.DimensionType; +import net.minestom.server.world.attribute.EnvironmentAttribute; +import net.minestom.server.world.clock.WorldClock; +import net.minestom.server.world.timeline.Timeline; /** * Prebuilt dimensions */ public enum Dimension { OVERWORLD( - NamespaceID.from("minenet:fullbright_overworld"), - DimensionType - .builder() - .ambientLight(2.0f) - .build() + Key.key("minenet:fullbright_overworld"), + DimensionType + .builder() + // Fullbright: seit MC 26.1 laeuft die Grundhelligkeit ueber das Environment-Attribut + // AMBIENT_LIGHT_COLOR (Vanilla-Overworld: #0A0A0A); der alte ambientLight-2.0-Hack + // liegt ausserhalb der validen Range und bricht das Client-Rendering + .setAttribute(EnvironmentAttribute.AMBIENT_LIGHT_COLOR, NamedTextColor.WHITE) + // defaultClock noetig, damit Instance#setTime/#defaultClock funktionieren (sonst null); Timelines wie Vanilla-Overworld + .defaultClock(WorldClock.OVERWORLD) + .timelines(RegistryTag.direct(Timeline.DAY, Timeline.MOON, Timeline.EARLY_GAME, Timeline.VILLAGER_SCHEDULE)) + .build() ), NETHER( - NamespaceID.from("minenet:fullbright_nether"), - DimensionType - .builder() - .ambientLight(2.0f) - .effects("minecraft:the_nether") - .build() + Key.key("minenet:fullbright_nether"), + DimensionType + .builder() + // Fullbright: seit MC 26.1 laeuft die Grundhelligkeit ueber das Environment-Attribut + // AMBIENT_LIGHT_COLOR (Vanilla-Overworld: #0A0A0A); der alte ambientLight-2.0-Hack + // liegt ausserhalb der validen Range und bricht das Client-Rendering + .setAttribute(EnvironmentAttribute.AMBIENT_LIGHT_COLOR, NamedTextColor.WHITE) + .skybox(DimensionType.Skybox.NONE) + .defaultClock(WorldClock.OVERWORLD) + .timelines(RegistryTag.direct(Timeline.VILLAGER_SCHEDULE)) + .build() ), THE_END( - NamespaceID.from("minenet:fullbright_end"), - DimensionType - .builder() - .ambientLight(2.0f) - .effects("minecraft:the_end") - .build() + Key.key("minenet:fullbright_end"), + DimensionType + .builder() + // Fullbright: seit MC 26.1 laeuft die Grundhelligkeit ueber das Environment-Attribut + // AMBIENT_LIGHT_COLOR (Vanilla-Overworld: #0A0A0A); der alte ambientLight-2.0-Hack + // liegt ausserhalb der validen Range und bricht das Client-Rendering + .setAttribute(EnvironmentAttribute.AMBIENT_LIGHT_COLOR, NamedTextColor.WHITE) + .skybox(DimensionType.Skybox.END) + .defaultClock(WorldClock.THE_END) + .timelines(RegistryTag.direct(Timeline.VILLAGER_SCHEDULE)) + .build() + ), + + BACKROOMS( + Key.key("minenet:backrooms"), + DimensionType + .builder() + // Sehr dunkles Grundlicht (knapp ueber Vanilla-Overworld #0A0A0A): die eigentliche + // Helligkeit kommt aus echten Lichtquellen via LightingChunk — Blackout-Biome + // sind dadurch tatsaechlich dunkel, mit weichen Uebergaengen am Zonenrand + .setAttribute(EnvironmentAttribute.AMBIENT_LIGHT_COLOR, TextColor.color(0x16160F)) + .skybox(DimensionType.Skybox.NONE) + .defaultClock(WorldClock.OVERWORLD) + .timelines(RegistryTag.direct(Timeline.VILLAGER_SCHEDULE)) + .build() ); public final DimensionType DIMENSION; - public final NamespaceID namespaceID; - public final DynamicRegistry.Key key; - Dimension(NamespaceID namespaceID, DimensionType dimType) { + public final Key namespaceID; + public final RegistryKey key; + + Dimension(Key namespaceID, DimensionType dimType) { this.DIMENSION = dimType; this.namespaceID = namespaceID; - this.key = MinecraftServer.getDimensionTypeRegistry().register(namespaceID, DIMENSION); + this.key = MinecraftServer.getDimensionTypeRegistry().register(namespaceID, this.DIMENSION); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/MineNetInstance.java b/src/main/java/eu/mhsl/minenet/minigames/instance/MineNetInstance.java index 051cfcd..7900dad 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/MineNetInstance.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/MineNetInstance.java @@ -1,27 +1,34 @@ package eu.mhsl.minenet.minigames.instance; import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import net.kyori.adventure.key.Key; import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Player; import net.minestom.server.event.instance.AddEntityToInstanceEvent; import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent; import net.minestom.server.instance.Instance; import net.minestom.server.instance.InstanceContainer; -import net.minestom.server.instance.InstanceManager; -import net.minestom.server.registry.DynamicRegistry; +import net.minestom.server.registry.RegistryKey; import net.minestom.server.timer.TaskSchedule; import net.minestom.server.world.DimensionType; import java.util.UUID; public class MineNetInstance extends InstanceContainer { - public MineNetInstance(DynamicRegistry.Key type) { - super(UUID.randomUUID(), type); + public MineNetInstance(RegistryKey type) { + this(UUID.randomUUID(), type); + } + + // Eindeutiger Dimension-Name pro Instanz: Minestom entscheidet anhand dieses Namens (nicht des + // DimensionTypes), ob beim setInstance das volle Respawn-Protokoll gefahren wird. Ohne den Reset + // rendert der 26.x-Client nach einem Wechsel zwischen Instanzen gleicher Dimension keine Chunks. + private MineNetInstance(UUID uuid, RegistryKey type) { + super(uuid, type, Key.key("minenet", "instance-" + uuid)); MinecraftServer.getInstanceManager().registerInstance(this); - eventNode() - .addListener(AddEntityToInstanceEvent.class, this::onEntityAdd) - .addListener(RemoveEntityFromInstanceEvent.class, this::onEntityRemove); + this.eventNode() + .addListener(AddEntityToInstanceEvent.class, this::onEntityAdd) + .addListener(RemoveEntityFromInstanceEvent.class, this::onEntityRemove); } private void onEntityRemove(RemoveEntityFromInstanceEvent removeEntityFromInstanceEvent) { @@ -38,6 +45,7 @@ public class MineNetInstance extends InstanceContainer { /** * Called when Player joins this instance + * * @param p player who is joining * @return setCanceled */ @@ -47,18 +55,15 @@ public class MineNetInstance extends InstanceContainer { /** * Called when Player leaves this instance + * * @param p player who is leaving */ protected void onPlayerLeave(Player p) { } - /** - * - * @param target - */ public void destroy(Instance target) { - getPlayers().forEach(player -> { + this.getPlayers().forEach(player -> { if(target != null) player.setInstance(target); else @@ -66,9 +71,9 @@ public class MineNetInstance extends InstanceContainer { }); MinecraftServer.getSchedulerManager().scheduleTask( - () -> MinecraftServer.getInstanceManager().unregisterInstance(this), - TaskSchedule.seconds(10), - TaskSchedule.stop() + () -> MinecraftServer.getInstanceManager().unregisterInstance(this), + TaskSchedule.seconds(10), + TaskSchedule.stop() ); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/Game.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/Game.java index 9133eb8..eb62945 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/Game.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/Game.java @@ -1,14 +1,14 @@ package eu.mhsl.minenet.minigames.instance.game; import eu.mhsl.minenet.minigames.instance.MineNetInstance; +import eu.mhsl.minenet.minigames.instance.Spawnable; import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.room.Room; import eu.mhsl.minenet.minigames.message.Icon; import eu.mhsl.minenet.minigames.message.type.ChatMessage; import eu.mhsl.minenet.minigames.score.Score; import eu.mhsl.minenet.minigames.util.CommonEventHandles; -import eu.mhsl.minenet.minigames.instance.Spawnable; -import eu.mhsl.minenet.minigames.instance.room.Room; import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Player; @@ -16,7 +16,7 @@ import net.minestom.server.event.item.ItemDropEvent; import net.minestom.server.event.player.PlayerBlockBreakEvent; import net.minestom.server.event.player.PlayerBlockPlaceEvent; import net.minestom.server.event.player.PlayerMoveEvent; -import net.minestom.server.registry.DynamicRegistry; +import net.minestom.server.registry.RegistryKey; import net.minestom.server.timer.ExecutionType; import net.minestom.server.timer.TaskSchedule; import net.minestom.server.world.DimensionType; @@ -28,64 +28,56 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Logger; public abstract class Game extends MineNetInstance implements Spawnable { - + protected final Random rnd = new Random(); + protected final Logger logger; protected Room parentRoom; protected boolean isRunning = false; protected boolean isBeforeBeginning = true; - protected final Random rnd = new Random(); //TODO better way than ths? - - protected final Logger logger; - - public Game(DynamicRegistry.Key dimensionType) { + public Game(RegistryKey dimensionType) { super(dimensionType); MinecraftServer.getInstanceManager().registerInstance(this); - logger = Logger.getLogger("Game:" + getUuid()); + this.logger = Logger.getLogger("Game:" + this.getUuid()); - eventNode() + this.eventNode() .addListener(PlayerMoveEvent.class, this::onPlayerMove) .addListener(PlayerBlockBreakEvent.class, this::onBlockBreak) .addListener(PlayerBlockPlaceEvent.class, this::onBlockPlace) .addListener(ItemDropEvent.class, this::onItemDrop); } + public static void initialize(GameFactory factory, List> options, Player owner) { + try { + Room originRoom = Room.getRoom(owner).orElseThrow(); + Game game = factory.manufacture(originRoom, options); + game.load(); + originRoom.moveMembersToInstance(game); + + new ChatMessage(Icon.INFO) + .appendTranslated(factory.name()) + .newLine() + .appendTranslated(factory.description()) + .send(originRoom.getAllMembers()); + + } catch(Exception e) { + new ChatMessage(Icon.ERROR).appendStatic("Instance crashed: " + e.getMessage()).send(owner); + MinecraftServer.getSchedulerManager().scheduleNextTick(() -> Room.getRoom(owner).orElseThrow().moveMembersToRoomLobby()); + e.printStackTrace(System.err); + } + } + public Game setParent(Room parentRoom) { this.parentRoom = parentRoom; return this; } - public static void initialize(GameFactory factory, List> options, Player owner) { - try { - - Game game = factory.manufacture(Room.getRoom(owner).orElseThrow(), options); - game.load(); - Room.getRoom(owner).orElseThrow().moveMembersToInstance(game); - - MinecraftServer.getSchedulerManager().scheduleTask(() -> { - game.getPlayers().forEach(player -> new ChatMessage(Icon.SCIENCE) - .appendStatic(factory.name().getAssembled(player).asComponent()) - .newLine() - .appendStatic(factory.description().getAssembled(player).asComponent()) - .send(player)); - - return TaskSchedule.stop(); - }, TaskSchedule.seconds(3)); - - - } catch (Exception e) { - new ChatMessage(Icon.ERROR).appendStatic("Instance crashed: " + e.getMessage()).send(owner); - MinecraftServer.getSchedulerManager().scheduleNextTick(() -> Room.getRoom(owner).orElseThrow().moveMembersToRoomLobby()); - e.printStackTrace(System.err); - } - } - /** * Load and start countdown */ public void load() { - scheduler().submitTask(() -> { + this.scheduler().submitTask(() -> { CompletableFuture callback = new CompletableFuture<>(); this.onLoad(callback); // callback.whenComplete((unused, throwable) -> this.start()); @@ -95,13 +87,13 @@ public abstract class Game extends MineNetInstance implements Spawnable { } protected void start() { - isRunning = true; - isBeforeBeginning = false; + this.isRunning = true; + this.isBeforeBeginning = false; this.onStart(); } public void stop() { - isRunning = false; + this.isRunning = false; this.onStop(); this.unload(); } @@ -109,12 +101,12 @@ public abstract class Game extends MineNetInstance implements Spawnable { public void unload() { this.onUnload(); - getPlayers().forEach(Room::setOwnRoom); + this.getPlayers().forEach(Room::setOwnRoom); - scheduler().scheduleTask(() -> { + this.scheduler().scheduleTask(() -> { - logger.info("stopping game instance " + this.uuid); - getPlayers().forEach(player -> player.kick("timeout")); + this.logger.info("stopping game instance " + this.uuid); + this.getPlayers().forEach(player -> player.kick("timeout")); MinecraftServer.getInstanceManager().unregisterInstance(this); @@ -125,9 +117,14 @@ public abstract class Game extends MineNetInstance implements Spawnable { callback.complete(null); } - protected void onStart() {} - protected void onStop() {} - protected void onUnload() {} + protected void onStart() { + } + + protected void onStop() { + } + + protected void onUnload() { + } protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { @@ -151,7 +148,7 @@ public abstract class Game extends MineNetInstance implements Spawnable { } protected void checkAbandoned() { - scheduleNextTick((instance) -> { + this.scheduleNextTick((instance) -> { if(instance.getPlayers().isEmpty()) this.unload(); }); } @@ -161,10 +158,10 @@ public abstract class Game extends MineNetInstance implements Spawnable { } public boolean isRunning() { - return isRunning; + return this.isRunning; } public Pos getSpawn() { - return new Pos(0,50,0); + return new Pos(0, 50, 0); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/GameList.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/GameList.java index 0cf3f47..4094fa8 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/GameList.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/GameList.java @@ -1,21 +1,34 @@ package eu.mhsl.minenet.minigames.instance.game; import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.acidRain.AcidRainFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.anvilRun.AnvilRunFactory; -import eu.mhsl.minenet.minigames.instance.game.stateless.types.bowSpleef.BowSpleefFactory; -import eu.mhsl.minenet.minigames.instance.game.stateless.types.elytraRace.ElytraRaceFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.bedwars.BedwarsFactory; -import eu.mhsl.minenet.minigames.instance.game.stateless.types.acidRain.AcidRainFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBattle.BlockBattleFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBreakRace.BlockBreakRaceFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.boatRace.BoatRaceFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.bowSpleef.BowSpleefFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.buildBattle.BuildBattleFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.colorJump.ColorJumpFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.deathcube.DeathcubeFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.elytraRace.ElytraRaceFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.fastbridge.FastbridgeFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.highGround.HighGroundFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.jumpDive.JumpDiveFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.minerun.MinerunFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArenaFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.pillars.PillarsFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.PlayerRaceFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.spaceSnake.SpaceSnakeFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.spleef.SpleefFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.stickfight.StickFightFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.sumo.SumoFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.TetrisFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.tntrun.TntRunFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.TowerdefenseFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.trafficlightrace.TrafficLightRaceFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.turtleGame.TurtleGameFactory; public enum GameList { DEATHCUBE(new DeathcubeFactory(), GameType.JUMPNRUN), @@ -24,27 +37,42 @@ public enum GameList { STICKFIGHT(new StickFightFactory(), GameType.PROTOTYPE), TOWERDEFENSE(new TowerdefenseFactory(), GameType.PROTOTYPE), BEDWARS(new BedwarsFactory(), GameType.PROTOTYPE), - BACKROOMS(new BackroomsFactory(), GameType.PROTOTYPE), + BACKROOMS(new BackroomsFactory(), GameType.PVE), BOWSPLEEF(new BowSpleefFactory(), GameType.PROTOTYPE), TETRIS(new TetrisFactory(), GameType.OTHER), TNTRUN(new TntRunFactory(), GameType.OTHER), ANVILRUN(new AnvilRunFactory(), GameType.PVE), ACIDRAIN(new AcidRainFactory(), GameType.PVE), + TURTLEGAME(new TurtleGameFactory(), GameType.PVE), ELYTRARACE(new ElytraRaceFactory(), GameType.PVP), SPLEEF(new SpleefFactory(), GameType.PVP), - JUMPDIVE(new JumpDiveFactory(), GameType.JUMPNRUN); + JUMPDIVE(new JumpDiveFactory(), GameType.JUMPNRUN), + SUMO(new SumoFactory(), GameType.PVP), + HIGHGROUND(new HighGroundFactory(), GameType.PVP), + FASTBRIDGE(new FastbridgeFactory(), GameType.OTHER), + BLOCKBREAKRACE(new BlockBreakRaceFactory(), GameType.OTHER), + SPACESNAKE(new SpaceSnakeFactory(), GameType.PVP), + BOATRACE(new BoatRaceFactory(), GameType.OTHER), + PILLARS(new PillarsFactory(), GameType.PROTOTYPE), + BLOCKBATTLE(new BlockBattleFactory(), GameType.PVP), + COLORJUMP(new ColorJumpFactory(), GameType.JUMPNRUN), + PLAYERRACE(new PlayerRaceFactory(), GameType.JUMPNRUN), + BUILDBATTLE(new BuildBattleFactory(), GameType.PROTOTYPE), + MOBARENA(new MobArenaFactory(), GameType.PVE); private final GameFactory factory; private final GameType type; + GameList(GameFactory factory, GameType type) { this.factory = factory; this.type = type; } + public GameFactory getFactory() { return this.factory; } public GameType getType() { - return type; + return this.type; } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/GameType.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/GameType.java index ea2d329..71bffb0 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/GameType.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/GameType.java @@ -22,15 +22,15 @@ public enum GameType { } public Material getIcon() { - return icon; + return this.icon; } public TranslatedComponent getTitle() { - return title; + return this.title; } public TranslatedComponent getDescription() { - return description; + return this.description; } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/StatelessGame.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/StatelessGame.java index e318dd5..566701a 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/StatelessGame.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/StatelessGame.java @@ -9,7 +9,7 @@ import eu.mhsl.minenet.minigames.score.Score; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; -import net.minestom.server.registry.DynamicRegistry; +import net.minestom.server.registry.RegistryKey; import net.minestom.server.timer.ExecutionType; import net.minestom.server.timer.Task; import net.minestom.server.timer.TaskSchedule; @@ -26,7 +26,7 @@ public class StatelessGame extends Game { private int timeLimit = 0; private int timePlayed = 0; - public StatelessGame(DynamicRegistry.Key dimensionType, String gameName, Score score) { + public StatelessGame(RegistryKey dimensionType, String gameName, Score score) { super(dimensionType); this.score = score; this.name = gameName; @@ -37,7 +37,7 @@ public class StatelessGame extends Game { } public String getName() { - return name; + return this.name; } public void setTimeLimit(int limit) { @@ -47,20 +47,23 @@ public class StatelessGame extends Game { this.timePlayed = 0; } if(this.timeLimit > 0) { - this.timeLimitTask = scheduler().submitTask(() -> { - if(!isRunning || timeLimit == 0) return TaskSchedule.stop(); - if(timeLimit <= timePlayed) { - stop(); + this.timeLimitTask = this.scheduler().submitTask(() -> { + if(!this.isRunning || this.timeLimit == 0) return TaskSchedule.stop(); + if(this.timeLimit <= this.timePlayed) { + this.stop(); return TaskSchedule.stop(); } - int timeLeft = timeLimit - timePlayed; - switch (timeLeft) { - case 90, 60, 30, 10, 5, 4, 3, 2, 1 -> - new ChatMessage(Icon.SCIENCE).appendStatic("Noch " + timeLeft + " Sekunden!").send(getPlayers()); + int timeLeft = this.timeLimit - this.timePlayed; + switch(timeLeft) { + case 90, 60, 30, 10, 5, 4, 3, 2, 1 -> new ChatMessage(Icon.TIME) + .appendStatic(String.valueOf(timeLeft)) + .appendSpace() + .appendTranslated(timeLeft == 1 ? "common#secondsLeft_singular" : "common#secondsLeft_plural") + .send(this.getPlayers()); } - timePlayed++; + this.timePlayed++; return TaskSchedule.seconds(1); }, ExecutionType.TICK_START); @@ -77,37 +80,48 @@ public class StatelessGame extends Game { * When overriding make sure to call this::start after countdown! */ protected CompletableFuture countdownStart() { - return new Countdown(TitleMessage.class) - .countdown(Audience.audience(getPlayers()), 5, countdownModifier -> countdownModifier.message = new TitleMessage(Duration.ofMillis(300), Duration.ofMillis(700)) - .subtitle(subtitleMessage -> subtitleMessage.appendStatic(Component.text("in ", NamedTextColor.DARK_GREEN)) - .appendStatic(Component.text(countdownModifier.timeLeft, NamedTextColor.GREEN)) - .appendStatic(Component.text(" seconds", NamedTextColor.DARK_GREEN)))); - } + Duration fadeIn = Duration.ofMillis(300); + Duration stay = Duration.ofMillis(700); + return new Countdown() + .countdown( + Audience.audience(this.getPlayers()), + 5, + modifier -> modifier.message = new TitleMessage(fadeIn, stay) + .subtitle(subtitleMessage -> subtitleMessage + .appendTranslated("common#startIn", NamedTextColor.DARK_GREEN) + .appendSpace() + .appendStatic(Component.text(modifier.timeLeft, NamedTextColor.GREEN)) + .appendSpace() + .appendTranslated(modifier.timeLeft == 1 ? "common#second" : "common#seconds", NamedTextColor.DARK_GREEN) + ) + ); + } public void startAccessor() { this.start(); } + @Override protected void start() { - score.setInstance(this); - score.attachListeners(); + this.score.setInstance(this); + this.score.attachListeners(); - countdownStart().thenRun(super::start); + this.countdownStart().thenRun(super::start); } @Override public void stop() { - isRunning = false; + this.isRunning = false; this.onStop(); - getScore().insertRemainingPlayers(getPlayers()); - this.publishScore(getScore()); + this.getScore().insertRemainingPlayers(this.getPlayers()); + this.publishScore(this.getScore()); - countdownUnload(); + this.countdownUnload(); } private void countdownUnload() { - new TitleMessage(Duration.ofSeconds(1)).appendTranslated("score#done").send(getPlayers()); - scheduler().scheduleTask(this::unload, TaskSchedule.seconds(5), TaskSchedule.stop()); + new TitleMessage(Duration.ofSeconds(1)).appendTranslated("score#done").send(this.getPlayers()); + this.scheduler().scheduleTask(this::unload, TaskSchedule.seconds(5), TaskSchedule.stop()); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/ConfigManager.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/ConfigManager.java index 1cda99a..4d4a72e 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/ConfigManager.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/ConfigManager.java @@ -5,12 +5,12 @@ import java.util.ArrayList; public class ConfigManager { private final ArrayList> items = new ArrayList<>(); - public ConfigManager addOption(Option option) { - items.add(option); + public ConfigManager addOption(Option option) { + this.items.add(option); return this; } public ArrayList> getAll() { - return items; + return this.items; } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/GameConfigurationInventory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/GameConfigurationInventory.java index 9bafa06..590926c 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/GameConfigurationInventory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/GameConfigurationInventory.java @@ -4,20 +4,19 @@ import eu.mhsl.minenet.minigames.instance.game.Game; import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.Restriction; import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.RestrictionData; import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.RestrictionHandler; +import eu.mhsl.minenet.minigames.instance.room.Room; import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; import eu.mhsl.minenet.minigames.shared.inventory.InteractableInventory; import eu.mhsl.minenet.minigames.util.InventoryItemAlignment; -import eu.mhsl.minenet.minigames.instance.room.Room; import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.format.NamedTextColor; import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Player; -import net.minestom.server.event.EventListener; import net.minestom.server.event.instance.AddEntityToInstanceEvent; import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent; +import net.minestom.server.event.inventory.InventoryPreClickEvent; import net.minestom.server.inventory.InventoryType; -import net.minestom.server.inventory.click.ClickType; -import net.minestom.server.inventory.condition.InventoryConditionResult; +import net.minestom.server.inventory.click.Click; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.sound.SoundEvent; @@ -40,54 +39,54 @@ public class GameConfigurationInventory extends InteractableInventory { this.factory = factory; room.eventNode() - .addListener(AddEntityToInstanceEvent.class, addEntityToInstanceEvent -> MinecraftServer.getSchedulerManager().scheduleNextTick(this::updatePlayButton)) - .addListener(RemoveEntityFromInstanceEvent.class, removeEntityFromInstanceEvent -> MinecraftServer.getSchedulerManager().scheduleNextTick(this::updatePlayButton)); + .addListener(AddEntityToInstanceEvent.class, addEntityToInstanceEvent -> MinecraftServer.getSchedulerManager().scheduleNextTick(this::updatePlayButton)) + .addListener(RemoveEntityFromInstanceEvent.class, removeEntityFromInstanceEvent -> MinecraftServer.getSchedulerManager().scheduleNextTick(this::updatePlayButton)); ConfigManager config = factory.configuration(); - setClickableItem( - ItemStack.builder(Material.RED_WOOL) - .customName( - TranslatedComponent.byId("common#back") - .setColor(NamedTextColor.RED) - .getAssembled(p) - ) - .build(), - 0, - itemClick -> itemClick.getPlayer().closeInventory(), - true + this.setClickableItem( + ItemStack.builder(Material.RED_WOOL) + .customName( + TranslatedComponent.byId("common#back") + .setColor(NamedTextColor.RED) + .getAssembled(p) + ) + .build(), + 0, + itemClick -> itemClick.getPlayer().closeInventory(), + true ); - setDummyItem(Material.BLACK_STAINED_GLASS_PANE,1); + this.setDummyItem(Material.BLACK_STAINED_GLASS_PANE, 1); - setDummyItem( - ItemStack.builder(Material.NAME_TAG) - .customName( - factory.name().setColor(NamedTextColor.GOLD).getAssembled(p) - ) - .build(), - 4 + this.setDummyItem( + ItemStack.builder(Material.NAME_TAG) + .customName( + factory.name().setColor(NamedTextColor.GOLD).getAssembled(p) + ) + .build(), + 4 ); - setDummyItem(Material.BLACK_STAINED_GLASS_PANE,7); + this.setDummyItem(Material.BLACK_STAINED_GLASS_PANE, 7); - updatePlayButton(); + this.updatePlayButton(); for(int i = 9; i <= 17; i++) { - setDummyItem(Material.BLACK_STAINED_GLASS_PANE, i); + this.setDummyItem(Material.BLACK_STAINED_GLASS_PANE, i); } if(config == null) { - setDummyItem( - ItemStack.builder(Material.BARRIER) - .customName( - TranslatedComponent.byId("room#noOption").setColor(NamedTextColor.RED).getAssembled(p) - ) - .lore( - TranslatedComponent.assemble("room#noOptionDescription", p) - ) - .build(), - 31 + this.setDummyItem( + ItemStack.builder(Material.BARRIER) + .customName( + TranslatedComponent.byId("room#noOption").setColor(NamedTextColor.RED).getAssembled(p) + ) + .lore( + TranslatedComponent.assemble("room#noOptionDescription", p) + ) + .build(), + 31 ); } else { @@ -95,11 +94,11 @@ public class GameConfigurationInventory extends InteractableInventory { InventoryItemAlignment itemAlignment = new InventoryItemAlignment(config.getAll().size(), 3); for(Option item : config.getAll()) { InventoryItemAlignment.ItemOffset current = itemAlignment.next(); - map.put(offset + current.get(), item); + this.map.put(offset + current.get(), item); - setDummyItem( - item.getCurrent(p), - offset + current.get() + this.setDummyItem( + item.getCurrent(p), + offset + current.get() ); } } @@ -107,62 +106,62 @@ public class GameConfigurationInventory extends InteractableInventory { } @Override - protected void onClick(Player player, int slot, ClickType clickType, InventoryConditionResult inventoryConditionResult) { - inventoryConditionResult.setCancel(true); + protected void onClick(Player player, int slot, Click click, InventoryPreClickEvent event) { + event.setCancelled(true); - if(!map.containsKey(slot)) return; + if(!this.map.containsKey(slot)) return; - Option item = map.get(slot); - setDummyItem( - item.getNext(p), - slot + Option item = this.map.get(slot); + this.setDummyItem( + item.getNext(this.p), + slot ); - update(); + this.update(); } private void updatePlayButton() { - RestrictionHandler restrictionHandler = factory.globalRestrictions(); - RestrictionData restrictionData = new RestrictionData(room); + RestrictionHandler restrictionHandler = this.factory.globalRestrictions(); + RestrictionData restrictionData = new RestrictionData(this.room); if(restrictionHandler.canPlay(restrictionData)) { - setClickableItem( - ItemStack.builder(restrictionHandler.getWarnings(restrictionData).size() > 0 ? Material.YELLOW_WOOL : Material.GREEN_WOOL) - .customName(TranslatedComponent.byId("restriction#success").setColor(NamedTextColor.GREEN).getAssembled(p)) - .lore(restrictionHandler.getWarnings(restrictionData).stream().map(translatedComponent -> translatedComponent.getAssembled(p)).collect(Collectors.toList())) - .build(), - 8, - itemClick -> Game.initialize(factory, map.values().stream().toList(), itemClick.getPlayer()), - true + this.setClickableItem( + ItemStack.builder(!restrictionHandler.getWarnings(restrictionData).isEmpty() ? Material.YELLOW_WOOL : Material.GREEN_WOOL) + .customName(TranslatedComponent.byId("restriction#success").setColor(NamedTextColor.GREEN).getAssembled(this.p)) + .lore(restrictionHandler.getWarnings(restrictionData).stream().map(translatedComponent -> translatedComponent.getAssembled(this.p)).collect(Collectors.toList())) + .build(), + 8, + itemClick -> Game.initialize(this.factory, this.map.values().stream().toList(), itemClick.getPlayer()), + true ); } else { - setClickableItem( - ItemStack.builder(Material.RED_WOOL) - .customName(TranslatedComponent.byId("restriction#fail").setColor(NamedTextColor.RED).getAssembled(p)) - .lore( - restrictionHandler.getRestrictions() - .stream() - .filter(iRestriction -> iRestriction.calculate(restrictionData).type().equals(Restriction.Type.FAIL)) - .map(iRestriction -> iRestriction.calculate(restrictionData).description().getAssembled(p)) - .collect(Collectors.toList())) - .build(), - 8, - itemClick -> { - if(restrictionHandler.canPlay(restrictionData)) { - updatePlayButton(); - return; - } - itemClick.getPlayer().playSound(Sound.sound(SoundEvent.ENTITY_SILVERFISH_DEATH, Sound.Source.AMBIENT, 1f, 1f)); - }, - false + this.setClickableItem( + ItemStack.builder(Material.RED_WOOL) + .customName(TranslatedComponent.byId("restriction#fail").setColor(NamedTextColor.RED).getAssembled(this.p)) + .lore( + restrictionHandler.getRestrictions() + .stream() + .filter(iRestriction -> iRestriction.calculate(restrictionData).type().equals(Restriction.Type.FAIL)) + .map(iRestriction -> iRestriction.calculate(restrictionData).description().getAssembled(this.p)) + .collect(Collectors.toList())) + .build(), + 8, + itemClick -> { + if(restrictionHandler.canPlay(restrictionData)) { + this.updatePlayButton(); + return; + } + itemClick.getPlayer().playSound(Sound.sound(SoundEvent.ENTITY_SILVERFISH_DEATH, Sound.Source.AMBIENT, 1f, 1f)); + }, + false ); } - update(); + this.update(); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/GameFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/GameFactory.java index 6f15bb7..07e72cb 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/GameFactory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/GameFactory.java @@ -12,9 +12,11 @@ import java.util.Map; public interface GameFactory { TranslatedComponent name(); + default ConfigManager configuration() { return null; } + default RestrictionHandler globalRestrictions() { return new RestrictionHandler(); } @@ -22,6 +24,7 @@ public interface GameFactory { default Material symbol() { return Material.GRASS_BLOCK; } + default TranslatedComponent description() { return TranslatedComponent.byId("GameFactory#missingDescription"); } @@ -29,17 +32,17 @@ public interface GameFactory { Game manufacture(Room parent, Map> configuration) throws Exception; default Game manufacture(Room parent, List> configuration) throws Exception { - if(configuration == null) return manufacture(parent); + if(configuration == null) return this.manufacture(parent); Map> cnf = new HashMap<>(); configuration.forEach(option -> cnf.put(option.getId(), option)); - return manufacture(parent, cnf); + return this.manufacture(parent, cnf); } default Game manufacture(Room parent) throws Exception { - if(this.configuration() == null) return manufacture(parent, List.of()); + if(this.configuration() == null) return this.manufacture(parent, List.of()); - return manufacture(parent, this.configuration().getAll()); + return this.manufacture(parent, this.configuration().getAll()); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/Option.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/Option.java index 1c6fc8f..b0244f2 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/Option.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/Option.java @@ -12,12 +12,12 @@ import java.util.List; import java.util.Objects; public abstract class Option { - private RestrictionHandler restrictionHandler; private final Material item; private final TranslatedComponent name; private final String id; - protected T currentValue; private final List options; + protected T currentValue; + private RestrictionHandler restrictionHandler; private int pointer = 0; public Option(String id, Material item, TranslatedComponent name, List options) { @@ -26,46 +26,45 @@ public abstract class Option { this.name = name; this.options = options; - currentValue = options.getFirst(); + this.currentValue = options.getFirst(); + } + + public RestrictionHandler getRestrictionHandler() { + return this.restrictionHandler; } public void setRestrictionHandler(RestrictionHandler restrictionHandler) { this.restrictionHandler = restrictionHandler; } - public RestrictionHandler getRestrictionHandler() { - return restrictionHandler; - } - - public ItemStack getNext(Player p) { - if(++pointer >= options.size()) pointer = 0; - currentValue = options.get(pointer); - return getCurrent(p); + if(++this.pointer >= this.options.size()) this.pointer = 0; + this.currentValue = this.options.get(this.pointer); + return this.getCurrent(p); } public ItemStack getCurrent(Player p) { - String value = options.get(pointer).toString(); - return ItemStack.builder(item) - .customName(name.getAssembled(p)) - .lore(TranslatedComponent.byId("optionCommon#value").setColor(NamedTextColor.GOLD).getAssembled(p) - .append(Component.text(": ")).append(Component.text(value))) - .build(); + String value = this.options.get(this.pointer).toString(); + return ItemStack.builder(this.item) + .customName(this.name.getAssembled(p)) + .lore(TranslatedComponent.byId("optionCommon#value").setColor(NamedTextColor.GOLD).getAssembled(p) + .append(Component.text(": ")).append(Component.text(value))) + .build(); } public int getAsInt() { - return Integer.parseInt(getAsString()); + return Integer.parseInt(this.getAsString()); } public boolean getAsBoolean() { - return Objects.equals(getAsString(), "true") || Objects.equals(getAsString(), "1"); + return Objects.equals(this.getAsString(), "true") || Objects.equals(this.getAsString(), "1"); } public String getAsString() { - return currentValue.toString(); + return this.currentValue.toString(); } public String getId() { - return id; + return this.id; } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/Restriction.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/Restriction.java index d9c8989..1d5e98e 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/Restriction.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/Restriction.java @@ -10,7 +10,7 @@ public record Restriction(TranslatedComponent name, TranslatedComponent descript this.warnMessage = warnMessage; this.type = type; - if (this.warnMessage == null && this.type.equals(Type.WARN)) { + if(this.warnMessage == null && this.type.equals(Type.WARN)) { throw new IllegalStateException("warnMessage cannot be null when RestrictionType is WARN"); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/RestrictionHandler.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/RestrictionHandler.java index 010f0a2..635c4e7 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/RestrictionHandler.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/RestrictionHandler.java @@ -22,18 +22,18 @@ public class RestrictionHandler { } public List getRestrictions() { - return restrictions; + return this.restrictions; } public boolean canPlay(RestrictionData data) { - if(restrictions.size() < 1) return true; + if(this.restrictions.isEmpty()) return true; return this.restrictions.stream() - .noneMatch(iRestriction -> iRestriction.calculate(data).type().equals(Restriction.Type.FAIL)); + .noneMatch(iRestriction -> iRestriction.calculate(data).type().equals(Restriction.Type.FAIL)); } public List getWarnings(RestrictionData data) { List warnings = new ArrayList<>(); - for (IRestriction r : this.restrictions) { + for(IRestriction r : this.restrictions) { Restriction calculated = r.calculate(data); if(!calculated.type().equals(Restriction.Type.WARN)) continue; diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/common/MaximalPlayeramountGameRestriction.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/common/MaximalPlayeramountGameRestriction.java index 0276a7b..7b13d59 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/common/MaximalPlayeramountGameRestriction.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/common/MaximalPlayeramountGameRestriction.java @@ -15,10 +15,10 @@ public class MaximalPlayeramountGameRestriction implements IRestriction { @Override public Restriction calculate(RestrictionData data) { return new Restriction( - TranslatedComponent.byId("restriction#maxPlayersInRoom"), - TranslatedComponent.byId("restriction#maxPlayersInRoomDescription"), - null, - data.room().getAllMembers().size() > this.max ? Restriction.Type.FAIL : Restriction.Type.OK + TranslatedComponent.byId("restriction#maxPlayersInRoom"), + TranslatedComponent.byId("restriction#maxPlayersInRoomDescription"), + null, + data.room().getAllMembers().size() > this.max ? Restriction.Type.FAIL : Restriction.Type.OK ); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/common/MinimalPlayeramountGameRestriction.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/common/MinimalPlayeramountGameRestriction.java index e460b78..c0ec0e1 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/common/MinimalPlayeramountGameRestriction.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/config/restriction/common/MinimalPlayeramountGameRestriction.java @@ -15,10 +15,10 @@ public class MinimalPlayeramountGameRestriction implements IRestriction { @Override public Restriction calculate(RestrictionData data) { return new Restriction( - TranslatedComponent.byId("restriction#minPlayersInRoom"), - TranslatedComponent.byId("restriction#minPlayersInRoomDescription"), - null, - data.room().getAllMembers().size() < this.min ? Restriction.Type.FAIL : Restriction.Type.OK + TranslatedComponent.byId("restriction#minPlayersInRoom"), + TranslatedComponent.byId("restriction#minPlayersInRoomDescription"), + null, + data.room().getAllMembers().size() < this.min ? Restriction.Type.FAIL : Restriction.Type.OK ); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/acidRain/AcidRain.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/acidRain/AcidRain.java index 2aecbcb..8bdfb7d 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/acidRain/AcidRain.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/acidRain/AcidRain.java @@ -1,6 +1,7 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.acidRain; -import de.articdive.jnoise.JNoise; +import de.articdive.jnoise.generators.noisegen.opensimplex.FastSimplexNoiseGenerator; +import de.articdive.jnoise.pipeline.JNoise; import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; import eu.mhsl.minenet.minigames.score.LastWinsScore; @@ -27,26 +28,25 @@ import java.util.concurrent.CompletableFuture; public class AcidRain extends StatelessGame { - final private int radius = 20; - private int generationOffset = 0; - - final private int roofHeight = 55; - private int difficulty = 0; final JNoise noise = JNoise.newBuilder() - .fastSimplex() - .setSeed(rnd.nextLong()) - .setFrequency(0.09) - .build(); + .fastSimplex(FastSimplexNoiseGenerator.newBuilder().setSeed(this.rnd.nextLong()).build()) + .scale(0.09) + .build(); + final private int radius = 20; + final private int roofHeight = 55; + private int generationOffset = 0; + private int difficulty = 0; + public AcidRain() { super(Dimension.OVERWORLD.key, "acidRain", new LastWinsScore()); - setGenerator( - new CircularPlateTerrainGenerator(radius) - .setPlateHeight(50) - .setPlatePallet(BlockPallet.STONE) + this.setGenerator( + new CircularPlateTerrainGenerator(this.radius) + .setPlateHeight(50) + .setPlatePallet(BlockPallet.STONE) ); - eventNode().addListener(PlayerTickEvent.class, this::onPlayerTick); + this.eventNode().addListener(PlayerTickEvent.class, this::onPlayerTick); } @Override @@ -61,39 +61,40 @@ public class AcidRain extends StatelessGame { MinecraftServer.getSchedulerManager().submitTask(() -> { - getPlayers().forEach(player -> { - player.sendPacket(new ParticlePacket(Particle.SNEEZE, 0, 60, 0, radius, radius, radius, 1f, 500)); - player.sendPacket(new ParticlePacket(Particle.ITEM_SLIME, 0, 60, 0, radius, radius, radius, 1f, 500)); + this.getPlayers().forEach(player -> { + player.sendPacket(new ParticlePacket(Particle.SNEEZE, 0, 60, 0, this.radius, this.radius, this.radius, 1f, 500)); + player.sendPacket(new ParticlePacket(Particle.ITEM_SLIME, 0, 60, 0, this.radius, this.radius, this.radius, 1f, 500)); }); - if(!isRunning) return TaskSchedule.stop(); + if(!this.isRunning) return TaskSchedule.stop(); return TaskSchedule.millis(100); }, ExecutionType.TICK_END); MinecraftServer.getSchedulerManager().submitTask(() -> { - generationOffset++; - generatePlatform(generationOffset); + this.generationOffset++; + this.generatePlatform(this.generationOffset); - if(!isRunning) return TaskSchedule.stop(); - return TaskSchedule.millis((long) NumberUtil.map(50 - difficulty, 0, 50, 100, 1000)); + if(!this.isRunning) return TaskSchedule.stop(); + return TaskSchedule.millis((long) NumberUtil.map(50 - this.difficulty, 0, 50, 100, 1000)); }, ExecutionType.TICK_END); MinecraftServer.getSchedulerManager().submitTask(() -> { - difficulty++; + this.difficulty++; - if(difficulty >= 50) return TaskSchedule.stop(); + if(this.difficulty >= 50) return TaskSchedule.stop(); return TaskSchedule.seconds(3); }); } @Override protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { - if(playerMoveEvent.getNewPosition().y() < 50) playerDeath(playerMoveEvent.getPlayer()); + if(playerMoveEvent.getNewPosition().y() < 50) this.playerDeath(playerMoveEvent.getPlayer()); } private void onPlayerTick(PlayerTickEvent playerTickEvent) { - if(isBeforeBeginning) return; - if(getBlock(playerTickEvent.getPlayer().getPosition().withY(roofHeight)).isAir()) playerDeath(playerTickEvent.getPlayer()); + if(this.isBeforeBeginning) return; + if(this.getBlock(playerTickEvent.getPlayer().getPosition().withY(this.roofHeight)).isAir()) + this.playerDeath(playerTickEvent.getPlayer()); } private void generatePlatform(long offset) { @@ -102,19 +103,20 @@ public class AcidRain extends StatelessGame { AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); for(int x = -radius; x <= radius; x++) { for(int z = -radius; z <= radius; z++) { - batch.setBlock(x, roofHeight, z, Block.AIR); + batch.setBlock(x, this.roofHeight, z, Block.AIR); if(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > radius) continue; - if(noise.getNoise(x + offset, z + offset) > 0.4) continue; + if(this.noise.evaluateNoise(x + offset, z + offset) > 0.4) continue; - batch.setBlock(x, roofHeight, z, Block.OAK_PLANKS); + batch.setBlock(x, this.roofHeight, z, Block.OAK_PLANKS); } } - BatchUtil.loadAndApplyBatch(batch, this, () -> {}); + BatchUtil.loadAndApplyBatch(batch, this, () -> { + }); } private void playerDeath(Player p) { p.setGameMode(GameMode.SPECTATOR); - getScore().insertResult(p); + this.getScore().insertResult(p); } @Override diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/anvilRun/AnvilRun.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/anvilRun/AnvilRun.java index 1496048..127b106 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/anvilRun/AnvilRun.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/anvilRun/AnvilRun.java @@ -1,8 +1,8 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.anvilRun; +import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; import eu.mhsl.minenet.minigames.score.LastWinsScore; -import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.util.BatchUtil; import eu.mhsl.minenet.minigames.world.generator.terrain.CircularPlateTerrainGenerator; import net.minestom.server.MinecraftServer; @@ -27,10 +27,10 @@ class AnvilRun extends StatelessGame { final int spawnHeight = 30; final int radius; - int anvilsPerSecond; final int seconds; final int anvilHeight = 200; final List anvilSpawnPositions = new ArrayList<>(); + int anvilsPerSecond; public AnvilRun(int radius, int seconds) { super(Dimension.OVERWORLD.key, "Anvil Run", new LastWinsScore()); @@ -38,25 +38,25 @@ class AnvilRun extends StatelessGame { this.seconds = seconds; this.setGenerator(new CircularPlateTerrainGenerator(radius)); - eventNode().addListener(EntityTickEvent.class, this::onEntityTick); + this.eventNode().addListener(EntityTickEvent.class, this::onEntityTick); } @Override protected void onLoad(@NotNull CompletableFuture callback) { AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); - for(int x = -radius; x <= radius; x++) { - for (int z = -radius; z <= radius; z++) { - if(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > radius) continue; + for(int x = -this.radius; x <= this.radius; x++) { + for(int z = -this.radius; z <= this.radius; z++) { + if(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > this.radius) continue; - anvilSpawnPositions.add(new Pos(x+0.5, anvilHeight, z+0.5)); + this.anvilSpawnPositions.add(new Pos(x + 0.5, this.anvilHeight, z + 0.5)); - batch.setBlock(x, spawnHeight-1, z, Block.SNOW_BLOCK); + batch.setBlock(x, this.spawnHeight - 1, z, Block.SNOW_BLOCK); } } - this.anvilsPerSecond = anvilSpawnPositions.size() / this.seconds; - Collections.shuffle(anvilSpawnPositions); + this.anvilsPerSecond = this.anvilSpawnPositions.size() / this.seconds; + Collections.shuffle(this.anvilSpawnPositions); BatchUtil.loadAndApplyBatch(batch, this, () -> callback.complete(null)); } @@ -72,12 +72,12 @@ class AnvilRun extends StatelessGame { super.onStart(); Scheduler scheduler = MinecraftServer.getSchedulerManager(); - for(int i=0; i spawnAnvil(this.anvilSpawnPositions.get(j)), + () -> this.spawnAnvil(this.anvilSpawnPositions.get(j)), TaskSchedule.millis( - (long) Math.floor((double) i/this.anvilsPerSecond * 1000) + (long) Math.floor((double) i / this.anvilsPerSecond * 1000) ), TaskSchedule.stop() ); @@ -87,25 +87,26 @@ class AnvilRun extends StatelessGame { @Override protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { super.onPlayerMove(playerMoveEvent); - if(isBeforeBeginning && playerMoveEvent.getNewPosition().y() < spawnHeight - 2) { + if(this.isBeforeBeginning && playerMoveEvent.getNewPosition().y() < this.spawnHeight - 2) { playerMoveEvent.setCancelled(true); - playerMoveEvent.getPlayer().teleport(getSpawn()); + playerMoveEvent.getPlayer().teleport(this.getSpawn()); return; } - if(playerMoveEvent.getNewPosition().y() < spawnHeight - 2) getScore().insertResult(playerMoveEvent.getPlayer()); + if(playerMoveEvent.getNewPosition().y() < this.spawnHeight - 2) + this.getScore().insertResult(playerMoveEvent.getPlayer()); } protected void onEntityTick(@NotNull EntityTickEvent entityTickEvent) { if(!entityTickEvent.getEntity().getEntityType().equals(EntityType.FALLING_BLOCK)) return; Pos anvilPosition = entityTickEvent.getEntity().getPosition(); - if(anvilPosition.y() > spawnHeight + 0.5) return; - if(anvilPosition.y() < spawnHeight - 3) entityTickEvent.getEntity().remove(); - if(this.getBlock(anvilPosition.withY(spawnHeight-1)).isAir()) return; - this.setBlock(anvilPosition.withY(spawnHeight-1), Block.AIR); + if(anvilPosition.y() > this.spawnHeight + 0.5) return; + if(anvilPosition.y() < this.spawnHeight - 3) entityTickEvent.getEntity().remove(); + if(this.getBlock(anvilPosition.withY(this.spawnHeight - 1)).isAir()) return; + this.setBlock(anvilPosition.withY(this.spawnHeight - 1), Block.AIR); } @Override public Pos getSpawn() { - return new Pos(0, spawnHeight, 0); + return new Pos(0, this.spawnHeight, 0); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/anvilRun/AnvilRunFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/anvilRun/AnvilRunFactory.java index 13f9d11..f7d7aac 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/anvilRun/AnvilRunFactory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/anvilRun/AnvilRunFactory.java @@ -1,9 +1,9 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.anvilRun; import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; -import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption; import eu.mhsl.minenet.minigames.instance.room.Room; import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; @@ -26,7 +26,7 @@ public class AnvilRunFactory implements GameFactory { public ConfigManager configuration() { return new ConfigManager() .addOption(new NumericOption("radius", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#radius"), 5, 10, 15, 20, 25, 30)) - .addOption(new NumericOption("seconds", Material.STICK, TranslatedComponent.byId("optionCommon#seconds"), 10, 30, 60, 90, 120)); + .addOption(new NumericOption("seconds", Material.CLOCK, TranslatedComponent.byId("optionCommon#seconds"), 10, 30, 60, 90, 120)); } @Override diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/Backrooms.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/Backrooms.java deleted file mode 100644 index 6eedd86..0000000 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/Backrooms.java +++ /dev/null @@ -1,20 +0,0 @@ -package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms; - -import eu.mhsl.minenet.minigames.instance.Dimension; -import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; -import eu.mhsl.minenet.minigames.score.NoScore; -import net.minestom.server.event.player.PlayerBlockBreakEvent; -import org.jetbrains.annotations.NotNull; - -public class Backrooms extends StatelessGame { - public Backrooms() { - super(Dimension.NETHER.key, "Backrooms", new NoScore()); - BackroomsGenerator generator = new BackroomsGenerator(); - setGenerator(unit -> generator.generateRoom(unit, 50)); - } - - @Override - protected void onBlockBreak(@NotNull PlayerBlockBreakEvent playerBlockBreakEvent) { - playerBlockBreakEvent.setCancelled(false); - } -} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsFactory.java index 73f3d7b..4277be6 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsFactory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsFactory.java @@ -1,21 +1,52 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms; import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption; import eu.mhsl.minenet.minigames.instance.room.Room; import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import net.minestom.server.item.Material; import java.util.Map; public class BackroomsFactory implements GameFactory { @Override public TranslatedComponent name() { - return TranslatedComponent.byId("gameBackrooms"); + return TranslatedComponent.byId("game_Backrooms#name"); + } + + @Override + public TranslatedComponent description() { + return TranslatedComponent.byId("game_Backrooms#description"); + } + + @Override + public Material symbol() { + return Material.YELLOW_TERRACOTTA; + } + + @Override + public ConfigManager configuration() { + return new ConfigManager() + .addOption(new NumericOption("levels", Material.LADDER, TranslatedComponent.byId("game_Backrooms#optionLevels"), 3, 2, 4, 5)) + .addOption(new NumericOption("lives", Material.TOTEM_OF_UNDYING, TranslatedComponent.byId("game_Backrooms#optionLives"), 3, 1, 2, 5)) + .addOption(new NumericOption("monsterDensity", Material.ZOMBIE_HEAD, TranslatedComponent.byId("game_Backrooms#optionMonsterDensity"), 2, 1, 3)) + .addOption(new NumericOption("size", Material.MAP, TranslatedComponent.byId("game_Backrooms#optionSize"), 0, 1, 2, 3)) + // 0/1 statt BoolOption: deren erster Eintrag "true" waere sonst der Default + .addOption(new NumericOption("hardcoreSanity", Material.WITHER_ROSE, TranslatedComponent.byId("game_Backrooms#optionHardcoreSanity"), 0, 1)); } @Override public Game manufacture(Room parent, Map> configuration) throws Exception { - return new Backrooms().setParent(parent); + return new BackroomsGame( + configuration.get("levels").getAsInt(), + configuration.get("lives").getAsInt(), + configuration.get("monsterDensity").getAsInt(), + configuration.get("size").getAsInt(), + configuration.get("hardcoreSanity").getAsBoolean(), + parent.getAllMembers().size() + ).setParent(parent); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsGame.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsGame.java new file mode 100644 index 0000000..f916d7a --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsGame.java @@ -0,0 +1,663 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience.AmbienceEngine; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.door.DoorManager; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.generator.BackroomsChunkGenerator; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.generator.MazeAlgorithms; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.item.BackroomsItems; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelDefinition; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelType; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.TransitionType; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.impl.Level0Lobby; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.impl.LevelFinale; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.impl.LevelFun; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.impl.LevelHabitableZone; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.impl.LevelLightsOut; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.impl.LevelPipeDreams; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.impl.LevelPoolrooms; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.loot.ChestManager; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.BackroomsMonster; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.sanity.SanityManager; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.transition.TransitionManager; +import eu.mhsl.minenet.minigames.message.Icon; +import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import eu.mhsl.minenet.minigames.message.type.ChatMessage; +import eu.mhsl.minenet.minigames.message.type.TitleMessage; +import net.kyori.adventure.sound.Sound; +import java.time.Duration; +import net.kyori.adventure.text.Component; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.event.entity.EntityAttackEvent; +import net.minestom.server.event.entity.EntityDamageEvent; +import net.minestom.server.event.player.PlayerBlockInteractEvent; +import net.minestom.server.event.player.PlayerMoveEvent; +import net.minestom.server.event.player.PlayerUseItemEvent; +import net.minestom.server.instance.LightingChunk; +import net.minestom.server.entity.attribute.Attribute; +import net.minestom.server.entity.damage.DamageType; +import net.minestom.server.item.ItemStack; +import net.minestom.server.potion.Potion; +import net.minestom.server.potion.PotionEffect; +import net.minestom.server.scoreboard.Sidebar; +import net.minestom.server.sound.SoundEvent; +import net.minestom.server.timer.ExecutionType; +import net.minestom.server.timer.TaskSchedule; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/** + * Backrooms: Wettlauf durch prozedural generierte, vertikal gestapelte Horror-Level. + * Wer zuerst durch alle Level entkommt gewinnt; sterben alle anderen, gewinnt der + * letzte Ueberlebende. Kein Spieler-Schaden untereinander — nur die Backrooms selbst. + */ +public class BackroomsGame extends StatelessGame { + /** Zufalls-Pool der Zwischen-Level; wird mit jedem fertigen Leveltyp erweitert. */ + private static final List> LEVEL_POOL = List.of( + LevelHabitableZone::new, + LevelPipeDreams::new, + LevelPoolrooms::new, + LevelLightsOut::new, + LevelFun::new + ); + + private final List levels = new ArrayList<>(); + private final Map playerStates = new HashMap<>(); + private final Map sidebars = new HashMap<>(); + + private final int lives; + private final int monsterDensity; + private final boolean hardcoreSanity; + + private final TransitionManager transitionManager = new TransitionManager(this); + private final ChestManager chestManager; + private final DoorManager doorManager = new DoorManager(this); + private final SanityManager sanityManager; + private final AmbienceEngine ambienceEngine = new AmbienceEngine(this); + + private final Set openedDoorLevels = new HashSet<>(); + + private long tickCounter = 0; + private long secondCounter = 0; + + public BackroomsGame(int totalLevels, int lives, int monsterDensity, int sizeOption, boolean hardcoreSanity, int expectedPlayers) { + super(Dimension.BACKROOMS.key, "Backrooms", new BackroomsScore()); + this.getScore().setIgnoreLastPlayers(1); + this.lives = lives; + this.monsterDensity = monsterDensity; + this.hardcoreSanity = hardcoreSanity; + this.sanityManager = new SanityManager(this, hardcoreSanity); + + int cells = this.cellsForSize(sizeOption, expectedPlayers); + + List sequence = this.buildSequence(totalLevels); + for(int i = 0; i < sequence.size(); i++) { + LevelDefinition definition = sequence.get(i); + Random layoutRnd = new Random(this.rnd.nextLong()); + var layout = definition.buildLayout(layoutRnd, cells, expectedPlayers); + this.levels.add(new LevelRuntime(this, i, definition, layout, layoutRnd)); + } + + this.connectShaftLandings(); + // Echte Lichtberechnung: nur die Deckenlampen leuchten, Blackout-Biome sind + // tatsaechlich dunkel (Dimension-Ambient ist fast schwarz) + this.setChunkSupplier(LightingChunk::new); + this.setGenerator(new BackroomsChunkGenerator(this.levels)); + this.chestManager = new ChestManager(this); + + this.eventNode() + .addListener(PlayerBlockInteractEvent.class, event -> { + this.chestManager.onInteract(event); + this.doorManager.onInteract(event); + }) + .addListener(PlayerUseItemEvent.class, this::onUseItem) + .addListener(EntityAttackEvent.class, this::onEntityAttack) + .addListener(EntityDamageEvent.class, this::onEntityDamage); + } + + /** + * HOLE/LADDER-Schaechte enden im Level darunter an einer beliebigen Zelle. + * Die Landezelle wird freigeraeumt und per kuerzestem Wanddurchbruch mit dem + * Labyrinth verbunden, damit niemand in einer Wandtasche landet. + */ + private void connectShaftLandings() { + for(int i = 0; i < this.levels.size() - 1; i++) { + LevelRuntime upper = this.levels.get(i); + TransitionType transition = upper.getDefinition().transitionType(); + if(transition != TransitionType.HOLE && transition != TransitionType.LADDER) continue; + + int[] exit = upper.getLayout().getExitCell(); + if(exit == null) continue; + + var lowerLayout = this.levels.get(i + 1).getLayout(); + int landingX = lowerLayout.cellX(upper.getLayout().worldX(exit[0]) + 1); + int landingZ = lowerLayout.cellZ(upper.getLayout().worldZ(exit[1]) + 1); + if(!lowerLayout.inBounds(landingX, landingZ)) continue; + if(lowerLayout.isWalkable(landingX, landingZ)) continue; + + lowerLayout.setCell(landingX, landingZ, LevelLayout.Cell.FLOOR); + MazeAlgorithms.ensureConnected( + lowerLayout, + lowerLayout.getSpawnCells().getFirst(), + List.of(new int[]{landingX, landingZ}) + ); + } + } + + private int cellsForSize(int sizeOption, int expectedPlayers) { + int cells = switch(sizeOption) { + case 1 -> 25; + case 2 -> 37; + case 3 -> 49; + // Auto: waechst mit der Wurzel der Spielerzahl, damit 50 Spieler nicht clustern + default -> 23 + 5 * (int) Math.ceil(Math.sqrt(Math.max(1, expectedPlayers))); + }; + cells = Math.min(63, cells); + return cells % 2 == 0 ? cells + 1 : cells; + } + + private List buildSequence(int totalLevels) { + List sequence = new ArrayList<>(); + sequence.add(new Level0Lobby()); + + List> pool = new ArrayList<>(LEVEL_POOL); + Collections.shuffle(pool, this.rnd); + int middleCount = Math.max(0, totalLevels - 2); + for(int i = 0; i < middleCount; i++) { + // Pool leer oder aufgebraucht: Lobby-Varianten als Fallback + sequence.add(i < pool.size() ? pool.get(i).get() : new Level0Lobby()); + } + + sequence.add(new LevelFinale()); + return sequence; + } + + public List getLevels() { + return this.levels; + } + + public Random rnd() { + return this.rnd; + } + + public BackroomsPlayer playerState(Player player) { + return this.playerStates.get(player.getUuid()); + } + + public List playersInLevel(int index) { + return this.getPlayers().stream() + .filter(player -> player.getGameMode() == GameMode.ADVENTURE) + .filter(player -> { + BackroomsPlayer state = this.playerState(player); + return state != null && state.getLevelIndex() == index; + }) + .toList(); + } + + public boolean isDoorOpened(int levelIndex) { + return this.openedDoorLevels.contains(levelIndex); + } + + /** + * Erster Spieler oeffnet die Tuer: Nachricht an alle, die Keys dieses Levels + * zerfallen — und alle Spieler im Level bekommen automatisch einen Kompass, + * um schnell nachzukommen. + */ + public void markDoorOpened(int levelIndex) { + if(!this.openedDoorLevels.add(levelIndex)) return; + + this.getPlayers().forEach(player -> + BackroomsItems.removeMatching(player, item -> BackroomsItems.isKeyFor(item, levelIndex)) + ); + + this.playersInLevel(levelIndex).forEach(player -> this.chestManager.giveCompassIfMissing(player, levelIndex)); + + new ChatMessage(Icon.INFO) + .appendTranslated("game_Backrooms#doorOpened") + .send(this.playersInLevel(levelIndex)); + } + + public LevelRuntime levelAt(int y) { + for(LevelRuntime level : this.levels) { + if(y >= level.getBaseY() && y < level.getBaseY() + LevelRuntime.LEVEL_HEIGHT) return level; + } + return null; + } + + @Override + protected void onLoad(@NotNull CompletableFuture callback) { + callback.complete(null); + } + + @Override + protected void onStart() { + // onStart laeuft in einem CompletableFuture — jede unbehandelte Exception wuerde + // lautlos verschluckt und ALLE Spielsysteme (Monster, Events, Sanity) blieben tot. + // Deshalb: alles einzeln kapseln und Fehler laut ins Log schreiben. + try { + this.getPlayers().forEach(player -> { + try { + this.playerStates.put(player.getUuid(), new BackroomsPlayer(this.lives)); + player.setExp(1f); + player.setFood(20); + player.setFoodSaturation(0); + this.createSidebar(player); + } catch(Exception e) { + this.logger.severe("player setup failed for " + player.getUsername() + ": " + e); + MinecraftServer.getExceptionManager().handleException(e); + } + }); + + for(LevelRuntime level : this.levels) { + try { + level.getDefinition().populateMonsters(this, level, this.monsterDensity); + this.logger.info("Backrooms level " + level.getIndex() + " (" + + level.getDefinition().type() + "): " + level.getMonsters().size() + " monsters spawned"); + } catch(Exception e) { + this.logger.severe("populateMonsters failed for level " + level.getIndex() + ": " + e); + MinecraftServer.getExceptionManager().handleException(e); + } + } + + // Zentraler Spiel-Tick: Monster-AI (nur Level mit Spielern), No-Clip-Erkennung, Dunkelheits-Effekte + this.scheduler().submitTask(() -> { + if(!this.isRunning()) return TaskSchedule.stop(); + this.tickCounter += 2; + + for(LevelRuntime level : this.levels) { + List playersInLevel = level.playersInLevel(); + if(playersInLevel.isEmpty()) continue; + for(BackroomsMonster monster : List.copyOf(level.getMonsters())) { + if(monster.isRemoved() || monster.isDead()) { + monster.remove(); + level.getMonsters().remove(monster); + continue; + } + // Spawn laeuft asynchron ueber loadChunk — bis dahin nicht ticken + if(monster.getInstance() == null) continue; + // Nur in Spielernaehe ticken: dort sind die Chunks garantiert geladen, + // sonst wirft der Pathfinder auf ungeladenen Chunks NPEs + boolean playerNear = playersInLevel.stream().anyMatch(player -> monster.getDistance(player) < 48); + if(!playerNear) continue; + try { + monster.monsterTick(this.tickCounter); + } catch(Exception e) { + // Ein einziges kaputtes Monster darf nicht den ganzen AI-Task toeten + this.logger.severe("monsterTick failed, removing monster " + monster.getEntityType() + ": " + e); + MinecraftServer.getExceptionManager().handleException(e); + monster.remove(); + } + } + } + + if(this.tickCounter % 10 == 0) { + this.getPlayers().stream() + .filter(player -> player.getGameMode() == GameMode.ADVENTURE) + .forEach(player -> { + try { + this.transitionManager.onTick(player); + this.applyDarkness(player); + } catch(Exception e) { + MinecraftServer.getExceptionManager().handleException(e); + } + }); + } + + return TaskSchedule.tick(2); + }, ExecutionType.TICK_START); + + // Level-Events (Blackouts, Finale-Todeszone) + Monster-Director im Sekundentakt + this.scheduler().submitTask(() -> { + if(!this.isRunning()) return TaskSchedule.stop(); + this.secondCounter++; + this.getPlayers().stream() + .filter(player -> player.getGameMode() == GameMode.ADVENTURE) + .forEach(player -> { + try { + this.tickHunger(player); + } catch(Exception e) { + // darf den Level-Event-Task nicht dauerhaft toeten + MinecraftServer.getExceptionManager().handleException(e); + } + }); + for(LevelRuntime level : this.levels) { + if(level.playersInLevel().isEmpty()) continue; + try { + level.getDefinition().onLevelTick(this, level); + if(this.secondCounter % 5 == 0) this.directMonsters(level); + } catch(Exception e) { + this.logger.severe("onLevelTick failed for level " + level.getIndex() + ": " + e); + MinecraftServer.getExceptionManager().handleException(e); + } + } + return TaskSchedule.seconds(1); + }, ExecutionType.TICK_END); + + this.sanityManager.start(); + this.ambienceEngine.start(); + this.setTimeLimit(600 + this.levels.size() * 60); + } catch(Exception e) { + this.logger.severe("BackroomsGame.onStart failed: " + e); + MinecraftServer.getExceptionManager().handleException(e); + } + } + + /** + * Sprinten kostet Hunger (sichtbar durch den Hunger-Effekt); unter 7 Punkten + * verweigert der Client das Sprinten von selbst. Der Vorrat reicht fuer rund + * 30 Sekunden Dauersprint; ohne Sprint regeneriert er langsam, Energy Bars + * fuellen ihn schnell. + */ + private void tickHunger(Player player) { + if(player.isSprinting()) { + if(this.secondCounter % 2 == 0) player.setFood(Math.max(0, player.getFood() - 1)); + player.addEffect(new Potion(PotionEffect.HUNGER, (byte) 0, 35)); + } else if(this.secondCounter % 3 == 0) { + player.setFood(Math.min(20, player.getFood() + 1)); + } + } + + /** + * Monster-Director: haelt die Monsterpopulation in Spielernaehe. Roaming-Monster + * spawnen 6-14 Zellen (18-42 Bloecke) neben einem zufaelligen Spieler und despawnen, + * sobald sie von allen Spielern des Levels > 64 Bloecke entfernt sind. Fest platzierte + * Monster (Fallen wie Clumps, Smiler-Nester) bleiben bestehen. + */ + private void directMonsters(LevelRuntime level) { + List players = level.playersInLevel(); + if(players.isEmpty()) return; + + for(BackroomsMonster monster : List.copyOf(level.getMonsters())) { + if(monster.isRemoved() || !monster.isRoaming() || monster.getInstance() == null) continue; + boolean anyoneNear = players.stream().anyMatch(player -> monster.getDistance(player) < 64); + if(!anyoneNear) { + monster.remove(); + level.getMonsters().remove(monster); + } + } + + long active = level.getMonsters().stream().filter(monster -> !monster.isRemoved()).count(); + int target = this.monsterDensity + players.size(); + if(active >= target) return; + + var layout = level.getLayout(); + for(int attempt = 0; attempt < 10 && active < target; attempt++) { + Player anchor = players.get(this.rnd.nextInt(players.size())); + int cx = layout.cellX(anchor.getPosition().x()) + this.randomOffset(6, 14); + int cz = layout.cellZ(anchor.getPosition().z()) + this.randomOffset(6, 14); + if(!layout.isWalkable(cx, cz)) continue; + + // Nicht neben ein bereits vorhandenes Monster spawnen — sonst klumpen sie + Pos candidate = level.toPlayerPos(new int[]{cx, cz}); + boolean crowded = level.getMonsters().stream() + .anyMatch(other -> !other.isRemoved() && other.getInstance() != null + && other.getPosition().distance(candidate) < 12); + if(crowded) continue; + + BackroomsMonster monster = level.getDefinition().createRoamingMonster(level, new int[]{cx, cz}); + if(monster == null) continue; + monster.setRoaming(true); + monster.spawnAtCell(this, new int[]{cx, cz}); + level.getMonsters().add(monster); + active++; + } + } + + private int randomOffset(int min, int max) { + int magnitude = min + this.rnd.nextInt(max - min + 1); + return this.rnd.nextBoolean() ? magnitude : -magnitude; + } + + /** + * DARKNESS-Effekt nur noch fuer Blackout-Events (ploetzlicher Lichtausfall) — + * dauerhaft dunkle Bereiche inkl. Lights Out verdunkelt bereits die echte + * Lichtberechnung; beides zusammen waere komplett blind. + */ + private void applyDarkness(Player player) { + BackroomsPlayer state = this.playerState(player); + if(state == null) return; + LevelRuntime level = this.levels.get(state.getLevelIndex()); + if(level.isBlackoutActive()) player.addEffect(new Potion(PotionEffect.DARKNESS, (byte) 0, 40)); + } + + @Override + protected void onStop() { + // Sidebars abrauemen — sonst nehmen die Spieler das Scoreboard mit in die Lobby + this.sidebars.values().forEach(sidebar -> + Set.copyOf(sidebar.getViewers()).forEach(sidebar::removeViewer) + ); + this.sidebars.clear(); + + // Leben und Hunger zuruecksetzen — der Lobby-Reset des Frameworks macht das nicht + this.getPlayers().forEach(this::restoreVitals); + } + + @Override + protected void onPlayerLeave(Player player) { + Sidebar sidebar = this.sidebars.remove(player.getUuid()); + if(sidebar != null) sidebar.removeViewer(player); + this.restoreVitals(player); + super.onPlayerLeave(player); + } + + private void restoreVitals(Player player) { + player.setHealth((float) player.getAttributeValue(Attribute.MAX_HEALTH)); + player.setFood(20); + player.setFoodSaturation(5); + } + + @Override + protected void onPlayerMove(@NotNull PlayerMoveEvent event) { + if(this.isBeforeBeginning) { + // Vor dem Start eingefroren: Umschauen erlaubt, Position bleibt fix + if(!event.getNewPosition().samePoint(event.getPlayer().getPosition())) { + event.setCancelled(true); + } + return; + } + this.transitionManager.onMove(event.getPlayer(), event.getNewPosition()); + } + + private void onUseItem(PlayerUseItemEvent event) { + Player player = event.getPlayer(); + ItemStack item = event.getItemStack(); + + if(BackroomsItems.is(item, BackroomsItems.ALMOND_WATER)) { + event.setCancelled(true); + this.consumeOne(player, event); + this.sanityManager.restore(player, 0.4); + float maxHealth = (float) player.getAttributeValue(Attribute.MAX_HEALTH); + player.setHealth(Math.min(maxHealth, player.getHealth() + 4)); + player.playSound(Sound.sound(SoundEvent.ENTITY_GENERIC_DRINK, Sound.Source.PLAYER, 1f, 1f), Sound.Emitter.self()); + } else if(BackroomsItems.is(item, BackroomsItems.ENERGY_BAR)) { + event.setCancelled(true); + this.consumeOne(player, event); + player.setFood(Math.min(20, player.getFood() + 8)); + player.playSound(Sound.sound(SoundEvent.ENTITY_PLAYER_BURP, Sound.Source.PLAYER, 1f, 1f), Sound.Emitter.self()); + } + } + + private void consumeOne(Player player, PlayerUseItemEvent event) { + ItemStack item = event.getItemStack(); + player.setItemInHand(event.getHand(), item.withAmount(item.amount() - 1)); + } + + /** Spieler duerfen Monster schlagen (zurueckdraengen), aber keine anderen Spieler. */ + private void onEntityAttack(EntityAttackEvent event) { + if(!(event.getEntity() instanceof Player attacker)) return; + if(!(event.getTarget() instanceof BackroomsMonster monster)) return; + if(this.isBeforeBeginning) return; + + monster.damage(DamageType.PLAYER_ATTACK, 3); + var knockback = monster.getPosition().sub(attacker.getPosition()).asVec().withY(0).normalize().mul(8).withY(4); + monster.setVelocity(monster.getVelocity().add(knockback)); + } + + /** Hit-Delay + Abfangen toedlichen Schadens fuer die Leben-Logik. */ + private void onEntityDamage(EntityDamageEvent event) { + if(!(event.getEntity() instanceof Player player)) return; + BackroomsPlayer state = this.playerState(player); + if(state == null || state.isEliminated() || state.isEscaped()) { + event.setCancelled(true); + return; + } + + // Immunitaetsfenster wie Vanilla: ohne PvP-Lib gibt es sonst keinen Hit-Delay + long now = System.currentTimeMillis(); + if(now - state.getLastDamageMillis() < 500) { + event.setCancelled(true); + return; + } + state.setLastDamageMillis(now); + + if(event.getDamage().getAmount() < player.getHealth()) return; + event.setCancelled(true); + this.handleDeath(player, state); + } + + private void handleDeath(Player player, BackroomsPlayer state) { + state.loseLife(); + player.setHealth((float) player.getAttributeValue(Attribute.MAX_HEALTH)); + + if(state.getLives() <= 0) { + state.setEliminated(true); + player.setGameMode(GameMode.SPECTATOR); + new TitleMessage(Duration.ofSeconds(2)).appendTranslated("game_Backrooms#eliminated").send(player); + this.playSound(Sound.sound(SoundEvent.ENTITY_WITHER_SPAWN, Sound.Source.HOSTILE, 0.4f, 0.6f)); + this.getScore().insertResult(player); + this.updateSidebars(); + return; + } + + // Respawn an zufaelliger Position im aktuellen Level; nur Schluessel bleiben im Inventar + this.stripInventoryExceptKeys(player); + LevelRuntime level = this.levels.get(state.getLevelIndex()); + // Im Finale bekommt der Respawn eine frische persoenliche Jagd samt Vorsprung + if(level.getDefinition().type() == LevelType.FINALE) { + state.setFinaleSeconds(BackroomsPlayer.FINALE_NOT_STARTED); + state.setFinaleZoneX(Double.NEGATIVE_INFINITY); + } + player.teleport(level.randomSpawn()); + player.addEffect(new Potion(PotionEffect.RESISTANCE, (byte) 4, 60)); + player.addEffect(new Potion(PotionEffect.BLINDNESS, (byte) 0, 40)); + player.playSound(Sound.sound(SoundEvent.ENTITY_ELDER_GUARDIAN_CURSE, Sound.Source.HOSTILE, 1f, 0.5f), Sound.Emitter.self()); + new TitleMessage(Duration.ofSeconds(1)).appendTranslated("game_Backrooms#died").send(player); + this.updateSidebars(); + } + + private void stripInventoryExceptKeys(Player player) { + BackroomsItems.removeMatching(player, item -> !item.isAir() + && !BackroomsItems.is(item, BackroomsItems.LEVEL_KEY) + && !BackroomsItems.is(item, BackroomsItems.DOOR_COMPASS)); + } + + /** Uebergang ins naechste Level (durch Fall, Glitch-Boden oder No-Clip-Wand). */ + public void advanceLevel(Player player, boolean teleport) { + BackroomsPlayer state = this.playerState(player); + if(state == null) return; + int previous = state.getLevelIndex(); + int next = previous + 1; + if(next >= this.levels.size()) return; + state.setLevelIndex(next); + state.setLevelEnteredMillis(System.currentTimeMillis()); + + // Key und Kompass des alten Levels sind hier wertlos — weg damit + BackroomsItems.removeMatching(player, item -> BackroomsItems.isDoorItemFor(item, previous)); + + LevelRuntime level = this.levels.get(next); + // Ins Finale steigt man immer am Korridor-Anfang ein — sonst wuerde der + // Fall-Schacht mitten auf der Rennstrecke enden + boolean isFinale = level.getDefinition().type() == LevelType.FINALE; + if(teleport || isFinale) player.teleport(level.randomSpawn()); + + // Ist die Tuer dieses Levels schon offen, gibt es den Kompass zum Nachkommen + if(this.isDoorOpened(next)) this.chestManager.giveCompassIfMissing(player, next); + + // Sichtbarkeitsregeln werden nur beim Aufruf ausgewertet — fuer den neuen + // Spieler im Level muessen alle Monster ihre Viewer neu berechnen, sonst + // bleiben sie fuer ihn unsichtbar (greifen aber serverseitig an) + level.getMonsters().forEach(monster -> { + if(!monster.isRemoved()) monster.updateViewableRule(); + }); + + player.addEffect(new Potion(PotionEffect.BLINDNESS, (byte) 0, 40)); + player.playSound(Sound.sound(SoundEvent.BLOCK_PORTAL_TRAVEL, Sound.Source.AMBIENT, 0.4f, 0.5f), Sound.Emitter.self()); + new TitleMessage(Duration.ofSeconds(2)) + .appendTranslated(level.getDefinition().type().translationKey()) + .send(player); + this.updateSidebars(); + } + + /** Der Spieler hat das Finale durchquert und ist entkommen. */ + public void escape(Player player) { + BackroomsPlayer state = this.playerState(player); + if(state == null || state.isEscaped()) return; + state.setEscaped(true); + + player.setGameMode(GameMode.SPECTATOR); + new TitleMessage(Duration.ofSeconds(3)).appendTranslated("game_Backrooms#escaped").send(player); + this.playSound(Sound.sound(SoundEvent.UI_TOAST_CHALLENGE_COMPLETE, Sound.Source.MASTER, 0.8f, 1f)); + this.getScore().insertResult(player, 1); + this.updateSidebars(); + } + + private void createSidebar(Player player) { + Sidebar sidebar = new Sidebar(Component.text("Backrooms")); + sidebar.createLine(new Sidebar.ScoreboardLine("level", this.sidebarLevelLine(player), 2)); + sidebar.createLine(new Sidebar.ScoreboardLine("lives", this.sidebarLivesLine(player), 1)); + sidebar.createLine(new Sidebar.ScoreboardLine("players", this.sidebarPlayersLine(), 0)); + sidebar.addViewer(player); + this.sidebars.put(player.getUuid(), sidebar); + } + + private Component sidebarLevelLine(Player player) { + BackroomsPlayer state = this.playerState(player); + int index = state == null ? 0 : state.getLevelIndex(); + return TranslatedComponent.byId(this.levels.get(index).getDefinition().type().translationKey()).getAssembled(player); + } + + private Component sidebarLivesLine(Player player) { + BackroomsPlayer state = this.playerState(player); + return Component.text("❤ " + (state == null ? this.lives : Math.max(0, state.getLives()))); + } + + private Component sidebarPlayersLine() { + long active = this.playerStates.values().stream() + .filter(state -> !state.isEliminated() && !state.isEscaped()) + .count(); + return Component.text("☠ " + active); + } + + private void updateSidebars() { + this.getPlayers().forEach(player -> { + Sidebar sidebar = this.sidebars.get(player.getUuid()); + if(sidebar == null) return; + sidebar.updateLineContent("level", this.sidebarLevelLine(player)); + sidebar.updateLineContent("lives", this.sidebarLivesLine(player)); + sidebar.updateLineContent("players", this.sidebarPlayersLine()); + }); + } + + @Override + public Pos getSpawn() { + // Jeder Spieler bekommt einen zufaelligen, weit gestreuten Spawn in Level 0 + return this.levels.getFirst().randomSpawn(); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsGenerator.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsGenerator.java deleted file mode 100644 index 7e521b1..0000000 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsGenerator.java +++ /dev/null @@ -1,86 +0,0 @@ -package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms; - -import net.minestom.server.instance.block.Block; -import net.minestom.server.instance.generator.GenerationUnit; -import net.minestom.server.instance.generator.UnitModifier; -import java.util.concurrent.ThreadLocalRandom; - -public class BackroomsGenerator { - public void generateRoom(GenerationUnit unit, int y) { - var modifier = unit.modifier(); - var start = unit.absoluteStart(); - var yPos = start.blockY() + y; - - for (int x = 0; x < unit.size().blockX(); x++) { - for (int z = 0; z < unit.size().blockZ(); z++) { - var xPos = start.blockX() + x; - var zPos = start.blockZ() + z; - - modifier.setBlock(xPos, yPos - 1, zPos, Block.BEDROCK); - modifier.setBlock(xPos, yPos + 5, zPos, Block.BEDROCK); - - modifier.setBlock(xPos, yPos, zPos, Block.LIGHT_GRAY_WOOL); - modifier.setBlock(xPos, yPos + 4, zPos, Block.SMOOTH_STONE_SLAB); - - // idk man... dont ask about this - if ( - (x - 2) % 4 == 0 && ( - ((z - 2) % 8 == 0) || - ((z - 2) % 8 == 1) || - ((z - 2) % 8 == 3) || - ((z - 2) % 8 == 4) - ) - ) { - modifier.setBlock(xPos, yPos + 4, zPos, Block.SEA_LANTERN); - } - } - } - - generateWalls(modifier, start.blockX() + 0, yPos + 1, start.blockZ() + 0); - generateWalls(modifier, start.blockX() + 8, yPos + 1, start.blockZ() + 0); - generateWalls(modifier, start.blockX() + 0, yPos + 1, start.blockZ() + 8); - generateWalls(modifier, start.blockX() + 8, yPos + 1, start.blockZ() + 8); - } - - private void generateWalls(UnitModifier modifier, int xPos, int yPos, int zPos) { - generatePillar(modifier, xPos, yPos, zPos, Block.CHISELED_SANDSTONE); - - var random = ThreadLocalRandom.current(); - - var wall1 = random.nextInt(4) != 0; - var wall2 = random.nextInt(4) != 0; - var door1 = random.nextInt(2) != 0; - var door2 = random.nextInt(2) != 0; - - var door1pos = random.nextInt(2, 6); - var door2pos = random.nextInt(2, 6); - - if (wall1) { - for (int x = xPos; x < xPos + 8; x++) { - generatePillar(modifier, x, yPos, zPos, Block.SMOOTH_SANDSTONE); - } - } - - if (wall2) { - for (int z = zPos; z < zPos + 8; z++) { - generatePillar(modifier, xPos, yPos, z, Block.SMOOTH_SANDSTONE); - } - } - - if (door1 && wall1) { - generatePillar(modifier, xPos + door1pos, yPos, zPos, Block.AIR); - generatePillar(modifier, xPos + door1pos + 1, yPos, zPos, Block.AIR); - } - - if (door2 && wall2) { - generatePillar(modifier, xPos, yPos, zPos + door2pos, Block.AIR); - generatePillar(modifier, xPos, yPos, zPos + door2pos + 1, Block.AIR); - } - } - - private void generatePillar(UnitModifier modifier, int xPos, int yPos, int zPos, Block material) { - for (int y = yPos; y < yPos + 3; y++) { - modifier.setBlock(xPos, y, zPos, material); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsPlayer.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsPlayer.java new file mode 100644 index 0000000..8982939 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsPlayer.java @@ -0,0 +1,96 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms; + +/** + * Laufzeit-Zustand eines Spielers: Leben, aktuelles Level, Sanity und Fortschritt. + */ +public class BackroomsPlayer { + private int lives; + private int levelIndex = 0; + private double sanity = 1.0; + private boolean escaped = false; + private boolean eliminated = false; + private long lastDamageMillis = 0; + private long levelEnteredMillis = System.currentTimeMillis(); + + // Persoenlicher Finale-Zustand: jeder Spieler hat seine eigene Verfolgungsjagd + public static final int FINALE_NOT_STARTED = Integer.MIN_VALUE; + private int finaleSeconds = FINALE_NOT_STARTED; + private double finaleZoneX = Double.NEGATIVE_INFINITY; + + public BackroomsPlayer(int lives) { + this.lives = lives; + } + + public int getLives() { + return this.lives; + } + + public void loseLife() { + this.lives--; + } + + public int getLevelIndex() { + return this.levelIndex; + } + + public void setLevelIndex(int levelIndex) { + this.levelIndex = levelIndex; + } + + public double getSanity() { + return this.sanity; + } + + public void setSanity(double sanity) { + this.sanity = sanity; + } + + public boolean isEscaped() { + return this.escaped; + } + + public void setEscaped(boolean escaped) { + this.escaped = escaped; + } + + public boolean isEliminated() { + return this.eliminated; + } + + public void setEliminated(boolean eliminated) { + this.eliminated = eliminated; + } + + public int getFinaleSeconds() { + return this.finaleSeconds; + } + + public void setFinaleSeconds(int finaleSeconds) { + this.finaleSeconds = finaleSeconds; + } + + public double getFinaleZoneX() { + return this.finaleZoneX; + } + + public void setFinaleZoneX(double finaleZoneX) { + this.finaleZoneX = finaleZoneX; + } + + public long getLevelEnteredMillis() { + return this.levelEnteredMillis; + } + + public void setLevelEnteredMillis(long levelEnteredMillis) { + this.levelEnteredMillis = levelEnteredMillis; + } + + public long getLastDamageMillis() { + return this.lastDamageMillis; + } + + public void setLastDamageMillis(long lastDamageMillis) { + this.lastDamageMillis = lastDamageMillis; + } + +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsScore.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsScore.java new file mode 100644 index 0000000..f5545e9 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsScore.java @@ -0,0 +1,58 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms; + +import eu.mhsl.minenet.minigames.message.TranslatableMessage; +import eu.mhsl.minenet.minigames.message.type.TitleMessage; +import eu.mhsl.minenet.minigames.score.Score; +import net.minestom.server.entity.Player; + +import java.time.Duration; +import java.util.Set; + +/** + * Kombi-Wertung: Entkommene stehen in Flucht-Reihenfolge vorn; danach folgen die + * Gestorbenen, spaeterer Tod = besserer Platz. Der letzte Ueberlebende (via + * insertRemainingPlayers) landet direkt hinter den Entkommenen — gewinnt also, + * wenn niemand entkommen ist. + */ +public class BackroomsScore extends Score { + private int escapedCount = 0; + + /** Entkommen (Punkte-Pfad) */ + @Override + protected void insertResultImplementation(Set p, int points) { + this.getScores().add(this.escapedCount, p); + this.escapedCount++; + } + + /** Tod bzw. verbliebene Spieler bei Spielende */ + @Override + protected void insertResultImplementation(Set p) { + this.getScores().add(this.escapedCount, p); + } + + /** + * Wie Score.checkGameEnd, aber mit Untergrenze von einem echten Ergebnis: + * mit ignoreLastPlayers=1 und nur einem Spieler wuerde das Spiel sonst + * sofort nach dem Start enden (0 >= 1-1). + */ + @Override + protected void checkGameEnd() { + if(this.isClosed()) return; + if(!this.instance.isRunning()) return; + if(this.instance.getPlayers().isEmpty()) return; + + int required = Math.max(1, this.instance.getPlayers().size() - this.getIgnoreLastPlayers()); + if(this.resultCount() >= required) { + if(this.getIgnoreLastPlayers() > 0) { + this.insertRemainingPlayers(this.instance.getPlayers()); + } else { + this.setDone(); + } + } + } + + @Override + protected TranslatableMessage scoreMessage() { + return new TitleMessage(Duration.ofMillis(1000), Duration.ofSeconds(1)).appendTranslated("score#finish"); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsSmokeTest.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsSmokeTest.java new file mode 100644 index 0000000..95020f8 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/BackroomsSmokeTest.java @@ -0,0 +1,43 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import net.minestom.server.Auth; +import net.minestom.server.MinecraftServer; + +/** + * Wegwerf-Smoke-Test (headless, ohne Client): baut ein Spiel, spawnt die Monster + * und prueft, ob sie tatsaechlich in der Instanz landen. Wird nach der Diagnose geloescht. + */ +public class BackroomsSmokeTest { + public static void main(String[] args) throws Exception { + MinecraftServer server = MinecraftServer.init(new Auth.Offline()); + Dimension.values(); + server.start("127.0.0.1", 25599); + + BackroomsGame game = new BackroomsGame(4, 3, 2, 0, false, 8); + + for(LevelRuntime level : game.getLevels()) { + System.out.println("[SMOKE] Level " + level.getIndex() + " = " + level.getDefinition().type() + + ", baseY=" + level.getBaseY() + + ", cells=" + level.getLayout().getCells() + + ", spawns=" + level.getLayout().getSpawnCells().size() + + ", chests=" + level.getLayout().getChestCells().size() + + ", exit=" + java.util.Arrays.toString(level.getLayout().getExitCell())); + level.getDefinition().populateMonsters(game, level, 2); + } + + Thread.sleep(8000); + + for(LevelRuntime level : game.getLevels()) { + long inInstance = level.getMonsters().stream().filter(m -> m.getInstance() != null).count(); + System.out.println("[SMOKE] Level " + level.getIndex() + " (" + level.getDefinition().type() + "): " + + level.getMonsters().size() + " Monster erzeugt, " + inInstance + " in der Instanz"); + level.getMonsters().forEach(m -> System.out.println("[SMOKE] - " + m.getEntityType() + + " @ " + m.getPosition() + " instance=" + (m.getInstance() != null))); + } + + System.out.println("[SMOKE] done"); + System.exit(0); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/ClumpTriggerTest.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/ClumpTriggerTest.java new file mode 100644 index 0000000..91db854 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/ClumpTriggerTest.java @@ -0,0 +1,96 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.BackroomsMonster; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Clump; +import net.minestom.server.Auth; +import net.minestom.server.MinecraftServer; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.network.packet.server.SendablePacket; +import net.minestom.server.network.player.GameProfile; +import net.minestom.server.network.player.PlayerConnection; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.UUID; + +/** + * Wegwerf-Repro (headless): spawnt einen Clump, stellt einen Dummy-Spieler daneben + * und treibt monsterTick von Hand — prueft, ob der Jumpscare-Trigger feuert. + */ +public class ClumpTriggerTest { + public static void main(String[] args) throws Exception { + MinecraftServer server = MinecraftServer.init(new Auth.Offline()); + Dimension.values(); + server.start("127.0.0.1", 25598); + + BackroomsGame game = new BackroomsGame(4, 3, 2, 0, false, 8); + LevelRuntime level = game.getLevels().getFirst(); + + PlayerConnection connection = new PlayerConnection() { + @Override + public void sendPacket(SendablePacket packet) { + } + + @Override + public SocketAddress getRemoteAddress() { + return new InetSocketAddress("127.0.0.1", 0); + } + }; + Player player = new Player(connection, new GameProfile(UUID.randomUUID(), "Testificate")); + int[] spawnCell = level.getLayout().getSpawnCells().getFirst(); + player.setInstance(game, level.toPlayerPos(spawnCell)).join(); + player.setGameMode(GameMode.ADVENTURE); + System.out.println("[REPRO] player instance=" + (player.getInstance() != null) + " gamemode=" + player.getGameMode()); + + game.onStart(); + System.out.println("[REPRO] playerState=" + game.playerState(player)); + + Clump clump = new Clump(level) { + @Override + public void remove() { + new Exception("[REPRO] remove() aufgerufen, dead=" + this.isDead() + " health=" + this.getHealth()).printStackTrace(System.out); + super.remove(); + } + }; + try { + var position = level.toPlayerPos(spawnCell); + for(int dx = -1; dx <= 1; dx++) { + for(int dz = -1; dz <= 1; dz++) { + game.loadChunk(position.chunkX() + dx, position.chunkZ() + dz).join(); + } + } + System.out.println("[REPRO] chunks loaded, calling setInstance to " + position); + clump.setInstance(game, position).join(); + } catch(Throwable t) { + System.out.println("[REPRO] spawn failed:"); + t.printStackTrace(System.out); + } + Thread.sleep(1000); + System.out.println("[REPRO] clump instance=" + (clump.getInstance() != null) + " pos=" + clump.getPosition() + " removed=" + clump.isRemoved()); + + player.teleport(clump.getPosition().add(1.2, 0, 0)).join(); + Thread.sleep(500); + System.out.println("[REPRO] distance=" + clump.getDistance(player)); + System.out.println("[REPRO] playersInLevel(0)=" + game.playersInLevel(0).size()); + + Field stateField = BackroomsMonster.class.getDeclaredField("state"); + stateField.setAccessible(true); + for(long t = 2; t <= 160; t += 2) { + clump.monsterTick(t); + if(t % 20 == 0) { + System.out.println("[REPRO] t=" + t + " state=" + stateField.get(clump) + + " removed=" + clump.isRemoved() + + " velocity=" + clump.getVelocity() + + " playerHealth=" + player.getHealth()); + } + Thread.sleep(50); + } + + System.out.println("[REPRO] done"); + System.exit(0); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/ambience/AmbienceEngine.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/ambience/AmbienceEngine.java new file mode 100644 index 0000000..e3c4989 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/ambience/AmbienceEngine.java @@ -0,0 +1,70 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.entity.Player; +import net.minestom.server.sound.SoundEvent; +import net.minestom.server.timer.ExecutionType; +import net.minestom.server.timer.TaskSchedule; + +import java.util.Random; + +/** + * Spielt pro Level den Ambience-Loop und seltene positionale One-Shots, + * dazu global einen sehr leisen End-Soundtrack. Level ohne Spieler bleiben stumm. + */ +public class AmbienceEngine { + private final BackroomsGame game; + private final Random rnd = new Random(); + private int secondCounter = 0; + + public AmbienceEngine(BackroomsGame game) { + this.game = game; + } + + public void start() { + this.game.scheduler().submitTask(() -> { + if(!this.game.isRunning()) return TaskSchedule.stop(); + try { + this.tick(); + } catch(Exception e) { + // Ein Fehler darf den Ambience-Task nicht dauerhaft toeten + net.minestom.server.MinecraftServer.getExceptionManager().handleException(e); + } + return TaskSchedule.seconds(1); + }, ExecutionType.TICK_END); + } + + private void tick() { + this.secondCounter++; + + for(LevelRuntime level : this.game.getLevels()) { + var players = level.playersInLevel(); + if(players.isEmpty()) continue; + + AmbienceProfile profile = level.getDefinition().ambience(); + if(this.secondCounter % profile.loopPeriodSeconds() == 0) { + players.forEach(player -> player.playSound( + Sound.sound(profile.loop(), Sound.Source.AMBIENT, profile.loopVolume(), profile.loopPitch()), + Sound.Emitter.self() + )); + } + + for(Player player : players) { + if(this.rnd.nextDouble() >= profile.oneShotChancePerSecond()) continue; + if(profile.oneShots().isEmpty()) continue; + SoundEvent oneShot = profile.oneShots().get(this.rnd.nextInt(profile.oneShots().size())); + Jumpscare.playBehind(player, oneShot, Sound.Source.AMBIENT, 5, 0.6f, 0.8f); + } + } + + // Der End-Soundtrack als kaum hoerbare, unheimliche Untermalung + if(this.secondCounter % 180 == 1) { + this.game.getPlayers().forEach(player -> player.playSound( + Sound.sound(SoundEvent.MUSIC_END, Sound.Source.MUSIC, 0.05f, 1f), + Sound.Emitter.self() + )); + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/ambience/AmbienceProfile.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/ambience/AmbienceProfile.java new file mode 100644 index 0000000..9198767 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/ambience/AmbienceProfile.java @@ -0,0 +1,20 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience; + +import net.minestom.server.sound.SoundEvent; + +import java.util.List; + +/** + * Klangprofil eines Levels: wiederkehrender Basis-Loop plus seltene Zufalls-One-Shots + * (entfernte Schritte, Knarren, Fluestern ...), die die AmbienceEngine positional + * hinter den Spielern abspielt. + */ +public record AmbienceProfile( + SoundEvent loop, + float loopVolume, + float loopPitch, + int loopPeriodSeconds, + List oneShots, + double oneShotChancePerSecond +) { +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/ambience/Jumpscare.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/ambience/Jumpscare.java new file mode 100644 index 0000000..7ae476f --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/ambience/Jumpscare.java @@ -0,0 +1,81 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience; + +import net.kyori.adventure.sound.Sound; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.EquipmentSlot; +import net.minestom.server.entity.LivingEntity; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.Instance; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.network.packet.server.play.ChangeGameStatePacket; +import net.minestom.server.sound.SoundEvent; +import net.minestom.server.timer.TaskSchedule; + +import java.util.List; +import java.util.Random; + +/** + * Jumpscare-Rezepte. Alle Methoden sind bewusst kurz und komponierbar: + * sound() nur akustisch, visual() mit kurzem Schreckbild, fake() als Sanity-Halluzination. + */ +public final class Jumpscare { + private static final List SCARE_SOUNDS = List.of( + SoundEvent.ENTITY_ENDERMAN_SCREAM, + SoundEvent.ENTITY_CAT_HISS, + SoundEvent.ENTITY_ELDER_GUARDIAN_CURSE, + SoundEvent.AMBIENT_CAVE + ); + + private Jumpscare() { + } + + /** Positionsgebundener Sound schraeg hinter dem Spieler. */ + public static void playBehind(Player player, SoundEvent event, Sound.Source source, double distance, float volume, float pitch) { + Pos behind = player.getPosition().sub(player.getPosition().direction().mul(distance)); + player.playSound(Sound.sound(event, source, volume, pitch), behind.x(), behind.y(), behind.z()); + } + + /** Lauter Schreckenssound direkt hinter dem Spieler. */ + public static void sound(Player player, Random rnd) { + SoundEvent event = SCARE_SOUNDS.get(rnd.nextInt(SCARE_SOUNDS.size())); + playBehind(player, event, Sound.Source.HOSTILE, 3, 1.5f, 0.5f + rnd.nextFloat() * 0.2f); + } + + /** + * Schreckbild: der Elder-Guardian-Geist blitzt als Fullscreen-Overlay auf, + * gleichzeitig erscheint fuer einen Wimpernschlag ein Schaedel direkt vor dem + * Gesicht — Doppelschlag aus Overlay und physischem Schreckbild. + */ + public static void visual(Player player, Instance instance) { + player.sendPacket(new ChangeGameStatePacket(ChangeGameStatePacket.Reason.PLAYER_ELDER_GUARDIAN_MOB_APPEARANCE, 0f)); + + Vec direction = player.getPosition().direction(); + Pos position = player.getPosition() + .add(direction.mul(1.6)) + .add(0, player.getEyeHeight() - 1.4, 0) + .withYaw(player.getPosition().yaw() + 180) + .withPitch(0); + + LivingEntity scare = new LivingEntity(EntityType.ARMOR_STAND); + scare.updateViewableRule(viewer -> viewer.getUuid().equals(player.getUuid())); + scare.setInvisible(true); + scare.setNoGravity(true); + scare.setEquipment(EquipmentSlot.HELMET, ItemStack.of(Material.WITHER_SKELETON_SKULL)); + scare.setInstance(instance, position); + MinecraftServer.getSchedulerManager().scheduleTask(scare::remove, TaskSchedule.tick(8), TaskSchedule.stop()); + + player.playSound(Sound.sound(SoundEvent.ENTITY_ELDER_GUARDIAN_CURSE, Sound.Source.HOSTILE, 1.8f, 0.6f), player.getPosition()); + player.playSound(Sound.sound(SoundEvent.ENTITY_ENDERMAN_SCREAM, Sound.Source.HOSTILE, 1.2f, 0.5f), player.getPosition()); + } + + + /** Harmlose Halluzination bei niedriger Sanity: nur ein Geraeusch, keine Gefahr. */ + public static void fake(Player player, Random rnd) { + SoundEvent event = rnd.nextBoolean() ? SoundEvent.BLOCK_WOODEN_DOOR_CLOSE : SoundEvent.BLOCK_STONE_STEP; + playBehind(player, event, Sound.Source.AMBIENT, 4, 0.8f, 0.9f); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/door/DoorManager.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/door/DoorManager.java new file mode 100644 index 0000000..df49dd3 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/door/DoorManager.java @@ -0,0 +1,72 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.door; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.item.BackroomsItems; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.entity.Player; +import net.minestom.server.event.player.PlayerBlockInteractEvent; +import net.minestom.server.instance.block.Block; +import net.minestom.server.item.ItemStack; +import net.minestom.server.sound.SoundEvent; + +/** + * Verschlossene Exit-Kammern: die DOOR-Zelle ist mit IRON_BARS gefuellt und + * oeffnet sich nur mit dem Level Key des jeweiligen Levels. + */ +public class DoorManager { + private final BackroomsGame game; + + public DoorManager(BackroomsGame game) { + this.game = game; + } + + public void onInteract(PlayerBlockInteractEvent event) { + if(!event.getBlock().compare(Block.IRON_DOOR)) return; + + Player player = event.getPlayer(); + LevelRuntime level = this.game.levelAt(event.getBlockPosition().blockY()); + if(level == null) return; + + event.setCancelled(true); + event.setBlockingItemUse(true); + + ItemStack held = player.getItemInHand(event.getHand()); + if(!BackroomsItems.isKeyFor(held, level.getIndex())) { + player.playSound(Sound.sound(SoundEvent.BLOCK_CHEST_LOCKED, Sound.Source.BLOCK, 1f, 0.7f), player.getPosition()); + return; + } + + // Einzeltuer-Semantik (Finale-Duell-Kammern): Key oeffnet nur DIESE Tuer und wird verbraucht + if(level.getDefinition().opensSingleDoors()) { + player.setItemInHand(event.getHand(), held.withAmount(held.amount() - 1)); + this.openSingleDoor(level, event.getBlockPosition().blockX(), event.getBlockPosition().blockZ()); + player.playSound(Sound.sound(SoundEvent.BLOCK_IRON_DOOR_OPEN, Sound.Source.BLOCK, 1f, 0.6f), player.getPosition()); + return; + } + + this.openDoor(level); + this.game.markDoorOpened(level.getIndex()); + player.playSound(Sound.sound(SoundEvent.BLOCK_IRON_DOOR_OPEN, Sound.Source.BLOCK, 1f, 0.6f), player.getPosition()); + } + + /** Oeffnet nur die Tuer an der angeklickten Zelle. */ + private void openSingleDoor(LevelRuntime level, int blockX, int blockZ) { + LevelLayout layout = level.getLayout(); + int cx = layout.cellX(blockX); + int cz = layout.cellZ(blockZ); + if(layout.cellAt(cx, cz) != LevelLayout.Cell.DOOR) return; + int doorX = layout.worldX(cx) + 1; + int doorZ = layout.worldZ(cz) + 1; + this.game.setBlock(doorX, level.getBaseY() + 2, doorZ, Block.AIR); + this.game.setBlock(doorX, level.getBaseY() + 3, doorZ, Block.AIR); + } + + /** Oeffnet die Exit-Kammer-Tuer des Levels (Position kennt das Layout). */ + private void openDoor(LevelRuntime level) { + int[] door = level.getLayout().getDoorCell(); + if(door == null) return; + this.openSingleDoor(level, level.getLayout().worldX(door[0]) + 1, level.getLayout().worldZ(door[1]) + 1); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/generator/BackroomsChunkGenerator.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/generator/BackroomsChunkGenerator.java new file mode 100644 index 0000000..68ec824 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/generator/BackroomsChunkGenerator.java @@ -0,0 +1,335 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.generator; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout.Cell; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelPalette; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.TransitionType; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; +import net.minestom.server.instance.generator.Generator; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +/** + * Rendert die vorab berechneten Level-Layouts als vertikal gestapelte Ebenen. + * Reines Lookup: saemtliche Struktur (Maze, Exits, Kisten, Licht) steht im LevelLayout. + * + * Aufbau je Level (baseY b): b Bedrock, b+1 Boden, b+2..b+1+roomHeight Raum, + * b+2+roomHeight Decke, b+3+roomHeight Bedrock. Exit-Schaechte (HOLE/LADDER) durchbrechen + * Boden und die Decke des Levels darunter. + */ +public class BackroomsChunkGenerator implements Generator { + private final List levels; + /** O(1)-Lookups pro Level (statt Listen-Scans pro Blocksaeule) */ + private final List> lightKeys = new ArrayList<>(); + private final List> chestKeys = new ArrayList<>(); + + /** wiederverwendetes Random pro Generator-Thread (Seed pro Saeule gesetzt) */ + private static final ThreadLocal COLUMN_RANDOM = ThreadLocal.withInitial(Random::new); + + public BackroomsChunkGenerator(List levels) { + this.levels = levels; + for(LevelRuntime level : levels) { + Set lights = new HashSet<>(); + level.getLayout().getLightCells().forEach(cell -> lights.add(cellKey(cell[0], cell[1]))); + this.lightKeys.add(lights); + + Set chests = new HashSet<>(); + level.getLayout().getChestCells().forEach(cell -> chests.add(cellKey(cell[0], cell[1]))); + int[] keyChest = level.getLayout().getKeyChestCell(); + if(keyChest != null) chests.add(cellKey(keyChest[0], keyChest[1])); + this.chestKeys.add(chests); + } + } + + private static long cellKey(int cx, int cz) { + return ((long) cx << 32) | (cz & 0xFFFFFFFFL); + } + + @Override + public void generate(@NotNull GenerationUnit unit) { + Point start = unit.absoluteStart(); + + // Massive Zwischenschichten als ein fillHeight pro Level statt ~17 setBlock + // pro Saeule; Schacht-Saeulen werden in generateColumn wieder freigeraeumt + for(int levelIndex = 1; levelIndex < this.levels.size(); levelIndex++) { + LevelRuntime level = this.levels.get(levelIndex); + int bandStart = level.getBaseY() + 2 + level.getDefinition().palette().roomHeight() + 2; + unit.modifier().fillHeight(bandStart, level.getBaseY() + LevelRuntime.LEVEL_HEIGHT, Block.BEDROCK); + } + + for(int x = 0; x < unit.size().blockX(); x++) { + for(int z = 0; z < unit.size().blockZ(); z++) { + int worldX = start.blockX() + x; + int worldZ = start.blockZ() + z; + + for(int levelIndex = 0; levelIndex < this.levels.size(); levelIndex++) { + this.generateColumn(unit, this.levels.get(levelIndex), levelIndex, worldX, worldZ); + } + } + } + } + + private void generateColumn(GenerationUnit unit, LevelRuntime level, int levelIndex, int worldX, int worldZ) { + LevelLayout layout = level.getLayout(); + int cx = layout.cellX(worldX); + int cz = layout.cellZ(worldZ); + if(!layout.inBounds(cx, cz) || !layout.inGenerationBounds(cx, cz)) return; + + LevelPalette palette = level.getDefinition().palette(); + int baseY = level.getBaseY(); + int ceilingY = baseY + 2 + palette.roomHeight(); + Cell cell = layout.cellAt(cx, cz); + TransitionType transition = level.getDefinition().transitionType(); + + // deterministische Variation pro Saeule (fleckige Boeden/Waende) + Random rnd = COLUMN_RANDOM.get(); + rnd.setSeed(worldX * 341873128712L + worldZ * 132897987541L + levelIndex); + + boolean openShaft = cell == Cell.EXIT && (transition == TransitionType.HOLE || transition == TransitionType.LADDER); + + // Boden + Fundament + if(openShaft) { + // Schacht: kein Boden, Loch faellt bis ins Level darunter + } else if(cell == Cell.WATER) { + this.set(unit, worldX, baseY, worldZ, Block.BEDROCK); + this.set(unit, worldX, baseY + 1, worldZ, Block.WATER); + } else if(cell == Cell.DEEP_WATER) { + // doppelt tiefes Becken: zwei Wasserschichten + this.set(unit, worldX, baseY, worldZ, Block.BEDROCK); + this.set(unit, worldX, baseY + 1, worldZ, Block.WATER); + this.set(unit, worldX, baseY + 2, worldZ, Block.WATER); + } else if(cell == Cell.LEAK) { + // Undichtigkeit: Bodenpfuetze, Wassersaeule in der Zellenmitte bis zur Leitung + this.set(unit, worldX, baseY, worldZ, Block.BEDROCK); + this.set(unit, worldX, baseY + 1, worldZ, Block.WATER); + } else if(cell == Cell.EXIT && transition == TransitionType.GLITCH_FLOOR) { + this.set(unit, worldX, baseY, worldZ, Block.BEDROCK); + this.set(unit, worldX, baseY + 1, worldZ, (worldX + worldZ) % 2 == 0 ? Block.BLACK_CONCRETE : Block.PURPLE_CONCRETE); + } else if(this.isEscapeGoal(layout, cx, transition)) { + // Sichtbare Ziel-Zone am Ende des Finale-Korridors: leuchtender Boden + this.set(unit, worldX, baseY, worldZ, Block.BEDROCK); + this.set(unit, worldX, baseY + 1, worldZ, Block.GLOWSTONE); + } else { + this.set(unit, worldX, baseY, worldZ, Block.BEDROCK); + Block floor = this.isDoorStain(layout, cx, cz, rnd) + ? this.stainBlock(rnd) + : palette.floorAt(worldX, worldZ, rnd); + this.set(unit, worldX, baseY + 1, worldZ, floor); + } + + // Loot-Kisten (Barrels) in der Zellenmitte + boolean isChestColumn = !openShaft && this.isCenterColumn(worldX, worldZ) && this.isChestCell(levelIndex, cx, cz); + if(isChestColumn) { + this.set(unit, worldX, baseY + 2, worldZ, Block.BARREL); + } + + // Deko: seltene Boden-Details (Spinnweben, Kuchen, Sculk ...) und Decken-Details (Ketten, Lampen) + if(cell == Cell.FLOOR && !isChestColumn) { + if(!palette.floorDeco().isEmpty() && rnd.nextDouble() < palette.floorDecoChance()) { + this.set(unit, worldX, baseY + 2, worldZ, palette.rndFloorDeco(rnd)); + } + if(!palette.ceilingDeco().isEmpty() && rnd.nextDouble() < palette.ceilingDecoChance()) { + this.set(unit, worldX, ceilingY - 1, worldZ, palette.rndCeilingDeco(rnd)); + } + } + + // Raumvolumen + if(cell == Cell.WALL) { + // Sockelleiste unten, darueber die Wandvarianten — gibt jedem Level eine eigene Linie + boolean stained = this.isDoorStain(layout, cx, cz, rnd); + this.set(unit, worldX, baseY + 2, worldZ, stained ? this.stainBlock(rnd) : palette.wallBase()); + Block wall = palette.rndWall(rnd); + for(int y = baseY + 3; y < ceilingY; y++) { + this.set(unit, worldX, y, worldZ, stained && rnd.nextInt(3) > 0 ? this.stainBlock(rnd) : wall); + } + } else if(cell == Cell.PILLAR) { + for(int y = baseY + 2; y < ceilingY; y++) { + this.set(unit, worldX, y, worldZ, palette.pillar()); + } + } else if(cell == Cell.DOOR) { + // Tuerwand mit begehbarem Durchgang: entlang der Laufrichtung wird eine + // 1 Block breite Schneise durch die Zellentiefe gecarvt, mittig sitzt die Eisentuer + String facing = this.doorFacing(layout, cx, cz); + boolean passageAlongX = facing.equals("east") || facing.equals("west"); + int localX = Math.floorMod(worldX, LevelLayout.CELL_SIZE); + int localZ = Math.floorMod(worldZ, LevelLayout.CELL_SIZE); + boolean onPassage = passageAlongX ? localZ == 1 : localX == 1; + boolean isCenter = localX == 1 && localZ == 1; + + if(isCenter) { + this.set(unit, worldX, baseY + 2, worldZ, Block.IRON_DOOR.withProperties(Map.of("half", "lower", "facing", facing))); + this.set(unit, worldX, baseY + 3, worldZ, Block.IRON_DOOR.withProperties(Map.of("half", "upper", "facing", facing))); + for(int y = baseY + 4; y < ceilingY; y++) { + this.set(unit, worldX, y, worldZ, palette.rndWall(rnd)); + } + } else if(onPassage) { + // Durchgang: unten frei, Tuersturz darueber + for(int y = baseY + 4; y < ceilingY; y++) { + this.set(unit, worldX, y, worldZ, palette.rndWall(rnd)); + } + } else { + this.set(unit, worldX, baseY + 2, worldZ, palette.wallBase()); + Block wall = palette.rndWall(rnd); + for(int y = baseY + 3; y < ceilingY; y++) { + this.set(unit, worldX, y, worldZ, wall); + } + } + } else if(cell == Cell.THIN_WALL_X || cell == Cell.THIN_WALL_Z || cell == Cell.THIN_PILLAR) { + // Prototyp-Optik: 1 Block duenne Wandebene bzw. Eckpfosten in der Zellenmitte; + // der Pfosten schlaegt Wand-Arme zu angrenzenden Segmenten, damit an den + // Rasterpunkten keine Ein-Block-Luecken bleiben + int localX = Math.floorMod(worldX, LevelLayout.CELL_SIZE); + int localZ = Math.floorMod(worldZ, LevelLayout.CELL_SIZE); + boolean isCenter = localX == 1 && localZ == 1; + boolean solid = switch(cell) { + case THIN_WALL_X -> localZ == 1; + case THIN_WALL_Z -> localX == 1; + default -> isCenter + || localZ == 1 && localX == 0 && this.joinsThinWallX(layout, cx - 1, cz) + || localZ == 1 && localX == 2 && this.joinsThinWallX(layout, cx + 1, cz) + || localX == 1 && localZ == 0 && this.joinsThinWallZ(layout, cx, cz - 1) + || localX == 1 && localZ == 2 && this.joinsThinWallZ(layout, cx, cz + 1); + }; + if(solid) { + Block block = cell == Cell.THIN_PILLAR && isCenter ? palette.pillar() : palette.rndWall(rnd); + for(int y = baseY + 2; y < ceilingY; y++) { + this.set(unit, worldX, y, worldZ, block); + } + } + } else if(cell == Cell.PIPE || cell == Cell.LEAK) { + // Kupfer-Leitung unter der Decke; beim Leck laeuft eine Wassersaeule herab + Block duct = rnd.nextInt(3) == 0 ? Block.EXPOSED_COPPER : Block.WAXED_COPPER_BLOCK; + this.set(unit, worldX, ceilingY - 1, worldZ, duct); + if(cell == Cell.LEAK && this.isCenterColumn(worldX, worldZ)) { + for(int y = baseY + 2; y < ceilingY - 1; y++) { + this.set(unit, worldX, y, worldZ, Block.WATER); + } + } + } else if(cell == Cell.EXIT && transition == TransitionType.LADDER) { + // Leiter an der westlichen Schachtwand + if(Math.floorMod(worldX, LevelLayout.CELL_SIZE) == 0) { + for(int y = baseY - LevelRuntime.LEVEL_HEIGHT + 2; y < ceilingY; y++) { + this.set(unit, worldX, y, worldZ, Block.LADDER.withProperty("facing", "east")); + } + } + } + + // Decke + Abschluss; Exit-Schaechte des Levels darueber durchbrechen beides — + // dort muss auch die per fillHeight gesetzte Zwischenschicht wieder aufgehen + if(this.shaftFromAbove(levelIndex, worldX, worldZ)) { + // Roehre freiraeumen — bei Leiter-Schaechten bleibt die Leiter an der Westwand erhalten + LevelRuntime above = this.levels.get(levelIndex - 1); + boolean ladderColumn = above.getDefinition().transitionType() == TransitionType.LADDER + && Math.floorMod(worldX, LevelLayout.CELL_SIZE) == 0; + Block fill = ladderColumn ? Block.LADDER.withProperty("facing", "east") : Block.AIR; + for(int y = ceilingY; y < baseY + LevelRuntime.LEVEL_HEIGHT; y++) { + this.set(unit, worldX, y, worldZ, fill); + } + return; + } + + boolean isLight = this.isLightColumn(levelIndex, cx, cz, worldX, worldZ) + || this.isEscapeGoal(layout, cx, transition); + this.set(unit, worldX, ceilingY, worldZ, isLight ? palette.light() : palette.rndCeiling(rnd)); + this.set(unit, worldX, ceilingY + 1, worldZ, Block.BEDROCK); + } + + /** Grenzt an dieser Nachbarzelle ein Wandsegment entlang X (oder eine massive Wand) an? */ + private boolean joinsThinWallX(LevelLayout layout, int cx, int cz) { + Cell neighbour = layout.cellAt(cx, cz); + return neighbour == Cell.THIN_WALL_X || neighbour == Cell.WALL; + } + + /** Grenzt an dieser Nachbarzelle ein Wandsegment entlang Z (oder eine massive Wand) an? */ + private boolean joinsThinWallZ(LevelLayout layout, int cx, int cz) { + Cell neighbour = layout.cellAt(cx, cz); + return neighbour == Cell.THIN_WALL_Z || neighbour == Cell.WALL; + } + + /** + * "Ruined-Portal-Fleck" um die Schluesseltuer: Korruption breitet sich mit + * abnehmender Dichte ueber Boden und Waende aus und macht die Tuer weithin sichtbar. + */ + private boolean isDoorStain(LevelLayout layout, int cx, int cz, Random rnd) { + int[] door = layout.getDoorCell(); + if(door == null) return false; + int distance = Math.max(Math.abs(cx - door[0]), Math.abs(cz - door[1])); + if(distance > 3) return false; + double chance = switch(distance) { + case 0, 1 -> 0.85; + case 2 -> 0.45; + default -> 0.15; + }; + return rnd.nextDouble() < chance; + } + + private Block stainBlock(Random rnd) { + int roll = rnd.nextInt(10); + if(roll == 0) return Block.CRYING_OBSIDIAN; + if(roll == 1) return Block.GILDED_BLACKSTONE; + if(roll <= 4) return Block.OBSIDIAN; + return Block.BLACKSTONE; + } + + /** Ziel-Band des Finales: alle begehbaren Zellen ab der Exit-Zelle leuchten */ + private boolean isEscapeGoal(LevelLayout layout, int cx, TransitionType transition) { + if(transition != TransitionType.ESCAPE) return false; + int[] exit = layout.getExitCell(); + return exit != null && cx >= exit[0]; + } + + /** + * Blickrichtung der Tuer: vom Exit weg (Exit-Kammer) bzw. zur begehbaren + * Seite hin (Seitentueren im Finale-Korridor). + */ + private String doorFacing(LevelLayout layout, int cx, int cz) { + if(layout.cellAt(cx - 1, cz) == Cell.EXIT) return "east"; + if(layout.cellAt(cx + 1, cz) == Cell.EXIT) return "west"; + if(layout.cellAt(cx, cz - 1) == Cell.EXIT) return "south"; + if(layout.cellAt(cx, cz + 1) == Cell.EXIT) return "north"; + if(layout.isWalkable(cx, cz - 1) || layout.isWalkable(cx, cz + 1)) return "north"; + return "east"; + } + + /** Lichtblock in der Deckenmitte jeder Lichtzelle */ + private boolean isLightColumn(int levelIndex, int cx, int cz, int worldX, int worldZ) { + if(!this.isCenterColumn(worldX, worldZ)) return false; + return this.lightKeys.get(levelIndex).contains(cellKey(cx, cz)); + } + + private boolean isCenterColumn(int worldX, int worldZ) { + int centerOffset = LevelLayout.CELL_SIZE / 2; + return Math.floorMod(worldX - centerOffset, LevelLayout.CELL_SIZE) == 0 + && Math.floorMod(worldZ - centerOffset, LevelLayout.CELL_SIZE) == 0; + } + + private boolean isChestCell(int levelIndex, int cx, int cz) { + return this.chestKeys.get(levelIndex).contains(cellKey(cx, cz)); + } + + /** Faellt an dieser Saeule ein HOLE/LADDER-Schacht aus dem Level darueber herein? */ + private boolean shaftFromAbove(int levelIndex, int worldX, int worldZ) { + if(levelIndex == 0) return false; + LevelRuntime above = this.levels.get(levelIndex - 1); + TransitionType transition = above.getDefinition().transitionType(); + if(transition != TransitionType.HOLE && transition != TransitionType.LADDER) return false; + + LevelLayout aboveLayout = above.getLayout(); + int[] exit = aboveLayout.getExitCell(); + if(exit == null) return false; + return aboveLayout.cellX(worldX) == exit[0] && aboveLayout.cellZ(worldZ) == exit[1]; + } + + private void set(GenerationUnit unit, int x, int y, int z, Block block) { + unit.modifier().setBlock(x, y, z, block); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/generator/MazeAlgorithms.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/generator/MazeAlgorithms.java new file mode 100644 index 0000000..e91f051 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/generator/MazeAlgorithms.java @@ -0,0 +1,300 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.generator; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout.Cell; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.Random; + +/** + * Layout-Bausteine: Labyrinth (Recursive Backtracker), Braiding (Sackgassen oeffnen), + * Raum-Carving und BFS-Distanzkarten. Arbeitet rein auf dem Zellenraster — servertauglich + * und unit-testbar ohne Minestom. + */ +public final class MazeAlgorithms { + private static final int[][] DIRECTIONS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + + private MazeAlgorithms() { + } + + /** + * Carvt ein perfektes Labyrinth in das (komplett mit WALL gefuellte) Layout. + * Maze-Knoten liegen auf ungeraden Zellkoordinaten; der Rand (Zelle 0 und cells-1) bleibt Wand. + */ + public static void backtracker(LevelLayout layout, Random rnd) { + int cells = layout.getCells(); + boolean[][] visited = new boolean[cells][cells]; + Deque stack = new ArrayDeque<>(); + + int[] start = {1, 1}; + visited[start[0]][start[1]] = true; + layout.setCell(start[0], start[1], Cell.FLOOR); + stack.push(start); + + while(!stack.isEmpty()) { + int[] current = stack.peek(); + List candidates = new ArrayList<>(); + for(int[] dir : DIRECTIONS) { + int nx = current[0] + dir[0] * 2; + int nz = current[1] + dir[1] * 2; + if(nx <= 0 || nz <= 0 || nx >= cells - 1 || nz >= cells - 1) continue; + if(!visited[nx][nz]) candidates.add(new int[]{nx, nz, dir[0], dir[1]}); + } + + if(candidates.isEmpty()) { + stack.pop(); + continue; + } + + int[] next = candidates.get(rnd.nextInt(candidates.size())); + visited[next[0]][next[1]] = true; + layout.setCell(current[0] + next[2], current[1] + next[3], Cell.FLOOR); + layout.setCell(next[0], next[1], Cell.FLOOR); + stack.push(new int[]{next[0], next[1]}); + } + } + + /** + * Oeffnet den angegebenen Anteil aller Sackgassen, indem eine zufaellige + * angrenzende Wand zu FLOOR wird — macht das Labyrinth schleifenreich. + */ + public static void braid(LevelLayout layout, Random rnd, double ratio) { + int cells = layout.getCells(); + List deadEnds = new ArrayList<>(); + for(int x = 1; x < cells - 1; x++) { + for(int z = 1; z < cells - 1; z++) { + if(!layout.isWalkable(x, z)) continue; + int open = 0; + for(int[] dir : DIRECTIONS) { + if(layout.isWalkable(x + dir[0], z + dir[1])) open++; + } + if(open == 1) deadEnds.add(new int[]{x, z}); + } + } + + Collections.shuffle(deadEnds, rnd); + int toOpen = (int) Math.round(deadEnds.size() * ratio); + for(int i = 0; i < toOpen; i++) { + int[] deadEnd = deadEnds.get(i); + List walls = new ArrayList<>(); + for(int[] dir : DIRECTIONS) { + int wx = deadEnd[0] + dir[0]; + int wz = deadEnd[1] + dir[1]; + int behindX = deadEnd[0] + dir[0] * 2; + int behindZ = deadEnd[1] + dir[1] * 2; + if(wx <= 0 || wz <= 0 || wx >= cells - 1 || wz >= cells - 1) continue; + if(layout.cellAt(wx, wz) == Cell.WALL && layout.isWalkable(behindX, behindZ)) { + walls.add(new int[]{wx, wz}); + } + } + if(!walls.isEmpty()) { + int[] wall = walls.get(rnd.nextInt(walls.size())); + layout.setCell(wall[0], wall[1], Cell.FLOOR); + } + } + } + + /** + * Raeumt zufaellige Rechtecke frei (Raeume). Entfernt nur Waende, die Konnektivitaet + * des Labyrinths bleibt daher erhalten. + */ + public static void carveRooms(LevelLayout layout, Random rnd, int roomCount, int minSize, int maxSize) { + int cells = layout.getCells(); + for(int i = 0; i < roomCount; i++) { + int width = minSize + rnd.nextInt(maxSize - minSize + 1); + int depth = minSize + rnd.nextInt(maxSize - minSize + 1); + int startX = 1 + rnd.nextInt(Math.max(1, cells - width - 2)); + int startZ = 1 + rnd.nextInt(Math.max(1, cells - depth - 2)); + for(int x = startX; x < startX + width; x++) { + for(int z = startZ; z < startZ + depth; z++) { + if(x >= cells - 1 || z >= cells - 1) continue; + if(layout.cellAt(x, z) == Cell.WALL) layout.setCell(x, z, Cell.FLOOR); + } + } + } + } + + /** + * BFS-Distanzen (in Zellen) von einer Startzelle aus; -1 = unerreichbar. + */ + public static int[][] bfsDistances(LevelLayout layout, int[] start) { + int cells = layout.getCells(); + int[][] distances = new int[cells][cells]; + for(int[] row : distances) Arrays.fill(row, -1); + + Deque queue = new ArrayDeque<>(); + distances[start[0]][start[1]] = 0; + queue.add(start); + while(!queue.isEmpty()) { + int[] current = queue.poll(); + for(int[] dir : DIRECTIONS) { + int nx = current[0] + dir[0]; + int nz = current[1] + dir[1]; + if(!layout.inBounds(nx, nz) || !layout.isWalkable(nx, nz)) continue; + if(distances[nx][nz] != -1) continue; + distances[nx][nz] = distances[current[0]][current[1]] + 1; + queue.add(new int[]{nx, nz}); + } + } + return distances; + } + + /** + * Alle begehbaren Zellen, deren BFS-Distanz im Perzentil-Fenster [minPercentile, maxPercentile] + * der maximalen Distanz liegt. + */ + public static List cellsInDistancePercentile(LevelLayout layout, int[][] distances, double minPercentile, double maxPercentile) { + int max = 0; + for(int[] row : distances) { + for(int distance : row) max = Math.max(max, distance); + } + + List result = new ArrayList<>(); + for(int x = 0; x < layout.getCells(); x++) { + for(int z = 0; z < layout.getCells(); z++) { + int distance = distances[x][z]; + if(distance < 0) continue; + double percentile = max == 0 ? 0 : (double) distance / max; + if(percentile >= minPercentile && percentile <= maxPercentile) result.add(new int[]{x, z}); + } + } + return result; + } + + /** + * Offene Halle: alle Innenzellen werden begehbar, nur der Rand bleibt Wand. + * Grundlage fuer Level mit "grosser Raum statt Gaenge"-Charakter (Parkhaus, Poolrooms). + */ + public static void openField(LevelLayout layout) { + int cells = layout.getCells(); + for(int x = 1; x < cells - 1; x++) { + for(int z = 1; z < cells - 1; z++) { + layout.setCell(x, z, Cell.FLOOR); + } + } + } + + /** + * Verstreut gerade Wandzuege (mit gelegentlichen Luecken und L-Knicken) in einer + * offenen Flaeche — ergibt Sichtblocker und Raumgefuehl statt Korridore. + */ + public static void scatterWallRuns(LevelLayout layout, Random rnd, int runCount, int minLength, int maxLength) { + int cells = layout.getCells(); + for(int i = 0; i < runCount; i++) { + int x = 2 + rnd.nextInt(cells - 4); + int z = 2 + rnd.nextInt(cells - 4); + int[] dir = DIRECTIONS[rnd.nextInt(DIRECTIONS.length)]; + int length = minLength + rnd.nextInt(Math.max(1, maxLength - minLength + 1)); + + for(int step = 0; step < length; step++) { + if(x <= 1 || z <= 1 || x >= cells - 2 || z >= cells - 2) break; + // gelegentliche Luecke, damit keine Sackgassen-Kaefige entstehen + if(rnd.nextInt(8) != 0) layout.setCell(x, z, Cell.WALL); + // seltener L-Knick + if(rnd.nextInt(6) == 0) dir = DIRECTIONS[rnd.nextInt(DIRECTIONS.length)]; + x += dir[0]; + z += dir[1]; + } + } + } + + /** + * Verbindet eine (moeglicherweise eingemauerte) Zelle mit der Hauptkomponente: + * BFS durch Waende bis zur ersten Zelle mit rootDistances >= 0, Pfad wird gecarvt. + */ + public static void connectToComponent(LevelLayout layout, int[][] rootDistances, int[] start) { + int cells = layout.getCells(); + Deque queue = new ArrayDeque<>(); + java.util.Map cameFrom = new java.util.HashMap<>(); + queue.add(start); + cameFrom.put(key(start[0], start[1]), null); + + while(!queue.isEmpty()) { + int[] current = queue.poll(); + for(int[] dir : DIRECTIONS) { + int nx = current[0] + dir[0]; + int nz = current[1] + dir[1]; + if(nx <= 0 || nz <= 0 || nx >= cells - 1 || nz >= cells - 1) continue; + // Exit-Kammern sind tabu: weder durch die Tuer noch durch die Kammermauern carven + if(isChamberProtected(layout, nx, nz)) continue; + long cellKey = key(nx, nz); + if(cameFrom.containsKey(cellKey)) continue; + cameFrom.put(cellKey, current); + + if(rootDistances[nx][nz] >= 0) { + // Pfad zurueckverfolgen und freiraeumen + int[] step = current; + while(step != null) { + if(!layout.isWalkable(step[0], step[1])) layout.setCell(step[0], step[1], Cell.FLOOR); + step = cameFrom.get(key(step[0], step[1])); + } + return; + } + queue.add(new int[]{nx, nz}); + } + } + } + + /** + * Stellt sicher, dass alle wichtigen Zellen von root aus erreichbar sind; + * abgetrennte Zellen werden per Wanddurchbruch angebunden. + */ + public static void ensureConnected(LevelLayout layout, int[] root, List important) { + int[][] distances = bfsDistances(layout, root); + for(int[] cell : important) { + if(!layout.inBounds(cell[0], cell[1])) continue; + if(distances[cell[0]][cell[1]] >= 0) continue; + connectToComponent(layout, distances, cell); + distances = bfsDistances(layout, root); + } + } + + private static long key(int x, int z) { + return ((long) x << 32) | (z & 0xFFFFFFFFL); + } + + /** Zelle gehoert zur verschlossenen Exit-Kammer (Tuer, Exit oder Kammermauer-Ring). */ + public static boolean isChamberProtected(LevelLayout layout, int cx, int cz) { + LevelLayout.Cell cell = layout.cellAt(cx, cz); + if(cell == LevelLayout.Cell.DOOR || cell == LevelLayout.Cell.EXIT) return true; + for(int dx = -1; dx <= 1; dx++) { + for(int dz = -1; dz <= 1; dz++) { + if(layout.cellAt(cx + dx, cz + dz) == LevelLayout.Cell.EXIT) return true; + } + } + return false; + } + + /** + * Rejection-Sampling: bis zu count Zellen aus candidates mit paarweisem Mindestabstand. + * Faellt der Abstand nicht mehr erfuellbar aus, wird er schrittweise verringert. + */ + public static List sampleSpread(List candidates, Random rnd, int count, double minDistance) { + List pool = new ArrayList<>(candidates); + Collections.shuffle(pool, rnd); + List chosen = new ArrayList<>(); + double distance = minDistance; + while(chosen.size() < count && distance >= 0) { + for(int[] candidate : pool) { + if(chosen.size() >= count) break; + boolean fits = true; + for(int[] existing : chosen) { + double dx = candidate[0] - existing[0]; + double dz = candidate[1] - existing[1]; + if(Math.sqrt(dx * dx + dz * dz) < distance) { + fits = false; + break; + } + } + if(fits) chosen.add(candidate); + } + distance -= 1; + } + return chosen; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/item/BackroomsItems.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/item/BackroomsItems.java new file mode 100644 index 0000000..809f2d2 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/item/BackroomsItems.java @@ -0,0 +1,106 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.item; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.minestom.server.component.DataComponents; +import net.minestom.server.coordinate.Point; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.entity.Player; +import net.minestom.server.item.component.LodestoneTracker; +import net.minestom.server.tag.Tag; + +import java.util.function.Predicate; + +/** + * ItemStack-Fabriken und Tag-basierte Erkennung aller Backrooms-Items. + */ +public final class BackroomsItems { + public static final Tag ITEM_TAG = Tag.String("backroomsItem"); + public static final Tag KEY_LEVEL_TAG = Tag.Integer("backroomsKeyLevel"); + + public static final String ALMOND_WATER = "almond_water"; + public static final String ENERGY_BAR = "energy_bar"; + public static final String LEVEL_KEY = "level_key"; + public static final String DOOR_COMPASS = "door_compass"; + + private BackroomsItems() { + } + + /** Stellt Sanity wieder her und heilt leicht — das Kern-Loot der Backrooms. */ + public static ItemStack almondWater() { + return ItemStack.builder(Material.POTION) + .customName(Component.text("Almond Water", NamedTextColor.AQUA).decoration(TextDecoration.ITALIC, false)) + .set(ITEM_TAG, ALMOND_WATER) + .build(); + } + + /** Kurzer Geschwindigkeits-Schub (auch als Strecken-Pickup im Finale). */ + public static ItemStack energyBar() { + return ItemStack.builder(Material.GOLDEN_CARROT) + .customName(Component.text("Energy Bar", NamedTextColor.GOLD).decoration(TextDecoration.ITALIC, false)) + .set(ITEM_TAG, ENERGY_BAR) + .build(); + } + + /** Schluessel fuer die verschlossene Exit-Kammer des angegebenen Levels. */ + public static ItemStack levelKey(int levelIndex) { + return ItemStack.builder(Material.TRIPWIRE_HOOK) + .customName(Component.text("Level Key", NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false)) + .set(ITEM_TAG, LEVEL_KEY) + .set(KEY_LEVEL_TAG, levelIndex) + .build(); + } + + /** Kompass, der zur verschlossenen Tuer des Levels zeigt. */ + public static ItemStack doorCompass(int levelIndex, String dimension, Point doorPosition) { + return ItemStack.builder(Material.COMPASS) + .customName(Component.text("Door Compass", NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false)) + .set(DataComponents.LODESTONE_TRACKER, new LodestoneTracker(dimension, doorPosition, false)) + .set(ITEM_TAG, DOOR_COMPASS) + .set(KEY_LEVEL_TAG, levelIndex) + .build(); + } + + /** Entfernt alle Items, auf die das Praedikat passt, aus dem Spieler-Inventar. */ + public static void removeMatching(Player player, Predicate filter) { + var inventory = player.getInventory(); + for(int slot = 0; slot < inventory.getSize(); slot++) { + if(filter.test(inventory.getItemStack(slot))) inventory.setItemStack(slot, ItemStack.AIR); + } + } + + public static boolean hasMatching(Player player, Predicate filter) { + var inventory = player.getInventory(); + for(int slot = 0; slot < inventory.getSize(); slot++) { + if(filter.test(inventory.getItemStack(slot))) return true; + } + return false; + } + + public static int count(Player player, String itemId) { + int count = 0; + var inventory = player.getInventory(); + for(int slot = 0; slot < inventory.getSize(); slot++) { + ItemStack item = inventory.getItemStack(slot); + if(is(item, itemId)) count += item.amount(); + } + return count; + } + + public static boolean is(ItemStack item, String id) { + return id.equals(item.getTag(ITEM_TAG)); + } + + /** Key oder Tuerkompass fuer das angegebene Level (beides verfaellt beim Tueroeffnen). */ + public static boolean isDoorItemFor(ItemStack item, int levelIndex) { + String id = item.getTag(ITEM_TAG); + if(!LEVEL_KEY.equals(id) && !DOOR_COMPASS.equals(id)) return false; + return Integer.valueOf(levelIndex).equals(item.getTag(KEY_LEVEL_TAG)); + } + + public static boolean isKeyFor(ItemStack item, int levelIndex) { + return is(item, LEVEL_KEY) && Integer.valueOf(levelIndex).equals(item.getTag(KEY_LEVEL_TAG)); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/LevelDefinition.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/LevelDefinition.java new file mode 100644 index 0000000..0ad1596 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/LevelDefinition.java @@ -0,0 +1,229 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience.AmbienceProfile; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.generator.MazeAlgorithms; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.BackroomsMonster; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Bauplan eines Leveltyps: Layout-Erzeugung, Blockpalette, Uebergang, Ambience und + * Monster-Besatz. Pro Spiel wird je Level eine frische Instanz erzeugt — Level-Events + * (Blackouts, Lichtausfaelle, Duelle) duerfen daher eigenen Zustand halten. + */ +public abstract class LevelDefinition { + + public abstract LevelType type(); + + public abstract LevelLayout buildLayout(Random rnd, int cells, int playerCount); + + public abstract LevelPalette palette(); + + public abstract TransitionType transitionType(); + + public abstract AmbienceProfile ambience(); + + /** Initiale Monster-Population; density 1-3 aus der Spielkonfiguration. */ + public void populateMonsters(BackroomsGame game, LevelRuntime runtime, int density) { + } + + /** Wird ~1x pro Sekunde aufgerufen, solange Spieler im Level sind (Events wie Blackouts). */ + public void onLevelTick(BackroomsGame game, LevelRuntime runtime) { + } + + /** + * Dynamischer Nachschub des Monster-Directors: liefert ein Monster fuer die gegebene + * Zelle in Spielernaehe — oder null, wenn dort (oder in diesem Level) nichts spawnen soll. + * Das Monster wird vom Director gespawnt und registriert. + */ + public BackroomsMonster createRoamingMonster(LevelRuntime runtime, int[] cell) { + return null; + } + + /** Exit-Kammer verschlossen, Schluessel in einer Kiste versteckt? */ + public boolean usesKeyDoor() { + return false; + } + + /** + * true: jeder Key oeffnet nur die angeklickte Tuer und wird verbraucht (Finale-Seitentueren); + * false: ein Key oeffnet die Exit-Kammer fuer alle, danach zerfallen alle Keys. + */ + public boolean opensSingleDoors() { + return false; + } + + /** Permanente Dunkelheit (Lights Out): DARKNESS-Effekt unabhaengig von darkZones. */ + public boolean permanentDarkness() { + return false; + } + + /** + * Standard-Bauweise fuer Maze-Level: Backtracker, Braiding, Raeume, dann garantierte + * Platzierung von Spawns (verteilt), Exit (fern der Spawns, optional mit verschlossener + * Kammer + Schluessel), Kisten und Lichtzellen. + */ + protected LevelLayout standardLayout( + Random rnd, + int cells, + int playerCount, + double braidRatio, + int roomCount, + int maxRoomSize, + double chestsPerHundredCells + ) { + LevelLayout layout = new LevelLayout(cells); + MazeAlgorithms.backtracker(layout, rnd); + MazeAlgorithms.braid(layout, rnd, braidRatio); + MazeAlgorithms.carveRooms(layout, rnd, roomCount, 3, maxRoomSize); + return this.finalizeLayout(layout, rnd, playerCount, chestsPerHundredCells); + } + + /** + * Alternative Bauweise: offene Halle mit verstreuten Wandzuegen statt Gaenge-Labyrinth. + * Fuer Level mit "grosser offener Raum"-Charakter (Parkhaus, Poolrooms). + */ + protected LevelLayout openLayout( + Random rnd, + int cells, + int playerCount, + int wallRunCount, + double chestsPerHundredCells + ) { + LevelLayout layout = new LevelLayout(cells); + MazeAlgorithms.openField(layout); + MazeAlgorithms.scatterWallRuns(layout, rnd, wallRunCount, 3, Math.max(5, cells / 4)); + return this.finalizeLayout(layout, rnd, playerCount, chestsPerHundredCells); + } + + /** Gemeinsame Platzierung: Spawns, Exit (+Kammer), Kisten, Konnektivitaet, Licht. */ + protected LevelLayout finalizeLayout(LevelLayout layout, Random rnd, int playerCount, double chestsPerHundredCells) { + int cells = layout.getCells(); + + // Spawns: ueber die ganze Flaeche verteilt, damit auch viele Spieler nicht clustern + int[][] fromCorner = MazeAlgorithms.bfsDistances(layout, new int[]{1, 1}); + List allFloor = MazeAlgorithms.cellsInDistancePercentile(layout, fromCorner, 0, 1); + int spawnCount = Math.max(8, playerCount + 2); + layout.getSpawnCells().addAll(MazeAlgorithms.sampleSpread(allFloor, rnd, spawnCount, cells / 4.0)); + + // Exit: weit weg vom Spawn-Schwerpunkt + int[] centroid = this.spawnCentroid(layout); + int[][] fromCentroid = MazeAlgorithms.bfsDistances(layout, this.nearestWalkable(layout, centroid)); + List farCells = MazeAlgorithms.cellsInDistancePercentile(layout, fromCentroid, 0.8, 1.0); + int[] exit = farCells.isEmpty() ? allFloor.getLast() : farCells.get(rnd.nextInt(farCells.size())); + layout.setExitCell(exit); + layout.setCell(exit[0], exit[1], LevelLayout.Cell.EXIT); + + int[] doorOutside = this.usesKeyDoor() ? this.buildExitChamber(layout, rnd, fromCentroid, allFloor) : null; + + // Kisten verteilt ueber die Flaeche + int chestCount = (int) Math.max(3, cells * cells * chestsPerHundredCells / 100.0); + List chestSpots = MazeAlgorithms.sampleSpread(allFloor, rnd, chestCount, cells / 6.0); + chestSpots.removeIf(cell -> layout.cellAt(cell[0], cell[1]) != LevelLayout.Cell.FLOOR); + // Kisten nie auf Spawn-Zellen — sonst steht der Barrel im Spieler + chestSpots.removeIf(cell -> layout.getSpawnCells().stream() + .anyMatch(spawn -> spawn[0] == cell[0] && spawn[1] == cell[1])); + layout.getChestCells().addAll(chestSpots); + + // Die Kammer-Mauern (oder Pech beim Carven) koennen Bereiche abtrennen — + // alle spielrelevanten Zellen garantiert mit der Hauptkomponente verbinden + List important = new ArrayList<>(layout.getSpawnCells()); + important.addAll(layout.getChestCells()); + if(layout.getKeyChestCell() != null) important.add(layout.getKeyChestCell()); + if(doorOutside != null) important.add(doorOutside); + if(!this.usesKeyDoor()) important.add(layout.getExitCell()); + MazeAlgorithms.ensureConnected(layout, layout.getSpawnCells().getFirst(), important); + + // Lichtzellen im Raster, ausserhalb von Dunkelzonen (darkZones setzen Level-Impls vorher/nachher) + for(int x = 1; x < cells - 1; x += 2) { + for(int z = 1; z < cells - 1; z += 2) { + if(layout.isWalkable(x, z) && !layout.isDarkCell(x, z)) layout.getLightCells().add(new int[]{x, z}); + } + } + + return layout; + } + + /** + * Ummauert die Exit-Zelle (3x3) und setzt genau einen DOOR-Zugang mit garantiert + * begehbarer Zelle davor; der Schluessel landet als Kiste im freien Bereich. + * + * @return die Zelle direkt vor der Tuer (fuer die Konnektivitaetspruefung) + */ + private int[] buildExitChamber(LevelLayout layout, Random rnd, int[][] distances, List allFloor) { + int[] exit = layout.getExitCell(); + + // Tuer-Richtung waehlen, bevor der Ring zugemauert wird: bevorzugt eine Richtung, + // deren Vorfeld-Zelle (exit + 2*dir) bereits begehbar ist + int[][] directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + List valid = new ArrayList<>(); + List preferred = new ArrayList<>(); + for(int[] dir : directions) { + int outsideX = exit[0] + dir[0] * 2; + int outsideZ = exit[1] + dir[1] * 2; + if(outsideX <= 0 || outsideZ <= 0 || outsideX >= layout.getCells() - 1 || outsideZ >= layout.getCells() - 1) continue; + valid.add(dir); + if(layout.isWalkable(outsideX, outsideZ)) preferred.add(dir); + } + List pool = preferred.isEmpty() ? valid : preferred; + int[] doorDir = pool.get(rnd.nextInt(pool.size())); + + // Ring zumauern + for(int dx = -1; dx <= 1; dx++) { + for(int dz = -1; dz <= 1; dz++) { + if(dx == 0 && dz == 0) continue; + int x = exit[0] + dx; + int z = exit[1] + dz; + if(!layout.inBounds(x, z)) continue; + layout.setCell(x, z, LevelLayout.Cell.WALL); + } + } + + int[] door = {exit[0] + doorDir[0], exit[1] + doorDir[1]}; + int[] outside = {exit[0] + doorDir[0] * 2, exit[1] + doorDir[1] * 2}; + layout.setCell(door[0], door[1], LevelLayout.Cell.DOOR); + layout.setDoorCell(door); + if(!layout.isWalkable(outside[0], outside[1])) layout.setCell(outside[0], outside[1], LevelLayout.Cell.FLOOR); + + List keySpots = MazeAlgorithms.cellsInDistancePercentile(layout, distances, 0.3, 0.7); + keySpots.removeIf(cell -> layout.cellAt(cell[0], cell[1]) != LevelLayout.Cell.FLOOR); + // Fallback nur auf Zellen, die nach dem Kammerbau noch begehbar sind + List keyFallback = allFloor.stream() + .filter(cell -> layout.cellAt(cell[0], cell[1]) == LevelLayout.Cell.FLOOR) + .toList(); + int[] keyCell = keySpots.isEmpty() + ? keyFallback.get(rnd.nextInt(keyFallback.size())) + : keySpots.get(rnd.nextInt(keySpots.size())); + layout.setKeyChestCell(keyCell); + + return outside; + } + + private int[] spawnCentroid(LevelLayout layout) { + int sumX = 0; + int sumZ = 0; + for(int[] spawn : layout.getSpawnCells()) { + sumX += spawn[0]; + sumZ += spawn[1]; + } + int count = Math.max(1, layout.getSpawnCells().size()); + return new int[]{sumX / count, sumZ / count}; + } + + private int[] nearestWalkable(LevelLayout layout, int[] cell) { + if(layout.isWalkable(cell[0], cell[1])) return cell; + for(int radius = 1; radius < layout.getCells(); radius++) { + for(int dx = -radius; dx <= radius; dx++) { + for(int dz = -radius; dz <= radius; dz++) { + int x = cell[0] + dx; + int z = cell[1] + dz; + if(layout.inBounds(x, z) && layout.isWalkable(x, z)) return new int[]{x, z}; + } + } + } + return new int[]{1, 1}; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/LevelLayout.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/LevelLayout.java new file mode 100644 index 0000000..37fd786 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/LevelLayout.java @@ -0,0 +1,187 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level; + +import net.minestom.server.coordinate.Pos; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Vorab berechnetes Layout eines Levels auf einem quadratischen Zellenraster. + * Eine Zelle ist {@link #CELL_SIZE} Bloecke breit; das Raster ist um x=0/z=0 zentriert. + * Der Chunk-Generator liest ausschliesslich aus dieser Struktur. + */ +public class LevelLayout { + public static final int CELL_SIZE = 3; + + public enum Cell { + WALL, + FLOOR, + WATER, + /** doppelt tiefes Wasserbecken */ + DEEP_WATER, + PILLAR, + /** Deckenleitung (Kupfer-Kanal unter der Decke, darunter begehbar) */ + PIPE, + /** duenne Wand (1 Block) durch die Zellenmitte entlang der X-Achse — Prototyp-Optik von Level 0 */ + THIN_WALL_X, + /** duenne Wand (1 Block) durch die Zellenmitte entlang der Z-Achse */ + THIN_WALL_Z, + /** einzelner Eckpfosten (1x1) in der Zellenmitte */ + THIN_PILLAR, + /** undichte Leitung: Wassersaeule laeuft von der Decke bis in eine Bodenpfuetze */ + LEAK, + /** Exit-Zelle: Wirkung haengt vom TransitionType des Levels ab (Loch, Leiter, Glitch, NoClip-Wand) */ + EXIT, + /** verschlossener Zugang zur Exit-Kammer, wird vom DoorManager geoeffnet */ + DOOR + } + + private final int cells; + private final Cell[][] grid; + private final boolean[][] darkZone; + + private final List spawnCells = new ArrayList<>(); + private final List chestCells = new ArrayList<>(); + private final List lightCells = new ArrayList<>(); + /** frei nutzbare Level-Marker (z. B. Dampf-Ventile in den Poolrooms) */ + private final List markerCells = new ArrayList<>(); + private int[] exitCell; + private int[] keyChestCell; + private int[] doorCell; + + // Generierungs-Bounds: ausserhalb rendert der Generator nichts (Finale nutzt + // nur einen schmalen Korridor seines quadratischen Rasters) + private int minGenCx = 0; + private int maxGenCx = Integer.MAX_VALUE; + private int minGenCz = 0; + private int maxGenCz = Integer.MAX_VALUE; + + public LevelLayout(int cells) { + this.cells = cells; + this.grid = new Cell[cells][cells]; + this.darkZone = new boolean[cells][cells]; + for(int x = 0; x < cells; x++) { + for(int z = 0; z < cells; z++) { + this.grid[x][z] = Cell.WALL; + } + } + } + + public int getCells() { + return this.cells; + } + + public boolean inBounds(int cx, int cz) { + return cx >= 0 && cx < this.cells && cz >= 0 && cz < this.cells; + } + + public Cell cellAt(int cx, int cz) { + if(!this.inBounds(cx, cz)) return Cell.WALL; + return this.grid[cx][cz]; + } + + public void setCell(int cx, int cz, Cell cell) { + this.grid[cx][cz] = cell; + } + + public boolean isWalkable(int cx, int cz) { + return switch(this.cellAt(cx, cz)) { + case FLOOR, WATER, DEEP_WATER, PIPE, LEAK, EXIT -> true; + default -> false; + }; + } + + + public boolean isDarkCell(int cx, int cz) { + return this.inBounds(cx, cz) && this.darkZone[cx][cz]; + } + + public void setDark(int cx, int cz, boolean dark) { + this.darkZone[cx][cz] = dark; + } + + /** + * Weltkoordinate (Block-Ecke) der Zelle; das Raster ist um den Ursprung zentriert. + */ + public int worldX(int cx) { + return (cx - this.cells / 2) * CELL_SIZE; + } + + public int worldZ(int cz) { + return this.worldX(cz); + } + + /** Zellkoordinate zu einer Block-Weltkoordinate */ + public int cellX(double worldX) { + return (int) Math.floor(worldX / CELL_SIZE) + this.cells / 2; + } + + public int cellZ(double worldZ) { + return this.cellX(worldZ); + } + + /** Spieler-Position in der Zellenmitte, einen Block ueber dem Boden */ + public Pos worldCenter(int[] cell, int baseY) { + return new Pos(this.worldX(cell[0]) + CELL_SIZE / 2.0, baseY + 2, this.worldZ(cell[1]) + CELL_SIZE / 2.0); + } + + public boolean isDarkAt(Pos position) { + return this.isDarkCell(this.cellX(position.x()), this.cellZ(position.z())); + } + + public List getSpawnCells() { + return this.spawnCells; + } + + public List getChestCells() { + return this.chestCells; + } + + public List getLightCells() { + return this.lightCells; + } + + public List getMarkerCells() { + return this.markerCells; + } + + public int[] getExitCell() { + return this.exitCell; + } + + public void setExitCell(int[] exitCell) { + this.exitCell = exitCell; + } + + public int[] getKeyChestCell() { + return this.keyChestCell; + } + + public int[] getDoorCell() { + return this.doorCell; + } + + public void setDoorCell(int[] doorCell) { + this.doorCell = doorCell; + } + + public void setKeyChestCell(int[] keyChestCell) { + this.keyChestCell = keyChestCell; + } + + public void setGenerationBounds(int minCx, int maxCx, int minCz, int maxCz) { + this.minGenCx = minCx; + this.maxGenCx = maxCx; + this.minGenCz = minCz; + this.maxGenCz = maxCz; + } + + public boolean inGenerationBounds(int cx, int cz) { + return cx >= this.minGenCx && cx <= this.maxGenCx && cz >= this.minGenCz && cz <= this.maxGenCz; + } + + public int[] randomSpawnCell(Random rnd) { + return this.spawnCells.get(rnd.nextInt(this.spawnCells.size())); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/LevelPalette.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/LevelPalette.java new file mode 100644 index 0000000..90151ad --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/LevelPalette.java @@ -0,0 +1,56 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level; + +import net.minestom.server.instance.block.Block; + +import java.util.List; +import java.util.Random; + +/** + * Blockauswahl eines Levels. Listen erlauben leichte Variation (z. B. fleckiger Teppich); + * die Auswahl erfolgt deterministisch aus dem Layout-Random. + * + * Fuer eine deutliche visuelle Identitaet pro Leveltyp: + * - wallBase: abweichende unterste Wandschicht (Sockelleiste) + * - checkerFloor: Schachbrettmuster aus den ersten beiden floor-Eintraegen + * - floorDeco/ceilingDeco: seltene Deko-Bloecke auf dem Boden bzw. unter der Decke + */ +public record LevelPalette( + List floor, + List wall, + List ceiling, + Block light, + Block pillar, + int roomHeight, + Block wallBase, + boolean checkerFloor, + List floorDeco, + double floorDecoChance, + List ceilingDeco, + double ceilingDecoChance +) { + + public Block rndFloor(Random rnd) { + return this.floor.get(rnd.nextInt(this.floor.size())); + } + + public Block floorAt(int worldX, int worldZ, Random rnd) { + if(!this.checkerFloor) return this.rndFloor(rnd); + return this.floor.get(Math.floorMod(worldX + worldZ, 2)); + } + + public Block rndWall(Random rnd) { + return this.wall.get(rnd.nextInt(this.wall.size())); + } + + public Block rndCeiling(Random rnd) { + return this.ceiling.get(rnd.nextInt(this.ceiling.size())); + } + + public Block rndFloorDeco(Random rnd) { + return this.floorDeco.get(rnd.nextInt(this.floorDeco.size())); + } + + public Block rndCeilingDeco(Random rnd) { + return this.ceilingDeco.get(rnd.nextInt(this.ceilingDeco.size())); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/LevelRuntime.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/LevelRuntime.java new file mode 100644 index 0000000..a540dcc --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/LevelRuntime.java @@ -0,0 +1,87 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.BackroomsMonster; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Laufzeit-Zustand eines Levels innerhalb einer Runde: Layout, Hoehenlage, + * aktive Monster und Level-Events. Die Level sind in einer Instanz vertikal gestapelt. + */ +public class LevelRuntime { + public static final int LEVEL_HEIGHT = 24; + public static final int TOP_BASE_Y = 96; + + private final BackroomsGame game; + private final int index; + private final LevelDefinition definition; + private final LevelLayout layout; + private final Random rnd; + private final List monsters = new ArrayList<>(); + + private boolean blackoutActive = false; + + + public LevelRuntime(BackroomsGame game, int index, LevelDefinition definition, LevelLayout layout, Random rnd) { + this.game = game; + this.index = index; + this.definition = definition; + this.layout = layout; + this.rnd = rnd; + } + + public static int baseY(int index) { + return TOP_BASE_Y - index * LEVEL_HEIGHT; + } + + public int getBaseY() { + return baseY(this.index); + } + + public int getIndex() { + return this.index; + } + + public LevelDefinition getDefinition() { + return this.definition; + } + + public LevelLayout getLayout() { + return this.layout; + } + + public Random getRnd() { + return this.rnd; + } + + public List getMonsters() { + return this.monsters; + } + + public boolean isBlackoutActive() { + return this.blackoutActive; + } + + public void setBlackoutActive(boolean blackoutActive) { + this.blackoutActive = blackoutActive; + } + + + public List playersInLevel() { + return this.game.playersInLevel(this.index); + } + + /** Spieler-Position (Zellenmitte) fuer eine Layout-Zelle dieses Levels */ + public Pos toPlayerPos(int[] cell) { + return this.layout.worldCenter(cell, this.getBaseY()); + } + + public Pos randomSpawn() { + return this.toPlayerPos(this.layout.randomSpawnCell(this.rnd)); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/LevelType.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/LevelType.java new file mode 100644 index 0000000..59668f5 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/LevelType.java @@ -0,0 +1,25 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level; + +/** + * Alle implementierten Backrooms-Level. LOBBY ist immer das Startlevel, + * FINALE immer das letzte; der Pool dazwischen steht in BackroomsGame.LEVEL_POOL. + */ +public enum LevelType { + LOBBY("level_lobby"), + HABITABLE_ZONE("level_habitable"), + PIPE_DREAMS("level_pipeDreams"), + POOLROOMS("level_poolrooms"), + LIGHTS_OUT("level_lightsOut"), + FUN("level_fun"), + FINALE("level_finale"); + + private final String translationKey; + + LevelType(String translationKey) { + this.translationKey = translationKey; + } + + public String translationKey() { + return "game_Backrooms#" + this.translationKey; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/TransitionType.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/TransitionType.java new file mode 100644 index 0000000..b2df745 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/TransitionType.java @@ -0,0 +1,15 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level; + +/** + * Art des Level-Ausgangs an der Exit-Zelle. + */ +public enum TransitionType { + /** Loch im Boden: offener Schacht, der Spieler faellt physisch ins naechste Level */ + HOLE, + /** Leiter-Schacht nach unten */ + LADDER, + /** flackernder "Glitch"-Boden: Betreten teleportiert */ + GLITCH_FLOOR, + /** Finale: Ziel-Region erreichen = entkommen */ + ESCAPE +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/Cells.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/Cells.java new file mode 100644 index 0000000..4125494 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/Cells.java @@ -0,0 +1,113 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.impl; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.generator.MazeAlgorithms; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.function.BiPredicate; + +/** + * Gemeinsame Zellen-Hilfen der Level-Implementierungen: Monster-Platzierung fern + * der Spawns und Schutz reservierter Zellen (Spawns, Kisten, Schluessel-Kiste). + */ +final class Cells { + private static final int MONSTER_MIN_SPAWN_DISTANCE = 10; + + private Cells() { + } + + /** + * Zufaellige begehbare Zellen mit Mindestabstand (> 10 Zellen) zu allen Spawns; + * der Abstand wird schrittweise gesenkt, falls sich sonst zu wenige Zellen finden. + */ + static List farWalkable(LevelLayout layout, Random rnd, int count) { + return far(layout, rnd, count, (x, z) -> true); + } + + /** + * Flaechen-Faktor fuer Monsteranzahlen: grosse Karten (mehr Spieler) brauchen + * proportional mehr Monster, sonst wirken sie leer. + */ + static int areaScale(LevelLayout layout) { + return Math.max(1, layout.getCells() / 16); + } + + /** Wie {@link #farWalkable}, zusaetzlich gefiltert; liefert notfalls weniger als count Zellen. */ + static List far(LevelLayout layout, Random rnd, int count, BiPredicate filter) { + for(int minDistance = MONSTER_MIN_SPAWN_DISTANCE; ; minDistance -= 2) { + List candidates = new ArrayList<>(); + for(int x = 0; x < layout.getCells(); x++) { + for(int z = 0; z < layout.getCells(); z++) { + if(!layout.isWalkable(x, z) || !filter.test(x, z)) continue; + if(distanceToSpawns(layout, x, z) > minDistance) candidates.add(new int[]{x, z}); + } + } + if(candidates.size() >= count || minDistance <= 0) { + // Paarweiser Mindestabstand, damit die Monster nicht am selben Fleck stehen + double spread = Math.max(4, layout.getCells() / 8.0); + return MazeAlgorithms.sampleSpread(candidates, rnd, count, spread); + } + } + } + + static double distanceToSpawns(LevelLayout layout, int cx, int cz) { + double min = Double.MAX_VALUE; + for(int[] spawn : layout.getSpawnCells()) { + double dx = cx - spawn[0]; + double dz = cz - spawn[1]; + min = Math.min(min, Math.sqrt(dx * dx + dz * dz)); + } + return min; + } + + /** Zelle ist als Spawn, Kiste oder Schluessel-Kiste reserviert und darf nicht ueberbaut werden. */ + static boolean reserved(LevelLayout layout, int cx, int cz) { + int[] keyChest = layout.getKeyChestCell(); + return contains(layout.getSpawnCells(), cx, cz) + || contains(layout.getChestCells(), cx, cz) + || (keyChest != null && keyChest[0] == cx && keyChest[1] == cz); + } + + static boolean contains(List cells, int cx, int cz) { + for(int[] cell : cells) { + if(cell[0] == cx && cell[1] == cz) return true; + } + return false; + } + + static int openOrthogonal(LevelLayout layout, int cx, int cz) { + int open = 0; + if(layout.isWalkable(cx + 1, cz)) open++; + if(layout.isWalkable(cx - 1, cz)) open++; + if(layout.isWalkable(cx, cz + 1)) open++; + if(layout.isWalkable(cx, cz - 1)) open++; + return open; + } + + static void removeLight(LevelLayout layout, int cx, int cz) { + layout.getLightCells().removeIf(cell -> cell[0] == cx && cell[1] == cz); + } + + /** + * Stempelt kreisfoermige Dunkelzonen ins Layout und entfernt dort die Beleuchtung. + */ + static void darkBlobs(LevelLayout layout, Random rnd, int count, int minRadius, int maxRadius) { + int cells = layout.getCells(); + for(int i = 0; i < count; i++) { + int centerX = 3 + rnd.nextInt(Math.max(1, cells - 6)); + int centerZ = 3 + rnd.nextInt(Math.max(1, cells - 6)); + int radius = minRadius + rnd.nextInt(Math.max(1, maxRadius - minRadius + 1)); + for(int x = centerX - radius; x <= centerX + radius; x++) { + for(int z = centerZ - radius; z <= centerZ + radius; z++) { + if(!layout.inBounds(x, z)) continue; + double dx = x - centerX; + double dz = z - centerZ; + if(Math.sqrt(dx * dx + dz * dz) <= radius) layout.setDark(x, z, true); + } + } + } + layout.getLightCells().removeIf(cell -> layout.isDarkCell(cell[0], cell[1])); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/Level0Lobby.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/Level0Lobby.java new file mode 100644 index 0000000..ecf10d4 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/Level0Lobby.java @@ -0,0 +1,151 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.impl; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience.AmbienceProfile; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.generator.MazeAlgorithms; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelDefinition; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelPalette; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelType; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.TransitionType; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Bacteria; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.BackroomsMonster; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Hound; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Smiler; +import net.minestom.server.instance.block.Block; +import net.minestom.server.sound.SoundEvent; + +import java.util.List; +import java.util.Random; + +/** + * Level 0 "The Lobby": endlose Raumfluchten im Stil des urspruenglichen Prototyps — + * duenne Sandsteinwaende mit Tuerluecken im Raster, heller Wollteppich, summende + * Deckenlampen, dazu dunkle Blackout-Zonen mit lauernden Smilern. + * Ausgang: Loch im Boden hinter einer verschlossenen Tuer. + */ +public class Level0Lobby extends LevelDefinition { + // Palette nach dem urspruenglichen Prototyp: heller Wollboden, Smooth Sandstone, + // Sea Lanterns im Deckenraster, gemeisselte Sandstein-Eckpfosten + private static final LevelPalette PALETTE = new LevelPalette( + List.of(Block.LIGHT_GRAY_WOOL, Block.LIGHT_GRAY_WOOL, Block.LIGHT_GRAY_WOOL, Block.BROWN_WOOL), + List.of(Block.SMOOTH_SANDSTONE, Block.SMOOTH_SANDSTONE, Block.SMOOTH_SANDSTONE, Block.SANDSTONE), + List.of(Block.SMOOTH_STONE, Block.SMOOTH_STONE, Block.STONE), + Block.SEA_LANTERN, + Block.CHISELED_SANDSTONE, + 3, + Block.SMOOTH_SANDSTONE, + false, + List.of(Block.COBWEB), + 0.008, + List.of(), + 0 + ); + + /** Rasterweite des Prototyp-Wandmusters (in Zellen) */ + private static final int SEGMENT_GRID = 3; + + @Override + public LevelType type() { + return LevelType.LOBBY; + } + + /** + * Generierung nach dem urspruenglichen Prototyp: offene Flaeche, in die ein + * 4-Zellen-Raster aus duennen Sandsteinwand-Segmenten mit zufaelligen + * Tuerluecken und Eckpfosten gelegt wird — statt eines Gaenge-Labyrinths. + */ + @Override + public LevelLayout buildLayout(Random rnd, int cells, int playerCount) { + LevelLayout layout = new LevelLayout(cells); + MazeAlgorithms.openField(layout); + + // Eckpfosten an den Rasterpunkten + for(int gx = SEGMENT_GRID; gx < cells - 1; gx += SEGMENT_GRID) { + for(int gz = SEGMENT_GRID; gz < cells - 1; gz += SEGMENT_GRID) { + layout.setCell(gx, gz, LevelLayout.Cell.THIN_PILLAR); + + // je Richtung: ~83% vorhanden, davon die Haelfte mit Tuerluecke + this.carveSegment(layout, rnd, gx, gz, true); + this.carveSegment(layout, rnd, gx, gz, false); + } + } + + this.finalizeLayout(layout, rnd, playerCount, 2); + + // Blackout-Zonen: dunkle Flecken, in denen die Deckenbeleuchtung fehlt + Cells.darkBlobs(layout, rnd, 2 + rnd.nextInt(3), 3, 6); + + return layout; + } + + + /** Ein Wandsegment vom Rasterpunkt aus entlang +x bzw. +z, mit optionaler Tuerluecke. */ + private void carveSegment(LevelLayout layout, Random rnd, int gx, int gz, boolean alongX) { + if(rnd.nextInt(6) == 0) return; + int cells = layout.getCells(); + int gap = rnd.nextInt(2) == 0 ? 1 + rnd.nextInt(SEGMENT_GRID - 1) : -1; + for(int step = 1; step < SEGMENT_GRID; step++) { + int x = alongX ? gx + step : gx; + int z = alongX ? gz : gz + step; + if((alongX ? x : z) >= cells - 1 || step == gap) continue; + if(layout.cellAt(x, z) == LevelLayout.Cell.FLOOR) { + layout.setCell(x, z, alongX ? LevelLayout.Cell.THIN_WALL_X : LevelLayout.Cell.THIN_WALL_Z); + } + } + } + + @Override + public LevelPalette palette() { + return PALETTE; + } + + @Override + public TransitionType transitionType() { + return TransitionType.HOLE; + } + + @Override + public boolean usesKeyDoor() { + return true; + } + + /** Smiler lauern in den Blackout-Zonen; vereinzelt steht eine Bacteria als "Wand" herum. */ + @Override + public void populateMonsters(BackroomsGame game, LevelRuntime runtime, int density) { + LevelLayout layout = runtime.getLayout(); + for(int[] cell : Cells.far(layout, runtime.getRnd(), density * Cells.areaScale(layout), layout::isDarkCell)) { + Smiler smiler = new Smiler(runtime); + smiler.spawnAtCell(game, cell); + runtime.getMonsters().add(smiler); + } + for(int[] cell : Cells.farWalkable(layout, runtime.getRnd(), Math.max(1, density - 1))) { + Bacteria bacteria = new Bacteria(runtime); + bacteria.spawnAtCell(game, cell); + runtime.getMonsters().add(bacteria); + } + } + + /** Nachschub: Smiler in Dunkelzonen, in den hellen Fluren selten Hound oder Bacteria. */ + @Override + public BackroomsMonster createRoamingMonster(LevelRuntime runtime, int[] cell) { + if(runtime.getLayout().isDarkCell(cell[0], cell[1])) return new Smiler(runtime); + int roll = runtime.getRnd().nextInt(100); + if(roll < 20) return new Hound(runtime); + if(roll < 35) return new Bacteria(runtime); + return null; + } + + @Override + public AmbienceProfile ambience() { + return new AmbienceProfile( + SoundEvent.BLOCK_BEACON_AMBIENT, + 0.15f, + 0.5f, + 8, + List.of(SoundEvent.AMBIENT_CAVE, SoundEvent.BLOCK_WOODEN_DOOR_OPEN, SoundEvent.BLOCK_STONE_STEP), + 0.015 + ); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelFinale.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelFinale.java new file mode 100644 index 0000000..309a859 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelFinale.java @@ -0,0 +1,368 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.impl; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience.AmbienceProfile; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.generator.MazeAlgorithms; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelDefinition; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelPalette; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelType; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.TransitionType; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.BackroomsMonster; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Deathmoth; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Hound; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Partygoer; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience.Jumpscare; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Smiler; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Wretch; +import eu.mhsl.minenet.minigames.message.type.ActionBarMessage; +import eu.mhsl.minenet.minigames.message.type.TitleMessage; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsPlayer; +import net.kyori.adventure.sound.Sound; +import net.kyori.adventure.text.Component; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.damage.DamageType; +import net.minestom.server.instance.block.Block; +import net.minestom.server.sound.SoundEvent; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.Random; + +/** + * Level ! "Run For Your Life": ein langer Krankenhauskorridor. Hinter den Spielern + * frisst sich eine toedliche Zone mit steigendem Tempo den Gang entlang — wer das + * Ende erreicht, ist entkommen und gewinnt. + */ +public class LevelFinale extends LevelDefinition { + private static final int CORRIDOR_CELLS = 67; + /** Vorsprung in Sekunden, bevor Meute und Todeszone losgelassen werden */ + private static final int HEAD_START_SECONDS = 12; + /** Wer diese Distanz vor dem Ende des Countdowns laeuft, loest seine Jagd sofort aus */ + private static final double RELEASE_DISTANCE = 12; + private static final double ZONE_BASE_SPEED = 1.2; + private static final double ZONE_ACCELERATION = 0.04; + private static final double ZONE_MAX_SPEED = 4.5; + + /** Sekunden Dunkelheit, bevor der Duell-Smiler erscheint */ + private static final int CHALLENGE_DELAY_SECONDS = 4; + + private final List challengeRooms = new ArrayList<>(); + private final Map challengeTimers = new HashMap<>(); + private final Map challengeSmilers = new HashMap<>(); + + private static final LevelPalette PALETTE = new LevelPalette( + List.of(Block.WHITE_CONCRETE, Block.WHITE_CONCRETE, Block.LIGHT_GRAY_CONCRETE), + List.of(Block.WHITE_CONCRETE, Block.QUARTZ_BLOCK, Block.QUARTZ_BLOCK), + List.of(Block.LIGHT_GRAY_CONCRETE), + Block.SEA_LANTERN, + Block.QUARTZ_PILLAR, + 4, + Block.LIGHT_BLUE_CONCRETE, + false, + List.of(Block.COBWEB), + 0.02, + List.of(), + 0 + ); + + @Override + public LevelType type() { + return LevelType.FINALE; + } + + @Override + public LevelLayout buildLayout(Random rnd, int cells, int playerCount) { + LevelLayout layout = new LevelLayout(CORRIDOR_CELLS); + + // Korridorbreite skaliert mit der Spielerzahl (2 Zellen = 6 Bloecke bis 8 Zellen = 24) + int widthCells = Math.clamp(2 + playerCount / 8, 2, 8); + int zStart = (CORRIDOR_CELLS - widthCells) / 2; + + for(int x = 1; x < CORRIDOR_CELLS - 1; x++) { + for(int z = zStart; z < zStart + widthCells; z++) { + layout.setCell(x, z, LevelLayout.Cell.FLOOR); + } + } + + // Hindernisse: einzelne Pfeiler-Zellen im Laufweg + for(int x = 8; x < CORRIDOR_CELLS - 6; x += 3 + rnd.nextInt(4)) { + int z = zStart + rnd.nextInt(widthCells); + layout.setCell(x, z, LevelLayout.Cell.PILLAR); + } + + // Alle Spieler starten am linken Ende + for(int z = zStart; z < zStart + widthCells; z++) { + layout.getSpawnCells().add(new int[]{2, z}); + layout.getSpawnCells().add(new int[]{3, z}); + } + + // Nur der Korridor + Kammern wird generiert — nicht die ~90 % toten WALL-Zellen des Rasters + layout.setGenerationBounds(0, CORRIDOR_CELLS - 1, zStart - 6, zStart + widthCells + 5); + + layout.setExitCell(new int[]{CORRIDOR_CELLS - 2, zStart + widthCells / 2}); + layout.setCell(CORRIDOR_CELLS - 2, zStart + widthCells / 2, LevelLayout.Cell.EXIT); + + // Pickups (Energy Bars/Almond Water) entlang der Strecke + List floorCells = new ArrayList<>(); + for(int x = 6; x < CORRIDOR_CELLS - 4; x++) { + for(int z = zStart; z < zStart + widthCells; z++) { + if(layout.cellAt(x, z) == LevelLayout.Cell.FLOOR) floorCells.add(new int[]{x, z}); + } + } + layout.getChestCells().addAll(MazeAlgorithms.sampleSpread(floorCells, rnd, 8, 6)); + + // Sparsame Beleuchtung in der Gangmitte + for(int x = 4; x < CORRIDOR_CELLS - 2; x += 5) { + layout.getLightCells().add(new int[]{x, zStart + widthCells / 2}); + } + + // Seitentueren zu dunklen Duell-Kammern: wer (mit Key) eintritt und den + // dort spawnenden Smiler toetet, entkommt ebenfalls + int doors = 3 + rnd.nextInt(3); + for(int i = 0; i < doors; i++) { + int x = 10 + i * (CORRIDOR_CELLS - 20) / doors + rnd.nextInt(4); + boolean north = rnd.nextBoolean(); + int wallZ = north ? zStart - 1 : zStart + widthCells; + int direction = north ? -1 : 1; + if(!layout.inBounds(x, wallZ + direction * 3)) continue; + if(layout.cellAt(x, wallZ) != LevelLayout.Cell.WALL) continue; + + layout.setCell(x, wallZ, LevelLayout.Cell.DOOR); + for(int dx = -1; dx <= 1; dx++) { + for(int step = 1; step <= 3; step++) { + int roomZ = wallZ + direction * step; + if(!layout.inBounds(x + dx, roomZ)) continue; + layout.setCell(x + dx, roomZ, LevelLayout.Cell.FLOOR); + layout.setDark(x + dx, roomZ, true); + } + } + // {RaumzentrumX, RaumzentrumZ, TuerzellenX, TuerzellenZ} + this.challengeRooms.add(new int[]{x, wallZ + direction * 2, x, wallZ}); + } + + return layout; + } + + /** + * Verschliesst die Kammertuer endgueltig: Tuerbloecke zurueck, Zelle wird Wand — + * der DoorManager oeffnet sie nie wieder, die Meute bleibt draussen. + */ + private void sealDoor(BackroomsGame game, LevelRuntime runtime, int[] room) { + LevelLayout layout = runtime.getLayout(); + int doorCx = room[2]; + int doorCz = room[3]; + if(layout.cellAt(doorCx, doorCz) == LevelLayout.Cell.WALL) return; + + int doorX = layout.worldX(doorCx) + 1; + int doorZ = layout.worldZ(doorCz) + 1; + game.setBlock(doorX, runtime.getBaseY() + 2, doorZ, Block.IRON_DOOR.withProperties(Map.of("half", "lower", "facing", "north"))); + game.setBlock(doorX, runtime.getBaseY() + 3, doorZ, Block.IRON_DOOR.withProperties(Map.of("half", "upper", "facing", "north"))); + layout.setCell(doorCx, doorCz, LevelLayout.Cell.WALL); + + game.playSound(Sound.sound(SoundEvent.BLOCK_IRON_DOOR_CLOSE, Sound.Source.BLOCK, 1.2f, 0.6f), + doorX, runtime.getBaseY() + 2, doorZ); + } + + /** Zentrum der Duell-Kammer, in der der Spieler gerade steht — oder null. */ + private int[] challengeRoomAt(LevelLayout layout, Player player) { + int cx = layout.cellX(player.getPosition().x()); + int cz = layout.cellZ(player.getPosition().z()); + for(int[] center : this.challengeRooms) { + if(Math.abs(cx - center[0]) <= 1 && Math.abs(cz - center[1]) <= 1) return center; + } + return null; + } + + @Override + public LevelPalette palette() { + return PALETTE; + } + + @Override + public TransitionType transitionType() { + return TransitionType.ESCAPE; + } + + /** Schaltet die 10%-Key-Chance der Strecken-Barrels frei (fuer die Duell-Tueren). */ + @Override + public boolean usesKeyDoor() { + return true; + } + + @Override + public boolean opensSingleDoors() { + return true; + } + + /** + * Jeder Spieler hat seine EIGENE Verfolgungsjagd: eigener Countdown, eigene Zone, + * eigene Meute. Freigabe entweder nach dem Vorsprungs-Countdown oder frueher, + * sobald der Spieler die ersten Meter gelaufen ist. Nachzuegler und Respawns + * starten dadurch immer fair mit eigenem Vorsprung. + */ + @Override + public void onLevelTick(BackroomsGame game, LevelRuntime runtime) { + LevelLayout layout = runtime.getLayout(); + double startX = layout.worldX(2); + + for(Player player : runtime.playersInLevel()) { + BackroomsPlayer state = game.playerState(player); + if(state == null) continue; + + // Ankunft: Sirene, Countdown beginnt + if(state.getFinaleSeconds() == BackroomsPlayer.FINALE_NOT_STARTED) { + state.setFinaleSeconds(-HEAD_START_SECONDS); + player.playSound(Sound.sound(SoundEvent.EVENT_RAID_HORN, Sound.Source.HOSTILE, 1f, 0.8f), Sound.Emitter.self()); + continue; + } + + int seconds = state.getFinaleSeconds() + 1; + state.setFinaleSeconds(seconds); + + if(seconds < 0) { + // Wer schon losgelaufen ist, loest seine Jagd sofort aus + if(player.getPosition().x() > startX + RELEASE_DISTANCE) { + seconds = 0; + state.setFinaleSeconds(0); + } else { + new ActionBarMessage() + .appendTranslated("game_Backrooms#finaleCountdown") + .appendStatic(Component.text(" " + (-seconds))) + .send(player); + player.playSound(Sound.sound(SoundEvent.BLOCK_NOTE_BLOCK_HAT, Sound.Source.AMBIENT, 1f, 1f), Sound.Emitter.self()); + continue; + } + } + + // Freigabe: "RENN!", persoenliche Meute hinter dem Spieler, Zone startet + if(seconds == 0) { + player.playSound(Sound.sound(SoundEvent.ENTITY_GENERIC_EXPLODE, Sound.Source.HOSTILE, 1.5f, 0.5f), Sound.Emitter.self()); + new TitleMessage(Duration.ofSeconds(2)).appendTranslated("game_Backrooms#finaleRun").send(player); + state.setFinaleZoneX(Math.max(layout.worldX(0), player.getPosition().x() - 14)); + this.spawnPackBehind(game, runtime, player); + continue; + } + + // In einer Duell-Kammer pausiert die persoenliche Zone — dort zaehlt nur der Smiler + if(this.tickChallenge(game, runtime, player)) continue; + + double speed = Math.min(ZONE_MAX_SPEED, ZONE_BASE_SPEED + seconds * ZONE_ACCELERATION); + state.setFinaleZoneX(state.getFinaleZoneX() + speed); + + if(seconds % 2 == 0) { + player.playSound(Sound.sound(SoundEvent.BLOCK_NOTE_BLOCK_PLING, Sound.Source.AMBIENT, 1f, 2f), Sound.Emitter.self()); + } + double distance = player.getPosition().x() - state.getFinaleZoneX(); + if(distance < 0) { + player.damage(DamageType.MOB_ATTACK, 3); + player.playSound(Sound.sound(SoundEvent.ENTITY_WARDEN_ATTACK_IMPACT, Sound.Source.HOSTILE, 1f, 0.7f), Sound.Emitter.self()); + } else if(distance < 15) { + player.playSound(Sound.sound(SoundEvent.ENTITY_WARDEN_HEARTBEAT, Sound.Source.HOSTILE, 1.2f, 0.8f), Sound.Emitter.self()); + } + } + } + + /** + * Duell-Kammer: nach kurzer Dunkelheit erscheint ein an den Spieler gebundener + * Smiler — wer ihn toetet, ist entkommen. + * + * @return true, wenn der Spieler gerade im Duell steckt (Zone pausiert dann) + */ + private boolean tickChallenge(BackroomsGame game, LevelRuntime runtime, Player player) { + // Sieg-Check unabhaengig vom Aufenthaltsort + Smiler smiler = this.challengeSmilers.get(player.getUuid()); + if(smiler != null && (smiler.isDead() || smiler.isRemoved())) { + this.challengeSmilers.remove(player.getUuid()); + game.escape(player); + return true; + } + + LevelLayout layout = runtime.getLayout(); + int[] room = this.challengeRoomAt(layout, player); + if(room == null) { + this.challengeTimers.remove(player.getUuid()); + return false; + } + + if(smiler != null) return true; + + Integer timer = this.challengeTimers.get(player.getUuid()); + if(timer == null) { + this.challengeTimers.put(player.getUuid(), CHALLENGE_DELAY_SECONDS); + // Die Tuer faellt hinter dir zu — und geht nie wieder auf + this.sealDoor(game, runtime, room); + player.playSound(Sound.sound(SoundEvent.BLOCK_SCULK_SHRIEKER_SHRIEK, Sound.Source.HOSTILE, 0.8f, 0.5f), Sound.Emitter.self()); + return true; + } + + if(timer > 1) { + this.challengeTimers.put(player.getUuid(), timer - 1); + player.playSound(Sound.sound(SoundEvent.ENTITY_WARDEN_HEARTBEAT, Sound.Source.HOSTILE, 0.8f, 0.6f), Sound.Emitter.self()); + return true; + } + + // Das Grinsen erscheint + this.challengeTimers.remove(player.getUuid()); + Smiler duelSmiler = new Smiler(runtime); + duelSmiler.setBoundPlayer(player); + duelSmiler.spawnAtCell(game, room); + runtime.getMonsters().add(duelSmiler); + this.challengeSmilers.put(player.getUuid(), duelSmiler); + Jumpscare.visual(player, game); + return true; + } + + /** Gemischte 3er-Meute wenige Zellen hinter dem Spieler. */ + private void spawnPackBehind(BackroomsGame game, LevelRuntime runtime, Player player) { + LevelLayout layout = runtime.getLayout(); + int playerCx = layout.cellX(player.getPosition().x()); + int playerCz = layout.cellZ(player.getPosition().z()); + + int spawned = 0; + List used = new ArrayList<>(); + for(int attempt = 0; attempt < 12 && spawned < 3; attempt++) { + // Wer noch am Korridoranfang steht, hat kein "Hinten" — dann spawnt die + // Meute in der ersten begehbaren Spalte direkt hinter bzw. neben ihm + int cx = Math.max(1, playerCx - 2 - runtime.getRnd().nextInt(4)); + int cz = playerCz + runtime.getRnd().nextInt(5) - 2; + if(!layout.isWalkable(cx, cz)) continue; + // jede Meute verteilt auf unterschiedliche Zellen + int finalCx = cx; + int finalCz = cz; + if(used.stream().anyMatch(cell -> cell[0] == finalCx && cell[1] == finalCz)) continue; + used.add(new int[]{cx, cz}); + + BackroomsMonster monster = switch(runtime.getRnd().nextInt(5)) { + case 0 -> new Partygoer(runtime); + case 1 -> new Wretch(runtime); + case 2 -> new Deathmoth(runtime); + default -> new Hound(runtime); + }; + monster.setRoaming(true); + monster.setRelentless(true); + monster.setBoundPlayer(player); + monster.spawnAtCell(game, new int[]{cx, cz}); + runtime.getMonsters().add(monster); + spawned++; + } + } + + @Override + public AmbienceProfile ambience() { + return new AmbienceProfile( + SoundEvent.ENTITY_WARDEN_HEARTBEAT, + 0.6f, + 1f, + 4, + List.of(SoundEvent.ENTITY_WARDEN_NEARBY_CLOSE, SoundEvent.ENTITY_GHAST_SCREAM), + 0.05 + ); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelFun.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelFun.java new file mode 100644 index 0000000..db4547b --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelFun.java @@ -0,0 +1,109 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.impl; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience.AmbienceProfile; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelDefinition; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelPalette; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelType; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.TransitionType; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.BackroomsMonster; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Partygoer; +import net.minestom.server.instance.block.Block; +import net.minestom.server.sound.SoundEvent; + +import java.util.List; +import java.util.Random; + +/** + * Level Fun "=)": eine endlose Kinderparty in grellen Farben, mit kaputter + * Partymusik aus der Deckenanlage. Die Partygoer streifen in Rudeln umher + * und wollen, dass man FUER IMMER bleibt. + */ +public class LevelFun extends LevelDefinition { + private static final LevelPalette PALETTE = new LevelPalette( + List.of(Block.LIGHT_BLUE_WOOL, Block.CYAN_WOOL), + List.of(Block.ORANGE_CONCRETE, Block.ORANGE_TERRACOTTA, Block.YELLOW_CONCRETE), + List.of(Block.WHITE_CONCRETE), + Block.GLOWSTONE, + Block.MAGENTA_CONCRETE, + 4, + Block.LIME_CONCRETE, + true, + List.of(Block.CAKE), + 0.012, + List.of(), + 0 + ); + + @Override + public LevelType type() { + return LevelType.FUN; + } + + @Override + public LevelLayout buildLayout(Random rnd, int cells, int playerCount) { + return this.standardLayout(rnd, cells, playerCount, 0.7, cells / 2, 7, 3); + } + + @Override + public LevelPalette palette() { + return PALETTE; + } + + @Override + public TransitionType transitionType() { + return TransitionType.GLITCH_FLOOR; + } + + @Override + public boolean usesKeyDoor() { + return true; + } + + /** Partygoer treten in Rudeln von 3-4 auf, jedes Rudel gemeinsam an einer Zelle. */ + @Override + public void populateMonsters(BackroomsGame game, LevelRuntime runtime, int density) { + Random rnd = runtime.getRnd(); + LevelLayout layout = runtime.getLayout(); + for(int[] cell : Cells.farWalkable(layout, rnd, density * Cells.areaScale(layout))) { + int packSize = 3 + rnd.nextInt(2); + for(int i = 0; i < packSize; i++) { + // Rudel bleibt zusammen, aber jedes Mitglied auf einer eigenen Nachbarzelle + int cx = cell[0]; + int cz = cell[1]; + for(int attempt = 0; attempt < 6; attempt++) { + int nx = cell[0] + rnd.nextInt(3) - 1; + int nz = cell[1] + rnd.nextInt(3) - 1; + if(layout.isWalkable(nx, nz)) { + cx = nx; + cz = nz; + break; + } + } + Partygoer partygoer = new Partygoer(runtime); + partygoer.spawnAtCell(game, new int[]{cx, cz}); + runtime.getMonsters().add(partygoer); + } + } + } + + /** "You can never catch a Partygoer alone" — der Nachschub kommt zu den Rudeln dazu. */ + @Override + public BackroomsMonster createRoamingMonster(LevelRuntime runtime, int[] cell) { + return new Partygoer(runtime); + } + + @Override + public AmbienceProfile ambience() { + return new AmbienceProfile( + SoundEvent.BLOCK_NOTE_BLOCK_BIT, + 0.6f, + 1.4f, + 6, + List.of(SoundEvent.ENTITY_VILLAGER_CELEBRATE, SoundEvent.ENTITY_FIREWORK_ROCKET_LAUNCH, SoundEvent.BLOCK_CAKE_ADD_CANDLE), + 0.04 + ); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelHabitableZone.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelHabitableZone.java new file mode 100644 index 0000000..fcf3fb8 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelHabitableZone.java @@ -0,0 +1,146 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.impl; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience.AmbienceProfile; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelDefinition; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelPalette; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelType; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.TransitionType; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Bacteria; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.BackroomsMonster; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Hound; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Wretch; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.instance.block.Block; +import net.minestom.server.sound.SoundEvent; + +import java.util.List; +import java.util.Random; + +/** + * Level 1 "Habitable Zone": Parkhaus-/Lagerhallen-Atmosphaere aus grauem Beton. + * Grosse offene Flaechen mit regelmaessigen Stuetzpfeilern und vereinzelten + * Wasserpfuetzen. Event "The Flickering": kurze, zufaellige Komplett-Blackouts. + */ +public class LevelHabitableZone extends LevelDefinition { + private int blackoutSecondsLeft = 0; + private static final double FLICKERING_CHANCE_PER_SECOND = 1.0 / 60.0; + private static final double PUDDLE_CHANCE = 0.02; + + private static final LevelPalette PALETTE = new LevelPalette( + List.of(Block.GRAY_CONCRETE, Block.STONE, Block.ANDESITE), + List.of(Block.STONE_BRICKS, Block.GRAY_CONCRETE, Block.CRACKED_STONE_BRICKS), + List.of(Block.GRAY_CONCRETE, Block.STONE), + Block.SEA_LANTERN, + Block.POLISHED_ANDESITE, + 5, + Block.POLISHED_ANDESITE, + false, + List.of(Block.COBWEB), + 0.006, + List.of(Block.IRON_CHAIN), + 0.015 + ); + + @Override + public LevelType type() { + return LevelType.HABITABLE_ZONE; + } + + @Override + public LevelLayout buildLayout(Random rnd, int cells, int playerCount) { + LevelLayout layout = this.openLayout(rnd, cells, playerCount, cells / 2, 2); + + // Stuetzpfeiler im 4er-Raster, nur mitten in Freiflaechen (Zelle + alle 4 Nachbarn frei) + for(int x = 2; x < cells - 2; x += 4) { + for(int z = 2; z < cells - 2; z += 4) { + if(layout.cellAt(x, z) != LevelLayout.Cell.FLOOR) continue; + if(layout.cellAt(x + 1, z) != LevelLayout.Cell.FLOOR) continue; + if(layout.cellAt(x - 1, z) != LevelLayout.Cell.FLOOR) continue; + if(layout.cellAt(x, z + 1) != LevelLayout.Cell.FLOOR) continue; + if(layout.cellAt(x, z - 1) != LevelLayout.Cell.FLOOR) continue; + if(Cells.reserved(layout, x, z)) continue; + layout.setCell(x, z, LevelLayout.Cell.PILLAR); + Cells.removeLight(layout, x, z); + } + } + + // Vereinzelte Wasserpfuetzen auf dem Hallenboden + for(int x = 1; x < cells - 1; x++) { + for(int z = 1; z < cells - 1; z++) { + if(layout.cellAt(x, z) != LevelLayout.Cell.FLOOR) continue; + if(Cells.reserved(layout, x, z)) continue; + if(rnd.nextDouble() < PUDDLE_CHANCE) layout.setCell(x, z, LevelLayout.Cell.WATER); + } + } + + return layout; + } + + @Override + public LevelPalette palette() { + return PALETTE; + } + + @Override + public TransitionType transitionType() { + return TransitionType.LADDER; + } + + @Override + public void populateMonsters(BackroomsGame game, LevelRuntime runtime, int density) { + for(int[] cell : Cells.farWalkable(runtime.getLayout(), runtime.getRnd(), (1 + density) * Cells.areaScale(runtime.getLayout()))) { + Hound hound = new Hound(runtime); + hound.spawnAtCell(game, cell); + runtime.getMonsters().add(hound); + } + // Getarnte Bacteria zwischen den Betonpfeilern + for(int[] cell : Cells.farWalkable(runtime.getLayout(), runtime.getRnd(), density)) { + Bacteria bacteria = new Bacteria(runtime); + bacteria.spawnAtCell(game, cell); + runtime.getMonsters().add(bacteria); + } + } + + /** Nachschub: meist Hounds, dazu Wretches und getarnte Bacteria. */ + @Override + public BackroomsMonster createRoamingMonster(LevelRuntime runtime, int[] cell) { + int roll = runtime.getRnd().nextInt(100); + if(roll < 60) return new Hound(runtime); + if(roll < 85) return new Wretch(runtime); + return new Bacteria(runtime); + } + + /** "The Flickering": zufaellig faellt fuer wenige Sekunden das komplette Licht aus. */ + @Override + public void onLevelTick(BackroomsGame game, LevelRuntime runtime) { + Random rnd = runtime.getRnd(); + if(!runtime.isBlackoutActive()) { + if(rnd.nextDouble() >= FLICKERING_CHANCE_PER_SECOND) return; + runtime.setBlackoutActive(true); + this.blackoutSecondsLeft = 5 + rnd.nextInt(4); + runtime.playersInLevel().forEach(player -> { + player.playSound(Sound.sound(SoundEvent.BLOCK_REDSTONE_TORCH_BURNOUT, Sound.Source.AMBIENT, 1f, 0.7f), Sound.Emitter.self()); + player.playSound(Sound.sound(SoundEvent.BLOCK_LEVER_CLICK, Sound.Source.AMBIENT, 0.8f, 0.5f), Sound.Emitter.self()); + }); + return; + } + + this.blackoutSecondsLeft--; + if(this.blackoutSecondsLeft <= 0) runtime.setBlackoutActive(false); + } + + @Override + public AmbienceProfile ambience() { + return new AmbienceProfile( + SoundEvent.AMBIENT_BASALT_DELTAS_LOOP, + 0.2f, + 0.6f, + 12, + List.of(SoundEvent.BLOCK_POINTED_DRIPSTONE_DRIP_WATER, SoundEvent.BLOCK_IRON_DOOR_CLOSE), + 0.02 + ); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelLightsOut.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelLightsOut.java new file mode 100644 index 0000000..fe687cb --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelLightsOut.java @@ -0,0 +1,110 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.impl; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience.AmbienceProfile; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelDefinition; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelPalette; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelType; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.TransitionType; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.block.Block; +import net.minestom.server.sound.SoundEvent; + +import java.util.List; +import java.util.Random; + +/** + * Level 6 "Lights Out": absolute Dunkelheit, keine einzige Lichtquelle, keine Monster — + * Level 6 ist gefaehrlich, weil NICHTS da ist. Navigation nur ueber einen periodischen + * Audio-Ping, der von der Exit-Position ausgeht. + */ +public class LevelLightsOut extends LevelDefinition { + private int pingSeconds = 0; + private static final int PING_PERIOD_SECONDS = 4; + private static final double HEARTBEAT_CHANCE = 0.25; + + private static final LevelPalette PALETTE = new LevelPalette( + List.of(Block.SMOOTH_BASALT, Block.BLACK_CONCRETE, Block.SCULK), + List.of(Block.POLISHED_BLACKSTONE, Block.SMOOTH_BASALT), + List.of(Block.SMOOTH_BASALT), + Block.SEA_LANTERN, + Block.POLISHED_BLACKSTONE, + 3, + Block.SCULK, + false, + List.of(Block.SCULK_SENSOR, Block.SCULK_SENSOR, Block.SCULK_CATALYST), + 0.012, + List.of(), + 0 + ); + + @Override + public LevelType type() { + return LevelType.LIGHTS_OUT; + } + + @Override + public LevelLayout buildLayout(Random rnd, int cells, int playerCount) { + LevelLayout layout = this.standardLayout(rnd, cells, playerCount, 0.25, cells / 4, 4, 1); + + // Das gesamte Level liegt in Dunkelheit; saemtliche Deckenlichter entfallen + for(int x = 0; x < cells; x++) { + for(int z = 0; z < cells; z++) { + layout.setDark(x, z, true); + } + } + layout.getLightCells().clear(); + + return layout; + } + + @Override + public boolean permanentDarkness() { + return true; + } + + @Override + public LevelPalette palette() { + return PALETTE; + } + + @Override + public TransitionType transitionType() { + return TransitionType.HOLE; + } + + /** Audio-Navigation: alle 4 s ein positionsgebundener Shriek vom Exit aus. */ + @Override + public void onLevelTick(BackroomsGame game, LevelRuntime runtime) { + this.pingSeconds++; + if(this.pingSeconds % PING_PERIOD_SECONDS != 0) return; + + Pos exit = runtime.toPlayerPos(runtime.getLayout().getExitCell()); + boolean heartbeat = runtime.getRnd().nextDouble() < HEARTBEAT_CHANCE; + for(Player player : runtime.playersInLevel()) { + player.playSound( + Sound.sound(SoundEvent.BLOCK_SCULK_SHRIEKER_SHRIEK, Sound.Source.AMBIENT, 1.5f, 1f), + exit.x(), exit.y(), exit.z() + ); + if(heartbeat) { + player.playSound(Sound.sound(SoundEvent.ENTITY_WARDEN_HEARTBEAT, Sound.Source.AMBIENT, 0.3f, 0.9f), Sound.Emitter.self()); + } + } + } + + @Override + public AmbienceProfile ambience() { + return new AmbienceProfile( + SoundEvent.AMBIENT_CAVE, + 0.4f, + 0.7f, + 20, + List.of(SoundEvent.BLOCK_SCULK_SENSOR_CLICKING, SoundEvent.ENTITY_WARDEN_NEARBY_CLOSE), + 0.04 + ); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelPipeDreams.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelPipeDreams.java new file mode 100644 index 0000000..ccf05ee --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelPipeDreams.java @@ -0,0 +1,206 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.impl; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience.AmbienceProfile; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.generator.MazeAlgorithms; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelDefinition; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelPalette; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelType; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.TransitionType; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.BackroomsMonster; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Clump; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Deathmoth; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Hound; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.instance.block.Block; +import net.minestom.server.sound.SoundEvent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Random; + +/** + * Level 2 "Pipe Dreams": enge, niedrige Industrietunnel voller Rohre und Maschinen. + * Fast reines Labyrinth mit verstreuten Maschinenbloecken. Event "Serienschaltung": + * regelmaessig faellt ein zusammenhaengendes Drittel der Deckenbeleuchtung aus. + */ +public class LevelPipeDreams extends LevelDefinition { + private static final double MACHINE_RATIO = 0.03; + + /** aktuell ausgefallenes Licht-Segment (-1 = keins) und Restdauer */ + private int failedSegment = -1; + private int eventSeconds = 0; + private static final int LIGHT_FAIL_PERIOD_SECONDS = 45; + private static final int LIGHT_FAIL_DURATION_SECONDS = 8; + + private static final LevelPalette PALETTE = new LevelPalette( + List.of(Block.MUD_BRICKS, Block.PACKED_MUD), + List.of(Block.MUD_BRICKS, Block.DEEPSLATE_BRICKS, Block.BROWN_TERRACOTTA), + List.of(Block.DEEPSLATE_TILES), + Block.SHROOMLIGHT, + Block.OXIDIZED_COPPER, + 3, + Block.EXPOSED_COPPER, + false, + List.of(Block.COBWEB, Block.IRON_BARS), + 0.01, + List.of(Block.OXIDIZED_COPPER_CHAIN, Block.LANTERN.withProperty("hanging", "true")), + 0.02 + ); + + @Override + public LevelType type() { + return LevelType.PIPE_DREAMS; + } + + @Override + public LevelLayout buildLayout(Random rnd, int cells, int playerCount) { + LevelLayout layout = this.standardLayout(rnd, cells, playerCount, 0.1, 2, 3, 2); + + // "Maschinen": verstreute PILLAR-Zellen, aber nur wenn sie keine Wege abschneiden + List floorCells = new ArrayList<>(); + for(int x = 1; x < cells - 1; x++) { + for(int z = 1; z < cells - 1; z++) { + if(layout.cellAt(x, z) != LevelLayout.Cell.FLOOR || Cells.reserved(layout, x, z)) continue; + // Nie vor der Schluesseltuer oder an der Exit-Kammer: die DOOR-Zelle ist nicht + // begehbar, der disconnects-Check wuerde eine Blockade dort nicht bemerken + if(MazeAlgorithms.isChamberProtected(layout, x, z)) continue; + if(this.nextToDoor(layout, x, z)) continue; + floorCells.add(new int[]{x, z}); + } + } + Collections.shuffle(floorCells, rnd); + int target = (int) Math.round(floorCells.size() * MACHINE_RATIO); + int placed = 0; + for(int[] cell : floorCells) { + if(placed >= target) break; + layout.setCell(cell[0], cell[1], LevelLayout.Cell.PILLAR); + if(this.disconnects(layout)) { + layout.setCell(cell[0], cell[1], LevelLayout.Cell.FLOOR); + continue; + } + Cells.removeLight(layout, cell[0], cell[1]); + placed++; + } + + return layout; + } + + private boolean nextToDoor(LevelLayout layout, int cx, int cz) { + return layout.cellAt(cx + 1, cz) == LevelLayout.Cell.DOOR + || layout.cellAt(cx - 1, cz) == LevelLayout.Cell.DOOR + || layout.cellAt(cx, cz + 1) == LevelLayout.Cell.DOOR + || layout.cellAt(cx, cz - 1) == LevelLayout.Cell.DOOR; + } + + /** Prueft per BFS, ob alle begehbaren Zellen (ausser der abgeschlossenen Exit-Kammer) verbunden bleiben. */ + private boolean disconnects(LevelLayout layout) { + int[][] distances = MazeAlgorithms.bfsDistances(layout, layout.getSpawnCells().getFirst()); + for(int x = 0; x < layout.getCells(); x++) { + for(int z = 0; z < layout.getCells(); z++) { + if(!layout.isWalkable(x, z)) continue; + if(layout.cellAt(x, z) == LevelLayout.Cell.EXIT) continue; + if(distances[x][z] < 0) return true; + } + } + return false; + } + + @Override + public LevelPalette palette() { + return PALETTE; + } + + @Override + public TransitionType transitionType() { + return TransitionType.GLITCH_FLOOR; + } + + @Override + public boolean usesKeyDoor() { + return true; + } + + @Override + public void populateMonsters(BackroomsGame game, LevelRuntime runtime, int density) { + LevelLayout layout = runtime.getLayout(); + + // Clumps lauern an Ecken (genau zwei orthogonal offene Richtungen) + List corners = Cells.far(layout, runtime.getRnd(), (2 + density) * Cells.areaScale(layout), (x, z) -> Cells.openOrthogonal(layout, x, z) == 2); + for(int[] cell : corners) { + Clump clump = new Clump(runtime); + clump.spawnAtCell(game, cell); + runtime.getMonsters().add(clump); + } + + // Hounds meiden die Clump-Zellen — sonst stapeln sich beide Populationen + for(int[] cell : Cells.far(layout, runtime.getRnd(), density * Cells.areaScale(layout), + (x, z) -> corners.stream().noneMatch(corner -> Math.abs(corner[0] - x) + Math.abs(corner[1] - z) < 4))) { + Hound hound = new Hound(runtime); + hound.spawnAtCell(game, cell); + runtime.getMonsters().add(hound); + } + } + + /** Nachschub: Clumps an Ecken, sonst Hounds. */ + @Override + public BackroomsMonster createRoamingMonster(LevelRuntime runtime, int[] cell) { + LevelLayout layout = runtime.getLayout(); + if(Cells.openOrthogonal(layout, cell[0], cell[1]) == 2) return new Clump(runtime); + return runtime.getRnd().nextInt(100) < 60 ? new Hound(runtime) : new Deathmoth(runtime); + } + + /** "Serienschaltung": ca. alle 45 s faellt ein Drittel der Lichter fuer ~8 s aus. */ + @Override + public void onLevelTick(BackroomsGame game, LevelRuntime runtime) { + if(this.failedSegment < 0) { + this.eventSeconds++; + if(this.eventSeconds < LIGHT_FAIL_PERIOD_SECONDS) return; + + int segment = runtime.getRnd().nextInt(3); + this.failedSegment = segment; + this.eventSeconds = LIGHT_FAIL_DURATION_SECONDS; + this.setSegmentLights(game, runtime, segment, Block.DEEPSLATE_TILES); + runtime.playersInLevel().forEach(player -> + player.playSound(Sound.sound(SoundEvent.BLOCK_REDSTONE_TORCH_BURNOUT, Sound.Source.AMBIENT, 1f, 0.6f), Sound.Emitter.self())); + return; + } + + this.eventSeconds--; + if(this.eventSeconds > 0) return; + this.setSegmentLights(game, runtime, this.failedSegment, PALETTE.light()); + this.failedSegment = -1; + this.eventSeconds = 0; + } + + /** Setzt die Deckenbloecke eines nach x sortierten Drittels aller Lichtzellen. */ + private void setSegmentLights(BackroomsGame game, LevelRuntime runtime, int segment, Block block) { + LevelLayout layout = runtime.getLayout(); + List lights = new ArrayList<>(layout.getLightCells()); + lights.sort(Comparator.comparingInt(cell -> cell[0])); + + int from = segment * lights.size() / 3; + int to = segment == 2 ? lights.size() : (segment + 1) * lights.size() / 3; + int ceilingY = runtime.getBaseY() + 2 + PALETTE.roomHeight(); + for(int i = from; i < to; i++) { + int[] cell = lights.get(i); + game.setBlock(layout.worldX(cell[0]) + 1, ceilingY, layout.worldZ(cell[1]) + 1, block); + } + } + + @Override + public AmbienceProfile ambience() { + return new AmbienceProfile( + SoundEvent.AMBIENT_NETHER_WASTES_LOOP, + 0.25f, + 0.5f, + 15, + List.of(SoundEvent.BLOCK_ANVIL_LAND, SoundEvent.BLOCK_LAVA_POP), + 0.02 + ); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelPoolrooms.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelPoolrooms.java new file mode 100644 index 0000000..9dd2245 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/level/impl/LevelPoolrooms.java @@ -0,0 +1,182 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.impl; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience.AmbienceProfile; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelDefinition; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelPalette; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelType; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.TransitionType; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.generator.MazeAlgorithms; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.BackroomsMonster; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster.Hound; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Player; +import net.minestom.server.instance.block.Block; +import net.minestom.server.network.packet.server.play.ParticlePacket; +import net.minestom.server.particle.Particle; +import net.minestom.server.sound.SoundEvent; + +import java.util.List; +import java.util.Random; + +/** + * "The Poolrooms": halb geflutete, weisse Fliesenhallen mit hohen Decken und + * grossen Wasserbecken — die Verschnaufpause zwischen den gefaehrlichen Ebenen. + * Ausgang: ein Abfluss (Loch) am Beckengrund. + */ +public class LevelPoolrooms extends LevelDefinition { + private static final LevelPalette PALETTE = new LevelPalette( + List.of(Block.QUARTZ_BLOCK, Block.WHITE_CONCRETE), + List.of(Block.QUARTZ_BLOCK, Block.WHITE_GLAZED_TERRACOTTA, Block.WHITE_CONCRETE), + List.of(Block.SMOOTH_QUARTZ), + Block.SEA_LANTERN, + Block.QUARTZ_PILLAR, + 6, + Block.WHITE_GLAZED_TERRACOTTA, + true, + List.of(), + 0, + List.of(), + 0 + ); + + @Override + public LevelType type() { + return LevelType.POOLROOMS; + } + + @Override + public LevelLayout buildLayout(Random rnd, int cells, int playerCount) { + LevelLayout layout = this.openLayout(rnd, cells, playerCount, cells / 4, 2); + + // 3-5 grosse rechteckige Wasserbecken; das erste ist doppelt tief + int pools = 3 + rnd.nextInt(3); + for(int i = 0; i < pools; i++) { + int width = 4 + rnd.nextInt(5); + int depth = 4 + rnd.nextInt(5); + int startX = 1 + rnd.nextInt(Math.max(1, cells - width - 2)); + int startZ = 1 + rnd.nextInt(Math.max(1, cells - depth - 2)); + LevelLayout.Cell poolType = i == 0 ? LevelLayout.Cell.DEEP_WATER : LevelLayout.Cell.WATER; + for(int x = startX; x < startX + width; x++) { + for(int z = startZ; z < startZ + depth; z++) { + if(layout.cellAt(x, z) != LevelLayout.Cell.FLOOR) continue; + if(Cells.reserved(layout, x, z)) continue; + layout.setCell(x, z, poolType); + } + } + } + + // Kupfer-Leitungsstraenge unter der Decke, quer durch die Hallen — mit Undichtigkeiten + int ducts = 3 + rnd.nextInt(3); + for(int i = 0; i < ducts; i++) { + boolean alongX = rnd.nextBoolean(); + int fixed = 3 + rnd.nextInt(cells - 6); + for(int j = 2; j < cells - 2; j++) { + int x = alongX ? j : fixed; + int z = alongX ? fixed : j; + if(layout.cellAt(x, z) != LevelLayout.Cell.FLOOR) continue; + if(Cells.reserved(layout, x, z)) continue; + if(MazeAlgorithms.isChamberProtected(layout, x, z)) continue; + // ~8% der Leitungszellen sind undicht — dort laeuft das Wasser heraus + layout.setCell(x, z, rnd.nextInt(100) < 8 ? LevelLayout.Cell.LEAK : LevelLayout.Cell.PIPE); + } + } + layout.getLightCells().removeIf(cell -> { + LevelLayout.Cell type = layout.cellAt(cell[0], cell[1]); + return type == LevelLayout.Cell.PIPE || type == LevelLayout.Cell.LEAK; + }); + + // Abfluss: Wasser stroemt ringfoermig auf das Exit-Loch zu — die Rutsche nach unten + int[] exit = layout.getExitCell(); + if(exit != null) { + for(int dx = -1; dx <= 1; dx++) { + for(int dz = -1; dz <= 1; dz++) { + if(dx == 0 && dz == 0) continue; + if(layout.cellAt(exit[0] + dx, exit[1] + dz) != LevelLayout.Cell.FLOOR) continue; + layout.setCell(exit[0] + dx, exit[1] + dz, LevelLayout.Cell.WATER); + } + } + } + + // Dampf-Ventile: an zufaelligen Leitungs- und Beckenraendern zischt es + for(int i = 0; i < cells / 5; i++) { + int x = 2 + rnd.nextInt(cells - 4); + int z = 2 + rnd.nextInt(cells - 4); + LevelLayout.Cell type = layout.cellAt(x, z); + if(type == LevelLayout.Cell.PIPE || type == LevelLayout.Cell.LEAK || type == LevelLayout.Cell.WATER) { + layout.getMarkerCells().add(new int[]{x, z}); + } + } + + // 1-2 kleine dunkle Ecken + Cells.darkBlobs(layout, rnd, 1 + rnd.nextInt(2), 2, 3); + + return layout; + } + + @Override + public LevelPalette palette() { + return PALETTE; + } + + @Override + public TransitionType transitionType() { + return TransitionType.HOLE; + } + + /** Dampf-Ventile: Wolkenpartikel + Zischen an den Markern in Spielernaehe. */ + @Override + public void onLevelTick(BackroomsGame game, LevelRuntime runtime) { + LevelLayout layout = runtime.getLayout(); + List players = runtime.playersInLevel(); + for(int[] vent : layout.getMarkerCells()) { + Pos ventPos = runtime.toPlayerPos(vent); + for(Player player : players) { + if(player.getPosition().distance(ventPos) > 28) continue; + player.sendPacket(new ParticlePacket( + Particle.CLOUD, + ventPos.x(), ventPos.y() + 2.5, ventPos.z(), + 0.3f, 0.6f, 0.3f, 0.02f, 6 + )); + if(runtime.getRnd().nextInt(100) < 12) { + player.playSound( + Sound.sound(SoundEvent.BLOCK_LAVA_EXTINGUISH, Sound.Source.AMBIENT, 0.4f, 1.6f), + ventPos.x(), ventPos.y(), ventPos.z() + ); + } + } + } + } + + /** Die Poolrooms sind bewusst fast leer — erst bei maximaler Dichte streift ein Hound umher. */ + @Override + public void populateMonsters(BackroomsGame game, LevelRuntime runtime, int density) { + if(density < 3) return; + for(int[] cell : Cells.farWalkable(runtime.getLayout(), runtime.getRnd(), 1)) { + Hound hound = new Hound(runtime); + hound.spawnAtCell(game, cell); + runtime.getMonsters().add(hound); + } + } + + /** Die Poolrooms bleiben ueberwiegend friedlich — nur selten verirrt sich ein Hound hierher. */ + @Override + public BackroomsMonster createRoamingMonster(LevelRuntime runtime, int[] cell) { + return runtime.getRnd().nextInt(100) < 20 ? new Hound(runtime) : null; + } + + @Override + public AmbienceProfile ambience() { + return new AmbienceProfile( + SoundEvent.AMBIENT_UNDERWATER_LOOP, + 0.3f, + 1f, + 10, + List.of(SoundEvent.BLOCK_POINTED_DRIPSTONE_DRIP_WATER, SoundEvent.ENTITY_DOLPHIN_AMBIENT_WATER), + 0.03 + ); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/loot/ChestManager.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/loot/ChestManager.java new file mode 100644 index 0000000..0b8c14c --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/loot/ChestManager.java @@ -0,0 +1,199 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.loot; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience.Jumpscare; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.item.BackroomsItems; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.coordinate.BlockVec; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.network.packet.server.play.BlockChangePacket; +import net.minestom.server.entity.Player; +import net.minestom.server.event.player.PlayerBlockInteractEvent; +import net.minestom.server.instance.block.Block; +import net.minestom.server.item.ItemStack; +import net.minestom.server.sound.SoundEvent; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * Loot-Kisten (Barrels): + * - Normale Kisten sind einmal pluenderbar und verschwinden. Der Loot haengt vom + * Inventar ab — wer schon viel Almond Water traegt, findet kaum noch welches; + * wer generell voll ist, findet hoechstens noch den Schluessel. + * - In Key-Door-Leveln kann JEDE Kiste mit etwas Glueck den Level Key enthalten + * (solange die Tuer zu ist und der Spieler noch keinen hat); die dedizierte + * Schluessel-Kiste gibt ihn jedem Spieler garantiert einmal. + */ +public class ChestManager { + private static final int RANDOM_KEY_CHANCE_PERCENT = 10; + private static final int RANDOM_COMPASS_CHANCE_PERCENT = 10; + + private final BackroomsGame game; + private final Map chestLevels = new HashMap<>(); + private final Map keyChestLevels = new HashMap<>(); + private final Map> keyChestLooters = new HashMap<>(); + private final Map> chestLooters = new HashMap<>(); + + public ChestManager(BackroomsGame game) { + this.game = game; + + for(LevelRuntime level : game.getLevels()) { + int chestY = level.getBaseY() + 2; + level.getLayout().getChestCells().forEach(cell -> + this.chestLevels.put(this.blockPos(level, cell, chestY), level.getIndex()) + ); + if(level.getLayout().getKeyChestCell() != null) { + this.keyChestLevels.put(this.blockPos(level, level.getLayout().getKeyChestCell(), chestY), level.getIndex()); + } + } + } + + private Point blockPos(LevelRuntime level, int[] cell, int y) { + Pos center = level.toPlayerPos(cell); + return new Pos(center.blockX(), y, center.blockZ()); + } + + public void onInteract(PlayerBlockInteractEvent event) { + if(!event.getBlock().compare(Block.BARREL)) return; + Point position = new Pos(event.getBlockPosition().blockX(), event.getBlockPosition().blockY(), event.getBlockPosition().blockZ()); + Player player = event.getPlayer(); + + Integer keyLevel = this.keyChestLevels.get(position); + if(keyLevel != null && !this.game.isDoorOpened(keyLevel)) { + event.setCancelled(true); + event.setBlockingItemUse(true); + Set looters = this.keyChestLooters.computeIfAbsent(position, unused -> new HashSet<>()); + if(!looters.add(player.getUuid())) { + player.playSound(Sound.sound(SoundEvent.BLOCK_CHEST_LOCKED, Sound.Source.BLOCK, 0.8f, 0.9f), position.x(), position.y(), position.z()); + return; + } + this.giveKey(player, keyLevel); + player.playSound(Sound.sound(SoundEvent.BLOCK_BARREL_OPEN, Sound.Source.BLOCK, 1f, 0.8f), position.x(), position.y(), position.z()); + this.hideChestFor(player, position); + return; + } + + Integer chestLevel = keyLevel != null ? keyLevel : this.chestLevels.get(position); + if(chestLevel == null) return; + + event.setCancelled(true); + event.setBlockingItemUse(true); + + // Kisten sind per Spieler pluenderbar — niemand nimmt anderen den Loot weg + Set looters = this.chestLooters.computeIfAbsent(position, unused -> new HashSet<>()); + if(!looters.add(player.getUuid())) { + // Falls der Chunk neu gesendet wurde und der Barrel wieder aufgetaucht ist: erneut ausblenden + this.hideChestFor(player, position); + return; + } + + List loot = this.rollFor(player, chestLevel); + loot.forEach(item -> player.getInventory().addItemStack(item)); + + SoundEvent sound = loot.isEmpty() ? SoundEvent.BLOCK_BARREL_CLOSE : SoundEvent.BLOCK_BARREL_OPEN; + player.playSound(Sound.sound(sound, Sound.Source.BLOCK, 1f, 0.8f), position.x(), position.y(), position.z()); + this.hideChestFor(player, position); + + // Kisten sind ein guter Jumpscare-Anker: kleine Chance auf einen Sound-Scare + if(this.game.rnd().nextInt(100) < 20) { + Jumpscare.sound(player, this.game.rnd()); + } + } + + /** + * Blendet die gepluenderte Kiste NUR fuer diesen Spieler aus — der Serverblock + * bleibt, alle anderen sehen den Barrel weiter. + */ + private void hideChestFor(Player player, Point position) { + player.sendPacket(new BlockChangePacket(new BlockVec(position.blockX(), position.blockY(), position.blockZ()), Block.AIR)); + } + + /** + * Inventarabhaengiger Loot. In Key-Door-Leveln sind Key (10 %) und Tuerkompass (10 %) + * getrennt voneinander findbar — beides maximal einmal pro Spieler. + */ + private List rollFor(Player player, int levelIndex) { + List result = new ArrayList<>(); + + LevelRuntime level = this.game.getLevels().get(levelIndex); + boolean doorRelevant = level.getDefinition().usesKeyDoor() && !this.game.isDoorOpened(levelIndex); + if(doorRelevant && !BackroomsItems.hasMatching(player, item -> BackroomsItems.isKeyFor(item, levelIndex)) && this.game.rnd().nextInt(100) < RANDOM_KEY_CHANCE_PERCENT) { + result.add(BackroomsItems.levelKey(levelIndex)); + } + if(doorRelevant && !this.hasCompassFor(player, levelIndex) && this.game.rnd().nextInt(100) < RANDOM_COMPASS_CHANCE_PERCENT) { + ItemStack compass = this.createCompass(levelIndex); + if(compass != null) result.add(compass); + } + + int almond = BackroomsItems.count(player, BackroomsItems.ALMOND_WATER); + int energy = BackroomsItems.count(player, BackroomsItems.ENERGY_BAR); + int carried = almond + energy; + + // Randvolles Inventar: die Backrooms geben dir nichts mehr — hoechstens Key/Kompass + if(carried >= 16) return result; + + LootTable table = new LootTable(); + int almondWeight = Math.max(0, 5 - almond); + int energyWeight = Math.max(1, 10 - energy); + if(almondWeight > 0) table.add(BackroomsItems::almondWater, almondWeight); + table.add(BackroomsItems::energyBar, energyWeight); + table.add(() -> ItemStack.AIR, 4); + result.addAll(table.roll(this.game.rnd())); + + return result; + } + + private void giveKey(Player player, int levelIndex) { + player.getInventory().addItemStack(BackroomsItems.levelKey(levelIndex)); + player.getInventory().addItemStack(BackroomsItems.almondWater()); + } + + private ItemStack createCompass(int levelIndex) { + Point doorPosition = this.doorPositionOf(levelIndex); + if(doorPosition == null) return null; + return BackroomsItems.doorCompass(levelIndex, this.game.getDimensionName(), doorPosition); + } + + /** Nach dem Tueroeffnen: alle Nachzuegler bekommen automatisch einen Kompass zur offenen Tuer. */ + public void giveCompassIfMissing(Player player, int levelIndex) { + if(this.hasCompassFor(player, levelIndex)) return; + ItemStack compass = this.createCompass(levelIndex); + if(compass != null) player.getInventory().addItemStack(compass); + } + + + private Point doorPositionOf(int levelIndex) { + LevelRuntime level = this.game.getLevels().get(levelIndex); + LevelLayout layout = level.getLayout(); + int[] door = layout.getDoorCell(); + if(door != null) { + return new Pos(layout.worldX(door[0]) + 1, level.getBaseY() + 2, layout.worldZ(door[1]) + 1); + } + // Fallback (Finale-Seitentueren setzen kein doorCell): erste DOOR-Zelle suchen + for(int cx = 0; cx < layout.getCells(); cx++) { + for(int cz = 0; cz < layout.getCells(); cz++) { + if(layout.cellAt(cx, cz) == LevelLayout.Cell.DOOR) { + return new Pos(layout.worldX(cx) + 1, level.getBaseY() + 2, layout.worldZ(cz) + 1); + } + } + } + return null; + } + + private boolean hasCompassFor(Player player, int levelIndex) { + return BackroomsItems.hasMatching(player, item -> + BackroomsItems.is(item, BackroomsItems.DOOR_COMPASS) + && Integer.valueOf(levelIndex).equals(item.getTag(BackroomsItems.KEY_LEVEL_TAG))); + } + + +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/loot/LootTable.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/loot/LootTable.java new file mode 100644 index 0000000..a7bc894 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/loot/LootTable.java @@ -0,0 +1,43 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.loot; + +import net.minestom.server.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.function.Supplier; + +/** + * Gewichtete Loot-Tabelle; eine Kiste liefert 1-2 Wuerfe. + */ +public class LootTable { + private record Entry(Supplier item, int weight) { + } + + private final List entries = new ArrayList<>(); + private int totalWeight = 0; + + public LootTable add(Supplier item, int weight) { + this.entries.add(new Entry(item, weight)); + this.totalWeight += weight; + return this; + } + + public List roll(Random rnd) { + List result = new ArrayList<>(); + int rolls = 1 + rnd.nextInt(2); + for(int i = 0; i < rolls; i++) { + int roll = rnd.nextInt(this.totalWeight); + for(Entry entry : this.entries) { + roll -= entry.weight(); + if(roll < 0) { + ItemStack item = entry.item().get(); + if(!item.isAir()) result.add(item); + break; + } + } + } + return result; + } + +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/BackroomsMonster.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/BackroomsMonster.java new file mode 100644 index 0000000..deea685 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/BackroomsMonster.java @@ -0,0 +1,207 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.EntityCreature; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.attribute.Attribute; +import net.minestom.server.entity.damage.DamageType; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.Instance; +import net.minestom.server.sound.SoundEvent; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.Optional; + +/** + * Basisklasse aller Backrooms-Monster: einfache Zustandsmaschine, Line-of-Sight- und + * Blickrichtungs-Helfer, Navigator-Pathing und Nahkampfangriff. Der zentrale AI-Tick + * des Spiels ruft {@link #monsterTick(long)} nur auf, solange Spieler im Level sind. + */ +public abstract class BackroomsMonster extends EntityCreature { + public enum State {IDLE, ALERT, CHASE, ATTACK, RETREAT} + + protected final LevelRuntime level; + protected State state = State.IDLE; + protected long stateSince = 0; + protected Player target; + + protected double viewRange = 16; + protected float attackDamage = 4; + protected double attackRange = 1.8; + protected int attackCooldownTicks = 20; + private long lastAttackTick = 0; + + /** Vom Monster-Director in Spielernaehe gespawnt — darf bei Distanz wieder despawnen */ + private boolean roaming = false; + + /** + * Finale-Modus: unbegrenzte Verfolgungsreichweite und Gummiband-Tempo — + * je weiter das Ziel voraus ist, desto schneller wird das Monster. + */ + private boolean relentless = false; + + /** + * Fest gebundenes Ziel (persoenliche Finale-Meute): das Monster jagt NUR diesen + * Spieler und entfernt sich selbst, sobald er entkommt, stirbt oder geht. + */ + private Player boundPlayer; + + protected BackroomsMonster(EntityType entityType, LevelRuntime level) { + super(entityType); + this.level = level; + } + + public abstract void monsterTick(long tick); + + + public boolean isRoaming() { + return this.roaming; + } + + public void setRoaming(boolean roaming) { + this.roaming = roaming; + } + + public void setRelentless(boolean relentless) { + this.relentless = relentless; + } + + public void setBoundPlayer(Player boundPlayer) { + this.boundPlayer = boundPlayer; + } + + + protected void setState(State state, long tick) { + if(this.state == state) return; + this.state = state; + this.stateSince = tick; + } + + public void spawnAtCell(Instance instance, int[] cell) { + Pos position = this.level.toPlayerPos(cell); + // Entity-Tracking ist 2D — ohne diese Regel wuerden Spieler die Monster + // aller vertikal gestapelten Level mitgesendet bekommen + this.updateViewableRule(viewer -> + Math.abs(viewer.getPosition().y() - this.getPosition().y()) < LevelRuntime.LEVEL_HEIGHT + ); + // Chunks sind beim Spielstart meist noch nicht geladen — die 3x3-Nachbarschaft + // vorladen, damit Physik und Pathfinding des Monsters nicht ins Leere greifen + int chunkX = position.chunkX(); + int chunkZ = position.chunkZ(); + List> loads = new ArrayList<>(); + for(int dx = -1; dx <= 1; dx++) { + for(int dz = -1; dz <= 1; dz++) { + loads.add(instance.loadChunk(chunkX + dx, chunkZ + dz)); + } + } + CompletableFuture.allOf(loads.toArray(CompletableFuture[]::new)) + .thenRun(() -> { + this.setInstance(instance, position); + MinecraftServer.getSchedulerManager().scheduleNextTick(() -> { + if(!this.isRemoved()) this.onSpawned(); + }); + }) + .exceptionally(throwable -> { + MinecraftServer.getExceptionManager().handleException(throwable); + return null; + }); + } + + /** Wird einen Tick nach dem Spawn aufgerufen (z. B. fuer Passagier-Displays). */ + protected void onSpawned() { + } + + /** Monster, die selbst in der Dunkelheit leben, verlieren dort keine Sichtweite */ + protected boolean ignoresDarkness = false; + + /** Sichtbarkeit inkl. Distanz; in Dunkelzonen sehen Monster nur halb so weit. */ + protected boolean canSee(Player player) { + if(player.getGameMode() != GameMode.ADVENTURE) return false; + // Relentless-Verfolger verlieren ihr Ziel nie — egal wie weit es flieht + if(this.relentless) return true; + boolean darkPenalty = !this.ignoresDarkness && this.level.getLayout().isDarkAt(player.getPosition()); + double range = darkPenalty ? this.viewRange / 2 : this.viewRange; + if(this.getDistance(player) > range) return false; + return this.hasLineOfSight(player); + } + + protected Optional nearestVisiblePlayer() { + // Gebundene Monster kennen nur ihr eigenes Ziel + if(this.boundPlayer != null) { + boolean valid = !this.boundPlayer.isRemoved() + && this.boundPlayer.getGameMode() == GameMode.ADVENTURE + && this.level.playersInLevel().contains(this.boundPlayer); + if(!valid) { + this.remove(); + return Optional.empty(); + } + return Optional.of(this.boundPlayer).filter(this::canSee); + } + + return this.level.playersInLevel().stream() + .filter(this::canSee) + .min(Comparator.comparingDouble(this::getDistance)); + } + + /** Schaut der Spieler dieses Monster direkt an? */ + protected boolean playerLooksAtMe(Player player) { + Pos playerPos = player.getPosition(); + var toMonster = this.getPosition().add(0, 1, 0).sub(playerPos.add(0, player.getEyeHeight(), 0)).asVec().normalize(); + return playerPos.direction().dot(toMonster) > 0.92 && player.hasLineOfSight(this); + } + + protected void moveTo(Point point, double speed) { + // Gummiband im Relentless-Modus: deutlich schneller, wenn das Ziel weit voraus ist + if(this.relentless) { + double distance = this.getPosition().distance(point); + speed += Math.clamp((distance - 20) * 0.008, 0, 0.18); + } + // Die Geschwindigkeit kommt ausschliesslich aus dem MOVEMENT_SPEED-Attribut — + // der double-Parameter von setPathTo ist die Zieldistanz, NICHT die Geschwindigkeit + this.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(speed); + this.getNavigator().setPathTo(point); + } + + /** Zufaellige begehbare Nachbarzelle als Wanderziel. */ + protected void wander(double speed) { + LevelLayout layout = this.level.getLayout(); + int cx = layout.cellX(this.getPosition().x()); + int cz = layout.cellZ(this.getPosition().z()); + for(int attempt = 0; attempt < 6; attempt++) { + int nx = cx + this.level.getRnd().nextInt(7) - 3; + int nz = cz + this.level.getRnd().nextInt(7) - 3; + if(!layout.isWalkable(nx, nz)) continue; + this.moveTo(this.level.toPlayerPos(new int[]{nx, nz}), speed); + return; + } + } + + /** Nahkampfangriff mit Cooldown; true, wenn zugeschlagen wurde. */ + protected boolean tryAttack(Player player, long tick) { + if(tick - this.lastAttackTick < this.attackCooldownTicks) return false; + if(this.getDistance(player) > this.attackRange) return false; + this.lastAttackTick = tick; + player.damage(DamageType.MOB_ATTACK, this.attackDamage); + var knockback = player.getPosition().sub(this.getPosition()).asVec().withY(0).normalize().mul(6).withY(4); + player.setVelocity(player.getVelocity().add(knockback)); + return true; + } + + /** Positionsgebundener Sound an alle Spieler des Levels. */ + protected void emitSound(SoundEvent sound, float volume, float pitch) { + Pos position = this.getPosition(); + this.level.playersInLevel().forEach(player -> + player.playSound(Sound.sound(sound, Sound.Source.HOSTILE, volume, pitch), position.x(), position.y(), position.z()) + ); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Bacteria.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Bacteria.java new file mode 100644 index 0000000..68aac22 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Bacteria.java @@ -0,0 +1,169 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.attribute.Attribute; +import net.minestom.server.entity.metadata.display.BlockDisplayMeta; +import net.minestom.server.instance.block.Block; +import net.minestom.server.sound.SoundEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * The Bacteria (nach Kane Pixels): eine turmhohe, skelettartige Gestalt mit + * ueberlangen Gliedmassen in unnatuerlichen Winkeln. Im Stillstand traegt sie die + * Wandtextur des Levels und verschmilzt mit der Umgebung — erwacht sie, faerbt + * sich der Koerper schlagartig schwarz. Sie lockt Wanderer mit imitierten + * menschlichen Geraeuschen an und bricht dann in einen rasend schnellen Sprint aus. + * Schleichende Spieler nimmt sie kaum wahr. + */ +public class Bacteria extends BackroomsMonster { + /** Distanz, ab der ein normal gehender Spieler den Angriff ausloest */ + private static final double TRIGGER_DISTANCE = 7; + /** Schleichende Spieler sind fast unsichtbar fuer ihn */ + private static final double SNEAK_TRIGGER_DISTANCE = 3.5; + /** Lock-Geraeusche fuer Spieler in diesem Ring */ + private static final double LURE_MIN = 8; + private static final double LURE_MAX = 24; + private static final double SPRINT_SPEED = 0.45; + private static final int CHASE_TIMEOUT_TICKS = 300; + /** + * Passagiere sitzen beim Zombie ~1.6 Bloecke ueber den Fuessen auf, Block-Displays + * rendern von ihrer Position nach oben — dieser Versatz stellt die Gestalt auf den + * Boden, sodass der Kopf auch unter 3er-Decken bleibt. + */ + private static final double BODY_Y_OFFSET = -1.6; + + private final List bodyDisplays = new ArrayList<>(); + private long lastLureTick = 0; + /** Wandtextur des Levels — die Tarnfarbe im Stillstand */ + private Block camouflageBlock = Block.BLACK_CONCRETE; + + public Bacteria(LevelRuntime level) { + super(EntityType.ZOMBIE, level); + this.viewRange = 24; + this.attackDamage = 5; + this.attackCooldownTicks = 25; + this.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(SPRINT_SPEED); + this.setInvisible(true); + } + + /** + * Koerper nach der Lore: eine skelettartige, humanoide Gestalt aus mehreren + * duennen, schwarzen, draht-artigen Segmenten — ueberlange Gliedmassen in + * unnatuerlichen Winkeln. Displays haben keine Kollision, der Traeger-Zombie + * bleibt normal gross und passt durch alle Raeume. + */ + @Override + protected void onSpawned() { + // Spawnt getarnt in der Wandtextur; die Jagd faerbt den Koerper schwarz + this.camouflageBlock = this.level.getDefinition().palette().wall().getFirst(); + Block wire = this.camouflageBlock; + double height = this.level.getDefinition().palette().roomHeight() - 0.2; + + // Beine: zwei duenne, leicht verkantete Staebe + this.spawnBodyPart(wire, new Vec(0.12, height * 0.5, 0.12), new Vec(-0.3, BODY_Y_OFFSET, -0.06), zTilt(8)); + this.spawnBodyPart(wire, new Vec(0.12, height * 0.5, 0.12), new Vec(0.16, BODY_Y_OFFSET, 0.02), zTilt(-6)); + // Torso: schmaler, schief sitzender Kern + this.spawnBodyPart(wire, new Vec(0.22, height * 0.45, 0.22), new Vec(-0.11, BODY_Y_OFFSET + height * 0.45, -0.11), zTilt(-4)); + // Arme: ueberlang, in seltsamen Winkeln abgespreizt + this.spawnBodyPart(wire, new Vec(0.1, height * 0.5, 0.1), new Vec(-0.45, BODY_Y_OFFSET + height * 0.42, 0), zTilt(25)); + this.spawnBodyPart(wire, new Vec(0.1, height * 0.55, 0.1), new Vec(0.32, BODY_Y_OFFSET + height * 0.38, 0.06), zTilt(-18)); + // Kopf: kleiner, versetzter Klumpen ohne Gesicht + this.spawnBodyPart(wire, new Vec(0.32, 0.35, 0.32), new Vec(-0.16, BODY_Y_OFFSET + height * 0.9, -0.16), null); + } + + /** Faerbt alle Koerperteile um (Tarnung an/aus). */ + private void setBodyBlock(Block block) { + this.bodyDisplays.forEach(display -> + ((BlockDisplayMeta) display.getEntityMeta()).setBlockState(block) + ); + } + + /** Quaternion fuer eine Neigung um die Z-Achse (Grad). */ + private static float[] zTilt(double degrees) { + double half = Math.toRadians(degrees) / 2; + return new float[]{0, 0, (float) Math.sin(half), (float) Math.cos(half)}; + } + + private void spawnBodyPart(Block block, Vec scale, Vec translation, float[] leftRotation) { + Entity display = new Entity(EntityType.BLOCK_DISPLAY); + BlockDisplayMeta meta = (BlockDisplayMeta) display.getEntityMeta(); + meta.setBlockState(block); + meta.setScale(scale); + meta.setTranslation(translation); + if(leftRotation != null) meta.setLeftRotation(leftRotation); + display.updateViewableRule(viewer -> + Math.abs(viewer.getPosition().y() - this.getPosition().y()) < LevelRuntime.LEVEL_HEIGHT + ); + display.setInstance(this.getInstance(), this.getPosition()); + this.addPassenger(display); + this.bodyDisplays.add(display); + } + + @Override + public void remove() { + this.bodyDisplays.forEach(Entity::remove); + super.remove(); + } + + @Override + public void monsterTick(long tick) { + if(this.state == State.CHASE) { + this.chaseTick(tick); + return; + } + + // Tarnung: absolute Regungslosigkeit — er IST die Wand + Player closest = this.level.playersInLevel().stream() + .min((a, b) -> Double.compare(this.getDistance(a), this.getDistance(b))) + .orElse(null); + if(closest == null) return; + + double distance = this.getDistance(closest); + double trigger = closest.isSneaking() ? SNEAK_TRIGGER_DISTANCE : TRIGGER_DISTANCE; + + if(distance < trigger) { + // Ausbruch: die Tarnung faellt — der Koerper wird schwarz, dann der Schrei und der Sprint + this.target = closest; + this.setState(State.CHASE, tick); + this.setBodyBlock(Block.BLACK_CONCRETE); + this.emitSound(SoundEvent.ENTITY_GHAST_SCREAM, 1.6f, 0.4f); + this.emitSound(SoundEvent.ENTITY_ENDERMAN_SCREAM, 1.2f, 0.4f); + return; + } + + // Koeder: imitierte menschliche Geraeusche locken Spieler in seine Richtung + if(distance >= LURE_MIN && distance <= LURE_MAX && tick - this.lastLureTick > 240) { + this.lastLureTick = tick; + if(this.level.getRnd().nextBoolean()) { + this.emitSound(SoundEvent.BLOCK_STONE_STEP, 0.9f, 0.8f); + } else { + this.emitSound(SoundEvent.ENTITY_ZOMBIE_AMBIENT, 0.7f, 0.35f); + } + } + } + + private void chaseTick(long tick) { + if(this.target == null || this.target.isRemoved() + || !this.level.playersInLevel().contains(this.target) + || tick - this.stateSince > CHASE_TIMEOUT_TICKS + || this.getDistance(this.target) > this.viewRange) { + // Beute verloren: schlagartig erstarren und zurueck in die Tarnfarbe + this.target = null; + this.setState(State.IDLE, tick); + this.getNavigator().setPathTo(null); + this.setBodyBlock(this.camouflageBlock); + this.emitSound(SoundEvent.BLOCK_STONE_PLACE, 0.8f, 0.5f); + return; + } + + if(tick % 6 < 2) this.moveTo(this.target.getPosition(), SPRINT_SPEED); + this.tryAttack(this.target, tick); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Clump.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Clump.java new file mode 100644 index 0000000..141c040 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Clump.java @@ -0,0 +1,59 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.attribute.Attribute; +import net.minestom.server.entity.metadata.cube.SlimeMeta; +import net.minestom.server.sound.SoundEvent; + +import java.util.Comparator; +import java.util.Optional; + +/** + * Clump: getarnte Proximity-Falle. Wartet regungslos, springt einen Spieler an, + * der zu nah kommt, und verschwindet 3 Sekunden nach dem Jumpscare. + */ +public class Clump extends BackroomsMonster { + private static final double TRIGGER_RANGE = 2.5; + private static final int DESPAWN_DELAY_TICKS = 60; + + public Clump(LevelRuntime level) { + super(EntityType.SLIME, level); + this.attackDamage = 4; + this.attackRange = 3; + if(this.getEntityMeta() instanceof SlimeMeta slimeMeta) { + slimeMeta.setSize(2); + } else { + this.getAttribute(Attribute.SCALE).setBaseValue(0.7); + } + } + + @Override + public void monsterTick(long tick) { + switch(this.state) { + case IDLE -> this.nearestPlayerInRange().ifPresent(player -> this.jumpscare(player, tick)); + case ATTACK -> { + if(this.target != null && !this.target.isRemoved()) this.tryAttack(this.target, tick); + if(tick - this.stateSince >= DESPAWN_DELAY_TICKS) this.remove(); + } + default -> this.setState(State.IDLE, tick); + } + } + + private Optional nearestPlayerInRange() { + return this.level.playersInLevel().stream() + .filter(player -> this.getDistance(player) < TRIGGER_RANGE) + .min(Comparator.comparingDouble(this::getDistance)); + } + + private void jumpscare(Player player, long tick) { + this.target = player; + this.emitSound(SoundEvent.ENTITY_SLIME_ATTACK, 1.5f, 0.8f); + this.emitSound(SoundEvent.ENTITY_ENDERMAN_SCREAM, 1.2f, 0.7f); + var leap = player.getPosition().sub(this.getPosition()).asVec().withY(0).normalize().mul(8).withY(6); + this.setVelocity(this.getVelocity().add(leap)); + this.tryAttack(player, tick); + this.setState(State.ATTACK, tick); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Deathmoth.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Deathmoth.java new file mode 100644 index 0000000..772116b --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Deathmoth.java @@ -0,0 +1,62 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.attribute.Attribute; +import net.minestom.server.sound.SoundEvent; + +/** + * Deathmoth: kleiner fliegender Schwarmjaeger (Phantom-Modell). Umkreist die Beute + * flatternd und stuerzt sich in kurzen Attacken auf sie — wenig Schaden, viel Unruhe. + */ +public class Deathmoth extends BackroomsMonster { + private int swoopCooldown = 0; + + public Deathmoth(LevelRuntime level) { + super(EntityType.PHANTOM, level); + this.viewRange = 26; + this.attackDamage = 2; + this.attackRange = 2.0; + this.attackCooldownTicks = 30; + this.getAttribute(Attribute.SCALE).setBaseValue(0.6); + this.setNoGravity(true); + } + + @Override + public void monsterTick(long tick) { + if(this.target == null || this.target.isRemoved() || !this.canSee(this.target)) { + this.target = this.nearestVisiblePlayer().orElse(null); + } + + if(this.target == null) { + // Flatterndes Schweben auf der Stelle + if(tick % 20 < 2) { + this.setVelocity(new Vec( + (this.level.getRnd().nextDouble() - 0.5) * 3, + (this.level.getRnd().nextDouble() - 0.3) * 1.5, + (this.level.getRnd().nextDouble() - 0.5) * 3 + )); + } + return; + } + + Player player = this.target; + if(tick % 10 < 2) { + // Zielpunkt: leicht ueber dem Kopf der Beute, Sturzflug wenn Cooldown vorbei + this.swoopCooldown = Math.max(0, this.swoopCooldown - 10); + double height = this.swoopCooldown > 0 ? 2.5 : 0.5; + Vec toPlayer = player.getPosition().add(0, height, 0).sub(this.getPosition()).asVec(); + if(toPlayer.length() > 0.5) { + this.setVelocity(toPlayer.normalize().mul(this.swoopCooldown > 0 ? 3.5 : 6)); + } + if(tick % 40 < 2) this.emitSound(SoundEvent.ENTITY_PHANTOM_FLAP, 0.8f, 1.4f); + } + + if(this.tryAttack(player, tick)) { + this.swoopCooldown = 60; + this.emitSound(SoundEvent.ENTITY_PHANTOM_BITE, 1f, 1.3f); + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Hound.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Hound.java new file mode 100644 index 0000000..48fb492 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Hound.java @@ -0,0 +1,64 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.EquipmentSlot; +import net.minestom.server.entity.attribute.Attribute; +import net.minestom.server.sound.SoundEvent; + +/** + * Hound: patrouilliert das Level, knurrt bei Sichtkontakt und jagt den Spieler, + * bis er ihn 200 Ticks lang nicht mehr sieht. + */ +public class Hound extends BackroomsMonster { + private long lastSeenTick = 0; + + public Hound(LevelRuntime level) { + super(EntityType.ZOMBIE, level); + this.viewRange = 28; + this.attackDamage = 3; + this.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.22); + this.getAttribute(Attribute.SCALE).setBaseValue(1.15); + this.setEquipment(EquipmentSlot.HELMET, MonsterAppearance.playerHead(MonsterAppearance.HOUND_HEAD)); + } + + @Override + public void monsterTick(long tick) { + switch(this.state) { + case IDLE -> { + if(tick % 40 < 2) this.wander(0.1); + this.nearestVisiblePlayer().ifPresent(player -> { + this.target = player; + this.lastSeenTick = tick; + this.setState(State.ALERT, tick); + this.getNavigator().setPathTo(null); + this.emitSound(SoundEvent.ENTITY_WOLF_GROWL, 1.2f, 0.6f); + player.playSound(Sound.sound(SoundEvent.ENTITY_WARDEN_HEARTBEAT, Sound.Source.HOSTILE, 0.4f, 1f)); + }); + } + case ALERT -> { + if(tick - this.stateSince >= 40) this.setState(State.CHASE, tick); + } + case CHASE, ATTACK -> { + if(this.target == null || this.target.isRemoved() || !this.level.playersInLevel().contains(this.target)) { + this.target = null; + this.setState(State.RETREAT, tick); + return; + } + if(this.canSee(this.target)) this.lastSeenTick = tick; + if(tick - this.lastSeenTick > 100) { + this.target = null; + this.setState(State.RETREAT, tick); + return; + } + if(tick % 10 < 2) this.moveTo(this.target.getPosition(), 0.25); + this.tryAttack(this.target, tick); + } + case RETREAT -> { + if(tick % 40 < 2) this.wander(0.1); + if(tick - this.stateSince >= 100) this.setState(State.IDLE, tick); + } + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/MonsterAppearance.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/MonsterAppearance.java new file mode 100644 index 0000000..1bb9472 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/MonsterAppearance.java @@ -0,0 +1,41 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster; + +import net.minestom.server.color.Color; +import net.minestom.server.component.DataComponents; +import net.minestom.server.entity.PlayerSkin; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.network.player.ResolvableProfile; + +/** + * Statische Helfer fuer das Aussehen der Backrooms-Monster: Custom Heads aus + * Base64-Texturen und gefaerbte Lederruestung. + */ +public final class MonsterAppearance { + /** Hound (Backrooms) — duesterer Hundekopf. */ + public static final String HOUND_HEAD = + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDgzMzhiMGIxNDkyMDU5YzIzMGNjYzg5ZmNhZTE0ZThhODU1ZTcwYjllNWVhNDVjYWIzZjVmNWI5ODJkYzc4MiJ9fX0="; + /** Smiler — weisses Grinsen auf schwarzem Grund (minecraft-heads.com #74583). */ + public static final String SMILER_HEAD = + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODc2ZmViODIxZTVkYTE1NTRiODkzZmM1MzY1ZjdiN2E5ODY3M2QxZjliNzU3OTZiODdmNzRlM2M5NjFjMGU3OCJ9fX0="; + /** Partygoer — gelber Smiley. */ + public static final String PARTYGOER_HEAD = + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDI1MzUwODhkNDQ1ZDZhNDQ5ODc2YWRhOTg4ZmExMDc5ZDU3MmQyMTQ3MTNiYjNkY2VhMWRhM2JjNGY3ZDk2YiJ9fX0="; + + private MonsterAppearance() { + } + + /** Spielerkopf mit Custom-Textur (Base64 des textures-JSON). */ + public static ItemStack playerHead(String base64Textures) { + return ItemStack.builder(Material.PLAYER_HEAD) + .set(DataComponents.PROFILE, new ResolvableProfile(new PlayerSkin(base64Textures, null))) + .build(); + } + + /** Gefaerbtes Lederruestungsteil, rgb z.B. 0xFFD83D. */ + public static ItemStack dyedLeather(Material leatherPiece, int rgb) { + return ItemStack.builder(leatherPiece) + .set(DataComponents.DYED_COLOR, new Color(rgb)) + .build(); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Partygoer.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Partygoer.java new file mode 100644 index 0000000..362e412 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Partygoer.java @@ -0,0 +1,49 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.EquipmentSlot; +import net.minestom.server.entity.attribute.Attribute; +import net.minestom.server.item.Material; +import net.minestom.server.potion.Potion; +import net.minestom.server.potion.PotionEffect; +import net.minestom.server.sound.SoundEvent; + +/** + * Partygoer: verfolgt unablaessig langsam den naechsten sichtbaren Spieler und + * "feiert" dabei hoerbar. Treffer verlangsamen den Spieler — die beginnende + * Verwandlung. + */ +public class Partygoer extends BackroomsMonster { + public Partygoer(LevelRuntime level) { + super(EntityType.ZOMBIE, level); + this.viewRange = 32; + this.attackDamage = 3; + this.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.15); + this.getAttribute(Attribute.SCALE).setBaseValue(1.1); + this.setEquipment(EquipmentSlot.HELMET, MonsterAppearance.playerHead(MonsterAppearance.PARTYGOER_HEAD)); + this.setEquipment(EquipmentSlot.CHESTPLATE, MonsterAppearance.dyedLeather(Material.LEATHER_CHESTPLATE, 0xFFD83D)); + } + + @Override + public void monsterTick(long tick) { + if(tick % 160 < 2) { + SoundEvent celebration = this.level.getRnd().nextBoolean() + ? SoundEvent.ENTITY_VILLAGER_CELEBRATE + : SoundEvent.BLOCK_NOTE_BLOCK_BIT; + this.emitSound(celebration, 1f, 1.2f); + } + + this.target = this.nearestVisiblePlayer().orElse(null); + if(this.target != null) { + this.setState(State.CHASE, tick); + if(tick % 10 < 2) this.moveTo(this.target.getPosition(), 0.17); + if(this.tryAttack(this.target, tick)) { + this.target.addEffect(new Potion(PotionEffect.SLOWNESS, 0, 60)); + } + } else { + this.setState(State.IDLE, tick); + if(tick % 40 < 2) this.wander(0.1); + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Smiler.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Smiler.java new file mode 100644 index 0000000..5a84643 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Smiler.java @@ -0,0 +1,132 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience.Jumpscare; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.attribute.Attribute; +import net.minestom.server.entity.metadata.display.AbstractDisplayMeta; +import net.minestom.server.entity.metadata.display.ItemDisplayMeta; +import net.minestom.server.sound.SoundEvent; + +/** + * Smiler: unsichtbarer Koerper, nur der grinsende Kopf schwebt in der Dunkelheit. + * Dreht sich immer zum Spieler (das Grinsen folgt dir), friert ein, solange er + * direkt angesehen wird, und kriecht sonst stetig naeher. Verlaesst die Dunkelzone + * nur fuer den finalen Schlag. + */ +public class Smiler extends BackroomsMonster { + /** Groesse des Grinsens (Item-Display, unabhaengig von der Koerpergroesse) */ + private static final float GRIN_SCALE = 2.2f; + /** Versatz relativ zum Aufsitzpunkt: Grinsen auf Brust-/Augenhoehe, nicht im Boden */ + private static final float GRIN_Y_OFFSET = 0.4f; + + private Entity headDisplay; + + public Smiler(LevelRuntime level) { + super(EntityType.ZOMBIE, level); + this.viewRange = 16; + this.attackDamage = 4; + this.ignoresDarkness = true; + this.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.11); + this.setInvisible(true); + } + + /** + * Das Grinsen ist ein grosses Item-Display, das als Passagier mitfaehrt: + * frei skalierbar, tief positioniert, und per Billboard dreht es sich + * clientseitig immer zu jedem Betrachter. + */ + @Override + protected void onSpawned() { + this.headDisplay = new Entity(EntityType.ITEM_DISPLAY); + ItemDisplayMeta meta = (ItemDisplayMeta) this.headDisplay.getEntityMeta(); + meta.setItemStack(MonsterAppearance.playerHead(MonsterAppearance.SMILER_HEAD)); + meta.setScale(new Vec(GRIN_SCALE, GRIN_SCALE, GRIN_SCALE)); + meta.setTranslation(new Vec(0, GRIN_Y_OFFSET, 0)); + meta.setBillboardRenderConstraints(AbstractDisplayMeta.BillboardConstraints.CENTER); + // Player-Heads rendern im Item-Display mit dem Hinterkopf zur Kamera — + // 180 Grad um die Y-Achse drehen, damit das Grinsen den Betrachter ansieht + meta.setLeftRotation(new float[]{0, 1, 0, 0}); + this.headDisplay.updateViewableRule(viewer -> + Math.abs(viewer.getPosition().y() - this.getPosition().y()) < LevelRuntime.LEVEL_HEIGHT + ); + this.headDisplay.setInstance(this.getInstance(), this.getPosition()); + this.addPassenger(this.headDisplay); + } + + @Override + public void remove() { + if(this.headDisplay != null) this.headDisplay.remove(); + super.remove(); + } + + @Override + public void monsterTick(long tick) { + // Ziel finden bzw. validieren — jeder sichtbare Spieler in Reichweite triggert + if(this.target == null || this.target.isRemoved() || !this.canSee(this.target)) { + Player next = this.nearestVisiblePlayer().orElse(null); + if(next != null && this.target == null) { + // Erster Sichtkontakt: leises Knistern beim Spieler + next.playSound( + Sound.sound(SoundEvent.BLOCK_SCULK_SENSOR_CLICKING, Sound.Source.HOSTILE, 0.6f, 0.5f), + Sound.Emitter.self() + ); + } + this.target = next; + } + + if(this.target == null) { + this.setState(State.IDLE, tick); + this.getNavigator().setPathTo(null); + return; + } + + this.setState(State.CHASE, tick); + // Das Grinsen folgt dir immer + this.lookAt(this.target); + + if(this.tryAttack(this.target, tick)) { + // Der Schlag aus der Dunkelheit ist DER Jumpscare-Moment des Smilers + Jumpscare.visual(this.target, this.getInstance()); + } + + // Blickkontakt friert ihn ein — wegsehen laesst ihn weiterkriechen + if(this.playerLooksAtMe(this.target)) { + this.getNavigator().setPathTo(null); + return; + } + + if(tick % 10 < 2) { + LevelLayout layout = this.level.getLayout(); + boolean selfInDark = layout.isDarkAt(this.getPosition()); + boolean closeKill = this.getDistance(this.target) < 4; + + if(selfInDark || closeKill) { + // Sprintende Beute macht ihn schneller + double speed = this.target.isSprinting() ? 0.18 : 0.11; + this.moveTo(this.target.getPosition(), speed); + } else { + // Zurueck in die Dunkelheit lauern + this.wanderIntoDark(); + } + } + } + + private void wanderIntoDark() { + LevelLayout layout = this.level.getLayout(); + int cx = layout.cellX(this.getPosition().x()); + int cz = layout.cellZ(this.getPosition().z()); + for(int attempt = 0; attempt < 8; attempt++) { + int nx = cx + this.level.getRnd().nextInt(5) - 2; + int nz = cz + this.level.getRnd().nextInt(5) - 2; + if(!layout.isWalkable(nx, nz) || !layout.isDarkCell(nx, nz)) continue; + this.moveTo(this.level.toPlayerPos(new int[]{nx, nz}), 0.11); + return; + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Wretch.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Wretch.java new file mode 100644 index 0000000..364a166 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/monster/Wretch.java @@ -0,0 +1,42 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.monster; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.attribute.Attribute; +import net.minestom.server.sound.SoundEvent; + +/** + * Wretch: ausgezehrter, durchgehend feindseliger Humanoid (Husk-Modell). + * Kein Warnverhalten wie der Hound — sieht er dich, rennt er sofort los. + * Schnell, aber schwach: der Schrecken liegt im Tempo. + */ +public class Wretch extends BackroomsMonster { + public Wretch(LevelRuntime level) { + super(EntityType.HUSK, level); + this.viewRange = 26; + this.attackDamage = 2; + this.attackCooldownTicks = 15; + this.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.28); + this.getAttribute(Attribute.SCALE).setBaseValue(0.95); + } + + @Override + public void monsterTick(long tick) { + if(this.target == null || this.target.isRemoved() || !this.canSee(this.target)) { + this.target = this.nearestVisiblePlayer().orElse(null); + if(this.target != null) { + this.setState(State.CHASE, tick); + this.emitSound(SoundEvent.ENTITY_HUSK_AMBIENT, 1.2f, 0.6f); + } + } + + if(this.target == null) { + this.setState(State.IDLE, tick); + if(tick % 40 < 2) this.wander(0.12); + return; + } + + if(tick % 10 < 2) this.moveTo(this.target.getPosition(), 0.3); + this.tryAttack(this.target, tick); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/sanity/SanityManager.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/sanity/SanityManager.java new file mode 100644 index 0000000..885d378 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/sanity/SanityManager.java @@ -0,0 +1,101 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.sanity; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsPlayer; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.ambience.Jumpscare; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.damage.DamageType; +import net.minestom.server.potion.Potion; +import net.minestom.server.potion.PotionEffect; +import net.minestom.server.sound.SoundEvent; +import net.minestom.server.timer.ExecutionType; +import net.minestom.server.timer.TaskSchedule; + +import java.util.Random; + +/** + * Leichtgewichtige Sanity: sinkt in Dunkelheit und Monsternaehe, steigt im Licht. + * Niedrige Werte erzeugen Halluzinationen (Fake-Scares, Herzschlag, Dunkelheits-Pulse); + * toedlich ist Sanity nur im Hardcore-Modus. + */ +public class SanityManager { + private final BackroomsGame game; + private final boolean hardcore; + private final Random rnd = new Random(); + + public SanityManager(BackroomsGame game, boolean hardcore) { + this.game = game; + this.hardcore = hardcore; + } + + public void start() { + this.game.scheduler().submitTask(() -> { + if(!this.game.isRunning()) return TaskSchedule.stop(); + this.game.getPlayers().stream() + .filter(player -> player.getGameMode() == GameMode.ADVENTURE) + .forEach(player -> { + try { + this.tickPlayer(player); + } catch(Exception e) { + // Ein Fehler darf den Sanity-Task nicht dauerhaft toeten + net.minestom.server.MinecraftServer.getExceptionManager().handleException(e); + } + }); + return TaskSchedule.seconds(1); + }, ExecutionType.TICK_END); + } + + private void tickPlayer(Player player) { + BackroomsPlayer state = this.game.playerState(player); + if(state == null) return; + LevelRuntime level = this.game.getLevels().get(state.getLevelIndex()); + + double delta = 0.01; + boolean inDark = level.getDefinition().permanentDarkness() + || level.isBlackoutActive() + || level.getLayout().isDarkAt(player.getPosition()); + if(inDark) delta = -0.01; + + boolean monsterNearby = level.getMonsters().stream() + .anyMatch(monster -> !monster.isRemoved() && monster.getDistance(player) < 8); + if(monsterNearby) delta -= 0.02; + + // Dunkelheit ist nie sicher: kleine Chance auf einen Scare, unabhaengig von der Sanity + if(inDark && this.rnd.nextInt(100) < 3) { + if(this.rnd.nextInt(4) == 0) { + Jumpscare.visual(player, this.game); + } else { + Jumpscare.sound(player, this.rnd); + } + } + + state.setSanity(Math.clamp(state.getSanity() + delta, 0, 1)); + player.setExp((float) state.getSanity()); + + double sanity = state.getSanity(); + if(sanity < 0.6 && this.rnd.nextInt(100) < 5) { + player.playSound(Sound.sound(SoundEvent.AMBIENT_CAVE, Sound.Source.AMBIENT, 0.7f, 0.5f), Sound.Emitter.self()); + } + if(sanity < 0.4 && this.rnd.nextInt(100) < 8) { + Jumpscare.fake(player, this.rnd); + player.playSound(Sound.sound(SoundEvent.ENTITY_WARDEN_HEARTBEAT, Sound.Source.AMBIENT, 0.9f, 1f), Sound.Emitter.self()); + } + if(sanity < 0.2) { + if(this.rnd.nextInt(100) < 20) player.addEffect(new Potion(PotionEffect.DARKNESS, (byte) 0, 45)); + if(this.rnd.nextInt(100) < 5) Jumpscare.visual(player, this.game); + } + if(this.hardcore && sanity <= 0 && this.rnd.nextInt(3) == 0) { + player.damage(DamageType.MAGIC, 1); + } + } + + public void restore(Player player, double amount) { + BackroomsPlayer state = this.game.playerState(player); + if(state == null) return; + state.setSanity(Math.clamp(state.getSanity() + amount, 0, 1)); + player.setExp((float) state.getSanity()); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/transition/TransitionManager.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/transition/TransitionManager.java new file mode 100644 index 0000000..6413a99 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/backrooms/transition/TransitionManager.java @@ -0,0 +1,87 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.transition; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsPlayer; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelLayout; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.LevelRuntime; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.level.TransitionType; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Player; +import net.minestom.server.potion.Potion; +import net.minestom.server.potion.PotionEffect; +import net.minestom.server.sound.SoundEvent; + +/** + * Erkennt Level-Uebergaenge: Fall durch den Exit-Schacht (HOLE/LADDER), Betreten des + * Glitch-Bodens, Verweilen an der No-Clip-Wand und das Erreichen des Finale-Ziels. + */ +public class TransitionManager { + + private final BackroomsGame game; + + public TransitionManager(BackroomsGame game) { + this.game = game; + } + + public void onMove(Player player, Pos newPosition) { + BackroomsPlayer state = this.game.playerState(player); + if(state == null || state.isEscaped() || state.isEliminated()) return; + + LevelRuntime level = this.game.getLevels().get(state.getLevelIndex()); + TransitionType transition = level.getDefinition().transitionType(); + + // Fall durch den offenen Schacht ins naechste Level + if(newPosition.y() < level.getBaseY() - 4) { + this.game.advanceLevel(player, false); + return; + } + + // Finale: der Zieleinlauf ist die gesamte Korridorbreite ab der Exit-Zelle + if(transition == TransitionType.ESCAPE) { + int[] exit = level.getLayout().getExitCell(); + if(exit != null && level.getLayout().cellX(newPosition.x()) >= exit[0]) this.game.escape(player); + return; + } + + if(!this.isInExitCell(level, newPosition)) return; + + if(transition == TransitionType.GLITCH_FLOOR) this.game.advanceLevel(player, true); + } + + /** Wird alle 10 Ticks aufgerufen: Level-Abgleich und Rueckweg-Sperre. */ + public void onTick(Player player) { + BackroomsPlayer state = this.game.playerState(player); + if(state == null || state.isEscaped() || state.isEliminated()) return; + + boolean settled = System.currentTimeMillis() - state.getLevelEnteredMillis() > 5000; + if(!settled) return; + + // Selbstheilung: registriertes Level mit der physischen Hoehe abgleichen. + // Ohne diesen Abgleich laufen Spieler nach einem verpassten oder + // zurueckgekletterten Uebergang in einem Level herum, in dem sie nicht + // registriert sind — Monster ohne AI, Level-Events starten nicht. + LevelRuntime physical = this.game.levelAt((int) player.getPosition().y()); + if(physical == null || physical.getIndex() == state.getLevelIndex()) return; + + if(physical.getIndex() > state.getLevelIndex()) { + // Drift nach unten (verpasster Uebergang): Zustand nachziehen + while(state.getLevelIndex() < physical.getIndex()) { + this.game.advanceLevel(player, false); + } + } else { + // Rueckweg gesperrt: die Backrooms holen dich zurueck ins registrierte Level + LevelRuntime registered = this.game.getLevels().get(state.getLevelIndex()); + player.teleport(registered.randomSpawn()); + player.addEffect(new Potion(PotionEffect.BLINDNESS, (byte) 0, 40)); + player.playSound(Sound.sound(SoundEvent.BLOCK_PORTAL_TRAVEL, Sound.Source.AMBIENT, 0.4f, 0.4f), Sound.Emitter.self()); + } + } + + private boolean isInExitCell(LevelRuntime level, Pos position) { + LevelLayout layout = level.getLayout(); + int[] exit = layout.getExitCell(); + if(exit == null) return false; + return layout.cellX(position.x()) == exit[0] && layout.cellZ(position.z()) == exit[1]; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bedwars/Bedwars.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bedwars/Bedwars.java index 88690d7..ee9aca2 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bedwars/Bedwars.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bedwars/Bedwars.java @@ -26,10 +26,13 @@ import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.timer.ExecutionType; import net.minestom.server.timer.TaskSchedule; +import net.minestom.server.world.DimensionType; import org.jetbrains.annotations.NotNull; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; public class Bedwars extends StatelessGame { @@ -39,22 +42,22 @@ public class Bedwars extends StatelessGame { public Bedwars() throws IOException { super(Dimension.OVERWORLD.key, "Bedwars", new LastWinsScore()); - setChunkLoader(new AnvilLoader(Resource.GAME_MAP.getPath().resolve("bedwars/test"))); + this.setChunkLoader(new AnvilLoader(Resource.GAME_MAP.getPath().resolve("bedwars/test"), DimensionType.OVERWORLD.key())); Configuration config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(Resource.GAME_MAP.getPath().resolve("bedwars/test/config.yml").toFile()); List teamNames = config.getStringList("setup.teams"); teamNames.forEach(teamName -> { String path = "teams." + teamName; - teams.add(new BedwarsTeam( - config.getString(path + ".name"), - Position.getPosFromCommaSeparated(config.getString(path + ".pos.spawn")), - Position.getPosFromCommaSeparated(config.getString(path + ".pos.spawner")), - config.getStringList(path + ".pos.bed").stream().map(Position::getPosFromCommaSeparated).toList().toArray(new Pos[0]), - MaterialUtil.fromString(config.getString(path + ".material.block"), Material.BLACK_WOOL) + this.teams.add(new BedwarsTeam( + config.getString(path + ".name"), + Position.getPosFromCommaSeparated(config.getString(path + ".pos.spawn")), + Position.getPosFromCommaSeparated(config.getString(path + ".pos.spawner")), + config.getStringList(path + ".pos.bed").stream().map(Position::getPosFromCommaSeparated).toList().toArray(new Pos[0]), + MaterialUtil.fromString(config.getString(path + ".material.block"), Material.BLACK_WOOL) )); }); - eventNode().addListener(PickupItemEvent.class, pickupItemEvent -> { + this.eventNode().addListener(PickupItemEvent.class, pickupItemEvent -> { if(pickupItemEvent.getEntity() instanceof Player p) { p.getInventory().addItemStack(pickupItemEvent.getItemStack()); } @@ -68,10 +71,10 @@ public class Bedwars extends StatelessGame { @Override protected void onStart() { - scheduler().submitTask(() -> { - if(!isRunning) return TaskSchedule.stop(); + this.scheduler().submitTask(() -> { + if(!this.isRunning) return TaskSchedule.stop(); - teams.forEach(bedwarsTeam -> { + this.teams.forEach(bedwarsTeam -> { ItemEntity item = new ItemEntity(ItemStack.of(bedwarsTeam.getBlock())); item.setNoGravity(true); item.setInstance(this, bedwarsTeam.getSpawner()); @@ -84,16 +87,17 @@ public class Bedwars extends StatelessGame { @Override protected void onBlockPlace(@NotNull PlayerBlockPlaceEvent playerBlockPlaceEvent) { - placedBlocks.add(playerBlockPlaceEvent.getBlockPosition()); + this.placedBlocks.add(playerBlockPlaceEvent.getBlockPosition()); } @Override protected void onBlockBreak(@NotNull PlayerBlockBreakEvent playerBlockBreakEvent) { - if(!placedBlocks.contains(playerBlockBreakEvent.getBlockPosition())) playerBlockBreakEvent.setCancelled(true); + if(!this.placedBlocks.contains(playerBlockBreakEvent.getBlockPosition())) + playerBlockBreakEvent.setCancelled(true); - teams.forEach(bedwarsTeam -> { + this.teams.forEach(bedwarsTeam -> { if(Arrays.stream(bedwarsTeam.getBed()).anyMatch(pos -> pos.sameBlock(playerBlockBreakEvent.getBlockPosition()))) - breakBed(bedwarsTeam); + this.breakBed(bedwarsTeam); }); if(playerBlockBreakEvent.isCancelled()) @@ -102,13 +106,13 @@ public class Bedwars extends StatelessGame { @Override protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { - if(playerMoveEvent.getNewPosition().y() < 0) death(playerMoveEvent.getPlayer()); + if(playerMoveEvent.getNewPosition().y() < 0) this.death(playerMoveEvent.getPlayer()); } private void breakBed(BedwarsTeam team) { team.removeBed(); - for (Pos blockPos : team.getBed()) { - setBlock(blockPos, Material.AIR.block()); + for(Pos blockPos : team.getBed()) { + this.setBlock(blockPos, Material.AIR.block()); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bedwars/BedwarsFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bedwars/BedwarsFactory.java index 0de3d94..3290e7e 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bedwars/BedwarsFactory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bedwars/BedwarsFactory.java @@ -20,7 +20,7 @@ public class BedwarsFactory implements GameFactory { @Override public RestrictionHandler globalRestrictions() { return new RestrictionHandler() - .addRestriction(new MinimalPlayeramountGameRestriction(1)); + .addRestriction(new MinimalPlayeramountGameRestriction(1)); } @Override diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bedwars/data/BedwarsTeam.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bedwars/data/BedwarsTeam.java index 219355c..6bd1a5c 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bedwars/data/BedwarsTeam.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bedwars/data/BedwarsTeam.java @@ -8,16 +8,12 @@ import java.util.ArrayList; import java.util.List; public class BedwarsTeam { - private List members = new ArrayList<>(); - private final String name; private final Pos spawn; private final Pos spawner; - private final Pos[] bed; - private final Material block; - + private List members = new ArrayList<>(); private boolean hasBed = true; public BedwarsTeam(String name, Pos spawn, Pos spawner, Pos[] bed, Material block) { @@ -33,11 +29,11 @@ public class BedwarsTeam { } public boolean isHasBed() { - return hasBed; + return this.hasBed; } public List getMembers() { - return members; + return this.members; } public void setMembers(List members) { @@ -45,22 +41,22 @@ public class BedwarsTeam { } public String getName() { - return name; + return this.name; } public Pos getSpawn() { - return spawn; + return this.spawn; } public Pos getSpawner() { - return spawner; + return this.spawner; } public Pos[] getBed() { - return bed; + return this.bed; } public Material getBlock() { - return block; + return this.block; } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBattle/BlockBattle.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBattle/BlockBattle.java new file mode 100644 index 0000000..e005442 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBattle/BlockBattle.java @@ -0,0 +1,207 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBattle; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; +import eu.mhsl.minenet.minigames.score.FirstWinsScore; +import eu.mhsl.minenet.minigames.util.BatchUtil; +import io.github.togar2.pvp.events.FinalAttackEvent; +import io.github.togar2.pvp.feature.CombatFeatures; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.event.player.PlayerBlockBreakEvent; +import net.minestom.server.event.player.PlayerBlockPlaceEvent; +import net.minestom.server.event.player.PlayerMoveEvent; +import net.minestom.server.instance.batch.AbsoluteBlockBatch; +import net.minestom.server.instance.block.Block; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class BlockBattle extends StatelessGame { + private final Team teamBlue = new Team(new Pos(0,101,20).add(0.5).withView(180, 0), Team.Color.BLUE); + private final Team teamRed = new Team(new Pos(0, 101, -20).add(0.5), Team.Color.RED); + private final int itemCount; + + private final Map teams = new WeakHashMap<>(); + + public BlockBattle(int itemCount) { + super(Dimension.THE_END.key, "Block Battle", new FirstWinsScore()); + + this.itemCount = itemCount; + + this.eventNode().addChild( + CombatFeatures.empty() + .add(CombatFeatures.VANILLA_ATTACK) + .add(CombatFeatures.VANILLA_DAMAGE) + .add(CombatFeatures.VANILLA_KNOCKBACK) + .build().createNode() + ); + + this.eventNode().addListener(FinalAttackEvent.class, finalAttackEvent -> { + if(!this.isRunning) finalAttackEvent.setCancelled(true); + finalAttackEvent.setBaseDamage(0); + ((Player) finalAttackEvent.getTarget()).setHealth(20); + }); + } + + @Override + protected void onLoad(@NotNull CompletableFuture callback) { + this.generatePlatform(new Pos(0, 100, 0), Block.GOLD_BLOCK, Block.YELLOW_CONCRETE_POWDER); + this.generatePlatform(new Pos(0, 101, 0), Block.AIR, Block.SANDSTONE_SLAB); + this.generatePlatform(new Pos(0, 100, 20), Block.BLUE_CONCRETE, Block.BLUE_CONCRETE_POWDER); + this.generatePlatform(new Pos(0, 100, -20), Block.RED_CONCRETE, Block.RED_CONCRETE_POWDER); + this.generatePlatform(new Pos(-5, 101, -14), Block.RED_STAINED_GLASS, Block.AIR); + this.generatePlatform(new Pos(5, 101, 14), Block.BLUE_STAINED_GLASS, Block.AIR); + + AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); + + Pos[] positionsRedGlass = { + new Pos(2, 102, -9), + new Pos(-1, 103, -9), + new Pos(-2, 104, -6), + new Pos(-5, 103, -7), + new Pos(-7, 102, -10), + new Pos(3, 102, -12), + new Pos(5, 101, -15) + }; + + Pos[] positionsBlueGlass = { + new Pos(-5, 101, 15), + new Pos(-3, 102, 12), + new Pos(-2, 102, 9), + new Pos(1, 103, 9), + new Pos(2, 104, 6), + new Pos(5, 103, 7), + new Pos(7, 102, 10) + }; + + for(Pos pos : positionsRedGlass) + batch.setBlock(pos, Block.RED_STAINED_GLASS); + + for(Pos pos : positionsBlueGlass) + batch.setBlock(pos, Block.BLUE_STAINED_GLASS); + + BatchUtil.loadAndApplyBatch(batch, this, () -> {}); + } + + @Override + protected void onBlockPlace(@NotNull PlayerBlockPlaceEvent playerBlockPlaceEvent) { + Pos posGoldBlock = new Pos(playerBlockPlaceEvent.getBlockPosition()).sub(0, 1, 0); + Block goldBlock = playerBlockPlaceEvent.getInstance().getBlock(posGoldBlock); + + playerBlockPlaceEvent.setCancelled(goldBlock != Block.GOLD_BLOCK); + + playerBlockPlaceEvent.getInstance().scheduler().scheduleNextTick(() -> { + Pos middle = new Pos(0, 101, 0); + boolean validBlue = true; + boolean validRed = true; + + for(int x = middle.blockX()-1; x < middle.blockX()+2; x++) { + for(int z = middle.blockZ()-1; z < middle.blockZ()+2; z++) { + if(playerBlockPlaceEvent.getInstance().getBlock(x, 101, z) != Block.BLUE_WOOL) { + validBlue = false; + break; + } + } + if(!validBlue) + break; + } + + for(int x = middle.blockX()-1; x < middle.blockX()+2; x++) { + for(int z = middle.blockZ()-1; z < middle.blockZ()+2; z++) { + if(playerBlockPlaceEvent.getInstance().getBlock(x, 101, z) != Block.RED_WOOL) { + validRed = false; + break; + } + } + if(!validRed) + break; + } + + if(!validBlue && !validRed) return; + var winningTeam = validBlue ? Team.Color.BLUE : Team.Color.RED; + var winningPlayers = this.teams.entrySet().stream() + .filter(entry -> entry.getValue().color().equals(winningTeam)) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + + this.getScore().insertMultiple(winningPlayers); + super.stop(); + }); + } + + @Override + protected void onBlockBreak(@NotNull PlayerBlockBreakEvent playerBlockBreakEvent) { + boolean isAllowed = Arrays.stream(Team.Color.values()) + .map(Team.Color::getMaterial) + .map(Material::block) + .toList() + .contains(playerBlockBreakEvent.getBlock()); + if(!isAllowed) playerBlockBreakEvent.setCancelled(true); + } + + @Override + protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { + if(playerMoveEvent.getNewPosition().y() < 95) { + var player = playerMoveEvent.getPlayer(); + + player.teleport( + this.isBeforeBeginning + ? this.getSpawn() + : this.teams.get(player).spawnPosition() + ); + + this.giveItems(player); + } + } + + @Override + protected void onStart() { + this.setTeams(); + this.getPlayers().forEach(player -> player.teleport(this.teams.get(player).spawnPosition()).thenRun(() -> { + this.giveItems(player); + player.setGameMode(GameMode.SURVIVAL); + })); + } + + private void generatePlatform(Pos center, Block inner, Block outer) { + for(int x = center.blockX()-2; x < center.blockX()+3; x++) { + for(int z = center.blockZ()-2; z < center.blockZ()+3; z++) { + this.setBlock(x, center.blockY(), z, outer); + } + } + + for(int x = center.blockX()-1; x < center.blockX()+2; x++) { + for(int z = center.blockZ()-1; z < center.blockZ()+2; z++) { + this.setBlock(x, center.blockY(), z, inner); + } + } + } + + private void setTeams() { + List players = new ArrayList<>(this.getPlayers()); + Collections.shuffle(players); + int halfPlayers = players.size()/2; + players.subList(0, halfPlayers).forEach(player -> this.teams.put(player, this.teamBlue)); + players.subList(halfPlayers, players.size()).forEach(player -> this.teams.put(player, this.teamRed)); + } + + @Override + public Pos getSpawn() { + return new Pos(0, 101, 0).add(0.5); + } + + private void giveItems(Player player) { + player.getInventory().clear(); + ItemStack item = ItemStack.of( + this.teams.get(player).color().getMaterial(), + this.itemCount + ); + player.getInventory().addItemStack(item); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBattle/BlockBattleFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBattle/BlockBattleFactory.java new file mode 100644 index 0000000..8884337 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBattle/BlockBattleFactory.java @@ -0,0 +1,40 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBattle; + +import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption; +import eu.mhsl.minenet.minigames.instance.room.Room; +import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import net.minestom.server.item.Material; + +import java.util.Map; + +public class BlockBattleFactory implements GameFactory { + @Override + public TranslatedComponent name() { + return TranslatedComponent.byId("game_BlockBattle#name"); + } + + @Override + public TranslatedComponent description() { + return TranslatedComponent.byId("game_BlockBattle#description"); + } + + @Override + public ConfigManager configuration() { + return new ConfigManager() + .addOption(new NumericOption("itemCount", Material.WHITE_WOOL, TranslatedComponent.byId("game_BlockBattle#itemCount"), 1, 2, 3)); + } + + @Override + public Material symbol() { + return Material.GREEN_CONCRETE; + } + + @Override + public Game manufacture(Room parent, Map> configuration) throws Exception { + return new BlockBattle(configuration.get("itemCount").getAsInt()).setParent(parent); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBattle/Team.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBattle/Team.java new file mode 100644 index 0000000..fda428a --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBattle/Team.java @@ -0,0 +1,21 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBattle; + +import net.minestom.server.coordinate.Pos; +import net.minestom.server.item.Material; + +public record Team(Pos spawnPosition, Color color) { + public enum Color { + RED(Material.RED_WOOL), + BLUE(Material.BLUE_WOOL); + + private final Material block; + + Color(Material block) { + this.block = block; + } + + public Material getMaterial() { + return this.block; + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBreakRace/BlockBreakRace.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBreakRace/BlockBreakRace.java new file mode 100644 index 0000000..f23b408 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBreakRace/BlockBreakRace.java @@ -0,0 +1,79 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBreakRace; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; +import eu.mhsl.minenet.minigames.score.FirstWinsScore; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.event.player.PlayerBlockBreakEvent; +import net.minestom.server.event.player.PlayerMoveEvent; +import net.minestom.server.inventory.PlayerInventory; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class BlockBreakRace extends StatelessGame { + private final int height; + private int spawnCount = 0; + + public BlockBreakRace(int height) { + super(Dimension.OVERWORLD.key, "blockBreakRace", new FirstWinsScore()); + this.height = height; + this.setGenerator(new BlockBreakRaceGenerator(height)); + } + + @Override + protected boolean onPlayerJoin(Player p) { + PlayerInventory inv = p.getInventory(); + inv.addItemStack(ItemStack.of(Material.DIAMOND_PICKAXE)); + inv.addItemStack(ItemStack.of(Material.DIAMOND_AXE)); + inv.addItemStack(ItemStack.of(Material.DIAMOND_SHOVEL)); + return super.onPlayerJoin(p); + } + + @Override + protected void onStart() { + this.getPlayers().forEach(player -> player.setGameMode(GameMode.SURVIVAL)); + } + + @Override + protected void onBlockBreak(@NotNull PlayerBlockBreakEvent event) { + List allowedMaterials = List.of(Material.STONE, Material.OAK_PLANKS, Material.DIRT); + if(!allowedMaterials.contains(event.getBlock().registry().material())) event.setCancelled(true); + if(this.isBeforeBeginning) event.setCancelled(true); + } + + @Override + protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { + if(this.isBeforeBeginning) return; + if(playerMoveEvent.getNewPosition().y() < BlockBreakRaceGenerator.BOTTOM_Y) { + Player player = playerMoveEvent.getPlayer(); + this.getScore().insertResult(player); + player.setGameMode(GameMode.SPECTATOR); + player.getInventory().clear(); + } + } + + @Override + public Pos getSpawn() { + int idx = this.spawnCount++; + + int cols = BlockBreakRaceGenerator.ROW_OFFSETS_X.length; + int rows = BlockBreakRaceGenerator.ROW_OFFSETS_Z.length; + int perChunk = cols * rows; + int zChunk = idx / perChunk; + + int gridIndex = idx % perChunk; + int xIndex = gridIndex % cols; + int zIndex = gridIndex / cols; + + int localX = BlockBreakRaceGenerator.ROW_OFFSETS_X[xIndex]; + int localZ = BlockBreakRaceGenerator.ROW_OFFSETS_Z[zIndex]; + + int absZ = (zChunk * 16) + localZ; + return new Pos(localX, BlockBreakRaceGenerator.BOTTOM_Y + this.height + 1, absZ).add(0.5); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBreakRace/BlockBreakRaceFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBreakRace/BlockBreakRaceFactory.java new file mode 100644 index 0000000..7ff5f1f --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBreakRace/BlockBreakRaceFactory.java @@ -0,0 +1,40 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBreakRace; + +import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption; +import eu.mhsl.minenet.minigames.instance.room.Room; +import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import net.minestom.server.item.Material; + +import java.util.Map; + +public class BlockBreakRaceFactory implements GameFactory { + @Override + public TranslatedComponent name() { + return TranslatedComponent.byId("game_BlockBreakRace#name"); + } + + @Override + public Material symbol() { + return Material.DIAMOND_PICKAXE; + } + + @Override + public TranslatedComponent description() { + return TranslatedComponent.byId("game_BlockBreakRace#description"); + } + + @Override + public ConfigManager configuration() { + return new ConfigManager() + .addOption(new NumericOption("height", Material.SCAFFOLDING, TranslatedComponent.byId("optionCommon#height"), 20, 30, 40, 50)); + } + + @Override + public Game manufacture(Room parent, Map> configuration) throws Exception { + return new BlockBreakRace(configuration.get("height").getAsInt()).setParent(parent); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBreakRace/BlockBreakRaceGenerator.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBreakRace/BlockBreakRaceGenerator.java new file mode 100644 index 0000000..06e77d8 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/blockBreakRace/BlockBreakRaceGenerator.java @@ -0,0 +1,65 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBreakRace; + +import eu.mhsl.minenet.minigames.world.generator.featureEnriched.ValeGenerator; +import eu.mhsl.minenet.minigames.world.generator.terrain.BaseGenerator; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +public class BlockBreakRaceGenerator extends BaseGenerator { + public static final int BOTTOM_Y = 50; + public static final int[] ROW_OFFSETS_X = {4, 8, 12}; + public static final int[] ROW_OFFSETS_Z = {4, 8, 12}; + private static final Block[] FILL_BLOCKS = { + Block.STONE, + Block.DIRT, + Block.OAK_PLANKS + }; + public final int TOP_Y; + private final Random random = ThreadLocalRandom.current(); + + public BlockBreakRaceGenerator(int height) { + this.TOP_Y = BOTTOM_Y + height; + ValeGenerator vale = new ValeGenerator(); + vale.setXShiftMultiplier(i -> 0.5d); + vale.setHeightNoiseMultiplier(i -> 2); + vale.setXShiftOffset(i -> 40d); + this.addMixIn(vale); + + this.addMixIn(unit -> { + if(unit.absoluteStart().chunkX() != 0) return; + + for(int localX : ROW_OFFSETS_X) { + for(int localZ : ROW_OFFSETS_Z) { + final int absZ = unit.absoluteStart().blockZ() + localZ; + this.placeTube(unit, localX, absZ); + } + } + }); + } + + private void placeTube(GenerationUnit unit, int x, int z) { + for(int y = BOTTOM_Y; y < this.TOP_Y; y++) { + Block fill = FILL_BLOCKS[this.random.nextInt(FILL_BLOCKS.length)]; + unit.modifier().fill( + new Pos(x, y, z), + new Pos(x, y, z).add(1), + fill + ); + } + + for(int dx = -1; dx <= 1; dx++) { + for(int dz = -1; dz <= 1; dz++) { + if(dx == 0 && dz == 0) continue; // Zentrum überspringen + unit.modifier().fill( + new Pos(x + dx, BOTTOM_Y, z + dz), + new Pos(x + dx, this.TOP_Y + 3, z + dz).add(1), + Block.BARRIER + ); + } + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/boatRace/BoatRace.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/boatRace/BoatRace.java new file mode 100644 index 0000000..2ace4bc --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/boatRace/BoatRace.java @@ -0,0 +1,76 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.boatRace; + +import eu.mhsl.minenet.minigames.Resource; +import eu.mhsl.minenet.minigames.handler.global.PlayerLoginHandler; +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; +import eu.mhsl.minenet.minigames.score.FirstWinsScore; +import eu.mhsl.minenet.minigames.util.CommonProperties; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.event.player.PlayerMoveEvent; +import net.minestom.server.instance.anvil.AnvilLoader; +import net.minestom.server.instance.block.Block; +import net.minestom.server.world.DimensionType; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + + +public class BoatRace extends StatelessGame { + public BoatRace() { + super(Dimension.OVERWORLD.key, "boatRace", new FirstWinsScore()); + this.setChunkLoader(new AnvilLoader(Resource.GAME_MAP.getPath().resolve("boatRace/woodlandMansion"), DimensionType.OVERWORLD.key())); + } + + @Override + protected boolean onPlayerJoin(Player p) { + Entity boat = new Entity(EntityType.OAK_BOAT); + boat.setInstance(this, this.getSpawn()); + boat.setSynchronizationTicks(100000); + PlayerLoginHandler.globalTeam.addMember(boat.getUuid().toString()); + MinecraftServer.getSchedulerManager().scheduleNextTick(() -> boat.addPassenger(p)); + return super.onPlayerJoin(p); + } + + @Override + protected void onLoad(@NotNull CompletableFuture callback) { + for(int z = 9; z <= 32; z++) { + this.setBlock(31, 235, z, Block.CHERRY_FENCE.withProperties(CommonProperties.fenceNorthSouth)); + } + } + + @Override + protected void onStart() { + for(int z = 9; z <= 32; z++) { + this.setBlock(31, 235, z, Block.AIR); + } + } + + @Override + protected void onPlayerMove(@NotNull PlayerMoveEvent event) { + if (this.isBeforeBeginning) return; + + if(event.getNewPosition().z() > 469) { + Player player = event.getPlayer(); + this.getScore().insertResult(player); + + Entity vehicle = player.getVehicle(); + if(vehicle != null) { + vehicle.removePassenger(player); + player.teleport(player.getPosition().withY(y -> y + 3)); + player.setGameMode(GameMode.SPECTATOR); + vehicle.remove(); + } + } + } + + @Override + public Pos getSpawn() { + return new Pos(20, 236, 20, -90, 0); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/boatRace/BoatRaceFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/boatRace/BoatRaceFactory.java new file mode 100644 index 0000000..5a126fa --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/boatRace/BoatRaceFactory.java @@ -0,0 +1,27 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.boatRace; + +import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.room.Room; +import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import net.minestom.server.item.Material; + +import java.util.Map; + +public class BoatRaceFactory implements GameFactory { + @Override + public TranslatedComponent name() { + return TranslatedComponent.byId("game_BoatRace#name"); + } + + @Override + public Material symbol() { + return Material.OAK_BOAT; + } + + @Override + public Game manufacture(Room parent, Map> configuration) throws Exception { + return new BoatRace().setParent(parent); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bowSpleef/BowSpleef.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bowSpleef/BowSpleef.java index bbb7d3b..f17f8cf 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bowSpleef/BowSpleef.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bowSpleef/BowSpleef.java @@ -9,7 +9,9 @@ import eu.mhsl.minenet.minigames.util.GeneratorUtils; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; -import net.minestom.server.entity.*; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.GameMode; import net.minestom.server.entity.metadata.other.PrimedTntMeta; import net.minestom.server.event.EventListener; import net.minestom.server.event.entity.projectile.ProjectileCollideWithBlockEvent; @@ -29,6 +31,7 @@ public class BowSpleef extends StatelessGame { private static final Tag ARROW_FIRST_HIT = Tag.Boolean("ARROW_ALREADY_LIT"); private final int radius = 30; private final int totalElevation = 50; + public BowSpleef() { super(Dimension.OVERWORLD.key, "bowSpleef", new LastWinsScore()); @@ -67,41 +70,41 @@ public class BowSpleef extends StatelessGame { // .build() // ); - eventNode().addListener( - EventListener - .builder(ProjectileCollideWithBlockEvent.class) - .handler(projectileBlockHitEvent -> { - Entity projectile = projectileBlockHitEvent.getEntity(); - if(!projectile.getTag(ARROW_FIRST_HIT)) { - projectile.remove(); - return; - } + this.eventNode().addListener( + EventListener + .builder(ProjectileCollideWithBlockEvent.class) + .handler(projectileBlockHitEvent -> { + Entity projectile = projectileBlockHitEvent.getEntity(); + if(!projectile.getTag(ARROW_FIRST_HIT)) { + projectile.remove(); + return; + } - float radius = 0.5F; - Point arrowPos = projectile.getPosition(); - GeneratorUtils.foreachXZ(arrowPos.add(radius), arrowPos.sub(radius), point -> { - Point location = point.add(projectile.getVelocity().mul(0.04)).withY(totalElevation); - if(!getBlock(location).isAir()) { - setBlock(location, Block.AIR); + float radius = 0.5F; + Point arrowPos = projectile.getPosition(); + GeneratorUtils.foreachXZ(arrowPos.add(radius), arrowPos.sub(radius), point -> { + Point location = point.add(projectile.getVelocity().mul(0.04)).withY(this.totalElevation); + if(!this.getBlock(location).isAir()) { + this.setBlock(location, Block.AIR); - Entity fallingTnt = new Entity(EntityType.TNT); - PrimedTntMeta fallingTntMeta = (PrimedTntMeta) fallingTnt.getEntityMeta(); - fallingTntMeta.setFuseTime(20); - fallingTnt.setInstance(this, location); - fallingTnt.setVelocity(new Vec(0, 3, 0)); - } - }); + Entity fallingTnt = new Entity(EntityType.TNT); + PrimedTntMeta fallingTntMeta = (PrimedTntMeta) fallingTnt.getEntityMeta(); + fallingTntMeta.setFuseTime(20); + fallingTnt.setInstance(this, location); + fallingTnt.setVelocity(new Vec(0, 3, 0)); + } + }); - projectile.setTag(ARROW_FIRST_HIT, false); - }) - .build() + projectile.setTag(ARROW_FIRST_HIT, false); + }) + .build() ); - eventNode().addListener( - EventListener - .builder(ProjectileCollideWithEntityEvent.class) - .handler(projectileEntityHitEvent -> projectileEntityHitEvent.setCancelled(true)) - .build() + this.eventNode().addListener( + EventListener + .builder(ProjectileCollideWithEntityEvent.class) + .handler(projectileEntityHitEvent -> projectileEntityHitEvent.setCancelled(true)) + .build() ); } @@ -109,11 +112,11 @@ public class BowSpleef extends StatelessGame { @Override protected void onLoad(@NotNull CompletableFuture callback) { AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); - for(int x = -radius; x <= radius; x++) { - for(int z = -radius; z <= radius; z++) { - if(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > radius) continue; + for(int x = -this.radius; x <= this.radius; x++) { + for(int z = -this.radius; z <= this.radius; z++) { + if(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > this.radius) continue; - batch.setBlock(x, totalElevation, z, Block.TNT); + batch.setBlock(x, this.totalElevation, z, Block.TNT); } } BatchUtil.loadAndApplyBatch(batch, this, () -> callback.complete(null)); @@ -121,28 +124,28 @@ public class BowSpleef extends StatelessGame { @Override protected void onStart() { - getPlayers().forEach(player -> { + this.getPlayers().forEach(player -> { player.getInventory().setItemStack( - 0, - ItemStack - .builder(Material.BOW) - .customName(TranslatedComponent.byId("bow").getAssembled(player)) - .glowing(true) - .build() + 0, + ItemStack + .builder(Material.BOW) + .customName(TranslatedComponent.byId("bow").getAssembled(player)) + .glowing(true) + .build() ); }); } @Override protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { - if(playerMoveEvent.getNewPosition().y() < totalElevation) { - getScore().insertResult(playerMoveEvent.getPlayer()); + if(playerMoveEvent.getNewPosition().y() < this.totalElevation) { + this.getScore().insertResult(playerMoveEvent.getPlayer()); playerMoveEvent.getPlayer().setGameMode(GameMode.SPECTATOR); } } @Override public Pos getSpawn() { - return new Pos(0, totalElevation + 1, 0); + return new Pos(0, this.totalElevation + 1, 0); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bowSpleef/BowSpleefFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bowSpleef/BowSpleefFactory.java index 0466661..97d2acb 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bowSpleef/BowSpleefFactory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/bowSpleef/BowSpleefFactory.java @@ -3,6 +3,8 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.bowSpleef; import eu.mhsl.minenet.minigames.instance.game.Game; import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.RestrictionHandler; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.common.MinimalPlayeramountGameRestriction; import eu.mhsl.minenet.minigames.instance.room.Room; import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; import net.minestom.server.item.Material; @@ -29,4 +31,10 @@ public class BowSpleefFactory implements GameFactory { public Game manufacture(Room parent, Map> configuration) throws Exception { return new BowSpleef().setParent(parent); } + + @Override + public RestrictionHandler globalRestrictions() { + return new RestrictionHandler() + .addRestriction(new MinimalPlayeramountGameRestriction(2)); + } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattle.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattle.java new file mode 100644 index 0000000..ccb2a25 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattle.java @@ -0,0 +1,226 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.buildBattle; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; +import eu.mhsl.minenet.minigames.score.PointsWinScore; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.title.Title; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.event.player.PlayerBlockBreakEvent; +import net.minestom.server.event.player.PlayerBlockInteractEvent; +import net.minestom.server.event.player.PlayerBlockPlaceEvent; +import net.minestom.server.event.player.PlayerUseItemEvent; +import net.minestom.server.event.player.PlayerUseItemOnBlockEvent; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.timer.TaskSchedule; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.stream.IntStream; + + +public class BuildBattle extends StatelessGame { + private static final int height = 100; + private static final int plotHalf = BuildBattleChunkgenerator.inner / 2; + private static final int rowCount = 5; + private static final int ratingSeconds = 15; + private static final List ratingMaterials = List.of( + Material.RED_CONCRETE, Material.ORANGE_CONCRETE, Material.YELLOW_CONCRETE, + Material.LIME_CONCRETE, Material.GREEN_CONCRETE + ); + private static final List ratingItems = IntStream.range(0, ratingMaterials.size()) + .mapToObj(i -> ItemStack.builder(ratingMaterials.get(i)) + .customName(Component.text((i + 1) + " Punkte")) + .build()) + .toList(); + private static final List themes = List.of( + "Burg", "Raumschiff", "Unterwasserwelt", "Pizzeria", "Spukhaus", + "Bauernhof", "Dschungel", "Zukunftsstadt", "Piratenschiff", "Vulkan", + "Freizeitpark", "Leuchtturm", "Wikingerdorf", "Weihnachtsmarkt", + "Schloss", "Strand", "Berghütte", "Bahnhof", "Zoo", "Schule", + "Zeitreise", "Ein Haus, das auf dem Kopf steht", "Eine Stadt auf dem Rücken eines Riesen", + "Wohnen in einer Schneekugel", "Alles ist aus Süßigkeiten", "Ein Schiff in der Luft", + "Das Museum der Zukunft", "Das letzte Haus vor dem Weltuntergang", + "Wenn Schweine fliegen könnten", "Das Gefängnis für Creeper", "Die Wohnung von Herobrine", + "Ein Dönerladen im Weltall", "Ein Spa für Enderman", "Der Käse-Palast", + "Mein Zimmer nach 3 Wochen ohne Aufräumen", "Die Hüpfburg des Todes" + ); + + private int spawnPosx = 0; + private int spawnPosz = 0; + private final Map spawnPositions = new WeakHashMap<>(); + private final int seconds; + private final Map currentVotes = new HashMap<>(); + private Player currentOwner = null; + + public BuildBattle(int seconds) { + super(Dimension.THE_END.key, "BuildBattle", new PointsWinScore()); + this.seconds = seconds; + this.setGenerator(new BuildBattleChunkgenerator(height)); + BuildBattleBlockRules.register(); + this.eventNode().addListener(PlayerUseItemEvent.class, this::onRatingItemUse); + this.eventNode().addListener(PlayerUseItemOnBlockEvent.class, e -> FluidFlowHelper.onBucketUse(e, p -> this.isInArea(p, e.getPlayer()))); + this.eventNode().addListener(PlayerBlockInteractEvent.class, e -> { + if (this.isInArea(e.getBlockPosition(), e.getPlayer())) BuildBattleBlockRules.onInteract(e); + }); + } + + @Override + protected void onStart() { + String theme = themes.get(this.rnd.nextInt(themes.size())); + + this.showTitle(Title.title( + Component.text("Thema", NamedTextColor.GOLD), + Component.text(theme, NamedTextColor.YELLOW) + )); + this.sendMessage(Component.text("Euer Thema: ", NamedTextColor.GRAY) + .append(Component.text(theme, NamedTextColor.YELLOW))); + this.getPlayers().forEach(player -> player.setGameMode(GameMode.CREATIVE)); + + this.scheduler().scheduleTask(this::endBuildPhase, TaskSchedule.seconds(this.seconds), TaskSchedule.stop()); + this.scheduleCountdown(); + } + + private void scheduleCountdown() { + int[] marks = {30, 10, 5, 4, 3, 2, 1}; + for (int m : marks) { + if (this.seconds <= m) continue; + int remaining = m; + this.scheduler().scheduleTask(() -> { + NamedTextColor color = remaining <= 5 ? NamedTextColor.RED : (remaining <= 10 ? NamedTextColor.GOLD : NamedTextColor.YELLOW); + this.sendMessage(Component.text("Noch ", NamedTextColor.GRAY) + .append(Component.text(remaining, color)) + .append(Component.text(" Sekunde" + (remaining == 1 ? "" : "n") + " Bauzeit!", NamedTextColor.GRAY))); + }, TaskSchedule.seconds(this.seconds - m), TaskSchedule.stop()); + } + } + + private void endBuildPhase() { + this.sendMessage(Component.text("Die Bauzeit ist beendet!")); + this.getPlayers().forEach(p -> { + p.setGameMode(GameMode.ADVENTURE); + p.setAllowFlying(true); + p.setFlying(true); + }); + this.scheduler().scheduleTask(this::rating, TaskSchedule.seconds(3), TaskSchedule.stop()); + } + + @Override + protected boolean onPlayerJoin(Player player) { + Pos plotCenter = new Pos( + this.spawnPosx * BuildBattleChunkgenerator.period + plotHalf, + height, + this.spawnPosz * BuildBattleChunkgenerator.period + plotHalf + ); + this.spawnPositions.put(player, plotCenter); + + if(this.spawnPosx >= rowCount - 1) { + this.spawnPosx = 0; + this.spawnPosz++; + } else this.spawnPosx ++; + + player.setGameMode(GameMode.ADVENTURE); + this.scheduler().scheduleNextTick(() -> player.teleport(plotCenter.add(0, 1.5, 0))); + return super.onPlayerJoin(player); + } + + @Override + protected void onBlockBreak(@NotNull PlayerBlockBreakEvent event) { + if (!this.isInArea(event.getBlockPosition(), event.getPlayer())) { + event.setCancelled(true); + return; + } + FluidFlowHelper.onBreak(event); + } + + @Override + protected void onBlockPlace(@NotNull PlayerBlockPlaceEvent event) { + if (!this.isInArea(event.getBlockPosition(), event.getPlayer())) { + event.setCancelled(true); + return; + } + FluidFlowHelper.onPlace(event); + } + + private Pos safeSpawn(Player owner) { + Pos center = this.spawnPositions.get(owner); + int topY = center.blockY(); + for (int y = 300; y >= center.blockY(); y--) { + if (!this.getBlock(center.blockX(), y, center.blockZ()).isAir()) { + topY = y; + break; + } + } + return new Pos(center.x(), topY + 3, center.z()).withView(center.yaw(), center.pitch()); + } + + private void rating() { + Queue owners = new ArrayDeque<>(this.getPlayers()); + + this.scheduler().submitTask(() -> { + if (this.currentOwner != null) this.finishRound(); + + Player next = owners.poll(); + while (next != null && !(next.isOnline() && this.spawnPositions.containsKey(next))) next = owners.poll(); + if (next == null) return TaskSchedule.stop(); + + this.startRound(next); + return TaskSchedule.seconds(ratingSeconds); + }); + } + + private void startRound(Player owner) { + this.currentOwner = owner; + this.currentVotes.clear(); + + Pos plot = this.safeSpawn(owner); + this.sendMessage(Component.text("Bewerte das Grundstück von " + owner.getUsername() + "!")); + this.getPlayers().forEach(p -> { + p.teleport(plot); + p.getInventory().clear(); + if (p != owner) this.giveRatingItems(p); + }); + } + + private void giveRatingItems(Player player) { + for (int i = 0; i < ratingItems.size(); i++) { + player.getInventory().setItemStack(i, ratingItems.get(i)); + } + } + + private void onRatingItemUse(PlayerUseItemEvent event) { + Player voter = event.getPlayer(); + if (this.currentOwner == null || voter == this.currentOwner) return; + + int slot = voter.getHeldSlot(); + if (slot >= ratingItems.size()) return; + + event.setCancelled(true); + this.currentVotes.put(voter, slot + 1); + voter.sendMessage(Component.text("Du hast " + (slot + 1) + " Punkte vergeben.")); + } + + private void finishRound() { + int voteSum = this.currentVotes.values().stream() + .mapToInt(Integer::intValue) + .sum(); + int score = (int) Math.round((double) voteSum / this.getPlayers().size()); + + this.getPlayers().forEach(p -> p.getInventory().clear()); + this.getScore().insertResult(this.currentOwner, score); + this.currentOwner = null; + } + + private boolean isInArea(Point block, Player player) { + Pos center = this.spawnPositions.get(player); + if (center == null) return false; + Pos min = new Pos(center.x() - plotHalf, center.y() + 1, center.z() - plotHalf); + Pos max = new Pos(center.x() + plotHalf - 1, 300, center.z() + plotHalf - 1); + return eu.mhsl.minenet.minigames.util.Position.isInArea(block, min, max); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattleBlockRules.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattleBlockRules.java new file mode 100644 index 0000000..5ab090c --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattleBlockRules.java @@ -0,0 +1,85 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.buildBattle; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.event.player.PlayerBlockInteractEvent; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.block.BlockHandler; +import net.minestom.server.instance.block.rule.BlockPlacementRule; +import org.everbuild.blocksandstuff.blocks.group.VanillaBlockBehaviour; +import org.everbuild.blocksandstuff.blocks.group.VanillaPlacementRules; +import org.everbuild.blocksandstuff.blocks.group.behaviour.BehaviourGroup; +import org.everbuild.blocksandstuff.blocks.group.placement.PlacementGroup; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public final class BuildBattleBlockRules { + private BuildBattleBlockRules() {} + + private static final ThreadLocal interactingInBuildBattle = ThreadLocal.withInitial(() -> false); + private static final Map behaviours = new HashMap<>(); + private static boolean registered = false; + + public static synchronized void register() { + if (registered) return; + registered = true; + + var blockManager = MinecraftServer.getBlockManager(); + for (PlacementGroup group : VanillaPlacementRules.INSTANCE.getALL()) { + for (Block block : group.getBlockGroup().allMatching()) { + blockManager.registerBlockPlacementRule(new BuildBattleOnlyRule(group.createRule(block))); + } + } + + for (BehaviourGroup group : VanillaBlockBehaviour.INSTANCE.getALL()) { + for (Block block : group.getBlockGroup().allMatching()) { + behaviours.put(block.id(), group.createHandler(block)); + } + } + + MinecraftServer.getGlobalEventHandler().addListener(PlayerBlockInteractEvent.class, + e -> interactingInBuildBattle.set(e.getInstance() instanceof BuildBattle)); + } + + public static void onInteract(PlayerBlockInteractEvent event) { + BlockHandler handler = behaviours.get(event.getBlock().id()); + if (handler == null) return; + boolean continueItemUse = handler.onInteract(new BlockHandler.Interaction( + event.getBlock(), event.getInstance(), event.getBlockFace(), event.getBlockPosition(), + event.getCursorPosition(), event.getPlayer(), event.getHand() + )); + if (!continueItemUse) event.setBlockingItemUse(true); + } + + private static final class BuildBattleOnlyRule extends BlockPlacementRule { + private final BlockPlacementRule delegate; + + private BuildBattleOnlyRule(BlockPlacementRule delegate) { + super(delegate.getBlock()); + this.delegate = delegate; + } + + @Override + public @Nullable Block blockPlace(PlacementState placementState) { + if (!(placementState.instance() instanceof BuildBattle)) return placementState.block(); + return this.delegate.blockPlace(placementState); + } + + @Override + public Block blockUpdate(UpdateState updateState) { + if (!(updateState.instance() instanceof BuildBattle)) return updateState.currentBlock(); + return this.delegate.blockUpdate(updateState); + } + + @Override + public boolean isSelfReplaceable(Replacement replacement) { + return interactingInBuildBattle.get() && this.delegate.isSelfReplaceable(replacement); + } + + @Override + public int maxUpdateDistance() { + return this.delegate.maxUpdateDistance(); + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattleChunkgenerator.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattleChunkgenerator.java new file mode 100644 index 0000000..af4b744 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattleChunkgenerator.java @@ -0,0 +1,41 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.buildBattle; + +import eu.mhsl.minenet.minigames.util.GeneratorUtils; +import eu.mhsl.minenet.minigames.world.generator.terrain.BaseGenerator; +import net.minestom.server.instance.block.Block; + +public class BuildBattleChunkgenerator extends BaseGenerator { + static final int inner = 32; + private static final int ring = 3; + private static final int wall = 1; + private static final int wallHeight = 200; + + static final int period = wall + ring + inner + ring; + + private static final int innerStart = wall + ring; + private static final int innerEnd = innerStart + inner; + + private static final Block middleBlocks = Block.QUARTZ_BLOCK; + private static final Block outerBlocks = Block.STONE_BRICKS; + private static final Block wallBlocks = Block.POLISHED_DEEPSLATE; + + public BuildBattleChunkgenerator(int height) { + this.addMixIn(unit -> GeneratorUtils.foreachXZ(unit, point -> { + int lx = Math.floorMod(point.blockX() + innerStart, period); + int lz = Math.floorMod(point.blockZ() + innerStart, period); + + if (lx < wall || lz < wall) { + unit.modifier().fill(point.withY(height), point.add(1, 0, 1).withY(height + wallHeight + 1), wallBlocks); + } else { + unit.modifier().setBlock(point.withY(height), floorBlock(lx, lz)); + } + })); + } + + private static Block floorBlock(int lx, int lz) { + boolean inInner = lx >= innerStart && lx < innerEnd + && lz >= innerStart && lz < innerEnd; + + return inInner ? middleBlocks : outerBlocks; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattleFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattleFactory.java new file mode 100644 index 0000000..cc89fe1 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/BuildBattleFactory.java @@ -0,0 +1,40 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.buildBattle; + +import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption; +import eu.mhsl.minenet.minigames.instance.room.Room; +import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import net.minestom.server.item.Material; + +import java.util.Map; + +public class BuildBattleFactory implements GameFactory { + @Override + public TranslatedComponent name() { + return TranslatedComponent.byId("game_BuildBattle#name"); + } + + @Override + public ConfigManager configuration() { + return new ConfigManager() + .addOption(new NumericOption("seconds", Material.CLOCK, TranslatedComponent.byId("game_BuildBattle#seconds"), 60, 120, 300, 600)); + } + + @Override + public Game manufacture(Room parent, Map> configuration) throws Exception { + return new BuildBattle(configuration.get("seconds").getAsInt()).setParent(parent); + } + + @Override + public Material symbol() { + return Material.SANDSTONE; + } + + @Override + public TranslatedComponent description() { + return TranslatedComponent.byId("game_BuildBattle#description"); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/FluidFlowHelper.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/FluidFlowHelper.java new file mode 100644 index 0000000..2e80a38 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/buildBattle/FluidFlowHelper.java @@ -0,0 +1,267 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.buildBattle; + +import net.minestom.server.coordinate.BlockVec; +import net.minestom.server.coordinate.Point; +import net.minestom.server.event.player.PlayerBlockBreakEvent; +import net.minestom.server.event.player.PlayerBlockPlaceEvent; +import net.minestom.server.event.player.PlayerUseItemOnBlockEvent; +import net.minestom.server.instance.Instance; +import net.minestom.server.instance.block.Block; +import net.minestom.server.item.Material; +import net.minestom.server.timer.TaskSchedule; +import net.minestom.server.utils.Direction; + +import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +public final class FluidFlowHelper { + private FluidFlowHelper() {} + + private static final int WATER_TICK = 5; + private static final int LAVA_TICK = 30; + private static final int WATER_RANGE = 7; + private static final int LAVA_RANGE = 3; + private static final int FALLING = 8; + + public static void onBucketUse(PlayerUseItemOnBlockEvent event, Predicate inArea) { + Material m = event.getItemStack().material(); + Instance inst = event.getPlayer().getInstance(); + if (inst == null) return; + Point hit = event.getPosition(); + var face = event.getBlockFace(); + BlockVec placePos = new BlockVec( + hit.blockX() + face.toDirection().normalX(), + hit.blockY() + face.toDirection().normalY(), + hit.blockZ() + face.toDirection().normalZ() + ); + if (!inArea.test(placePos)) return; + + if (m == Material.WATER_BUCKET) { + inst.setBlock(placePos, Block.WATER.withProperty("level", "0")); + scheduleFlow(inst, placePos, true); + } else if (m == Material.LAVA_BUCKET) { + inst.setBlock(placePos, Block.LAVA.withProperty("level", "0")); + scheduleFlow(inst, placePos, false); + } else if (m == Material.BUCKET) { + Block existing = inst.getBlock(placePos); + if (isWater(existing) || isLava(existing)) inst.setBlock(placePos, Block.AIR); + } + } + + public static void onPlace(PlayerBlockPlaceEvent event) { + Block b = event.getBlock(); + if (isWater(b) || isLava(b)) { + Instance inst = event.getInstance(); + BlockVec pos = event.getBlockPosition(); + scheduleFlow(inst, pos, isWater(b)); + } + triggerNeighbors(event.getInstance(), event.getBlockPosition()); + } + + public static void onBreak(PlayerBlockBreakEvent event) { + triggerNeighbors(event.getInstance(), event.getBlockPosition()); + } + + private static void triggerNeighbors(Instance inst, BlockVec pos) { + for (Direction d : Direction.values()) { + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY() + d.normalY(), pos.blockZ() + d.normalZ()); + Block nb = inst.getBlock(np); + if (isWater(nb) || isLava(nb)) scheduleFlow(inst, np, isWater(nb)); + } + } + + private static void scheduleFlow(Instance inst, BlockVec pos, boolean water) { + int delay = water ? WATER_TICK : LAVA_TICK; + inst.scheduler().buildTask(() -> step(inst, pos, water)).delay(TaskSchedule.tick(delay)).schedule(); + } + + private static void step(Instance inst, BlockVec pos, boolean water) { + Block self = inst.getBlock(pos); + if (water ? !isWater(self) : !isLava(self)) return; + + int level = parseLevel(self); + boolean source = level == 0; + boolean falling = level >= FALLING; + int flowLevel = source ? 1 : (falling ? 1 : level + 1); + int maxRange = water ? WATER_RANGE : LAVA_RANGE; + + if (!source && !falling) { + int newLevel = computeIncomingLevel(inst, pos, water, maxRange); + if (newLevel == -1) { + inst.setBlock(pos, Block.AIR); + triggerNeighbors(inst, pos); + return; + } + if (newLevel != level) { + inst.setBlock(pos, (water ? Block.WATER : Block.LAVA).withProperty("level", String.valueOf(newLevel))); + level = newLevel; + flowLevel = level + 1; + } + } + + if (water && !source && countAdjacentSources(inst, pos, true) >= 2 + && hasSolidBelow(inst, pos)) { + inst.setBlock(pos, Block.WATER.withProperty("level", "0")); + scheduleFlow(inst, pos, true); + triggerNeighbors(inst, pos); + return; + } + + BlockVec below = new BlockVec(pos.blockX(), pos.blockY() - 1, pos.blockZ()); + Block belowBlock = inst.getBlock(below); + if (isReplaceable(belowBlock) || (water ? isWater(belowBlock) : isLava(belowBlock))) { + if (handleFluidCollision(inst, below, water)) { + triggerNeighbors(inst, below); + return; + } + Block target = (water ? Block.WATER : Block.LAVA).withProperty("level", String.valueOf(FALLING)); + if (!belowBlock.compare(target) || !target.getProperty("level").equals(belowBlock.getProperty("level"))) { + if (!(isWater(belowBlock) && parseLevel(belowBlock) == 0) + && !(isLava(belowBlock) && parseLevel(belowBlock) == 0)) { + inst.setBlock(below, target); + scheduleFlow(inst, below, water); + triggerNeighbors(inst, below); + } + } + if (!hasSolidBelow(inst, pos) && !source) return; + } + + if (flowLevel > maxRange) return; + + Set targets = findFlowTargets(inst, pos, water, maxRange - (source ? 0 : level)); + for (Direction d : Direction.HORIZONTAL) { + if (!targets.contains(d)) continue; + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + Block nb = inst.getBlock(np); + if (handleFluidCollision(inst, np, water)) continue; + if (!isReplaceable(nb) && !(water ? isWater(nb) : isLava(nb))) continue; + int existingLevel = (water ? isWater(nb) : isLava(nb)) ? parseLevel(nb) : Integer.MAX_VALUE; + if (existingLevel == 0) continue; + if (flowLevel < existingLevel) { + inst.setBlock(np, (water ? Block.WATER : Block.LAVA).withProperty("level", String.valueOf(flowLevel))); + scheduleFlow(inst, np, water); + triggerNeighbors(inst, np); + } + } + } + + private static int computeIncomingLevel(Instance inst, BlockVec pos, boolean water, int maxRange) { + int min = Integer.MAX_VALUE; + BlockVec above = new BlockVec(pos.blockX(), pos.blockY() + 1, pos.blockZ()); + Block ab = inst.getBlock(above); + if ((water ? isWater(ab) : isLava(ab))) return FALLING; + for (Direction d : Direction.HORIZONTAL) { + BlockVec np = new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ()); + Block nb = inst.getBlock(np); + if (water ? isWater(nb) : isLava(nb)) { + int nl = parseLevel(nb); + if (nl >= FALLING) nl = 0; + if (nl < min) min = nl; + } + } + if (min == Integer.MAX_VALUE) return -1; + int result = min + 1; + if (result > maxRange) return -1; + return result; + } + + private static Set findFlowTargets(Instance inst, BlockVec pos, boolean water, int remaining) { + Set result = new HashSet<>(); + if (remaining <= 0) return result; + int bestDist = Integer.MAX_VALUE; + int[] dist = new int[4]; + Direction[] dirs = {Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}; + for (int i = 0; i < 4; i++) { + BlockVec np = new BlockVec(pos.blockX() + dirs[i].normalX(), pos.blockY(), pos.blockZ() + dirs[i].normalZ()); + Block nb = inst.getBlock(np); + if (!isReplaceable(nb) && !(water ? isWater(nb) : isLava(nb))) { dist[i] = Integer.MAX_VALUE; continue; } + dist[i] = bfsDownhill(inst, np, water, remaining); + if (dist[i] < bestDist) bestDist = dist[i]; + } + if (bestDist == Integer.MAX_VALUE) { + for (int i = 0; i < 4; i++) { + if (dist[i] < Integer.MAX_VALUE) result.add(dirs[i]); + } + } else { + for (int i = 0; i < 4; i++) { + if (dist[i] == bestDist) result.add(dirs[i]); + } + } + return result; + } + + private static int bfsDownhill(Instance inst, BlockVec start, boolean water, int remaining) { + ArrayDeque queue = new ArrayDeque<>(); + Set visited = new HashSet<>(); + queue.add(new int[]{start.blockX(), start.blockY(), start.blockZ(), 0}); + visited.add(key(start.blockX(), start.blockY(), start.blockZ())); + while (!queue.isEmpty()) { + int[] c = queue.poll(); + BlockVec cp = new BlockVec(c[0], c[1], c[2]); + Block below = inst.getBlock(new BlockVec(c[0], c[1] - 1, c[2])); + if (isReplaceable(below) && !(water ? isWater(below) : isLava(below))) return c[3]; + if (c[3] >= remaining) continue; + for (Direction d : Direction.HORIZONTAL) { + int nx = c[0] + d.normalX(), nz = c[2] + d.normalZ(); + long k = key(nx, c[1], nz); + if (visited.contains(k)) continue; + Block nb = inst.getBlock(new BlockVec(nx, c[1], nz)); + if (!isReplaceable(nb) && !(water ? isWater(nb) : isLava(nb))) continue; + visited.add(k); + queue.add(new int[]{nx, c[1], nz, c[3] + 1}); + } + } + return Integer.MAX_VALUE; + } + + private static long key(int x, int y, int z) { + return ((long) x & 0x3FFFFFFL) | (((long) z & 0x3FFFFFFL) << 26) | (((long) y & 0xFFFL) << 52); + } + + private static int countAdjacentSources(Instance inst, BlockVec pos, boolean water) { + int c = 0; + for (Direction d : Direction.HORIZONTAL) { + Block nb = inst.getBlock(new BlockVec(pos.blockX() + d.normalX(), pos.blockY(), pos.blockZ() + d.normalZ())); + if ((water ? isWater(nb) : isLava(nb)) && parseLevel(nb) == 0) c++; + } + return c; + } + + private static boolean hasSolidBelow(Instance inst, BlockVec pos) { + Block below = inst.getBlock(new BlockVec(pos.blockX(), pos.blockY() - 1, pos.blockZ())); + return below.isSolid(); + } + + private static boolean handleFluidCollision(Instance inst, BlockVec pos, boolean incomingIsWater) { + Block target = inst.getBlock(pos); + if (incomingIsWater && isLava(target)) { + boolean lavaSource = parseLevel(target) == 0; + inst.setBlock(pos, lavaSource ? Block.OBSIDIAN : Block.COBBLESTONE); + return true; + } + if (!incomingIsWater && isWater(target)) { + inst.setBlock(pos, Block.STONE); + return true; + } + return false; + } + + private static boolean isReplaceable(Block b) { + if (b.isAir()) return true; + String p = b.key().value(); + return p.equals("short_grass") || p.equals("tall_grass") || p.equals("fern") || p.equals("large_fern") + || p.equals("dead_bush") || p.equals("seagrass") || p.equals("tall_seagrass") || p.equals("vine") + || p.equals("snow") || p.equals("fire") || p.equals("soul_fire"); + } + + private static boolean isWater(Block b) { return b.compare(Block.WATER); } + private static boolean isLava(Block b) { return b.compare(Block.LAVA); } + + private static int parseLevel(Block b) { + String s = b.getProperty("level"); + if (s == null) return 0; + try { return Integer.parseInt(s); } catch (NumberFormatException e) { return 0; } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/colorJump/ColorJump.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/colorJump/ColorJump.java new file mode 100644 index 0000000..0e6c79d --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/colorJump/ColorJump.java @@ -0,0 +1,180 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.colorJump; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; +import eu.mhsl.minenet.minigames.score.LastWinsScore; +import eu.mhsl.minenet.minigames.util.BatchUtil; +import eu.mhsl.minenet.minigames.world.generator.terrain.CircularPlateTerrainGenerator; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.GameMode; +import net.minestom.server.event.inventory.InventoryPreClickEvent; +import net.minestom.server.event.player.PlayerMoveEvent; +import net.minestom.server.event.player.PlayerSwapItemEvent; +import net.minestom.server.instance.batch.AbsoluteBlockBatch; +import net.minestom.server.instance.block.Block; +import net.minestom.server.item.ItemStack; +import net.minestom.server.timer.Scheduler; +import net.minestom.server.timer.TaskSchedule; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadLocalRandom; + +public class ColorJump extends StatelessGame { + private final List blocks = List.of( + Block.RED_CONCRETE, + Block.ORANGE_CONCRETE, + Block.YELLOW_CONCRETE, + Block.LIME_CONCRETE, + Block.LIGHT_BLUE_CONCRETE, + Block.BLUE_CONCRETE, + Block.PURPLE_CONCRETE, + Block.PINK_CONCRETE + ); + private Block currentBlock; + private Block blockLastRound; + private double roundTime = 5; + private final double multiplierNextRoundTime = 0.9; + + public ColorJump() { + super(Dimension.THE_END.key, "ColorJump", new LastWinsScore()); + this.getScore().setIgnoreLastPlayers(1); + + this.setGenerator(new CircularPlateTerrainGenerator(100)); + + this.eventNode().addListener( + InventoryPreClickEvent.class, + inventoryPreClickEvent -> inventoryPreClickEvent.setCancelled(true) + ); + + this.eventNode().addListener( + PlayerSwapItemEvent.class, + playerSwapItemEvent -> playerSwapItemEvent.setCancelled(true) + ); + } + + @Override + protected void onLoad(@NotNull CompletableFuture callback) { + this.generate(); + } + + @Override + protected void onStart() { + this.nextRound(); + } + + private void nextRound() { + this.generate(); + do { + this.currentBlock = this.blocks.get(ThreadLocalRandom.current().nextInt(this.blocks.size())); + } while(this.currentBlock == this.blockLastRound); + this.blockLastRound = this.currentBlock; + + ItemStack item = ItemStack.of(Objects.requireNonNull(this.currentBlock.registry().material())); + this.getPlayers().forEach(player -> { + player.getInventory().clear(); + for(int i = 0; i < 9; i++) { + player.getInventory().setItemStack(i, item); + } + }); + + var scheduler = MinecraftServer.getSchedulerManager(); + TaskSchedule stop = TaskSchedule.stop(); + + scheduler.scheduleTask(() -> { + if(!this.isRunning) return stop; + this.destroyAllExcept(this.currentBlock); + + scheduler.scheduleTask(() -> { + if(this.isRunning) this.nextRound(); + return stop; + }, TaskSchedule.seconds(3)); + return stop; + }, TaskSchedule.seconds((long) this.roundTime)); + this.roundTime = this.roundTime * this.multiplierNextRoundTime; + if(this.roundTime <= 1) this.roundTime = 1; + + long totalTicks = (long) (this.roundTime * 20); + this.scheduleExpUpdate(scheduler, totalTicks, totalTicks); + } + + private void scheduleExpUpdate(Scheduler scheduler, long remainingTicks, long totalTicks) { + if (!this.isRunning) return; + + if (remainingTicks <= 0) { + this.getPlayers().forEach(player -> { + player.setExp(0f); + player.setLevel(0); + }); + return; + } + + float progress = (float) remainingTicks / totalTicks; + + this.getPlayers().forEach(player -> { + player.setExp(progress); + player.setLevel((int) Math.ceil(remainingTicks / 20.0)); + }); + scheduler.scheduleTask(() -> { + this.scheduleExpUpdate(scheduler, remainingTicks -1, totalTicks); + return TaskSchedule.stop(); + }, TaskSchedule.tick(1)); + } + + private void generate() { + int y = 30; + AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); + + for (int x = -20; x <= 21; x += 2) { + for (int z = -11; z <= 10; z += 2) { + Block randomBlock = this.blocks.get(ThreadLocalRandom.current().nextInt(this.blocks.size())); + + for (int dx = 0; dx < 2; dx++) { + for (int dz = 0; dz < 2; dz++) { + batch.setBlock(x + dx, y, z + dz, randomBlock); + } + } + } + } + BatchUtil.loadAndApplyBatch(batch, this, () -> {}); + } + + private void destroyAllExcept(Block block) { + AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); + int y = 30; + + for (int x = -20; x <= 21; x++) { + for(int z = -11; z <= 10; z++) { + Pos blockPos = new Pos(x, y, z); + if(this.getBlock(blockPos) != block) + batch.setBlock(blockPos, Block.AIR); + } + } + BatchUtil.loadAndApplyBatch(batch, this, () -> {}); + } + + @Override + public Pos getSpawn() { + return new Pos(0, 31, 0); + } + + @Override + protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { + var player = playerMoveEvent.getPlayer(); + + if(playerMoveEvent.getNewPosition().y() >= 29) + return; + player.teleport(this.getSpawn()); + if(this.isRunning) { + this.getScore().insertResult(player); + player.setGameMode(GameMode.SPECTATOR); + } + if(!this.isBeforeBeginning && !this.isRunning) { + player.setGameMode(GameMode.SPECTATOR); + player.teleport(this.getSpawn()); + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/colorJump/ColorJumpFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/colorJump/ColorJumpFactory.java new file mode 100644 index 0000000..8c6d53b --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/colorJump/ColorJumpFactory.java @@ -0,0 +1,33 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.colorJump; + +import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.room.Room; +import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import net.minestom.server.item.Material; + +import java.util.Map; + +public class ColorJumpFactory implements GameFactory { + + @Override + public TranslatedComponent name() { + return TranslatedComponent.byId("game_ColorJump#name"); + } + + @Override + public Material symbol() { + return Material.PINK_CONCRETE; + } + + @Override + public TranslatedComponent description() { + return TranslatedComponent.byId("game_ColorJump#description"); + } + + @Override + public Game manufacture(Room parent, Map> configuration) throws Exception { + return new ColorJump().setParent(parent); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/deathcube/Deathcube.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/deathcube/Deathcube.java index e43a59f..16c6acd 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/deathcube/Deathcube.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/deathcube/Deathcube.java @@ -1,13 +1,14 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.deathcube; +import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; import eu.mhsl.minenet.minigames.score.FirstWinsScore; import eu.mhsl.minenet.minigames.util.BatchUtil; -import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.world.BlockPallet; import eu.mhsl.minenet.minigames.world.generator.terrain.CircularPlateTerrainGenerator; import io.github.togar2.pvp.feature.CombatFeatures; import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.GameMode; import net.minestom.server.event.player.PlayerMoveEvent; import net.minestom.server.instance.batch.AbsoluteBlockBatch; import org.jetbrains.annotations.NotNull; @@ -25,9 +26,9 @@ class Deathcube extends StatelessGame { this.radius = radius; this.height = height + 49; this.percentage = percentage; - this.setGenerator(new CircularPlateTerrainGenerator(radius+10).setPlateHeight(50)); + this.setGenerator(new CircularPlateTerrainGenerator(radius + 10).setPlateHeight(50)); - if(pvpEnabled == 1) eventNode().addChild( + if(pvpEnabled == 1) this.eventNode().addChild( CombatFeatures.empty() .add(CombatFeatures.VANILLA_ATTACK) .add(CombatFeatures.VANILLA_DAMAGE) @@ -41,12 +42,12 @@ class Deathcube extends StatelessGame { protected void onLoad(@NotNull CompletableFuture callback) { AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); - for(int x = -radius; x <= radius; x++) { - for (int z = -radius; z <= radius; z++) { - if(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > radius) continue; + for(int x = -this.radius; x <= this.radius; x++) { + for(int z = -this.radius; z <= this.radius; z++) { + if(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > this.radius) continue; - for (int y = 49; y < height; y++) { - if(super.rnd.nextInt(1, 100) <= percentage) { + for(int y = 49; y < this.height; y++) { + if(super.rnd.nextInt(1, 100) <= this.percentage) { batch.setBlock(x, y, z, BlockPallet.WOOD.rnd()); } } @@ -61,18 +62,20 @@ class Deathcube extends StatelessGame { super.onPlayerMove(playerMoveEvent); if(playerMoveEvent.getNewPosition().y() < 48) { playerMoveEvent.setCancelled(true); - playerMoveEvent.getPlayer().teleport(getSpawn()); + playerMoveEvent.getPlayer().teleport(this.getSpawn()); return; } - if(isBeforeBeginning && playerMoveEvent.getNewPosition().y() > 51.5) { + if(this.isBeforeBeginning && playerMoveEvent.getNewPosition().y() > 51.5) { playerMoveEvent.setCancelled(true); return; } - if(playerMoveEvent.getNewPosition().y() > height) getScore().insertResult(playerMoveEvent.getPlayer()); + if(playerMoveEvent.getNewPosition().y() <= this.height) return; + this.getScore().insertResult(playerMoveEvent.getPlayer()); + playerMoveEvent.getPlayer().setGameMode(GameMode.SPECTATOR); } @Override public Pos getSpawn() { - return new Pos(0, 50, -(radius+5)); + return new Pos(0, 50, -(this.radius + 5)); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/deathcube/DeathcubeFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/deathcube/DeathcubeFactory.java index eda3151..52134eb 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/deathcube/DeathcubeFactory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/deathcube/DeathcubeFactory.java @@ -1,9 +1,9 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.deathcube; import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; -import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption; import eu.mhsl.minenet.minigames.instance.room.Room; import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; @@ -25,10 +25,10 @@ public class DeathcubeFactory implements GameFactory { @Override public ConfigManager configuration() { return new ConfigManager() - .addOption(new NumericOption("radius", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#radius"), 10, 20, 30)) - .addOption(new NumericOption("height", Material.SCAFFOLDING, TranslatedComponent.byId("optionCommon#height"), 10, 30, 50)) - .addOption(new NumericOption("percentage", Material.COBWEB, TranslatedComponent.byId("game_Deathcube#optionPercentageBlocks"), 5, 7, 9, 11, 13)) - .addOption(new NumericOption("pvpEnabled", Material.STICK, TranslatedComponent.byId("game_Deathcube#optionPvpEnabled"), 0, 1)); + .addOption(new NumericOption("radius", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#radius"), 10, 20, 30)) + .addOption(new NumericOption("height", Material.SCAFFOLDING, TranslatedComponent.byId("optionCommon#height"), 10, 30, 50)) + .addOption(new NumericOption("percentage", Material.COBWEB, TranslatedComponent.byId("game_Deathcube#optionPercentageBlocks"), 5, 7, 9, 11, 13)) + .addOption(new NumericOption("pvpEnabled", Material.STICK, TranslatedComponent.byId("game_Deathcube#optionPvpEnabled"), 0, 1)); } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/elytraRace/ElytraRace.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/elytraRace/ElytraRace.java index 27cd19f..a355839 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/elytraRace/ElytraRace.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/elytraRace/ElytraRace.java @@ -20,7 +20,11 @@ import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.EquipmentSlot; import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; -import net.minestom.server.event.player.*; +import net.minestom.server.event.player.PlayerMoveEvent; +import net.minestom.server.event.player.PlayerStartFlyingWithElytraEvent; +import net.minestom.server.event.player.PlayerStopFlyingWithElytraEvent; +import net.minestom.server.event.player.PlayerUseItemEvent; +import net.minestom.server.instance.Chunk; import net.minestom.server.instance.batch.AbsoluteBlockBatch; import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; @@ -33,6 +37,7 @@ import org.jetbrains.annotations.NotNull; import java.time.Duration; import java.util.HashMap; import java.util.Map; +import java.util.Random; import java.util.concurrent.CompletableFuture; @@ -48,80 +53,74 @@ public class ElytraRace extends StatelessGame { private final Material resetMaterial = Material.RED_DYE; private final int boostMultiplier = 50; private final Material ringMaterial = Material.GOLD_BLOCK; - private int generatedUntil = 0; - - private record CheckPointData(int currentCheckpoint, int nextCheckpoint) { - public CheckPointData next(int spacing) { - return new CheckPointData(nextCheckpoint, nextCheckpoint + spacing); - } - } private final Map playerCheckpoints = new HashMap<>(); + private int generatedUntil = 0; public ElytraRace(int ringCount) { super(Dimension.OVERWORLD.key, "ElytraRace", new FirstWinsScore()); this.ringCount = ringCount; - setGenerator(vale); - vale.setCalculateSeaLevel(point -> seaLevel); - vale.setXShiftMultiplier(integer -> NumberUtil.map(integer, 50, 500, 0, 1)); - vale.addMixIn(new PlaneTerrainGenerator(gameHeight, Block.BARRIER)); + this.setGenerator(this.vale); + this.vale.setCalculateSeaLevel(point -> this.seaLevel); + this.vale.setXShiftMultiplier(integer -> NumberUtil.map(integer, 50, 500, 0, 1)); + this.vale.addMixIn(new PlaneTerrainGenerator(this.gameHeight, Block.BARRIER)); - eventNode().addListener(PlayerUseItemEvent.class, playerUseItemEvent -> { + this.eventNode().addListener(PlayerUseItemEvent.class, playerUseItemEvent -> { Player player = playerUseItemEvent.getPlayer(); Material usedMaterial = playerUseItemEvent.getItemStack().material(); - if(usedMaterial.equals(boostMaterial)) { - if(!player.isFlyingWithElytra()) return; + if(usedMaterial.equals(this.boostMaterial)) { + if(!player.isFlyingWithElytra()) return; - boost(player); - InventoryUtil.removeItemFromPlayer(player, boostMaterial, 1); - } else if(usedMaterial.equals(resetMaterial)) { - toCheckpoint(player); - InventoryUtil.removeItemFromPlayer(player, resetMaterial, 1); + this.boost(player); + InventoryUtil.removeItemFromPlayer(player, this.boostMaterial, 1); + } else if(usedMaterial.equals(this.resetMaterial)) { + this.toCheckpoint(player); + InventoryUtil.removeItemFromPlayer(player, this.resetMaterial, 1); } }); - eventNode().addListener(PlayerStopFlyingWithElytraEvent.class, playerStopFlyingWithElytraEvent -> { + this.eventNode().addListener(PlayerStopFlyingWithElytraEvent.class, playerStopFlyingWithElytraEvent -> { Player player = playerStopFlyingWithElytraEvent.getPlayer(); - if(Position.blocksBelowPlayer(this, player).contains(ringMaterial.block())) { + if(Position.blocksBelowPlayer(this, player).contains(this.ringMaterial.block())) { player.setFlyingWithElytra(true); - boost(player); + this.boost(player); } else { - toCheckpoint(player); + this.toCheckpoint(player); // getScore().insertResult(playerStopFlyingWithElytraEvent.getPlayer()); // playerStopFlyingWithElytraEvent.getPlayer().setGameMode(GameMode.SPECTATOR); } }); - eventNode().addListener(PlayerStartFlyingWithElytraEvent.class, playerStartFlyingWithElytraEvent -> { - if(!isRunning) { + this.eventNode().addListener(PlayerStartFlyingWithElytraEvent.class, playerStartFlyingWithElytraEvent -> { + if(!this.isRunning) { playerStartFlyingWithElytraEvent.getPlayer().setFlyingWithElytra(false); return; } - boost(playerStartFlyingWithElytraEvent.getPlayer()); + this.boost(playerStartFlyingWithElytraEvent.getPlayer()); }); } @Override protected void onLoad(@NotNull CompletableFuture callback) { - Point spawnpoint = new Pos(vale.getXShiftAtZ(0), -46, 0); - GeneratorUtils.iterateArea(spawnpoint.sub(5, 0, 5), spawnpoint.add(5, 0, 5), point -> setBlock(point, BlockPallet.STREET.rnd())); + Point spawnpoint = new Pos(this.vale.getXShiftAtZ(0), -46, 0); + GeneratorUtils.iterateArea(spawnpoint.sub(5, 0, 5), spawnpoint.add(5, 0, 5), point -> this.setBlock(point, BlockPallet.STREET.rnd())); - generateRing(ringSpacing); - generateRing(ringSpacing * 2); + this.generateRing(this.ringSpacing); + this.generateRing(this.ringSpacing * 2); callback.complete(null); } @Override protected void onStart() { - getPlayers().forEach(player -> { + this.getPlayers().forEach(player -> { player.getInventory().setEquipment(EquipmentSlot.CHESTPLATE, (byte) 0, ItemStack.of(Material.ELYTRA)); for(int i = 0; i < 3; i++) { - player.getInventory().setItemStack(i, ItemStack.builder(boostMaterial).customName(TranslatedComponent.byId("boost").getAssembled(player)).build()); + player.getInventory().setItemStack(i, ItemStack.builder(this.boostMaterial).customName(TranslatedComponent.byId("boost").getAssembled(player)).build()); } - addResetItemToPlayer(player); + this.addResetItemToPlayer(player); }); } @@ -130,44 +129,44 @@ public class ElytraRace extends StatelessGame { Player player = playerMoveEvent.getPlayer(); Point newPos = playerMoveEvent.getNewPosition(); - if(isBeforeBeginning && playerMoveEvent.getNewPosition().y() < getSpawn().y()) { - player.teleport(getSpawn()); + if(this.isBeforeBeginning && playerMoveEvent.getNewPosition().y() < this.getSpawn().y()) { + player.teleport(this.getSpawn()); return; } - playerCheckpoints.putIfAbsent(player, new CheckPointData(ringSpacing, ringSpacing * 2)); + this.playerCheckpoints.putIfAbsent(player, new CheckPointData(0, this.ringSpacing)); - if(newPos.z() > generatedUntil - ringSpacing) { - generateRing(generatedUntil + ringSpacing); + if(newPos.z() > this.generatedUntil - this.ringSpacing) { + this.generateRing(this.generatedUntil + this.ringSpacing); } - if(newPos.z() > playerCheckpoints.get(player).nextCheckpoint) { - playerCheckpoints.put(player, playerCheckpoints.get(player).next(ringSpacing)); - boost(player); + if(newPos.z() > this.playerCheckpoints.get(player).nextCheckpoint) { + this.playerCheckpoints.put(player, this.playerCheckpoints.get(player).next(this.ringSpacing)); + this.boost(player); } - if(newPos.y() > gameHeight - 5) { - Point particlePoint = newPos.withY(gameHeight); + if(newPos.y() > this.gameHeight - 5) { + Point particlePoint = newPos.withY(this.gameHeight); ParticlePacket particle = new ParticlePacket( - Particle.WAX_ON, - particlePoint.blockX(), - particlePoint.blockY(), - particlePoint.withZ(z -> z+10).blockZ(), - 20, - 0, - 30, - 1f, - Math.toIntExact((long) NumberUtil.map(newPos.y(), gameHeight - 5, gameHeight, 50, 500)) + Particle.WAX_ON, + particlePoint.blockX(), + particlePoint.blockY(), + particlePoint.withZ(z -> z + 10).blockZ(), + 20, + 0, + 30, + 1f, + Math.toIntExact((long) NumberUtil.map(newPos.y(), this.gameHeight - 5, this.gameHeight, 50, 500)) ); player.sendPacket(particle); } - if(getBlock(player.getPosition()).equals(Block.WATER)) { - toCheckpoint(player); + if(this.getBlock(player.getPosition()).equals(Block.WATER)) { + this.toCheckpoint(player); } - if(newPos.z() > ringCount * ringSpacing) { - getScore().insertResult(player); + if(newPos.z() > this.ringCount * this.ringSpacing) { + this.getScore().insertResult(player); player.setGameMode(GameMode.SPECTATOR); player.setFlyingWithElytra(false); } @@ -175,43 +174,53 @@ public class ElytraRace extends StatelessGame { @Override public Pos getSpawn() { - return new Pos(vale.getXShiftAtZ(0), -45, 0); + return new Pos(this.vale.getXShiftAtZ(0), -45, 0); } private void addResetItemToPlayer(Player p) { - p.getInventory().setItemStack(8, ItemStack.builder(resetMaterial).customName(TranslatedComponent.byId("reset").getAssembled(p)).build()); + p.getInventory().setItemStack(8, ItemStack.builder(this.resetMaterial).customName(TranslatedComponent.byId("reset").getAssembled(p)).build()); } private Point getRingPositionAtZ(int z) { - return new Pos(vale.getXShiftAtZ(z), -45, z); + Random random = new Random(this.hashCode() + z); + return new Pos(this.vale.getXShiftAtZ(z), -45 + random.nextInt(-5, 15), z); } - private CompletableFuture generateRing(int zPos) { - if(zPos > ringCount * ringSpacing) return null; - boolean isLast = (zPos == ringCount * ringSpacing); + private void generateRing(int zPos) { + if(zPos > this.ringCount * this.ringSpacing) return; + boolean isLast = (zPos == this.ringCount * this.ringSpacing); - generatedUntil = zPos; + this.generatedUntil = zPos; AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); - Point ringPos = getRingPositionAtZ(zPos); + Point ringPos = this.getRingPositionAtZ(zPos); GeneratorUtils.iterateArea( - ringPos.sub(100, 0, 0).withY(0), - ringPos.add(100, 0, 0).withY(seaLevel), - point -> batch.setBlock(point, Block.BARRIER) + ringPos.sub(100, 0, 0).withY(0), + ringPos.add(100, 0, 0).withY(this.seaLevel), + point -> batch.setBlock(point, Block.BARRIER) ); GeneratorUtils.iterateArea( - ringPos.sub(3, 3, 0), - ringPos.add(3, 3, 0), - point -> batch.setBlock(point, isLast ? Block.DIAMOND_BLOCK : ringMaterial.block()) + ringPos.sub(3, 3, 0), + ringPos.add(3, 3, 0), + point -> batch.setBlock(point, isLast ? Block.DIAMOND_BLOCK : this.ringMaterial.block()) ); GeneratorUtils.iterateArea( - ringPos.sub(2, 2, 0), - ringPos.add(2, 2, 0), - point -> batch.setBlock(point, Block.AIR) + ringPos.sub(2, 2, 0), + ringPos.add(2, 2, 0), + point -> batch.setBlock(point, Block.AIR) ); - BatchUtil.loadAndApplyBatch(batch, this, () -> {}); - return null; + int minChunkX = (ringPos.blockX() - 100) >> 4; + int maxChunkX = (ringPos.blockX() + 100) >> 4; + int chunkZ = ringPos.blockZ() >> 4; + + BatchUtil.loadAndApplyBatch(batch, this, () -> { + for(int cx = minChunkX; cx <= maxChunkX; cx++) { + Chunk chunk = this.getChunk(cx, chunkZ); + if(chunk == null) continue; + this.getPlayers().forEach(chunk::sendChunk); + } + }); } private void boost(Player player) { @@ -219,42 +228,52 @@ public class ElytraRace extends StatelessGame { Vec playerVelocity = player.getPosition().direction(); player.setVelocity( - player.getVelocity().add(playerVelocity.mul(boostMultiplier)) - .withY(playerVelocity.withY(v -> v * boostMultiplier).y()) + player.getVelocity().add(playerVelocity.mul(this.boostMultiplier)) + .withY(playerVelocity.withY(v -> v * this.boostMultiplier).y()) ); } private void toCheckpoint(Player p) { - Point checkpointPos = getRingPositionAtZ(playerCheckpoints.get(p).currentCheckpoint); + CheckPointData data = this.playerCheckpoints.get(p); + Point checkpointPos = this.getRingPositionAtZ(this.playerCheckpoints.get(p).currentCheckpoint); + if(data.currentCheckpoint == 0) checkpointPos = this.getSpawn().add(0, 3, 0); p.setVelocity(Vec.ZERO); p.setFlyingWithElytra(false); - p.teleport(Pos.fromPoint(checkpointPos).add(0.5, 0, 0.5)); + p.teleport(new Pos(checkpointPos).add(0.5, 0, 0.5)); p.setFlying(true); p.setFlyingSpeed(0); - new Countdown(TitleMessage.class) - .countdown( - Audience.audience(p), - 3, - countdownModifier -> - countdownModifier.message = new TitleMessage( - Duration.ofMillis(300), - Duration.ofMillis(700) - ) - .subtitle( - subtitleMessage -> - subtitleMessage - .appendStatic(Component.text("Launch in ", NamedTextColor.DARK_GREEN)) - .appendStatic(Component.text(countdownModifier.timeLeft, NamedTextColor.GREEN)) - .appendStatic(Component.text(" seconds", NamedTextColor.DARK_GREEN)) - ) - ).thenRun(() -> { - p.setFlying(false); - p.setFlyingSpeed(1); - p.setFlyingWithElytra(true); - boost(p); - addResetItemToPlayer(p); - }); + new Countdown() + .countdown( + Audience.audience(p), + 3, + countdownModifier -> + countdownModifier.message = new TitleMessage( + Duration.ofMillis(300), + Duration.ofMillis(700) + ) + .subtitle( + subtitleMessage -> + subtitleMessage + .appendTranslated("game_ElytraRace#launchIn") + .appendSpace() + .appendStatic(Component.text(countdownModifier.timeLeft, NamedTextColor.GREEN)) + .appendSpace() + .appendTranslated(countdownModifier.timeLeft == 1 ? "common#second" : "common#seconds") + ) + ).thenRun(() -> { + p.setFlying(false); + p.setFlyingSpeed(1); + p.setFlyingWithElytra(true); + this.boost(p); + this.addResetItemToPlayer(p); + }); + } + + private record CheckPointData(int currentCheckpoint, int nextCheckpoint) { + public CheckPointData next(int spacing) { + return new CheckPointData(this.nextCheckpoint, this.nextCheckpoint + spacing); + } } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/elytraRace/ElytraRaceFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/elytraRace/ElytraRaceFactory.java index 2a61818..9aa7525 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/elytraRace/ElytraRaceFactory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/elytraRace/ElytraRaceFactory.java @@ -30,7 +30,7 @@ public class ElytraRaceFactory implements GameFactory { @Override public ConfigManager configuration() { return new ConfigManager() - .addOption(new NumericOption("ringCount", Material.DIAMOND_BLOCK, TranslatedComponent.byId("game_ElytraRace#ringCount"), 5, 10, 20, 30, 40, 50)); + .addOption(new NumericOption("ringCount", Material.DIAMOND_BLOCK, TranslatedComponent.byId("game_ElytraRace#ringCount"), 5, 10, 20, 30, 40, 50)); } @Override diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/fastbridge/Fastbridge.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/fastbridge/Fastbridge.java new file mode 100644 index 0000000..6ca3950 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/fastbridge/Fastbridge.java @@ -0,0 +1,64 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.fastbridge; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; +import eu.mhsl.minenet.minigames.score.FirstWinsScore; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.event.player.PlayerBlockPlaceEvent; +import net.minestom.server.event.player.PlayerMoveEvent; +import net.minestom.server.instance.Chunk; +import net.minestom.server.inventory.PlayerInventory; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; + +public class Fastbridge extends StatelessGame { + private int currentSpawn = 0; + + public Fastbridge() { + super(Dimension.OVERWORLD.key, "Fastbridge", new FirstWinsScore()); + this.setGenerator(new FastbridgeChunkgenerator()); + } + + @Override + protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { + Player player = playerMoveEvent.getPlayer(); + Pos newPos = playerMoveEvent.getNewPosition(); + if(this.getScore().hasResult(player)) return; + if(newPos.y() < 0) { + player.teleport(this.getSpawn()); + if(!this.isBeforeBeginning) this.resetPlayer(player); + } + if(newPos.x() > 53) { + this.getScore().insertResult(player); + player.setGameMode(GameMode.SPECTATOR); + } + } + + @Override + protected void onStart() { + this.getPlayers().forEach(player -> { + player.setGameMode(GameMode.SURVIVAL); + this.resetPlayer(player); + }); + } + + @Override + protected void onBlockPlace(@NotNull PlayerBlockPlaceEvent playerBlockPlaceEvent) { + if(this.isBeforeBeginning) playerBlockPlaceEvent.setCancelled(true); + } + + private void resetPlayer(Player player) { + if(this.isBeforeBeginning) return; + PlayerInventory inventory = player.getInventory(); + inventory.clear(); + inventory.addItemStack(ItemStack.of(Material.WHITE_WOOL, 64)); + } + + @Override + public synchronized Pos getSpawn() { + return new Pos(24, 1, this.currentSpawn++ * Chunk.CHUNK_SIZE_Z * 2 - 8, -90, 0); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/fastbridge/FastbridgeChunkgenerator.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/fastbridge/FastbridgeChunkgenerator.java new file mode 100644 index 0000000..b0a05b7 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/fastbridge/FastbridgeChunkgenerator.java @@ -0,0 +1,29 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.fastbridge; + +import eu.mhsl.minenet.minigames.world.generator.featureEnriched.ValeGenerator; +import eu.mhsl.minenet.minigames.world.generator.terrain.BaseGenerator; +import net.minestom.server.instance.block.Block; + +public class FastbridgeChunkgenerator extends BaseGenerator { + public FastbridgeChunkgenerator() { + this.addMixIn(unit -> { + if(unit.absoluteStart().chunkZ() % 2 == 0) { + unit.modifier().fill(Block.BARRIER); + return; + } + + if(unit.absoluteStart().chunkX() != 1 && unit.absoluteStart().chunkX() != 3) return; + for(int x = 5; x <= 10; x++) { + for(int z = 5; z <= 10; z++) { + unit.modifier().setRelative(x, 64, z, unit.absoluteStart().chunkX() == 3 ? Block.GOLD_BLOCK : Block.GRASS_BLOCK); + } + } + }); + + ValeGenerator vale = new ValeGenerator(); + vale.setXShiftMultiplier(integer -> 0.5d); + vale.setHeightNoiseMultiplier(integer -> 2); + vale.setXShiftOffset(integer -> 40d); + this.addMixIn(vale); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/fastbridge/FastbridgeFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/fastbridge/FastbridgeFactory.java new file mode 100644 index 0000000..b045988 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/fastbridge/FastbridgeFactory.java @@ -0,0 +1,33 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.fastbridge; + +import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.room.Room; +import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import net.minestom.server.item.Material; + +import java.util.Map; + +public class FastbridgeFactory implements GameFactory { + + @Override + public TranslatedComponent name() { + return TranslatedComponent.byId("game_Fastbridge#name"); + } + + @Override + public TranslatedComponent description() { + return TranslatedComponent.byId("game_Fastbridge#description"); + } + + @Override + public Material symbol() { + return Material.WHITE_WOOL; + } + + @Override + public Game manufacture(Room parent, Map> configuration) throws Exception { + return new Fastbridge().setParent(parent); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/highGround/HighGround.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/highGround/HighGround.java new file mode 100644 index 0000000..e13986e --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/highGround/HighGround.java @@ -0,0 +1,139 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.highGround; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; +import eu.mhsl.minenet.minigames.score.PointsWinScore; +import eu.mhsl.minenet.minigames.world.BlockPallet; +import io.github.togar2.pvp.events.EntityKnockbackEvent; +import io.github.togar2.pvp.events.FinalAttackEvent; +import io.github.togar2.pvp.events.PrepareAttackEvent; +import io.github.togar2.pvp.feature.CombatFeatures; +import io.github.togar2.pvp.feature.knockback.KnockbackSettings; +import net.minestom.server.ServerFlag; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Player; +import net.minestom.server.event.instance.InstanceTickEvent; +import net.minestom.server.event.player.PlayerMoveEvent; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; + +import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; + +class HighGround extends StatelessGame { + private final int radius; + private final int seconds; + private final WeakHashMap scoreMap = new WeakHashMap<>(); + + HighGround(int radius, int seconds) { + super(Dimension.THE_END.key, "highground", new PointsWinScore()); + this.radius = radius; + this.seconds = seconds; + + this.eventNode().addChild( + CombatFeatures.empty() + .add(CombatFeatures.VANILLA_ATTACK) + .add(CombatFeatures.VANILLA_DAMAGE) + .add(CombatFeatures.VANILLA_KNOCKBACK) + .build() + .createNode() + ); + + this.eventNode().addListener( + FinalAttackEvent.class, + finalAttackEvent -> finalAttackEvent.setBaseDamage(0) + ); + + this.eventNode().addListener(PrepareAttackEvent.class, prepareAttackEvent -> { + if(this.isBeforeBeginning) { + prepareAttackEvent.setCancelled(true); + } + }); + + this.eventNode().addListener( + EntityKnockbackEvent.class, + entityKnockbackEvent -> { + double tps = ServerFlag.SERVER_TICKS_PER_SECOND; + entityKnockbackEvent.setSettings( + KnockbackSettings.builder() + .horizontal(1.1) + .vertical(KnockbackSettings.DEFAULT.vertical() / tps) + .verticalLimit(KnockbackSettings.DEFAULT.verticalLimit() / tps) + .extraHorizontal(KnockbackSettings.DEFAULT.extraHorizontal() / tps) + .extraVertical(KnockbackSettings.DEFAULT.extraVertical() / tps) + .build() + ); + } + ); + + this.eventNode().addListener(InstanceTickEvent.class, instanceTickEvent -> { + if(this.isBeforeBeginning || !this.isRunning) return; + this.getPlayers().forEach(player -> { + if((player.isOnGround() && player.getPosition().y() >= 1) || (!player.isOnGround() && player.getPosition().y() >= 1.5)) { + this.scoreMap.put(player, this.scoreMap.get(player) + 1); + player.setLevel(this.scoreMap.get(player) / 20); + player.setExp((this.scoreMap.get(player) % 20) / 20.0f); + } + }); + }); + } + + @Override + protected void onLoad(@NotNull CompletableFuture callback) { + for(int y = 0; y >= -3; y--) { + int radius = (Math.abs(y) * 5) + this.radius; + for(int x = -radius; x <= radius; x++) { + for(int z = -radius; z <= radius; z++) { + double distance = new Pos(x, 0, z).distance(0, 0, 0); + if(distance <= radius) { + this.setBlock(x, y, z, y == 0 ? Block.DIAMOND_BLOCK : Block.GRASS_BLOCK); + Pos featurePosition = new Pos(x, y + 1, z); + + if(y >= 0 || this.getBlock(featurePosition).isSolid()) continue; + if(this.rnd.nextDouble() < 0.1) { + this.setBlock(featurePosition, Block.SHORT_GRASS); + } + if(this.rnd.nextDouble() < 0.01) { + this.setBlock(featurePosition, BlockPallet.FLOWER.rnd()); + } + } + } + } + } + } + + @Override + protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { + Player player = playerMoveEvent.getPlayer(); + if(playerMoveEvent.getNewPosition().y() < -10) { + player.teleport(this.getSpawn()); + } + } + + @Override + protected void start() { + this.getPlayers().forEach(player -> this.scoreMap.put(player, 0)); + super.start(); + } + + @Override + protected void onStart() { + this.setTimeLimit(this.seconds); + } + + @Override + protected void onStop() { + this.getPlayers().forEach(player -> this.getScore().insertResult(player, this.scoreMap.get(player))); + } + + @Override + public Pos getSpawn() { + double theta = this.rnd.nextDouble() * 2 * Math.PI; + + double spawnRadius = this.radius + 5; + double x = spawnRadius * Math.cos(theta); + double z = spawnRadius * Math.sin(theta); + + return new Pos(x, 0, z).withLookAt(new Pos(0, 0, 0)); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/highGround/HighGroundFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/highGround/HighGroundFactory.java new file mode 100644 index 0000000..6c77cac --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/highGround/HighGroundFactory.java @@ -0,0 +1,49 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.highGround; + +import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.RestrictionHandler; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.common.MinimalPlayeramountGameRestriction; +import eu.mhsl.minenet.minigames.instance.room.Room; +import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import net.minestom.server.item.Material; + +import java.util.Map; + +public class HighGroundFactory implements GameFactory { + @Override + public TranslatedComponent name() { + return TranslatedComponent.byId("game_Highground#name"); + } + + @Override + public TranslatedComponent description() { + return TranslatedComponent.byId("game_Highground#description"); + } + + @Override + public Material symbol() { + return Material.GOLDEN_HELMET; + } + + @Override + public ConfigManager configuration() { + return new ConfigManager() + .addOption(new NumericOption("radius", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#radius"), 3, 5, 7, 10)) + .addOption(new NumericOption("seconds", Material.CLOCK, TranslatedComponent.byId("optionCommon#seconds"), 30, 60, 90, 120)); + } + + @Override + public Game manufacture(Room parent, Map> configuration) throws Exception { + return new HighGround(configuration.get("radius").getAsInt(), configuration.get("seconds").getAsInt()).setParent(parent); + } + + @Override + public RestrictionHandler globalRestrictions() { + return new RestrictionHandler() + .addRestriction(new MinimalPlayeramountGameRestriction(2)); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/jumpDive/JumpDive.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/jumpDive/JumpDive.java index dd4cf4e..f1bd09b 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/jumpDive/JumpDive.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/jumpDive/JumpDive.java @@ -35,10 +35,10 @@ public class JumpDive extends StatelessGame { protected void onLoad(@NotNull CompletableFuture callback) { AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); - for(int x = -radius*2; x <= radius*2; x++) { - for(int z = -radius*2; z <= radius*2; z++) { - if(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > radius) { - batch.setBlock(x, height, z, BlockPallet.STONE.rnd()); + for(int x = -this.radius * 2; x <= this.radius * 2; x++) { + for(int z = -this.radius * 2; z <= this.radius * 2; z++) { + if(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > this.radius) { + batch.setBlock(x, this.height, z, BlockPallet.STONE.rnd()); } else { batch.setBlock(x, 0, z, BlockPallet.GROUND.rnd()); batch.setBlock(x, 1, z, Block.WATER); @@ -51,7 +51,7 @@ public class JumpDive extends StatelessGame { @Override protected void onStart() { - setTimeLimit(timeLimit); + this.setTimeLimit(this.timeLimit); } @Override @@ -59,22 +59,22 @@ public class JumpDive extends StatelessGame { Player p = playerMoveEvent.getPlayer(); if( - p.isOnGround() && playerMoveEvent.getNewPosition().y() < height - || playerMoveEvent.getNewPosition().y() < 0 - || isBeforeBeginning && playerMoveEvent.getNewPosition().y() < height + p.isOnGround() && playerMoveEvent.getNewPosition().y() < this.height + || playerMoveEvent.getNewPosition().y() < 0 + || this.isBeforeBeginning && playerMoveEvent.getNewPosition().y() < this.height ) { - p.teleport(getSpawn()); + p.teleport(this.getSpawn()); playerMoveEvent.setCancelled(true); } if( playerMoveEvent.getNewPosition().y() <= 1 - && playerMoveEvent.getNewPosition().distance(0, 1, 0) < radius + 0.5 - && !(!isBeforeBeginning && !isRunning) + && playerMoveEvent.getNewPosition().distance(0, 1, 0) < this.radius + 0.5 + && !(!this.isBeforeBeginning && !this.isRunning) ) { - setBlock(playerMoveEvent.getNewPosition().withY(1), Block.REDSTONE_BLOCK); - scores.merge(p, 1, Integer::sum); - p.teleport(getSpawn()); + this.setBlock(playerMoveEvent.getNewPosition().withY(1), Block.REDSTONE_BLOCK); + this.scores.merge(p, 1, Integer::sum); + p.teleport(this.getSpawn()); playerMoveEvent.setCancelled(true); p.playSound(Sound.sound(SoundEvent.ENTITY_EXPERIENCE_ORB_PICKUP, Sound.Source.PLAYER, 2f, 2f)); } @@ -82,18 +82,18 @@ public class JumpDive extends StatelessGame { @Override protected void onStop() { - getPlayers().forEach(player -> getScore().insertResult(player, scores.getOrDefault(player, 0))); + this.getPlayers().forEach(player -> this.getScore().insertResult(player, this.scores.getOrDefault(player, 0))); } @Override public Pos getSpawn() { - double theta = rnd.nextDouble() * 2 * Math.PI; + double theta = this.rnd.nextDouble() * 2 * Math.PI; - double spawnRadius = radius + 2; + double spawnRadius = this.radius + 2; double x = spawnRadius * Math.cos(theta); double z = spawnRadius * Math.sin(theta); - return new Pos(x, height + 2, z).withLookAt(new Pos(0, height, 0)); + return new Pos(x, this.height + 2, z).withLookAt(new Pos(0, this.height, 0)); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/minerun/Minerun.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/minerun/Minerun.java index 79255b0..1dad159 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/minerun/Minerun.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/minerun/Minerun.java @@ -1,11 +1,11 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.minerun; +import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; import eu.mhsl.minenet.minigames.score.FirstWinsScore; import eu.mhsl.minenet.minigames.util.BatchUtil; import eu.mhsl.minenet.minigames.util.CommonProperties; import eu.mhsl.minenet.minigames.util.Intersect; -import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.world.BlockPallet; import eu.mhsl.minenet.minigames.world.generator.terrain.SquarePlateTerrainGenerator; import net.kyori.adventure.sound.Sound; @@ -18,7 +18,7 @@ import net.minestom.server.instance.block.Block; import net.minestom.server.sound.SoundEvent; import org.jetbrains.annotations.NotNull; -import java.util.*; +import java.util.Random; import java.util.concurrent.CompletableFuture; class Minerun extends StatelessGame { @@ -33,7 +33,7 @@ class Minerun extends StatelessGame { public Minerun(int width, int length, int minePercentage) { super(Dimension.THE_END.key, "Minerun", new FirstWinsScore()); - setGenerator(new SquarePlateTerrainGenerator(width, length + preRun + afterFinishLine).setPlateHeight(50).setGenerateBorders(true)); + this.setGenerator(new SquarePlateTerrainGenerator(width, length + this.preRun + this.afterFinishLine).setPlateHeight(50).setGenerateBorders(true)); this.width = width; this.length = length; @@ -42,24 +42,24 @@ class Minerun extends StatelessGame { @Override protected void onLoad(@NotNull CompletableFuture callback) { - int spawnToFinishLine = preRun + length + afterMines; + int spawnToFinishLine = this.preRun + this.length + this.afterMines; Random random = new Random(); AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); - for(int x = 0; x <= width; x++) { - for(int z = preRun; z <= length + preRun; z++) { + for(int x = 0; x <= this.width; x++) { + for(int z = this.preRun; z <= this.length + this.preRun; z++) { - if (random.nextInt(0, 100) < minePercentage) { + if(random.nextInt(0, 100) < this.minePercentage) { batch.setBlock(x, 50, z, BlockPallet.PRESSURE_PLATES.rnd()); } } } - for(int x = 0; x <= width; x++) { + for(int x = 0; x <= this.width; x++) { batch.setBlock(x, 49, spawnToFinishLine, Block.GOLD_BLOCK); - batch.setBlock(x, 49, preRun, Block.GOLD_BLOCK); - batch.setBlock(x, 50, preRun, Block.OAK_FENCE.withProperties(CommonProperties.fenceEastWest)); + batch.setBlock(x, 49, this.preRun, Block.GOLD_BLOCK); + batch.setBlock(x, 50, this.preRun, Block.OAK_FENCE.withProperties(CommonProperties.fenceEastWest)); } BatchUtil.loadAndApplyBatch(batch, this, () -> callback.complete(null)); } @@ -67,11 +67,11 @@ class Minerun extends StatelessGame { @Override protected void onStart() { AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); - for(int x = 0; x <= width; x++) { - batch.setBlock(x, 50, preRun, Block.AIR); + for(int x = 0; x <= this.width; x++) { + batch.setBlock(x, 50, this.preRun, Block.AIR); } - BatchUtil.loadAndApplyBatch(batch, this, () -> playSound(Sound.sound(SoundEvent.BLOCK_WOOD_BREAK, Sound.Source.BLOCK, 1f, 1f))); + BatchUtil.loadAndApplyBatch(batch, this, () -> this.playSound(Sound.sound(SoundEvent.BLOCK_WOOD_BREAK, Sound.Source.BLOCK, 1f, 1f))); } @Override @@ -80,23 +80,23 @@ class Minerun extends StatelessGame { Player p = playerMoveEvent.getPlayer(); Pos middle = playerMoveEvent.getNewPosition(); - if(isBeforeBeginning && middle.z() > preRun+0.5) { //player cannot go forward before the game start + if(this.isBeforeBeginning && middle.z() > this.preRun + 0.5) { //player cannot go forward before the game start playerMoveEvent.setCancelled(true); } if(Intersect.withPressurePlate(this, BlockPallet.PRESSURE_PLATES, middle)) { //Player died p.playSound(Sound.sound(SoundEvent.ENTITY_GENERIC_EXPLODE, Sound.Source.PLAYER, 1f, 1f)); - p.teleport(new Pos(p.getPosition().x(), getSpawn().y(), getSpawn().z())); + p.teleport(new Pos(p.getPosition().x(), this.getSpawn().y(), this.getSpawn().z())); } - if(middle.z() > preRun + length + afterMines) { // Player finished - getScore().insertResult(p); + if(middle.z() > this.preRun + this.length + this.afterMines) { // Player finished + this.getScore().insertResult(p); p.setGameMode(GameMode.SPECTATOR); } } @Override public Pos getSpawn() { - return new Pos((double) width /2, 50, 3); + return new Pos((double) this.width / 2, 50, 3); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/minerun/MinerunFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/minerun/MinerunFactory.java index ef534f5..43ebdca 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/minerun/MinerunFactory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/minerun/MinerunFactory.java @@ -20,9 +20,9 @@ public class MinerunFactory implements GameFactory { @Override public ConfigManager configuration() { return new ConfigManager() - .addOption(new NumericOption("width", Material.OAK_FENCE, TranslatedComponent.byId("optionCommon#width"), 10, 30, 50, 100)) - .addOption(new NumericOption("length", Material.ZOMBIE_HEAD, TranslatedComponent.byId("optionCommon#length"), 50, 100, 150, 200)) - .addOption(new NumericOption("percentage", Material.LIGHT_WEIGHTED_PRESSURE_PLATE, TranslatedComponent.byId("game_Minerun#optionPercentageMines"), 30, 40, 50, 60, 70)); + .addOption(new NumericOption("width", Material.OAK_FENCE, TranslatedComponent.byId("optionCommon#width"), 10, 30, 50, 100)) + .addOption(new NumericOption("length", Material.ZOMBIE_HEAD, TranslatedComponent.byId("optionCommon#length"), 50, 100, 150, 200)) + .addOption(new NumericOption("percentage", Material.LIGHT_WEIGHTED_PRESSURE_PLATE, TranslatedComponent.byId("game_Minerun#optionPercentageMines"), 30, 40, 50, 60, 70)); } @Override diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/Knockback.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/Knockback.java new file mode 100644 index 0000000..ee6ab92 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/Knockback.java @@ -0,0 +1,18 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena; + +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.LivingEntity; + +/** + * Knockback in viewing direction of the attacker + */ +public record Knockback(double x, double z) { + public static Knockback of(Entity attacker) { + double yaw = Math.toRadians(attacker.getPosition().yaw()); + return new Knockback(Math.sin(yaw), -Math.cos(yaw)); + } + + public void apply(LivingEntity victim, float strength) { + victim.takeKnockback(strength, this.x, this.z); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/MobArena.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/MobArena.java new file mode 100644 index 0000000..0450e14 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/MobArena.java @@ -0,0 +1,342 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.MobType; +import eu.mhsl.minenet.minigames.message.type.ActionBarMessage; +import eu.mhsl.minenet.minigames.message.type.TitleMessage; +import eu.mhsl.minenet.minigames.score.PointsWinScore; +import eu.mhsl.minenet.minigames.util.BatchUtil; +import eu.mhsl.minenet.minigames.util.NumberUtil; +import eu.mhsl.minenet.minigames.world.BlockPallet; +import eu.mhsl.minenet.minigames.world.generator.terrain.CircularPlateTerrainGenerator; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.sound.Sound; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.ItemEntity; +import net.minestom.server.entity.LivingEntity; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.attribute.Attribute; +import net.minestom.server.entity.attribute.AttributeModifier; +import net.minestom.server.entity.attribute.AttributeOperation; +import net.minestom.server.entity.damage.Damage; +import net.minestom.server.entity.damage.DamageType; +import net.minestom.server.event.entity.EntityAttackEvent; +import net.minestom.server.event.entity.EntityDamageEvent; +import net.minestom.server.event.entity.EntityDeathEvent; +import net.minestom.server.event.item.PickupExperienceEvent; +import net.minestom.server.event.item.PickupItemEvent; +import net.minestom.server.instance.batch.AbsoluteBlockBatch; +import net.minestom.server.instance.block.Block; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.network.packet.server.play.ParticlePacket; +import net.minestom.server.particle.Particle; +import net.minestom.server.sound.SoundEvent; +import net.minestom.server.timer.TaskSchedule; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Duration; +import java.util.Collection; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; + +public class MobArena extends StatelessGame { + private static final int MAX_MOBS = 150; + private static final int SPAWN_INTERVAL_SECONDS = 2; + private static final float SWORD_DAMAGE = 6; + private static final double SWEEP_RANGE = 3; + private static final long ATTACK_COOLDOWN_MILLIS = 500; + private static final int WALL_HEIGHT = 3; + private static final Key SPEED_MODIFIER = Key.key("minenet", "mob_arena_speed"); + + private final int seconds; + private final int radius; + private final int difficulty; + private final int deathPenalty; + private final boolean powerUps; + private int elapsedSeconds = 0; + + private final Map points = new WeakHashMap<>(); + private final Map lastAttack = new WeakHashMap<>(); + private final Map strengthUntil = new WeakHashMap<>(); + private final Map speedUntil = new WeakHashMap<>(); + + public MobArena(int seconds, int radius, int difficulty, int deathPenalty, boolean powerUps) { + super(Dimension.OVERWORLD.key, "MobArena", new PointsWinScore()); + this.seconds = seconds; + this.radius = radius; + this.difficulty = difficulty; + this.deathPenalty = deathPenalty; + this.powerUps = powerUps; + this.setTime(18000); + this.defaultClock().pause(); + + this.setGenerator( + new CircularPlateTerrainGenerator(radius) + .setPlateHeight(1) + .setPlatePallet(BlockPallet.STONE) + ); + + this.eventNode() + .addListener(EntityAttackEvent.class, this::onPlayerAttack) + .addListener(EntityDamageEvent.class, this::onPlayerDamage) + .addListener(EntityDeathEvent.class, this::onMobDeath) + .addListener(PickupItemEvent.class, this::onPickupItem) + .addListener(PickupExperienceEvent.class, pickupExperienceEvent -> { + // Orbs sind nur Effekt, das Level zeigt die Punkte an + pickupExperienceEvent.setCancelled(true); + Knockback knockback = Knockback.of(pickupExperienceEvent.getPlayer()); + pickupExperienceEvent.getExperienceOrb().setVelocity(new Vec(knockback.x(), 0, knockback.z()).mul(5)); + }); + } + + @Override + protected void onLoad(@NotNull CompletableFuture callback) { + // Mauer am Plattformrand, damit weder Spieler noch Mobs die Arena verlassen + AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); + int outer = this.radius + 2; + for(int x = -outer; x <= outer; x++) { + for(int z = -outer; z <= outer; z++) { + double distance = new Pos(x, 0, z).distance(Pos.ZERO); + if(distance <= this.radius || distance > this.radius + 1.5) continue; + for(int y = 0; y <= WALL_HEIGHT; y++) batch.setBlock(x, y, z, Block.STONE_BRICKS); + } + } + BatchUtil.loadAndApplyBatch(batch, this, () -> callback.complete(null)); + } + + @Override + protected void onStart() { + this.getPlayers().forEach(player -> { + this.points.put(player, 0); + player.setLevel(0); + player.getInventory().clear(); + player.getInventory().addItemStack(ItemStack.of(Material.IRON_SWORD)); + player.setHealth(this.maxHealth(player)); + }); + + this.setTimeLimit(this.seconds); + + this.scheduler().submitTask(() -> { + if(!this.isRunning) return TaskSchedule.stop(); + int amount = (int) Math.ceil(this.getPlayers().size() * this.difficulty / 2d * this.escalation()); + for(int i = 0; i < amount; i++) this.spawnMob(MobType.getRandom().create()); + this.elapsedSeconds += SPAWN_INTERVAL_SECONDS; + return TaskSchedule.seconds(SPAWN_INTERVAL_SECONDS); + }); + + this.scheduler().submitTask(() -> { + if(!this.isRunning) return TaskSchedule.stop(); + this.getPlayers().forEach(player -> this.heal(player, 1)); + return TaskSchedule.seconds(3); + }); + } + + @Override + protected void onStop() { + this.points.forEach((player, points) -> this.getScore().insertResult(player, points)); + } + + private void onPlayerAttack(@NotNull EntityAttackEvent event) { + if(!(event.getEntity() instanceof Player attacker)) return; + if(!(event.getTarget() instanceof ArenaMob target)) return; + if(!this.isRunning) return; + + long now = System.currentTimeMillis(); + if(now - this.lastAttack.getOrDefault(attacker, 0L) < ATTACK_COOLDOWN_MILLIS) return; + this.lastAttack.put(attacker, now); + + float damage = now < this.strengthUntil.getOrDefault(attacker, 0L) ? SWORD_DAMAGE * 2 : SWORD_DAMAGE; + Collection nearby = this.getNearbyEntities(target.getPosition(), SWEEP_RANGE); + if(nearby.size() > 1) + ArenaMob.particle(this, Particle.SWEEP_ATTACK, target.getPosition().add(0, 1, 0), new Vec(0.5), (int) NumberUtil.map(nearby.size(), 1, 10, 1, 5)); + + Knockback knockback = Knockback.of(attacker); + for(Entity entity : nearby) { + if(!(entity instanceof ArenaMob mob) || mob.isDead()) continue; + double distance = mob.getDistance(target); + + mob.damage(Damage.fromPlayer(attacker, (float) NumberUtil.map(distance, SWEEP_RANGE, 0, 0, damage))); + knockback.apply(mob, (float) NumberUtil.map(distance, SWEEP_RANGE, 0, 0.1, 0.5)); + ArenaMob.particle(this, Particle.FALLING_OBSIDIAN_TEAR, mob.getPosition(), new Vec(knockback.x(), 0, knockback.z()), 10); + + this.scheduler().scheduleNextTick(() -> { + if(mob.isDead()) knockback.apply(mob, 1f); + }); + } + } + + private void onPlayerDamage(@NotNull EntityDamageEvent event) { + if(!(event.getEntity() instanceof Player player)) return; + if(!this.isRunning) { + event.setCancelled(true); + return; + } + if(event.getDamage().getAmount() < player.getHealth()) return; + + // Spieler sterben nicht wirklich, sondern verlieren Punkte und starten am Spawn neu + event.setCancelled(true); + this.addPoints(player, -this.deathPenalty); + player.teleport(this.getSpawn()); + player.setHealth(this.maxHealth(player)); + player.setInvulnerable(true); + player.scheduler().scheduleTask(() -> player.setInvulnerable(false), TaskSchedule.seconds(3), TaskSchedule.stop()); + + new TitleMessage(Duration.ofSeconds(1)) + .subtitle(subtitleMessage -> subtitleMessage.appendStatic(Component.text("-" + this.deathPenalty, NamedTextColor.RED))) + .appendTranslated("game_MobArena#death") + .send(player); + } + + private void onMobDeath(@NotNull EntityDeathEvent event) { + if(!(event.getEntity() instanceof ArenaMob mob)) return; + if(!this.isRunning) return; + if(mob.getLastDamageSource() == null) return; + if(!(mob.getLastDamageSource().getAttacker() instanceof Player killer)) return; + + this.addPoints(killer, mob.getPoints()); + this.heal(killer, 1); + if(this.powerUps && this.rnd.nextDouble() < mob.getDropChance()) this.dropPowerUp(mob.getPosition()); + } + + private void dropPowerUp(Pos pos) { + ItemEntity item = new ItemEntity(ItemStack.of(PowerUp.getRandom().getMaterial()).withGlowing(true)); + item.setPickupDelay(Duration.ofMillis(500)); + item.setInstance(this, pos.add(0, 0.5, 0)); + item.setVelocity(new Vec(0, 4, 0)); + item.scheduleRemove(Duration.ofSeconds(15)); + } + + private void onPickupItem(@NotNull PickupItemEvent event) { + if(!(event.getLivingEntity() instanceof Player player)) { + event.setCancelled(true); + return; + } + + PowerUp.byMaterial(event.getItemStack().material()).ifPresentOrElse(powerUp -> { + this.applyPowerUp(player, powerUp); + player.playSound(Sound.sound(SoundEvent.ENTITY_PLAYER_LEVELUP, Sound.Source.PLAYER, 0.5f, 1.5f)); + new ActionBarMessage().appendTranslated(powerUp.getTranslationKey()).send(player); + }, () -> event.setCancelled(true)); + } + + private void applyPowerUp(Player player, PowerUp powerUp) { + long until = System.currentTimeMillis() + PowerUp.DURATION_SECONDS * 1000L; + switch(powerUp) { + case HEAL -> this.heal(player, 8); + case STRENGTH -> this.strengthUntil.put(player, until); + case SPEED -> { + this.speedUntil.put(player, until); + player.getAttribute(Attribute.MOVEMENT_SPEED).removeModifier(SPEED_MODIFIER); + player.getAttribute(Attribute.MOVEMENT_SPEED).addModifier(new AttributeModifier(SPEED_MODIFIER, 0.4, AttributeOperation.ADD_MULTIPLIED_BASE)); + player.scheduler().scheduleTask(() -> { + // ein erneut aufgesammeltes Power-up verlaengert die Wirkung + if(System.currentTimeMillis() < this.speedUntil.getOrDefault(player, 0L)) return; + player.getAttribute(Attribute.MOVEMENT_SPEED).removeModifier(SPEED_MODIFIER); + }, TaskSchedule.seconds(PowerUp.DURATION_SECONDS), TaskSchedule.stop()); + } + } + } + + @Override + protected void onUnload() { + this.getPlayers().forEach(player -> player.getAttribute(Attribute.MOVEMENT_SPEED).removeModifier(SPEED_MODIFIER)); + } + + private void addPoints(Player player, int amount) { + int newPoints = Math.max(0, this.points.getOrDefault(player, 0) + amount); + this.points.put(player, newPoints); + player.setLevel(newPoints); + } + + private float maxHealth(Player player) { + return (float) player.getAttributeValue(Attribute.MAX_HEALTH); + } + + private void heal(Player player, float amount) { + player.setHealth(Math.min(this.maxHealth(player), player.getHealth() + amount)); + } + + public void spawnMob(ArenaMob mob) { + this.spawnMob(mob, this.randomSpawnPosition(), 0); + } + + /** + * Only living arena mobs count towards the limits, orbs, items, projectiles and corpses are ignored + */ + public void spawnMob(ArenaMob mob, Pos pos, int area) { + if(!this.isRunning) return; + int players = this.getPlayers().size(); + + int maxMobs = (int) Math.min(MAX_MOBS, (16 + 8 * players) * this.difficulty / 2d * this.escalation()); + long livingMobs = this.livingMobs(ArenaMob.class); + if(livingMobs >= maxMobs) return; + if(this.livingMobs(mob.getClass()) >= mob.maxAlive(players)) return; + + Pos spawnPos = area > 0 ? pos.add(this.rnd.nextInt(-area, area), 0, this.rnd.nextInt(-area, area)) : pos; + mob.setInstance(this, spawnPos).thenRun(() -> ArenaMob.particle(this, Particle.POOF, spawnPos, new Vec(1), 3)); + } + + /** + * Grows over the course of the round, more and more mobs spawn the longer the game runs + */ + private double escalation() { + return Math.min(2.5, 1 + this.elapsedSeconds / 90d); + } + + public float getDamageMultiplier() { + return switch(this.difficulty) { + case 1 -> 0.5f; + case 3 -> 1f; + default -> 0.75f; + }; + } + + private long livingMobs(Class type) { + return this.getEntities().stream() + .filter(type::isInstance) + .filter(entity -> !((ArenaMob) entity).isDead()) + .count(); + } + + /** + * Random position on the arena floor, preferably not right next to a player + */ + private Pos randomSpawnPosition() { + Pos pos = this.getSpawn(); + for(int i = 0; i < 10; i++) { + double angle = this.rnd.nextDouble(2 * Math.PI); + double distance = this.rnd.nextDouble(this.radius - 3); + pos = this.getSpawn().add(Math.cos(angle) * distance, 0, Math.sin(angle) * distance); + + Pos candidate = pos; + if(this.getPlayers().stream().noneMatch(player -> player.getPosition().distance(candidate) < 6)) break; + } + return pos; + } + + public void explode(Pos pos, @Nullable Entity source, @Nullable Entity attacker, float damage, double range) { + this.sendGroupedPacket(new ParticlePacket(Particle.EXPLOSION, pos.x(), pos.y(), pos.z(), 1, 1, 1, 0, 5)); + this.playSound(Sound.sound(SoundEvent.ENTITY_GENERIC_EXPLODE, Sound.Source.NEUTRAL, 0.5f, 1), pos.x(), pos.y(), pos.z()); + + for(Entity entity : this.getNearbyEntities(pos, range)) { + if(!(entity instanceof LivingEntity livingEntity) || livingEntity == source) continue; + float amount = (float) NumberUtil.map(entity.getDistance(pos), range, 0, 0, damage); + if(livingEntity instanceof Player) amount *= this.getDamageMultiplier(); + livingEntity.damage(new Damage(DamageType.EXPLOSION, source, attacker, pos, amount)); + } + } + + @Override + public Pos getSpawn() { + return new Pos(0.5, 1, 0.5); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/MobArenaFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/MobArenaFactory.java new file mode 100644 index 0000000..06a90bb --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/MobArenaFactory.java @@ -0,0 +1,51 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena; + +import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.BoolOption; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption; +import eu.mhsl.minenet.minigames.instance.room.Room; +import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import net.minestom.server.item.Material; + +import java.util.Map; + +public class MobArenaFactory implements GameFactory { + @Override + public TranslatedComponent name() { + return TranslatedComponent.byId("game_MobArena#name"); + } + + @Override + public TranslatedComponent description() { + return TranslatedComponent.byId("game_MobArena#description"); + } + + @Override + public Material symbol() { + return Material.ZOMBIE_HEAD; + } + + @Override + public ConfigManager configuration() { + return new ConfigManager() + .addOption(new NumericOption("seconds", Material.CLOCK, TranslatedComponent.byId("optionCommon#seconds"), 120, 180, 300, 60)) + .addOption(new NumericOption("radius", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#radius"), 30, 40, 50, 20)) + .addOption(new NumericOption("difficulty", Material.SKELETON_SKULL, TranslatedComponent.byId("game_MobArena#difficulty"), 2, 3, 1)) + .addOption(new NumericOption("deathPenalty", Material.TOTEM_OF_UNDYING, TranslatedComponent.byId("game_MobArena#deathPenalty"), 3, 5, 10, 0)) + .addOption(new BoolOption("powerUps", Material.GOLDEN_APPLE, TranslatedComponent.byId("game_MobArena#powerUps"))); + } + + @Override + public Game manufacture(Room parent, Map> configuration) { + return new MobArena( + configuration.get("seconds").getAsInt(), + configuration.get("radius").getAsInt(), + configuration.get("difficulty").getAsInt(), + configuration.get("deathPenalty").getAsInt(), + configuration.get("powerUps").getAsBoolean() + ).setParent(parent); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/PowerUp.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/PowerUp.java new file mode 100644 index 0000000..3743171 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/PowerUp.java @@ -0,0 +1,40 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena; + +import net.minestom.server.item.Material; + +import java.util.Arrays; +import java.util.Optional; +import java.util.Random; + +public enum PowerUp { + HEAL(Material.GOLDEN_APPLE, "game_MobArena#powerUp_heal"), + STRENGTH(Material.BLAZE_POWDER, "game_MobArena#powerUp_strength"), + SPEED(Material.SUGAR, "game_MobArena#powerUp_speed"); + + public static final int DURATION_SECONDS = 10; + private static final Random RANDOM = new Random(); + + private final Material material; + private final String translationKey; + + PowerUp(Material material, String translationKey) { + this.material = material; + this.translationKey = translationKey; + } + + public Material getMaterial() { + return this.material; + } + + public String getTranslationKey() { + return this.translationKey; + } + + public static Optional byMaterial(Material material) { + return Arrays.stream(values()).filter(powerUp -> powerUp.material == material).findFirst(); + } + + public static PowerUp getRandom() { + return values()[RANDOM.nextInt(values().length)]; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/ArenaMob.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/ArenaMob.java new file mode 100644 index 0000000..09a8ab4 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/ArenaMob.java @@ -0,0 +1,193 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.Knockback; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArena; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.component.DataComponents; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.EntityCreature; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.ExperienceOrb; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.attribute.Attribute; +import net.minestom.server.entity.attribute.AttributeInstance; +import net.minestom.server.entity.ai.goal.MeleeAttackGoal; +import net.minestom.server.entity.ai.goal.RandomLookAroundGoal; +import net.minestom.server.entity.ai.goal.RandomStrollGoal; +import net.minestom.server.entity.ai.target.ClosestEntityTarget; +import net.minestom.server.entity.damage.Damage; +import net.minestom.server.event.entity.EntityAttackEvent; +import net.minestom.server.event.entity.EntityDamageEvent; +import net.minestom.server.event.entity.EntityDeathEvent; +import net.minestom.server.instance.Instance; +import net.minestom.server.network.packet.server.play.ParticlePacket; +import net.minestom.server.particle.Particle; +import net.minestom.server.sound.SoundEvent; +import net.minestom.server.timer.TaskSchedule; +import net.minestom.server.utils.time.TimeUnit; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class ArenaMob extends EntityCreature { + private static final int MAX_ORBS = 8; + private static final int BLOCK_LENGTH = 6; + private static final List CHARACTERS = List.of("", "▏", "▎", "▍", "▌", "▋", "▊", "▉"); + private static final String FULL_BLOCK_CHAR = "█"; + + private float damage = 2; + private int experience = 10; + private int points = 1; + private double dropChance = 0.08; + + public ArenaMob(@NotNull EntityType entityType) { + super(entityType); + + this.set(DataComponents.CUSTOM_NAME, generateHealthBar(this.getMaxHealth(), this.getHealth())); + this.setCustomNameVisible(true); + this.setRemovalAnimationDelay(3500); + + this.aiGroup(); + + this.eventNode() + .addListener(EntityAttackEvent.class, this::onAttack) + .addListener(EntityDamageEvent.class, this::onDamage) + .addListener(EntityDeathEvent.class, this::onDeath); + } + + public static void particle(Instance instance, Particle particle, Point pos, Vec offset, int count) { + instance.sendGroupedPacket(new ParticlePacket(particle, pos.x(), pos.y(), pos.z(), (float) offset.x(), (float) offset.y(), (float) offset.z(), 0, count)); + } + + protected void particle(Particle particle, Point pos, float offset, int count) { + if(this.getInstance() == null) return; + particle(this.getInstance(), particle, pos, new Vec(offset), count); + } + + protected ClosestEntityTarget playerTarget() { + return new ClosestEntityTarget(this, 32, entity -> entity instanceof Player player && player.getGameMode() != GameMode.SPECTATOR); + } + + protected void aiGroup() { + this.addAIGroup( + List.of( + new MeleeAttackGoal(this, 1.2, 20, TimeUnit.SERVER_TICK), + new RandomLookAroundGoal(this, 30), + new RandomStrollGoal(this, 5) + ), + List.of(this.playerTarget()) + ); + } + + protected void onAttack(@NotNull EntityAttackEvent event) { + if(!(event.getTarget() instanceof Player target)) return; + + this.particle(Particle.LANDING_LAVA, target.getPosition(), 0.5f, 10); + Knockback.of(this).apply(target, 0.2f); + target.damage(Damage.fromEntity(this, this.scaledDamage(this.damage))); + } + + protected void onDamage(@NotNull EntityDamageEvent event) { + this.particle(Particle.DAMAGE_INDICATOR, this.getPosition(), 1, 5); + event.setSound(SoundEvent.BLOCK_AMETHYST_BLOCK_STEP); + } + + protected void onDeath(@NotNull EntityDeathEvent event) { + Instance instance = this.getInstance(); + this.particle(Particle.SOUL, this.getPosition(), 0.5f, 3); + + this.setCustomNameVisible(false); + + for(int i = 0; i < Math.min(this.experience, MAX_ORBS); i++) { + ExperienceOrb orb = new ExperienceOrb((short) 1); + orb.setNoGravity(true); + orb.setInstance(instance, this.getPosition().add(0, 0.1, 0)); + orb.setVelocity(new Vec(this.random(-5, 5), this.random(1, 3), this.random(-5, 5))); + + instance.scheduler().scheduleTask(() -> { + particle(instance, Particle.LAVA, orb.getPosition(), Vec.ZERO, 1); + orb.remove(); + }, TaskSchedule.millis((long) this.random(1000, 5000)), TaskSchedule.stop()); + } + + this.scheduler().scheduleTask(() -> this.teleport(this.getPosition().sub(0, 0.1, 0)), TaskSchedule.seconds(3), TaskSchedule.stop()); + } + + private double random(double min, double max) { + return min + Math.random() * (max - min); + } + + @Override + public void setHealth(float health) { + super.setHealth(health); + if(!this.isDead()) this.set(DataComponents.CUSTOM_NAME, generateHealthBar(this.getMaxHealth(), this.getHealth())); + } + + /** + * Damage against players, scaled by the arena difficulty + */ + protected float scaledDamage(float damage) { + if(this.getInstance() instanceof MobArena arena) return damage * arena.getDamageMultiplier(); + return damage; + } + + protected void setMaxHealth(float maxHealth) { + this.getAttribute(Attribute.MAX_HEALTH).setBaseValue(maxHealth); + this.setHealth(maxHealth); + } + + protected void multiplySpeed(double factor) { + AttributeInstance speed = this.getAttribute(Attribute.MOVEMENT_SPEED); + speed.setBaseValue(speed.getBaseValue() * factor); + } + + /** + * Maximum amount of living mobs of this type in the arena + */ + public int maxAlive(int players) { + return 6 + 4 * players; + } + + public double getDropChance() { + return this.dropChance; + } + + public void setDropChance(double dropChance) { + this.dropChance = dropChance; + } + + public float getMaxHealth() { + return (float) this.getAttributeValue(Attribute.MAX_HEALTH); + } + + public void setDamage(float damage) { + this.damage = damage; + } + + public void setExperience(int experience) { + this.experience = experience; + } + + public int getPoints() { + return this.points; + } + + public void setPoints(int points) { + this.points = points; + } + + private static @NotNull Component generateHealthBar(float maxHealth, float currentHealth) { + // Converts the health percentage into a number from 0-{blockLength} -- only 0 if the mob's health is 0 + final double charHealth = (currentHealth / maxHealth) * BLOCK_LENGTH; + return Component.text() + .append(Component.text(FULL_BLOCK_CHAR.repeat((int) Math.floor(charHealth)), NamedTextColor.DARK_RED)) + .append(Component.text( + CHARACTERS.get((int) Math.round((charHealth - Math.floor(charHealth)) * (CHARACTERS.size() - 1))), + NamedTextColor.GOLD + )) + .build(); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/MobArrow.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/MobArrow.java new file mode 100644 index 0000000..9629392 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/MobArrow.java @@ -0,0 +1,52 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob; + +import io.github.togar2.pvp.entity.projectile.CustomEntityProjectile; +import net.minestom.server.coordinate.Point; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.damage.Damage; +import net.minestom.server.entity.damage.DamageType; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; + +/** + * Arrow shot by arena mobs, only hits players and flies through other mobs + */ +public class MobArrow extends CustomEntityProjectile { + private final float damage; + + public MobArrow(@NotNull Entity shooter, float damage) { + super(shooter, EntityType.ARROW); + this.damage = damage; + } + + /** + * Velocity has to be set before spawning, otherwise the client never receives it + */ + public void shootAt(Point target, double power, double spread) { + Entity shooter = this.getShooter(); + if(shooter == null || shooter.getInstance() == null) return; + this.shootTo(target, power, spread); + this.setInstance(shooter.getInstance(), shooter.getPosition().add(0, shooter.getEyeHeight(), 0)); + } + + @Override + protected boolean canHit(Entity entity) { + return entity instanceof Player player && player.getGameMode() != GameMode.SPECTATOR; + } + + @Override + public boolean onHit(Entity entity) { + if(entity instanceof Player player) player.damage(new Damage(DamageType.ARROW, this, this.getShooter(), null, this.damage)); + return true; + } + + @Override + public boolean onStuck() { + this.scheduleRemove(Duration.ofSeconds(2)); + return false; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/MobType.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/MobType.java new file mode 100644 index 0000000..e035420 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/MobType.java @@ -0,0 +1,47 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Creeper; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Evoker; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Pillager; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Skeleton; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Slime; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Spider; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature.Zombie; + +import java.util.Arrays; +import java.util.Random; +import java.util.function.Supplier; + +public enum MobType { + ZOMBIE(Zombie::new, 30), + SKELETON(Skeleton::new, 14), + SPIDER(Spider::new, 14), + PILLAGER(Pillager::new, 12), + CREEPER(Creeper::new, 10), + SLIME(Slime::new, 10), + EVOKER(Evoker::new, 6); + + private static final Random RANDOM = new Random(); + private static final int TOTAL_WEIGHT = Arrays.stream(values()).mapToInt(type -> type.weight).sum(); + + private final Supplier factory; + private final int weight; + + MobType(Supplier factory, int weight) { + this.factory = factory; + this.weight = weight; + } + + public ArenaMob create() { + return this.factory.get(); + } + + public static MobType getRandom() { + int roll = RANDOM.nextInt(TOTAL_WEIGHT); + for(MobType type : values()) { + roll -= type.weight; + if(roll < 0) return type; + } + return ZOMBIE; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Creeper.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Creeper.java new file mode 100644 index 0000000..33a0af6 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Creeper.java @@ -0,0 +1,39 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArena; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.metadata.monster.CreeperMeta; +import net.minestom.server.event.entity.EntityAttackEvent; +import net.minestom.server.sound.SoundEvent; +import net.minestom.server.timer.TaskSchedule; +import org.jetbrains.annotations.NotNull; + +public class Creeper extends ArenaMob { + private boolean fusing = false; + + public Creeper() { + super(EntityType.CREEPER); + this.setMaxHealth(12); + this.setPoints(2); + } + + /** + * Ignites instead of attacking and explodes shortly after, no matter if the target is still close + */ + @Override + protected void onAttack(@NotNull EntityAttackEvent event) { + if(this.fusing) return; + this.fusing = true; + + ((CreeperMeta) this.getEntityMeta()).setState(CreeperMeta.State.FUSE); + this.getInstance().playSound(Sound.sound(SoundEvent.ENTITY_CREEPER_PRIMED, Sound.Source.HOSTILE, 1, 1), this.getPosition()); + + this.scheduler().scheduleTask(() -> { + if(this.isDead() || this.isRemoved()) return; + if(this.getInstance() instanceof MobArena arena) arena.explode(this.getPosition(), this, null, 8, 4); + this.remove(); + }, TaskSchedule.millis(1500), TaskSchedule.stop()); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Evoker.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Evoker.java new file mode 100644 index 0000000..9d72c32 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Evoker.java @@ -0,0 +1,121 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArena; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityCreature; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.ai.GoalSelector; +import net.minestom.server.entity.ai.goal.RangedAttackGoal; +import net.minestom.server.entity.metadata.monster.raider.EvokerMeta; +import net.minestom.server.entity.metadata.monster.raider.SpellcasterIllagerMeta; +import net.minestom.server.event.entity.EntityDamageEvent; +import net.minestom.server.particle.Particle; +import net.minestom.server.sound.SoundEvent; +import net.minestom.server.timer.TaskSchedule; +import net.minestom.server.utils.time.Cooldown; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; + +public class Evoker extends ArenaMob { + private boolean hasTotem; + + public Evoker() { + super(EntityType.EVOKER); + this.hasTotem = Math.random() < 0.5; + this.setMaxHealth(14); + this.setPoints(3); + this.setDropChance(0.3); + } + + @Override + protected void aiGroup() { + Random rnd = new Random(); + int range = rnd.nextInt(3, 16); + + // keeps distance to the target without actually shooting + RangedAttackGoal keepDistance = new RangedAttackGoal(this, Duration.ofDays(1), range, range, true, 0, 0); + keepDistance.setProjectileGenerator((shooter, to, power, spread) -> {}); + + this.addAIGroup( + List.of( + new ActionGoal(this, Duration.ofSeconds(25), target -> { + this.lookAt(target); + ((EvokerMeta) this.getEntityMeta()).setSpell(SpellcasterIllagerMeta.Spell.SUMMON_VEX); + + this.scheduler().scheduleTask(() -> { + if(this.isDead()) return; + if(this.getInstance() instanceof MobArena arena) { + int amount = rnd.nextInt(1, 3); + for(int i = 0; i < amount; i++) arena.spawnMob(new Vex(), this.getPosition().add(0, 2, 0), 3); + } + ((EvokerMeta) this.getEntityMeta()).setSpell(SpellcasterIllagerMeta.Spell.NONE); + }, TaskSchedule.seconds(2), TaskSchedule.stop()); + }), + keepDistance + ), + List.of(this.playerTarget()) + ); + } + + @Override + protected void onDamage(@NotNull EntityDamageEvent event) { + super.onDamage(event); + if(!this.hasTotem || event.getDamage().getAmount() < this.getHealth()) return; + + event.setCancelled(true); + this.hasTotem = false; + this.particle(Particle.TOTEM_OF_UNDYING, this.getPosition(), 2, 60); + this.getInstance().playSound(Sound.sound(SoundEvent.ITEM_TOTEM_USE, Sound.Source.HOSTILE, 0.5f, 1), this.getPosition()); + this.setHealth(6); + } + + private static final class ActionGoal extends GoalSelector { + private final Duration cooldown; + private final Consumer consumer; + private long lastAction; + + public ActionGoal(@NotNull EntityCreature entityCreature, @NotNull Duration cooldown, Consumer consumer) { + super(entityCreature); + this.cooldown = cooldown; + this.consumer = consumer; + } + + @Override + public boolean shouldStart() { + Entity target = this.entityCreature.getTarget(); + if(target == null) target = this.findTarget(); + if(target == null) return false; + if(Cooldown.hasCooldown(System.currentTimeMillis(), this.lastAction, this.cooldown)) return false; + this.entityCreature.setTarget(target); + return true; + } + + @Override + public void start() { + } + + @Override + public void tick(long time) { + if(!Cooldown.hasCooldown(time, this.lastAction, this.cooldown) && this.entityCreature.getTarget() != null) { + this.lastAction = time; + this.consumer.accept(this.entityCreature.getTarget()); + } + } + + @Override + public boolean shouldEnd() { + final Entity target = this.entityCreature.getTarget(); + return target == null || target.isRemoved() || Cooldown.hasCooldown(System.currentTimeMillis(), this.lastAction, this.cooldown); + } + + @Override + public void end() { + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Pillager.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Pillager.java new file mode 100644 index 0000000..02a1f04 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Pillager.java @@ -0,0 +1,15 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob; +import net.minestom.server.entity.EntityType; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; + +public class Pillager extends ArenaMob { + public Pillager() { + super(EntityType.PILLAGER); + this.setItemInMainHand(ItemStack.of(Material.STONE_AXE)); + this.setMaxHealth(12); + this.setDamage(3); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Skeleton.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Skeleton.java new file mode 100644 index 0000000..11490b7 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Skeleton.java @@ -0,0 +1,141 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.MobArrow; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.ai.GoalSelector; +import net.minestom.server.entity.ai.goal.RandomStrollGoal; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.sound.SoundEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class Skeleton extends ArenaMob { + private static final float ARROW_DAMAGE = 2; + + public Skeleton() { + super(EntityType.SKELETON); + this.setItemInMainHand(ItemStack.of(Material.BOW)); + this.setMaxHealth(10); + this.multiplySpeed(0.6); + this.setPoints(2); + } + + @Override + protected void aiGroup() { + this.addAIGroup( + List.of(new ArcherGoal(this), new RandomStrollGoal(this, 5)), + List.of(this.playerTarget()) + ); + } + + @Override + public int maxAlive(int players) { + return 3 + 2 * players; + } + + private void shoot(Entity target) { + if(this.getInstance() == null) return; + Pos to = target.getPosition().add(0, target.getEyeHeight() * 0.7, 0); + new MobArrow(this, this.scaledDamage(ARROW_DAMAGE)).shootAt(to, 1.6, 4); + this.getInstance().playSound(Sound.sound(SoundEvent.ENTITY_SKELETON_SHOOT, Sound.Source.HOSTILE, 1, 1), this.getPosition()); + } + + /** + * Keeps a medium distance to the target, backs off when it gets too close and shoots in fixed intervals + */ + private static final class ArcherGoal extends GoalSelector { + private static final double MIN_DISTANCE = 5; + private static final double MAX_DISTANCE = 12; + private static final double SHOOT_RANGE = 18; + private static final long DRAW_MILLIS = 1000; + private static final long COOLDOWN_MILLIS = 2500; + + private final Skeleton skeleton; + private long drawStart = 0; + private long lastShot = 0; + private long lastPathUpdate = 0; + private boolean retreating = false; + + ArcherGoal(@NotNull Skeleton skeleton) { + super(skeleton); + this.skeleton = skeleton; + } + + @Override + public boolean shouldStart() { + return this.findTarget() != null; + } + + @Override + public void start() { + } + + @Override + public void tick(long time) { + Entity target = this.findTarget(); + if(target == null) return; + this.entityCreature.setTarget(target); + + double distance = this.entityCreature.getDistance(target); + this.updateMovement(target, distance, time); + this.entityCreature.lookAt(target); + + // beim Zurückweichen wird nicht geschossen + if(this.retreating || distance > SHOOT_RANGE) { + this.stopDrawing(); + return; + } + + if(this.drawStart == 0) { + if(time - this.lastShot < COOLDOWN_MILLIS) return; + this.drawStart = time; + this.skeleton.refreshActiveHand(true, false, false); + } else if(time - this.drawStart >= DRAW_MILLIS) { + this.skeleton.shoot(target); + this.lastShot = time; + this.stopDrawing(); + } + } + + private void updateMovement(Entity target, double distance, long time) { + if(time - this.lastPathUpdate < 500) return; + this.lastPathUpdate = time; + + Pos position = this.entityCreature.getPosition(); + this.retreating = distance < MIN_DISTANCE; + if(distance > MAX_DISTANCE) { + this.entityCreature.getNavigator().setPathTo(target.getPosition()); + } else if(this.retreating) { + Vec away = position.sub(target.getPosition()).asVec().withY(0); + if(away.isZero()) away = new Vec(1, 0, 0); + this.entityCreature.getNavigator().setPathTo(position.add(away.normalize().mul(4))); + } else { + this.entityCreature.getNavigator().setPathTo(null); + } + } + + private void stopDrawing() { + if(this.drawStart == 0) return; + this.drawStart = 0; + this.skeleton.refreshActiveHand(false, false, false); + } + + @Override + public boolean shouldEnd() { + return this.findTarget() == null; + } + + @Override + public void end() { + this.stopDrawing(); + this.entityCreature.getNavigator().setPathTo(null); + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Slime.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Slime.java new file mode 100644 index 0000000..8776f31 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Slime.java @@ -0,0 +1,34 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArena; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.metadata.cube.SlimeMeta; +import net.minestom.server.event.entity.EntityDeathEvent; +import org.jetbrains.annotations.NotNull; + +public class Slime extends ArenaMob { + private final int size; + + public Slime() { + this(Math.random() < 0.25 ? 3 : 2); + } + + public Slime(int size) { + super(EntityType.SLIME); + this.size = size; + + ((SlimeMeta) this.getEntityMeta()).setSize(size); + this.setMaxHealth(size * size); + this.setDamage(size); + this.setExperience(size * 2); + } + + @Override + protected void onDeath(@NotNull EntityDeathEvent event) { + super.onDeath(event); + if(this.size <= 1 || !(this.getInstance() instanceof MobArena arena)) return; + + for(int i = 0; i < 2; i++) arena.spawnMob(new Slime(this.size - 1), this.getPosition(), 1); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Spider.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Spider.java new file mode 100644 index 0000000..44562ad --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Spider.java @@ -0,0 +1,12 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob; +import net.minestom.server.entity.EntityType; + +public class Spider extends ArenaMob { + public Spider() { + super(EntityType.SPIDER); + this.setMaxHealth(4); + this.setDamage(1); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Vex.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Vex.java new file mode 100644 index 0000000..fc3584e --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Vex.java @@ -0,0 +1,32 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob; +import net.minestom.server.entity.EntityType; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.particle.Particle; +import net.minestom.server.timer.TaskSchedule; + +public class Vex extends ArenaMob { + private static final int LIFETIME_SECONDS = 12; + + public Vex() { + super(EntityType.VEX); + this.setItemInMainHand(ItemStack.of(Material.GOLDEN_SWORD)); + this.setMaxHealth(4); + this.setDamage(1); + this.setExperience(2); + this.setDropChance(0); + + this.scheduler().scheduleTask(() -> { + if(this.isDead() || this.isRemoved()) return; + this.particle(Particle.POOF, this.getPosition(), 0.3f, 5); + this.remove(); + }, TaskSchedule.seconds(LIFETIME_SECONDS), TaskSchedule.stop()); + } + + @Override + public int maxAlive(int players) { + return 2 + players / 2; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Zombie.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Zombie.java new file mode 100644 index 0000000..8fb499f --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/mobArena/mob/creature/Zombie.java @@ -0,0 +1,75 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.creature; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.MobArena; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.mobArena.mob.ArenaMob; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.EquipmentSlot; +import net.minestom.server.entity.metadata.monster.zombie.ZombieMeta; +import net.minestom.server.event.entity.EntityDeathEvent; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; + +public class Zombie extends ArenaMob { + private enum Variant { + NORMAL, BABY, ARMORED, EXPLODER, EXPERIENCE; + + static Variant random() { + double roll = Math.random(); + if(roll < 0.10) return EXPLODER; + if(roll < 0.18) return EXPERIENCE; + if(roll < 0.33) return BABY; + if(roll < 0.45) return ARMORED; + return NORMAL; + } + } + + private final Variant variant = Variant.random(); + + public Zombie() { + super(EntityType.ZOMBIE); + this.setMaxHealth(14); + + switch(this.variant) { + case BABY -> { + ((ZombieMeta) this.getEntityMeta()).setBaby(true); + this.setMaxHealth(6); + this.multiplySpeed(1.4); + this.setDamage(1); + this.setPoints(2); + } + case ARMORED -> { + this.setEquipment(EquipmentSlot.HELMET, ItemStack.of(Material.IRON_HELMET)); + this.setEquipment(EquipmentSlot.CHESTPLATE, ItemStack.of(Material.IRON_CHESTPLATE)); + this.setItemInMainHand(ItemStack.of(Material.IRON_SWORD)); + this.setMaxHealth(24); + this.setDamage(3); + this.multiplySpeed(0.8); + this.setPoints(3); + this.setDropChance(0.25); + } + case EXPLODER -> { + this.setItemInMainHand(ItemStack.of(Material.TNT)); + this.setItemInOffHand(ItemStack.of(Material.FLINT_AND_STEEL)); + } + case EXPERIENCE -> { + this.setItemInMainHand(ItemStack.of(Material.EXPERIENCE_BOTTLE)); + this.setExperience(50); + this.setPoints(3); + this.setDropChance(0.5); + } + case NORMAL -> { + } + } + } + + @Override + protected void onDeath(@NotNull EntityDeathEvent event) { + super.onDeath(event); + if(this.variant == Variant.EXPLODER && this.getInstance() instanceof MobArena arena) { + Entity killer = this.getLastDamageSource() != null ? this.getLastDamageSource().getAttacker() : null; + arena.explode(this.getPosition(), this, killer, 7, 3); + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/pillars/Pillars.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/pillars/Pillars.java new file mode 100644 index 0000000..53d6ef6 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/pillars/Pillars.java @@ -0,0 +1,143 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.pillars; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; +import eu.mhsl.minenet.minigames.score.LastWinsScore; +import eu.mhsl.minenet.minigames.util.Position; +import io.github.togar2.pvp.feature.CombatFeatures; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.event.item.ItemDropEvent; +import net.minestom.server.event.player.PlayerBlockBreakEvent; +import net.minestom.server.event.player.PlayerBlockPlaceEvent; +import net.minestom.server.event.player.PlayerMoveEvent; +import net.minestom.server.instance.block.Block; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.timer.TaskSchedule; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +class Pillars extends StatelessGame { + private int spawnPosx = 0; + private int spawnPosz = 0; + private final int pillarSpacing = 10; + private final int pillarRowCount = 5; + private final int timeToNextItem = 5; + + private static final Set IGNORED_SUFFIXES = Set.of( + "_SWORD", "_PICKAXE", "_AXE", "_SHOVEL", "_HOE", + "_HELMET", "_CHESTPLATE", "_LEGGINGS", "_BOOTS", + "_BOAT", "_MINECART", "_BUCKET", "_SPAWN_EGG", + "_BANNER", "_CANDLE", "_PRESSURE_PLATE", "_TEMPLATE", + "_BUTTON", "_HARNESS" + ); + + private static final Set IGNORED_EXACT = Set.of( + Material.BOW, Material.CROSSBOW, Material.TRIDENT, Material.MACE, Material.SHIELD, + Material.SHEARS, Material.FLINT_AND_STEEL, Material.FISHING_ROD, Material.SADDLE, + Material.TURTLE_HELMET, Material.ELYTRA, + Material.LADDER, Material.VINE, Material.SCAFFOLDING, Material.TWISTING_VINES, Material.WEEPING_VINES, + Material.BAMBOO_RAFT, + Material.TNT, Material.SPAWNER, Material.END_PORTAL_FRAME, + Material.COMMAND_BLOCK, Material.CHAIN_COMMAND_BLOCK, Material.REPEATING_COMMAND_BLOCK, + Material.STRUCTURE_BLOCK, Material.STRUCTURE_VOID, Material.JIGSAW, + Material.BARRIER, Material.LIGHT, Material.DEBUG_STICK, Material.KNOWLEDGE_BOOK, + Material.BEDROCK, Material.AIR + ); + + private static final Set IGNORED_MATERIALS = new HashSet<>(); + + static { + for (Material material : Material.values()) { + String id = material.key().asString(); + + for (String suffix : IGNORED_SUFFIXES) { + if (id.endsWith(suffix.toLowerCase())) { + IGNORED_MATERIALS.add(material); + break; + } + } + } + } + + public Pillars() { + super(Dimension.THE_END.key, "Pillars", new LastWinsScore()); + this.getScore().setIgnoreLastPlayers(1); + + this.eventNode().addChild( + CombatFeatures.empty() + .add(CombatFeatures.VANILLA_ATTACK) + .add(CombatFeatures.VANILLA_DAMAGE) + .add(CombatFeatures.VANILLA_KNOCKBACK) + .build().createNode() + ); + } + + @Override + protected boolean onPlayerJoin(Player p) { + Pos pos = new Pos(this.spawnPosx * this.pillarSpacing, 100, this.spawnPosz * this.pillarSpacing); + this.setBlock(pos.sub(0, 1, 0), Block.BEDROCK); + + if(this.spawnPosx >= this.pillarRowCount) { + this.spawnPosx = 0; + this.spawnPosz++; + } + this.spawnPosx++; + MinecraftServer.getSchedulerManager().scheduleNextTick(() -> p.teleport(pos.add(0.5, 0, 0.5))); + + return super.onPlayerJoin(p); + } + + @Override + protected void onBlockPlace(@NotNull PlayerBlockPlaceEvent playerBlockPlaceEvent) {} + + @Override + protected void onBlockBreak(@NotNull PlayerBlockBreakEvent playerBlockBreakEvent) {} + + @Override + protected void onItemDrop(@NotNull ItemDropEvent itemDropEvent) {} + + @Override + public Pos getSpawn() { + return new Pos(0, 105, 0); + } + + @Override + protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { + var player = playerMoveEvent.getPlayer(); + + if(this.isBeforeBeginning && Position.hasPositionChanged(player.getPosition(), playerMoveEvent.getNewPosition())) + playerMoveEvent.setCancelled(true); + + if(playerMoveEvent.getNewPosition().y() < 80) { + this.getScore().insertResult(player); + player.teleport(this.getSpawn()); + player.setGameMode(GameMode.SPECTATOR); + } + } + + @Override + protected void onStart() { + this.getPlayers().forEach(player -> player.setGameMode(GameMode.SURVIVAL)); + + MinecraftServer.getSchedulerManager().submitTask(() -> { + List materials = Material.values().stream() + .filter(material -> !IGNORED_EXACT.contains(material)) + .filter(material -> !IGNORED_MATERIALS.contains(material)) + .toList(); + this.getPlayers().forEach(player -> { + ItemStack item = ItemStack.of(materials.get(ThreadLocalRandom.current().nextInt(materials.size()))); + player.getInventory().addItemStack(item); + }); + + return TaskSchedule.seconds(this.timeToNextItem); + }); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/pillars/PillarsFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/pillars/PillarsFactory.java new file mode 100644 index 0000000..22e03a2 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/pillars/PillarsFactory.java @@ -0,0 +1,32 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.pillars; + +import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.room.Room; +import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import net.minestom.server.item.Material; + +import java.util.Map; + +public class PillarsFactory implements GameFactory { + @Override + public TranslatedComponent name() { + return TranslatedComponent.byId("game_Pillars#name"); + } + + @Override + public TranslatedComponent description() { + return TranslatedComponent.byId("game_Pillars#description"); + } + + @Override + public Material symbol() { + return Material.BEDROCK; + } + + @Override + public Game manufacture(Room parent, Map> configuration) throws Exception { + return new Pillars().setParent(parent); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/PlayerRace.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/PlayerRace.java new file mode 100644 index 0000000..be6480f --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/PlayerRace.java @@ -0,0 +1,86 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; +import eu.mhsl.minenet.minigames.score.FirstWinsScore; +import eu.mhsl.minenet.minigames.util.BatchUtil; +import eu.mhsl.minenet.minigames.util.CommonProperties; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.event.player.PlayerMoveEvent; +import net.minestom.server.instance.batch.AbsoluteBlockBatch; +import net.minestom.server.instance.block.Block; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; + +public class PlayerRace extends StatelessGame { + private final int height = 64; + private final Map zLine = new WeakHashMap<>(); + private final Map zSpawnPoint = new WeakHashMap<>(); + private final int obstacleCount; + + public PlayerRace(int obstacleCount) { + super(Dimension.THE_END.key, "PlayerRace", new FirstWinsScore()); + this.setGenerator(new PlayerRaceChunkgenerator(this.height, obstacleCount)); + this.obstacleCount = obstacleCount; + } + + @Override + protected boolean onPlayerJoin(Player p) { + p.setGameMode(GameMode.ADVENTURE); + this.zLine.put(p, 32.0); + this.zSpawnPoint.put(p, 14.5); + return false; + } + + @Override + protected void onLoad(@NotNull CompletableFuture callback) { + AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); + for(int x = 0; x < 16; x++) { + batch.setBlock(x,this.height + 1, 15, Block.OAK_FENCE.withProperties(CommonProperties.fenceEastWest)); + }BatchUtil.loadAndApplyBatch(batch, this, () -> callback.complete(null)); + } + + @Override + protected void onStart() { + AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); + for(int x = 0; x < 16; x++) { + batch.setBlock(x,this.height + 1, 15, Block.AIR); + }BatchUtil.loadAndApplyBatch(batch, this, () -> {}); + } + + @Override + public Pos getSpawn() { + return new Pos(8, this.height + 1.5, 8); + } + + public Pos getNewSpawn(Player player) { + return new Pos(8, this.height + 1.5, this.zSpawnPoint.get(player)); + } + + @Override + protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { + var player = playerMoveEvent.getPlayer(); + if(playerMoveEvent.getNewPosition().y() < this.height - 5) { + player.teleport(this.isBeforeBeginning ? this.getSpawn() : this.getNewSpawn(player)); + return; + } + + if(playerMoveEvent.getNewPosition().z() >= this.zLine.get(player) && this.isRunning) { + this.zLine.merge(player, 19.0, Double::sum); + this.zSpawnPoint.merge(player, 19.0, Double::sum); + } + + if(playerMoveEvent.getNewPosition().z() >= 16 + this.obstacleCount * 19 && this.isRunning) { + this.getScore().insertResult(player); + player.setGameMode(GameMode.SPECTATOR); + } + + if(playerMoveEvent.getNewPosition().z() >= 16 && this.isBeforeBeginning) + player.teleport(this.getSpawn()); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/PlayerRaceChunkgenerator.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/PlayerRaceChunkgenerator.java new file mode 100644 index 0000000..16342cb --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/PlayerRaceChunkgenerator.java @@ -0,0 +1,61 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace; + +import eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles.Obstacle; +import eu.mhsl.minenet.minigames.util.GeneratorUtils; +import eu.mhsl.minenet.minigames.world.BlockPallet; +import eu.mhsl.minenet.minigames.world.generator.featureEnriched.ValeGenerator; +import eu.mhsl.minenet.minigames.world.generator.terrain.BaseGenerator; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +public class PlayerRaceChunkgenerator extends BaseGenerator { + private static final int START_LENGTH = 16; + private static final int GAP_LENGTH = 3; + private static final int PERIOD = Obstacle.LENGTH + GAP_LENGTH; + + public PlayerRaceChunkgenerator(int height, int obstacleCount) { + ValeGenerator vale = new ValeGenerator(); + vale.setXShiftMultiplier(integer -> 0.5d); + vale.setHeightNoiseMultiplier(integer -> 2); + this.addMixIn(vale); + + int finishStart = START_LENGTH + obstacleCount * PERIOD; + int finishEnd = finishStart + 16; + + this.addMixIn(unit -> { + Point chunkStart = unit.absoluteStart(); + if( + chunkStart.chunkX() != 0 || + chunkStart.blockZ() + 16 <= 0 || + chunkStart.blockZ() >= finishEnd + ) return; + + GeneratorUtils.foreachXZ(unit, point -> { + Block ground = groundBlockAt(point.blockZ(), finishStart, finishEnd); + if(ground != null) unit.modifier().setBlock(point.withY(height), ground); + }); + + for(int i = 0; i < obstacleCount; i++) { + int obstacleZ = START_LENGTH + i * PERIOD; + if(obstacleZ < chunkStart.blockZ() || obstacleZ >= chunkStart.blockZ() + 16) continue; + + GenerationUnit fork = unit.fork( + new Vec(chunkStart.blockX(), height - 8, obstacleZ), + new Vec(chunkStart.blockX() + 16, height + 16, obstacleZ + Obstacle.LENGTH) + ); + Obstacle.getRandomObstacle().generate(fork, new Vec(chunkStart.blockX(), height, obstacleZ)); + } + }); + } + + private static Block groundBlockAt(int z, int finishStart, int finishEnd) { + if(z < 0 || z >= finishEnd) return null; + if(z < START_LENGTH) return BlockPallet.GROUND.rnd(); + if(z >= finishStart) return Block.DIAMOND_BLOCK; + + int relative = (z - START_LENGTH) % PERIOD; + return relative >= Obstacle.LENGTH ? BlockPallet.GROUND.rnd() : null; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/PlayerRaceFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/PlayerRaceFactory.java new file mode 100644 index 0000000..0c0816d --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/PlayerRaceFactory.java @@ -0,0 +1,40 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace; + +import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption; +import eu.mhsl.minenet.minigames.instance.room.Room; +import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import net.minestom.server.item.Material; + +import java.util.Map; + +public class PlayerRaceFactory implements GameFactory { + @Override + public TranslatedComponent name() { + return TranslatedComponent.byId("game_PlayerRace#name"); + } + + @Override + public ConfigManager configuration() { + return new ConfigManager() + .addOption(new NumericOption("length", Material.RAIL, TranslatedComponent.byId("optionCommon#length"), 8, 12, 16, 20)); + } + + @Override + public Game manufacture(Room parent, Map> configuration) throws Exception { + return new PlayerRace(configuration.get("length").getAsInt()).setParent(parent); + } + + @Override + public Material symbol() { + return Material.DIRT_PATH; + } + + @Override + public TranslatedComponent description() { + return TranslatedComponent.byId("game_PlayerRace#description"); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/BambooThicket.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/BambooThicket.java new file mode 100644 index 0000000..13fe528 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/BambooThicket.java @@ -0,0 +1,30 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class BambooThicket extends Obstacle { + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + unit.modifier().setBlock(point.withY(start.blockY()), rnd.nextInt(3) == 0 ? Block.PODZOL : Block.GRASS_BLOCK); + + int z = relZ(point, start); + if(z < 2 || z > 13 || rnd.nextInt(100) >= 45) return; + + int height = 3 + rnd.nextInt(3); + for(int y = 1; y <= height; y++) { + String leaves = y == height ? "large" : y == height - 1 ? "small" : "none"; + unit.modifier().setBlock( + point.withY(start.blockY() + y), + Block.BAMBOO.withProperty("age", "1").withProperty("leaves", leaves) + ); + } + }); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/BasaltPillars.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/BasaltPillars.java new file mode 100644 index 0000000..121d0f7 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/BasaltPillars.java @@ -0,0 +1,44 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import de.articdive.jnoise.generators.noisegen.perlin.PerlinNoiseGenerator; +import de.articdive.jnoise.pipeline.JNoise; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class BasaltPillars extends Obstacle { + private final JNoise ground = JNoise.newBuilder() + .perlin(PerlinNoiseGenerator.newBuilder().build()) + .scale(0.13) + .build(); + + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + double noise = this.ground.evaluateNoise(point.blockX(), point.blockZ()); + Block floor = noise > 0.4 ? Block.SOUL_SOIL : noise < -0.4 ? Block.BASALT : Block.BLACKSTONE; + unit.modifier().setBlock(point.withY(start.blockY()), floor); + }); + + for(int i = 0; i < 16; i++) { + int x = rnd.nextInt(16); + int z = 2 + rnd.nextInt(12); + int height = 2 + rnd.nextInt(3); + boolean thick = rnd.nextInt(3) == 0 && x < 15; + Block pillar = rnd.nextInt(3) == 0 ? Block.POLISHED_BASALT : Block.BASALT; + for(int y = 1; y <= height; y++) { + unit.modifier().setBlock(start.add(x, y, z), pillar); + if(thick) { + unit.modifier().setBlock(start.add(x + 1, y, z), pillar); + } + } + if(rnd.nextInt(3) == 0) { + unit.modifier().setBlock(start.add(x, height + 1, z), Block.SOUL_LANTERN); + } + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/BerryPatch.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/BerryPatch.java new file mode 100644 index 0000000..be68a5b --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/BerryPatch.java @@ -0,0 +1,28 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class BerryPatch extends Obstacle { + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + int z = relZ(point, start); + int pathX = 7 + (int) Math.round(5 * Math.sin(z * Math.PI / 7.5)); + boolean onPath = relX(point, start) == pathX || relX(point, start) == pathX + 1; + boolean overgrown = z == 8; + + unit.modifier().setBlock(point.withY(start.blockY()), onPath ? Block.DIRT_PATH : Block.GRASS_BLOCK); + + boolean bush = onPath ? overgrown : z >= 1 && z <= 14 && rnd.nextInt(100) < 45; + if(bush) { + unit.modifier().setBlock(point.withY(start.blockY() + 1), Block.SWEET_BERRY_BUSH.withProperty("age", "3")); + } + }); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/BoulderTrench.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/BoulderTrench.java new file mode 100644 index 0000000..ada9800 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/BoulderTrench.java @@ -0,0 +1,40 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class BoulderTrench extends Obstacle { + private static final int[] DEPTHS = {0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0}; + + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + int depth = DEPTHS[relZ(point, start)]; + for(int y = start.blockY() - 2; y < start.blockY() - depth; y++) { + unit.modifier().setBlock(point.withY(y), Block.STONE); + } + Block floor = depth == 0 + ? Block.GRASS_BLOCK + : rnd.nextBoolean() ? Block.COARSE_DIRT : Block.GRAVEL; + unit.modifier().setBlock(point.withY(start.blockY() - depth), floor); + }); + + for(int i = 0; i < 10; i++) { + int x = rnd.nextInt(16); + int z = 6 + rnd.nextInt(4); + unit.modifier().setBlock(start.add(x, -1, z), rnd.nextBoolean() ? Block.MOSSY_COBBLESTONE : Block.COBBLESTONE); + if(rnd.nextInt(3) == 0) { + unit.modifier().setBlock(start.add(x, 0, z), Block.MOSSY_COBBLESTONE); + } + } + + for(int i = 0; i < 4; i++) { + unit.modifier().setBlock(start.add(rnd.nextInt(16), -1, 6 + rnd.nextInt(4)), Block.COBWEB); + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/ChorusGrove.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/ChorusGrove.java new file mode 100644 index 0000000..dea7dd2 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/ChorusGrove.java @@ -0,0 +1,65 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class ChorusGrove extends Obstacle { + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + Block floor = rnd.nextInt(14) == 0 ? Block.PURPUR_BLOCK : Block.END_STONE; + unit.modifier().setBlock(point.withY(start.blockY()), floor); + }); + + int[] rows = {5, 10}; + for(int i = 0; i < rows.length; i++) { + int gapStart = i % 2 == 0 ? 1 + rnd.nextInt(4) : 10 + rnd.nextInt(4); + + boolean[] hasWall = new boolean[16]; + for(int x = 0; x < 16; x++) { + hasWall[x] = x < gapStart || x >= gapStart + 2; + } + + for(int x = 0; x < 16; x++) { + if(!hasWall[x]) continue; + boolean westConnected = x > 0 && hasWall[x - 1]; + boolean eastConnected = x < 15 && hasWall[x + 1]; + Block wall = Block.END_STONE_BRICK_WALL + .withProperty("west", westConnected ? "low" : "none") + .withProperty("east", eastConnected ? "low" : "none") + .withProperty("up", String.valueOf(!westConnected || !eastConnected || x % 4 == 0)); + unit.modifier().setBlock(start.add(x, 1, rows[i]), wall); + + if(rnd.nextInt(6) == 0) { + unit.modifier().setBlock(start.add(x, 2, rows[i]), Block.END_ROD); + } + } + } + + for(int i = 0; i < 15; i++) { + int x = rnd.nextInt(16); + int z = 2 + rnd.nextInt(12); + if(z == 5 || z == 10) continue; + int height = 1 + rnd.nextInt(3); + for(int y = 1; y <= height; y++) { + unit.modifier().setBlock(start.add(x, y, z), Block.CHORUS_PLANT); + } + unit.modifier().setBlock(start.add(x, height + 1, z), Block.CHORUS_FLOWER.withProperty("age", "5")); + } + + for(int i = 0; i < 5; i++) { + int x = rnd.nextInt(16); + int z = 2 + rnd.nextInt(12); + if(z == 5 || z == 10) continue; + unit.modifier().setBlock(start.add(x, 1, z), Block.PURPUR_BLOCK); + if(rnd.nextBoolean()) { + unit.modifier().setBlock(start.add(x, 2, z), Block.PURPUR_BLOCK); + } + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/CobwebGrove.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/CobwebGrove.java new file mode 100644 index 0000000..a16231d --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/CobwebGrove.java @@ -0,0 +1,46 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import de.articdive.jnoise.generators.noisegen.perlin.PerlinNoiseGenerator; +import de.articdive.jnoise.pipeline.JNoise; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class CobwebGrove extends Obstacle { + private final JNoise soil = JNoise.newBuilder() + .perlin(PerlinNoiseGenerator.newBuilder().build()) + .scale(0.12) + .build(); + + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + Block floor = this.soil.evaluateNoise(point.blockX(), point.blockZ()) > 0 ? Block.PODZOL : Block.COARSE_DIRT; + unit.modifier().setBlock(point.withY(start.blockY()), floor); + + int z = relZ(point, start); + if(z < 2 || z > 13) return; + + int roll = rnd.nextInt(100); + if(roll < 28) { + unit.modifier().setBlock(point.withY(start.blockY() + 1), Block.COBWEB); + if(roll < 8) unit.modifier().setBlock(point.withY(start.blockY() + 2), Block.COBWEB); + } else if(roll < 36) { + unit.modifier().setBlock(point.withY(start.blockY() + 1), Block.FERN); + } + }); + + for(int i = 0; i < 4; i++) { + int x = rnd.nextInt(16); + int z = 3 + rnd.nextInt(10); + int height = 1 + rnd.nextInt(2); + for(int y = 1; y <= height; y++) { + unit.modifier().setBlock(start.add(x, y, z), Block.SPRUCE_LOG); + } + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/DripstoneSpikes.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/DripstoneSpikes.java new file mode 100644 index 0000000..e198d55 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/DripstoneSpikes.java @@ -0,0 +1,46 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class DripstoneSpikes extends Obstacle { + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> + unit.modifier().setBlock(point.withY(start.blockY()), rnd.nextInt(6) == 0 ? Block.STONE : Block.DRIPSTONE_BLOCK)); + + int[] rows = {4, 8, 12}; + for(int i = 0; i < rows.length; i++) { + int gapStart = i % 2 == 0 ? 2 + rnd.nextInt(4) : 9 + rnd.nextInt(4); + for(int x = 0; x < 16; x++) { + if(x >= gapStart && x < gapStart + 2) continue; + unit.modifier().setBlock(start.add(x, 1, rows[i]), Block.DRIPSTONE_BLOCK); + this.placeSpike(unit, start.add(0, 1, 0), x, rows[i], 1 + rnd.nextInt(2)); + } + } + + for(int i = 0; i < 12; i++) { + int x = rnd.nextInt(16); + int z = 2 + rnd.nextInt(12); + if(z == 4 || z == 8 || z == 12) continue; + this.placeSpike(unit, start, x, z, 1 + rnd.nextInt(3)); + } + } + + private void placeSpike(GenerationUnit unit, Point start, int x, int z, int height) { + for(int y = 1; y <= height; y++) { + String thickness = y == height ? "tip" : y == height - 1 ? "frustum" : "base"; + unit.modifier().setBlock( + start.add(x, y, z), + Block.POINTED_DRIPSTONE + .withProperty("thickness", thickness) + .withProperty("vertical_direction", "up") + ); + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/GlassPaneWeave.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/GlassPaneWeave.java new file mode 100644 index 0000000..17c4166 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/GlassPaneWeave.java @@ -0,0 +1,36 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class GlassPaneWeave extends Obstacle { + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> + unit.modifier().setBlock(point.withY(start.blockY()), rnd.nextInt(5) == 0 ? Block.CALCITE : Block.QUARTZ_BLOCK)); + + int[] rows = {4, 8, 12}; + for(int i = 0; i < rows.length; i++) { + int gapStart = i % 2 == 0 ? 2 + rnd.nextInt(3) : 11 + rnd.nextInt(3); + + boolean[] hasPane = new boolean[16]; + for(int x = 0; x < 16; x++) { + hasPane[x] = x < gapStart || x >= gapStart + 2; + } + + for(int x = 0; x < 16; x++) { + if(!hasPane[x]) continue; + Block pane = (rnd.nextInt(6) == 0 ? Block.GRAY_STAINED_GLASS_PANE : Block.WHITE_STAINED_GLASS_PANE) + .withProperty("west", String.valueOf(x > 0 && hasPane[x - 1])) + .withProperty("east", String.valueOf(x < 15 && hasPane[x + 1])); + unit.modifier().setBlock(start.add(x, 1, rows[i]), pane); + unit.modifier().setBlock(start.add(x, 2, rows[i]), pane); + } + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/HayHurdles.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/HayHurdles.java new file mode 100644 index 0000000..be3134f --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/HayHurdles.java @@ -0,0 +1,35 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class HayHurdles extends Obstacle { + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + unit.modifier().setBlock(point.withY(start.blockY()), Block.GRASS_BLOCK); + if(relZ(point, start) % 3 != 0 && rnd.nextInt(8) == 0) { + unit.modifier().setBlock(point.withY(start.blockY() + 1), Block.SHORT_GRASS); + } + }); + + int[] rows = {3, 6, 9, 12}; + for(int i = 0; i < rows.length; i++) { + boolean tall = i % 2 == 1; + int gapStart = rnd.nextInt(14); + for(int x = 0; x < 16; x++) { + if(tall && (x == gapStart || x == gapStart + 1)) continue; + Block block = rnd.nextInt(12) == 0 ? Block.CARVED_PUMPKIN : Block.HAY_BLOCK; + unit.modifier().setBlock(start.add(x, 1, rows[i]), block); + if(tall) { + unit.modifier().setBlock(start.add(x, 2, rows[i]), Block.HAY_BLOCK); + } + } + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/HoneyField.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/HoneyField.java new file mode 100644 index 0000000..daf2cb0 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/HoneyField.java @@ -0,0 +1,35 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import de.articdive.jnoise.generators.noisegen.perlin.PerlinNoiseGenerator; +import de.articdive.jnoise.pipeline.JNoise; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class HoneyField extends Obstacle { + private final JNoise sticky = JNoise.newBuilder() + .perlin(PerlinNoiseGenerator.newBuilder().build()) + .scale(0.2) + .build(); + + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + boolean stickyFloor = this.sticky.evaluateNoise(point.blockX(), point.blockZ()) > 0.35; + unit.modifier().setBlock(point.withY(start.blockY()), stickyFloor ? Block.HONEY_BLOCK : Block.HONEYCOMB_BLOCK); + }); + + for(int wallZ : new int[]{5, 10}) { + int gapStart = wallZ == 5 ? 1 + rnd.nextInt(5) : 8 + rnd.nextInt(5); + for(int x = 0; x < 16; x++) { + if(x >= gapStart && x < gapStart + 2) continue; + unit.modifier().setBlock(start.add(x, 1, wallZ), Block.HONEY_BLOCK); + unit.modifier().setBlock(start.add(x, 2, wallZ), Block.HONEY_BLOCK); + } + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/IceRink.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/IceRink.java new file mode 100644 index 0000000..a704570 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/IceRink.java @@ -0,0 +1,54 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import de.articdive.jnoise.generators.noisegen.perlin.PerlinNoiseGenerator; +import de.articdive.jnoise.pipeline.JNoise; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class IceRink extends Obstacle { + private final JNoise streaks = JNoise.newBuilder() + .perlin(PerlinNoiseGenerator.newBuilder().build()) + .scale(0.15) + .build(); + + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + double noise = this.streaks.evaluateNoise(point.blockX(), point.blockZ()); + Block floor = noise > 0.45 ? Block.BLUE_ICE : noise < -0.55 ? Block.ICE : Block.PACKED_ICE; + unit.modifier().setBlock(point.withY(start.blockY()), floor); + + if(relZ(point, start) >= 2 && relZ(point, start) <= 13 && rnd.nextInt(14) == 0) { + unit.modifier().setBlock( + point.withY(start.blockY() + 1), + Block.SNOW.withProperty("layers", String.valueOf(1 + rnd.nextInt(2))) + ); + } + }); + + for(int wallZ : new int[]{5, 10}) { + int gapStart = wallZ == 5 ? 1 + rnd.nextInt(4) : 9 + rnd.nextInt(4); + for(int x = 0; x < 16; x++) { + if(x >= gapStart && x < gapStart + 3) continue; + unit.modifier().setBlock(start.add(x, 1, wallZ), Block.PACKED_ICE); + unit.modifier().setBlock(start.add(x, 2, wallZ), Block.ICE); + if(rnd.nextInt(4) == 0) { + unit.modifier().setBlock(start.add(x, 3, wallZ), Block.SNOW.withProperty("layers", "1")); + } + } + } + + for(int i = 0; i < 5; i++) { + int x = rnd.nextInt(16); + int z = 2 + rnd.nextInt(12); + if(z == 5 || z == 10) continue; + unit.modifier().setBlock(start.add(x, 1, z), Block.PACKED_ICE); + unit.modifier().setBlock(start.add(x, 2, z), Block.SNOW.withProperty("layers", "1")); + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/LadderWall.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/LadderWall.java new file mode 100644 index 0000000..f1a2468 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/LadderWall.java @@ -0,0 +1,36 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import eu.mhsl.minenet.minigames.world.BlockPallet; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class LadderWall extends Obstacle { + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> + unit.modifier().setBlock(point.withY(start.blockY()), BlockPallet.STONE.rnd())); + + for(int x = 0; x < 16; x++) { + boolean hasLadder = rnd.nextInt(3) != 0; + for(int y = 1; y <= 3; y++) { + for(int z = 7; z <= 8; z++) { + Block block = rnd.nextInt(3) == 0 ? Block.MOSSY_STONE_BRICKS : Block.STONE_BRICKS; + unit.modifier().setBlock(start.add(x, y, z), block); + } + if(hasLadder) { + unit.modifier().setBlock(start.add(x, y, 6), Block.LADDER.withProperty("facing", "north")); + } + unit.modifier().setBlock(start.add(x, y, 9), Block.LADDER.withProperty("facing", "south")); + } + + if(x % 5 == 2) { + unit.modifier().setBlock(start.add(x, 4, 7), Block.LANTERN); + } + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/LadderWall2.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/LadderWall2.java new file mode 100644 index 0000000..b8fc13d --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/LadderWall2.java @@ -0,0 +1,48 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import eu.mhsl.minenet.minigames.world.BlockPallet; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.*; + +class LadderWall2 extends Obstacle{ + @Override + public void generate(GenerationUnit unit, Point start) { + forEachColumn(start, point -> + unit.modifier().setBlock(point.withY(start.blockY()), BlockPallet.STREET.rnd())); + + this.buildWall(unit, start,4, 4, false); + this.buildWall(unit, start,10, 8, true); + } + + private void buildWall(GenerationUnit unit, Point start, int z, int height, boolean lastRow) { + Random random = new Random(); + int y = 1; + for(int x = 0; x < 16; x++) { + for(int wy = 0; wy <= height; wy++) { + unit.modifier().setBlock(start.add(x, y + wy, z), BlockPallet.STONE.rnd()); + } + } + + for(int wy = lastRow ? 2 : 0; wy <= height; wy++) { + + Set noLadder = new HashSet<>(); + for(int i = 0; i < 5; i++) { + noLadder.add(random.nextInt(16)); + } + + for(int x = 0; x < 16; x++) { + if(!noLadder.contains(x)) + unit.modifier().setBlock(start.add(x, y + wy, z - 1), Block.LADDER); + } + + if(!lastRow) { + for(int x = 7; x < 9; x++) { + unit.modifier().setBlock(start.add(x, y + wy, z + 1), Block.LADDER.withProperty("facing", "south")); + } + } + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/LeafHedges.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/LeafHedges.java new file mode 100644 index 0000000..04f51f0 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/LeafHedges.java @@ -0,0 +1,35 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import eu.mhsl.minenet.minigames.world.BlockPallet; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class LeafHedges extends Obstacle { + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + unit.modifier().setBlock(point.withY(start.blockY()), Block.GRASS_BLOCK); + int z = relZ(point, start); + if(z % 4 != 0 && z >= 1 && z <= 14 && rnd.nextInt(10) == 0) { + unit.modifier().setBlock(point.withY(start.blockY() + 1), BlockPallet.FLOWER.rnd()); + } + }); + + int[] rows = {4, 8, 12}; + for(int i = 0; i < rows.length; i++) { + int gapStart = i % 2 == 0 ? 11 + rnd.nextInt(3) : 1 + rnd.nextInt(3); + for(int x = 0; x < 16; x++) { + if(x >= gapStart && x < gapStart + 2) continue; + Block leaves = (rnd.nextInt(10) < 3 ? Block.FLOWERING_AZALEA_LEAVES : Block.AZALEA_LEAVES) + .withProperty("persistent", "true"); + unit.modifier().setBlock(start.add(x, 1, rows[i]), leaves); + unit.modifier().setBlock(start.add(x, 2, rows[i]), leaves); + } + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/MudPools.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/MudPools.java new file mode 100644 index 0000000..9695713 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/MudPools.java @@ -0,0 +1,40 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import de.articdive.jnoise.generators.noisegen.perlin.PerlinNoiseGenerator; +import de.articdive.jnoise.pipeline.JNoise; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class MudPools extends Obstacle { + private final JNoise pools = JNoise.newBuilder() + .perlin(PerlinNoiseGenerator.newBuilder().build()) + .scale(0.11) + .build(); + + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + int z = relZ(point, start); + double noise = this.pools.evaluateNoise(point.blockX(), point.blockZ()); + boolean pool = z >= 1 && z <= 14 && noise > 0.05; + + if(pool) { + unit.modifier().setBlock(point.withY(start.blockY() - 1), Block.MUD); + return; + } + + Block floor = noise > -0.3 ? Block.MUDDY_MANGROVE_ROOTS : Block.GRASS_BLOCK; + unit.modifier().setBlock(point.withY(start.blockY() - 1), Block.MUD); + unit.modifier().setBlock(point.withY(start.blockY()), floor); + + if(z >= 2 && z <= 13 && rnd.nextInt(6) == 0) { + unit.modifier().setBlock(point.withY(start.blockY() + 1), Block.MANGROVE_ROOTS); + } + }); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/Mudpit.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/Mudpit.java new file mode 100644 index 0000000..0e677a8 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/Mudpit.java @@ -0,0 +1,25 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import de.articdive.jnoise.generators.noisegen.perlin.PerlinNoiseGenerator; +import de.articdive.jnoise.pipeline.JNoise; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +class Mudpit extends Obstacle { + private final JNoise curves = JNoise.newBuilder() + .perlin(PerlinNoiseGenerator.newBuilder().build()) + .scale(0.1) + .build(); + + @Override + public void generate(GenerationUnit unit, Point start) { + forEachColumn(start, point -> { + double relative = relZ(point, start); + double modifier = -1.5 * Math.cos(relative * Math.PI/7.5) + 1.5; + double heightAtBlock = start.blockY() + this.curves.evaluateNoise(point.blockX(), point.blockZ()) * modifier; + unit.modifier().setBlock(point.withY(heightAtBlock), Block.SOUL_SAND); + unit.modifier().setBlock(point.withY(heightAtBlock-1), Block.SOUL_SAND); + }); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/MushroomRoofs.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/MushroomRoofs.java new file mode 100644 index 0000000..a0dc290 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/MushroomRoofs.java @@ -0,0 +1,37 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class MushroomRoofs extends Obstacle { + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + unit.modifier().setBlock(point.withY(start.blockY()), Block.MYCELIUM); + int z = relZ(point, start); + if(z >= 1 && z <= 14 && rnd.nextInt(20) == 0) { + unit.modifier().setBlock(point.withY(start.blockY() + 1), rnd.nextBoolean() ? Block.RED_MUSHROOM : Block.BROWN_MUSHROOM); + } + }); + + for(int stemZ : new int[]{4, 8, 12}) { + int x = 4 + rnd.nextInt(8); + Block cap = rnd.nextBoolean() ? Block.RED_MUSHROOM_BLOCK : Block.BROWN_MUSHROOM_BLOCK; + + unit.modifier().setBlock(start.add(x, 1, stemZ), Block.MUSHROOM_STEM); + + for(int capX = x - 3; capX <= x + 3; capX++) { + for(int capZ = stemZ - 1; capZ <= stemZ + 1; capZ++) { + if(capX < 0 || capX > 15 || capZ < 0 || capZ > 15) continue; + if(Math.abs(capX - x) == 3 && Math.abs(capZ - stemZ) == 1) continue; + unit.modifier().setBlock(start.add(capX, 2, capZ), cap); + } + } + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/Obstacle.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/Obstacle.java new file mode 100644 index 0000000..c092b83 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/Obstacle.java @@ -0,0 +1,69 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import eu.mhsl.minenet.minigames.util.GeneratorUtils; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Consumer; + +public abstract class Obstacle { + public static final int LENGTH = 16; + + @SuppressWarnings("StaticInitializerReferencesSubClass") + private static final List obstacles = List.of( + new Mudpit(), + new IceRink(), + new HoneyField(), + new CobwebGrove(), + new BerryPatch(), + new WaterChannel(), + new HayHurdles(), + new SlimePatches(), + new MudPools(), + new PowderSnowField(), + new DripstoneSpikes(), + new ChorusGrove(), + new BasaltPillars(), + new WallSlalom(), + new LeafHedges(), + new MushroomRoofs(), + new SandDunes(), + new LadderWall(), + new LadderWall2(), + new GlassPaneWeave(), + new BoulderTrench(), + new BambooThicket() + ); + + public abstract void generate(GenerationUnit unit, Point start); + + private static final List remaining = new ArrayList<>(); + + public static synchronized Obstacle getRandomObstacle() { + if(remaining.isEmpty()) { + remaining.addAll(obstacles); + } + int index = ThreadLocalRandom.current().nextInt(remaining.size()); + return remaining.remove(index); + } + + protected static void forEachColumn(Point start, Consumer callback) { + GeneratorUtils.foreachXZ(start, start.add(15, 0, LENGTH - 1), callback); + } + + protected static int relX(Point point, Point start) { + return point.blockX() - start.blockX(); + } + + protected static int relZ(Point point, Point start) { + return point.blockZ() - start.blockZ(); + } + + protected static Random seededRandom(Point start) { + return new Random(start.blockX() * 341873128712L + start.blockZ() * 132897987541L); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/PowderSnowField.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/PowderSnowField.java new file mode 100644 index 0000000..a843a40 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/PowderSnowField.java @@ -0,0 +1,40 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import de.articdive.jnoise.generators.noisegen.perlin.PerlinNoiseGenerator; +import de.articdive.jnoise.pipeline.JNoise; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class PowderSnowField extends Obstacle { + private final JNoise drifts = JNoise.newBuilder() + .perlin(PerlinNoiseGenerator.newBuilder().build()) + .scale(0.14) + .build(); + + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + int z = relZ(point, start); + int laneX = 7 + (int) Math.round(3 * Math.sin(z * Math.PI / 7.5)); + boolean onLane = relX(point, start) == laneX || relX(point, start) == laneX + 1; + double noise = this.drifts.evaluateNoise(point.blockX(), point.blockZ()); + boolean drift = z >= 2 && z <= 13 && noise > (onLane ? 0.45 : 0.0); + + unit.modifier().setBlock(point.withY(start.blockY()), onLane ? Block.PACKED_ICE : Block.SNOW_BLOCK); + + if(drift) { + unit.modifier().setBlock(point.withY(start.blockY() + 1), Block.POWDER_SNOW); + if(noise > 0.6) { + unit.modifier().setBlock(point.withY(start.blockY() + 2), Block.POWDER_SNOW); + } + } else if(!onLane && rnd.nextInt(6) == 0) { + unit.modifier().setBlock(point.withY(start.blockY() + 1), Block.SNOW.withProperty("layers", "1")); + } + }); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/SandDunes.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/SandDunes.java new file mode 100644 index 0000000..bcad507 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/SandDunes.java @@ -0,0 +1,41 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import de.articdive.jnoise.generators.noisegen.perlin.PerlinNoiseGenerator; +import de.articdive.jnoise.pipeline.JNoise; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class SandDunes extends Obstacle { + private final JNoise dunes = JNoise.newBuilder() + .perlin(PerlinNoiseGenerator.newBuilder().build()) + .scale(0.08) + .build(); + + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + double taper = -1.25 * Math.cos(relZ(point, start) * Math.PI / 7.5) + 1.25; + double noise = (this.dunes.evaluateNoise(point.blockX(), point.blockZ()) + 1) / 2; + int top = start.blockY() + (int) Math.round(noise * taper); + + for(int y = start.blockY() - 1; y < top; y++) { + unit.modifier().setBlock(point.withY(y), Block.SANDSTONE); + } + unit.modifier().setBlock(point.withY(top), Block.SAND); + + if(top > start.blockY() + 1 && rnd.nextInt(14) == 0) { + unit.modifier().setBlock(point.withY(top + 1), Block.DEAD_BUSH); + } else if(top == start.blockY() && relZ(point, start) >= 2 && relZ(point, start) <= 13 && rnd.nextInt(22) == 0) { + int cactusHeight = 1 + rnd.nextInt(2); + for(int y = 1; y <= cactusHeight; y++) { + unit.modifier().setBlock(point.withY(top + y), Block.CACTUS); + } + } + }); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/SlimePatches.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/SlimePatches.java new file mode 100644 index 0000000..95031fe --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/SlimePatches.java @@ -0,0 +1,31 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import de.articdive.jnoise.generators.noisegen.perlin.PerlinNoiseGenerator; +import de.articdive.jnoise.pipeline.JNoise; +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class SlimePatches extends Obstacle { + private final JNoise blobs = JNoise.newBuilder() + .perlin(PerlinNoiseGenerator.newBuilder().build()) + .scale(0.18) + .build(); + + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + int z = relZ(point, start); + boolean slime = z >= 1 && z <= 14 && this.blobs.evaluateNoise(point.blockX(), point.blockZ()) > -0.2; + unit.modifier().setBlock(point.withY(start.blockY()), slime ? Block.SLIME_BLOCK : Block.MOSS_BLOCK); + + if(!slime && z >= 2 && z <= 13 && rnd.nextInt(18) == 0) { + unit.modifier().setBlock(point.withY(start.blockY() + 1), Block.AZALEA); + } + }); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/WallSlalom.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/WallSlalom.java new file mode 100644 index 0000000..0517deb --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/WallSlalom.java @@ -0,0 +1,38 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class WallSlalom extends Obstacle { + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> + unit.modifier().setBlock(point.withY(start.blockY()), rnd.nextInt(4) == 0 ? Block.ANDESITE : Block.STONE)); + + int[] rows = {4, 8, 12}; + for(int i = 0; i < rows.length; i++) { + int gapStart = i % 2 == 0 ? 1 + rnd.nextInt(3) : 10 + rnd.nextInt(3); + + boolean[] hasWall = new boolean[16]; + for(int x = 0; x < 16; x++) { + hasWall[x] = x < gapStart || x >= gapStart + 2; + } + + for(int x = 0; x < 16; x++) { + if(!hasWall[x]) continue; + boolean westConnected = x > 0 && hasWall[x - 1]; + boolean eastConnected = x < 15 && hasWall[x + 1]; + Block wall = Block.STONE_BRICK_WALL + .withProperty("west", westConnected ? "low" : "none") + .withProperty("east", eastConnected ? "low" : "none") + .withProperty("up", String.valueOf(!westConnected || !eastConnected || x % 4 == 0)); + unit.modifier().setBlock(start.add(x, 1, rows[i]), wall); + } + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/WaterChannel.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/WaterChannel.java new file mode 100644 index 0000000..c29b6bb --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/playerRace/obstacles/WaterChannel.java @@ -0,0 +1,37 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.playerRace.obstacles; + +import net.minestom.server.coordinate.Point; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.generator.GenerationUnit; + +import java.util.Random; + +class WaterChannel extends Obstacle { + private static final int[] DEPTHS = {0, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 0, 0}; + + @Override + public void generate(GenerationUnit unit, Point start) { + Random rnd = seededRandom(start); + + forEachColumn(start, point -> { + int depth = DEPTHS[relZ(point, start)]; + if(depth == 0) { + unit.modifier().setBlock(point.withY(start.blockY()), rnd.nextInt(3) == 0 ? Block.SMOOTH_SANDSTONE : Block.SAND); + return; + } + + Block bottom = rnd.nextInt(5) == 0 ? Block.DARK_PRISMARINE : Block.PRISMARINE; + unit.modifier().setBlock(point.withY(start.blockY() - depth), bottom); + for(int y = start.blockY() - depth + 1; y <= start.blockY(); y++) { + unit.modifier().setBlock(point.withY(y), Block.WATER); + } + + if(depth >= 2 && rnd.nextInt(4) == 0) { + unit.modifier().setBlock(point.withY(start.blockY() - depth + 1), Block.SEAGRASS); + } + if(rnd.nextInt(20) == 0) { + unit.modifier().setBlock(point.withY(start.blockY() + 1), Block.LILY_PAD); + } + }); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spaceSnake/SpaceSnake.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spaceSnake/SpaceSnake.java new file mode 100644 index 0000000..40d7208 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spaceSnake/SpaceSnake.java @@ -0,0 +1,185 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.spaceSnake; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; +import eu.mhsl.minenet.minigames.score.PointsWinScore; +import eu.mhsl.minenet.minigames.util.MaterialUtil; +import io.github.togar2.pvp.events.FinalAttackEvent; +import io.github.togar2.pvp.events.PrepareAttackEvent; +import io.github.togar2.pvp.feature.CombatFeatures; +import net.kyori.adventure.sound.Sound; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.metadata.other.FallingBlockMeta; +import net.minestom.server.event.entity.EntityTickEvent; +import net.minestom.server.event.player.PlayerBlockPlaceEvent; +import net.minestom.server.event.player.PlayerMoveEvent; +import net.minestom.server.instance.WorldBorder; +import net.minestom.server.instance.block.Block; +import net.minestom.server.inventory.PlayerInventory; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.sound.SoundEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class SpaceSnake extends StatelessGame { + private final Map playerStates = new WeakHashMap<>(); + private final Supplier posInBoundsH = () -> this.rnd.nextInt(-60, 300); + private int mapSize; + private final Supplier posInBoundsW = () -> this.rnd.nextInt(-this.mapSize / 2, this.mapSize / 2); + + public SpaceSnake(int mapSize, int powerUpCount) { + super(Dimension.THE_END.key, "spaceSnake", new PointsWinScore()); + this.mapSize = mapSize; + this.setWorldBorder(new WorldBorder(this.mapSize, 0, 0, 0, 0)); + + for(int i = 0; i < powerUpCount; i++) { + this.spawnPowerUp(); + } + + this.eventNode().addChild( + CombatFeatures.empty() + .add(CombatFeatures.VANILLA_ATTACK) + .add(CombatFeatures.VANILLA_DAMAGE) + .add(CombatFeatures.VANILLA_KNOCKBACK) + .build() + .createNode() + ); + + this.eventNode().addListener(PrepareAttackEvent.class, prepareAttackEvent -> { + if(this.isBeforeBeginning) prepareAttackEvent.setCancelled(true); + }); + + this.eventNode().addListener(FinalAttackEvent.class, finalAttackEvent -> { + finalAttackEvent.setBaseDamage(0); + ((Player) finalAttackEvent.getTarget()).setHealth(20); + }); + } + + @Override + protected void onStart() { + this.getPlayers().forEach(player -> { + player.setGameMode(GameMode.SURVIVAL); + this.updateInv(player); + player.setHeldItemSlot((byte) 1); + }); + } + + @Override + protected void onStop() { + this.getPlayers().forEach(player -> this.getScore().insertResult(player, this.playerStates.get(player).length.get())); + } + + @Override + protected boolean onPlayerJoin(Player p) { + Pos spawn = new Pos(this.posInBoundsW.get(), -60, this.posInBoundsW.get()); + PlayState state = new PlayState( + new AtomicInteger(3), + new ArrayDeque<>(List.of(spawn)), + MaterialUtil.getRandomFullBlock(material -> !material.equals(Material.DIAMOND_BLOCK)), + spawn + ); + this.playerStates.put(p, state); + this.setBlock(spawn, state.blockType.block()); + MinecraftServer.getSchedulerManager().scheduleNextTick( + () -> p.teleport(this.getSaveSpawn(spawn)) + ); + return super.onPlayerJoin(p); + } + + @Override + protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { + PlayState state = this.playerStates.get(playerMoveEvent.getPlayer()); + if(this.isBeforeBeginning) { + boolean falling = state.blocks.stream().anyMatch(pos -> pos.y() > playerMoveEvent.getNewPosition().y()); + if(falling) playerMoveEvent.getPlayer().teleport(this.getSaveSpawn(state.spawn)); + return; + } + + if(playerMoveEvent.getNewPosition().y() < -64) { + this.getScore().insertResult(playerMoveEvent.getPlayer(), state.length.get()); + playerMoveEvent.getPlayer().teleport(this.getSpawn()); + playerMoveEvent.getPlayer().setGameMode(GameMode.SPECTATOR); + long livingPlayers = this.getPlayers().stream() + .filter(p -> !this.getScore().hasResult(p)) + .count(); + if(livingPlayers == 1) this.setTimeLimit(10); + if(livingPlayers == 0) this.stop(); + } + } + + @Override + protected void onBlockPlace(@NotNull PlayerBlockPlaceEvent playerBlockPlaceEvent) { + if(this.isBeforeBeginning) { + playerBlockPlaceEvent.setCancelled(true); + return; + } + + PlayState state = this.playerStates.get(playerBlockPlaceEvent.getPlayer()); + state.blocks.add(new Pos(playerBlockPlaceEvent.getBlockPosition())); + state.cutToLength(pos -> this.setBlock(pos, Block.AIR)); + + MinecraftServer.getSchedulerManager().scheduleNextTick(() -> this.updateInv(playerBlockPlaceEvent.getPlayer())); + playerBlockPlaceEvent.getPlayer().setLevel(state.length.get()); + } + + private Pos getSaveSpawn(Pos blockPos) { + return blockPos.add(0.5).withY((y) -> y + 2); + } + + private void updateInv(Player player) { + PlayerInventory inventory = player.getInventory(); + inventory.clear(); + inventory.addItemStack(ItemStack.of(Material.STICK, 1).with(builder -> builder.glowing(true))); + inventory.addItemStack(ItemStack.of(this.playerStates.get(player).blockType, 64)); + } + + private void spawnPowerUp() { + Pos spawnPos = new Pos(this.posInBoundsW.get(), this.posInBoundsH.get(), this.posInBoundsW.get()); + Entity display = new Entity(EntityType.FALLING_BLOCK); + ((FallingBlockMeta) display.getEntityMeta()).setBlock(Block.DIAMOND_BLOCK); + display.setGlowing(true); + display.setNoGravity(true); + display.setInstance(this, spawnPos); + + display.eventNode().addListener(EntityTickEvent.class, onTick -> { + Player player = this.getPlayers().stream() + .filter(p -> !this.getScore().hasResult(p)) + .filter(p -> p.getBoundingBox() + .grow(1, 1, 1) + .intersectBox(display.getPosition().sub(p.getPosition()), display.getBoundingBox()) + ) + .findAny() + .orElse(null); + if(player == null) return; + + this.spawnPowerUp(); + display.remove(); + this.onPowerup(player); + }); + } + + private void onPowerup(Player player) { + PlayState state = this.playerStates.get(player); + state.length.incrementAndGet(); + player.setLevel(player.getLevel() + 1); + player.playSound(Sound.sound(SoundEvent.ENTITY_EXPERIENCE_ORB_PICKUP, Sound.Source.PLAYER, 1f, 1f)); + } + + record PlayState(AtomicInteger length, Queue blocks, Material blockType, Pos spawn) { + public void cutToLength(Consumer removed) { + while(this.blocks.size() > this.length.get()) { + removed.accept(this.blocks.poll()); + } + } + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spaceSnake/SpaceSnakeFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spaceSnake/SpaceSnakeFactory.java new file mode 100644 index 0000000..97a17a5 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spaceSnake/SpaceSnakeFactory.java @@ -0,0 +1,41 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.spaceSnake; + +import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption; +import eu.mhsl.minenet.minigames.instance.room.Room; +import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import net.minestom.server.item.Material; + +import java.util.Map; + +public class SpaceSnakeFactory implements GameFactory { + @Override + public TranslatedComponent name() { + return TranslatedComponent.byId("game_SpaceSnake#name"); + } + + @Override + public ConfigManager configuration() { + return new ConfigManager() + .addOption(new NumericOption("width", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#width"), 20, 30, 40, 50, 60, 70, 80)) + .addOption(new NumericOption("powerUpCount", Material.DIAMOND, TranslatedComponent.byId("game_SpaceSnake#powerUpCount"), 50, 100, 200, 300)); + } + + @Override + public Material symbol() { + return Material.GREEN_CONCRETE_POWDER; + } + + @Override + public TranslatedComponent description() { + return TranslatedComponent.byId("game_SpaceSnake#description"); + } + + @Override + public Game manufacture(Room parent, Map> configuration) throws Exception { + return new SpaceSnake(configuration.get("width").getAsInt(), configuration.get("powerUpCount").getAsInt()).setParent(parent); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spleef/Spleef.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spleef/Spleef.java index d0b5969..e2caf54 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spleef/Spleef.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spleef/Spleef.java @@ -9,44 +9,45 @@ import eu.mhsl.minenet.minigames.world.BlockPallet; import eu.mhsl.minenet.minigames.world.generator.terrain.CircularPlateTerrainGenerator; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.GameMode; +import net.minestom.server.event.player.PlayerBlockBreakEvent; import net.minestom.server.event.player.PlayerMoveEvent; import net.minestom.server.event.player.PlayerStartDiggingEvent; import net.minestom.server.instance.batch.AbsoluteBlockBatch; import net.minestom.server.instance.block.Block; -import net.minestom.server.item.*; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; import org.jetbrains.annotations.NotNull; import java.util.concurrent.CompletableFuture; public class Spleef extends StatelessGame { + final int heightPerLevel = 20; + final int totalElevation = 50; int radius; int stackCount; - final int heightPerLevel = 20; - final int totalElevation = 50; - public Spleef(int radius, int stackCount) { super(Dimension.OVERWORLD.key, "Spleef", new LastWinsScore()); - getScore().setIgnoreLastPlayers(1); + this.getScore().setIgnoreLastPlayers(1); this.radius = radius; this.stackCount = stackCount; - setGenerator(new CircularPlateTerrainGenerator(50)); + this.setGenerator(new CircularPlateTerrainGenerator(50)); - eventNode().addListener(PlayerStartDiggingEvent.class, this::destroyBlock); + this.eventNode().addListener(PlayerStartDiggingEvent.class, this::destroyBlock); } @Override protected void onLoad(@NotNull CompletableFuture callback) { AbsoluteBlockBatch circle = new AbsoluteBlockBatch(); - for (int level = 0; level < stackCount; level++) { - for(int x = -radius; x <= radius; x++) { - for(int z = -radius; z <= radius; z++) { - if(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > radius) continue; + for(int level = 0; level < this.stackCount; level++) { + for(int x = -this.radius; x <= this.radius; x++) { + for(int z = -this.radius; z <= this.radius; z++) { + if(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > this.radius) continue; - circle.setBlock(x, totalElevation + (level * heightPerLevel), z, BlockPallet.WINTER.rnd()); + circle.setBlock(x, this.totalElevation + (level * this.heightPerLevel), z, BlockPallet.WINTER.rnd()); } } } @@ -56,13 +57,13 @@ public class Spleef extends StatelessGame { @Override protected void onStart() { - getPlayers().forEach(player -> { + this.getPlayers().forEach(player -> { player.setGameMode(GameMode.SURVIVAL); player.getInventory().addItemStack( - ItemStack - .builder(Material.DIAMOND_SHOVEL) - .customName(TranslatedComponent.byId("game_Spleef#shovelName").getAssembled(player)) - .build() + ItemStack + .builder(Material.DIAMOND_SHOVEL) + .customName(TranslatedComponent.byId("game_Spleef#shovelName").getAssembled(player)) + .build() ); player.setHeldItemSlot((byte) 0); }); @@ -70,20 +71,29 @@ public class Spleef extends StatelessGame { @Override protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { - if(playerMoveEvent.getNewPosition().y() < totalElevation) { + if(playerMoveEvent.getNewPosition().y() < this.totalElevation) { playerMoveEvent.getPlayer().setGameMode(GameMode.SPECTATOR); playerMoveEvent.getPlayer().getInventory().clear(); - getScore().insertResult(playerMoveEvent.getPlayer()); + this.getScore().insertResult(playerMoveEvent.getPlayer()); } } + @Override + protected void onBlockBreak(@NotNull PlayerBlockBreakEvent event) { + if(!this.isRunning) { + event.setCancelled(true); + return; + } + this.setBlock(event.getBlockPosition(), Block.AIR); + } + private void destroyBlock(PlayerStartDiggingEvent event) { - if(!isRunning) return; - setBlock(event.getBlockPosition(), Block.AIR); + if(!this.isRunning) return; + this.setBlock(event.getBlockPosition(), Block.AIR); } @Override public Pos getSpawn() { - return new Pos(0, totalElevation + heightPerLevel * (stackCount-1) + 1, 0); + return new Pos(0, this.totalElevation + this.heightPerLevel * (this.stackCount - 1) + 1, 0); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spleef/SpleefFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spleef/SpleefFactory.java index 9d8a328..1fb459f 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spleef/SpleefFactory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/spleef/SpleefFactory.java @@ -22,7 +22,7 @@ public class SpleefFactory implements GameFactory { @Override public RestrictionHandler globalRestrictions() { return new RestrictionHandler() - .addRestriction(new MinimalPlayeramountGameRestriction(2)); + .addRestriction(new MinimalPlayeramountGameRestriction(2)); } @Override @@ -33,8 +33,8 @@ public class SpleefFactory implements GameFactory { @Override public ConfigManager configuration() { return new ConfigManager() - .addOption(new NumericOption("radius", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#radius"), 10, 20, 30)) - .addOption(new NumericOption("stackCount", Material.SCAFFOLDING, TranslatedComponent.byId("game_Spleef#stackCount"), 1, 2, 3, 4, 5)); + .addOption(new NumericOption("radius", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#radius"), 10, 20, 30)) + .addOption(new NumericOption("stackCount", Material.SCAFFOLDING, TranslatedComponent.byId("game_Spleef#stackCount"), 1, 2, 3, 4, 5)); } @Override diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/StickFightFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/StickFightFactory.java index f3be090..0193c3d 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/StickFightFactory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/StickFightFactory.java @@ -28,19 +28,20 @@ public class StickFightFactory implements GameFactory { @Override public ConfigManager configuration() { return new ConfigManager() - .addOption(new NumericOption("length", Material.SANDSTONE, TranslatedComponent.byId("optionCommon#length"), 5, 7, 9, 11)); + .addOption(new NumericOption("length", Material.SANDSTONE, TranslatedComponent.byId("optionCommon#length"), 7, 10, 13, 16, 19)) + .addOption(new NumericOption("seconds", Material.CLOCK, TranslatedComponent.byId("optionCommon#seconds"), 30, 60, 90, 120)); } @Override public RestrictionHandler globalRestrictions() { return new RestrictionHandler() - .addRestriction(new MinimalPlayeramountGameRestriction(2)) - .addRestriction(new MaximalPlayeramountGameRestriction(4)); + .addRestriction(new MinimalPlayeramountGameRestriction(2)) + .addRestriction(new MaximalPlayeramountGameRestriction(4)); } @Override public Game manufacture(Room parent, Map> configuration) { - return new Stickfight().setParent(parent); + return new Stickfight(configuration.get("length").getAsInt(), configuration.get("seconds").getAsInt()).setParent(parent); } @Override diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/Stickfight.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/Stickfight.java index d3c6d02..39596d2 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/Stickfight.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/stickfight/Stickfight.java @@ -2,7 +2,7 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.stickfight; import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; -import eu.mhsl.minenet.minigames.score.LastWinsScore; +import eu.mhsl.minenet.minigames.score.LowestPointsWinScore; import eu.mhsl.minenet.minigames.world.generator.terrain.CircularPlateTerrainGenerator; import io.github.togar2.pvp.events.FinalAttackEvent; import io.github.togar2.pvp.feature.CombatFeatures; @@ -10,20 +10,28 @@ import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerMoveEvent; import net.minestom.server.instance.block.Block; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; import org.jetbrains.annotations.NotNull; import java.util.List; +import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.CompletableFuture; public class Stickfight extends StatelessGame { - private final double radius = 20; + private final double radius; private final WeakHashMap spawnPoints = new WeakHashMap<>(); + private final Map scoreMap = new WeakHashMap<>(); + private boolean countdownStarted = false; + private final int seconds; - public Stickfight() { - super(Dimension.OVERWORLD.key, "Stickfight", new LastWinsScore()); + public Stickfight(int length, int seconds) { + super(Dimension.OVERWORLD.key, "Stickfight", new LowestPointsWinScore()); + this.radius = length; + this.seconds = seconds; - eventNode().addChild( + this.eventNode().addChild( CombatFeatures.empty() .add(CombatFeatures.VANILLA_ATTACK) .add(CombatFeatures.VANILLA_DAMAGE) @@ -31,66 +39,97 @@ public class Stickfight extends StatelessGame { .build().createNode() ); - eventNode().addListener(FinalAttackEvent.class, finalAttackEvent -> { + this.eventNode().addListener(FinalAttackEvent.class, finalAttackEvent -> { + if(this.isBeforeBeginning) finalAttackEvent.setCancelled(true); finalAttackEvent.setBaseDamage(0); ((Player) finalAttackEvent.getTarget()).setHealth(20); }); - setGenerator(new CircularPlateTerrainGenerator(20)); + this.setGenerator(new CircularPlateTerrainGenerator(20)); } @Override protected void onLoad(@NotNull CompletableFuture callback) { - setBlock(0, 50, 0, Block.DIAMOND_BLOCK); + this.replaceCircle(Block.SANDSTONE); + } + + private void replaceCircle(Block block) { + int radius = 8; + for(int x = -radius; x <= radius; x++) { + for(int z = -radius; z <= radius; z++) { + Pos blockPosition = this.getSpawn().add(x, -1, z); + if(blockPosition.distance(this.getSpawn().sub(0, 1, 0)) <= radius) this.setBlock(blockPosition, block); + } + } } @Override protected void start() { - List players = getPlayers().stream().toList(); + List players = this.getPlayers().stream().toList(); int numPlayers = players.size(); + this.countdownStarted = true; - for (int i = 0; i < numPlayers; i++) { + this.replaceCircle(Block.AIR); + for(int i = 0; i < numPlayers; i++) { double angle = (2 * Math.PI / numPlayers) * i; - int spawnX = (int) (radius * Math.cos(angle)); - int spawnZ = (int) (radius * Math.sin(angle)); + int spawnX = (int) (this.radius * Math.cos(angle)); + int spawnZ = (int) (this.radius * Math.sin(angle)); int spawnY = 50; Pos spawnpoint = new Pos(spawnX, spawnY + 1, spawnZ).add(0.5); - spawnPoints.put(players.get(i), spawnpoint.withLookAt(getSpawn())); + this.spawnPoints.put(players.get(i), spawnpoint.withLookAt(this.getSpawn())); players.get(i).teleport(spawnpoint); - generateBridge(spawnX, spawnY, spawnZ); + this.generateBridge(spawnX, spawnY, spawnZ); } - setBlock(0, 50, 0, Block.GOLD_BLOCK); + ItemStack item = ItemStack.of(Material.STICK).withGlowing(true); + players.forEach(player -> player.getInventory().addItemStack(item)); + + this.setBlock(0, 50, 0, Block.GOLD_BLOCK); super.start(); } + @Override + protected void onStart() { + this.setTimeLimit(this.seconds); + } + + @Override + protected void onStop() { + this.scoreMap.forEach((player, score) -> this.getScore().insertResult(player, score)); + } + private void generateBridge(int startX, int startY, int startZ) { - int steps = (int) (radius * 1.5); - for (int i = 0; i < steps; i++) { + int steps = (int) (this.radius * 1.5); + for(int i = 0; i < steps; i++) { double t = (double) i / steps; int x = (int) (startX * (1 - t)); int z = (int) (startZ * (1 - t)); - setBlock(x, startY, z, Block.SANDSTONE); + this.setBlock(x, startY, z, Block.SANDSTONE); } } @Override protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { - if(!spawnPoints.containsKey(playerMoveEvent.getPlayer())) { - playerMoveEvent.setCancelled(true); + Player player = playerMoveEvent.getPlayer(); + if(!this.spawnPoints.containsKey(player)) { + if(playerMoveEvent.getNewPosition().y() < 45) player.teleport(this.getSpawn()); + if(this.countdownStarted) playerMoveEvent.setCancelled(true); return; } - if(isBeforeBeginning) { - if(spawnPoints.get(playerMoveEvent.getPlayer()).distance(playerMoveEvent.getNewPosition()) < 1) return; + if(this.isBeforeBeginning) { + if(this.spawnPoints.get(player).distance(playerMoveEvent.getNewPosition()) < 1) return; playerMoveEvent.setCancelled(true); - playerMoveEvent.getPlayer().teleport(spawnPoints.get(playerMoveEvent.getPlayer())); + player.teleport(this.spawnPoints.get(player)); } if(playerMoveEvent.getNewPosition().y() < 40) { - playerMoveEvent.getPlayer().teleport(spawnPoints.get(playerMoveEvent.getPlayer())); + player.teleport(this.spawnPoints.get(player)); + this.scoreMap.putIfAbsent(player, 0); + this.scoreMap.put(player, this.scoreMap.get(player) + 1); + player.setLevel(this.scoreMap.get(player)); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/sumo/Sumo.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/sumo/Sumo.java new file mode 100644 index 0000000..26a3f7d --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/sumo/Sumo.java @@ -0,0 +1,129 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.sumo; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; +import eu.mhsl.minenet.minigames.score.LastWinsScore; +import io.github.togar2.pvp.events.FinalAttackEvent; +import io.github.togar2.pvp.events.PrepareAttackEvent; +import io.github.togar2.pvp.feature.CombatFeatures; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.event.player.PlayerMoveEvent; +import net.minestom.server.instance.block.Block; +import net.minestom.server.timer.TaskSchedule; +import org.jetbrains.annotations.NotNull; + +import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; + +public class Sumo extends StatelessGame { + int radius; + int health; + int seconds; + + int originalRadius; + int timer; + + WeakHashMap healthMap = new WeakHashMap<>(); + + public Sumo(int radius, int health, int seconds) { + super(Dimension.OVERWORLD.key, "Sumo", new LastWinsScore()); + this.getScore().setIgnoreLastPlayers(1); + this.setTime(6000); + this.defaultClock().pause(); + + this.radius = radius; + this.health = health; + this.seconds = seconds; + + this.originalRadius = radius; + this.timer = seconds; + + this.eventNode().addChild( + CombatFeatures.empty() + .add(CombatFeatures.VANILLA_ATTACK) + .add(CombatFeatures.VANILLA_DAMAGE) + .add(CombatFeatures.VANILLA_KNOCKBACK) + .build() + .createNode() + ); + + this.eventNode().addListener(PrepareAttackEvent.class, prepareAttackEvent -> { + if(this.isBeforeBeginning) + prepareAttackEvent.setCancelled(true); + }); + + this.eventNode().addListener(FinalAttackEvent.class, finalAttackEvent -> { + finalAttackEvent.setBaseDamage(0); + ((Player) finalAttackEvent.getTarget()).setHealth(20); + }); + } + + @Override + protected void start() { + this.getPlayers().forEach(player -> { + this.healthMap.put(player, this.health); + player.setLevel(this.healthMap.get(player)); + }); + + MinecraftServer.getSchedulerManager().scheduleTask( + () -> { + if(this.isBeforeBeginning) return TaskSchedule.seconds(1); + this.timer--; + double percent = (double) this.timer / this.seconds; + int radius = (int) (this.originalRadius * percent); + if(this.radius >= 5) { + this.radius = radius; + this.generatePlatform(); + return TaskSchedule.seconds(1); + } + + return TaskSchedule.stop(); + }, + TaskSchedule.seconds(1) + ); + + super.start(); + } + + @Override + protected void onLoad(@NotNull CompletableFuture callback) { + this.generatePlatform(); + } + + private void generatePlatform() { + int buffer = 5; + for(int x = -this.radius - buffer; x <= this.radius + buffer; x++) { + for(int z = -this.radius - buffer; z <= this.radius + buffer; z++) { + double distance = new Pos(x, 0, z).distance(new Pos(0, 0, 0)); + if(distance <= this.radius) { + boolean isEdge = this.radius - 1 < distance; + this.setBlock(x, 0, z, isEdge ? Block.RED_CONCRETE : Block.WHITE_CONCRETE); + } else { + this.setBlock(x, 0, z, Block.AIR); + } + } + } + } + + @Override + protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { + Player player = playerMoveEvent.getPlayer(); + if(playerMoveEvent.getNewPosition().y() < -10) { + player.teleport(this.getSpawn()); + this.healthMap.put(player, this.healthMap.get(player) - 1); + player.setLevel(this.healthMap.get(player)); + if(this.healthMap.get(player) == 0) { + this.getScore().insertResult(player); + player.setGameMode(GameMode.SPECTATOR); + } + } + } + + @Override + public Pos getSpawn() { + return new Pos(0, 2, 0); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/sumo/SumoFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/sumo/SumoFactory.java new file mode 100644 index 0000000..0cc6c26 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/sumo/SumoFactory.java @@ -0,0 +1,49 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.sumo; + +import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.RestrictionHandler; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.common.MinimalPlayeramountGameRestriction; +import eu.mhsl.minenet.minigames.instance.room.Room; +import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import net.minestom.server.item.Material; + +import java.util.Map; + +public class SumoFactory implements GameFactory { + @Override + public TranslatedComponent name() { + return TranslatedComponent.byId("game_Sumo#name"); + } + + public TranslatedComponent description() { + return TranslatedComponent.byId("game_Sumo#description"); + } + + @Override + public Material symbol() { + return Material.SLIME_BALL; + } + + @Override + public ConfigManager configuration() { + return new ConfigManager() + .addOption(new NumericOption("radius", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#radius"), 10, 20, 30)) + .addOption(new NumericOption("health", Material.GOLDEN_APPLE, TranslatedComponent.byId("game_Sumo#lives"), 1, 2, 3, 4, 5)) + .addOption(new NumericOption("seconds", Material.CLOCK, TranslatedComponent.byId("optionCommon#seconds"), 30, 60, 90, 120)); + } + + @Override + public Game manufacture(Room parent, Map> configuration) throws Exception { + return new Sumo(configuration.get("radius").getAsInt(), configuration.get("health").getAsInt(), configuration.get("seconds").getAsInt()).setParent(parent); + } + + @Override + public RestrictionHandler globalRestrictions() { + return new RestrictionHandler() + .addRestriction(new MinimalPlayeramountGameRestriction(2)); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/Tetris.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/Tetris.java index 11c25ac..b293b61 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/Tetris.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/Tetris.java @@ -1,34 +1,43 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris; +import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; import eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game.TetrisGame; -import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game.Tetromino; import eu.mhsl.minenet.minigames.score.PointsWinScore; import net.kyori.adventure.text.Component; +import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; -import net.minestom.server.event.player.*; +import net.minestom.server.entity.metadata.display.BlockDisplayMeta; +import net.minestom.server.event.player.PlayerHandAnimationEvent; +import net.minestom.server.event.player.PlayerTickEvent; +import net.minestom.server.event.player.PlayerUseItemEvent; +import net.minestom.server.instance.block.Block; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; +import net.minestom.server.timer.TaskSchedule; import org.jetbrains.annotations.NotNull; -import java.util.*; +import java.util.Map; +import java.util.Random; +import java.util.WeakHashMap; class Tetris extends StatelessGame { private final Map tetrisGames = new WeakHashMap<>(); private final int nextTetrominoesCount; private final boolean isFast; private final boolean hasCombat; - private boolean setTimeLimit = false; private final long randomSeed; + private boolean setTimeLimit = false; public Tetris(int nextTetrominoesCount, boolean isFast, boolean hasCombat) { super(Dimension.THE_END.key, "Tetris", new PointsWinScore()); - eventNode() + this.eventNode() .addListener(PlayerUseItemEvent.class, this::onPlayerInteract) .addListener(PlayerHandAnimationEvent.class, this::onPlayerAttack) .addListener(PlayerTickEvent.class, this::onPlayerTick); @@ -58,7 +67,7 @@ class Tetris extends StatelessGame { protected void onStop() { this.tetrisGames.forEach((player, tetrisGame) -> { tetrisGame.loose(); - getScore().insertResult(player, tetrisGame.getScore()); + this.getScore().insertResult(player, tetrisGame.getScore()); tetrisGame.sidebar.removeViewer(player); }); } @@ -66,21 +75,29 @@ class Tetris extends StatelessGame { @Override protected void onPlayerLeave(Player p) { this.tetrisGames.get(p).sidebar.removeViewer(p); + p.clearEffects(); this.letPlayerLoose(p); + p.setGameMode(GameMode.SURVIVAL); } - @Override - protected void onPlayerMove(@NotNull PlayerMoveEvent event) { + private void onPlayerInteract(@NotNull PlayerUseItemEvent event) { + event.setItemUseTime(0); + this.tetrisGames.get(event.getPlayer()).pressedButton(TetrisGame.Button.mouseRight); + } + + private void onPlayerAttack(@NotNull PlayerHandAnimationEvent event) { + this.tetrisGames.get(event.getPlayer()).pressedButton(TetrisGame.Button.mouseLeft); + } + + private void onPlayerTick(PlayerTickEvent event) { Player player = event.getPlayer(); - Pos currentPosition = event.getNewPosition(); - TetrisGame tetrisGame = this.tetrisGames.get(player); - - if(tetrisGame == null) { - event.setCancelled(true); + if(tetrisGame == null) return; + if(tetrisGame.lost) { + this.letPlayerLoose(player); return; } - if(tetrisGame.lost) return; + if(player.getGameMode() == GameMode.SPECTATOR) return; if(player.inputs().forward()) tetrisGame.pressedButton(TetrisGame.Button.W); @@ -88,41 +105,24 @@ class Tetris extends StatelessGame { if(player.inputs().right()) tetrisGame.pressedButton(TetrisGame.Button.D); if(player.inputs().left()) tetrisGame.pressedButton(TetrisGame.Button.A); if(player.inputs().jump()) tetrisGame.pressedButton(TetrisGame.Button.space); - - event.setNewPosition(tetrisGame.getPlayerSpawnPosition().withView(currentPosition)); - player.setSprinting(false); - } - - protected void onPlayerInteract(@NotNull PlayerUseItemEvent event) { - this.tetrisGames.get(event.getPlayer()).pressedButton(TetrisGame.Button.mouseRight); - } - - protected void onPlayerAttack(@NotNull PlayerHandAnimationEvent event) { - this.tetrisGames.get(event.getPlayer()).pressedButton(TetrisGame.Button.mouseLeft); - } - - protected void onPlayerTick(PlayerTickEvent event) { - Player player = event.getPlayer(); - TetrisGame tetrisGame = this.tetrisGames.get(player); - if(tetrisGame == null) return; - if(tetrisGame.lost && player.getGameMode() != GameMode.SPECTATOR) { - letPlayerLoose(player); - } } private void letPlayerLoose(Player player) { TetrisGame tetrisGame = this.tetrisGames.get(player); - player.setGameMode(GameMode.SPECTATOR); - player.setInvisible(true); - getScore().insertResult(player, tetrisGame.getScore()); + if(!this.getScore().hasResult(player)) { + player.setGameMode(GameMode.SPECTATOR); + player.setInvisible(true); + if(player.getVehicle() != null) player.getVehicle().removePassenger(player); + this.getScore().insertResult(player, tetrisGame.getScore()); + } boolean allGamesLost = this.tetrisGames.values().stream() .filter(game -> !game.lost) .toList() .isEmpty(); - if(!setTimeLimit && !allGamesLost) { + if(!this.setTimeLimit && !allGamesLost) { this.setTimeLimit(90); - setTimeLimit = true; + this.setTimeLimit = true; } } @@ -134,7 +134,7 @@ class Tetris extends StatelessGame { if(this.tetrisGames.get(p) == null) { this.tetrisGames.put(p, new TetrisGame( this, - getSpawn().sub(6, 8, 15).add(this.tetrisGames.size()*30, 0, 0), + this.getSpawn().sub(6, 8, 15).add(this.tetrisGames.size() * 30, 0, 0), Tetromino.Shape.J, this.nextTetrominoesCount, this.isFast, @@ -147,9 +147,18 @@ class Tetris extends StatelessGame { TetrisGame tetrisGame = this.tetrisGames.get(p); - p.teleport(tetrisGame.getPlayerSpawnPosition()); tetrisGame.sidebar.addViewer(p); + MinecraftServer.getSchedulerManager().scheduleTask(() -> { + p.teleport(tetrisGame.getPlayerSpawnPosition()); + Entity ghostBlock = new Entity(EntityType.BLOCK_DISPLAY); + ((BlockDisplayMeta) ghostBlock.getEntityMeta()).setBlockState(Block.AIR); + ghostBlock.setNoGravity(true); + ghostBlock.setInstance(this, tetrisGame.getPlayerSpawnPosition()); + ghostBlock.addPassenger(p); + return TaskSchedule.stop(); + }, TaskSchedule.nextTick()); + return super.onPlayerJoin(p); } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/TetrisFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/TetrisFactory.java index 56630f7..94d018b 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/TetrisFactory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/TetrisFactory.java @@ -1,9 +1,9 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris; import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; -import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.BoolOption; import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption; import eu.mhsl.minenet.minigames.instance.room.Room; diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Orientation.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Orientation.java new file mode 100644 index 0000000..efbc78b --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Orientation.java @@ -0,0 +1,17 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game; + +public enum Orientation { + NONE, + RIGHT, + LEFT, + UPSIDE_DOWN; + + public Orientation rotated(boolean clockwise) { + return switch(this) { + case NONE -> clockwise ? Orientation.RIGHT : Orientation.LEFT; + case RIGHT -> clockwise ? Orientation.UPSIDE_DOWN : Orientation.NONE; + case UPSIDE_DOWN -> clockwise ? Orientation.LEFT : Orientation.RIGHT; + case LEFT -> clockwise ? Orientation.NONE : Orientation.UPSIDE_DOWN; + }; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Playfield.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Playfield.java index edf4ddb..64dbc24 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Playfield.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Playfield.java @@ -10,10 +10,10 @@ import org.apache.commons.lang3.ArrayUtils; import java.util.Random; public class Playfield { - private final Pos lowerLeftCorner; - private final StatelessGame instance; private final static int height = 22; private final static Block scoreBlock = Block.STONE; + private final Pos lowerLeftCorner; + private final StatelessGame instance; private final int nextTetrominoesCount; private final Random random; @@ -41,19 +41,19 @@ public class Playfield { } public Pos getScorePosition() { - return this.lowerLeftCorner.add(-5, height+3, 0); + return this.lowerLeftCorner.add(-5, height + 3, 0); } public void generate() { AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); // actual playfield: - for(int x=0; x<12; x++) { - for(int y = 0; y< height; y++) { + for(int x = 0; x < 12; x++) { + for(int y = 0; y < height; y++) { batch.setBlock(this.lowerLeftCorner.add(x, y, 0), Block.GLASS); batch.setBlock(this.lowerLeftCorner.add(x, y, -1), Block.BLACK_CONCRETE); - if(x==0 || x==11 || y==0) { + if(x == 0 || x == 11 || y == 0) { batch.setBlock(this.lowerLeftCorner.add(x, y, 1), Block.GRAY_CONCRETE); batch.setBlock(this.lowerLeftCorner.add(x, y, 0), Block.GRAY_CONCRETE); } @@ -63,34 +63,35 @@ public class Playfield { // hold position: for(int x = 0; x < 4; x++) { for(int y = 0; y < 4; y++) { - batch.setBlock(this.getHoldPosition().add(x-1, y-1, -1), Block.QUARTZ_BLOCK); + batch.setBlock(this.getHoldPosition().add(x - 1, y - 1, -1), Block.QUARTZ_BLOCK); } } // next positions: for(int x = 0; x < 4; x++) { - for(int y = -4*this.nextTetrominoesCount+4; y < 4; y++) { - batch.setBlock(this.getNextPosition().add(x-1, y-1, -1), Block.QUARTZ_BLOCK); + for(int y = -4 * this.nextTetrominoesCount + 4; y < 4; y++) { + batch.setBlock(this.getNextPosition().add(x - 1, y - 1, -1), Block.QUARTZ_BLOCK); } } - batch.setBlock(getPlayerSpawnPosition().sub(0, 1, 0), Block.STONE); - batch.setBlock(getPlayerSpawnPosition().sub(1, 1, 0), Block.STONE); - batch.setBlock(getPlayerSpawnPosition().sub(1, 1, 1), Block.STONE); - batch.setBlock(getPlayerSpawnPosition().sub(0, 1, 1), Block.STONE); + batch.setBlock(this.getPlayerSpawnPosition().sub(0, 1, 0), Block.STONE); + batch.setBlock(this.getPlayerSpawnPosition().sub(1, 1, 0), Block.STONE); + batch.setBlock(this.getPlayerSpawnPosition().sub(1, 1, 1), Block.STONE); + batch.setBlock(this.getPlayerSpawnPosition().sub(0, 1, 1), Block.STONE); - BatchUtil.loadAndApplyBatch(batch, this.instance, () -> {}); + BatchUtil.loadAndApplyBatch(batch, this.instance, () -> { + }); } public int removeFullLines() { int removedLinesCounter = 0; - for(int y = 1; y< height; y++) { + for(int y = 1; y < height; y++) { boolean isFullLine = true; - for(int x=1; x<11; x++) { + for(int x = 1; x < 11; x++) { if(this.instance.getBlock(this.lowerLeftCorner.add(x, y, 1)) == Block.AIR) isFullLine = false; } if(isFullLine) { - removeFullLine(y); + this.removeFullLine(y); removedLinesCounter += 1; y -= 1; } @@ -99,11 +100,11 @@ public class Playfield { } public void addLines(int lines) { - int xPosMissing = random.nextInt(1, 10); + int xPosMissing = this.random.nextInt(1, 10); - for (int i = 0; i < lines; i++) { - moveAllLinesUp(); - for (int x = 1; x < 11; x++) { + for(int i = 0; i < lines; i++) { + this.moveAllLinesUp(); + for(int x = 1; x < 11; x++) { if(x != xPosMissing) { this.instance.setBlock(this.lowerLeftCorner.add(x, 1, 1), Block.LIGHT_GRAY_CONCRETE); } else { @@ -114,7 +115,7 @@ public class Playfield { } public void updateAttackingLines(int attackingLines) { - for (int y = 0; y < height + 5; y++) { + for(int y = 0; y < height + 5; y++) { if(attackingLines > 0) { this.instance.setBlock(this.lowerLeftCorner.add(12, y, 1), Block.REDSTONE_BLOCK); attackingLines -= 1; @@ -132,48 +133,49 @@ public class Playfield { for(int i = 6; i > 0; i--) { char digit; if(i <= characters.length) { - digit = characters[i-1]; + digit = characters[i - 1]; } else { digit = '0'; } - this.displayDigit(digit, 6-i); + this.displayDigit(digit, 6 - i); } } private void displayDigit(char digit, int positionFromLeft) { int[][] digitArray; - switch (digit) { - case '1' -> digitArray = new int[][]{{0,0,1},{0,1,1},{0,0,1},{0,0,1},{0,0,1}}; - case '2' -> digitArray = new int[][]{{1,1,1},{0,0,1},{1,1,1},{1,0,0},{1,1,1}}; - case '3' -> digitArray = new int[][]{{1,1,1},{0,0,1},{0,1,1},{0,0,1},{1,1,1}}; - case '4' -> digitArray = new int[][]{{1,0,1},{1,0,1},{1,1,1},{0,0,1},{0,0,1}}; - case '5' -> digitArray = new int[][]{{1,1,1},{1,0,0},{1,1,1},{0,0,1},{1,1,1}}; - case '6' -> digitArray = new int[][]{{1,1,1},{1,0,0},{1,1,1},{1,0,1},{1,1,1}}; - case '7' -> digitArray = new int[][]{{1,1,1},{0,0,1},{0,1,0},{0,1,0},{0,1,0}}; - case '8' -> digitArray = new int[][]{{1,1,1},{1,0,1},{1,1,1},{1,0,1},{1,1,1}}; - case '9' -> digitArray = new int[][]{{1,1,1},{1,0,1},{1,1,1},{0,0,1},{1,1,1}}; - default -> digitArray = new int[][]{{1,1,1},{1,0,1},{1,0,1},{1,0,1},{1,1,1}}; + switch(digit) { + case '1' -> digitArray = new int[][]{{0, 0, 1}, {0, 1, 1}, {0, 0, 1}, {0, 0, 1}, {0, 0, 1}}; + case '2' -> digitArray = new int[][]{{1, 1, 1}, {0, 0, 1}, {1, 1, 1}, {1, 0, 0}, {1, 1, 1}}; + case '3' -> digitArray = new int[][]{{1, 1, 1}, {0, 0, 1}, {0, 1, 1}, {0, 0, 1}, {1, 1, 1}}; + case '4' -> digitArray = new int[][]{{1, 0, 1}, {1, 0, 1}, {1, 1, 1}, {0, 0, 1}, {0, 0, 1}}; + case '5' -> digitArray = new int[][]{{1, 1, 1}, {1, 0, 0}, {1, 1, 1}, {0, 0, 1}, {1, 1, 1}}; + case '6' -> digitArray = new int[][]{{1, 1, 1}, {1, 0, 0}, {1, 1, 1}, {1, 0, 1}, {1, 1, 1}}; + case '7' -> digitArray = new int[][]{{1, 1, 1}, {0, 0, 1}, {0, 1, 0}, {0, 1, 0}, {0, 1, 0}}; + case '8' -> digitArray = new int[][]{{1, 1, 1}, {1, 0, 1}, {1, 1, 1}, {1, 0, 1}, {1, 1, 1}}; + case '9' -> digitArray = new int[][]{{1, 1, 1}, {1, 0, 1}, {1, 1, 1}, {0, 0, 1}, {1, 1, 1}}; + default -> digitArray = new int[][]{{1, 1, 1}, {1, 0, 1}, {1, 0, 1}, {1, 0, 1}, {1, 1, 1}}; } - for (int x = 0; x < 3; x++) { - for (int y = 0; y < 5; y++) { - if(digitArray[4-y][x] == 1) this.instance.setBlock(this.getScorePosition().add(positionFromLeft*4+x, y, 0), scoreBlock); + for(int x = 0; x < 3; x++) { + for(int y = 0; y < 5; y++) { + if(digitArray[4 - y][x] == 1) + this.instance.setBlock(this.getScorePosition().add(positionFromLeft * 4 + x, y, 0), scoreBlock); } } } private void removeDigits() { - for (int x = 0; x < 4 * 6; x++) { - for (int y = 0; y < 5; y++) { + for(int x = 0; x < 4 * 6; x++) { + for(int y = 0; y < 5; y++) { this.instance.setBlock(this.getScorePosition().add(x, y, 0), Block.AIR); } } } private void moveAllLinesUp() { - for (int y = height + 3; y > 1; y--) { - for (int x = 1; x < 11; x++) { + for(int y = height + 3; y > 1; y--) { + for(int x = 1; x < 11; x++) { Block blockBeneath = this.instance.getBlock(this.lowerLeftCorner.add(x, y - 1, 1)); this.instance.setBlock(this.lowerLeftCorner.add(x, y, 1), blockBeneath); } @@ -181,9 +183,9 @@ public class Playfield { } private void removeFullLine(int positionY) { - for(int y = positionY; y< height; y++) { - for(int x=1; x<11; x++) { - Block blockAbove = this.instance.getBlock(this.lowerLeftCorner.add(x, y+1, 1)); + for(int y = positionY; y < height; y++) { + for(int x = 1; x < 11; x++) { + Block blockAbove = this.instance.getBlock(this.lowerLeftCorner.add(x, y + 1, 1)); this.instance.setBlock(this.lowerLeftCorner.add(x, y, 1), blockAbove); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/RotationChecker.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/RotationChecker.java new file mode 100644 index 0000000..3f755a0 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/RotationChecker.java @@ -0,0 +1,53 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game; + +import java.util.Map; +import java.util.stream.IntStream; + +public final class RotationChecker { + private static final Map STANDARD_WALL_KICKS = Map.of( + Orientation.NONE, new int[][] {{0,0}, {0,0}, {0,0}, {0,0}, {0,0}}, + Orientation.RIGHT, new int[][] {{0,0}, {1,0}, {1,-1}, {0,2}, {1,2}}, + Orientation.UPSIDE_DOWN, new int[][] {{0,0}, {0,0}, {0,0}, {0,0}, {0,0}}, + Orientation.LEFT, new int[][] {{0,0}, {-1,0}, {-1,-1}, {0,2}, {-1,2}} + ); + + private static final Map I_WALL_KICKS = Map.of( + Orientation.NONE, new int[][] {{0,0}, {-1,0}, {2,0}, {-1,0}, {2,0}}, + Orientation.RIGHT, new int[][] {{-1,0}, {0,0}, {0,0}, {0,1}, {0,-2}}, + Orientation.UPSIDE_DOWN, new int[][] {{-1,1}, {1,1}, {-2,1}, {1,0}, {-2,0}}, + Orientation.LEFT, new int[][] {{0,1}, {0,1}, {0,1}, {0,-1}, {0,2}} + ); + + private static final Map O_WALL_KICKS = Map.of( + Orientation.NONE, new int[][] {{0,0}}, + Orientation.RIGHT, new int[][] {{0,-1}}, + Orientation.UPSIDE_DOWN, new int[][] {{-1,-1}}, + Orientation.LEFT, new int[][] {{-1,0}} + ); + + public static int[][] getKicksArray(Orientation from, Orientation to, Tetromino.Shape shape) { + int[][] firstOffsets = getOffsetData(from, shape); + int[][] secondOffsets = getOffsetData(to, shape); + int[][] result = new int[firstOffsets.length][2]; + + for(int i = 0; i < firstOffsets.length; i++) { + result[i] = subtractInt(firstOffsets[i], secondOffsets[i]); + } + + return result; + } + + private static int[] subtractInt(int[] first, int[] second) { + return IntStream.range(0, first.length) + .map(i -> first[i] - second[i]) + .toArray(); + } + + private static int[][] getOffsetData(Orientation orientation, Tetromino.Shape shape) { + return switch(shape) { + case J, L, S, T, Z -> STANDARD_WALL_KICKS.get(orientation); + case I -> I_WALL_KICKS.get(orientation); + case O -> O_WALL_KICKS.get(orientation); + }; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/TetrisGame.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/TetrisGame.java index 20d2902..e50c7ba 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/TetrisGame.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/TetrisGame.java @@ -2,10 +2,9 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.game; import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; import net.kyori.adventure.text.Component; -import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Pos; import net.minestom.server.scoreboard.Sidebar; -import net.minestom.server.timer.Scheduler; +import net.minestom.server.timer.Task; import net.minestom.server.timer.TaskSchedule; import java.util.*; @@ -14,36 +13,29 @@ public class TetrisGame { private final StatelessGame instance; private final Playfield playfield; private final boolean isFast; + private final boolean hasCombat; + private final List nextTetrominoes = new ArrayList<>(); + private final List tetrominoBag = new ArrayList<>(); + private final Pos nextPosition; + private final Pos holdPosition; + private final Pos tetrominoSpawnPosition; + private final Map lastPresses = new HashMap<>(); + private final List otherTetrisGames = new ArrayList<>(); + private final Random random; + private Task tetrominoLockTask; + private int lockDelayResets; + private int currentTetrominoLowestY = Integer.MAX_VALUE; + public boolean lost = false; + public boolean paused = true; + public Tetromino currentTetromino; + public Sidebar sidebar = new Sidebar(Component.text("Info:")); private int level = 1; private int lines = 0; private int score = 0; private int combo = 0; private int attackingLines = 0; - public boolean lost = false; - public boolean paused = true; - private final boolean hasCombat; - public Tetromino currentTetromino; - private final List nextTetrominoes = new ArrayList<>(); private Tetromino holdTetromino; - private final List tetrominoBag = new ArrayList<>(); private boolean holdPossible = true; - private final Pos nextPosition; - private final Pos holdPosition; - private final Pos tetrominoSpawnPosition; - public Sidebar sidebar = new Sidebar(Component.text("Info:")); - private final Map lastPresses = new HashMap<>(); - private final List otherTetrisGames = new ArrayList<>(); - private final Random random; - - public enum Button { - W, - A, - S, - D, - mouseLeft, - mouseRight, - space - } public TetrisGame(StatelessGame instance, Pos lowerLeftCorner, Tetromino.Shape startTetrominoShape, int nextTetrominoesCount, boolean isfast, boolean hasCombat, long randomSeed) { this.isFast = isfast; @@ -58,30 +50,33 @@ public class TetrisGame { this.buildSidebar(); this.currentTetromino = new Tetromino(this.instance, startTetrominoShape); - for (int i = 0; i < nextTetrominoesCount; i++) { + for(int i = 0; i < nextTetrominoesCount; i++) { this.updateNextTetrominoes(); } } public void pressedButton(Button button) { - final int standardButtonDelay = 100; - final int buttonDebounce = 70; + final int standardButtonDelay = 95; + final int wsButtonDebounce = 70; - if(this.lastPresses.getOrDefault(button, 0L) >= System.currentTimeMillis()-standardButtonDelay) return; + if(this.lastPresses.getOrDefault(button, 0L) >= System.currentTimeMillis() - standardButtonDelay) return; - this.lastPresses.put(button, System.currentTimeMillis()); - if(button == Button.W) this.lastPresses.put(button, System.currentTimeMillis()+buttonDebounce); - if(button == Button.S) this.lastPresses.put(button, System.currentTimeMillis()-buttonDebounce); + switch(button) { + case W -> this.lastPresses.put(button, System.currentTimeMillis() + wsButtonDebounce); + case S -> this.lastPresses.put(button, System.currentTimeMillis() - wsButtonDebounce); + case mouseLeft, mouseRight -> this.lastPresses.put(button, 0L); + default -> this.lastPresses.put(button, System.currentTimeMillis()); + } if(this.lost || this.paused) return; - switch (button) { - case A -> this.currentTetromino.moveLeft(); + switch(button) { + case A -> this.moveLeft(); case S -> this.moveDown(); - case D -> this.currentTetromino.moveRight(); + case D -> this.moveRight(); case W -> this.hardDrop(); - case mouseLeft -> this.currentTetromino.rotate(false); - case mouseRight -> this.currentTetromino.rotate(true); + case mouseLeft -> this.rotate(false); + case mouseRight -> this.rotate(true); case space -> this.switchHold(); } } @@ -92,13 +87,12 @@ public class TetrisGame { public void start() { this.paused = false; - Scheduler scheduler = MinecraftServer.getSchedulerManager(); - scheduler.submitTask(() -> { + this.instance.scheduler().submitTask(() -> { if(this.lost) return TaskSchedule.stop(); int standardTickDelay = 40; if(this.isFast) standardTickDelay = 20; - TaskSchedule nextTick = TaskSchedule.tick(Math.round((float) standardTickDelay /this.level)); + TaskSchedule nextTick = TaskSchedule.tick(Math.max(Math.round((float) standardTickDelay / this.level), 1)); if(this.paused) return nextTick; this.tick(); return nextTick; @@ -107,7 +101,7 @@ public class TetrisGame { this.updateInfo(); this.nextTetrominoes.forEach(tetromino -> { double xChange = -tetromino.getXChange(); - tetromino.setPosition(this.nextPosition.sub(xChange, 4*this.nextTetrominoes.indexOf(tetromino), 0)); + tetromino.setPosition(this.nextPosition.sub(xChange, 4 * this.nextTetrominoes.indexOf(tetromino), 0)); tetromino.drawAsEntities(); }); } @@ -121,9 +115,12 @@ public class TetrisGame { public void tick() { if(this.lost || this.paused) return; - if(!currentTetromino.moveDown()) { - setActiveTetrominoDown(); + if(this.currentTetromino.moveDown()) { + this.stopTetrominoLockTask(this.currentTetromino.getYPosition() < this.currentTetrominoLowestY, false); + } else { + this.scheduleTetrominoLock(); } + this.currentTetrominoLowestY = Math.min(this.currentTetrominoLowestY, this.currentTetromino.getYPosition()); } public int getScore() { @@ -149,34 +146,43 @@ public class TetrisGame { this.lost = true; } - - private boolean moveDown() { + private void moveDown() { if(!this.currentTetromino.moveDown()) { - this.setActiveTetrominoDown(); - return false; + this.scheduleTetrominoLock(); + return; } + this.stopTetrominoLockTask(this.currentTetromino.getYPosition() < this.currentTetrominoLowestY, false); this.score += 1; this.updateInfo(); - return true; } - private boolean hardDrop() { + private void moveLeft() { + if(this.currentTetromino.moveLeft()) this.onSuccessfulMoveOrRotate(); + } + + private void moveRight() { + if(this.currentTetromino.moveRight()) this.onSuccessfulMoveOrRotate(); + } + + private void rotate(boolean clockwise) { + if(this.currentTetromino.rotate(clockwise)) this.onSuccessfulMoveOrRotate(); + } + + private void hardDrop() { if(!this.currentTetromino.moveDown()) { - this.setActiveTetrominoDown(); - return false; + this.lockActiveTetromino(); + return; } - this.score += 2; - this.updateInfo(); - while(this.currentTetromino.moveDown()) { + do { this.score += 2; - this.updateInfo(); - } - this.setActiveTetrominoDown(); - return true; + } while(this.currentTetromino.moveDown()); + this.updateInfo(); + this.lockActiveTetromino(); } - private boolean switchHold() { - if(!holdPossible) return false; + private void switchHold() { + if(!this.holdPossible) return; + this.stopTetrominoLockTask(true); Tetromino newCurrentTetromino; if(this.holdTetromino == null) { @@ -192,17 +198,22 @@ public class TetrisGame { this.holdTetromino = new Tetromino(this.instance, this.currentTetromino.getShape()); this.currentTetromino = newCurrentTetromino; + this.currentTetrominoLowestY = Integer.MAX_VALUE; this.currentTetromino.setPosition(this.tetrominoSpawnPosition); this.currentTetromino.draw(); - if(!this.currentTetromino.moveDown()) loose(); + if(!this.currentTetromino.moveDown()) this.loose(); double xChange = this.holdTetromino.getXChange(); this.holdTetromino.setPosition(this.holdPosition.add(xChange, 0, 0)); this.holdTetromino.drawAsEntities(); this.holdPossible = false; - return true; } + private void onSuccessfulMoveOrRotate() { + if(!this.currentTetromino.isGrounded()) return; + this.stopTetrominoLockTask(false); + this.scheduleTetrominoLock(); + } private void updateNextTetrominoes() { if(this.tetrominoBag.isEmpty()) { @@ -217,7 +228,7 @@ public class TetrisGame { this.nextTetrominoes.add(newTetromino); this.nextTetrominoes.forEach(tetromino -> { double xChange = -tetromino.getXChange(); - tetromino.setPosition(this.nextPosition.sub(xChange, 4*this.nextTetrominoes.indexOf(tetromino), 0)); + tetromino.setPosition(this.nextPosition.sub(xChange, 4 * this.nextTetrominoes.indexOf(tetromino), 0)); tetromino.drawAsEntities(); }); } @@ -242,21 +253,49 @@ public class TetrisGame { private void updateInfo() { this.playfield.updateScore(this.score); - this.sidebar.updateLineScore("0", this.score); + this.sidebar.updateLineScore("0", this.score); this.sidebar.updateLineScore("1", this.lines); this.sidebar.updateLineScore("2", this.level); } - private void setActiveTetrominoDown() { + private void scheduleTetrominoLock() { + if(this.tetrominoLockTask == null || !this.tetrominoLockTask.isAlive()) + this.tetrominoLockTask = this.instance.scheduler().scheduleTask(() -> { + if(this.currentTetromino.isGrounded()) { + this.lockActiveTetromino(); + } else { + this.stopTetrominoLockTask(false, false); + } + return TaskSchedule.stop(); + }, TaskSchedule.millis(500)); + } + + private void stopTetrominoLockTask(boolean resetHard) { + this.stopTetrominoLockTask(resetHard, true); + } + + private void stopTetrominoLockTask(boolean resetHard, boolean addToResets) { + if(resetHard) this.lockDelayResets = 0; + + if(this.lockDelayResets >= 15 && addToResets) { + this.lockActiveTetromino(); + return; + } + if(this.tetrominoLockTask != null) { + this.tetrominoLockTask.cancel(); + this.tetrominoLockTask = null; + if(!resetHard && addToResets) this.lockDelayResets++; + } + } + + private void lockActiveTetromino() { + this.stopTetrominoLockTask(true); this.currentTetromino.removeOwnEntities(); - this.currentTetromino = this.nextTetrominoes.removeFirst(); - this.currentTetromino.remove(); - this.updateNextTetrominoes(); int removedLines = this.playfield.removeFullLines(); int combatLines = 0; this.combo += 1; - switch (removedLines) { + switch(removedLines) { case 0 -> this.combo = 0; case 1 -> { this.lines += 1; @@ -281,7 +320,7 @@ public class TetrisGame { this.score += 50 * this.combo * this.level; if(this.combo >= 2) { - combatLines += (int) Math.floor((double) this.combo /2); + combatLines += (int) Math.floor((double) this.combo / 2); } if(this.hasCombat && this.attackingLines > 0) { @@ -309,10 +348,25 @@ public class TetrisGame { this.updateInfo(); + this.currentTetromino = this.nextTetrominoes.removeFirst(); + this.currentTetromino.remove(); + this.updateNextTetrominoes(); + this.currentTetrominoLowestY = Integer.MAX_VALUE; + this.currentTetromino.setPosition(this.tetrominoSpawnPosition); this.currentTetromino.draw(); if(!this.currentTetromino.moveDown()) { - loose(); + this.loose(); } } + + public enum Button { + W, + A, + S, + D, + mouseLeft, + mouseRight, + space + } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Tetromino.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Tetromino.java index f673961..6d8b0b0 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Tetromino.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tetris/game/Tetromino.java @@ -8,43 +8,31 @@ import net.minestom.server.entity.metadata.other.FallingBlockMeta; import net.minestom.server.instance.block.Block; import net.minestom.server.tag.Tag; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; +import java.util.*; public class Tetromino { + private final static EntityType ghostEntityType = EntityType.FALLING_BLOCK; + private final static Tag uuidTag = Tag.String("uuid"); + private Orientation orientation = Orientation.NONE; private final Shape shape; private final StatelessGame instance; + private final UUID uuid; private Pos position; private int[][] shapeArray; - private final static EntityType ghostEntityType = EntityType.FALLING_BLOCK; - private final UUID uuid; - private final static Tag uuidTag = Tag.String("uuid"); - - public enum Shape { - I, - J, - L, - O, - S, - T, - Z - } public Tetromino(StatelessGame instance, Shape shape) { this.instance = instance; this.shape = shape; this.uuid = UUID.randomUUID(); - switch (this.shape) { - case I -> shapeArray = new int[][]{{0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}}; - case J -> shapeArray = new int[][]{{1,0,0}, {1,1,1}, {0,0,0}}; - case L -> shapeArray = new int[][]{{0,0,1}, {1,1,1}, {0,0,0}}; - case O -> shapeArray = new int[][]{{1,1}, {1,1}}; - case S -> shapeArray = new int[][]{{0,1,1}, {1,1,0}, {0,0,0}}; - case T -> shapeArray = new int[][]{{0,1,0}, {1,1,1}, {0,0,0}}; - case Z -> shapeArray = new int[][]{{1,1,0}, {0,1,1}, {0,0,0}}; + switch(this.shape) { + case I -> this.shapeArray = new int[][]{{0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 1, 1, 1, 1}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}}; + case J -> this.shapeArray = new int[][]{{1, 0, 0}, {1, 1, 1}, {0, 0, 0}}; + case L -> this.shapeArray = new int[][]{{0, 0, 1}, {1, 1, 1}, {0, 0, 0}}; + case O -> this.shapeArray = new int[][]{{0, 1, 1}, {0, 1, 1}, {0, 0, 0}}; + case S -> this.shapeArray = new int[][]{{0, 1, 1}, {1, 1, 0}, {0, 0, 0}}; + case T -> this.shapeArray = new int[][]{{0, 1, 0}, {1, 1, 1}, {0, 0, 0}}; + case Z -> this.shapeArray = new int[][]{{1, 1, 0}, {0, 1, 1}, {0, 0, 0}}; } } @@ -53,27 +41,38 @@ public class Tetromino { } public void setPosition(Pos newPosition) { - this.position = newPosition; + this.position = new Pos(newPosition.x(), newPosition.y(), newPosition.z()); } public boolean rotate(boolean clockwise) { int[][] newShapeArray = this.getTurnedShapeArray(clockwise); - return checkCollisionAndMove(this.position, newShapeArray); + Orientation newOrientation = this.orientation.rotated(clockwise); + + int[][] kicksArray = RotationChecker.getKicksArray(this.orientation, newOrientation, this.shape); + for(int[] k : kicksArray) { + Pos candidate = new Pos(this.position.x() + k[0], this.position.y() + k[1], this.position.z()); + if(this.checkCollisionAndMove(candidate, newShapeArray)) { + this.orientation = newOrientation; + return true; + } + } + + return false; } public boolean moveDown() { Pos newPosition = this.position.sub(0, 1, 0); - return checkCollisionAndMove(newPosition, this.shapeArray); + return this.checkCollisionAndMove(newPosition, this.shapeArray); } public boolean moveLeft() { Pos newPosition = this.position.sub(1, 0, 0); - return checkCollisionAndMove(newPosition, this.shapeArray); + return this.checkCollisionAndMove(newPosition, this.shapeArray); } public boolean moveRight() { Pos newPosition = this.position.add(1, 0, 0); - return checkCollisionAndMove(newPosition, this.shapeArray); + return this.checkCollisionAndMove(newPosition, this.shapeArray); } public void draw() { @@ -83,11 +82,11 @@ public class Tetromino { public void draw(boolean withGhost) { if(withGhost) { Pos ghostPos = this.position; - while (!checkCollision(ghostPos.sub(0, 1, 0), this.shapeArray)) { + while(!this.hasCollision(ghostPos.sub(0, 1, 0), this.shapeArray)) { ghostPos = ghostPos.sub(0, 1, 0); } Pos positionChange = this.position.sub(ghostPos); - getBlockPositions().forEach(pos -> { + this.getBlockPositions().forEach(pos -> { Entity ghostBlock = new Entity(ghostEntityType); ((FallingBlockMeta) ghostBlock.getEntityMeta()).setBlock(this.getGhostBlock()); ghostBlock.setNoGravity(true); @@ -97,11 +96,11 @@ public class Tetromino { }); } - getBlockPositions().forEach(pos -> this.instance.setBlock(pos, this.getColoredBlock())); + this.getBlockPositions().forEach(pos -> this.instance.setBlock(pos, this.getColoredBlock())); } public void drawAsEntities() { - getBlockPositions().forEach(pos -> { + this.getBlockPositions().forEach(pos -> { Entity ghostBlock = new Entity(ghostEntityType); ((FallingBlockMeta) ghostBlock.getEntityMeta()).setBlock(this.getColoredBlock()); ghostBlock.setNoGravity(true); @@ -117,7 +116,7 @@ public class Tetromino { public Block getColoredBlock() { Block returnBlock; - switch (this.shape) { + switch(this.shape) { case I -> returnBlock = Block.LIGHT_BLUE_CONCRETE; case J -> returnBlock = Block.BLUE_CONCRETE; case L -> returnBlock = Block.ORANGE_CONCRETE; @@ -141,7 +140,7 @@ public class Tetromino { } public double getXChange() { - switch (this.shape) { + switch(this.shape) { case O, I -> { return 0; } @@ -151,14 +150,13 @@ public class Tetromino { } } - public Shape getShape() { return this.shape; } private Block getGhostBlock() { Block returnBlock; - switch (this.shape) { + switch(this.shape) { case I -> returnBlock = Block.LIGHT_BLUE_STAINED_GLASS; case J -> returnBlock = Block.BLUE_STAINED_GLASS; case L -> returnBlock = Block.ORANGE_STAINED_GLASS; @@ -179,10 +177,10 @@ public class Tetromino { int[][] startArray = Arrays.stream(this.shapeArray).map(int[]::clone).toArray(int[][]::new); int[][] returnArray = new int[arrayLength][arrayLength]; - for(int k=0; k pos.equals(position)); + .anyMatch(pos -> pos.sameBlock(position)); } private List getBlockPositions() { @@ -206,13 +204,13 @@ public class Tetromino { int arrayLength = shapeArray.length; - for(int x=0; x returnList.add(position.add(x-1, y-2, 0)); - case O -> returnList.add(position.add(x, y, 0)); - default -> returnList.add(position.add(x-1, y-1, 0)); + for(int x = 0; x < arrayLength; x++) { + for(int y = 0; y < arrayLength; y++) { + if(shapeArray[arrayLength - 1 - y][x] == 1) { + if(Objects.requireNonNull(this.shape) == Shape.I) { + returnList.add(position.add(x - 2, y - 2, 0)); + } else { + returnList.add(position.add(x - 1, y - 1, 0)); } } } @@ -221,11 +219,11 @@ public class Tetromino { return returnList; } - private boolean checkCollision(Pos newPosition, int[][] newShapeArray) { - List newBlockPositions = getBlockPositions(newPosition, newShapeArray); + private boolean hasCollision(Pos newPosition, int[][] newShapeArray) { + List newBlockPositions = this.getBlockPositions(newPosition, newShapeArray); for(Pos pos : newBlockPositions) { - if(isPartOfTetromino(pos)) continue; + if(this.isPartOfTetromino(pos)) continue; if(this.instance.getBlock(pos) == this.getGhostBlock()) continue; if(this.instance.getBlock(pos) != Block.AIR) return true; } @@ -233,14 +231,30 @@ public class Tetromino { return false; } + public boolean isGrounded() { + return this.hasCollision(this.position.sub(0, 1, 0), this.shapeArray); + } + private boolean checkCollisionAndMove(Pos newPosition, int[][] newShapeArray) { - if(!checkCollision(newPosition, newShapeArray)) { - this.remove(); - this.shapeArray = Arrays.stream(newShapeArray).map(int[]::clone).toArray(int[][]::new); - this.setPosition(newPosition); - this.draw(); - return true; - } - return false; + if(this.hasCollision(newPosition, newShapeArray)) return false; + this.remove(); + this.shapeArray = Arrays.stream(newShapeArray).map(int[]::clone).toArray(int[][]::new); + this.setPosition(newPosition); + this.draw(); + return true; + } + + public int getYPosition() { + return Math.toIntExact(Math.round(this.position.y())); + } + + public enum Shape { + I, + J, + L, + O, + S, + T, + Z } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tntrun/TntRun.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tntrun/TntRun.java index 403a657..4a95429 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tntrun/TntRun.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tntrun/TntRun.java @@ -23,22 +23,23 @@ public class TntRun extends StatelessGame { final int radius; final int stackCount; + public TntRun(int radius, int stackCount) { super(Dimension.OVERWORLD.key, "tntRun", new LastWinsScore()); this.radius = radius; this.stackCount = stackCount; - setGenerator(new CircularPlateTerrainGenerator(radius)); + this.setGenerator(new CircularPlateTerrainGenerator(radius)); } @Override protected void onLoad(@NotNull CompletableFuture callback) { AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); - for (int level = 0; level < stackCount; level++) { - for(int x = -radius; x <= radius; x++) { - for(int z = -radius; z <= radius; z++) { - if(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > radius) continue; + for(int level = 0; level < this.stackCount; level++) { + for(int x = -this.radius; x <= this.radius; x++) { + for(int z = -this.radius; z <= this.radius; z++) { + if(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > this.radius) continue; - batch.setBlock(x, totalElevation + (level * heightPerLevel), z, Block.TNT); + batch.setBlock(x, this.totalElevation + (level * this.heightPerLevel), z, Block.TNT); } } } @@ -50,25 +51,25 @@ public class TntRun extends StatelessGame { protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { - if(playerMoveEvent.getNewPosition().y() < totalElevation) { - if(isBeforeBeginning) { - playerMoveEvent.getPlayer().teleport(getSpawn()); + if(playerMoveEvent.getNewPosition().y() < this.totalElevation) { + if(this.isBeforeBeginning) { + playerMoveEvent.getPlayer().teleport(this.getSpawn()); return; } playerMoveEvent.getPlayer().setGameMode(GameMode.SPECTATOR); - getScore().insertResult(playerMoveEvent.getPlayer()); + this.getScore().insertResult(playerMoveEvent.getPlayer()); } - if(isRunning && !getScore().hasResult(playerMoveEvent.getPlayer())) { + if(this.isRunning && !this.getScore().hasResult(playerMoveEvent.getPlayer())) { MinecraftServer.getSchedulerManager().scheduleTask(() -> { float radius = 0.5F; - for (float x = -radius; x <= radius; x++) { - for (float z = -radius; z <= radius; z++) { + for(float x = -radius; x <= radius; x++) { + for(float z = -radius; z <= radius; z++) { Pos firstLocation = playerMoveEvent.getNewPosition().add(x, -1, z); - Pos secondLocation = firstLocation.withY(y -> y-1); - if(!getBlock(firstLocation).isAir() || !getBlock(secondLocation).isAir()) { - setBlock(firstLocation, Block.AIR); - setBlock(secondLocation, Block.AIR); + Pos secondLocation = firstLocation.withY(y -> y - 1); + if(!this.getBlock(firstLocation).isAir() || !this.getBlock(secondLocation).isAir()) { + this.setBlock(firstLocation, Block.AIR); + this.setBlock(secondLocation, Block.AIR); // Entity fallingTnt = new Entity(EntityType.TNT); // PrimedTntMeta fallingTntMeta = (PrimedTntMeta) fallingTnt.getEntityMeta(); @@ -83,6 +84,6 @@ public class TntRun extends StatelessGame { @Override public Pos getSpawn() { - return new Pos(0, totalElevation + heightPerLevel * (stackCount-1) + 1, 0); + return new Pos(0, this.totalElevation + this.heightPerLevel * (this.stackCount - 1) + 1, 0); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tntrun/TntRunFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tntrun/TntRunFactory.java index 79665b2..c5cb351 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tntrun/TntRunFactory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/tntrun/TntRunFactory.java @@ -30,8 +30,8 @@ public class TntRunFactory implements GameFactory { @Override public ConfigManager configuration() { return new ConfigManager() - .addOption(new NumericOption("radius", Material.STICK, TranslatedComponent.byId("optionCommon#radius"), 20, 30, 50, 60)) - .addOption(new NumericOption("levels", Material.SCAFFOLDING, TranslatedComponent.byId("game_TntRun#levels"), 1, 2, 3, 4, 5)); + .addOption(new NumericOption("radius", Material.STICK, TranslatedComponent.byId("optionCommon#radius"), 20, 30, 50, 60)) + .addOption(new NumericOption("levels", Material.SCAFFOLDING, TranslatedComponent.byId("game_TntRun#levels"), 1, 2, 3, 4, 5)); } @Override diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/TowerdefenseFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/TowerdefenseFactory.java index 1c215b5..02043e7 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/TowerdefenseFactory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/towerdefense/TowerdefenseFactory.java @@ -14,6 +14,7 @@ public class TowerdefenseFactory implements GameFactory { public TranslatedComponent name() { return TranslatedComponent.byId("game_Towerdefense#name"); } + @Override public Material symbol() { return Material.ARMOR_STAND; diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/trafficlightrace/LightPhase.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/trafficlightrace/LightPhase.java index 9cd6d34..ada7f2f 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/trafficlightrace/LightPhase.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/trafficlightrace/LightPhase.java @@ -11,10 +11,10 @@ enum LightPhase { YELLOW(Material.YELLOW_WOOL, 500, 1500), GREEN(Material.GREEN_WOOL, 2000, 5000); + private static final Random rnd = new Random(); public final ItemStack item; private final int minDuration; private final int maxDuration; - private static final Random rnd = new Random(); LightPhase(Material material, int minDuration, int maxDuration) { this.item = ItemStack.of(material); @@ -23,7 +23,7 @@ enum LightPhase { } public TaskSchedule taskScheduleRandomDuration() { - return TaskSchedule.millis(rnd.nextLong(minDuration, maxDuration)); + return TaskSchedule.millis(rnd.nextLong(this.minDuration, this.maxDuration)); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/trafficlightrace/TrafficLightRace.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/trafficlightrace/TrafficLightRace.java index d69dfd4..b189a4d 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/trafficlightrace/TrafficLightRace.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/trafficlightrace/TrafficLightRace.java @@ -1,10 +1,11 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.trafficlightrace; -import de.articdive.jnoise.JNoise; +import de.articdive.jnoise.generators.noisegen.opensimplex.FastSimplexNoiseGenerator; +import de.articdive.jnoise.pipeline.JNoise; +import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; import eu.mhsl.minenet.minigames.score.FirstWinsScore; import eu.mhsl.minenet.minigames.util.BatchUtil; -import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.util.CommonProperties; import eu.mhsl.minenet.minigames.world.BlockPallet; import eu.mhsl.minenet.minigames.world.generator.terrain.SquarePlateTerrainGenerator; @@ -26,56 +27,54 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; class TrafficLightRace extends StatelessGame { - private LightPhase phase = LightPhase.RED; - private int phaseCounter = 0; - private final int width; private final int length; private final int preRun = 10; private final int afterRun = 10; - private final List trafficLights = new ArrayList<>(); + private LightPhase phase = LightPhase.RED; + private int phaseCounter = 0; public TrafficLightRace(int width, int length) { super(Dimension.THE_END.key, "Ampelrennen", new FirstWinsScore()); this.width = width; this.length = length; - setGenerator( - new SquarePlateTerrainGenerator(width, length + preRun + afterRun) - .setPlateHeight(50) - .setGenerateBorders(true) + this.setGenerator( + new SquarePlateTerrainGenerator(width, length + this.preRun + this.afterRun) + .setPlateHeight(50) + .setGenerateBorders(true) ); } @Override protected void onLoad(@NotNull CompletableFuture callback) { final JNoise slowBatches = JNoise.newBuilder() - .fastSimplex() - .setSeed(rnd.nextLong()) - .setFrequency(0.1) - .build(); + .fastSimplex(FastSimplexNoiseGenerator.newBuilder().setSeed(this.rnd.nextLong()).build()) + .scale(0.1) + .build(); AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); - for (int x = 0; x <= width; x++) { - for (int z = 0; z <= preRun; z++) { + for(int x = 0; x <= this.width; x++) { + for(int z = 0; z <= this.preRun; z++) { batch.setBlock(x, 50, z, BlockPallet.GROUND.rnd()); } - for (int z = preRun; z <= length + preRun; z++) { - batch.setBlock(x, 50, z, slowBatches.getNoise(x, z) > 0.6 ? Block.SOUL_SAND : BlockPallet.STREET.rnd()); + for(int z = this.preRun; z <= this.length + this.preRun; z++) { + batch.setBlock(x, 50, z, slowBatches.evaluateNoise(x, z) > 0.6 ? Block.SOUL_SAND : BlockPallet.STREET.rnd()); } - for (int z = preRun + length; z <= preRun + length + afterRun; z++) { + for(int z = this.preRun + this.length; z <= this.preRun + this.length + this.afterRun; z++) { batch.setBlock(x, 50, z, BlockPallet.GROUND.rnd()); } - batch.setBlock(x, 51, preRun-1, Block.OAK_FENCE.withProperties(CommonProperties.fenceEastWest)); - batch.setBlock(x, 50, preRun + length, Block.GOLD_BLOCK); + batch.setBlock(x, 51, this.preRun - 1, Block.OAK_FENCE.withProperties(CommonProperties.fenceEastWest)); + batch.setBlock(x, 50, this.preRun + this.length, Block.GOLD_BLOCK); } - record TrafficLightsInput(int x, int z) {} + record TrafficLightsInput(int x, int z) { + } Consumer generateTrafficLight = (input) -> { batch.setBlock(input.x, 51, input.z, Block.ANVIL); @@ -84,36 +83,36 @@ class TrafficLightRace extends StatelessGame { batch.setBlock(input.x, y, input.z, Block.ANDESITE_WALL); } - for (int x = input.x-3; x <= input.x+3; x++) { + for(int x = input.x - 3; x <= input.x + 3; x++) { batch.setBlock(x, 60, input.z, Block.SMOOTH_STONE_SLAB); } - batch.setBlock(input.x-2, 59, input.z, Block.STONE_BRICK_WALL); - trafficLights.add(new Pos(input.x-2, 58, input.z)); - trafficLights.add(new Pos(input.x-2, 57, input.z)); + batch.setBlock(input.x - 2, 59, input.z, Block.STONE_BRICK_WALL); + this.trafficLights.add(new Pos(input.x - 2, 58, input.z)); + this.trafficLights.add(new Pos(input.x - 2, 57, input.z)); - batch.setBlock(input.x+2, 59, input.z, Block.STONE_BRICK_WALL); - trafficLights.add(new Pos(input.x+2, 58, input.z)); - trafficLights.add(new Pos(input.x+2, 57, input.z)); + batch.setBlock(input.x + 2, 59, input.z, Block.STONE_BRICK_WALL); + this.trafficLights.add(new Pos(input.x + 2, 58, input.z)); + this.trafficLights.add(new Pos(input.x + 2, 57, input.z)); - for (Pos trafficLight : trafficLights) { + for(Pos trafficLight : this.trafficLights) { batch.setBlock(trafficLight, Block.WHITE_WOOL); } }; - for (int count = 0; count <= this.rnd.nextInt(1, 2); count++) { - generateTrafficLight.accept(new TrafficLightsInput(0, this.rnd.nextInt(preRun + 10, length + preRun - 10))); - generateTrafficLight.accept(new TrafficLightsInput(width, this.rnd.nextInt(preRun + 10, length + preRun - 10))); + for(int count = 0; count <= this.rnd.nextInt(1, 2); count++) { + generateTrafficLight.accept(new TrafficLightsInput(0, this.rnd.nextInt(this.preRun + 10, this.length + this.preRun - 10))); + generateTrafficLight.accept(new TrafficLightsInput(this.width, this.rnd.nextInt(this.preRun + 10, this.length + this.preRun - 10))); } - if(width > 30) { - for (int count = 0; count <= this.rnd.nextInt(1, 2); count++) { - generateTrafficLight.accept(new TrafficLightsInput(width/2, this.rnd.nextInt(preRun, length + preRun))); + if(this.width > 30) { + for(int count = 0; count <= this.rnd.nextInt(1, 2); count++) { + generateTrafficLight.accept(new TrafficLightsInput(this.width / 2, this.rnd.nextInt(this.preRun, this.length + this.preRun))); } } - generateTrafficLight.accept(new TrafficLightsInput(0, length + preRun)); - generateTrafficLight.accept(new TrafficLightsInput(width, length + preRun)); + generateTrafficLight.accept(new TrafficLightsInput(0, this.length + this.preRun)); + generateTrafficLight.accept(new TrafficLightsInput(this.width, this.length + this.preRun)); BatchUtil.loadAndApplyBatch(batch, this, () -> callback.complete(null)); } @@ -121,49 +120,51 @@ class TrafficLightRace extends StatelessGame { @Override protected void onStart() { AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); - for(int x = 0; x <= width; x++) { - batch.setBlock(x, 51, preRun-1, Block.AIR); + for(int x = 0; x <= this.width; x++) { + batch.setBlock(x, 51, this.preRun - 1, Block.AIR); } - BatchUtil.loadAndApplyBatch(batch, this, () -> playSound(Sound.sound(SoundEvent.BLOCK_WOOD_BREAK, Sound.Source.BLOCK, 1f, 1f))); + BatchUtil.loadAndApplyBatch(batch, this, () -> this.playSound(Sound.sound(SoundEvent.BLOCK_WOOD_BREAK, Sound.Source.BLOCK, 1f, 1f))); - scheduler().submitTask(() -> { + this.scheduler().submitTask(() -> { if(!super.isRunning) return TaskSchedule.stop(); - phaseCounter++; - if(phaseCounter >= 3) phaseCounter = 0; + this.phaseCounter++; + if(this.phaseCounter >= 3) this.phaseCounter = 0; - if(phaseCounter == 0) phase = LightPhase.RED; - if(phaseCounter == 1) phase = LightPhase.GREEN; - if(phaseCounter == 2) phase = LightPhase.YELLOW; + if(this.phaseCounter == 0) this.phase = LightPhase.RED; + if(this.phaseCounter == 1) this.phase = LightPhase.GREEN; + if(this.phaseCounter == 2) this.phase = LightPhase.YELLOW; - getPlayers().forEach(player -> { + this.getPlayers().forEach(player -> { for(int i = 0; i < 9; i++) { - player.getInventory().setItemStack(i, phase.item); + player.getInventory().setItemStack(i, this.phase.item); } }); AbsoluteBlockBatch changeLightsBatch = new AbsoluteBlockBatch(); - for (Pos trafficLight : trafficLights) { - changeLightsBatch.setBlock(trafficLight, phase.item.material().block()); + for(Pos trafficLight : this.trafficLights) { + changeLightsBatch.setBlock(trafficLight, this.phase.item.material().block()); } - BatchUtil.loadAndApplyBatch(changeLightsBatch, this, () -> {}); + BatchUtil.loadAndApplyBatch(changeLightsBatch, this, () -> { + }); - return phase.taskScheduleRandomDuration(); + return this.phase.taskScheduleRandomDuration(); }, ExecutionType.TICK_END); } @Override protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { - if(isBeforeBeginning) return; + if(this.isBeforeBeginning) return; + if(this.getScore().hasResult(playerMoveEvent.getPlayer())) return; - if(phase.equals(LightPhase.RED) && playerMoveEvent.getNewPosition().z()-0.01 > playerMoveEvent.getPlayer().getPosition().z()) { + if(this.phase.equals(LightPhase.RED) && playerMoveEvent.getNewPosition().z() - 0.01 > playerMoveEvent.getPlayer().getPosition().z()) { playerMoveEvent.getPlayer().setVelocity(new Vec(0, 8, -15)); playerMoveEvent.getPlayer().playSound(Sound.sound(SoundEvent.ENTITY_BLAZE_SHOOT, Sound.Source.PLAYER, 1f, 1f)); } - if(playerMoveEvent.getNewPosition().z() > preRun+length) { - getScore().insertResult(playerMoveEvent.getPlayer()); + if(playerMoveEvent.getNewPosition().z() > this.preRun + this.length) { + this.getScore().insertResult(playerMoveEvent.getPlayer()); playerMoveEvent.getPlayer().getInventory().clear(); playerMoveEvent.getPlayer().setGameMode(GameMode.SPECTATOR); } @@ -171,6 +172,6 @@ class TrafficLightRace extends StatelessGame { @Override public Pos getSpawn() { - return new Pos((double) width/2, 51, 3); + return new Pos((double) this.width / 2, 51, 3); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/trafficlightrace/TrafficLightRaceFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/trafficlightrace/TrafficLightRaceFactory.java index cf78b58..c2d1be4 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/trafficlightrace/TrafficLightRaceFactory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/trafficlightrace/TrafficLightRaceFactory.java @@ -1,8 +1,8 @@ package eu.mhsl.minenet.minigames.instance.game.stateless.types.trafficlightrace; import eu.mhsl.minenet.minigames.instance.game.Game; -import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption; import eu.mhsl.minenet.minigames.instance.room.Room; @@ -25,8 +25,8 @@ public class TrafficLightRaceFactory implements GameFactory { @Override public ConfigManager configuration() { return new ConfigManager() - .addOption(new NumericOption("width", Material.OAK_FENCE, TranslatedComponent.byId("game_TrafficlightRace#width"), 20, 30, 40)) - .addOption(new NumericOption("length", Material.BLACK_CONCRETE_POWDER, TranslatedComponent.byId("game_TrafficlightRace#length"), 50, 80, 100, 120, 150)); + .addOption(new NumericOption("width", Material.OAK_FENCE, TranslatedComponent.byId("game_TrafficlightRace#width"), 20, 30, 40)) + .addOption(new NumericOption("length", Material.BLACK_CONCRETE_POWDER, TranslatedComponent.byId("game_TrafficlightRace#length"), 50, 80, 100, 120, 150)); } @Override diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/TurtleGame.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/TurtleGame.java new file mode 100644 index 0000000..899f8bc --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/TurtleGame.java @@ -0,0 +1,251 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.turtleGame; + +import eu.mhsl.minenet.minigames.instance.Dimension; +import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; +import eu.mhsl.minenet.minigames.instance.game.stateless.types.turtleGame.gameObjects.Turtle; +import eu.mhsl.minenet.minigames.score.PointsWinScore; +import net.kyori.adventure.sound.Sound; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.metadata.other.FallingBlockMeta; +import net.minestom.server.event.player.PlayerInputEvent; +import net.minestom.server.event.player.PlayerTickEvent; +import net.minestom.server.instance.block.Block; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import net.minestom.server.network.packet.server.play.ParticlePacket; +import net.minestom.server.particle.Particle; +import net.minestom.server.sound.SoundEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.CompletableFuture; + +class TurtleGame extends StatelessGame { + private final int radius; + private final Map turtlePlayerMap = new WeakHashMap<>(); + private final ArrayList snacks = new ArrayList<>(); + private final ArrayList bombs = new ArrayList<>(); + private final Block snackBlock = Block.SUNFLOWER.withProperty("half", "upper"); + private final double startSpeed; + + public TurtleGame(int radius, int startSpeed) { + super(Dimension.OVERWORLD.key, "Turtle Game", new PointsWinScore()); + this.radius = radius; + this.startSpeed = startSpeed; + + this.eventNode() + .addListener(PlayerTickEvent.class, this::onPlayerTick) + .addListener(PlayerInputEvent.class, this::onPlayerInput); + } + + @Override + protected void onLoad(@NotNull CompletableFuture callback) { + for(int x = -this.radius - 1; x <= this.radius + 1; x++) { + for(int z = -this.radius - 1; z <= this.radius + 1; z++) { + double distance = new Pos(x, 0, z).distance(new Pos(0, 0, 0)); + if(distance <= this.radius) { + boolean isEdge = this.radius - 1 < distance; + this.setBlock(x, 0, z, Block.SAND); + this.setBlock(x, 1, z, isEdge ? Block.STONE : Block.AIR); + this.setBlock(x, 2, z, isEdge ? Block.STONE_SLAB : Block.AIR); + } else { + this.setBlock(x, 0, z, Block.AIR); + } + } + } + } + + private void onPlayerInput(@NotNull PlayerInputEvent event) { + Player p = event.getPlayer(); + if(event.hasPressedShiftKey()) this.turtlePlayerMap.get(p).boostSpeed(); + if(event.hasReleasedShiftKey()) this.turtlePlayerMap.get(p).cancelBoost(); + } + + protected void onPlayerTick(@NotNull PlayerTickEvent event) { + Player p = event.getPlayer(); + if(p.getGameMode() == GameMode.SPECTATOR) return; + Turtle turtle = this.turtlePlayerMap.get(p); + turtle.adaptView(); + if(this.isRunning()) { + turtle.move(); + this.snacks.stream() + .filter(turtle::checkCollisionWithEntity) + .toList() + .forEach(snack -> { + this.eat(p, snack); + this.generateNewSnack(); + if(this.turtlePlayerMap.get(p).getScore() % 5 == 0) { + this.generateNewBomb(); + this.addSpeed(0.4, p); + } + }); + this.bombs.stream() + .filter(turtle::checkCollisionWithEntity) + .toList() + .forEach(bomb -> { + this.explode(p, bomb); + this.generateNewBombs(2); + this.addGlobalSpeed(0.3); + }); + } + } + + protected void addSpeed(double amount, Player p) { + this.turtlePlayerMap.get(p).addSpeed(amount); + } + + protected void addGlobalSpeed(double amount) { + this.turtlePlayerMap.values().forEach(turtle -> turtle.addSpeed(amount)); + } + + protected void eat(Player p, Entity snack) { + p.playSound(Sound.sound(SoundEvent.ENTITY_GENERIC_EAT, Sound.Source.MASTER, 1f, 1f), snack.getPosition()); + Material snackMaterial = Objects.requireNonNull(this.snackBlock.registry().material()); + p.sendPacket(new ParticlePacket(Particle.ITEM.withItem(ItemStack.of(snackMaterial)), p.getPosition(), new Pos(0.5, 0.5, 0.5), 0, 8)); + this.snacks.remove(snack); + snack.remove(); + this.turtlePlayerMap.get(p).increaseScore(); + this.turtlePlayerMap.get(p).increaseBoostChargeLevel(0.04f); + } + + protected void explode(Player p, Entity bomb) { + this.letPlayerLoose(p); + p.playSound(Sound.sound(SoundEvent.ENTITY_GENERIC_EXPLODE, Sound.Source.MASTER, 2f, 1f), bomb.getPosition()); + p.playSound(Sound.sound(SoundEvent.ENTITY_GENERIC_EXPLODE, Sound.Source.MASTER, 2f, 1f), bomb.getPosition()); + p.sendPacket(new ParticlePacket(Particle.EXPLOSION, p.getPosition(), new Pos(0.5, 0.5, 0.5), 0, 8)); + if(this.getLeftPlayers().size() == 1) this.setTimeLimit(10); + this.bombs.remove(bomb); + bomb.remove(); + } + + protected void letPlayerLoose(Player p) { + p.setGameMode(GameMode.SPECTATOR); + p.setFlying(true); + this.turtlePlayerMap.get(p).destroy(); + this.getScore().insertResult(p, this.turtlePlayerMap.get(p).getScore()); + } + + protected List getLeftPlayers() { + return this.turtlePlayerMap.keySet().stream() + .filter(player -> !this.getScore().hasResult(player)) + .toList(); + } + + @Override + protected boolean onPlayerJoin(Player p) { + this.turtlePlayerMap.putIfAbsent(p, new Turtle(p, this.startSpeed)); + p.setLevel(this.turtlePlayerMap.get(p).getScore()); + + Turtle turtle = this.turtlePlayerMap.get(p); + MinecraftServer.getSchedulerManager().scheduleNextTick(turtle::spawnTurtle); + return super.onPlayerJoin(p); + } + + @Override + protected void onPlayerLeave(Player p) { + Turtle turtle = this.turtlePlayerMap.get(p); + turtle.remove(); + } + + @Override + public Pos getSpawn() { + double theta = this.rnd.nextDouble() * 2 * Math.PI; + + double spawnRadius = this.radius - 4; + double x = spawnRadius * Math.cos(theta); + double z = spawnRadius * Math.sin(theta); + + return new Pos(x, 1, z).withLookAt(new Pos(0, 0, 0)); + } + + private void generateNewSnacks(int count) { + for(int i = 0; i < count; i++) { + this.generateNewSnack(); + } + } + + private void generateNewSnack() { + Entity snack = new Entity(EntityType.FALLING_BLOCK); + FallingBlockMeta meta = (FallingBlockMeta) snack.getEntityMeta(); + meta.setBlock(this.snackBlock); + meta.setCustomName(Component.text("Snack")); + meta.setCustomNameVisible(true); + Pos spawnPosition = this.newSpawnPosition(snack); + if(spawnPosition == null) { + snack.remove(); + return; + } + snack.setInstance(this, spawnPosition); + this.snacks.add(snack); + } + + private void generateNewBombs(int count) { + for(int i = 0; i < count; i++) { + this.generateNewBomb(); + } + } + + private void generateNewBomb() { + Entity bomb = new Entity(EntityType.FALLING_BLOCK); + FallingBlockMeta meta = (FallingBlockMeta) bomb.getEntityMeta(); + meta.setBlock(Block.TNT); + meta.setCustomName(Component.text("Bomb").color(NamedTextColor.RED).decorate(TextDecoration.BOLD)); + meta.setCustomNameVisible(true); + Pos spawnPosition = this.newSpawnPosition(bomb, false); + if(spawnPosition == null) { + bomb.remove(); + return; + } + bomb.setInstance(this, spawnPosition); + this.bombs.add(bomb); + } + + private @Nullable Pos newSpawnPosition(Entity entity) { + return this.newSpawnPosition(entity, true); + } + + private @Nullable Pos newSpawnPosition(Entity entity, boolean nearPlayers) { + Pos spawnPosition; + int counter = 0; + boolean isInRadius, collides; + do { + if(counter > 200) return null; + int x = this.rnd.nextInt(-this.radius + 2, this.radius - 2); + int z = this.rnd.nextInt(-this.radius + 2, this.radius - 2); + spawnPosition = new Pos(x, 1, z).add(0.5, 0, 0.5); + Pos checkPosition = spawnPosition; + isInRadius = checkPosition.distance(0, 1, 0) < this.radius - 2; + collides = this.getEntities().stream() + .filter(e -> !e.equals(entity)) + .anyMatch(e -> entity.getBoundingBox().intersectBox(e.getPosition().sub(checkPosition), e.getBoundingBox())); + if(!collides && !nearPlayers) collides = this.turtlePlayerMap.values().stream() + .filter(turtle -> !turtle.equals(entity)) + .anyMatch(turtle -> entity.getBoundingBox() + .growSymmetrically(turtle.getBombBorder(), 1, turtle.getBombBorder()) + .intersectBox(turtle.getPosition().sub(checkPosition), turtle.getBoundingBox())); + counter++; + } while(!isInRadius || collides); + return spawnPosition; + } + + @Override + protected void onStart() { + this.generateNewSnacks(this.turtlePlayerMap.size() + 1); + this.generateNewBombs((int) Math.ceil(this.snacks.size() * 0.5)); + this.turtlePlayerMap.values().forEach(Turtle::startBoostRefill); + } + + @Override + protected void onStop() { + this.getLeftPlayers().forEach(this::letPlayerLoose); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/TurtleGameFactory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/TurtleGameFactory.java new file mode 100644 index 0000000..1f66cf5 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/TurtleGameFactory.java @@ -0,0 +1,42 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.turtleGame; + +import eu.mhsl.minenet.minigames.instance.game.Game; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.ConfigManager; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option; +import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption; +import eu.mhsl.minenet.minigames.instance.room.Room; +import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; +import net.minestom.server.item.Material; + +import java.util.Map; + +public class TurtleGameFactory implements GameFactory { + + @Override + public TranslatedComponent name() { + return TranslatedComponent.byId("game_TurtleGame#name"); + } + + @Override + public TranslatedComponent description() { + return TranslatedComponent.byId("game_TurtleGame#description"); + } + + @Override + public ConfigManager configuration() { + return new ConfigManager() + .addOption(new NumericOption("radius", Material.HEART_OF_THE_SEA, TranslatedComponent.byId("optionCommon#radius"), 10, 20, 30, 40)) + .addOption(new NumericOption("startSpeed", Material.LEATHER_BOOTS, TranslatedComponent.byId("game_TurtleGame#startSpeed"), 2, 3, 4, 6, 8)); + } + + @Override + public Game manufacture(Room parent, Map> configuration) throws Exception { + return new TurtleGame(configuration.get("radius").getAsInt(), configuration.get("startSpeed").getAsInt()).setParent(parent); + } + + @Override + public Material symbol() { + return Material.TURTLE_EGG; + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/gameObjects/Turtle.java b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/gameObjects/Turtle.java new file mode 100644 index 0000000..7e83ff2 --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/game/stateless/types/turtleGame/gameObjects/Turtle.java @@ -0,0 +1,111 @@ +package eu.mhsl.minenet.minigames.instance.game.stateless.types.turtleGame.gameObjects; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +import net.minestom.server.entity.Entity; +import net.minestom.server.entity.EntityCreature; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.Player; +import net.minestom.server.entity.attribute.Attribute; +import net.minestom.server.timer.Task; +import net.minestom.server.timer.TaskSchedule; + +public class Turtle extends EntityCreature { + private final Player player; + private int score = 0; + private double speed; + private double boostSpeedMultiplier = 1; + private float boostChargeLevel = 0f; + private Task boostTask; + private Task boostRefillTask; + + public Turtle(Player player, double speed) { + super(EntityType.TURTLE); + this.player = player; + this.speed = speed; + + this.getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.15); + this.getAttribute(Attribute.MAX_HEALTH).setBaseValue(1); + this.player.setExp(this.boostChargeLevel); + } + + public void spawnTurtle() { + this.setInstance(this.player.getInstance(), this.player.getPosition()); + this.addPassenger(this.player); + } + + public void startBoostRefill() { + this.boostRefillTask = MinecraftServer.getSchedulerManager().scheduleTask(() -> { + if(this.boostChargeLevel >= 1f) return; + this.increaseBoostChargeLevel(0.02f); + }, TaskSchedule.seconds(1), TaskSchedule.seconds(1)); + } + + public void destroy() { + this.removePassenger(this.player); + this.remove(); + if(this.boostRefillTask != null && this.boostRefillTask.isAlive()) this.boostRefillTask.cancel(); + if(this.boostTask != null && this.boostTask.isAlive()) this.boostTask.cancel(); + } + + public void adaptView() { + if(this.getInstance() == null) return; + this.teleport(this.getPosition().withView(this.player.getPosition().withPitch(this.getPosition().pitch()))); + Vec lookingVector = this.player.getPosition().direction().withY(0).mul(100); + this.lookAt(this.getPosition().add(new Pos(lookingVector))); + } + + public void move() { + this.adaptView(); + Vec direction = this.player.getPosition().direction(); + Vec movementVector = direction.withY(0).normalize().mul(this.speed * this.boostSpeedMultiplier); + this.setVelocity(movementVector); + } + + public boolean checkCollisionWithEntity(Entity other) { + return this.getBoundingBox().intersectBox(other.getPosition().sub(this.getPosition()), other.getBoundingBox()); + } + + public void boostSpeed() { + if(this.boostChargeLevel <= 0f) return; + this.boostSpeedMultiplier = 3.5; + this.boostTask = MinecraftServer.getSchedulerManager().scheduleTask(() -> { + if(this.boostChargeLevel <= 0f) { + this.cancelBoost(); + return; + } + this.boostChargeLevel = Math.max(0f, this.boostChargeLevel - 0.025f); + this.player.setExp(this.boostChargeLevel); + }, TaskSchedule.millis(30), TaskSchedule.millis(30)); + } + + public void cancelBoost() { + if(this.boostTask == null || !this.boostTask.isAlive()) return; + this.boostTask.cancel(); + this.boostSpeedMultiplier = 1; + } + + public void increaseBoostChargeLevel(float amount) { + this.boostChargeLevel = Math.min(1f, this.boostChargeLevel + amount); + this.player.setExp(this.boostChargeLevel); + } + + public void addSpeed(double amount) { + this.speed += amount; + } + + public int getScore() { + return this.score; + } + + public double getBombBorder() { + // 1 bei speed 2; 2 bei speed 4; 4 bei speed 8 + return Math.clamp((this.speed * this.boostSpeedMultiplier) / 2, 1.5, 4); + } + + public void increaseScore() { + this.score += 1; + this.player.setLevel(this.score); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/hub/Hub.java b/src/main/java/eu/mhsl/minenet/minigames/instance/hub/Hub.java index 06ea4a2..383c8ce 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/hub/Hub.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/hub/Hub.java @@ -1,17 +1,18 @@ package eu.mhsl.minenet.minigames.instance.hub; import eu.mhsl.minenet.minigames.Resource; +import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.instance.MineNetInstance; +import eu.mhsl.minenet.minigames.instance.Spawnable; import eu.mhsl.minenet.minigames.instance.hub.entity.RoomSelector; import eu.mhsl.minenet.minigames.util.CommonEventHandles; -import eu.mhsl.minenet.minigames.instance.Spawnable; -import eu.mhsl.minenet.minigames.instance.Dimension; import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Pos; -import net.minestom.server.event.player.*; +import net.minestom.server.event.player.PlayerBlockBreakEvent; +import net.minestom.server.event.player.PlayerBlockInteractEvent; +import net.minestom.server.event.player.PlayerBlockPlaceEvent; import net.minestom.server.instance.anvil.AnvilLoader; - -import java.nio.file.Path; +import net.minestom.server.world.DimensionType; public class Hub extends MineNetInstance implements Spawnable { public static final Hub INSTANCE = new Hub(); @@ -19,22 +20,21 @@ public class Hub extends MineNetInstance implements Spawnable { static { MinecraftServer.getInstanceManager().registerInstance(INSTANCE); - INSTANCE.setChunkLoader(new AnvilLoader(Resource.HUB_MAP.getPath())); + INSTANCE.setChunkLoader(new AnvilLoader(Resource.HUB_MAP.getPath(), DimensionType.OVERWORLD.key())); INSTANCE.eventNode() - .addListener(PlayerBlockBreakEvent.class, CommonEventHandles::cancel) - .addListener(PlayerBlockPlaceEvent.class, CommonEventHandles::cancel) - .addListener(PlayerBlockInteractEvent.class, CommonEventHandles::cancel); + .addListener(PlayerBlockBreakEvent.class, CommonEventHandles::cancel) + .addListener(PlayerBlockPlaceEvent.class, CommonEventHandles::cancel) + .addListener(PlayerBlockInteractEvent.class, CommonEventHandles::cancel); new RoomSelector().setInstance(INSTANCE, new Pos(0.5, 11, 4.5)); } private Hub() { super(Dimension.THE_END.key); - setChunkLoader(new AnvilLoader(Path.of("maps/hub"))); - setTime(18000); - setTimeRate(0); + this.setTime(18000); + this.defaultClock().pause(); } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/hub/entity/RoomSelector.java b/src/main/java/eu/mhsl/minenet/minigames/instance/hub/entity/RoomSelector.java index 37839b4..09f32a2 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/hub/entity/RoomSelector.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/hub/entity/RoomSelector.java @@ -11,10 +11,11 @@ import org.jetbrains.annotations.NotNull; public class RoomSelector extends InteractableEntity { final AbstractVillagerMeta abstractVillagerMeta; + public RoomSelector() { super(EntityType.VILLAGER); - abstractVillagerMeta = (AbstractVillagerMeta) this.getEntityMeta(); + this.abstractVillagerMeta = (AbstractVillagerMeta) this.getEntityMeta(); } @Override @@ -25,7 +26,7 @@ public class RoomSelector extends InteractableEntity { @Override public void onAttack(@NotNull EntityAttackEvent entityAttackEvent) { super.onAttack(entityAttackEvent); - abstractVillagerMeta.setHeadShakeTimer(20); + this.abstractVillagerMeta.setHeadShakeTimer(20); } @Override diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/hub/inventory/HubInventory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/hub/inventory/HubInventory.java index 1cb0c51..b49a5db 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/hub/inventory/HubInventory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/hub/inventory/HubInventory.java @@ -12,25 +12,25 @@ public class HubInventory extends InteractableInventory { public HubInventory(Player p) { super(InventoryType.CHEST_3_ROW, TranslatedComponent.assemble("hub#invTitle", p)); - setClickableItem( - ItemStack - .builder(Material.WRITABLE_BOOK) - .customName(TranslatedComponent.assemble("hub#create", p)) - .lore(TranslatedComponent.assemble("hub#create_description", p)) - .build(), - 12, - itemClick -> Room.createRoom(itemClick.getPlayer()), - true + this.setClickableItem( + ItemStack + .builder(Material.WRITABLE_BOOK) + .customName(TranslatedComponent.assemble("hub#create", p)) + .lore(TranslatedComponent.assemble("hub#create_description", p)) + .build(), + 12, + itemClick -> Room.createRoom(itemClick.getPlayer()), + true ); - setClickableItem( - ItemStack - .builder(Material.KNOWLEDGE_BOOK) - .customName(TranslatedComponent.assemble("hub#join", p)) - .lore(TranslatedComponent.assemble("hub#join_description", p)) - .build(), - 14, - itemClick -> itemClick.getPlayer().openInventory(new JoinInventory(itemClick.getPlayer())) + this.setClickableItem( + ItemStack + .builder(Material.KNOWLEDGE_BOOK) + .customName(TranslatedComponent.assemble("hub#join", p)) + .lore(TranslatedComponent.assemble("hub#join_description", p)) + .build(), + 14, + itemClick -> itemClick.getPlayer().openInventory(new JoinInventory(itemClick.getPlayer())) ); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/hub/inventory/JoinInventory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/hub/inventory/JoinInventory.java index 467edcb..b2cbf83 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/hub/inventory/JoinInventory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/hub/inventory/JoinInventory.java @@ -1,18 +1,18 @@ package eu.mhsl.minenet.minigames.instance.hub.inventory; +import eu.mhsl.minenet.minigames.instance.hub.Hub; import eu.mhsl.minenet.minigames.instance.room.Room; import eu.mhsl.minenet.minigames.message.Icon; import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; import eu.mhsl.minenet.minigames.message.type.ChatMessage; import eu.mhsl.minenet.minigames.shared.inventory.InteractableInventory; -import eu.mhsl.minenet.minigames.instance.hub.Hub; import net.kyori.adventure.text.Component; import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Player; +import net.minestom.server.event.inventory.InventoryPreClickEvent; import net.minestom.server.event.player.PlayerPacketEvent; import net.minestom.server.inventory.InventoryType; -import net.minestom.server.inventory.click.ClickType; -import net.minestom.server.inventory.condition.InventoryConditionResult; +import net.minestom.server.inventory.click.Click; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.network.packet.client.play.ClientNameItemPacket; @@ -21,46 +21,51 @@ import java.util.Locale; import java.util.Optional; public class JoinInventory extends InteractableInventory { - private String typedText = ""; private final String prefix = "Suche: "; + private String typedText = ""; public JoinInventory(Player p) { super(InventoryType.ANVIL, TranslatedComponent.assemble("hub#join_title", p)); - setClickableItem( - ItemStack.builder(Material.GREEN_STAINED_GLASS_PANE) - .customName(Component.text(prefix)) - .build(), - 0, - itemClick -> {} + this.setClickableItem( + ItemStack.builder(Material.GREEN_STAINED_GLASS_PANE) + .customName(Component.text(this.prefix)) + .build(), + 0, + itemClick -> { + } ); Hub.INSTANCE.eventNode().addListener(PlayerPacketEvent.class, event -> { - if (event.getPacket() instanceof ClientNameItemPacket packet) { - typedText = packet.itemName(); + if(event.getPacket() instanceof ClientNameItemPacket(String itemName)) { + this.typedText = itemName; } }); } @Override - protected void onClick(Player player, int slot, ClickType clickType, InventoryConditionResult inventoryConditionResult) { + protected void onClick(Player player, int slot, Click click, InventoryPreClickEvent event) { if(slot != 2) return; - inventoryConditionResult.setCancel(true); + event.setCancelled(true); player.closeInventory(); - typedText = formatInput(typedText); + this.typedText = this.formatInput(this.typedText); - Optional target = Room.getRoom(MinecraftServer.getConnectionManager().findOnlinePlayer(typedText)); + Optional target = Room.getRoom(MinecraftServer.getConnectionManager().findOnlinePlayer(this.typedText)); if(target.isPresent()) Room.setRoom(player, target.get()); else - new ChatMessage(Icon.ERROR).appendTranslated("hub#join_notFound").appendStatic(" " + typedText).send(player); + new ChatMessage(Icon.ERROR) + .appendTranslated("hub#join_notFound") + .appendSpace() + .quote(this.typedText.trim()) + .send(player); } private String formatInput(String raw) { - if(raw.startsWith(prefix)) { - raw = raw.split(prefix)[1]; + if(raw.startsWith(this.prefix)) { + raw = raw.split(this.prefix)[1]; } return raw.toLowerCase(Locale.ROOT).trim(); diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/room/Room.java b/src/main/java/eu/mhsl/minenet/minigames/instance/room/Room.java index afa2577..40cefd5 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/room/Room.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/room/Room.java @@ -1,24 +1,24 @@ package eu.mhsl.minenet.minigames.instance.room; import eu.mhsl.minenet.minigames.Resource; +import eu.mhsl.minenet.minigames.instance.Dimension; import eu.mhsl.minenet.minigames.instance.MineNetInstance; +import eu.mhsl.minenet.minigames.instance.Spawnable; +import eu.mhsl.minenet.minigames.instance.hub.Hub; +import eu.mhsl.minenet.minigames.instance.room.entity.GameSelector; import eu.mhsl.minenet.minigames.message.Icon; import eu.mhsl.minenet.minigames.message.type.ChatMessage; import eu.mhsl.minenet.minigames.score.tournament.Tournament; import eu.mhsl.minenet.minigames.util.CommonEventHandles; import eu.mhsl.minenet.minigames.util.MoveInstance; -import eu.mhsl.minenet.minigames.instance.Spawnable; -import eu.mhsl.minenet.minigames.instance.Dimension; -import eu.mhsl.minenet.minigames.instance.hub.Hub; -import eu.mhsl.minenet.minigames.instance.room.entity.GameSelector; import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; -import net.minestom.server.entity.attribute.Attribute; import net.minestom.server.event.player.PlayerBlockBreakEvent; import net.minestom.server.event.player.PlayerDisconnectEvent; import net.minestom.server.instance.anvil.AnvilLoader; +import net.minestom.server.world.DimensionType; import java.util.*; import java.util.logging.Logger; @@ -28,6 +28,24 @@ public class Room extends MineNetInstance implements Spawnable { private static final Set rooms = new HashSet<>(); private static final Map players = new WeakHashMap<>(); private static final Logger logger = Logger.getLogger("room"); + public final UUID uuid = UUID.randomUUID(); + public final boolean apiDriven; + private final Tournament tournament = new Tournament(); + private Player owner; + private GameSelector gameSelector; + + private Room(Player owner) { + super(Dimension.THE_END.key); + this.apiDriven = false; + this.construct(); + this.setOwner(owner); + } + + protected Room() { + super(Dimension.THE_END.key); + this.apiDriven = true; + this.construct(); + } public static Room createRoom(Player owner) { logger.info("Creating room with owner " + owner.getUsername()); @@ -67,9 +85,6 @@ public class Room extends MineNetInstance implements Spawnable { p.getInventory().clear(); p.setGameMode(GameMode.ADVENTURE); p.setInvisible(false); - p.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).setBaseValue(p.getAttribute(Attribute.BLOCK_INTERACTION_RANGE).attribute().defaultValue()); - p.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).setBaseValue(p.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).attribute().defaultValue()); - p.heal(); p.setExp(0); p.setLevel(0); rooms.add(room); @@ -86,37 +101,19 @@ public class Room extends MineNetInstance implements Spawnable { return rooms; } - private Player owner; - public final UUID uuid = UUID.randomUUID(); - public final boolean apiDriven; - private GameSelector gameSelector; - private final Tournament tournament = new Tournament(); - private Room(Player owner) { - super(Dimension.THE_END.key); - this.apiDriven = false; - construct(); - setOwner(owner); - } - - protected Room() { - super(Dimension.THE_END.key); - this.apiDriven = true; - construct(); - } - private void construct() { MinecraftServer.getInstanceManager().registerInstance(this); - setChunkLoader(new AnvilLoader(Resource.LOBBY_MAP.getPath())); + this.setChunkLoader(new AnvilLoader(Resource.LOBBY_MAP.getPath(), DimensionType.OVERWORLD.key())); this.gameSelector = new GameSelector(); this.gameSelector.setInstance(this, new Pos(0.5, 50, 19.5)); - eventNode().addListener(PlayerBlockBreakEvent.class, CommonEventHandles::cancel); - eventNode().addListener(PlayerDisconnectEvent.class, playerDisconnectEvent -> unsetRoom(playerDisconnectEvent.getPlayer())); + this.eventNode().addListener(PlayerBlockBreakEvent.class, CommonEventHandles::cancel); + this.eventNode().addListener(PlayerDisconnectEvent.class, playerDisconnectEvent -> unsetRoom(playerDisconnectEvent.getPlayer())); } public Player getOwner() { - return owner; + return this.owner; } public void setOwner(Player newOwner) { @@ -129,19 +126,27 @@ public class Room extends MineNetInstance implements Spawnable { if(p != this.owner) return; - getAllMembers().stream() - .filter(player -> player != p) // exclude the current leaving owner - .findFirst() - .ifPresentOrElse( - this::setOwner, - () -> Room.getRoom(p).ifPresent(Room::deleteRoom) // delete room if no players are left - ); + this.getAllMembers().stream() + .filter(player -> player != p) // exclude the current leaving owner + .findFirst() + .ifPresentOrElse( + this::setOwner, + () -> Room.getRoom(p).ifPresent(Room::deleteRoom) // delete room if no players are left + ); Room.unsetRoom(p); - new ChatMessage(Icon.ERROR).appendStatic("The room leader left!").send(getAllMembers()); - new ChatMessage(Icon.SCIENCE).appendStatic(this.owner.getUsername()).appendStatic(" is the new Leader!").send(getAllMembers().stream().filter(player -> player != this.owner).collect(Collectors.toSet())); - new ChatMessage(Icon.SUCCESS).appendStatic("You are now the leader.").send(this.owner); + new ChatMessage(Icon.ERROR) + .appendTranslated("room#ownerLeft") + .send(this.getAllMembers()); + new ChatMessage(Icon.SCIENCE) + .appendStatic(this.owner.getUsername()) + .appendSpace() + .appendTranslated("room#newOwnerAnnounce") + .send(this.getAllMembers().stream().filter(player -> player != this.owner).collect(Collectors.toSet())); + new ChatMessage(Icon.SUCCESS) + .appendTranslated("room#ownerSelf") + .send(this.owner); }); } @@ -155,8 +160,8 @@ public class Room extends MineNetInstance implements Spawnable { public Set getAllMembers() { return players.keySet().stream() - .filter(player -> getRoom(player).orElse(null) == this) - .collect(Collectors.toSet()); + .filter(player -> getRoom(player).orElse(null) == this) + .collect(Collectors.toSet()); } public Tournament getTournament() { diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/room/entity/GameSelector.java b/src/main/java/eu/mhsl/minenet/minigames/instance/room/entity/GameSelector.java index 7603fe7..ffdd17c 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/room/entity/GameSelector.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/room/entity/GameSelector.java @@ -14,10 +14,11 @@ import org.jetbrains.annotations.NotNull; public class GameSelector extends InteractableEntity { final AbstractVillagerMeta abstractVillagerMeta; + public GameSelector() { super(EntityType.VILLAGER); - abstractVillagerMeta = (AbstractVillagerMeta) this.getEntityMeta(); + this.abstractVillagerMeta = (AbstractVillagerMeta) this.getEntityMeta(); } @Override @@ -32,12 +33,14 @@ public class GameSelector extends InteractableEntity { @Override public void onInteract(@NotNull PlayerEntityInteractEvent playerEntityInteractEvent) { - Room room = (Room) instance; + Room room = (Room) this.instance; if(playerEntityInteractEvent.getPlayer() != room.getOwner()) { - abstractVillagerMeta.setHeadShakeTimer(20); + this.abstractVillagerMeta.setHeadShakeTimer(20); - new ChatMessage(Icon.ERROR).appendStatic("Only the room leader can start games!").send(playerEntityInteractEvent.getPlayer()); + new ChatMessage(Icon.ERROR) + .appendTranslated("room#onlyOwnerCanStart") + .send(playerEntityInteractEvent.getPlayer()); return; } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/room/inventory/MinigameSelectInventory.java b/src/main/java/eu/mhsl/minenet/minigames/instance/room/inventory/MinigameSelectInventory.java index cc3da93..f8709ec 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/room/inventory/MinigameSelectInventory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/room/inventory/MinigameSelectInventory.java @@ -19,25 +19,26 @@ import java.util.List; public class MinigameSelectInventory extends InteractableInventory { final private Room room; final private Player p; + public MinigameSelectInventory(Room room, Player p) { super(InventoryType.CHEST_6_ROW, TranslatedComponent.assemble("room#invTitle", p)); this.room = room; this.p = p; InventoryItemAlignment itemAlignment = new InventoryItemAlignment(GameType.values().length, 1); - for (GameType type : GameType.values()) { - setClickableItem( - ItemStack.builder(type.getIcon()) - .customName(type.getTitle().getAssembled(p)) - .lore(type.getDescription().addWrap().getWrappedAssembled(p)) - .build(), - itemAlignment.next().get(), - itemClick -> drawGames(type) + for(GameType type : GameType.values()) { + this.setClickableItem( + ItemStack.builder(type.getIcon()) + .customName(type.getTitle().getAssembled(p)) + .lore(type.getDescription().addWrap().getWrappedAssembled(p)) + .build(), + itemAlignment.next().get(), + itemClick -> this.drawGames(type) ); } for(int i = 9; i <= 17; i++) { - setDummyItem(Material.CYAN_STAINED_GLASS_PANE, i); + this.setDummyItem(Material.CYAN_STAINED_GLASS_PANE, i); } @@ -45,23 +46,23 @@ public class MinigameSelectInventory extends InteractableInventory { private void drawGames(GameType type) { for(int i = 18; i <= 53; i++) { - setDummyItem(Material.AIR, i); + this.setDummyItem(Material.AIR, i); } int offset = 18; List games = Arrays.stream(GameList.values()).filter(gameList -> gameList.getType().equals(type)).toList(); InventoryItemAlignment itemAlignment = new InventoryItemAlignment(games.size(), 4); - for (GameList game : games) { + for(GameList game : games) { GameFactory gameFactory = game.getFactory(); - setClickableItem( - ItemStack.builder(gameFactory.symbol()) - .customName(gameFactory.name().getAssembled(p)) - .lore(gameFactory.description().addWrap().getWrappedAssembled(p)) - .build(), - offset + itemAlignment.next().get(), - itemClick -> itemClick.getPlayer().openInventory(new GameConfigurationInventory(room, itemClick.getPlayer(), gameFactory)) + this.setClickableItem( + ItemStack.builder(gameFactory.symbol()) + .customName(gameFactory.name().getAssembled(this.p)) + .lore(gameFactory.description().addWrap().getWrappedAssembled(this.p)) + .build(), + offset + itemAlignment.next().get(), + itemClick -> itemClick.getPlayer().openInventory(new GameConfigurationInventory(this.room, itemClick.getPlayer(), gameFactory)) ); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/instance/transfer/Transfer.java b/src/main/java/eu/mhsl/minenet/minigames/instance/transfer/Transfer.java index 1b527c7..0c63b57 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/instance/transfer/Transfer.java +++ b/src/main/java/eu/mhsl/minenet/minigames/instance/transfer/Transfer.java @@ -11,8 +11,8 @@ import net.minestom.server.instance.block.Block; public class Transfer extends MineNetInstance implements Spawnable { public Transfer() { super(Dimension.THE_END.key); - eventNode().addListener(PlayerMoveEvent.class, CommonEventHandles::cancel); - setBlock(0, 0, 0, Block.BARRIER); + this.eventNode().addListener(PlayerMoveEvent.class, CommonEventHandles::cancel); + this.setBlock(0, 0, 0, Block.BARRIER); } @Override diff --git a/src/main/java/eu/mhsl/minenet/minigames/lang/Lang.java b/src/main/java/eu/mhsl/minenet/minigames/lang/Lang.java index 2dd6d49..1da619f 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/lang/Lang.java +++ b/src/main/java/eu/mhsl/minenet/minigames/lang/Lang.java @@ -14,17 +14,17 @@ public class Lang { } public void addEntry(String key, String value) { - entries.put(key, value); + this.entries.put(key, value); } public String getEntry(String key) { - return entries.computeIfAbsent(key, s -> { + return this.entries.computeIfAbsent(key, s -> { Logger.getLogger("localisation").warning(s + " is not known by translation files!"); return new DummyLang().getEntry(s); }); } public String getLangId() { - return langId; + return this.langId; } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/lang/Languages.java b/src/main/java/eu/mhsl/minenet/minigames/lang/Languages.java index 66bf0bb..67a6da7 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/lang/Languages.java +++ b/src/main/java/eu/mhsl/minenet/minigames/lang/Languages.java @@ -8,35 +8,37 @@ import java.nio.file.Files; import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.logging.Logger; public class Languages { - private static Languages instance; + public static final String defaultLanguage = "de_de"; private static final Logger logger = Logger.getLogger("translation"); - - + private static Languages instance; private final Map languages = new HashMap<>(); - public static final String defaultLanguage = "de_de"; + private Languages() { + this.readAll(); + } + public static Languages getInstance() { if(instance == null) instance = new Languages(); return instance; } - private Languages() { - readAll(); + public Lang getLanguage(Player p) { + return this.getLanguage(p.getSettings().locale().toString()); } - public Lang getLanguage(Player p) { - return getLanguage(p.getSettings().locale().toString()); // TODO funktioniert die locale noch? - } public Lang getLanguage(String mapId) { - return languages.computeIfAbsent(mapId.toLowerCase(), unused -> languages.computeIfAbsent(defaultLanguage, (key) -> new DummyLang())); + return this.languages.computeIfAbsent(mapId.toLowerCase(), unused -> this.languages.computeIfAbsent(defaultLanguage, (key) -> new DummyLang())); } private void readAll() { File locales = new File(Resource.LOCALES.getPath().toString()); - File[] files = Arrays.stream(locales.listFiles(File::canRead)).filter(file -> file.getName().endsWith("map.csv")).toArray(File[]::new); + File[] files = Arrays.stream(Objects.requireNonNull(locales.listFiles(File::canRead))) + .filter(file -> file.getName().endsWith("map.csv")) + .toArray(File[]::new); if(files.length == 0) { logger.warning("Failed to find any Language-files!"); @@ -53,7 +55,7 @@ public class Languages { for(String line : Files.readAllLines(locale.toPath())) { //line = line.replaceAll("[^\\p{L}\\s,#_+.:;]+", ""); - line = line.replaceAll("[^a-zA-Z0-9äöüÄÖÜ ,:;#_+]", ""); + line = line.replaceAll("[^a-zA-Z0-9äöüÄÖÜ ,:;#!_+-]", ""); String[] columns = line.split(";"); if(columns.length < 1) continue; @@ -66,20 +68,22 @@ public class Languages { index++; if(langId.endsWith("map")) continue; - languages.computeIfAbsent(langId, Lang::new); - Lang lang = languages.get(langId); + this.languages.computeIfAbsent(langId, Lang::new); + Lang lang = this.languages.get(langId); langColumn.put(index, lang); - languages.put(langId, lang); + this.languages.put(langId, lang); } } else if(columns[0].startsWith("ns:")) { - if(!computedFileHeader) throw new IllegalStateException("Cannot compute namespace data before file-header was red!"); + if(!computedFileHeader) + throw new IllegalStateException("Cannot compute namespace data before file-header was red!"); namespace = columns[0].split(":")[1]; } else { // file contents - if(!computedFileHeader) throw new IllegalStateException("Cannot compute translation data before file-header was red!"); + if(!computedFileHeader) + throw new IllegalStateException("Cannot compute translation data before file-header was red!"); int index = 0; String mapId = ""; for(String translation : columns) { @@ -97,7 +101,7 @@ public class Languages { } } - } catch (Exception e) { + } catch(Exception e) { logger.warning("Exception while parsing lang-files: " + e.getMessage()); e.printStackTrace(); } diff --git a/src/main/java/eu/mhsl/minenet/minigames/message/Countdown.java b/src/main/java/eu/mhsl/minenet/minigames/message/Countdown.java index 0e7df40..e1fb6bd 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/message/Countdown.java +++ b/src/main/java/eu/mhsl/minenet/minigames/message/Countdown.java @@ -9,8 +9,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; public class Countdown { - public Countdown(Class messageType) { - } public CompletableFuture countdown(Audience targets, int from, Consumer modifier) { CompletableFuture future = new CompletableFuture<>(); AtomicInteger count = new AtomicInteger(from); @@ -23,7 +21,7 @@ public class Countdown { CountdownModifier countdownModifier = new CountdownModifier(count.getAndDecrement()); modifier.accept(countdownModifier); countdownModifier.message.send(targets); - } catch (Exception e) { + } catch(Exception e) { throw new RuntimeException(e); } return TaskSchedule.seconds(1); @@ -33,8 +31,9 @@ public class Countdown { public static class CountdownModifier { - public TranslatableMessage message; public final int timeLeft; + public TranslatableMessage message; + public CountdownModifier(int timeLeft) { this.timeLeft = timeLeft; } diff --git a/src/main/java/eu/mhsl/minenet/minigames/message/Icon.java b/src/main/java/eu/mhsl/minenet/minigames/message/Icon.java index 35efac2..8ed8fbf 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/message/Icon.java +++ b/src/main/java/eu/mhsl/minenet/minigames/message/Icon.java @@ -3,29 +3,34 @@ package eu.mhsl.minenet.minigames.message; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +@SuppressWarnings("UnnecessaryUnicodeEscape") public enum Icon { SCIENCE("\uD83E\uDDEA", NamedTextColor.LIGHT_PURPLE), STAR("\u2606", NamedTextColor.GOLD), CHAT("\u276F\u276F", NamedTextColor.WHITE), SUCCESS("\u2714", NamedTextColor.GREEN), - ERROR("\u274C", NamedTextColor.RED); + ERROR("\u274C", NamedTextColor.RED), + TIME("\u231B", NamedTextColor.YELLOW), + INFO("\uD83D\uDD14", NamedTextColor.AQUA), + LEAVE("\u2B05", NamedTextColor.DARK_GRAY); private final String symbol; private final NamedTextColor color; + Icon(String symbol, NamedTextColor color) { this.symbol = symbol; this.color = color; } public String getSymbol() { - return symbol; + return this.symbol; } public NamedTextColor getColor() { - return color; + return this.color; } public Component getComponent() { - return Component.text(getSymbol(), getColor()); + return Component.text(this.getSymbol(), this.getColor()); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/message/TranslatableMessage.java b/src/main/java/eu/mhsl/minenet/minigames/message/TranslatableMessage.java index 855ee5c..d51bab0 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/message/TranslatableMessage.java +++ b/src/main/java/eu/mhsl/minenet/minigames/message/TranslatableMessage.java @@ -14,60 +14,75 @@ import java.util.ArrayList; import java.util.List; public abstract class TranslatableMessage implements Sendable { - protected int indention = 0; final List chain = new ArrayList<>(); + protected int indention = 0; public TranslatableMessage() { } public TranslatableMessage appendStatic(Component component) { - chain.add(component); + this.chain.add(component); return this; } public TranslatableMessage appendStatic(String text) { - chain.add(Component.text(text)); + this.chain.add(Component.text(text)); + return this; + } + + public TranslatableMessage appendSpace() { + this.chain.add(Component.text(" ")); return this; } public TranslatableMessage appendTranslated(String mapId) { - chain.add(TranslatedComponent.byId(mapId)); + this.chain.add(TranslatedComponent.byId(mapId).setColor(NamedTextColor.WHITE)); + return this; + } + + public TranslatableMessage appendTranslated(String mapId, NamedTextColor color) { + this.chain.add(TranslatedComponent.byId(mapId).setColor(color)); + return this; + } + + public TranslatableMessage appendTranslated(TranslatedComponent component) { + this.chain.add(component); return this; } public TranslatableMessage list(List list) { - newLine(); + this.newLine(); list.forEach(s -> { - chain.add(Component.text(s)); - newLine(); + this.chain.add(Component.text(s)); + this.newLine(); }); return this; } public TranslatableMessage numberedList(List list) { - newLine(); - for (int i = 0; i < list.size(); i++) { - appendStatic(i+1 + ". "); - appendStatic(list.get(i)); - newLine(); + this.newLine(); + for(int i = 0; i < list.size(); i++) { + this.appendStatic(i + 1 + ". "); + this.appendStatic(list.get(i)); + this.newLine(); } return this; } public TranslatableMessage pipe() { - chain.add(Component.text(" | ", NamedTextColor.DARK_GRAY)); + this.chain.add(Component.text(" | ", NamedTextColor.DARK_GRAY)); return this; } public TranslatableMessage quote(String text) { - chain.add(Component.text("'" + text + "'")); + this.chain.add(Component.text("'" + text + "'")); return this; } public TranslatableMessage newLine() { - chain.add(Component.text("\n")); - this.appendStatic(" ".repeat(indention)); + this.chain.add(Component.text("\n")); + this.appendStatic(" ".repeat(this.indention)); return this; } @@ -87,15 +102,15 @@ public abstract class TranslatableMessage implements Sendable { } public TranslatableMessage resetIndention() { - indention = 0; + this.indention = 0; return this; } - public Component build(Player p) { ComponentBuilder out = Component.text(); - chain.forEach(componentLike -> { - if(componentLike == null) componentLike = Component.text("NULL", NamedTextColor.RED, TextDecoration.ITALIC); + this.chain.forEach(componentLike -> { + if(componentLike == null) + componentLike = Component.text("NULL", NamedTextColor.RED, TextDecoration.ITALIC); if(componentLike instanceof Translatable t) t.assemble(p); out.append(componentLike); }); diff --git a/src/main/java/eu/mhsl/minenet/minigames/message/component/NamespacedTranslatable.java b/src/main/java/eu/mhsl/minenet/minigames/message/component/NamespacedTranslatable.java deleted file mode 100644 index a5bae9e..0000000 --- a/src/main/java/eu/mhsl/minenet/minigames/message/component/NamespacedTranslatable.java +++ /dev/null @@ -1,14 +0,0 @@ -package eu.mhsl.minenet.minigames.message.component; - - -public class NamespacedTranslatable { - private final String namespace; - - public NamespacedTranslatable(String namespace) { - this.namespace = namespace; - } - - public TranslatedComponent get(String mapId) { - return TranslatedComponent.byId(namespace + mapId); - } -} diff --git a/src/main/java/eu/mhsl/minenet/minigames/message/component/TranslatedComponent.java b/src/main/java/eu/mhsl/minenet/minigames/message/component/TranslatedComponent.java index 311635f..3c5cc39 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/message/component/TranslatedComponent.java +++ b/src/main/java/eu/mhsl/minenet/minigames/message/component/TranslatedComponent.java @@ -18,6 +18,10 @@ public class TranslatedComponent implements ComponentLike, Translatable { private NamedTextColor color; private int wrap = 0; + private TranslatedComponent(String mapId) { + this.mapId = mapId; + } + public static TranslatedComponent byId(String mapId) { return new TranslatedComponent(mapId); } @@ -26,20 +30,17 @@ public class TranslatedComponent implements ComponentLike, Translatable { return new TranslatedComponent(mapid).getAssembled(p); } - private TranslatedComponent(String mapId) { - this.mapId = mapId; - } - public void assemble(Player p) { - result = Languages.getInstance().getLanguage(p).getEntry(mapId); - if(wrap > 0) { - result = WordUtils.wrap(result, wrap, "\n", false); + this.result = Languages.getInstance().getLanguage(p).getEntry(this.mapId); + if(this.wrap > 0) { + this.result = WordUtils.wrap(this.result, this.wrap, "\n", false); } } public TranslatedComponent addWrap() { return this.addWrap(30); } + public TranslatedComponent addWrap(int wrap) { this.wrap = wrap; return this; @@ -47,12 +48,12 @@ public class TranslatedComponent implements ComponentLike, Translatable { public Component getAssembled(Player p) { this.assemble(p); - return asComponent(); + return this.asComponent(); } public List getWrappedAssembled(Player p) { this.assemble(p); - return asWrappedComponent(); + return this.asWrappedComponent(); } public TranslatedComponent setColor(NamedTextColor color) { @@ -62,16 +63,16 @@ public class TranslatedComponent implements ComponentLike, Translatable { @Override public @NotNull Component asComponent() { - return this.componentFromString(result); + return this.componentFromString(this.result); } public List asWrappedComponent() { - return Arrays.stream(result.split("\n")).map(this::componentFromString).toList(); + return Arrays.stream(this.result.split("\n")).map(this::componentFromString).toList(); } private Component componentFromString(String data) { - if(color != null) - return Component.text(data, color); + if(this.color != null) + return Component.text(data, this.color); else return Component.text(data); } diff --git a/src/main/java/eu/mhsl/minenet/minigames/message/type/ActionBarMessage.java b/src/main/java/eu/mhsl/minenet/minigames/message/type/ActionBarMessage.java index 622e48e..f5123aa 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/message/type/ActionBarMessage.java +++ b/src/main/java/eu/mhsl/minenet/minigames/message/type/ActionBarMessage.java @@ -6,6 +6,6 @@ import net.minestom.server.entity.Player; public class ActionBarMessage extends TranslatableMessage { @Override public void send(Player p) { - p.sendActionBar(build(p)); + p.sendActionBar(this.build(p)); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/message/type/ChatMessage.java b/src/main/java/eu/mhsl/minenet/minigames/message/type/ChatMessage.java index 62bf22d..55762c7 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/message/type/ChatMessage.java +++ b/src/main/java/eu/mhsl/minenet/minigames/message/type/ChatMessage.java @@ -6,25 +6,25 @@ import net.minestom.server.entity.Player; public class ChatMessage extends TranslatableMessage { public ChatMessage() { - construct(Icon.CHAT, false); + this.construct(Icon.CHAT, false); } public ChatMessage(Icon icon) { - construct(icon, false); + this.construct(icon, false); } public ChatMessage(Icon icon, boolean preSpace) { - construct(icon, preSpace); + this.construct(icon, preSpace); } private void construct(Icon icon, boolean preSpace) { if(preSpace) { super.appendStatic(" "); - pipe(); + this.pipe(); super.appendStatic("\n"); } - appendStatic(icon.getComponent()); - pipe(); + this.appendStatic(icon.getComponent()); + this.pipe(); } @Override @@ -37,6 +37,6 @@ public class ChatMessage extends TranslatableMessage { } public void send(Player p) { - p.sendMessage(build(p)); + p.sendMessage(this.build(p)); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/message/type/TitleMessage.java b/src/main/java/eu/mhsl/minenet/minigames/message/type/TitleMessage.java index 0cc484a..1df6093 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/message/type/TitleMessage.java +++ b/src/main/java/eu/mhsl/minenet/minigames/message/type/TitleMessage.java @@ -2,13 +2,8 @@ package eu.mhsl.minenet.minigames.message.type; import eu.mhsl.minenet.minigames.message.TranslatableMessage; import net.kyori.adventure.audience.Audience; -import net.kyori.adventure.text.Component; import net.kyori.adventure.title.Title; -import net.kyori.adventure.title.TitlePart; import net.minestom.server.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.UnknownNullability; import java.time.Duration; import java.util.function.Consumer; @@ -18,13 +13,15 @@ public class TitleMessage extends TranslatableMessage { private SubtitleMessage subtitle = new SubtitleMessage(); public TitleMessage() { - times = Title.Times.times(Duration.ZERO, Duration.ofSeconds(1), Duration.ZERO); + this.times = Title.Times.times(Duration.ZERO, Duration.ofSeconds(1), Duration.ZERO); } + public TitleMessage(Duration stay) { - times = Title.Times.times(Duration.ZERO, stay, Duration.ZERO); + this.times = Title.Times.times(Duration.ZERO, stay, Duration.ZERO); } + public TitleMessage(Duration stay, Duration fade) { - times = Title.Times.times(fade, stay, fade); + this.times = Title.Times.times(fade, stay, fade); } public void setTimes(Title.Times times) { @@ -33,32 +30,12 @@ public class TitleMessage extends TranslatableMessage { public TranslatableMessage subtitle(Consumer callback) { this.subtitle = new SubtitleMessage(); - callback.accept(subtitle); + callback.accept(this.subtitle); return this; } @Override public void send(Player p) { - Audience.audience(p).showTitle(new Title() { - @Override - public @NotNull Component title() { - return build(p); - } - - @Override - public @NotNull Component subtitle() { - return subtitle.build(p); - } - - @Override - public @Nullable Times times() { - return times; - } - - @Override - public @UnknownNullability T part(@NotNull TitlePart part) { - return null; - } - }); + Audience.audience(p).showTitle(Title.title(this.build(p), this.subtitle.build(p), this.times)); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/score/FirstWinsScore.java b/src/main/java/eu/mhsl/minenet/minigames/score/FirstWinsScore.java index bc505dd..16a2886 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/score/FirstWinsScore.java +++ b/src/main/java/eu/mhsl/minenet/minigames/score/FirstWinsScore.java @@ -10,7 +10,7 @@ import java.util.Set; public class FirstWinsScore extends Score { @Override public void insertResultImplementation(Set p) { - getScores().add(p); + this.getScores().add(p); } @Override diff --git a/src/main/java/eu/mhsl/minenet/minigames/score/LastWinsScore.java b/src/main/java/eu/mhsl/minenet/minigames/score/LastWinsScore.java index c37cb96..a93e173 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/score/LastWinsScore.java +++ b/src/main/java/eu/mhsl/minenet/minigames/score/LastWinsScore.java @@ -10,7 +10,7 @@ import java.util.Set; public class LastWinsScore extends Score { @Override public void insertResultImplementation(Set p) { - getScores().addFirst(p); + this.getScores().addFirst(p); } @Override diff --git a/src/main/java/eu/mhsl/minenet/minigames/score/LowestPointsWinScore.java b/src/main/java/eu/mhsl/minenet/minigames/score/LowestPointsWinScore.java new file mode 100644 index 0000000..384e35b --- /dev/null +++ b/src/main/java/eu/mhsl/minenet/minigames/score/LowestPointsWinScore.java @@ -0,0 +1,29 @@ +package eu.mhsl.minenet.minigames.score; + +import eu.mhsl.minenet.minigames.util.MapUtil; +import net.minestom.server.entity.Player; + +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public class LowestPointsWinScore extends PointsWinScore { + @Override + protected void insertResultImplementation(Set p, int currentPoints) { + Set combined = this.scoreOrder.entrySet().stream() + .filter(entrySet -> Objects.equals(entrySet.getValue(), currentPoints)) + .map(Map.Entry::getKey) + .findFirst() + .orElseGet(() -> { + Set s = new HashSet<>(); + this.scoreOrder.put(s, currentPoints); + return s; + }); + combined.addAll(p); + + this.scoreOrder = MapUtil.sortReversedByValue(this.scoreOrder); + this.getScores().clear(); + this.scoreOrder.forEach((player, integer) -> this.getScores().addFirst(player)); + } +} diff --git a/src/main/java/eu/mhsl/minenet/minigames/score/PointsWinScore.java b/src/main/java/eu/mhsl/minenet/minigames/score/PointsWinScore.java index d9190f7..2f50458 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/score/PointsWinScore.java +++ b/src/main/java/eu/mhsl/minenet/minigames/score/PointsWinScore.java @@ -12,14 +12,24 @@ import java.util.*; import java.util.stream.Collectors; public class PointsWinScore extends Score { - private Map, Integer> scoreOrder = new HashMap<>(); + Map, Integer> scoreOrder = new HashMap<>(); @Override protected void insertResultImplementation(Set p, int currentPoints) { - this.scoreOrder.put(p, currentPoints); + Set combined = this.scoreOrder.entrySet().stream() + .filter(entrySet -> Objects.equals(entrySet.getValue(), currentPoints)) + .map(Map.Entry::getKey) + .findFirst() + .orElseGet(() -> { + Set s = new HashSet<>(); + this.scoreOrder.put(s, currentPoints); + return s; + }); + combined.addAll(p); + this.scoreOrder = MapUtil.sortByValue(this.scoreOrder); - getScores().clear(); - this.scoreOrder.forEach((player, integer) -> getScores().addFirst(player)); + this.getScores().clear(); + this.scoreOrder.forEach((player, integer) -> this.getScores().addFirst(player)); } @Override @@ -29,33 +39,32 @@ public class PointsWinScore extends Score { @Override public void setDone() { - if(isClosed()) return; - close(); + if(this.isClosed()) return; + this.close(); new ChatMessage(Icon.STAR, true) .appendTranslated("score#result") .newLine() .indent() .numberedList( - getScores() + this.getScores() .stream() .filter(players -> !players.stream() .filter(player -> !player.getUsername().isBlank()) .toList() .isEmpty()) - .map(players -> players - .stream() - .filter(player -> scoreOrder.get(Set.of(player)) != null) - .map(player -> player.getUsername()+" : "+scoreOrder.get(Set.of(player)).toString()) + .map(players -> players.stream() + .map(Player::getUsername) + .sorted() .collect(Collectors.joining(", ")) - ) - .toList() + + " : " + this.scoreOrder.get(players) + ).toList() ) .undent() .newLine() .appendTranslated("score#thanks") - .send(instance.getPlayers()); + .send(this.instance.getPlayers()); - instance.stop(); + this.instance.stop(); } @Override diff --git a/src/main/java/eu/mhsl/minenet/minigames/score/Score.java b/src/main/java/eu/mhsl/minenet/minigames/score/Score.java index dce3e92..967cc0d 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/score/Score.java +++ b/src/main/java/eu/mhsl/minenet/minigames/score/Score.java @@ -16,12 +16,13 @@ import java.util.Set; import java.util.stream.Collectors; public abstract class Score { + private final List> scores = new ArrayList<>(); + protected Game instance; private int ignoreLastPlayers = 0; private boolean isClosed = false; - protected Game instance; - private final List> scores = new ArrayList<>(); - public Score() {} + public Score() { + } protected abstract void insertResultImplementation(Set p); @@ -29,6 +30,11 @@ public abstract class Score { throw new NotImplementedException("This Score type is not able to process points"); } + public void insertMultiple(Set p) { + p.forEach(player -> this.insertResultProcessor(player, () -> {})); + this.insertResultImplementation(p); + } + public void insertResult(Player p) { this.insertResultProcessor(p, () -> this.insertResultImplementation(Set.of(p))); } @@ -39,28 +45,28 @@ public abstract class Score { public void attachListeners() { this.instance.eventNode() - .addListener(AddEntityToInstanceEvent.class, addEntityToInstanceEvent -> checkGameEnd()) - .addListener(RemoveEntityFromInstanceEvent.class, addEntityToInstanceEvent -> checkGameEnd()) - .addListener(PlayerDisconnectEvent.class, addEntityToInstanceEvent -> checkGameEnd()); + .addListener(AddEntityToInstanceEvent.class, addEntityToInstanceEvent -> this.checkGameEnd()) + .addListener(RemoveEntityFromInstanceEvent.class, addEntityToInstanceEvent -> this.checkGameEnd()) + .addListener(PlayerDisconnectEvent.class, addEntityToInstanceEvent -> this.checkGameEnd()); } protected void checkGameEnd() { if(this.isClosed()) return; - if(!instance.isRunning()) return; - if(instance.getPlayers().isEmpty()) return; - if(resultCount() >= instance.getPlayers().size() - ignoreLastPlayers) { - if(ignoreLastPlayers > 0) { - insertRemainingPlayers(instance.getPlayers()); + if(!this.instance.isRunning()) return; + if(this.instance.getPlayers().isEmpty()) return; + if(this.resultCount() >= this.instance.getPlayers().size() - this.ignoreLastPlayers) { + if(this.ignoreLastPlayers > 0) { + this.insertRemainingPlayers(this.instance.getPlayers()); } - setDone(); + this.setDone(); } } protected abstract TranslatableMessage scoreMessage(); public void insertRemainingPlayers(Set players) { - this.insertResultImplementation(players.stream().filter(p -> !hasResult(p)).collect(Collectors.toSet())); - setDone(); + this.insertResultImplementation(players.stream().filter(p -> !this.hasResult(p)).collect(Collectors.toSet())); + this.setDone(); } public boolean hasResult(Player p) { @@ -72,49 +78,47 @@ public abstract class Score { } public void setDone() { - if(isClosed) return; - isClosed = true; + if(this.isClosed) return; + this.isClosed = true; new ChatMessage(Icon.STAR, true) - .appendTranslated("score#result") - .newLine() - .indent() - .numberedList( - getScores() - .stream() - .map(players -> players - .stream() - .map(Player::getUsername) - .collect(Collectors.joining(", ")) - ) - .toList() - ) - .undent() - .newLine() - .appendTranslated("score#thanks") - .send(instance.getPlayers()); + .appendTranslated("score#result") + .newLine() + .indent() + .numberedList( + this.getScores() + .stream() + .map(players -> players + .stream() + .map(Player::getUsername) + .collect(Collectors.joining(", ")) + ) + .toList() + ) + .undent() + .newLine() + .appendTranslated("score#thanks") + .send(this.instance.getPlayers()); - instance.stop(); + this.instance.stop(); } private void insertResultProcessor(Player p, Runnable callback) { - if(hasResult(p)) return; + if(this.hasResult(p)) return; this.scoreMessage().send(p); callback.run(); this.checkGameEnd(); } public boolean isClosed() { - return isClosed; + return this.isClosed; } - public void close() { isClosed = true; } - - protected void onGameEnd() { - this.instance.stop(); + public void close() { + this.isClosed = true; } public List> getScores() { - return scores; + return this.scores; } public void setInstance(Game instance) { @@ -122,7 +126,7 @@ public abstract class Score { } public int getIgnoreLastPlayers() { - return ignoreLastPlayers; + return this.ignoreLastPlayers; } public void setIgnoreLastPlayers(int ignoreLastPlayers) { diff --git a/src/main/java/eu/mhsl/minenet/minigames/score/tournament/Rewards.java b/src/main/java/eu/mhsl/minenet/minigames/score/tournament/Rewards.java index ed62832..a17aed0 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/score/tournament/Rewards.java +++ b/src/main/java/eu/mhsl/minenet/minigames/score/tournament/Rewards.java @@ -4,5 +4,6 @@ import java.util.List; import java.util.Map; import java.util.UUID; -public record Rewards(String memorialMaterial, String memorialTitle, String memorialLore, List memorials, String material, Map rewards) { +public record Rewards(String memorialMaterial, String memorialTitle, String memorialLore, List memorials, + String material, Map rewards) { } diff --git a/src/main/java/eu/mhsl/minenet/minigames/score/tournament/Tournament.java b/src/main/java/eu/mhsl/minenet/minigames/score/tournament/Tournament.java index 8c2cc90..35acaa7 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/score/tournament/Tournament.java +++ b/src/main/java/eu/mhsl/minenet/minigames/score/tournament/Tournament.java @@ -4,6 +4,7 @@ import eu.mhsl.minenet.minigames.score.Score; import net.minestom.server.entity.Player; import java.util.*; +import java.util.stream.Collectors; public class Tournament { private final List gameScores = new ArrayList<>(); @@ -14,23 +15,14 @@ public class Tournament { this.gameScores.add(score); } - public int getScoreCount() { - return this.gameScores.size(); - } - - public void reset() { - this.gameScores.clear(); - } - - public Map getGameScores() { Map data = new HashMap<>(); - gameScores.forEach(game -> { + this.gameScores.forEach(game -> { int count = 0; - for (Set players : game.getScores()) { + for(Set players : game.getScores()) { count++; - for (Player player : players) { - int points = (game.getScores().size() - count) + boost(count, game.getScores().size()); + for(Player player : players) { + int points = (game.getScores().size() - count) + this.boost(count, game.getScores().size()); data.computeIfPresent(player, (existingPlayer, existingPoints) -> existingPoints + points); data.computeIfAbsent(player, newPlayer -> points); @@ -43,36 +35,41 @@ public class Tournament { public Rewards getRewards() { Map itemCount = new HashMap<>(); int count = 0; - for (Player player : getPlaces()) { + for(Set players : this.getPlaces()) { if(count >= this.rewardConfiguration.rewardCount().size()) break; - itemCount.put(player.getUuid(), this.rewardConfiguration.rewardCount().get(count)); + for(Player player : players) { + itemCount.put(player.getUuid(), this.rewardConfiguration.rewardCount().get(count)); + } count++; } return new Rewards( - this.memorialConfiguration.memorialMaterial().namespace().value(), - this.memorialConfiguration.memorialTitle(), - this.memorialConfiguration.memorialLore(), - getPlaces().stream().map(Player::getUuid).toList(), - this.rewardConfiguration.item().namespace().value(), - itemCount + this.memorialConfiguration.memorialMaterial().key().value(), + this.memorialConfiguration.memorialTitle(), + this.memorialConfiguration.memorialLore(), + this.getGameScores().keySet().stream().map(Player::getUuid).toList(), + this.rewardConfiguration.item().key().value(), + itemCount ); } - public List getPlaces() { - List players = new ArrayList<>( - getGameScores().entrySet().stream() - .sorted(Map.Entry.comparingByValue()) - .map(Map.Entry::getKey) - .toList() - ); + public List> getPlaces() { + Map> players = this.getGameScores().entrySet().stream() + .collect( + Collectors.groupingBy( + Map.Entry::getValue, + Collectors.mapping(Map.Entry::getKey, Collectors.toSet()) + ) + ); - Collections.reverse(players); - return players; + return players.entrySet().stream() + .sorted(Map.Entry.>comparingByKey().reversed()) + .map(Map.Entry::getValue) + .toList(); } private int boost(int selfPlace, int placeCount) { - return switch (selfPlace) { + return switch(selfPlace) { case 0 -> placeCount / 3; case 1 -> placeCount / 4; case 2 -> placeCount / 5; diff --git a/src/main/java/eu/mhsl/minenet/minigames/score/tournament/TournamentDisplay.java b/src/main/java/eu/mhsl/minenet/minigames/score/tournament/TournamentDisplay.java index c917e14..6fe9782 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/score/tournament/TournamentDisplay.java +++ b/src/main/java/eu/mhsl/minenet/minigames/score/tournament/TournamentDisplay.java @@ -8,57 +8,67 @@ import eu.mhsl.minenet.minigames.message.Icon; import eu.mhsl.minenet.minigames.message.type.ChatMessage; import net.minestom.server.MinecraftServer; import net.minestom.server.coordinate.Pos; -import net.minestom.server.entity.*; +import net.minestom.server.entity.GameMode; +import net.minestom.server.entity.Player; import net.minestom.server.event.player.PlayerMoveEvent; import net.minestom.server.instance.anvil.AnvilLoader; +import net.minestom.server.world.DimensionType; +import java.util.Comparator; import java.util.List; +import java.util.Set; public class TournamentDisplay extends MineNetInstance implements Spawnable { - private final List places; + private final List> places; private final Tournament tournament; - private final Pos[] placePositions = new Pos[] { - new Pos(8.5, -56, 22.5, 180, 0), - new Pos(12.5, -57, 20.5, 180, 0), - new Pos(4.5, -58, 20.5, 180, 0) + private final Pos[] placePositions = new Pos[]{ + new Pos(8.5, -56, 22.5, 180, 0), + new Pos(12.5, -57, 20.5, 180, 0), + new Pos(4.5, -58, 20.5, 180, 0) }; public TournamentDisplay(Tournament tournament) { super(Dimension.OVERWORLD.key); - setChunkLoader(new AnvilLoader(Resource.RESULT_DISPLAY.getPath())); + this.setChunkLoader(new AnvilLoader(Resource.RESULT_DISPLAY.getPath(), DimensionType.OVERWORLD.key())); this.places = tournament.getPlaces(); this.tournament = tournament; - this.places.forEach(player -> player.setGameMode(GameMode.ADVENTURE)); + this.places.forEach(players -> players.forEach(player -> player.setGameMode(GameMode.ADVENTURE))); - eventNode().addListener(PlayerMoveEvent.class, playerMoveEvent -> { - if(isOnDisplay(playerMoveEvent.getPlayer()) && !playerMoveEvent.getNewPosition().sameBlock(placePositions[getRankPosition(playerMoveEvent.getPlayer())])) { + this.eventNode().addListener(PlayerMoveEvent.class, playerMoveEvent -> { + if(this.isOnDisplay(playerMoveEvent.getPlayer()) && !playerMoveEvent.getNewPosition().sameBlock(this.placePositions[this.getRankPosition(playerMoveEvent.getPlayer())])) { playerMoveEvent.setCancelled(true); } }); } private boolean isOnDisplay(Player player) { - if(getRankPosition(player) == -1) return false; - return getRankPosition(player) < this.placePositions.length; + if(this.getRankPosition(player) == -1) return false; + return this.getRankPosition(player) < this.placePositions.length; } private int getRankPosition(Player player) { - return this.places.indexOf(player); + for(int i = 0; i < this.places.size(); i++) { + if(this.places.get(i).contains(player)) return i; + } + return -1; } @Override protected boolean onPlayerJoin(Player p) { MinecraftServer.getSchedulerManager().scheduleNextTick(() -> { - if(isOnDisplay(p)) { + if(this.isOnDisplay(p)) { p.sendMessage("You are on Display!"); - p.teleport(placePositions[getRankPosition(p)]); + p.teleport(this.placePositions[this.getRankPosition(p)]); } }); + List players = this.places.stream() + .flatMap(s -> s.stream().sorted(Comparator.comparing(Player::getUsername))) + .toList(); new ChatMessage(Icon.STAR) - .numberedList(this.places.stream().map(player -> String.format("%s - %s Punkte", player.getUsername(), tournament.getGameScores().get(player))).toList()) - .send(p); + .list(players.stream().map(player -> String.format("%d. %s - %s Punkte", this.getRankPosition(player) + 1, player.getUsername(), this.tournament.getGameScores().get(player))).toList()) + .send(p); return false; } diff --git a/src/main/java/eu/mhsl/minenet/minigames/server/tasks/TablistUpdateTask.java b/src/main/java/eu/mhsl/minenet/minigames/server/tasks/TablistUpdateTask.java index 4422c6d..4f5d306 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/server/tasks/TablistUpdateTask.java +++ b/src/main/java/eu/mhsl/minenet/minigames/server/tasks/TablistUpdateTask.java @@ -13,21 +13,21 @@ public class TablistUpdateTask implements Runnable { @Override public void run() { Collection players = MinecraftServer.getConnectionManager().getOnlinePlayers(); - if (players.isEmpty()) return; + if(players.isEmpty()) return; final Component header = - Component.newline() - .append(Component.text("MineNet Network", NamedTextColor.GOLD)) - .append(Component.newline()).append(Component.text("Players: " + players.size())) - .append(Component.newline()) - .append(Component.newline()).append(Component.text("RAM: " + Monitoring.getRamUsage() + " MB", NamedTextColor.GRAY)) - .append(Component.newline()).append(Component.text("TICK: " + Monitoring.getTickMonitor().getTickTime() + "ms", NamedTextColor.GRAY)) - .append(Component.newline()); + Component.newline() + .append(Component.text("MineNet Network", NamedTextColor.GOLD)) + .append(Component.newline()).append(Component.text("Players: " + players.size())) + .append(Component.newline()) + .append(Component.newline()).append(Component.text("RAM: " + Monitoring.getRamUsage() + " MB", NamedTextColor.GRAY)) + .append(Component.newline()).append(Component.text("TICK: " + Monitoring.getTickMonitor().getTickTime() + "ms", NamedTextColor.GRAY)) + .append(Component.newline()); final Component footer = - Component.newline() - .append(Component.text("mhsl.eu")) - .append(Component.newline()); + Component.newline() + .append(Component.text("mhsl.eu")) + .append(Component.newline()); Audiences.players().sendPlayerListHeaderAndFooter(header, footer); } diff --git a/src/main/java/eu/mhsl/minenet/minigames/shared/entity/InteractableEntity.java b/src/main/java/eu/mhsl/minenet/minigames/shared/entity/InteractableEntity.java index 70e6d86..b3900b6 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/shared/entity/InteractableEntity.java +++ b/src/main/java/eu/mhsl/minenet/minigames/shared/entity/InteractableEntity.java @@ -15,47 +15,44 @@ public class InteractableEntity extends EntityCreature { /** * Declares an Entity with direct callbacks on interaction + * * @param entityType type of entity */ public InteractableEntity(@NotNull EntityType entityType) { super(entityType); - eventNode() - .addListener(AddEntityToInstanceEvent.class, this::setInstanceEvent) - - .addListener(AddEntityToInstanceEvent.class, this::onSpawn) - .addListener(RemoveEntityFromInstanceEvent.class, this::onDespawn) - .addListener(PlayerEntityInteractEvent.class, this::onInteract) //TODO Why are some of these listeners not working? - - .addListener(EntityAttackEvent.class, this::onAttack); + this.eventNode() + .addListener(AddEntityToInstanceEvent.class, this::setInstanceEvent) + .addListener(AddEntityToInstanceEvent.class, this::onSpawn) + .addListener(RemoveEntityFromInstanceEvent.class, this::onDespawn) + .addListener(PlayerEntityInteractEvent.class, this::onInteract) + .addListener(EntityAttackEvent.class, this::onAttack); } private void setInstanceEvent(@NotNull AddEntityToInstanceEvent addEntityToInstanceEvent) { - if(addEntityToInstanceEvent.getInstance() instanceof Spawnable instance) { - scheduleNextTick((unused) -> this.teleport(this.position.withPitch(180).withYaw(0))); + if(addEntityToInstanceEvent.getInstance() instanceof Spawnable) { + this.scheduleNextTick((unused) -> this.teleport(this.position.withPitch(180).withYaw(0))); } addEntityToInstanceEvent.getInstance().eventNode() - .addListener(PlayerMoveEvent.class, playerMoveEvent -> { - //TODO might be expensive - if(this.position.distance(playerMoveEvent.getNewPosition()) < 5) - this.lookAt(playerMoveEvent.getPlayer()); - }) - .addListener(PlayerEntityInteractEvent.class, playerEntityInteractEvent -> { - if(playerEntityInteractEvent.getTarget() == this) { - onInteract(playerEntityInteractEvent); - } - }) - .addListener(EntityAttackEvent.class, entityAttackEvent -> { - if(entityAttackEvent.getTarget() == this) { - onAttack(entityAttackEvent); - } - }); + .addListener(PlayerMoveEvent.class, playerMoveEvent -> { + if(this.position.distance(playerMoveEvent.getNewPosition()) < 5) + this.lookAt(playerMoveEvent.getPlayer()); + }) + .addListener(PlayerEntityInteractEvent.class, playerEntityInteractEvent -> { + if(playerEntityInteractEvent.getTarget() == this) { + this.onInteract(playerEntityInteractEvent); + } + }) + .addListener(EntityAttackEvent.class, entityAttackEvent -> { + if(entityAttackEvent.getTarget() == this) { + this.onAttack(entityAttackEvent); + } + }); } /** * Called when instance of entity is set - * @param addEntityToInstanceEvent */ protected void onSpawn(@NotNull AddEntityToInstanceEvent addEntityToInstanceEvent) { @@ -63,7 +60,6 @@ public class InteractableEntity extends EntityCreature { /** * Called when instance of entity is unset - * @param removeEntityFromInstanceEvent */ protected void onDespawn(@NotNull RemoveEntityFromInstanceEvent removeEntityFromInstanceEvent) { @@ -71,7 +67,6 @@ public class InteractableEntity extends EntityCreature { /** * Called when a Player interacts with the entity - * @param playerEntityInteractEvent */ protected void onInteract(@NotNull PlayerEntityInteractEvent playerEntityInteractEvent) { @@ -79,7 +74,6 @@ public class InteractableEntity extends EntityCreature { /** * Called when a Player attacks the entity - * @param entityAttackEvent */ protected void onAttack(@NotNull EntityAttackEvent entityAttackEvent) { diff --git a/src/main/java/eu/mhsl/minenet/minigames/shared/inventory/InteractableInventory.java b/src/main/java/eu/mhsl/minenet/minigames/shared/inventory/InteractableInventory.java index 01095eb..920fde7 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/shared/inventory/InteractableInventory.java +++ b/src/main/java/eu/mhsl/minenet/minigames/shared/inventory/InteractableInventory.java @@ -2,43 +2,48 @@ package eu.mhsl.minenet.minigames.shared.inventory; import net.kyori.adventure.text.Component; import net.minestom.server.entity.Player; +import net.minestom.server.event.inventory.InventoryPreClickEvent; import net.minestom.server.inventory.Inventory; import net.minestom.server.inventory.InventoryType; -import net.minestom.server.inventory.click.ClickType; -import net.minestom.server.inventory.condition.InventoryConditionResult; +import net.minestom.server.inventory.click.Click; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import org.jetbrains.annotations.NotNull; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; public class InteractableInventory extends Inventory { + private final Map clickActions = new ConcurrentHashMap<>(); + + private record ClickAction(ItemStack item, Consumer callback, boolean closeAfter) { + } + /** * Defines an Inventory with direct callbacks for ItemSlots - * @param inventoryType - * @param title */ protected InteractableInventory(@NotNull InventoryType inventoryType, @NotNull Component title) { super(inventoryType, title); - addInventoryCondition(this::onClick); + this.eventNode().addListener(InventoryPreClickEvent.class, event -> { + event.setCancelled(true); + this.onClick(event.getPlayer(), event.getSlot(), event.getClick(), event); + + ClickAction action = this.clickActions.get(event.getSlot()); + if(action == null) return; + + if(action.closeAfter()) event.getPlayer().closeInventory(); + action.callback().accept(new ItemClick(event.getPlayer(), this, event.getSlot(), action.item(), event.getClick())); + }); } /** * Set Item with Callback - * @param item - * @param slot - * @param callback */ protected void setClickableItem(ItemStack item, int slot, Consumer callback, boolean closeAfter) { - setItemStack(slot, item); - addInventoryCondition((player, clickedSlot, clickType, inventoryConditionResult) -> { - if(clickedSlot == slot) { - if(closeAfter) player.closeInventory(); - callback.accept(new ItemClick(player, this, clickedSlot, item, clickType)); - } - inventoryConditionResult.setCancel(true); - }); + this.setItemStack(slot, item); + this.clickActions.put(slot, new ClickAction(item, callback, closeAfter)); } protected void setClickableItem(ItemStack item, int slot, Consumer callback) { @@ -47,32 +52,27 @@ public class InteractableInventory extends Inventory { /** * Set Item without handler - * @param item - * @param slot */ protected void setDummyItem(ItemStack item, int slot) { this.setClickableItem( - item, - slot, - itemClick -> {} + item, + slot, + itemClick -> { + } ); } protected void setDummyItem(Material material, int slot) { this.setDummyItem( - ItemStack.builder(material).customName(Component.text("")).build(), - slot + ItemStack.builder(material).customName(Component.text("")).build(), + slot ); } /** * You may want to Override this method to get more generic click events - * @param player - * @param slot - * @param clickType - * @param inventoryConditionResult */ - protected void onClick(Player player, int slot, ClickType clickType, InventoryConditionResult inventoryConditionResult) { + protected void onClick(Player player, int slot, Click click, InventoryPreClickEvent event) { } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/shared/inventory/ItemClick.java b/src/main/java/eu/mhsl/minenet/minigames/shared/inventory/ItemClick.java index dd1c762..d7a1051 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/shared/inventory/ItemClick.java +++ b/src/main/java/eu/mhsl/minenet/minigames/shared/inventory/ItemClick.java @@ -2,7 +2,7 @@ package eu.mhsl.minenet.minigames.shared.inventory; import net.minestom.server.entity.Player; import net.minestom.server.inventory.Inventory; -import net.minestom.server.inventory.click.ClickType; +import net.minestom.server.inventory.click.Click; import net.minestom.server.item.ItemStack; public class ItemClick { @@ -10,41 +10,36 @@ public class ItemClick { private final InteractableInventory inventory; private final int clickedSlot; private final ItemStack item; - private final ClickType clickType; + private final Click click; /** * Describes a click on an Item from an IntractableInventory - * @param player - * @param inventory - * @param clickedSlot - * @param item - * @param clickType */ - public ItemClick(Player player, InteractableInventory inventory, int clickedSlot, ItemStack item, ClickType clickType) { + public ItemClick(Player player, InteractableInventory inventory, int clickedSlot, ItemStack item, Click click) { this.player = player; this.inventory = inventory; this.clickedSlot = clickedSlot; this.item = item; - this.clickType = clickType; + this.click = click; } public Player getPlayer() { - return player; + return this.player; } public Inventory getInventory() { - return inventory; + return this.inventory; } public int getClickedSlot() { - return clickedSlot; + return this.clickedSlot; } public ItemStack getItem() { - return item; + return this.item; } - public ClickType getClickType() { - return clickType; + public Click getClick() { + return this.click; } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/BatchUtil.java b/src/main/java/eu/mhsl/minenet/minigames/util/BatchUtil.java index 225b115..22249cd 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/BatchUtil.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/BatchUtil.java @@ -2,6 +2,7 @@ package eu.mhsl.minenet.minigames.util; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import net.minestom.server.instance.InstanceContainer; +import net.minestom.server.instance.batch.AbsoluteBlockBatch; import net.minestom.server.instance.batch.Batch; import net.minestom.server.instance.batch.ChunkBatch; import net.minestom.server.utils.chunk.ChunkUtils; @@ -10,7 +11,7 @@ import java.lang.reflect.Field; import java.util.concurrent.CompletableFuture; public class BatchUtil { - public static long[] getAffectedChunks(Batch batch) { + public static long[] getAffectedChunks(Batch batch) { try { Field field = batch.getClass().getDeclaredField("chunkBatchesMap"); field.setAccessible(true); @@ -19,36 +20,28 @@ public class BatchUtil { Long2ObjectMap chunkBatchesMap = (Long2ObjectMap) field.get(batch); return chunkBatchesMap.keySet().toLongArray(); - } catch (NoSuchFieldException | IllegalAccessException e) { + } catch(NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException(e); } } - public static void loadAndApplyBatch(Batch batch, InstanceContainer instance, Runnable onFinish) { + public static void loadAndApplyBatch(AbsoluteBlockBatch batch, InstanceContainer instance, Runnable onFinish) { batch.awaitReady(); long[] affectedChunks = BatchUtil.getAffectedChunks(batch); - ChunkUtils.optionalLoadAll(instance, affectedChunks, null).thenRun(() -> batch.apply(instance, onFinish)); + ChunkUtils.optionalLoadAll(instance, affectedChunks, null).thenRun(() -> batch.apply(instance, b -> onFinish.run())); } - public static void loadAndApplyBatchBlocking(Batch batch, InstanceContainer instance) { + public static void loadAndApplyBatchBlocking(AbsoluteBlockBatch batch, InstanceContainer instance) { CompletableFuture future = new CompletableFuture<>(); batch.awaitReady(); long[] affectedChunks = BatchUtil.getAffectedChunks(batch); CompletableFuture loadChunksTask = ChunkUtils.optionalLoadAll(instance, affectedChunks, null); - Runnable completerTask = () -> { - System.out.println("COMPLETE"); - future.complete(null); - }; - - loadChunksTask.thenRun(() -> { - System.out.println("BEGIN"); - batch.apply(instance, completerTask); - }); + loadChunksTask.thenRun(() -> batch.apply(instance, b -> future.complete(null))); try { future.get(); - } catch (Exception e) { + } catch(Exception e) { throw new RuntimeException(e); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/ColorUtil.java b/src/main/java/eu/mhsl/minenet/minigames/util/ColorUtil.java deleted file mode 100644 index b09466d..0000000 --- a/src/main/java/eu/mhsl/minenet/minigames/util/ColorUtil.java +++ /dev/null @@ -1,15 +0,0 @@ -package eu.mhsl.minenet.minigames.util; - -import net.kyori.adventure.text.format.NamedTextColor; - -public class ColorUtil { - public static NamedTextColor scoreColor(int score) { - return switch (score) { - case 1 -> NamedTextColor.GOLD; - case 2 -> NamedTextColor.GREEN; - case 3 -> NamedTextColor.DARK_GREEN; - default -> NamedTextColor.GRAY; - }; - - } -} diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/CommonEventHandles.java b/src/main/java/eu/mhsl/minenet/minigames/util/CommonEventHandles.java index fc72991..e28a8d2 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/CommonEventHandles.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/CommonEventHandles.java @@ -5,13 +5,8 @@ import net.minestom.server.event.trait.CancellableEvent; public class CommonEventHandles { /** * Cancels the given Event - * @param event */ public static void cancel(CancellableEvent event) { event.setCancelled(true); } - - public static void cancel(CancellableEvent event, boolean condition) { - event.setCancelled(condition); - } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/CommonProperties.java b/src/main/java/eu/mhsl/minenet/minigames/util/CommonProperties.java index 588cf66..357f4d6 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/CommonProperties.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/CommonProperties.java @@ -6,8 +6,15 @@ import java.util.Map; public class CommonProperties { public static Map fenceEastWest = new HashMap<>() { { - put("west", "true"); - put("east", "true"); + this.put("west", "true"); + this.put("east", "true"); + } + }; + + public static Map fenceNorthSouth = new HashMap<>() { + { + this.put("north", "true"); + this.put("south", "true"); } }; } diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/GeneratorUtils.java b/src/main/java/eu/mhsl/minenet/minigames/util/GeneratorUtils.java index 6b7c5cc..c450567 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/GeneratorUtils.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/GeneratorUtils.java @@ -14,8 +14,8 @@ public class GeneratorUtils { double startZ = unit.absoluteStart().z(); double endZ = unit.absoluteEnd().z() - 1; - for (double x = startX; x <= endX; x++) { - for (double z = startZ; z <= endZ; z++) { + for(double x = startX; x <= endX; x++) { + for(double z = startZ; z <= endZ; z++) { Point currentPoint = new Pos(x, unit.absoluteStart().y(), z); bottomPoint.accept(currentPoint); } @@ -28,8 +28,8 @@ public class GeneratorUtils { double startZ = Math.min(start.z(), end.z()); double endZ = Math.max(start.z(), end.z()); - for (double x = startX; x <= endX; x++) { - for (double z = startZ; z <= endZ; z++) { + for(double x = startX; x <= endX; x++) { + for(double z = startZ; z <= endZ; z++) { Point currentPoint = new Pos(x, start.y(), z); callback.accept(currentPoint); } @@ -44,9 +44,9 @@ public class GeneratorUtils { int minZ = (int) Math.min(pos1.z(), pos2.z()); int maxZ = (int) Math.max(pos1.z(), pos2.z()); - for (int x = minX; x <= maxX; x++) { - for (int y = minY; y <= maxY; y++) { - for (int z = minZ; z <= maxZ; z++) { + for(int x = minX; x <= maxX; x++) { + for(int y = minY; y <= maxY; y++) { + for(int z = minZ; z <= maxZ; z++) { Point point = new Pos(x, y, z); callback.accept(point); } @@ -55,7 +55,8 @@ public class GeneratorUtils { } public static void outline(Point pos1, Point pos2, Consumer outline) { - outline(pos1, pos2, outline, point -> {}); + outline(pos1, pos2, outline, point -> { + }); } public static void outline(Point pos1, Point pos2, Consumer outline, Consumer innerFill) { @@ -67,11 +68,11 @@ public class GeneratorUtils { int maxY = (int) Math.max(pos1.y(), pos2.y()); int maxZ = (int) Math.max(pos1.z(), pos2.z()); - for (int x = minX; x <= maxX; x++) { - for (int y = minY; y <= maxY; y++) { - for (int z = minZ; z <= maxZ; z++) { + for(int x = minX; x <= maxX; x++) { + for(int y = minY; y <= maxY; y++) { + for(int z = minZ; z <= maxZ; z++) { Point blockPos = new Pos(x, y, z); - if (x == minX || x == maxX || y == minY || y == maxY || z == minZ || z == maxZ) { + if(x == minX || x == maxX || y == minY || y == maxY || z == minZ || z == maxZ) { outline.accept(blockPos); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/Intersect.java b/src/main/java/eu/mhsl/minenet/minigames/util/Intersect.java index a3e9cb3..e9937d3 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/Intersect.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/Intersect.java @@ -8,10 +8,10 @@ import net.minestom.server.instance.block.Block; public class Intersect { public static boolean withPressurePlate(Instance instance, BlockPallet target, Pos playerPosition) { Pos[] corners = { - playerPosition.add(0.3-Position.PIXEL, 0, 0.3-Position.PIXEL), - playerPosition.add(0.3-Position.PIXEL, 0, -0.3+Position.PIXEL), - playerPosition.add(-0.3+Position.PIXEL, 0, 0.3-Position.PIXEL), - playerPosition.add(-0.3+Position.PIXEL, 0, -0.3+Position.PIXEL) + playerPosition.add(0.3 - Position.PIXEL, 0, 0.3 - Position.PIXEL), + playerPosition.add(0.3 - Position.PIXEL, 0, -0.3 + Position.PIXEL), + playerPosition.add(-0.3 + Position.PIXEL, 0, 0.3 - Position.PIXEL), + playerPosition.add(-0.3 + Position.PIXEL, 0, -0.3 + Position.PIXEL) }; for(Pos coroner : corners) { diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/InventoryItemAlignment.java b/src/main/java/eu/mhsl/minenet/minigames/util/InventoryItemAlignment.java index 283cfa1..94c800c 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/InventoryItemAlignment.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/InventoryItemAlignment.java @@ -24,13 +24,13 @@ public class InventoryItemAlignment { this.x = 9; this.y = y; - if(x * y < size) throw new RuntimeException("Not enough space for Item alignment"); + if(this.x * y < size) throw new RuntimeException("Not enough space for Item alignment"); } public ItemOffset next() { - counter++; + this.counter++; - if(this.size < 9) return new ItemOffset(this.row(this.size).get(counter), y > 1 ? 1 : 0); + if(this.size < 9) return new ItemOffset(this.row(this.size).get(this.counter), this.y > 1 ? 1 : 0); //TODO more items? return new ItemOffset(0, 0); @@ -41,89 +41,89 @@ public class InventoryItemAlignment { return new ArrayList>() { { //noinspection EmptyClassInitializer - add(new ArrayList<>() { + this.add(new ArrayList<>() { static { } }); - add(new ArrayList<>() { + this.add(new ArrayList<>() { { - add(4); + this.add(4); } }); - add(new ArrayList<>() { + this.add(new ArrayList<>() { { - add(3); - add(5); + this.add(3); + this.add(5); } }); - add(new ArrayList<>() { + this.add(new ArrayList<>() { { - add(2); - add(4); - add(6); + this.add(2); + this.add(4); + this.add(6); } }); - add(new ArrayList<>() { + this.add(new ArrayList<>() { { - add(1); - add(3); - add(5); - add(7); + this.add(1); + this.add(3); + this.add(5); + this.add(7); } }); - add(new ArrayList<>() { + this.add(new ArrayList<>() { { - add(2); - add(3); - add(4); - add(5); - add(6); + this.add(2); + this.add(3); + this.add(4); + this.add(5); + this.add(6); } }); - add(new ArrayList<>() { + this.add(new ArrayList<>() { { - add(1); - add(2); - add(3); - add(5); - add(6); - add(7); + this.add(1); + this.add(2); + this.add(3); + this.add(5); + this.add(6); + this.add(7); } }); - add(new ArrayList<>() { + this.add(new ArrayList<>() { { - add(1); - add(2); - add(3); - add(4); - add(5); - add(6); - add(7); + this.add(1); + this.add(2); + this.add(3); + this.add(4); + this.add(5); + this.add(6); + this.add(7); } }); - add(new ArrayList<>() { + this.add(new ArrayList<>() { { - add(0); - add(1); - add(2); - add(3); - add(5); - add(6); - add(7); - add(8); + this.add(0); + this.add(1); + this.add(2); + this.add(3); + this.add(5); + this.add(6); + this.add(7); + this.add(8); } }); - add(new ArrayList<>() { + this.add(new ArrayList<>() { { - add(0); - add(1); - add(2); - add(3); - add(4); - add(5); - add(6); - add(7); - add(8); + this.add(0); + this.add(1); + this.add(2); + this.add(3); + this.add(4); + this.add(5); + this.add(6); + this.add(7); + this.add(8); } }); } @@ -133,7 +133,7 @@ public class InventoryItemAlignment { public record ItemOffset(int x, int z) { public int get() { - return x + (z * 9); - } + return this.x + (this.z * 9); } + } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/InventoryUtil.java b/src/main/java/eu/mhsl/minenet/minigames/util/InventoryUtil.java index b47f1a3..8fd51d3 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/InventoryUtil.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/InventoryUtil.java @@ -7,12 +7,12 @@ import net.minestom.server.item.Material; public class InventoryUtil { public static void removeItemFromPlayer(Player player, Material material, int amount) { ItemStack[] items = player.getInventory().getItemStacks(); - for (int i = 0; i < items.length; i++) { + for(int i = 0; i < items.length; i++) { ItemStack item = items[i]; - if (item.material() != material) continue; + if(item.material() != material) continue; int stackSize = item.amount(); - if (stackSize < amount) { + if(stackSize < amount) { amount -= stackSize; player.getInventory().setItemStack(i, ItemStack.AIR); } else { diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/MapUtil.java b/src/main/java/eu/mhsl/minenet/minigames/util/MapUtil.java index ac5506b..cc5b611 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/MapUtil.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/MapUtil.java @@ -11,7 +11,19 @@ public class MapUtil { list.sort(Map.Entry.comparingByValue()); Map result = new LinkedHashMap<>(); - for (Map.Entry entry : list) { + for(Map.Entry entry : list) { + result.put(entry.getKey(), entry.getValue()); + } + + return result; + } + + public static > Map sortReversedByValue(Map map) { + List> list = new ArrayList<>(map.entrySet()); + list.sort(Map.Entry.comparingByValue().reversed()); + + Map result = new LinkedHashMap<>(); + for(Map.Entry entry : list) { result.put(entry.getKey(), entry.getValue()); } diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/MaterialUtil.java b/src/main/java/eu/mhsl/minenet/minigames/util/MaterialUtil.java index 6effaf9..3fbfa90 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/MaterialUtil.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/MaterialUtil.java @@ -1,9 +1,28 @@ package eu.mhsl.minenet.minigames.util; +import net.minestom.server.instance.block.BlockFace; import net.minestom.server.item.Material; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Predicate; + public class MaterialUtil { public static Material fromString(String name, Material def) { return Material.values().stream().filter(material -> material.name().equals(name)).findFirst().orElse(def); } + + public static Material getRandomFullBlock(Predicate filter) { + List blocks = Material.values().stream() + .filter(filter) + .filter(Material::isBlock) + .filter(material -> material.block().isSolid()) + .filter(material -> Arrays.stream(BlockFace.values()) + .allMatch(face -> material.block().registry().collisionShape().isFaceFull(face)) + ) + .toList(); + + return blocks.get(ThreadLocalRandom.current().nextInt(blocks.size())); + } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/Monitoring.java b/src/main/java/eu/mhsl/minenet/minigames/util/Monitoring.java index 9ce0114..f703d7a 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/Monitoring.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/Monitoring.java @@ -11,7 +11,7 @@ public class Monitoring { private static final AtomicReference lastTick = new AtomicReference<>(); static { - MinecraftServer.getGlobalEventHandler().addListener(ServerTickMonitorEvent .class, event -> lastTick.set(event.getTickMonitor())); + MinecraftServer.getGlobalEventHandler().addListener(ServerTickMonitorEvent.class, event -> lastTick.set(event.getTickMonitor())); } public static long getRamUsage() { diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/MoveInstance.java b/src/main/java/eu/mhsl/minenet/minigames/util/MoveInstance.java index 33aeaf5..6b3cf4c 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/MoveInstance.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/MoveInstance.java @@ -14,6 +14,7 @@ public class MoveInstance { public static void move(Set playerList, Spawnable destination) { playerList.forEach(player -> move(player, destination)); } + public static void move(Entity p, Spawnable destination) { if(p.getInstance() != destination) p.setInstance((Instance) destination, destination.getSpawn()); } diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/NumberUtil.java b/src/main/java/eu/mhsl/minenet/minigames/util/NumberUtil.java index 714a144..e072033 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/NumberUtil.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/NumberUtil.java @@ -2,7 +2,7 @@ package eu.mhsl.minenet.minigames.util; public class NumberUtil { public static double map(double oldValue, double oldMin, double oldMax, double newMin, double newMax) { - double out = (((oldValue - oldMin) * (newMax - newMin)) / (oldMax - oldMin)) + newMin; + double out = (((oldValue - oldMin) * (newMax - newMin)) / (oldMax - oldMin)) + newMin; if(out > newMax) out = newMax; if(out < newMin) out = newMin; @@ -10,8 +10,8 @@ public class NumberUtil { } public static > T clamp(T val, T min, T max) { - if (val.compareTo(min) < 0) return min; - else if (val.compareTo(max) > 0) return max; + if(val.compareTo(min) < 0) return min; + else if(val.compareTo(max) > 0) return max; else return val; } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/PluginMessageUtil.java b/src/main/java/eu/mhsl/minenet/minigames/util/PluginMessageUtil.java index 7b3d499..4ae1ed3 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/PluginMessageUtil.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/PluginMessageUtil.java @@ -7,6 +7,7 @@ import net.minestom.server.network.packet.server.common.PluginMessagePacket; public class PluginMessageUtil { private static final String bungeeTargetSelector = "bungeecord:main"; + public static void connect(Player p, String bungeeServerTargetName) { ByteArrayDataOutput out = ByteStreams.newDataOutput(); out.writeUTF("Connect"); diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/Position.java b/src/main/java/eu/mhsl/minenet/minigames/util/Position.java index 4dd189f..e9d0d50 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/Position.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/Position.java @@ -33,9 +33,28 @@ public class Position { public static List blocksBelowPlayer(Instance instance, Player p) { Point playerPos = p.getPosition(); List blocks = new ArrayList<>(); - GeneratorUtils.foreachXZ(playerPos.sub(0.5, 1, 0.5), playerPos.add(0.5, -1, 0.5), point -> { - blocks.add(instance.getBlock(point)); - }); + GeneratorUtils.foreachXZ( + playerPos.sub(0.5, 1, 0.5), + playerPos.add(0.5, -1, 0.5), + point -> blocks.add(instance.getBlock(point)) + ); return blocks.stream().distinct().toList(); } + + public static boolean hasPositionChanged(Pos oldPos, Pos newPos) { + return !oldPos.withView(0, 0).equals(newPos.withView(0, 0)); + } + + public static boolean isInArea(Point point, Point pos1, Point pos2) { + double minX = Math.min(pos1.x(), pos2.x()); + double maxX = Math.max(pos1.x(), pos2.x()); + double minY = Math.min(pos1.y(), pos2.y()); + double maxY = Math.max(pos1.y(), pos2.y()); + double minZ = Math.min(pos1.z(), pos2.z()); + double maxZ = Math.max(pos1.z(), pos2.z()); + + return point.x() >= minX && point.x() <= maxX + && point.y() >= minY && point.y() <= maxY + && point.z() >= minZ && point.z() <= maxZ; + } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/ResourceUtils.java b/src/main/java/eu/mhsl/minenet/minigames/util/ResourceUtils.java index fcf510b..7542113 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/ResourceUtils.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/ResourceUtils.java @@ -1,5 +1,7 @@ package eu.mhsl.minenet.minigames.util; +import org.jetbrains.annotations.NotNull; + import java.io.File; import java.io.IOException; import java.net.URI; @@ -19,7 +21,7 @@ public final class ResourceUtils { final URI uri = Objects.requireNonNull(ResourceUtils.class.getResource("/" + source)).toURI(); FileSystem fileSystem = null; - if (uri.toString().startsWith("jar:")) + if(uri.toString().startsWith("jar:")) fileSystem = FileSystems.newFileSystem(uri, Map.of("create", "true")); try { @@ -28,36 +30,36 @@ public final class ResourceUtils { final Path jarPath = Paths.get(uri); final Path target = Path.of("resources/" + source); - if (Files.exists(target)) { + if(Files.exists(target)) { if(keepOutdated) return; - try (Stream pathStream = Files.walk(target)) { + try(Stream pathStream = Files.walk(target)) { pathStream.sorted(Comparator.reverseOrder()) - .forEach(path -> { - try { - Files.delete(path); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); + .forEach(path -> { + try { + Files.delete(path); + } catch(IOException e) { + throw new RuntimeException(e); + } + }); } } Files.walkFileTree(jarPath, new SimpleFileVisitor<>() { @Override - public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) throws IOException { + public @NotNull FileVisitResult preVisitDirectory(final @NotNull Path dir, final @NotNull BasicFileAttributes attrs) throws IOException { Path currentTarget = target.resolve(jarPath.relativize(dir).toString()); Files.createDirectories(currentTarget); return FileVisitResult.CONTINUE; } @Override - public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException { + public @NotNull FileVisitResult visitFile(final @NotNull Path file, final @NotNull BasicFileAttributes attrs) throws IOException { final Path to = target.resolve(jarPath.relativize(file).toString()); Files.copy(file, to, StandardCopyOption.REPLACE_EXISTING); return FileVisitResult.CONTINUE; } }); } finally { - if (fileSystem != null) + if(fileSystem != null) fileSystem.close(); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/SingleExecution.java b/src/main/java/eu/mhsl/minenet/minigames/util/SingleExecution.java index db78c4f..73b900d 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/SingleExecution.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/SingleExecution.java @@ -1,18 +1,14 @@ package eu.mhsl.minenet.minigames.util; public class SingleExecution { + private boolean wasExecuted = false; + public static SingleExecution make() { return new SingleExecution(); } - private boolean wasExecuted = false; - - public void reset() { - this.wasExecuted = false; - } - public void singleRun(Runnable task) { - if(wasExecuted) return; + if(this.wasExecuted) return; task.run(); this.wasExecuted = true; } diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/TextUtil.java b/src/main/java/eu/mhsl/minenet/minigames/util/TextUtil.java deleted file mode 100644 index a9e65b8..0000000 --- a/src/main/java/eu/mhsl/minenet/minigames/util/TextUtil.java +++ /dev/null @@ -1,132 +0,0 @@ -package eu.mhsl.minenet.minigames.util; - -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class TextUtil { - public static Component autoWrap(String input, NamedTextColor color) { - return Component.text(wrap(input, 30, "\n", true, "-", " "), color); - } - - public static Component autoWrap(String input) { - return autoWrap(input, NamedTextColor.WHITE); - } - - /** - * Wraps a source String into a series of lines having a maximum specified length. The source is - * wrapped at: spaces, horizontal tabs, system newLine characters, or a specified newLine character - * sequence. Existing newLine character sequences in the source string, whether they be the system - * newLine or the specified newLine, are honored. Existing whitespace (spaces and horizontal tabs) - * is preserved. - *

- * When wrapLongWords is true, words having a length greater than the specified - * lineLength will be broken, the specified longWordBreak terminator appended, - * and a new line initiated with the text of the specified longWordLinePrefix string. The - * position of the break will be unceremoniously chosen such that ineLength is honored. - * One use of longWordLinePrefix is to effect "hanging indents" by specifying a series of - * spaces for this parameter. This parameter can contain the lineFeed character(s). Although - * longWordLinePrefix can contain the horizontal tab character, the results are not - * guaranteed because no attempt is made to determine the quantity of character positions occupied by a - * horizontal tab.

- *

- * Example usage: - *

-     * wrap( "  A very long word is Abracadabra in my book", 11, "\n", true, "-", "  ");
- * returns (note the effect of the single-character lineFeed): - *
-     *   A very
-     * long word
-     * is Abraca-
-     *   dabra in
-     * my book
- * Whereas, the following: - *
-     * wrap( "  A very long word is Abracadabra in my book", 11, null, true, null, "  ");
- * returns (due to the 2-character system linefeed): - *
-     *   A very
-     * long
-     * word is A
-     *   bracada
-     *   bra in
-     * my book

- * - * @param src the String to be word wrapped, may be null - * @param lineLength the maximum line length, including the length of newLineStr and, when - * applicable, longWordLinePrefix. If the value is insufficient to accommodate - * these two parameters + 1 character, it will be increased accordingly. - * @param newLineStr the string to insert for a new line, or null to use the value - * reported as the system line separator by the JVM - * @param wrapLongWords when false, words longer than wrapLength
will not be broken - * @param longWordBreak string with which to precede newLineStr on each line of a broken word, - * excepting the last line, or null if this feature is not to be used - * @param longWordLinePrefix string with which to prefix each line of a broken word, subsequent - * to the first line, or null if no prefix is to be used - * @return a line with newlines inserted, or null if src is null - */ - private static String wrap(String src, int lineLength, String newLineStr, boolean wrapLongWords, String longWordBreak, String longWordLinePrefix) { - // Trivial case - if ( src == null ) return null; - - if ( newLineStr == null ) - newLineStr = System.getProperty( "line.separator" ); - - if ( longWordBreak == null ) - longWordBreak = ""; - - if ( longWordLinePrefix == null ) - longWordLinePrefix = ""; - - // Adjust maximum line length to accommodate the newLine string - lineLength -= newLineStr.length(); - if ( lineLength < 1 ) - lineLength = 1; - - // Guard for long word break or prefix that would create an infinite loop - if ( wrapLongWords && lineLength - longWordBreak.length() - longWordLinePrefix.length() < 1 ) - lineLength += longWordBreak.length() + longWordLinePrefix.length(); - - int - remaining = lineLength, - breakLength = longWordBreak.length(); - - Matcher m = Pattern.compile( ".+?[ \\t]|.+?mis" + newLineStr + "singValue|.+?$" ).matcher( src ); - - StringBuilder cache = new StringBuilder(); - - while ( m.find() ) { - String word = m.group(); - - // Breakup long word - while ( wrapLongWords && word.length() > lineLength ) { - cache - .append(word, 0, remaining - breakLength) - .append( longWordBreak ) - .append( newLineStr ); - word = longWordLinePrefix + word.substring( remaining - breakLength ); - remaining = lineLength; - } // if - - // Linefeed if word exceeds remaining space - if ( word.length() > remaining ) { - cache - .append( newLineStr ) - .append( word ); - remaining = lineLength; - } // if - - // Word fits in remaining space - else - cache.append( word ); - - remaining -= word.length(); - } // while - - return cache.toString(); - } // wrap() - -} - diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/UuidUtil.java b/src/main/java/eu/mhsl/minenet/minigames/util/UuidUtil.java index be26293..73a3219 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/UuidUtil.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/UuidUtil.java @@ -10,7 +10,7 @@ public class UuidUtil { builder.insert(16, "-"); builder.insert(12, "-"); builder.insert(8, "-"); - } catch (StringIndexOutOfBoundsException e) { + } catch(StringIndexOutOfBoundsException e) { throw new IllegalArgumentException(); } return UUID.fromString(builder.toString()); diff --git a/src/main/java/eu/mhsl/minenet/minigames/util/WeatherUtils.java b/src/main/java/eu/mhsl/minenet/minigames/util/WeatherUtils.java index d2e981e..e1d3af2 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/util/WeatherUtils.java +++ b/src/main/java/eu/mhsl/minenet/minigames/util/WeatherUtils.java @@ -11,15 +11,16 @@ import java.util.Set; public class WeatherUtils { private float intensity; + public void startRain(Set players) { - intensity = 0; + this.intensity = 0; players.forEach(p -> p.sendPacket(new ChangeGameStatePacket(ChangeGameStatePacket.Reason.BEGIN_RAINING, 0f))); MinecraftServer.getSchedulerManager().submitTask(() -> { - intensity += 0.1f; - players.forEach(p -> p.sendPacket(new ChangeGameStatePacket(ChangeGameStatePacket.Reason.RAIN_LEVEL_CHANGE, intensity))); - System.out.println(intensity); - if(intensity < 1) { + this.intensity += 0.1f; + players.forEach(p -> p.sendPacket(new ChangeGameStatePacket(ChangeGameStatePacket.Reason.RAIN_LEVEL_CHANGE, this.intensity))); + System.out.println(this.intensity); + if(this.intensity < 1) { return TaskSchedule.millis(500); } else { return TaskSchedule.stop(); diff --git a/src/main/java/eu/mhsl/minenet/minigames/world/BlockPallet.java b/src/main/java/eu/mhsl/minenet/minigames/world/BlockPallet.java index 72bbc3a..a60d03d 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/world/BlockPallet.java +++ b/src/main/java/eu/mhsl/minenet/minigames/world/BlockPallet.java @@ -7,13 +7,13 @@ import java.util.List; import java.util.Random; public enum BlockPallet { - GROUND(new Block[] {Block.GRAVEL, Block.STONE, Block.DIRT, Block.GRASS_BLOCK, Block.COARSE_DIRT, Block.ROOTED_DIRT}), - WOOD(new Block[] {Block.ACACIA_WOOD, Block.BIRCH_WOOD, Block.JUNGLE_WOOD, Block.OAK_WOOD, Block.DARK_OAK_WOOD, Block.SPRUCE_WOOD}), - STONE(new Block[] {Block.CHISELED_STONE_BRICKS, Block.STONE_BRICKS, Block.POLISHED_ANDESITE, Block.POLISHED_BLACKSTONE, Block.POLISHED_DIORITE}), - WINTER(new Block[] {Block.SNOW_BLOCK, Block.ICE, Block.PACKED_ICE, Block.BLUE_CONCRETE, Block.SEA_LANTERN}), - STREET(new Block[] {Block.BLACK_CONCRETE_POWDER, Block.GRAY_CONCRETE_POWDER, Block.GRAVEL, Block.BLACK_CONCRETE, Block.GRAY_CONCRETE}), - - PRESSURE_PLATES(new Block[] {Block.ACACIA_PRESSURE_PLATE, Block.BIRCH_PRESSURE_PLATE, Block.CRIMSON_PRESSURE_PLATE, Block.JUNGLE_PRESSURE_PLATE, Block.OAK_PRESSURE_PLATE, Block.DARK_OAK_PRESSURE_PLATE, Block.HEAVY_WEIGHTED_PRESSURE_PLATE, Block.HEAVY_WEIGHTED_PRESSURE_PLATE, Block.POLISHED_BLACKSTONE_PRESSURE_PLATE, Block.SPRUCE_PRESSURE_PLATE, Block.STONE_PRESSURE_PLATE, Block.WARPED_PRESSURE_PLATE}); + GROUND(new Block[]{Block.GRAVEL, Block.STONE, Block.DIRT, Block.GRASS_BLOCK, Block.COARSE_DIRT, Block.ROOTED_DIRT}), + WOOD(new Block[]{Block.ACACIA_WOOD, Block.BIRCH_WOOD, Block.JUNGLE_WOOD, Block.OAK_WOOD, Block.DARK_OAK_WOOD, Block.SPRUCE_WOOD}), + STONE(new Block[]{Block.CHISELED_STONE_BRICKS, Block.STONE_BRICKS, Block.POLISHED_ANDESITE, Block.POLISHED_BLACKSTONE, Block.POLISHED_DIORITE}), + WINTER(new Block[]{Block.SNOW_BLOCK, Block.ICE, Block.PACKED_ICE, Block.BLUE_CONCRETE, Block.SEA_LANTERN}), + STREET(new Block[]{Block.BLACK_CONCRETE_POWDER, Block.GRAY_CONCRETE_POWDER, Block.GRAVEL, Block.BLACK_CONCRETE, Block.GRAY_CONCRETE}), + FLOWER(new Block[]{Block.ORANGE_TULIP, Block.PINK_TULIP, Block.RED_TULIP, Block.WHITE_TULIP, Block.DANDELION, Block.POPPY, Block.CORNFLOWER}), + PRESSURE_PLATES(new Block[]{Block.ACACIA_PRESSURE_PLATE, Block.BIRCH_PRESSURE_PLATE, Block.CRIMSON_PRESSURE_PLATE, Block.JUNGLE_PRESSURE_PLATE, Block.OAK_PRESSURE_PLATE, Block.DARK_OAK_PRESSURE_PLATE, Block.HEAVY_WEIGHTED_PRESSURE_PLATE, Block.HEAVY_WEIGHTED_PRESSURE_PLATE, Block.POLISHED_BLACKSTONE_PRESSURE_PLATE, Block.SPRUCE_PRESSURE_PLATE, Block.STONE_PRESSURE_PLATE, Block.WARPED_PRESSURE_PLATE}); final List list; final Random rnd = new Random(); @@ -23,11 +23,11 @@ public enum BlockPallet { } public Block rnd() { - return list.get(rnd.nextInt(list.size())); + return this.list.get(this.rnd.nextInt(this.list.size())); } public boolean contains(Block b) { - return list.contains(b); + return this.list.contains(b); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/world/generator/PlateTerrainGenerator.java b/src/main/java/eu/mhsl/minenet/minigames/world/generator/PlateTerrainGenerator.java index 2e265b2..6129ad0 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/world/generator/PlateTerrainGenerator.java +++ b/src/main/java/eu/mhsl/minenet/minigames/world/generator/PlateTerrainGenerator.java @@ -24,6 +24,6 @@ public abstract class PlateTerrainGenerator implements Generator { } public boolean generatePlate() { - return plateHeight > 0; + return this.plateHeight > 0; } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/world/generator/featureEnriched/CircularPlateGenerator.java b/src/main/java/eu/mhsl/minenet/minigames/world/generator/featureEnriched/CircularPlateGenerator.java index a838212..ee7f87c 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/world/generator/featureEnriched/CircularPlateGenerator.java +++ b/src/main/java/eu/mhsl/minenet/minigames/world/generator/featureEnriched/CircularPlateGenerator.java @@ -13,15 +13,13 @@ public class CircularPlateGenerator extends HeightTerrainGenerator { private final SingleExecution execution = SingleExecution.make(); private final int radius; private final int height; + private final boolean generated = false; private BlockPallet blockPallet = BlockPallet.GROUND; - private int centerX = 0; private int centerZ = 0; - private boolean generated = false; - public CircularPlateGenerator(int radius, int height) { - setCalculateHeight(point -> (int) ((point.distance(new Pos(0, point.y(), 0)) / 2) - radius)); + this.setCalculateHeight(point -> (int) ((point.distance(new Pos(0, point.y(), 0)) / 2) - radius)); this.radius = radius; this.height = height; } @@ -39,19 +37,19 @@ public class CircularPlateGenerator extends HeightTerrainGenerator { @Override public void generate(@NotNull GenerationUnit unit) { - execution.singleRun(() -> unit.fork(setter -> { - System.out.println(new Pos(-(radius + centerX), unit.absoluteStart().y(), -(radius + centerZ))); - System.out.println(new Pos(radius + centerX, height, radius + centerZ)); + this.execution.singleRun(() -> unit.fork(setter -> { + System.out.println(new Pos(-(this.radius + this.centerX), unit.absoluteStart().y(), -(this.radius + this.centerZ))); + System.out.println(new Pos(this.radius + this.centerX, this.height, this.radius + this.centerZ)); GeneratorUtils.foreachXZ( - new Pos(-(radius + centerX), unit.absoluteStart().y(), -(radius + centerZ)), - new Pos(radius + centerX, height, radius + centerZ), - point -> { - double distance = point.distance(new Pos(centerX, point.y(), centerZ)); + new Pos(-(this.radius + this.centerX), unit.absoluteStart().y(), -(this.radius + this.centerZ)), + new Pos(this.radius + this.centerX, this.height, this.radius + this.centerZ), + point -> { + double distance = point.distance(new Pos(this.centerX, point.y(), this.centerZ)); - if(distance <= this.radius) { - setter.setBlock(point.add(1, height, 1), Block.GOLD_BLOCK); - } + if(distance <= this.radius) { + setter.setBlock(point.add(1, this.height, 1), Block.GOLD_BLOCK); } + } ); })); diff --git a/src/main/java/eu/mhsl/minenet/minigames/world/generator/featureEnriched/ValeGenerator.java b/src/main/java/eu/mhsl/minenet/minigames/world/generator/featureEnriched/ValeGenerator.java index 6ece504..eb7f6fe 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/world/generator/featureEnriched/ValeGenerator.java +++ b/src/main/java/eu/mhsl/minenet/minigames/world/generator/featureEnriched/ValeGenerator.java @@ -1,6 +1,7 @@ package eu.mhsl.minenet.minigames.world.generator.featureEnriched; -import de.articdive.jnoise.JNoise; +import de.articdive.jnoise.generators.noisegen.opensimplex.FastSimplexNoiseGenerator; +import de.articdive.jnoise.pipeline.JNoise; import eu.mhsl.minenet.minigames.util.NumberUtil; import eu.mhsl.minenet.minigames.world.generator.terrain.HeightTerrainGenerator; import net.minestom.server.coordinate.Point; @@ -9,33 +10,36 @@ import java.util.function.Function; public class ValeGenerator extends HeightTerrainGenerator { private final JNoise curves = JNoise.newBuilder() - .fastSimplex() - .setSeed(rnd.nextLong()) - .setFrequency(0.01) - .build(); + .fastSimplex(FastSimplexNoiseGenerator.newBuilder().setSeed(this.rnd.nextLong()).build()) + .scale(0.01) + .build(); private final JNoise largeCurves = JNoise.newBuilder() - .fastSimplex() - .setSeed(rnd.nextLong()) - .setFrequency(0.001) - .build(); + .fastSimplex(FastSimplexNoiseGenerator.newBuilder().setSeed(this.rnd.nextLong()).build()) + .scale(0.001) + .build(); private Function xShiftMultiplier = multiplier -> 1d; + private Function xShiftOffset = z -> 0d; public ValeGenerator() { - setCalculateHeight(this::calculateY); - setHeightNoiseMultiplier(point -> (int) NumberUtil.map(calculateY(point), -64, 100, 0, 16)); + this.setCalculateHeight(this::calculateY); + this.setHeightNoiseMultiplier(point -> (int) NumberUtil.map(this.calculateY(point), -64, 100, 0, 16)); } private int calculateY(Point point) { - return (int) (Math.abs(point.blockX() - getXShiftAtZ(point.blockZ())) / 1.5); + return (int) (Math.abs(point.blockX() - this.getXShiftAtZ(point.blockZ())) / 1.5); } public int getXShiftAtZ(int z) { - return (int) ((curves.getNoise(z) * 32 + largeCurves.getNoise(z) * 64) * xShiftMultiplier.apply(z)); + return (int) ((this.curves.evaluateNoise(z) * 32 + this.largeCurves.evaluateNoise(z) * 64) * this.xShiftMultiplier.apply(z) + this.xShiftOffset.apply(z)); } public void setXShiftMultiplier(Function xShiftMultiplier) { this.xShiftMultiplier = xShiftMultiplier; } + + public void setXShiftOffset(Function xShiftOffset) { + this.xShiftOffset = xShiftOffset; + } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/world/generator/structures/generatable/PeakRock.java b/src/main/java/eu/mhsl/minenet/minigames/world/generator/structures/generatable/PeakRock.java index 8fe0130..1059254 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/world/generator/structures/generatable/PeakRock.java +++ b/src/main/java/eu/mhsl/minenet/minigames/world/generator/structures/generatable/PeakRock.java @@ -8,6 +8,7 @@ import net.minestom.server.instance.block.Block; public class PeakRock extends Structure { private final Point position; private final BlockPallet blockPallet; + public PeakRock(Point position, BlockPallet blockPallet) { this.position = position; this.blockPallet = blockPallet; @@ -15,14 +16,14 @@ public class PeakRock extends Structure { @Override public void generateStructure(Block.Setter setter) { - for (int stoneX = -4; stoneX < 4; stoneX++) { - for (int stoneZ = -4; stoneZ < 4; stoneZ++) { + for(int stoneX = -4; stoneX < 4; stoneX++) { + for(int stoneZ = -4; stoneZ < 4; stoneZ++) { - double distanceToCenter = position.add(stoneX, 0, stoneZ).distance(position); + double distanceToCenter = this.position.add(stoneX, 0, stoneZ).distance(this.position); if(distanceToCenter > 3) continue; - for (int stoneY = 0; stoneY < 10-(int) distanceToCenter * rnd.nextDouble(2, 5); stoneY++) { - Point blockPos = position.add(stoneX, stoneY, stoneZ); + for(int stoneY = 0; stoneY < 10 - (int) distanceToCenter * this.rnd.nextDouble(2, 5); stoneY++) { + Point blockPos = this.position.add(stoneX, stoneY, stoneZ); setter.setBlock(blockPos, this.blockPallet.rnd()); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/world/generator/structures/generatable/Tree.java b/src/main/java/eu/mhsl/minenet/minigames/world/generator/structures/generatable/Tree.java index 2196b76..e0d2a7a 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/world/generator/structures/generatable/Tree.java +++ b/src/main/java/eu/mhsl/minenet/minigames/world/generator/structures/generatable/Tree.java @@ -13,18 +13,18 @@ public class Tree extends Structure { @Override public void generateStructure(Block.Setter setter) { - int trunkX = position.blockX(); - int trunkBottomY = position.blockY(); - int trunkZ = position.blockZ(); + int trunkX = this.position.blockX(); + int trunkBottomY = this.position.blockY(); + int trunkZ = this.position.blockZ(); - for (int i = 0; i < 2; i++) { + for(int i = 0; i < 2; i++) { setter.setBlock(trunkX + 1, trunkBottomY + 3 + i, trunkZ, Block.OAK_LEAVES); setter.setBlock(trunkX - 1, trunkBottomY + 3 + i, trunkZ, Block.OAK_LEAVES); setter.setBlock(trunkX, trunkBottomY + 3 + i, trunkZ + 1, Block.OAK_LEAVES); setter.setBlock(trunkX, trunkBottomY + 3 + i, trunkZ - 1, Block.OAK_LEAVES); - for (int x = -1; x <= 1; x++) { - for (int z = -1; z <= 1; z++) { + for(int x = -1; x <= 1; x++) { + for(int z = -1; z <= 1; z++) { setter.setBlock(trunkX + x, trunkBottomY + 2 + i, trunkZ - z, Block.OAK_LEAVES); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/BaseGenerator.java b/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/BaseGenerator.java index a5348a9..0b0bcb8 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/BaseGenerator.java +++ b/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/BaseGenerator.java @@ -21,12 +21,12 @@ public abstract class BaseGenerator implements Generator { } protected void applyStructures(GenerationUnit unit) { - structures.forEach(structure -> unit.fork(structure::generateStructure)); + this.structures.forEach(structure -> unit.fork(structure::generateStructure)); } @Override public void generate(@NotNull GenerationUnit unit) { - mixIns.forEach(generator -> generator.generate(unit)); + this.mixIns.forEach(generator -> generator.generate(unit)); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/CircularPlateTerrainGenerator.java b/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/CircularPlateTerrainGenerator.java index 4b82ea7..b5c2d76 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/CircularPlateTerrainGenerator.java +++ b/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/CircularPlateTerrainGenerator.java @@ -1,6 +1,7 @@ package eu.mhsl.minenet.minigames.world.generator.terrain; -import de.articdive.jnoise.JNoise; +import de.articdive.jnoise.generators.noisegen.opensimplex.FastSimplexNoiseGenerator; +import de.articdive.jnoise.pipeline.JNoise; import eu.mhsl.minenet.minigames.util.NumberUtil; import eu.mhsl.minenet.minigames.world.BlockPallet; import eu.mhsl.minenet.minigames.world.generator.PlateTerrainGenerator; @@ -16,29 +17,27 @@ import java.util.Random; public class CircularPlateTerrainGenerator extends PlateTerrainGenerator { protected final Random rnd = new Random(); private final int size; + private final JNoise base = JNoise.newBuilder() + .fastSimplex(FastSimplexNoiseGenerator.newBuilder().setSeed(this.rnd.nextLong()).build()) + .scale(0.01) + .build(); + private final JNoise batches = JNoise.newBuilder() + .fastSimplex(FastSimplexNoiseGenerator.newBuilder().setSeed(this.rnd.nextLong()).build()) + .scale(0.05) + .build(); + private final JNoise peaks = JNoise.newBuilder() + .fastSimplex(FastSimplexNoiseGenerator.newBuilder().setSeed(this.rnd.nextLong()).build()) + .scale(0.1) + .build(); public CircularPlateTerrainGenerator(int size) { this.size = size; } - private final JNoise base = JNoise.newBuilder() - .fastSimplex() - .setSeed(rnd.nextLong()) - .setFrequency(0.01) - .build(); - - private final JNoise batches = JNoise.newBuilder() - .fastSimplex() - .setSeed(rnd.nextLong()) - .setFrequency(0.05) - .build(); - - private final JNoise peaks = JNoise.newBuilder() - .fastSimplex() - .setSeed(rnd.nextLong()) - .setFrequency(0.1) - .build(); - + private static double minTwo(double input) { + if(input < 2) return 2; + return input; + } @Override public void generate(@NotNull GenerationUnit unit) { @@ -46,40 +45,40 @@ public class CircularPlateTerrainGenerator extends PlateTerrainGenerator { if(unit.absoluteStart().distance(new Pos(0, 0, 0)) > 500 + this.size) return; - for (int x = 0; x < unit.size().x(); x++) { - for (int z = 0; z < unit.size().z(); z++) { + for(int x = 0; x < unit.size().x(); x++) { + for(int z = 0; z < unit.size().z(); z++) { Point bottom = start.add(x, 0, z); double distance = bottom.distance(new Pos(0, 0, 0)); - if(distance <= this.size && generatePlate()) { - unit.modifier().fill(bottom, bottom.add(1, plateHeight, 1), platePallet.rnd()); + if(distance <= this.size && this.generatePlate()) { + unit.modifier().fill(bottom, bottom.add(1, this.plateHeight, 1), this.platePallet.rnd()); continue; } unit.modifier().setBlock(bottom, Block.GRASS_BLOCK); - synchronized (base) { - double baseNoise = base.getNoise(bottom.x(), bottom.z()); + synchronized(this.base) { + double baseNoise = this.base.evaluateNoise(bottom.x(), bottom.z()); double currentHeight = minTwo(NumberUtil.map(distance, 0, 400, -((double) this.size / 5), 200)) + baseNoise * 8; - synchronized (batches) { - double elementNoise = batches.getNoise(bottom.x(), bottom.z()); + synchronized(this.batches) { + double elementNoise = this.batches.evaluateNoise(bottom.x(), bottom.z()); unit.modifier().fill( - bottom, - bottom.add(1, 1, 1) - .withY(currentHeight), - elementNoise < 0.9 ? elementNoise > 0 ? Block.GRASS_BLOCK : Block.SOUL_SAND : Block.STONE + bottom, + bottom.add(1, 1, 1) + .withY(currentHeight), + elementNoise < 0.9 ? elementNoise > 0 ? Block.GRASS_BLOCK : Block.SOUL_SAND : Block.STONE ); } - synchronized (peaks) { - double peakNoise = peaks.getNoise(bottom.x(), bottom.z()); + synchronized(this.peaks) { + double peakNoise = this.peaks.evaluateNoise(bottom.x(), bottom.z()); if(peakNoise > 0.97 && bottom.distance(new Pos(0, 0, 0)) > (this.size + 20)) { - Point center = bottom.add(1, currentHeight-3, 1); + Point center = bottom.add(1, currentHeight - 3, 1); unit.fork(setter -> new PeakRock(center, BlockPallet.STONE).generateStructure(setter)); } } @@ -90,9 +89,4 @@ public class CircularPlateTerrainGenerator extends PlateTerrainGenerator { } } - - private static double minTwo(double input) { - if(input < 2) return 2; - return input; - } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/FlatTerrainGenerator.java b/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/FlatTerrainGenerator.java deleted file mode 100644 index 07bde3f..0000000 --- a/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/FlatTerrainGenerator.java +++ /dev/null @@ -1,26 +0,0 @@ -package eu.mhsl.minenet.minigames.world.generator.terrain; - -import net.minestom.server.instance.block.Block; -import net.minestom.server.instance.generator.GenerationUnit; -import net.minestom.server.instance.generator.Generator; -import org.jetbrains.annotations.NotNull; - -public class FlatTerrainGenerator implements Generator { - private final int height; - private final Block block; - - public FlatTerrainGenerator(int height, Block block) { - this.height = height; - this.block = block; - } - - FlatTerrainGenerator() { - this.height = 5; - this.block = Block.GRASS_BLOCK; - } - - @Override - public void generate(@NotNull GenerationUnit unit) { - unit.modifier().fillHeight(0, height, block); - } -} diff --git a/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/HeightTerrainGenerator.java b/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/HeightTerrainGenerator.java index 362d3fe..0d456b3 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/HeightTerrainGenerator.java +++ b/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/HeightTerrainGenerator.java @@ -1,6 +1,7 @@ package eu.mhsl.minenet.minigames.world.generator.terrain; -import de.articdive.jnoise.JNoise; +import de.articdive.jnoise.generators.noisegen.opensimplex.FastSimplexNoiseGenerator; +import de.articdive.jnoise.pipeline.JNoise; import eu.mhsl.minenet.minigames.util.GeneratorUtils; import eu.mhsl.minenet.minigames.util.NumberUtil; import eu.mhsl.minenet.minigames.world.BlockPallet; @@ -16,60 +17,65 @@ import java.util.function.Function; public class HeightTerrainGenerator extends BaseGenerator { protected final Random rnd = new Random(); - private Function calculateHeight = (Point) -> 0; + private final JNoise base = JNoise.newBuilder() + .fastSimplex(FastSimplexNoiseGenerator.newBuilder().setSeed(this.rnd.nextLong()).build()) + .scale(0.01) + .build(); + private final JNoise batches = JNoise.newBuilder() + .fastSimplex(FastSimplexNoiseGenerator.newBuilder().setSeed(this.rnd.nextLong()).build()) + .scale(0.05) + .build(); + private final JNoise peaks = JNoise.newBuilder() + .fastSimplex(FastSimplexNoiseGenerator.newBuilder().setSeed(this.rnd.nextLong()).build()) + .scale(0.1) + .build(); + private final BlockPallet blockPallet = BlockPallet.STONE; protected Function heightNoiseMultiplier = (Point) -> 16; protected Function calculateSeaLevel = null; - - private final JNoise base = JNoise.newBuilder() - .fastSimplex() - .setSeed(rnd.nextLong()) - .setFrequency(0.01) - .build(); - - private final JNoise batches = JNoise.newBuilder() - .fastSimplex() - .setSeed(rnd.nextLong()) - .setFrequency(0.05) - .build(); - - private final JNoise peaks = JNoise.newBuilder() - .fastSimplex() - .setSeed(rnd.nextLong()) - .setFrequency(0.1) - .build(); - - private final BlockPallet blockPallet = BlockPallet.STONE; + private Function calculateHeight = (Point) -> 0; @Override public void generate(@NotNull GenerationUnit unit) { super.generate(unit); GeneratorUtils.foreachXZ(unit, bottomPoint -> { - unit.modifier().setBlock(bottomPoint, blockPallet.rnd()); + unit.modifier().setBlock(bottomPoint, this.blockPallet.rnd()); - synchronized (base) { - double heightNoise = base.getNoise(bottomPoint.x(), bottomPoint.z()); - double noiseModifier = heightNoise * heightNoiseMultiplier.apply(bottomPoint); - double heightModifier = NumberUtil.clamp(calculateHeight.apply(bottomPoint) + noiseModifier, 1d, unit.size().y()); + synchronized(this.base) { + double heightNoise = this.base.evaluateNoise(bottomPoint.x(), bottomPoint.z()); + double noiseModifier = heightNoise * this.heightNoiseMultiplier.apply(bottomPoint); + double heightModifier = NumberUtil.clamp(this.calculateHeight.apply(bottomPoint) + noiseModifier, 1d, unit.size().y()); if(heightModifier < 1) System.out.println("HEIGHT MODIFIER ILLEGAL"); - synchronized (batches) { - double batchNoise = batches.getNoise(bottomPoint.x(), bottomPoint.z()); + synchronized(this.batches) { + double batchNoise = this.batches.evaluateNoise(bottomPoint.x(), bottomPoint.z()); - unit.modifier().fill(bottomPoint, bottomPoint.add(1, heightModifier, 1), batchNoise < 0.9 ? batchNoise > 0 ? Block.GRASS_BLOCK : Block.SOUL_SAND : Block.STONE); + Block block = batchNoise < 0.9 ? batchNoise > -0.2 ? Block.GRASS_BLOCK : Block.SOUL_SAND : Block.STONE; + unit.modifier().fill(bottomPoint, bottomPoint.add(1, heightModifier, 1), block); + if(this.rnd.nextInt(0, 5) < 1 && block == Block.GRASS_BLOCK) { + int randomInt = this.rnd.nextInt(0, 6); + if(randomInt > 1) { + unit.modifier().setBlock(bottomPoint.add(0, heightModifier, 0), Block.SHORT_GRASS); + } else if(randomInt > 0) { + unit.modifier().setBlock(bottomPoint.add(0, heightModifier, 0), BlockPallet.FLOWER.rnd()); + } else { + unit.modifier().setBlock(bottomPoint.add(0, heightModifier, 0), Block.TALL_GRASS); + unit.modifier().setBlock(bottomPoint.add(0, heightModifier + 1, 0), Block.TALL_GRASS.withProperty("half", "upper")); + } + } - if(calculateSeaLevel != null) { + if(this.calculateSeaLevel != null) { Point absoluteHeight = bottomPoint.add(0, heightModifier, 0); - int seaLevel = calculateSeaLevel.apply(bottomPoint); + int seaLevel = this.calculateSeaLevel.apply(bottomPoint); if(absoluteHeight.y() < seaLevel) { // System.out.println("HM:" + absoluteHeight.y() + " SL:" + seaLevel); // System.out.println("Filling from " + bottomPoint.y() + " to " + absoluteHeight.withY(seaLevel).y()); - unit.modifier().fill(bottomPoint.withY(v -> v+heightModifier), absoluteHeight.add(1, 0, 1).withY(seaLevel), Block.WATER); + unit.modifier().fill(bottomPoint.withY(v -> v + heightModifier), absoluteHeight.add(1, 0, 1).withY(seaLevel), Block.WATER); } } } - synchronized (peaks) { - double peakNoise = peaks.getNoise(bottomPoint.x(), bottomPoint.z()); + synchronized(this.peaks) { + double peakNoise = this.peaks.evaluateNoise(bottomPoint.x(), bottomPoint.z()); if(peakNoise > 0.97 && bottomPoint.distance(new Pos(0, 0, 0)) > (50 + 20)) { diff --git a/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/PlaneTerrainGenerator.java b/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/PlaneTerrainGenerator.java index 9694799..aee7d24 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/PlaneTerrainGenerator.java +++ b/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/PlaneTerrainGenerator.java @@ -14,13 +14,8 @@ public class PlaneTerrainGenerator implements Generator { this.block = block; } - PlaneTerrainGenerator() { - this.height = 5; - this.block = Block.GRASS_BLOCK; - } - @Override public void generate(@NotNull GenerationUnit unit) { - unit.modifier().fillHeight(height-1, height, block); + unit.modifier().fillHeight(this.height - 1, this.height, this.block); } } diff --git a/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/SquarePlateTerrainGenerator.java b/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/SquarePlateTerrainGenerator.java index 30bfd3b..33cb63e 100644 --- a/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/SquarePlateTerrainGenerator.java +++ b/src/main/java/eu/mhsl/minenet/minigames/world/generator/terrain/SquarePlateTerrainGenerator.java @@ -1,6 +1,7 @@ package eu.mhsl.minenet.minigames.world.generator.terrain; -import de.articdive.jnoise.JNoise; +import de.articdive.jnoise.generators.noisegen.opensimplex.FastSimplexNoiseGenerator; +import de.articdive.jnoise.pipeline.JNoise; import eu.mhsl.minenet.minigames.util.NumberUtil; import eu.mhsl.minenet.minigames.world.BlockPallet; import eu.mhsl.minenet.minigames.world.generator.PlateTerrainGenerator; @@ -20,31 +21,28 @@ public class SquarePlateTerrainGenerator extends PlateTerrainGenerator { private final int width; private final int length; - + private final JNoise base = JNoise.newBuilder() + .fastSimplex(FastSimplexNoiseGenerator.newBuilder().setSeed(this.rnd.nextLong()).build()) + .scale(0.01) + .build(); + private final JNoise batches = JNoise.newBuilder() + .fastSimplex(FastSimplexNoiseGenerator.newBuilder().setSeed(this.rnd.nextLong()).build()) + .scale(0.05) + .build(); + private final JNoise peaks = JNoise.newBuilder() + .fastSimplex(FastSimplexNoiseGenerator.newBuilder().setSeed(this.rnd.nextLong()).build()) + .scale(0.1) + .build(); public SquarePlateTerrainGenerator(int width, int length) { this.width = width; this.length = length; } - private final JNoise base = JNoise.newBuilder() - .fastSimplex() - .setSeed(rnd.nextLong()) - .setFrequency(0.01) - .build(); - - private final JNoise batches = JNoise.newBuilder() - .fastSimplex() - .setSeed(rnd.nextLong()) - .setFrequency(0.05) - .build(); - - private final JNoise peaks = JNoise.newBuilder() - .fastSimplex() - .setSeed(rnd.nextLong()) - .setFrequency(0.1) - .build(); - + private static double minTwo(double input) { + if(input < 2) return 2; + return input; + } @Override public void generate(@NotNull GenerationUnit unit) { @@ -52,28 +50,28 @@ public class SquarePlateTerrainGenerator extends PlateTerrainGenerator { // don't generate more than 500 blocks outwards Point chunkStart = unit.absoluteStart(); - if(chunkStart.z() > length + 500 || chunkStart.z() < -500) return; - if(chunkStart.x() < -500 || chunkStart.x() > width + 500) return; + if(chunkStart.z() > this.length + 500 || chunkStart.z() < -500) return; + if(chunkStart.x() < -500 || chunkStart.x() > this.width + 500) return; - for (int x = 0; x < unit.size().x(); x++) { - for (int z = 0; z < unit.size().z(); z++) { + for(int x = 0; x < unit.size().x(); x++) { + for(int z = 0; z < unit.size().z(); z++) { Point bottom = start.add(x, 0, z); - if(generatePlate()) { - if(bottom.x() <= width && bottom.x() >= 0 && bottom.z() <= length && bottom.z() >= 0) { - unit.modifier().fill(bottom, bottom.add(1, plateHeight, 1), platePallet.rnd()); + if(this.generatePlate()) { + if(bottom.x() <= this.width && bottom.x() >= 0 && bottom.z() <= this.length && bottom.z() >= 0) { + unit.modifier().fill(bottom, bottom.add(1, this.plateHeight, 1), this.platePallet.rnd()); continue; } } - if(generateBorders) { + if(this.generateBorders) { Runnable generateBorder = () -> unit.modifier().fill(bottom, bottom.add(1, DimensionType.VANILLA_MAX_Y, 1), Block.BARRIER); - if(bottom.z() <= length+1 && bottom.z() >= -1 && bottom.x() >= -1 && bottom.x() <= width+1) { - if(bottom.x() == -1 || bottom.x() == width+1) { + if(bottom.z() <= this.length + 1 && bottom.z() >= -1 && bottom.x() >= -1 && bottom.x() <= this.width + 1) { + if(bottom.x() == -1 || bottom.x() == this.width + 1) { generateBorder.run(); } - if(bottom.z() == -1 || bottom.z() == length + 1) { + if(bottom.z() == -1 || bottom.z() == this.length + 1) { generateBorder.run(); } } @@ -81,34 +79,34 @@ public class SquarePlateTerrainGenerator extends PlateTerrainGenerator { unit.modifier().setBlock(bottom, Block.GRASS_BLOCK); - synchronized (base) { - double baseNoise = base.getNoise(bottom.x(), bottom.z()); + synchronized(this.base) { + double baseNoise = this.base.evaluateNoise(bottom.x(), bottom.z()); double[] possibleHeights = { minTwo(NumberUtil.map(bottom.distance(new Pos(0, 0, 0)), 0, 400, -((double) this.width / 5), 200)) + baseNoise * 8, - minTwo(NumberUtil.map(bottom.distance(new Pos(width, 0, 0)), 0, 400, -((double) this.width / 5), 200)) + baseNoise * 8, - minTwo(NumberUtil.map(bottom.distance(new Pos(0, 0, length)), 0, 400, -((double) this.width / 5), 200)) + baseNoise * 8, - minTwo(NumberUtil.map(bottom.distance(new Pos(width, 0, length)), 0, 400, -((double) this.width / 5), 200)) + baseNoise * 8 + minTwo(NumberUtil.map(bottom.distance(new Pos(this.width, 0, 0)), 0, 400, -((double) this.width / 5), 200)) + baseNoise * 8, + minTwo(NumberUtil.map(bottom.distance(new Pos(0, 0, this.length)), 0, 400, -((double) this.width / 5), 200)) + baseNoise * 8, + minTwo(NumberUtil.map(bottom.distance(new Pos(this.width, 0, this.length)), 0, 400, -((double) this.width / 5), 200)) + baseNoise * 8 }; double currentHeight = Arrays.stream(possibleHeights).min().getAsDouble(); - synchronized (batches) { - double elementNoise = batches.getNoise(bottom.x(), bottom.z()); + synchronized(this.batches) { + double elementNoise = this.batches.evaluateNoise(bottom.x(), bottom.z()); unit.modifier().fill( - bottom, - bottom.add(1, 1, 1) - .withY(currentHeight), - elementNoise < 0.9 ? elementNoise > 0 ? Block.GRASS_BLOCK : Block.SOUL_SAND : Block.STONE + bottom, + bottom.add(1, 1, 1) + .withY(currentHeight), + elementNoise < 0.9 ? elementNoise > 0 ? Block.GRASS_BLOCK : Block.SOUL_SAND : Block.STONE ); } - synchronized (peaks) { - double peakNoise = peaks.getNoise(bottom.x(), bottom.z()); + synchronized(this.peaks) { + double peakNoise = this.peaks.evaluateNoise(bottom.x(), bottom.z()); if(peakNoise > 0.97 && bottom.distance(new Pos(0, 0, 0)) > (this.width + 20)) { - Point center = bottom.add(1, currentHeight-3, 1); + Point center = bottom.add(1, currentHeight - 3, 1); unit.fork(setter -> new PeakRock(center, BlockPallet.STONE).generateStructure(setter)); } } @@ -119,9 +117,4 @@ public class SquarePlateTerrainGenerator extends PlateTerrainGenerator { } } - - private static double minTwo(double input) { - if(input < 2) return 2; - return input; - } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 39956b0..f7c93d6 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -9,6 +9,8 @@ bungeecord: velocity: enabled: false + # Chat nur über den Proxy verteilen, hier verwerfen (chat-bridge) + chat-via-proxy: false secret: '' api: diff --git a/src/main/resources/lang/locales.map.csv b/src/main/resources/lang/locales.map.csv index d0e2435..79185ef 100644 --- a/src/main/resources/lang/locales.map.csv +++ b/src/main/resources/lang/locales.map.csv @@ -9,6 +9,13 @@ select_language;Please select your prefered Language;Bitte wähle deine bevorzug welcome;Welcome!;Willkommen! back;Back;Zurück forward;Next;Nächste +leave;left the game;hat das Spiel verlassen +secondsLeft_singular;second left;Sekunde verbleibt +secondsLeft_plural;seconds left;Sekunden verbleiben +unknownCommand;Unknown command;Unbekannter Befehl +startIn;Start in;Startet in +second;second;Sekunde +seconds;seconds;Sekunden ;; ns:tablist#UNUSED;; title;MineNet Network;MineNet Servernetzwerk @@ -65,6 +72,11 @@ ns:room#;; invTitle;Select a Minigame;Wähle einen Spielmodus noOption;No options here;Keine Optionen hier noOptionDescription;There are no options for this Game;Es gibt keine Einstellungen für dieses Spiel +ownerSelf;You are now the owner of this room!;Du bist nun Besitzer dieser Lobby! +ownerSet;The new owner has been set!;Der neue Besitzer wurde festgelegt! +ownerLeft;The room owner has left!;Der Lobbybesitzer hat das Spiel verlassen! +newOwnerAnnounce;is the new owner!;ist der neue Besitzer +onlyOwnerCanStart;Only the room leader can start games!;Nur der Raumbesitzer kann Spiele starten! ;; ns:GameFactory#;; missingDescription;No description;Keine Beschreibung @@ -78,6 +90,7 @@ ns:game_ElytraRace#;; name;Elytra race;Elytra Rennen description;Be fast while flying through the rings;Sei schnell während du durch die Ringe fliegst ringCount;ring count;Anzahl der Ringe +launchIn;Launch in;Abschuss in ;; ns:game_Minerun#;; name;Minerun;Minenrennen @@ -92,7 +105,7 @@ optionPvpEnabled;pvp enabled;PvP aktiviert ;; ns:game_Stickfight#;; name;Stickfight;Stockschlacht -description;Push your opponents off the Bridge;Stoße deine Gegener von der Brücke +description;Push your opponents off the Bridge;Stoße deine Gegener von der Brücke, der Spieler mit den wenigsten Toden gewinnt! ;; ns:game_TrafficlightRace#;; name;Red light green light;Rotes licht, Grünes licht @@ -130,3 +143,89 @@ description;Run away from falling anvils;Renne von fallenden Ambossen davon ns:game_jumpDive#;; name;Jump dive;Wassersprung description;Jump into the water, avoiding already used spots!;Springe ins wasser an stellen, in denen noch niemand zuvor gelandet ist! +;; +ns:game_Sumo#;; +name;Sumo;Sumo +lives;Lives;Leben +description;Knock your enemies off and stay on top!;Versuche deinen Gegner von der Plattform zu schubsen! +;; +ns:game_Highground#;; +name;Highground;Hochburg +description;Stay on the high ground to win!;Bleibe solange wie möglich auf der Hochburg, um zu gewinnen! +;; +ns:game_Fastbridge#;; +name;Fastbridge;Fastbridge +description;Speedbridge to the other platform. The first one there wins!;Baue dich so schnell wie möglich zur anderen Plattform. Wer zuerst dort ist, gewinnt! +;; +ns:game_BlockBreakRace#;; +name;Block Break Race;Blockbruch-Rennen +description;Dig down through the tubes using the right tools. The first player to reach the bottom wins!;Grabe dich durch die Röhren nach unten und verwende dabei das richtige Werkzeug. Wer zuerst unten ankommt, gewinnt! +;; +ns:game_SpaceSnake#;; +name;Space Snake;Weltraum-Snake +description;Collect diamonds while extending your snake bridge through space and fight other players. The player with the longest bridge wins!;Sammle Diamanten, während du deine Schlangenbrücke durchs All erweiterst und gegen andere Spieler kämpfst. Der Spieler mit den der Längsten Brücke gewinnt! +powerUpCount;Number of diamonds in the arena;Anzahl der Diamanten in der Arena +;; +ns:game_TurtleGame#;; +name;Turtle Game;Turtle Game +description;Eat snacks and dodge bombs to get the highest score!;Esse Snacks und weiche Bomben aus, um den höchsten Score zu erreichen! +startSpeed;Start Speed;Startgeschwindigkeit +;; +ns:game_BoatRace#;; +name;Boatrace;Bootrennen +description;; +;; +ns:game_Pillars#;; +name;Pillars;Pillars +description;Build yourself up with your random blocks to reach your opponents and push them down!;Baue dich mit deinen zufälligen Blöcken zu deinen Gegnern und schupse sie runter! +;; +ns:game_BlockBattle#;; +name;Block Battle;Block Kampf +description;The team that fills the center with their color first wins!;Das Team, welches als erstes die Mitte mit seiner Farbe gefüllt hat, gewinnt! +itemCount;Block Count;Block Anzahl +;; +ns:game_ColorJump#;; +name;Color Jump;Farbsprung +description;Jump on the right color!;Springe auf die richtige Farbe! +;; +ns:game_PlayerRace#;; +name;Player Race;Wettrennen +description;Get to the other side as quickly as possible!;Komme so schnell wie möglich auf die andere Seite! +;; +ns:game_BuildBattle#;; +name;Build Battle;Bauwettbewerb +description;Build the best creation for the given theme before time runs out and win through the other players’ ratings.;Baue in der Zeit das beste Bauwerk zum Thema und gewinne durch die Bewertung der anderen Spieler. +seconds;Build Time;Bauzeit +;; +ns:game_MobArena#;; +name;Mob Arena;Mob-Arena +description;Fight hordes of monsters with a sweeping sword and collect points for every kill. Dying costs points!;Kämpfe mit deinem Schwert gegen Horden von Monstern und sammle Punkte für jeden Kill. Sterben kostet Punkte! +death;You died!;Du bist gestorben! +difficulty;Difficulty;Schwierigkeit +deathPenalty;Points lost on death;Punktabzug bei Tod +powerUps;Power-ups;Power-ups +powerUp_heal;Healed!;Geheilt! +powerUp_strength;Double damage for 10 seconds!;Doppelter Schaden für 10 Sekunden! +powerUp_speed;Speed for 10 seconds!;Schneller für 10 Sekunden! +;; +ns:game_Backrooms#;; +name;Backrooms;Backrooms +description;Escape the Backrooms before the others do - or be the last one alive!;Entkomme den Backrooms vor allen anderen - oder überlebe als Letzter! +level_lobby;Level 0 - The Lobby;Level 0 - Die Lobby +level_habitable;Level 1 - Habitable Zone;Level 1 - Bewohnbare Zone +level_pipeDreams;Level 2 - Pipe Dreams;Level 2 - Rohrträume +level_poolrooms;The Poolrooms;Die Poolrooms +level_lightsOut;Level 6 - Lights Out;Level 6 - Licht aus +level_fun;Level Fun;Level Fun +level_finale;Level ! - Run For Your Life;Level ! - Lauf um dein Leben +optionLevels;Number of levels;Anzahl der Level +optionLives;Lives;Leben +optionMonsterDensity;Monster density;Monsterdichte +optionSize;Level size - 0 is auto;Levelgroesse - 0 ist automatisch +optionHardcoreSanity;Hardcore sanity;Hardcore-Verstand +escaped;You escaped the Backrooms!;Du bist den Backrooms entkommen! +eliminated;The Backrooms got you;Die Backrooms haben dich +died;You died - but the Backrooms are not done with you;Du bist gestorben - aber die Backrooms sind noch nicht fertig mit dir +finaleRun;RUN!;RENN! +finaleCountdown;The horde arrives in;Die Horde kommt in +doorOpened;Someone opened the exit door - all keys for this level crumble to dust;Jemand hat die Ausgangstuer geoeffnet - alle Schluessel dieses Levels zerfallen zu Staub diff --git a/src/main/resources/maps/game/bedwars/test/region/r.-1.-1.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-1.-1.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.-1.-1.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-1.-1.mca diff --git a/src/main/resources/maps/game/bedwars/test/region/r.-1.-2.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-1.-2.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.-1.-2.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-1.-2.mca diff --git a/src/main/resources/maps/game/bedwars/test/region/r.-1.0.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-1.0.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.-1.0.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-1.0.mca diff --git a/src/main/resources/maps/game/bedwars/test/region/r.-1.1.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-1.1.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.-1.1.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-1.1.mca diff --git a/src/main/resources/maps/game/bedwars/test/region/r.-2.-1.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-2.-1.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.-2.-1.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-2.-1.mca diff --git a/src/main/resources/maps/game/bedwars/test/region/r.-2.-2.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-2.-2.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.-2.-2.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-2.-2.mca diff --git a/src/main/resources/maps/game/bedwars/test/region/r.-2.0.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-2.0.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.-2.0.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-2.0.mca diff --git a/src/main/resources/maps/game/bedwars/test/region/r.-2.1.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-2.1.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.-2.1.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.-2.1.mca diff --git a/src/main/resources/maps/game/bedwars/test/region/r.0.-1.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.0.-1.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.0.-1.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.0.-1.mca diff --git a/src/main/resources/maps/game/bedwars/test/region/r.0.-2.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.0.-2.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.0.-2.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.0.-2.mca diff --git a/src/main/resources/maps/game/bedwars/test/region/r.0.0.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.0.0.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.0.0.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.0.0.mca diff --git a/src/main/resources/maps/game/bedwars/test/region/r.0.1.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.0.1.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.0.1.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.0.1.mca diff --git a/src/main/resources/maps/game/bedwars/test/region/r.1.-1.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.1.-1.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.1.-1.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.1.-1.mca diff --git a/src/main/resources/maps/game/bedwars/test/region/r.1.-2.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.1.-2.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.1.-2.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.1.-2.mca diff --git a/src/main/resources/maps/game/bedwars/test/region/r.1.0.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.1.0.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.1.0.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.1.0.mca diff --git a/src/main/resources/maps/game/bedwars/test/region/r.1.1.mca b/src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.1.1.mca similarity index 100% rename from src/main/resources/maps/game/bedwars/test/region/r.1.1.mca rename to src/main/resources/maps/game/bedwars/test/dimensions/minecraft/overworld/region/r.1.1.mca diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-1.-1.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-1.-1.mca new file mode 100644 index 0000000..2b2fad4 Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-1.-1.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-1.-2.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-1.-2.mca new file mode 100644 index 0000000..eb31c6d Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-1.-2.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-1.0.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-1.0.mca new file mode 100644 index 0000000..1adccba Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-1.0.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-1.1.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-1.1.mca new file mode 100644 index 0000000..af8b339 Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-1.1.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-1.2.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-1.2.mca new file mode 100644 index 0000000..1177b79 Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-1.2.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-2.-1.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-2.-1.mca new file mode 100644 index 0000000..201ea03 Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-2.-1.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-2.-2.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-2.-2.mca new file mode 100644 index 0000000..9a006fa Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-2.-2.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-2.0.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-2.0.mca new file mode 100644 index 0000000..ec2e80e Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-2.0.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-2.1.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-2.1.mca new file mode 100644 index 0000000..7abeebe Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-2.1.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-2.2.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-2.2.mca new file mode 100644 index 0000000..c549eeb Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.-2.2.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.0.-1.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.0.-1.mca new file mode 100644 index 0000000..945c89d Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.0.-1.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.0.-2.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.0.-2.mca new file mode 100644 index 0000000..2a2c179 Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.0.-2.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.0.0.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.0.0.mca new file mode 100644 index 0000000..07e3e6a Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.0.0.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.0.1.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.0.1.mca new file mode 100644 index 0000000..75994be Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.0.1.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.0.2.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.0.2.mca new file mode 100644 index 0000000..0d42ff5 Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.0.2.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.1.-1.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.1.-1.mca new file mode 100644 index 0000000..125dece Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.1.-1.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.1.-2.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.1.-2.mca new file mode 100644 index 0000000..e610f32 Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.1.-2.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.1.0.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.1.0.mca new file mode 100644 index 0000000..7c94872 Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.1.0.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.1.1.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.1.1.mca new file mode 100644 index 0000000..e3d84f8 Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.1.1.mca differ diff --git a/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.1.2.mca b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.1.2.mca new file mode 100644 index 0000000..6b32012 Binary files /dev/null and b/src/main/resources/maps/game/boatRace/woodlandMansion/dimensions/minecraft/overworld/region/r.1.2.mca differ diff --git a/src/main/resources/maps/hub/region/r.-1.-1.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-1.-1.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.-1.-1.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-1.-1.mca diff --git a/src/main/resources/maps/hub/region/r.-1.-2.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-1.-2.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.-1.-2.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-1.-2.mca diff --git a/src/main/resources/maps/hub/region/r.-1.0.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-1.0.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.-1.0.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-1.0.mca diff --git a/src/main/resources/maps/hub/region/r.-1.1.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-1.1.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.-1.1.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-1.1.mca diff --git a/src/main/resources/maps/hub/region/r.-2.-1.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-2.-1.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.-2.-1.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-2.-1.mca diff --git a/src/main/resources/maps/hub/region/r.-2.-2.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-2.-2.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.-2.-2.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-2.-2.mca diff --git a/src/main/resources/maps/hub/region/r.-2.0.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-2.0.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.-2.0.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-2.0.mca diff --git a/src/main/resources/maps/hub/region/r.-2.1.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-2.1.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.-2.1.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.-2.1.mca diff --git a/src/main/resources/maps/hub/region/r.0.-1.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.0.-1.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.0.-1.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.0.-1.mca diff --git a/src/main/resources/maps/hub/region/r.0.-2.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.0.-2.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.0.-2.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.0.-2.mca diff --git a/src/main/resources/maps/hub/region/r.0.0.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.0.0.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.0.0.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.0.0.mca diff --git a/src/main/resources/maps/hub/region/r.0.1.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.0.1.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.0.1.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.0.1.mca diff --git a/src/main/resources/maps/hub/region/r.1.-1.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.1.-1.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.1.-1.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.1.-1.mca diff --git a/src/main/resources/maps/hub/region/r.1.-2.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.1.-2.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.1.-2.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.1.-2.mca diff --git a/src/main/resources/maps/hub/region/r.1.0.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.1.0.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.1.0.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.1.0.mca diff --git a/src/main/resources/maps/hub/region/r.1.1.mca b/src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.1.1.mca similarity index 100% rename from src/main/resources/maps/hub/region/r.1.1.mca rename to src/main/resources/maps/hub/dimensions/minecraft/overworld/region/r.1.1.mca diff --git a/src/main/resources/maps/lobby/region/r.-1.-1.mca b/src/main/resources/maps/lobby/dimensions/minecraft/overworld/region/r.-1.-1.mca similarity index 100% rename from src/main/resources/maps/lobby/region/r.-1.-1.mca rename to src/main/resources/maps/lobby/dimensions/minecraft/overworld/region/r.-1.-1.mca diff --git a/src/main/resources/maps/lobby/region/r.-1.0.mca b/src/main/resources/maps/lobby/dimensions/minecraft/overworld/region/r.-1.0.mca similarity index 100% rename from src/main/resources/maps/lobby/region/r.-1.0.mca rename to src/main/resources/maps/lobby/dimensions/minecraft/overworld/region/r.-1.0.mca diff --git a/src/main/resources/maps/lobby/region/r.0.-1.mca b/src/main/resources/maps/lobby/dimensions/minecraft/overworld/region/r.0.-1.mca similarity index 100% rename from src/main/resources/maps/lobby/region/r.0.-1.mca rename to src/main/resources/maps/lobby/dimensions/minecraft/overworld/region/r.0.-1.mca diff --git a/src/main/resources/maps/lobby/region/r.0.0.mca b/src/main/resources/maps/lobby/dimensions/minecraft/overworld/region/r.0.0.mca similarity index 100% rename from src/main/resources/maps/lobby/region/r.0.0.mca rename to src/main/resources/maps/lobby/dimensions/minecraft/overworld/region/r.0.0.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.-1.-1.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-1.-1.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.-1.-1.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-1.-1.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.-1.-2.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-1.-2.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.-1.-2.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-1.-2.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.-1.0.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-1.0.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.-1.0.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-1.0.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.-1.1.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-1.1.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.-1.1.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-1.1.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.-2.-1.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-2.-1.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.-2.-1.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-2.-1.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.-2.-2.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-2.-2.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.-2.-2.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-2.-2.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.-2.0.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-2.0.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.-2.0.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-2.0.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.-2.1.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-2.1.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.-2.1.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.-2.1.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.0.-1.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.0.-1.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.0.-1.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.0.-1.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.0.-2.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.0.-2.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.0.-2.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.0.-2.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.0.0.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.0.0.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.0.0.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.0.0.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.0.1.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.0.1.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.0.1.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.0.1.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.1.-1.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.1.-1.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.1.-1.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.1.-1.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.1.-2.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.1.-2.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.1.-2.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.1.-2.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.1.0.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.1.0.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.1.0.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.1.0.mca diff --git a/src/main/resources/maps/resultdisplay/region/r.1.1.mca b/src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.1.1.mca similarity index 100% rename from src/main/resources/maps/resultdisplay/region/r.1.1.mca rename to src/main/resources/maps/resultdisplay/dimensions/minecraft/overworld/region/r.1.1.mca