Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a9f3a8ad0 | |||
| 817693239b | |||
| 26eff149d4 | |||
| 7734b225eb | |||
| 1a9b9f5fd2 | |||
| b0cf527977 | |||
| 01646546a3 | |||
| 13a0cdde3a | |||
| 7d60c74d85 | |||
| 0d7f1fc646 | |||
| ee421245d1 | |||
| d75332927c | |||
| 2c579f4afe | |||
| 82984cf5cf | |||
| 14f6a38212 | |||
| 3bdc4113c0 | |||
| b7e292fe0e | |||
| 57cd5acdfd | |||
| 0f72b8049d | |||
| 9992b07ed5 | |||
| f55148203e | |||
| 41753f2805 | |||
| 9bdb98cdc9 | |||
| 6f0ebe4d12 | |||
| 58983be156 | |||
| ee7d434d7e | |||
| 410f7b4027 | |||
| e1515200ab | |||
| 27005f4fc4 | |||
| 2808393c23 | |||
| d0825695b3 | |||
| e822a5c5e2 | |||
| bf95c8dcc2 | |||
| 2f70b25e87 | |||
| cbec1ea7f8 | |||
| c176cfaf2c | |||
| 56f56d48b6 | |||
| 991e51bc10 | |||
| ffba86ac41 | |||
| f95c670514 | |||
| 0b3c757ce0 | |||
| 76df6643db | |||
| 7186b4dbea | |||
| 24d79d7a11 | |||
| 70fd5bafdb | |||
| 11ddd01470 | |||
| 7d5fb025bd | |||
| 05240660f2 | |||
| d0031b4ea5 | |||
| 360266339d | |||
| 325fba2a53 | |||
| e760cdb2c6 | |||
| c111c027ff |
Generated
+7
@@ -3,4 +3,11 @@
|
|||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="21" />
|
<bytecodeTargetLevel target="21" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="JavacSettings">
|
||||||
|
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||||
|
<module name="Minigames" options="-Xlint:deprecation" />
|
||||||
|
<module name="Minigames.test" options="-Xlint:deprecation" />
|
||||||
|
<module name="eu.mhsl.minenet.Minigames.main" options="-Xlint:deprecation" />
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
</project>
|
</project>
|
||||||
Generated
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK" />
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK" />
|
||||||
</project>
|
</project>
|
||||||
+17
-40
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id "com.github.johnrengelman.shadow" version "7.1.0"
|
id "com.gradleup.shadow" version "9.6.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
group 'eu.mhsl.minenet'
|
group 'eu.mhsl.minenet'
|
||||||
@@ -11,58 +11,33 @@ version '1.0-SNAPSHOT'
|
|||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
|
||||||
|
|
||||||
maven {
|
|
||||||
url 'https://jitpack.io'
|
|
||||||
}
|
|
||||||
|
|
||||||
maven {
|
|
||||||
url "https://repo.unnamed.team/repository/unnamed-public/"
|
|
||||||
}
|
|
||||||
|
|
||||||
maven {
|
|
||||||
url 'https://oss.sonatype.org/content/repositories/snapshots'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
maven { url "https://jitpack.io" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(21)
|
languageVersion = JavaLanguageVersion.of(25)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
|
testImplementation 'org.junit.jupiter:junit-jupiter:6.1.2'
|
||||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||||
|
|
||||||
//https://jitpack.io/#Minestom/Minestom
|
//https://mvnrepository.com/artifact/net.minestom/minestom
|
||||||
implementation 'net.minestom:minestom-snapshots:fd51c8d17a'
|
implementation 'net.minestom:minestom:2026.07.12-26.2'
|
||||||
|
|
||||||
//Tools
|
//Tools
|
||||||
implementation 'de.articdive:jnoise:3.0.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 'net.md-5:bungeecord-config:1.21-R0.3'
|
||||||
implementation 'org.apache.commons:commons-text:1.10.0'
|
implementation 'org.apache.commons:commons-text:1.15.0'
|
||||||
implementation 'org.spongepowered:configurate-yaml:4.1.2'
|
implementation 'org.spongepowered:configurate-yaml:4.2.0'
|
||||||
implementation 'com.sparkjava:spark-core:2.9.4'
|
implementation 'com.sparkjava:spark-core:2.9.4'
|
||||||
implementation 'com.google.code.gson:gson:2.10.1'
|
implementation 'com.google.code.gson:gson:2.14.0'
|
||||||
implementation 'com.google.guava:guava:32.0.1-android'
|
implementation 'com.google.guava:guava:33.4.8-jre'
|
||||||
|
|
||||||
|
|
||||||
//PvP
|
//PvP
|
||||||
implementation 'io.github.TogAr2:MinestomPvP:PR62-SNAPSHOT'
|
implementation 'io.github.togar2:MinestomPvP:2026.05.30-26.1.1'
|
||||||
|
|
||||||
// Hephaestus engine
|
|
||||||
// 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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
@@ -71,8 +46,6 @@ tasks {
|
|||||||
attributes 'Main-Class': 'eu.mhsl.minenet.minigames.Main'
|
attributes 'Main-Class': 'eu.mhsl.minenet.minigames.Main'
|
||||||
attributes 'Multi-Release': true
|
attributes 'Multi-Release': true
|
||||||
}
|
}
|
||||||
duplicatesStrategy = 'exclude'
|
|
||||||
from configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) }
|
|
||||||
}
|
}
|
||||||
build {
|
build {
|
||||||
dependsOn(shadowJar)
|
dependsOn(shadowJar)
|
||||||
@@ -89,3 +62,7 @@ tasks.register('copyJarToServer', Exec) {
|
|||||||
|
|
||||||
commandLine 'scp', 'build/libs/Minigames-1.0-SNAPSHOT.jar', 'root@10.20.6.5:/root/minigames'
|
commandLine 'scp', 'build/libs/Minigames-1.0-SNAPSHOT.jar', 'root@10.20.6.5:/root/minigames'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType(JavaCompile) {
|
||||||
|
options.compilerArgs << "-Xlint:deprecation"
|
||||||
|
}
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+3
-1
@@ -1,7 +1,9 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
networkTimeout=10000
|
||||||
|
retries=0
|
||||||
|
retryBackOffMs=500
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015-2021 the original authors.
|
# Copyright © 2015 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with 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:
|
# Important for running:
|
||||||
#
|
#
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
# bash, then to run this script, type that shell name before the whole
|
# bash, then to run this script, type that shell name before the whole
|
||||||
# command line, like:
|
# command line, like:
|
||||||
#
|
#
|
||||||
# ksh Gradle
|
# ksh gradlew
|
||||||
#
|
#
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
# requires all of these POSIX shell features:
|
# requires all of these POSIX shell features:
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (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.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
@@ -86,8 +86,7 @@ done
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# 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
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -115,7 +114,6 @@ case "$( uname )" in #(
|
|||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# 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
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if "$cygwin" || "$msys" ; then
|
if "$cygwin" || "$msys" ; then
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
@@ -206,15 +203,14 @@ fi
|
|||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# 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.
|
# 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
|
# * 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.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-classpath "$CLASSPATH" \
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|||||||
Vendored
+12
-24
@@ -19,12 +19,12 @@
|
|||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem gradlew startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
setlocal EnableExtensions
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
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 Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
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 Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute gradlew
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
@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
|
:exitWithErrorLevel
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||||
|
|
||||||
: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
|
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ package eu.mhsl.minenet.minigames;
|
|||||||
import eu.mhsl.minenet.minigames.api.HttpServer;
|
import eu.mhsl.minenet.minigames.api.HttpServer;
|
||||||
import eu.mhsl.minenet.minigames.command.Commands;
|
import eu.mhsl.minenet.minigames.command.Commands;
|
||||||
import eu.mhsl.minenet.minigames.handler.Listeners;
|
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.lang.Languages;
|
||||||
import eu.mhsl.minenet.minigames.server.tasks.TablistUpdateTask;
|
import eu.mhsl.minenet.minigames.server.tasks.TablistUpdateTask;
|
||||||
|
import net.minestom.server.Auth;
|
||||||
import net.minestom.server.MinecraftServer;
|
import net.minestom.server.MinecraftServer;
|
||||||
import net.minestom.server.extras.bungee.BungeeCordProxy;
|
|
||||||
import net.minestom.server.extras.lan.OpenToLAN;
|
import net.minestom.server.extras.lan.OpenToLAN;
|
||||||
import net.minestom.server.extras.velocity.VelocityProxy;
|
|
||||||
import net.minestom.server.timer.TaskSchedule;
|
import net.minestom.server.timer.TaskSchedule;
|
||||||
import org.spongepowered.configurate.ConfigurateException;
|
import org.spongepowered.configurate.ConfigurateException;
|
||||||
import org.spongepowered.configurate.ConfigurationNode;
|
import org.spongepowered.configurate.ConfigurationNode;
|
||||||
@@ -25,7 +25,7 @@ public class Main {
|
|||||||
public static ConfigurationNode globalConfig;
|
public static ConfigurationNode globalConfig;
|
||||||
|
|
||||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||||
public static void main(String[] args) throws ConfigurateException {
|
static void main(String[] args) throws ConfigurateException {
|
||||||
//noinspection ResultOfMethodCallIgnored
|
//noinspection ResultOfMethodCallIgnored
|
||||||
Resource.values(); // This initializes and preloads the enum and extracts the resources
|
Resource.values(); // This initializes and preloads the enum and extracts the resources
|
||||||
Languages.getInstance(); //Preload languages into the jvm
|
Languages.getInstance(); //Preload languages into the jvm
|
||||||
@@ -38,14 +38,31 @@ public class Main {
|
|||||||
|
|
||||||
ConfigurationNode serverConfig = globalConfig.node("server");
|
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...");
|
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();
|
// 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.setBrandName("mhsl.eu - minenet - credits to minestom");
|
||||||
MinecraftServer.setCompressionThreshold(serverConfig.node("compression-threshold").getInt(0));
|
MinecraftServer.setCompressionThreshold(serverConfig.node("compression-threshold").getInt(0));
|
||||||
System.setProperty("minestom.chunk-view-distance", String.valueOf(serverConfig.node("view-distance").getInt()));
|
|
||||||
|
|
||||||
Commands.values();
|
Commands.values();
|
||||||
Listeners.values();
|
Listeners.values();
|
||||||
@@ -57,12 +74,6 @@ public class Main {
|
|||||||
|
|
||||||
if(serverConfig.node("open-to-lan").getBoolean()) OpenToLAN.open();
|
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));
|
server.start("0.0.0.0", serverConfig.node("port").getInt(25565));
|
||||||
System.gc();
|
System.gc();
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public class SetMemorialCommand extends PrivilegedCommand {
|
|||||||
.getTournament()
|
.getTournament()
|
||||||
.setMemorialConfiguration(
|
.setMemorialConfiguration(
|
||||||
new MemorialConfiguration(
|
new MemorialConfiguration(
|
||||||
Material.fromNamespaceId(context.get(materialArgument)),
|
Material.fromKey(context.get(materialArgument)),
|
||||||
context.get(titleArgument),
|
context.get(titleArgument),
|
||||||
context.get(loreArgument)
|
context.get(loreArgument)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public class SetRewardCommand extends PrivilegedCommand {
|
|||||||
.getTournament()
|
.getTournament()
|
||||||
.setRewardConfiguration(
|
.setRewardConfiguration(
|
||||||
new RewardConfiguration(
|
new RewardConfiguration(
|
||||||
Material.fromNamespaceId(context.get(materialArgument)),
|
Material.fromKey(context.get(materialArgument)),
|
||||||
Arrays.stream(context.get(amountsArgument)).map(Integer::valueOf).collect(Collectors.toList())
|
Arrays.stream(context.get(amountsArgument)).map(Integer::valueOf).collect(Collectors.toList())
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eu.mhsl.minenet.minigames.handler;
|
|||||||
|
|
||||||
import eu.mhsl.minenet.minigames.handler.global.AddEntityToInstanceEventListener;
|
import eu.mhsl.minenet.minigames.handler.global.AddEntityToInstanceEventListener;
|
||||||
import eu.mhsl.minenet.minigames.handler.global.ChatFormatHandler;
|
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.PlayerLeaveHandler;
|
||||||
import eu.mhsl.minenet.minigames.handler.global.PlayerLoginHandler;
|
import eu.mhsl.minenet.minigames.handler.global.PlayerLoginHandler;
|
||||||
import net.minestom.server.MinecraftServer;
|
import net.minestom.server.MinecraftServer;
|
||||||
@@ -11,6 +12,7 @@ public enum Listeners {
|
|||||||
SPAWN(new AddEntityToInstanceEventListener()),
|
SPAWN(new AddEntityToInstanceEventListener()),
|
||||||
LOGIN(new PlayerLoginHandler()),
|
LOGIN(new PlayerLoginHandler()),
|
||||||
LEAVE(new PlayerLeaveHandler()),
|
LEAVE(new PlayerLeaveHandler()),
|
||||||
|
FREEZE(new PlayerFreezeHandler()),
|
||||||
CHAT(new ChatFormatHandler());
|
CHAT(new ChatFormatHandler());
|
||||||
|
|
||||||
Listeners(EventListener<?> event) {
|
Listeners(EventListener<?> event) {
|
||||||
|
|||||||
@@ -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<PlayerTickEvent> {
|
||||||
|
private static final int TICKS_REQUIRED_TO_FREEZE = 140;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Class<PlayerTickEvent> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +1,81 @@
|
|||||||
package eu.mhsl.minenet.minigames.instance;
|
package eu.mhsl.minenet.minigames.instance;
|
||||||
|
|
||||||
|
import net.kyori.adventure.key.Key;
|
||||||
import net.minestom.server.MinecraftServer;
|
import net.minestom.server.MinecraftServer;
|
||||||
import net.minestom.server.registry.DynamicRegistry;
|
import net.minestom.server.registry.RegistryKey;
|
||||||
import net.minestom.server.utils.NamespaceID;
|
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.DimensionType;
|
||||||
|
import net.minestom.server.world.attribute.EnvironmentAttribute;
|
||||||
|
import net.minestom.server.world.clock.WorldClock;
|
||||||
|
import net.minestom.server.world.timeline.Timeline;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prebuilt dimensions
|
* Prebuilt dimensions
|
||||||
*/
|
*/
|
||||||
public enum Dimension {
|
public enum Dimension {
|
||||||
OVERWORLD(
|
OVERWORLD(
|
||||||
NamespaceID.from("minenet:fullbright_overworld"),
|
Key.key("minenet:fullbright_overworld"),
|
||||||
DimensionType
|
DimensionType
|
||||||
.builder()
|
.builder()
|
||||||
.ambientLight(2.0f)
|
// 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()
|
.build()
|
||||||
),
|
),
|
||||||
|
|
||||||
NETHER(
|
NETHER(
|
||||||
NamespaceID.from("minenet:fullbright_nether"),
|
Key.key("minenet:fullbright_nether"),
|
||||||
DimensionType
|
DimensionType
|
||||||
.builder()
|
.builder()
|
||||||
.ambientLight(2.0f)
|
// Fullbright: seit MC 26.1 laeuft die Grundhelligkeit ueber das Environment-Attribut
|
||||||
.effects("minecraft:the_nether")
|
// 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()
|
.build()
|
||||||
),
|
),
|
||||||
|
|
||||||
THE_END(
|
THE_END(
|
||||||
NamespaceID.from("minenet:fullbright_end"),
|
Key.key("minenet:fullbright_end"),
|
||||||
DimensionType
|
DimensionType
|
||||||
.builder()
|
.builder()
|
||||||
.ambientLight(2.0f)
|
// Fullbright: seit MC 26.1 laeuft die Grundhelligkeit ueber das Environment-Attribut
|
||||||
.effects("minecraft:the_end")
|
// 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()
|
.build()
|
||||||
);
|
);
|
||||||
|
|
||||||
public final DimensionType DIMENSION;
|
public final DimensionType DIMENSION;
|
||||||
public final NamespaceID namespaceID;
|
public final Key namespaceID;
|
||||||
public final DynamicRegistry.Key<DimensionType> key;
|
public final RegistryKey<DimensionType> key;
|
||||||
|
|
||||||
Dimension(NamespaceID namespaceID, DimensionType dimType) {
|
Dimension(Key namespaceID, DimensionType dimType) {
|
||||||
this.DIMENSION = dimType;
|
this.DIMENSION = dimType;
|
||||||
this.namespaceID = namespaceID;
|
this.namespaceID = namespaceID;
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
package eu.mhsl.minenet.minigames.instance;
|
package eu.mhsl.minenet.minigames.instance;
|
||||||
|
|
||||||
import eu.mhsl.minenet.minigames.message.component.TranslatedComponent;
|
import eu.mhsl.minenet.minigames.message.component.TranslatedComponent;
|
||||||
|
import net.kyori.adventure.key.Key;
|
||||||
import net.minestom.server.MinecraftServer;
|
import net.minestom.server.MinecraftServer;
|
||||||
import net.minestom.server.entity.Player;
|
import net.minestom.server.entity.Player;
|
||||||
import net.minestom.server.event.instance.AddEntityToInstanceEvent;
|
import net.minestom.server.event.instance.AddEntityToInstanceEvent;
|
||||||
import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent;
|
import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent;
|
||||||
import net.minestom.server.instance.Instance;
|
import net.minestom.server.instance.Instance;
|
||||||
import net.minestom.server.instance.InstanceContainer;
|
import net.minestom.server.instance.InstanceContainer;
|
||||||
import net.minestom.server.registry.DynamicRegistry;
|
import net.minestom.server.registry.RegistryKey;
|
||||||
import net.minestom.server.timer.TaskSchedule;
|
import net.minestom.server.timer.TaskSchedule;
|
||||||
import net.minestom.server.world.DimensionType;
|
import net.minestom.server.world.DimensionType;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public class MineNetInstance extends InstanceContainer {
|
public class MineNetInstance extends InstanceContainer {
|
||||||
public MineNetInstance(DynamicRegistry.Key<DimensionType> type) {
|
public MineNetInstance(RegistryKey<DimensionType> type) {
|
||||||
super(UUID.randomUUID(), 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<DimensionType> type) {
|
||||||
|
super(uuid, type, Key.key("minenet", "instance-" + uuid));
|
||||||
MinecraftServer.getInstanceManager().registerInstance(this);
|
MinecraftServer.getInstanceManager().registerInstance(this);
|
||||||
|
|
||||||
this.eventNode()
|
this.eventNode()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import net.minestom.server.event.item.ItemDropEvent;
|
|||||||
import net.minestom.server.event.player.PlayerBlockBreakEvent;
|
import net.minestom.server.event.player.PlayerBlockBreakEvent;
|
||||||
import net.minestom.server.event.player.PlayerBlockPlaceEvent;
|
import net.minestom.server.event.player.PlayerBlockPlaceEvent;
|
||||||
import net.minestom.server.event.player.PlayerMoveEvent;
|
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.ExecutionType;
|
||||||
import net.minestom.server.timer.TaskSchedule;
|
import net.minestom.server.timer.TaskSchedule;
|
||||||
import net.minestom.server.world.DimensionType;
|
import net.minestom.server.world.DimensionType;
|
||||||
@@ -34,7 +34,7 @@ public abstract class Game extends MineNetInstance implements Spawnable {
|
|||||||
protected boolean isRunning = false;
|
protected boolean isRunning = false;
|
||||||
protected boolean isBeforeBeginning = true;
|
protected boolean isBeforeBeginning = true;
|
||||||
|
|
||||||
public Game(DynamicRegistry.Key<DimensionType> dimensionType) {
|
public Game(RegistryKey<DimensionType> dimensionType) {
|
||||||
super(dimensionType);
|
super(dimensionType);
|
||||||
|
|
||||||
MinecraftServer.getInstanceManager().registerInstance(this);
|
MinecraftServer.getInstanceManager().registerInstance(this);
|
||||||
|
|||||||
@@ -5,15 +5,19 @@ import eu.mhsl.minenet.minigames.instance.game.stateless.types.acidRain.AcidRain
|
|||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.anvilRun.AnvilRunFactory;
|
import eu.mhsl.minenet.minigames.instance.game.stateless.types.anvilRun.AnvilRunFactory;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsFactory;
|
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.bedwars.BedwarsFactory;
|
||||||
|
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.blockBreakRace.BlockBreakRaceFactory;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.boatRace.BoatRaceFactory;
|
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.bowSpleef.BowSpleefFactory;
|
||||||
|
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.deathcube.DeathcubeFactory;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.elytraRace.ElytraRaceFactory;
|
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.fastbridge.FastbridgeFactory;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.highGround.HighGroundFactory;
|
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.jumpDive.JumpDiveFactory;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.minerun.MinerunFactory;
|
import eu.mhsl.minenet.minigames.instance.game.stateless.types.minerun.MinerunFactory;
|
||||||
|
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.spaceSnake.SpaceSnakeFactory;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.types.spleef.SpleefFactory;
|
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.stickfight.StickFightFactory;
|
||||||
@@ -31,7 +35,7 @@ public enum GameList {
|
|||||||
STICKFIGHT(new StickFightFactory(), GameType.PROTOTYPE),
|
STICKFIGHT(new StickFightFactory(), GameType.PROTOTYPE),
|
||||||
TOWERDEFENSE(new TowerdefenseFactory(), GameType.PROTOTYPE),
|
TOWERDEFENSE(new TowerdefenseFactory(), GameType.PROTOTYPE),
|
||||||
BEDWARS(new BedwarsFactory(), GameType.PROTOTYPE),
|
BEDWARS(new BedwarsFactory(), GameType.PROTOTYPE),
|
||||||
BACKROOMS(new BackroomsFactory(), GameType.PROTOTYPE),
|
BACKROOMS(new BackroomsFactory(), GameType.PVE),
|
||||||
BOWSPLEEF(new BowSpleefFactory(), GameType.PROTOTYPE),
|
BOWSPLEEF(new BowSpleefFactory(), GameType.PROTOTYPE),
|
||||||
TETRIS(new TetrisFactory(), GameType.OTHER),
|
TETRIS(new TetrisFactory(), GameType.OTHER),
|
||||||
TNTRUN(new TntRunFactory(), GameType.OTHER),
|
TNTRUN(new TntRunFactory(), GameType.OTHER),
|
||||||
@@ -46,7 +50,11 @@ public enum GameList {
|
|||||||
FASTBRIDGE(new FastbridgeFactory(), GameType.OTHER),
|
FASTBRIDGE(new FastbridgeFactory(), GameType.OTHER),
|
||||||
BLOCKBREAKRACE(new BlockBreakRaceFactory(), GameType.OTHER),
|
BLOCKBREAKRACE(new BlockBreakRaceFactory(), GameType.OTHER),
|
||||||
SPACESNAKE(new SpaceSnakeFactory(), GameType.PVP),
|
SPACESNAKE(new SpaceSnakeFactory(), GameType.PVP),
|
||||||
BOATRACE(new BoatRaceFactory(), GameType.OTHER);
|
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);
|
||||||
|
|
||||||
private final GameFactory factory;
|
private final GameFactory factory;
|
||||||
private final GameType type;
|
private final GameType type;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import eu.mhsl.minenet.minigames.score.Score;
|
|||||||
import net.kyori.adventure.audience.Audience;
|
import net.kyori.adventure.audience.Audience;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
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.ExecutionType;
|
||||||
import net.minestom.server.timer.Task;
|
import net.minestom.server.timer.Task;
|
||||||
import net.minestom.server.timer.TaskSchedule;
|
import net.minestom.server.timer.TaskSchedule;
|
||||||
@@ -26,7 +26,7 @@ public class StatelessGame extends Game {
|
|||||||
private int timeLimit = 0;
|
private int timeLimit = 0;
|
||||||
private int timePlayed = 0;
|
private int timePlayed = 0;
|
||||||
|
|
||||||
public StatelessGame(DynamicRegistry.Key<DimensionType> dimensionType, String gameName, Score score) {
|
public StatelessGame(RegistryKey<DimensionType> dimensionType, String gameName, Score score) {
|
||||||
super(dimensionType);
|
super(dimensionType);
|
||||||
this.score = score;
|
this.score = score;
|
||||||
this.name = gameName;
|
this.name = gameName;
|
||||||
|
|||||||
+4
-4
@@ -14,9 +14,9 @@ import net.minestom.server.MinecraftServer;
|
|||||||
import net.minestom.server.entity.Player;
|
import net.minestom.server.entity.Player;
|
||||||
import net.minestom.server.event.instance.AddEntityToInstanceEvent;
|
import net.minestom.server.event.instance.AddEntityToInstanceEvent;
|
||||||
import net.minestom.server.event.instance.RemoveEntityFromInstanceEvent;
|
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.InventoryType;
|
||||||
import net.minestom.server.inventory.click.ClickType;
|
import net.minestom.server.inventory.click.Click;
|
||||||
import net.minestom.server.inventory.condition.InventoryConditionResult;
|
|
||||||
import net.minestom.server.item.ItemStack;
|
import net.minestom.server.item.ItemStack;
|
||||||
import net.minestom.server.item.Material;
|
import net.minestom.server.item.Material;
|
||||||
import net.minestom.server.sound.SoundEvent;
|
import net.minestom.server.sound.SoundEvent;
|
||||||
@@ -106,8 +106,8 @@ public class GameConfigurationInventory extends InteractableInventory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onClick(Player player, int slot, ClickType clickType, InventoryConditionResult inventoryConditionResult) {
|
protected void onClick(Player player, int slot, Click click, InventoryPreClickEvent event) {
|
||||||
inventoryConditionResult.setCancel(true);
|
event.setCancelled(true);
|
||||||
|
|
||||||
if(!this.map.containsKey(slot)) return;
|
if(!this.map.containsKey(slot)) return;
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,7 @@
|
|||||||
package eu.mhsl.minenet.minigames.instance.game.stateless.types.acidRain;
|
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.Dimension;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
|
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
|
||||||
import eu.mhsl.minenet.minigames.score.LastWinsScore;
|
import eu.mhsl.minenet.minigames.score.LastWinsScore;
|
||||||
@@ -28,9 +29,8 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
|
|
||||||
public class AcidRain extends StatelessGame {
|
public class AcidRain extends StatelessGame {
|
||||||
final JNoise noise = JNoise.newBuilder()
|
final JNoise noise = JNoise.newBuilder()
|
||||||
.fastSimplex()
|
.fastSimplex(FastSimplexNoiseGenerator.newBuilder().setSeed(this.rnd.nextLong()).build())
|
||||||
.setSeed(this.rnd.nextLong())
|
.scale(0.09)
|
||||||
.setFrequency(0.09)
|
|
||||||
.build();
|
.build();
|
||||||
final private int radius = 20;
|
final private int radius = 20;
|
||||||
final private int roofHeight = 55;
|
final private int roofHeight = 55;
|
||||||
@@ -105,7 +105,7 @@ public class AcidRain extends StatelessGame {
|
|||||||
for(int z = -radius; z <= radius; z++) {
|
for(int z = -radius; z <= radius; z++) {
|
||||||
batch.setBlock(x, this.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(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > radius) continue;
|
||||||
if(this.noise.getNoise(x + offset, z + offset) > 0.4) continue;
|
if(this.noise.evaluateNoise(x + offset, z + offset) > 0.4) continue;
|
||||||
|
|
||||||
batch.setBlock(x, this.roofHeight, z, Block.OAK_PLANKS);
|
batch.setBlock(x, this.roofHeight, z, Block.OAK_PLANKS);
|
||||||
}
|
}
|
||||||
|
|||||||
-20
@@ -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();
|
|
||||||
this.setGenerator(unit -> generator.generateRoom(unit, 50));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onBlockBreak(@NotNull PlayerBlockBreakEvent playerBlockBreakEvent) {
|
|
||||||
playerBlockBreakEvent.setCancelled(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+33
-2
@@ -1,21 +1,52 @@
|
|||||||
package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms;
|
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.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.GameFactory;
|
||||||
import eu.mhsl.minenet.minigames.instance.game.stateless.config.Option;
|
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.instance.room.Room;
|
||||||
import eu.mhsl.minenet.minigames.message.component.TranslatedComponent;
|
import eu.mhsl.minenet.minigames.message.component.TranslatedComponent;
|
||||||
|
import net.minestom.server.item.Material;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class BackroomsFactory implements GameFactory {
|
public class BackroomsFactory implements GameFactory {
|
||||||
@Override
|
@Override
|
||||||
public TranslatedComponent name() {
|
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
|
@Override
|
||||||
public Game manufacture(Room parent, Map<String, Option<?>> configuration) throws Exception {
|
public Game manufacture(Room parent, Map<String, Option<?>> 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+663
@@ -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<Supplier<LevelDefinition>> LEVEL_POOL = List.of(
|
||||||
|
LevelHabitableZone::new,
|
||||||
|
LevelPipeDreams::new,
|
||||||
|
LevelPoolrooms::new,
|
||||||
|
LevelLightsOut::new,
|
||||||
|
LevelFun::new
|
||||||
|
);
|
||||||
|
|
||||||
|
private final List<LevelRuntime> levels = new ArrayList<>();
|
||||||
|
private final Map<UUID, BackroomsPlayer> playerStates = new HashMap<>();
|
||||||
|
private final Map<UUID, Sidebar> 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<Integer> 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<LevelDefinition> 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<LevelDefinition> buildSequence(int totalLevels) {
|
||||||
|
List<LevelDefinition> sequence = new ArrayList<>();
|
||||||
|
sequence.add(new Level0Lobby());
|
||||||
|
|
||||||
|
List<Supplier<LevelDefinition>> 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<LevelRuntime> getLevels() {
|
||||||
|
return this.levels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Random rnd() {
|
||||||
|
return this.rnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BackroomsPlayer playerState(Player player) {
|
||||||
|
return this.playerStates.get(player.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Player> 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<Void> 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<Player> 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<Player> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
-87
@@ -1,87 +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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.generateWalls(modifier, start.blockX(), yPos + 1, start.blockZ());
|
|
||||||
this.generateWalls(modifier, start.blockX() + 8, yPos + 1, start.blockZ());
|
|
||||||
this.generateWalls(modifier, start.blockX(), yPos + 1, start.blockZ() + 8);
|
|
||||||
this.generateWalls(modifier, start.blockX() + 8, yPos + 1, start.blockZ() + 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void generateWalls(UnitModifier modifier, int xPos, int yPos, int zPos) {
|
|
||||||
this.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++) {
|
|
||||||
this.generatePillar(modifier, x, yPos, zPos, Block.SMOOTH_SANDSTONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(wall2) {
|
|
||||||
for(int z = zPos; z < zPos + 8; z++) {
|
|
||||||
this.generatePillar(modifier, xPos, yPos, z, Block.SMOOTH_SANDSTONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(door1 && wall1) {
|
|
||||||
this.generatePillar(modifier, xPos + door1pos, yPos, zPos, Block.AIR);
|
|
||||||
this.generatePillar(modifier, xPos + door1pos + 1, yPos, zPos, Block.AIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(door2 && wall2) {
|
|
||||||
this.generatePillar(modifier, xPos, yPos, zPos + door2pos, Block.AIR);
|
|
||||||
this.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+96
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+58
@@ -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<Player> p, int points) {
|
||||||
|
this.getScores().add(this.escapedCount, p);
|
||||||
|
this.escapedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tod bzw. verbliebene Spieler bei Spielende */
|
||||||
|
@Override
|
||||||
|
protected void insertResultImplementation(Set<Player> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+96
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+70
@@ -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()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -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<SoundEvent> oneShots,
|
||||||
|
double oneShotChancePerSecond
|
||||||
|
) {
|
||||||
|
}
|
||||||
+81
@@ -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<SoundEvent> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+72
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+335
@@ -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<LevelRuntime> levels;
|
||||||
|
/** O(1)-Lookups pro Level (statt Listen-Scans pro Blocksaeule) */
|
||||||
|
private final List<Set<Long>> lightKeys = new ArrayList<>();
|
||||||
|
private final List<Set<Long>> chestKeys = new ArrayList<>();
|
||||||
|
|
||||||
|
/** wiederverwendetes Random pro Generator-Thread (Seed pro Saeule gesetzt) */
|
||||||
|
private static final ThreadLocal<Random> COLUMN_RANDOM = ThreadLocal.withInitial(Random::new);
|
||||||
|
|
||||||
|
public BackroomsChunkGenerator(List<LevelRuntime> levels) {
|
||||||
|
this.levels = levels;
|
||||||
|
for(LevelRuntime level : levels) {
|
||||||
|
Set<Long> lights = new HashSet<>();
|
||||||
|
level.getLayout().getLightCells().forEach(cell -> lights.add(cellKey(cell[0], cell[1])));
|
||||||
|
this.lightKeys.add(lights);
|
||||||
|
|
||||||
|
Set<Long> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+300
@@ -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<int[]> 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<int[]> 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<int[]> 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<int[]> 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<int[]> 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<int[]> 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<int[]> 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<int[]> queue = new ArrayDeque<>();
|
||||||
|
java.util.Map<Long, int[]> 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<int[]> 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<int[]> sampleSpread(List<int[]> candidates, Random rnd, int count, double minDistance) {
|
||||||
|
List<int[]> pool = new ArrayList<>(candidates);
|
||||||
|
Collections.shuffle(pool, rnd);
|
||||||
|
List<int[]> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+106
@@ -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<String> ITEM_TAG = Tag.String("backroomsItem");
|
||||||
|
public static final Tag<Integer> 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<ItemStack> 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<ItemStack> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
+229
@@ -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<int[]> 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<int[]> 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<int[]> 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<int[]> 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<int[]> 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<int[]> valid = new ArrayList<>();
|
||||||
|
List<int[]> 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<int[]> 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<int[]> 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<int[]> 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};
|
||||||
|
}
|
||||||
|
}
|
||||||
+187
@@ -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<int[]> spawnCells = new ArrayList<>();
|
||||||
|
private final List<int[]> chestCells = new ArrayList<>();
|
||||||
|
private final List<int[]> lightCells = new ArrayList<>();
|
||||||
|
/** frei nutzbare Level-Marker (z. B. Dampf-Ventile in den Poolrooms) */
|
||||||
|
private final List<int[]> 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<int[]> getSpawnCells() {
|
||||||
|
return this.spawnCells;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<int[]> getChestCells() {
|
||||||
|
return this.chestCells;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<int[]> getLightCells() {
|
||||||
|
return this.lightCells;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<int[]> 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
+56
@@ -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<Block> floor,
|
||||||
|
List<Block> wall,
|
||||||
|
List<Block> ceiling,
|
||||||
|
Block light,
|
||||||
|
Block pillar,
|
||||||
|
int roomHeight,
|
||||||
|
Block wallBase,
|
||||||
|
boolean checkerFloor,
|
||||||
|
List<Block> floorDeco,
|
||||||
|
double floorDecoChance,
|
||||||
|
List<Block> 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
@@ -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<BackroomsMonster> 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<BackroomsMonster> getMonsters() {
|
||||||
|
return this.monsters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBlackoutActive() {
|
||||||
|
return this.blackoutActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlackoutActive(boolean blackoutActive) {
|
||||||
|
this.blackoutActive = blackoutActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public List<Player> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -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
|
||||||
|
}
|
||||||
+113
@@ -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<int[]> 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<int[]> far(LevelLayout layout, Random rnd, int count, BiPredicate<Integer, Integer> filter) {
|
||||||
|
for(int minDistance = MONSTER_MIN_SPAWN_DISTANCE; ; minDistance -= 2) {
|
||||||
|
List<int[]> 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<int[]> 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]));
|
||||||
|
}
|
||||||
|
}
|
||||||
+151
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+368
@@ -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<int[]> challengeRooms = new ArrayList<>();
|
||||||
|
private final Map<UUID, Integer> challengeTimers = new HashMap<>();
|
||||||
|
private final Map<UUID, Smiler> 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<int[]> 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<int[]> 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+109
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+146
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+110
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+206
@@ -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<int[]> 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<int[]> 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<int[]> 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+182
@@ -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<Player> 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+199
@@ -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<Point, Integer> chestLevels = new HashMap<>();
|
||||||
|
private final Map<Point, Integer> keyChestLevels = new HashMap<>();
|
||||||
|
private final Map<Point, Set<UUID>> keyChestLooters = new HashMap<>();
|
||||||
|
private final Map<Point, Set<UUID>> 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<UUID> 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<UUID> 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<ItemStack> 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<ItemStack> rollFor(Player player, int levelIndex) {
|
||||||
|
List<ItemStack> 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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+43
@@ -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<ItemStack> item, int weight) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private final List<Entry> entries = new ArrayList<>();
|
||||||
|
private int totalWeight = 0;
|
||||||
|
|
||||||
|
public LootTable add(Supplier<ItemStack> item, int weight) {
|
||||||
|
this.entries.add(new Entry(item, weight));
|
||||||
|
this.totalWeight += weight;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ItemStack> roll(Random rnd) {
|
||||||
|
List<ItemStack> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+207
@@ -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<CompletableFuture<Chunk>> 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<Player> 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())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+169
@@ -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<Entity> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -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<Player> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+132
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+101
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -26,6 +26,7 @@ import net.minestom.server.item.ItemStack;
|
|||||||
import net.minestom.server.item.Material;
|
import net.minestom.server.item.Material;
|
||||||
import net.minestom.server.timer.ExecutionType;
|
import net.minestom.server.timer.ExecutionType;
|
||||||
import net.minestom.server.timer.TaskSchedule;
|
import net.minestom.server.timer.TaskSchedule;
|
||||||
|
import net.minestom.server.world.DimensionType;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -41,7 +42,7 @@ public class Bedwars extends StatelessGame {
|
|||||||
|
|
||||||
public Bedwars() throws IOException {
|
public Bedwars() throws IOException {
|
||||||
super(Dimension.OVERWORLD.key, "Bedwars", new LastWinsScore());
|
super(Dimension.OVERWORLD.key, "Bedwars", new LastWinsScore());
|
||||||
this.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());
|
Configuration config = ConfigurationProvider.getProvider(YamlConfiguration.class).load(Resource.GAME_MAP.getPath().resolve("bedwars/test/config.yml").toFile());
|
||||||
List<String> teamNames = config.getStringList("setup.teams");
|
List<String> teamNames = config.getStringList("setup.teams");
|
||||||
|
|||||||
+207
@@ -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<Player, Team> 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.isBeforeBeginning) finalAttackEvent.setCancelled(true);
|
||||||
|
finalAttackEvent.setBaseDamage(0);
|
||||||
|
((Player) finalAttackEvent.getTarget()).setHealth(20);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLoad(@NotNull CompletableFuture<Void> 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<Player> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -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<String, Option<?>> configuration) throws Exception {
|
||||||
|
return new BlockBattle(configuration.get("itemCount").getAsInt()).setParent(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -15,6 +15,7 @@ import net.minestom.server.entity.Player;
|
|||||||
import net.minestom.server.event.player.PlayerMoveEvent;
|
import net.minestom.server.event.player.PlayerMoveEvent;
|
||||||
import net.minestom.server.instance.anvil.AnvilLoader;
|
import net.minestom.server.instance.anvil.AnvilLoader;
|
||||||
import net.minestom.server.instance.block.Block;
|
import net.minestom.server.instance.block.Block;
|
||||||
|
import net.minestom.server.world.DimensionType;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@@ -23,7 +24,7 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
public class BoatRace extends StatelessGame {
|
public class BoatRace extends StatelessGame {
|
||||||
public BoatRace() {
|
public BoatRace() {
|
||||||
super(Dimension.OVERWORLD.key, "boatRace", new FirstWinsScore());
|
super(Dimension.OVERWORLD.key, "boatRace", new FirstWinsScore());
|
||||||
this.setChunkLoader(new AnvilLoader(Resource.GAME_MAP.getPath().resolve("boatRace/woodlandMansion")));
|
this.setChunkLoader(new AnvilLoader(Resource.GAME_MAP.getPath().resolve("boatRace/woodlandMansion"), DimensionType.OVERWORLD.key()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
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<Block> 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<Void> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -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<String, Option<?>> configuration) throws Exception {
|
||||||
|
return new ColorJump().setParent(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-1
@@ -24,6 +24,7 @@ import net.minestom.server.event.player.PlayerMoveEvent;
|
|||||||
import net.minestom.server.event.player.PlayerStartFlyingWithElytraEvent;
|
import net.minestom.server.event.player.PlayerStartFlyingWithElytraEvent;
|
||||||
import net.minestom.server.event.player.PlayerStopFlyingWithElytraEvent;
|
import net.minestom.server.event.player.PlayerStopFlyingWithElytraEvent;
|
||||||
import net.minestom.server.event.player.PlayerUseItemEvent;
|
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.batch.AbsoluteBlockBatch;
|
||||||
import net.minestom.server.instance.block.Block;
|
import net.minestom.server.instance.block.Block;
|
||||||
import net.minestom.server.item.ItemStack;
|
import net.minestom.server.item.ItemStack;
|
||||||
@@ -209,7 +210,16 @@ public class ElytraRace extends StatelessGame {
|
|||||||
point -> batch.setBlock(point, Block.AIR)
|
point -> batch.setBlock(point, Block.AIR)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
int minChunkX = (ringPos.blockX() - 100) >> 4;
|
||||||
|
int maxChunkX = (ringPos.blockX() + 100) >> 4;
|
||||||
|
int chunkZ = ringPos.blockZ() >> 4;
|
||||||
|
|
||||||
BatchUtil.loadAndApplyBatch(batch, this, () -> {
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +239,7 @@ public class ElytraRace extends StatelessGame {
|
|||||||
if(data.currentCheckpoint == 0) checkpointPos = this.getSpawn().add(0, 3, 0);
|
if(data.currentCheckpoint == 0) checkpointPos = this.getSpawn().add(0, 3, 0);
|
||||||
p.setVelocity(Vec.ZERO);
|
p.setVelocity(Vec.ZERO);
|
||||||
p.setFlyingWithElytra(false);
|
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.setFlying(true);
|
||||||
p.setFlyingSpeed(0);
|
p.setFlyingSpeed(0);
|
||||||
|
|||||||
+10
-1
@@ -8,6 +8,7 @@ import io.github.togar2.pvp.events.EntityKnockbackEvent;
|
|||||||
import io.github.togar2.pvp.events.FinalAttackEvent;
|
import io.github.togar2.pvp.events.FinalAttackEvent;
|
||||||
import io.github.togar2.pvp.events.PrepareAttackEvent;
|
import io.github.togar2.pvp.events.PrepareAttackEvent;
|
||||||
import io.github.togar2.pvp.feature.CombatFeatures;
|
import io.github.togar2.pvp.feature.CombatFeatures;
|
||||||
|
import io.github.togar2.pvp.feature.knockback.KnockbackSettings;
|
||||||
import net.minestom.server.coordinate.Pos;
|
import net.minestom.server.coordinate.Pos;
|
||||||
import net.minestom.server.entity.Player;
|
import net.minestom.server.entity.Player;
|
||||||
import net.minestom.server.event.instance.InstanceTickEvent;
|
import net.minestom.server.event.instance.InstanceTickEvent;
|
||||||
@@ -50,7 +51,15 @@ class HighGround extends StatelessGame {
|
|||||||
|
|
||||||
this.eventNode().addListener(
|
this.eventNode().addListener(
|
||||||
EntityKnockbackEvent.class,
|
EntityKnockbackEvent.class,
|
||||||
entityKnockbackEvent -> entityKnockbackEvent.setStrength(1.1f)
|
entityKnockbackEvent -> entityKnockbackEvent.setSettings(
|
||||||
|
KnockbackSettings.builder()
|
||||||
|
.horizontal(1.1)
|
||||||
|
.vertical(KnockbackSettings.DEFAULT.vertical())
|
||||||
|
.verticalLimit(KnockbackSettings.DEFAULT.verticalLimit())
|
||||||
|
.extraHorizontal(KnockbackSettings.DEFAULT.extraHorizontal())
|
||||||
|
.extraVertical(KnockbackSettings.DEFAULT.extraVertical())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
this.eventNode().addListener(InstanceTickEvent.class, instanceTickEvent -> {
|
this.eventNode().addListener(InstanceTickEvent.class, instanceTickEvent -> {
|
||||||
|
|||||||
+143
@@ -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<String> 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<Material> 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<Material> 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<Material> 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
@@ -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<String, Option<?>> configuration) throws Exception {
|
||||||
|
return new Pillars().setParent(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
+86
@@ -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<Player, Double> zLine = new WeakHashMap<>();
|
||||||
|
private final Map<Player, Double> 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<Void> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -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<String, Option<?>> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -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"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
@@ -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<Integer> 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+69
@@ -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<Obstacle> 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<Obstacle> 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<Point> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -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"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -125,7 +125,7 @@ public class SpaceSnake extends StatelessGame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PlayState state = this.playerStates.get(playerBlockPlaceEvent.getPlayer());
|
PlayState state = this.playerStates.get(playerBlockPlaceEvent.getPlayer());
|
||||||
state.blocks.add(playerBlockPlaceEvent.getBlockPosition().asVec().asPosition());
|
state.blocks.add(new Pos(playerBlockPlaceEvent.getBlockPosition()));
|
||||||
state.cutToLength(pos -> this.setBlock(pos, Block.AIR));
|
state.cutToLength(pos -> this.setBlock(pos, Block.AIR));
|
||||||
|
|
||||||
MinecraftServer.getSchedulerManager().scheduleNextTick(() -> this.updateInv(playerBlockPlaceEvent.getPlayer()));
|
MinecraftServer.getSchedulerManager().scheduleNextTick(() -> this.updateInv(playerBlockPlaceEvent.getPlayer()));
|
||||||
|
|||||||
+3
-2
@@ -28,7 +28,8 @@ public class StickFightFactory implements GameFactory {
|
|||||||
@Override
|
@Override
|
||||||
public ConfigManager configuration() {
|
public ConfigManager configuration() {
|
||||||
return new ConfigManager()
|
return new ConfigManager()
|
||||||
.addOption(new NumericOption("length", Material.SANDSTONE, TranslatedComponent.byId("optionCommon#length"), 7, 10, 13, 16, 19));
|
.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
|
@Override
|
||||||
@@ -40,7 +41,7 @@ public class StickFightFactory implements GameFactory {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Game manufacture(Room parent, Map<String, Option<?>> configuration) {
|
public Game manufacture(Room parent, Map<String, Option<?>> configuration) {
|
||||||
return new Stickfight(configuration.get("length").getAsInt()).setParent(parent);
|
return new Stickfight(configuration.get("length").getAsInt(), configuration.get("seconds").getAsInt()).setParent(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user