Compare commits
25
Commits
ead09520d9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbce781099 | ||
|
|
05f2fcb9da | ||
|
|
d4465063d4 | ||
|
|
c9da982285 | ||
|
|
f2b7602321 | ||
|
|
a262010c82 | ||
|
|
38bb9c6dc8 | ||
|
|
555f4024e4 | ||
|
|
7e85a6d229 | ||
|
|
40bedc3c43 | ||
|
|
de3578eafe | ||
|
|
249cd0c08a | ||
|
|
e2c574bf0b | ||
|
|
115064cafa | ||
|
|
806d76e560 | ||
|
|
137e162ea4 | ||
|
|
de6b987ffd | ||
|
|
ef1662b144 | ||
|
|
f56a3765c6 | ||
|
|
9e1edbfc7c | ||
|
|
8ba25758be | ||
|
|
b1950dd664 | ||
|
|
452f2f3ef7 | ||
|
|
0faff2bd0c | ||
|
|
8e046c8603 |
No files matched your search
@@ -0,0 +1,9 @@
|
||||
.gradle/
|
||||
build/
|
||||
target/
|
||||
*.iml
|
||||
.idea/workspace.xml
|
||||
forwarding.secret
|
||||
progress.properties
|
||||
progress.properties.tmp
|
||||
admins.txt
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="minecraft" name="Minecraft">
|
||||
<configuration>
|
||||
<autoDetectTypes>
|
||||
<platformType>MIXIN</platformType>
|
||||
<platformType>ADVENTURE</platformType>
|
||||
</autoDetectTypes>
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,48 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'application'
|
||||
id 'com.gradleup.shadow' version '9.6.1'
|
||||
}
|
||||
|
||||
group = 'eu.mhsl.minecraft'
|
||||
version = '1.0-SNAPSHOT'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'net.minestom:minestom:2026.10.05-26.2'
|
||||
runtimeOnly 'org.slf4j:slf4j-simple:2.0.20'
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain.languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass = 'eu.mhsl.minecraft.endlessjumper.EndlessJumper'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
archiveFileName = 'limbo.jar'
|
||||
mergeServiceFiles()
|
||||
}
|
||||
|
||||
// Hochladen auf den Limbo: ./gradlew uploadLimbo, danach `sudo systemctl restart limbo`.
|
||||
// Erst in eine Temp-Datei, Prüfsumme vergleichen, dann atomar umbenennen: ein abgebrochener Upload
|
||||
// lässt das laufende JAR unberührt, und der laufende Prozess behält bis zum Neustart sein altes JAR.
|
||||
tasks.register('uploadLimbo') {
|
||||
dependsOn shadowJar
|
||||
doLast {
|
||||
def jar = shadowJar.archiveFile.get().asFile
|
||||
def checksum = jar.bytes.digest('SHA-256')
|
||||
providers.exec {
|
||||
commandLine 'scp', jar.absolutePath, 'minecraft@10.20.9.3:/home/minecraft/limbo/limbo.jar.new'
|
||||
}.result.get()
|
||||
providers.exec {
|
||||
commandLine 'ssh', 'minecraft@10.20.9.3',
|
||||
"cd ~/limbo && echo '${checksum} limbo.jar.new' | sha256sum -c --quiet && mv -f limbo.jar.new limbo.jar"
|
||||
}.result.get()
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.example</groupId>
|
||||
<artifactId>EndlessJumper</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer>
|
||||
<mainClass>eu.mhsl.minecraft.endlessjumper.EndlessJumper</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spongepowered</id>
|
||||
<url>https://repo.spongepowered.org/maven</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>jitpack</id>
|
||||
<url>https://jitpack.io</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<properties>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
</properties>
|
||||
</project>
|
||||
Vendored
BIN
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,249 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
+92
@@ -0,0 +1,92 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -1,59 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.example</groupId>
|
||||
<artifactId>EndlessJumper</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spongepowered</id>
|
||||
<url>https://repo.spongepowered.org/maven</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>jitpack</id>
|
||||
<url>https://jitpack.io</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.minestom</groupId>
|
||||
<artifactId>minestom-snapshots</artifactId>
|
||||
<version>d707b0674f</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>eu.mhsl.minecraft.endlessjumper.EndlessJumper</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1 @@
|
||||
rootProject.name = 'EndlessJumper'
|
||||
@@ -0,0 +1,32 @@
|
||||
package eu.mhsl.minecraft.endlessjumper;
|
||||
|
||||
import net.minestom.server.entity.Player;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Admins aus admins.txt im Arbeitsverzeichnis: eine UUID pro Zeile, „#“ leitet einen Kommentar ein.
|
||||
* Der Limbo kennt kein LuckPerms, die Datei wird aus dessen Admin-Gruppen erzeugt.
|
||||
*/
|
||||
public class Admins {
|
||||
private static final Path FILE = Path.of("admins.txt");
|
||||
private static Set<UUID> admins = Set.of();
|
||||
|
||||
public static void load() throws IOException {
|
||||
if(!Files.exists(FILE)) return;
|
||||
admins = Files.readAllLines(FILE).stream()
|
||||
.map(line -> line.replaceFirst("#.*", "").trim())
|
||||
.filter(line -> !line.isEmpty())
|
||||
.map(UUID::fromString)
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
public static boolean is(Player player) {
|
||||
return admins.contains(player.getUuid());
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package eu.mhsl.minecraft.endlessjumper;
|
||||
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
|
||||
public class BlockMath {
|
||||
public static Pos roundToBlock(Pos p) {
|
||||
return new Pos(p.blockX(), p.blockY(), p.blockZ());
|
||||
}
|
||||
}
|
||||
@@ -1,88 +1,153 @@
|
||||
package eu.mhsl.minecraft.endlessjumper;
|
||||
|
||||
import eu.mhsl.minecraft.endlessjumper.generator.JumpGenerator;
|
||||
import net.minestom.server.Auth;
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.command.builder.Command;
|
||||
import net.minestom.server.command.builder.arguments.ArgumentType;
|
||||
import net.minestom.server.command.builder.arguments.ArgumentWord;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.entity.PlayerSkin;
|
||||
import net.minestom.server.event.GlobalEventHandler;
|
||||
import net.minestom.server.event.item.ItemDropEvent;
|
||||
import net.minestom.server.event.item.PickupItemEvent;
|
||||
import net.minestom.server.event.player.AsyncPlayerConfigurationEvent;
|
||||
import net.minestom.server.event.player.PlayerBlockBreakEvent;
|
||||
import net.minestom.server.event.player.PlayerBlockPlaceEvent;
|
||||
import net.minestom.server.event.player.PlayerChatEvent;
|
||||
import net.minestom.server.event.player.PlayerDisconnectEvent;
|
||||
import net.minestom.server.event.player.PlayerMoveEvent;
|
||||
import net.minestom.server.extras.velocity.VelocityProxy;
|
||||
import net.minestom.server.event.player.PlayerSpawnEvent;
|
||||
import net.minestom.server.event.player.PlayerSwapItemEvent;
|
||||
import net.minestom.server.event.player.PlayerUseItemEvent;
|
||||
import net.minestom.server.instance.Instance;
|
||||
import net.minestom.server.instance.InstanceContainer;
|
||||
import net.minestom.server.instance.InstanceManager;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import net.minestom.server.timer.TaskSchedule;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class EndlessJumper {
|
||||
public static void main(String[] args) {
|
||||
MinecraftServer server = MinecraftServer.init();
|
||||
InstanceManager instanceManager = MinecraftServer.getInstanceManager();
|
||||
private static final Pos START = new Pos(0, 100, 0);
|
||||
private static final Pos SPAWN = START.add(0.5, 1, 0.5);
|
||||
private static final Map<Player, JumpGenerator> games = new ConcurrentHashMap<>();
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
MinecraftServer server = MinecraftServer.init(new Auth.Velocity(readForwardingSecret()));
|
||||
Progress.load();
|
||||
Admins.load();
|
||||
GlobalEventHandler eventHandler = MinecraftServer.getGlobalEventHandler();
|
||||
PlayerSkin skin = PlayerSkin.fromUsername("Notch");
|
||||
|
||||
HashMap<Player, JumpGenerator> games = new HashMap<>();
|
||||
|
||||
eventHandler.addListener(AsyncPlayerConfigurationEvent.class, event -> {
|
||||
final Player p = event.getPlayer();
|
||||
p.setRespawnPoint(new Pos(0.5,101.5,0.5));
|
||||
|
||||
final InstanceContainer world = instanceManager.createInstanceContainer();
|
||||
world.setGenerator(new TerrainGenerator());
|
||||
|
||||
Pos startingAt = new Pos(0,100,0);
|
||||
world.setBlock(startingAt, Block.DIAMOND_BLOCK);
|
||||
|
||||
world.setTimeRate(0);
|
||||
world.setTime(18000);
|
||||
|
||||
p.setRespawnPoint(SPAWN);
|
||||
p.setSkin(skin);
|
||||
event.setSpawningInstance(createWorld());
|
||||
});
|
||||
|
||||
event.setSpawningInstance(world);
|
||||
|
||||
MinecraftServer.getSchedulerManager().scheduleTask(
|
||||
() -> games.put(p, new JumpGenerator(startingAt, p, world)),
|
||||
TaskSchedule.seconds(3),
|
||||
TaskSchedule.stop()
|
||||
);
|
||||
eventHandler.addListener(PlayerSpawnEvent.class, event -> {
|
||||
if(!event.isFirstSpawn()) return;
|
||||
Player p = event.getPlayer();
|
||||
p.setCanPickupItem(true);
|
||||
ScoreTab.join(p);
|
||||
Jokers.clear(p);
|
||||
// sobald der Spieler in der Welt steht, geht es los
|
||||
games.put(p, new JumpGenerator(START, Progress.checkpoint(p.getUuid()), p, p.getInstance()));
|
||||
});
|
||||
|
||||
eventHandler.addListener(PlayerMoveEvent.class, event -> {
|
||||
Player p = event.getPlayer();
|
||||
|
||||
if(!games.containsKey(p)) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
JumpGenerator generator = games.get(p);
|
||||
|
||||
//reset when falling down
|
||||
if(p.getPosition().y() < generator.getNextLanding().y()-5 && p.getPosition().y() < generator.getCheckpoint().y()) generator.reset();
|
||||
|
||||
Pos p1 = BlockMath.roundToBlock(event.getNewPosition().sub(0.0,1.0,0.0));
|
||||
Pos p2 = BlockMath.roundToBlock(event.getNewPosition().sub(0.0, 2.0, 0.0));
|
||||
|
||||
|
||||
for(int i = 0; i <= generator.getPreview(); i++) {
|
||||
Pos target = generator.getBlocks().get(generator.getBlocks().size()-i-1);
|
||||
if(p1.sameBlock(target) || p2.sameBlock(target)) generator.generateNext();
|
||||
}
|
||||
|
||||
|
||||
//if(p.getPosition().z() > target.z()+2) generator.highlight();
|
||||
JumpGenerator generator = games.get(event.getPlayer());
|
||||
if(generator == null) event.setCancelled(true);
|
||||
else generator.onMove(event.getNewPosition());
|
||||
});
|
||||
|
||||
eventHandler.addListener(PlayerBlockBreakEvent.class, event -> event.setCancelled(true));
|
||||
eventHandler.addListener(PlayerBlockPlaceEvent.class, event -> event.setCancelled(true));
|
||||
eventHandler.addListener(ItemDropEvent.class, event -> event.setCancelled(true));
|
||||
eventHandler.addListener(PlayerSwapItemEvent.class, event -> event.setCancelled(true));
|
||||
// den Chat verteilt Velocity (chat-bridge) an das ganze Netzwerk, hier würde er doppelt stehen
|
||||
eventHandler.addListener(PlayerChatEvent.class, event -> event.setCancelled(true));
|
||||
|
||||
eventHandler.addListener(PlayerDisconnectEvent.class, event -> games.remove(event.getPlayer()));
|
||||
// der Client meldet Rechtsklicks auch auf Blöcke als Item-Nutzung, solange der Block sie nicht
|
||||
// selbst verarbeitet (auf der Strecke nie) – ein Event genügt, Joker werden nicht doppelt verbraucht
|
||||
eventHandler.addListener(PlayerUseItemEvent.class, event -> {
|
||||
JumpGenerator generator = games.get(event.getPlayer());
|
||||
Jokers joker = Jokers.of(event.getItemStack());
|
||||
if(generator != null && joker != null && generator.useJoker(joker)) joker.take(event.getPlayer());
|
||||
});
|
||||
|
||||
eventHandler.addListener(PickupItemEvent.class, event -> {
|
||||
Jokers joker = Jokers.of(event.getItemStack());
|
||||
if(event.getLivingEntity() instanceof Player player && joker != null) joker.add(player);
|
||||
else event.setCancelled(true);
|
||||
});
|
||||
|
||||
eventHandler.addListener(PlayerDisconnectEvent.class, event -> {
|
||||
Player p = event.getPlayer();
|
||||
games.remove(p);
|
||||
ScoreTab.leave(p);
|
||||
// jede Partie hat eine eigene Welt: nach dem Verlassen freigeben
|
||||
releaseLater(p.getInstance());
|
||||
});
|
||||
|
||||
Command reset = new Command("reset");
|
||||
reset.setDefaultExecutor((sender, context) -> {
|
||||
if(sender instanceof Player player) resetGame(player);
|
||||
});
|
||||
MinecraftServer.getCommandManager().register(reset);
|
||||
|
||||
// nur für Admins (admins.txt): Joker zum Testen direkt geben
|
||||
Command joker = new Command("joker");
|
||||
joker.setCondition((sender, commandString) -> sender instanceof Player player && Admins.is(player));
|
||||
ArgumentWord type = ArgumentType.Word("typ").from(Jokers.keys());
|
||||
joker.addSyntax((sender, context) -> Jokers.byKey(context.get(type)).add((Player) sender), type);
|
||||
MinecraftServer.getCommandManager().register(joker);
|
||||
|
||||
VelocityProxy.enable("iph0YHK9jNWZ");
|
||||
server.start("0.0.0.0", 25501);
|
||||
}
|
||||
|
||||
private static InstanceContainer createWorld() {
|
||||
InstanceContainer world = MinecraftServer.getInstanceManager().createInstanceContainer();
|
||||
world.setBlock(START, Block.DIAMOND_BLOCK);
|
||||
// Tageszeit steuert der jeweilige Abschnitt
|
||||
world.defaultClock().pause();
|
||||
return world;
|
||||
}
|
||||
|
||||
private static void releaseLater(Instance world) {
|
||||
if(world != null) MinecraftServer.getSchedulerManager().scheduleNextTick(() -> MinecraftServer.getInstanceManager().unregisterInstance(world));
|
||||
}
|
||||
|
||||
/** /reset: Fortschritt, Strecke, Score und Joker zurücksetzen und in einer frischen Welt bei 0 beginnen. */
|
||||
private static void resetGame(Player player) {
|
||||
JumpGenerator old = games.remove(player);
|
||||
if(old != null) old.close();
|
||||
Progress.reset(player.getUuid());
|
||||
|
||||
Instance oldWorld = player.getInstance();
|
||||
InstanceContainer world = createWorld();
|
||||
player.setVelocity(Vec.ZERO);
|
||||
player.setInstance(world, SPAWN).thenRun(() -> {
|
||||
Jokers.clear(player);
|
||||
games.put(player, new JumpGenerator(START, 0, player, world));
|
||||
releaseLater(oldWorld);
|
||||
});
|
||||
}
|
||||
|
||||
// Wie bei Velocity: Secret aus forwarding.secret im Arbeitsverzeichnis, alternativ per Umgebungsvariable
|
||||
private static String readForwardingSecret() throws IOException {
|
||||
String fromEnv = System.getenv("VELOCITY_FORWARDING_SECRET");
|
||||
if(fromEnv != null && !fromEnv.isBlank()) return fromEnv.trim();
|
||||
|
||||
Path file = Path.of("forwarding.secret");
|
||||
if(!Files.exists(file))
|
||||
throw new IllegalStateException("forwarding.secret fehlt im Arbeitsverzeichnis (oder VELOCITY_FORWARDING_SECRET setzen)");
|
||||
return Files.readString(file).trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package eu.mhsl.minecraft.endlessjumper;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.item.ItemStack;
|
||||
import net.minestom.server.item.Material;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Joker-Items in der Hotbar, die Anzahl ist die Stapelgröße. Es gibt sie zum Einsammeln auf der
|
||||
* Strecke; wer bei 0 beginnt, bekommt ein Totem. Das Totem wirkt automatisch, die anderen per Rechtsklick (siehe JumpGenerator#useJoker).
|
||||
*/
|
||||
public enum Jokers {
|
||||
BRIDGE("bruecke", 0, Material.BRICK, "Brücke", NamedTextColor.AQUA, "Rechtsklick: Weg über die nächsten 10 Blöcke"),
|
||||
FEATHER_FALL("federfall", 1, Material.FEATHER, "Federfall", NamedTextColor.WHITE, "Rechtsklick: 15 Sekunden langsames Fallen"),
|
||||
SPRING("sprungfeder", 2, Material.RABBIT_FOOT, "Sprungfeder", NamedTextColor.GREEN, "Rechtsklick: die nächsten 10 Sprünge mit Sprungkraft"),
|
||||
TOTEM("totem", 8, Material.TOTEM_OF_UNDYING, "Rettungstotem", NamedTextColor.GOLD, "Rettet dich automatisch beim Runterfallen");
|
||||
|
||||
// Name im Befehl /joker (Brigadier erlaubt keine Umlaute)
|
||||
private final String key;
|
||||
private final int slot;
|
||||
private final String name;
|
||||
private final ItemStack item;
|
||||
|
||||
Jokers(String key, int slot, Material material, String name, TextColor color, String lore) {
|
||||
this.key = key;
|
||||
this.slot = slot;
|
||||
this.name = name;
|
||||
this.item = ItemStack.builder(material)
|
||||
.customName(Component.text(name, color).decoration(TextDecoration.ITALIC, false))
|
||||
.lore(Component.text(lore, NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, false))
|
||||
.build();
|
||||
}
|
||||
|
||||
public ItemStack item() {
|
||||
return this.item;
|
||||
}
|
||||
|
||||
public boolean matches(ItemStack stack) {
|
||||
return stack.material() == this.item.material();
|
||||
}
|
||||
|
||||
public void add(Player player) {
|
||||
player.getInventory().replaceItemStack(this.slot, current -> this.matches(current) ? current.withAmount(a -> a + 1) : this.item);
|
||||
player.sendMessage(Component.text("+1 " + this.name, NamedTextColor.GOLD));
|
||||
}
|
||||
|
||||
/** Nimmt einen Joker weg; false, wenn keiner mehr da ist. */
|
||||
public boolean take(Player player) {
|
||||
if(!this.matches(player.getInventory().getItemStack(this.slot))) return false;
|
||||
player.getInventory().replaceItemStack(this.slot, current -> current.consume(1));
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Zum Start (und nach /reset) gibt es keine Joker. */
|
||||
public static void clear(Player player) {
|
||||
for(Jokers joker : values()) player.getInventory().setItemStack(joker.slot, ItemStack.AIR);
|
||||
}
|
||||
|
||||
public static Jokers random(Random random) {
|
||||
return values()[random.nextInt(values().length)];
|
||||
}
|
||||
|
||||
public static String[] keys() {
|
||||
String[] keys = new String[values().length];
|
||||
for(Jokers joker : values()) keys[joker.ordinal()] = joker.key;
|
||||
return keys;
|
||||
}
|
||||
|
||||
public static Jokers byKey(String key) {
|
||||
for(Jokers joker : values()) {
|
||||
if(joker.key.equals(key)) return joker;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Jokers of(ItemStack stack) {
|
||||
for(Jokers joker : values()) {
|
||||
if(joker.matches(stack)) return joker;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package eu.mhsl.minecraft.endlessjumper;
|
||||
|
||||
import net.minestom.server.MinecraftServer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* Letzter erreichter Checkpoint pro Spieler, damit man beim nächsten Besuch dort weiterspielt.
|
||||
* Liegt als progress.properties im Arbeitsverzeichnis; für ein neues Projekt einfach löschen.
|
||||
*/
|
||||
public class Progress {
|
||||
private static final Path FILE = Path.of("progress.properties");
|
||||
private static final Path TEMP = Path.of("progress.properties.tmp");
|
||||
// Properties ist synchronisiert; geschrieben wird nacheinander außerhalb des Server-Ticks
|
||||
private static final Properties checkpoints = new Properties();
|
||||
private static final ExecutorService writer = Executors.newSingleThreadExecutor(Thread.ofVirtual().factory());
|
||||
|
||||
public static void load() throws IOException {
|
||||
if(!Files.exists(FILE)) return;
|
||||
try(Reader reader = Files.newBufferedReader(FILE)) {
|
||||
checkpoints.load(reader);
|
||||
}
|
||||
}
|
||||
|
||||
public static int checkpoint(UUID player) {
|
||||
return Integer.parseInt(checkpoints.getProperty(player.toString(), "0"));
|
||||
}
|
||||
|
||||
public static void save(UUID player, int checkpoint) {
|
||||
checkpoints.setProperty(player.toString(), String.valueOf(checkpoint));
|
||||
writer.execute(Progress::write);
|
||||
}
|
||||
|
||||
public static void reset(UUID player) {
|
||||
checkpoints.remove(player.toString());
|
||||
writer.execute(Progress::write);
|
||||
}
|
||||
|
||||
/** Erst in eine Temp-Datei, dann atomar umbenennen: ein Absturz hinterlässt nie eine halbe Datei. */
|
||||
private static void write() {
|
||||
try {
|
||||
try(Writer out = Files.newBufferedWriter(TEMP)) {
|
||||
checkpoints.store(out, "EndlessJumper: letzter Checkpoint pro Spieler");
|
||||
}
|
||||
Files.move(TEMP, FILE, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch(IOException e) {
|
||||
MinecraftServer.getExceptionManager().handleException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package eu.mhsl.minecraft.endlessjumper;
|
||||
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.network.packet.server.play.ScoreboardObjectivePacket;
|
||||
import net.minestom.server.scoreboard.TabList;
|
||||
|
||||
/**
|
||||
* Zeigt den aktuellen Fortschritt jedes Spielers als Zahl in der Tab-Liste.
|
||||
* Der Fortschritt ist zugleich das Level des Spielers, daher braucht es keinen eigenen Speicher.
|
||||
*/
|
||||
public class ScoreTab {
|
||||
private static final TabList TAB_LIST = new TabList("jumps", ScoreboardObjectivePacket.Type.INTEGER);
|
||||
|
||||
public static void join(Player player) {
|
||||
TAB_LIST.addViewer(player);
|
||||
// dem neuen Spieler auch die Scores aller anderen zeigen
|
||||
TAB_LIST.getViewers().forEach(viewer -> TAB_LIST.updateScore(viewer, viewer.getLevel()));
|
||||
}
|
||||
|
||||
public static void update(Player player, int score) {
|
||||
if(TAB_LIST.getViewers().contains(player)) TAB_LIST.updateScore(player, score);
|
||||
}
|
||||
|
||||
public static void leave(Player player) {
|
||||
TAB_LIST.removeViewer(player);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package eu.mhsl.minecraft.endlessjumper;
|
||||
|
||||
import net.minestom.server.instance.generator.GenerationUnit;
|
||||
import net.minestom.server.instance.generator.Generator;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class TerrainGenerator implements Generator {
|
||||
@Override
|
||||
public void generate(@NotNull GenerationUnit unit) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,115 @@
|
||||
package eu.mhsl.minecraft.endlessjumper.generator;
|
||||
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import net.minestom.server.particle.Particle;
|
||||
import net.minestom.server.sound.SoundEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Abschnitte entlang des Fortschritts: einmal durch das komplette Spiel, von der Wiese über
|
||||
* Untergrund, Nether und Stronghold bis ins End; ab Sprung 1000 geht es endlos weiter, alle 50 Sprünge
|
||||
* mit einem zufälligen Thema aus diesen Abschnitten. Jeder Abschnitt hat eigene
|
||||
* Blöcke und eine eigene Stimmung (Tageszeit, Partikel, Musik).
|
||||
*/
|
||||
public class BlockPalette {
|
||||
public static Block getRandom(int seed) {
|
||||
Random r = new Random(seed);
|
||||
ArrayList<Block> blocks = new ArrayList<>();
|
||||
/** @param ambient Partikel um den Spieler, {@code null} für keine */
|
||||
public record Stage(int from, String name, long time, Particle ambient, SoundEvent music, List<Block> blocks) {
|
||||
}
|
||||
|
||||
blocks.add(Block.COPPER_BLOCK);
|
||||
blocks.add(Block.CUT_COPPER);
|
||||
blocks.add(Block.WEATHERED_COPPER);
|
||||
blocks.add(Block.WEATHERED_CUT_COPPER);
|
||||
blocks.add(Block.OXIDIZED_COPPER);
|
||||
blocks.add(Block.OXIDIZED_CUT_COPPER);
|
||||
private static final long MORNING = 1000, NOON = 6000, NIGHT = 18000;
|
||||
|
||||
return blocks.get(r.nextInt(0, blocks.size()));
|
||||
private static final List<Stage> STAGES = List.of(
|
||||
new Stage(0, "Wiese", MORNING, null, SoundEvent.MUSIC_OVERWORLD_MEADOW, List.of(
|
||||
Block.GRASS_BLOCK, Block.GRASS_BLOCK, Block.MOSS_BLOCK, Block.PODZOL, Block.ROOTED_DIRT
|
||||
)),
|
||||
new Stage(70, "Wald", NOON, Particle.CHERRY_LEAVES, SoundEvent.MUSIC_OVERWORLD_FOREST, List.of(
|
||||
Block.OAK_LOG, Block.BIRCH_LOG, Block.SPRUCE_LOG, Block.CHERRY_LOG,
|
||||
Block.OAK_LEAVES, Block.BIRCH_LEAVES, Block.SPRUCE_LEAVES, Block.CHERRY_LEAVES
|
||||
)),
|
||||
new Stage(140, "Untergrund", NIGHT, null, SoundEvent.MUSIC_OVERWORLD_DRIPSTONE_CAVES, List.of(
|
||||
Block.DIRT, Block.COARSE_DIRT, Block.STONE, Block.STONE, Block.ANDESITE,
|
||||
Block.GRANITE, Block.DIORITE, Block.COBBLESTONE
|
||||
)),
|
||||
new Stage(220, "Höhlen", NIGHT, Particle.DRIPPING_WATER, SoundEvent.MUSIC_OVERWORLD_LUSH_CAVES, List.of(
|
||||
Block.STONE, Block.COAL_ORE, Block.IRON_ORE, Block.COPPER_ORE,
|
||||
Block.GOLD_ORE, Block.REDSTONE_ORE, Block.LAPIS_ORE
|
||||
)),
|
||||
new Stage(300, "Tiefenschiefer", NIGHT, Particle.SCULK_SOUL, SoundEvent.MUSIC_OVERWORLD_DEEP_DARK, List.of(
|
||||
Block.DEEPSLATE, Block.DEEPSLATE, Block.TUFF, Block.DEEPSLATE_DIAMOND_ORE,
|
||||
Block.DEEPSLATE_IRON_ORE, Block.AMETHYST_BLOCK
|
||||
)),
|
||||
new Stage(390, "Netherportal", NIGHT, Particle.PORTAL, SoundEvent.MUSIC_NETHER_NETHER_WASTES, List.of(
|
||||
Block.OBSIDIAN, Block.OBSIDIAN, Block.CRYING_OBSIDIAN
|
||||
)),
|
||||
new Stage(410, "Nether", NIGHT, Particle.CRIMSON_SPORE, SoundEvent.MUSIC_NETHER_CRIMSON_FOREST, List.of(
|
||||
Block.NETHERRACK, Block.NETHERRACK, Block.CRIMSON_NYLIUM, Block.WARPED_NYLIUM,
|
||||
Block.SOUL_SOIL, Block.GLOWSTONE, Block.NETHER_QUARTZ_ORE, Block.NETHER_GOLD_ORE
|
||||
)),
|
||||
new Stage(540, "Festung & Bastion", NIGHT, Particle.WHITE_ASH, SoundEvent.MUSIC_NETHER_BASALT_DELTAS, List.of(
|
||||
Block.NETHER_BRICKS, Block.RED_NETHER_BRICKS, Block.BLACKSTONE,
|
||||
Block.POLISHED_BLACKSTONE_BRICKS, Block.GILDED_BLACKSTONE, Block.BASALT
|
||||
)),
|
||||
new Stage(660, "Stronghold", NIGHT, Particle.ENCHANT, SoundEvent.MUSIC_DISC_MELLOHI, List.of(
|
||||
Block.STONE_BRICKS, Block.MOSSY_STONE_BRICKS, Block.CRACKED_STONE_BRICKS,
|
||||
Block.CHISELED_STONE_BRICKS, Block.BOOKSHELF
|
||||
)),
|
||||
new Stage(800, "Portalraum", NIGHT, Particle.REVERSE_PORTAL, SoundEvent.MUSIC_DRAGON, List.of(
|
||||
Block.END_PORTAL_FRAME, Block.END_PORTAL_FRAME.withProperty("eye", "true"),
|
||||
Block.STONE_BRICKS, Block.MOSSY_STONE_BRICKS
|
||||
)),
|
||||
new Stage(850, "End", NIGHT, Particle.END_ROD, SoundEvent.MUSIC_END, List.of(
|
||||
Block.END_STONE, Block.END_STONE, Block.END_STONE_BRICKS,
|
||||
Block.PURPUR_BLOCK, Block.PURPUR_PILLAR, Block.OBSIDIAN
|
||||
))
|
||||
);
|
||||
|
||||
// ab hier endlos: alle ENDLESS_SEGMENT Sprünge ein zufälliges Thema aus den Abschnitten oben
|
||||
private static final int ENDLESS_FROM = 1000;
|
||||
private static final int ENDLESS_SEGMENT = 50;
|
||||
private static final Map<Integer, Stage> endlessStages = new ConcurrentHashMap<>();
|
||||
|
||||
public static Stage stageAt(int index) {
|
||||
if(index >= ENDLESS_FROM) return endlessStage((index - ENDLESS_FROM) / ENDLESS_SEGMENT);
|
||||
for(Stage stage : STAGES.reversed()) {
|
||||
if(index >= stage.from()) return stage;
|
||||
}
|
||||
return STAGES.getFirst();
|
||||
}
|
||||
|
||||
public static boolean isEndless(Stage stage) {
|
||||
return stage.from() >= ENDLESS_FROM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thema eines endlosen Abschnitts. Fest an die Abschnittsnummer gebunden (gleiches Thema beim
|
||||
* Weiterspielen) und nie zweimal dasselbe hintereinander. Zwischengespeichert, damit jeder
|
||||
* Abschnitt genau ein Stage-Objekt hat (Abschnittswechsel werden per Identität erkannt).
|
||||
*/
|
||||
private static Stage endlessStage(int segment) {
|
||||
return endlessStages.computeIfAbsent(segment, n -> {
|
||||
Stage theme = themeOf(n);
|
||||
return new Stage(ENDLESS_FROM + n * ENDLESS_SEGMENT, "Unendlich · " + theme.name(), theme.time(), theme.ambient(), theme.music(), theme.blocks());
|
||||
});
|
||||
}
|
||||
|
||||
/** Würfelt die Themen ab Abschnitt 0 der Reihe nach, damit „nie zweimal hintereinander“ sicher gilt. */
|
||||
private static Stage themeOf(int segment) {
|
||||
int previous = -1;
|
||||
int pick = 0;
|
||||
for(int n = 0; n <= segment; n++) {
|
||||
pick = new Random(n * 31L + 7).nextInt(STAGES.size());
|
||||
if(pick == previous) pick = (pick + 1) % STAGES.size();
|
||||
previous = pick;
|
||||
}
|
||||
return STAGES.get(pick);
|
||||
}
|
||||
|
||||
public static Block getRandom(int index, Random random) {
|
||||
List<Block> blocks = stageAt(index).blocks();
|
||||
return blocks.get(random.nextInt(blocks.size()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package eu.mhsl.minecraft.endlessjumper.generator;
|
||||
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.entity.EntityType;
|
||||
import net.minestom.server.entity.metadata.display.BlockDisplayMeta;
|
||||
import net.minestom.server.instance.Instance;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import net.minestom.server.timer.TaskSchedule;
|
||||
|
||||
/**
|
||||
* Lässt einen neuen Block als Block-Display klein oben aus dem vorherigen Block herauswachsen
|
||||
* und zu seiner Zielposition gleiten. Erst bei Ankunft wird der echte Block gesetzt.
|
||||
*/
|
||||
public class BlockTransition {
|
||||
public static final int DURATION_TICKS = 8;
|
||||
/** Zeit vom Start bis der echte Block steht (Spawn-Vorlauf + Animation + Puffer). */
|
||||
public static final int TOTAL_TICKS = DURATION_TICKS + 3;
|
||||
private static final double START_SCALE = 0.3;
|
||||
// minimal kleiner als der Block: sobald der echte Block steht, verdeckt er das Display ohne Z-Fighting
|
||||
private static final double END_SCALE = 0.998;
|
||||
// der Client braucht nach dem Setzen ein, zwei Frames bis der Block sichtbar ist: so lange bleibt das Display
|
||||
private static final int OVERLAP_TICKS = 4;
|
||||
|
||||
public static void animate(Instance world, Pos from, Pos to, Block block, int delayTicks, Runnable onArrival) {
|
||||
world.scheduler().buildTask(() -> spawn(world, from, to, block, onArrival))
|
||||
.delay(TaskSchedule.tick(Math.max(1, delayTicks)))
|
||||
.schedule();
|
||||
}
|
||||
|
||||
private static void spawn(Instance world, Pos from, Pos to, Block block, Runnable onArrival) {
|
||||
Entity display = new Entity(EntityType.BLOCK_DISPLAY);
|
||||
display.setNoGravity(true);
|
||||
display.editEntityMeta(BlockDisplayMeta.class, meta -> {
|
||||
meta.setBlockState(block);
|
||||
// feste Helligkeit: das Display startet im vorherigen Block, dort ist Lichtstufe 0 (sonst schwarz)
|
||||
meta.setBrightness(15, 15);
|
||||
meta.setPosRotInterpolationDuration(DURATION_TICKS);
|
||||
meta.setTransformationInterpolationDuration(DURATION_TICKS);
|
||||
// klein und mittig auf der Oberseite des vorherigen Blocks starten
|
||||
meta.setScale(new Vec(START_SCALE));
|
||||
meta.setTranslation(new Vec((1 - START_SCALE) / 2, 1, (1 - START_SCALE) / 2));
|
||||
});
|
||||
|
||||
display.setInstance(world, from).thenRun(() -> {
|
||||
// kurz warten, damit der Client das Display kennt, bevor die Interpolation startet
|
||||
display.scheduler().buildTask(() -> {
|
||||
display.editEntityMeta(BlockDisplayMeta.class, meta -> {
|
||||
meta.setTransformationInterpolationStartDelta(0);
|
||||
meta.setScale(new Vec(END_SCALE));
|
||||
meta.setTranslation(new Vec((1 - END_SCALE) / 2));
|
||||
});
|
||||
display.teleport(to);
|
||||
}).delay(TaskSchedule.tick(2)).schedule();
|
||||
|
||||
display.scheduler().buildTask(onArrival).delay(TaskSchedule.tick(TOTAL_TICKS)).schedule();
|
||||
display.scheduler().buildTask(display::remove).delay(TaskSchedule.tick(TOTAL_TICKS + OVERLAP_TICKS)).schedule();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package eu.mhsl.minecraft.endlessjumper.generator;
|
||||
|
||||
import net.kyori.adventure.util.RGBLike;
|
||||
import net.minestom.server.color.Color;
|
||||
import net.minestom.server.component.DataComponents;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.entity.EntityType;
|
||||
import net.minestom.server.entity.metadata.projectile.FireworkRocketMeta;
|
||||
import net.minestom.server.instance.Instance;
|
||||
import net.minestom.server.item.ItemStack;
|
||||
import net.minestom.server.item.Material;
|
||||
import net.minestom.server.item.component.FireworkExplosion;
|
||||
import net.minestom.server.item.component.FireworkList;
|
||||
import net.minestom.server.timer.TaskSchedule;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/** Steigt eine Feuerwerksrakete auf und explodiert in zufälliger Form und Farbe. */
|
||||
class Fireworks {
|
||||
private static final List<RGBLike> COLORS = List.of(
|
||||
new Color(255, 85, 85), new Color(255, 170, 0), new Color(255, 255, 85),
|
||||
new Color(85, 255, 85), new Color(85, 255, 255), new Color(85, 85, 255), new Color(255, 85, 255)
|
||||
);
|
||||
private static final int FLIGHT_TICKS = 20;
|
||||
|
||||
static void launch(Instance world, Pos at, Random random) {
|
||||
FireworkExplosion.Shape[] shapes = FireworkExplosion.Shape.values();
|
||||
FireworkExplosion explosion = new FireworkExplosion(
|
||||
shapes[random.nextInt(shapes.length)],
|
||||
List.of(COLORS.get(random.nextInt(COLORS.size())), COLORS.get(random.nextInt(COLORS.size()))),
|
||||
List.of(COLORS.get(random.nextInt(COLORS.size()))),
|
||||
random.nextBoolean(),
|
||||
random.nextBoolean()
|
||||
);
|
||||
ItemStack rocketItem = ItemStack.of(Material.FIREWORK_ROCKET).with(DataComponents.FIREWORKS, new FireworkList(1, List.of(explosion)));
|
||||
|
||||
Entity rocket = new Entity(EntityType.FIREWORK_ROCKET);
|
||||
rocket.setNoGravity(true);
|
||||
rocket.editEntityMeta(FireworkRocketMeta.class, meta -> meta.setFireworkInfo(rocketItem));
|
||||
rocket.setVelocity(new Vec(0, 15, 0));
|
||||
rocket.setInstance(world, at).thenRun(() -> rocket.scheduler().buildTask(() -> {
|
||||
// Status 17: der Client zeigt die Explosion
|
||||
rocket.triggerStatus((byte) 17);
|
||||
rocket.remove();
|
||||
}).delay(TaskSchedule.tick(FLIGHT_TICKS)).schedule());
|
||||
}
|
||||
}
|
||||
@@ -1,105 +1,408 @@
|
||||
package eu.mhsl.minecraft.endlessjumper.generator;
|
||||
|
||||
import eu.mhsl.minecraft.endlessjumper.Jokers;
|
||||
import eu.mhsl.minecraft.endlessjumper.Progress;
|
||||
import eu.mhsl.minecraft.endlessjumper.ScoreTab;
|
||||
import net.kyori.adventure.sound.Sound;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.entity.ItemEntity;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.instance.InstanceContainer;
|
||||
import net.minestom.server.instance.Chunk;
|
||||
import net.minestom.server.instance.Instance;
|
||||
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.potion.Potion;
|
||||
import net.minestom.server.potion.PotionEffect;
|
||||
import net.minestom.server.sound.SoundEvent;
|
||||
import net.minestom.server.timer.TaskSchedule;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Eine Partie Jump'n'Run. Die Strecke ist eine Liste von Schritten; {@link #standing} ist der Index
|
||||
* des zuletzt erreichten Blocks und zugleich der Score. Vorgeneriert werden {@link #PREVIEW} Blöcke
|
||||
* über den nächsten Landeblock hinaus.
|
||||
*/
|
||||
public class JumpGenerator {
|
||||
private record Step(Pos pos, Block block) {
|
||||
}
|
||||
|
||||
private record BlockKey(int x, int y, int z) {
|
||||
static BlockKey of(Point point) {
|
||||
return new BlockKey(point.blockX(), point.blockY(), point.blockZ());
|
||||
}
|
||||
}
|
||||
|
||||
private static final int PREVIEW = 3;
|
||||
private static final int FULL_DIFFICULTY_AT = 1000;
|
||||
private static final int FINALE_AT = 1000;
|
||||
private static final int FIREWORKS_NEAR = 6, FIREWORKS_FAR = 14, FIREWORKS_SPAN_TICKS = 100;
|
||||
|
||||
private static final Block BRIDGE_BLOCK = Block.WHITE_STAINED_GLASS;
|
||||
private static final int BRIDGE_STEP_TICKS = 4;
|
||||
private static final int BRIDGE_LENGTH = 10;
|
||||
private static final Block RESCUE_BLOCK = Block.OAK_PLANKS;
|
||||
private static final int RESCUE_DEPTH = 6;
|
||||
private static final int FEATHER_FALL_TICKS = 15 * 20;
|
||||
private static final int SPRING_JUMPS = 10;
|
||||
private static final double CHECKPOINT_JOKER_CHANCE = 0.1;
|
||||
|
||||
private static final TextColor COMBO_COLOR = TextColor.color(255, 69, 0);
|
||||
private static final Title CHECKPOINT_TITLE = Title.title(Component.empty(), Component.text("Checkpoint", NamedTextColor.GREEN));
|
||||
private static final Title FINALE_TITLE = Title.title(Component.text("Geschafft!", NamedTextColor.GOLD), Component.text("Durchgespielt – ab jetzt geht es endlos weiter", NamedTextColor.YELLOW));
|
||||
private static final Sound CHECKPOINT_SOUND = Sound.sound(SoundEvent.ENTITY_PLAYER_LEVELUP, Sound.Source.BLOCK, 0.3f, 1f);
|
||||
private static final Sound STAGE_SOUND = Sound.sound(SoundEvent.UI_TOAST_CHALLENGE_COMPLETE, Sound.Source.MASTER, 0.4f, 1f);
|
||||
private static final Sound BRIDGE_SOUND = Sound.sound(SoundEvent.BLOCK_AMETHYST_BLOCK_CHIME, Sound.Source.MASTER, 1f, 1f);
|
||||
private static final Sound RESCUE_SOUND = Sound.sound(SoundEvent.ITEM_TOTEM_USE, Sound.Source.MASTER, 0.6f, 1f);
|
||||
private static final Sound JOKER_SOUND = Sound.sound(SoundEvent.ENTITY_EXPERIENCE_ORB_PICKUP, Sound.Source.MASTER, 0.8f, 1.2f);
|
||||
private static final Sound RESET_SOUND = Sound.sound(SoundEvent.BLOCK_GLASS_BREAK, Sound.Source.BLOCK, 0.3f, 1f);
|
||||
|
||||
private final Player player;
|
||||
private final InstanceContainer world;
|
||||
private final ArrayList<Pos> blocks = new ArrayList<>();
|
||||
private Pos checkpoint;
|
||||
private Pos starting;
|
||||
private final Instance world;
|
||||
private final Random random = new Random();
|
||||
private final StageAtmosphere atmosphere;
|
||||
|
||||
// Index des ersten Schritts in course (beim Weiterspielen der gespeicherte Checkpoint)
|
||||
private final int base;
|
||||
private final List<Step> course = new ArrayList<>();
|
||||
private final Map<BlockKey, Integer> courseIndex = new HashMap<>();
|
||||
private int standing;
|
||||
private int combo = 0;
|
||||
private Pos checkpoint;
|
||||
// zuletzt per Brücke überbrückter Bereich: diese Blöcke zählen nicht zur Combo
|
||||
private int bridgeStart = -1;
|
||||
private int bridgeEnd = -1;
|
||||
|
||||
private final int preview = 2;
|
||||
// Sprungfeder: so viele Sprünge noch mit Sprungkraft
|
||||
private int springJumpsLeft = 0;
|
||||
|
||||
// laufende Rettung durch das Totem: der Block, auf den man über die Leiter zurückklettert
|
||||
private Pos rescueSafe;
|
||||
|
||||
public JumpGenerator(Pos starting, Player player, InstanceContainer world) {
|
||||
/** @param startIndex Fortschritt, ab dem gespielt wird (0 oder ein gespeicherter Checkpoint) */
|
||||
public JumpGenerator(Pos start, int startIndex, Player player, Instance world) {
|
||||
this.player = player;
|
||||
this.world = world;
|
||||
this.starting = starting;
|
||||
generateStart(starting);
|
||||
}
|
||||
|
||||
public void generateNext() {
|
||||
boolean isTenth = this.blocks.size() % 10 == 0;
|
||||
boolean standingTenth = (this.blocks.size()-this.preview-1) % 10 == 0;
|
||||
boolean isHundredth = this.blocks.size() % 100 == 0;
|
||||
|
||||
float pitch = 1 + (combo * 0.025f);
|
||||
this.player.playSound(Sound.sound(SoundEvent.BLOCK_NOTE_BLOCK_PLING, Sound.Source.MASTER, 0.1f, pitch));
|
||||
this.player.showTitle(Title.title(Component.text(""), Component.text((combo)+"", TextColor.color(255, 69, 0))));
|
||||
this.player.setLevel((this.blocks.size()-this.preview-1));
|
||||
|
||||
Random r = new Random();
|
||||
Pos target = blocks.get(blocks.size()-1).add(r.nextInt(-1, 2), r.nextInt(this.player.getPosition().y() < 80 ? 0 : -1, this.player.getPosition().y() > 120 ? 1 : 2), r.nextInt(3, 5));
|
||||
this.world.setBlock(target, isHundredth ? Block.DIAMOND_BLOCK : isTenth ? Block.GOLD_BLOCK : BlockPalette.getRandom((this.blocks.size() / 10)));
|
||||
|
||||
if(standingTenth) {
|
||||
Pos current = this.blocks.get(this.blocks.size()-this.preview-1);
|
||||
this.checkpoint = current;
|
||||
this.player.playSound(Sound.sound(SoundEvent.ENTITY_PLAYER_LEVELUP, Sound.Source.BLOCK, 0.3f, 1f));
|
||||
this.player.showTitle(Title.title(Component.text(""), Component.text("Checkpoint", TextColor.color(0, 255, 0))));
|
||||
this.player.sendPacket(new ParticlePacket(Particle.CRIT, current, current.add(0.5), 1.5f, 50));
|
||||
}
|
||||
this.world.setBlock(target.add(0, 1 ,0), Block.LIGHT);
|
||||
this.blocks.add(target);
|
||||
this.player.sendPacket(new ParticlePacket(Particle.CRIT, target, target.add(0.5), 1, 10));
|
||||
this.player.sendPacket(new ParticlePacket(Particle.FALLING_NECTAR, target, target.add(0.5),0.3f, 10));
|
||||
this.combo++;
|
||||
}
|
||||
|
||||
public Pos getNextLanding() {
|
||||
return this.blocks.get(this.blocks.size()-(preview+1));
|
||||
}
|
||||
|
||||
|
||||
public void generateStart(Pos start) {
|
||||
this.starting = start;
|
||||
this.base = startIndex;
|
||||
this.standing = startIndex;
|
||||
this.checkpoint = start;
|
||||
this.blocks.add(this.starting);
|
||||
this.world.setBlock(this.starting, Block.DIAMOND_BLOCK);
|
||||
for(int i = 0; i <= preview; i++) generateNext();
|
||||
this.atmosphere = new StageAtmosphere(player, world, BlockPalette.stageAt(startIndex));
|
||||
|
||||
Block startBlock = startIndex == 0 || isMilestone(startIndex) ? Block.DIAMOND_BLOCK : Block.GOLD_BLOCK;
|
||||
this.world.setBlock(start, startBlock);
|
||||
this.addStep(new Step(start, startBlock));
|
||||
this.player.setLevel(startIndex);
|
||||
ScoreTab.update(this.player, startIndex);
|
||||
// wer ganz von vorn beginnt, bekommt ein Totem; beim Weiterspielen gibt es keine Joker
|
||||
if(startIndex > 0) this.player.sendMessage(Component.text("Weiter ab Sprung " + startIndex, NamedTextColor.GOLD));
|
||||
else Jokers.TOTEM.add(this.player);
|
||||
|
||||
// nacheinander animieren, damit jeder Block aus seinem bereits stehenden Vorgänger wächst
|
||||
this.ensureGenerated(startIndex + PREVIEW + 1, BlockTransition.TOTAL_TICKS);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
/*
|
||||
for(Pos p : this.blocks) {
|
||||
this.world.setBlock(p, Block.AIR);
|
||||
private Step step(int index) {
|
||||
return this.course.get(index - this.base);
|
||||
}
|
||||
|
||||
/** Index des nächsten zu erzeugenden Schritts. */
|
||||
private int generated() {
|
||||
return this.base + this.course.size();
|
||||
}
|
||||
|
||||
private static boolean isCheckpoint(int index) {
|
||||
return index % 10 == 0;
|
||||
}
|
||||
|
||||
private static boolean isMilestone(int index) {
|
||||
return index % 100 == 0;
|
||||
}
|
||||
|
||||
private static double height(Block block) {
|
||||
return block.collisionShape().relativeEnd().y();
|
||||
}
|
||||
|
||||
private Pos landing() {
|
||||
return this.step(this.standing + 1).pos();
|
||||
}
|
||||
|
||||
/** Wird bei jeder Bewegung aufgerufen: erkennt Landungen und behandelt das Runterfallen. */
|
||||
public void onMove(Pos newPosition) {
|
||||
// Block unter den Füßen; -0.5 trifft auch niedrige Blöcke wie den Endportalrahmen
|
||||
Integer index = this.courseIndex.get(BlockKey.of(newPosition.sub(0, 0.5, 0)));
|
||||
if(index != null && index > this.standing) this.advanceTo(index);
|
||||
this.checkFall();
|
||||
}
|
||||
|
||||
private void advanceTo(int index) {
|
||||
int previous = this.standing;
|
||||
this.standing = index;
|
||||
this.player.setLevel(index);
|
||||
ScoreTab.update(this.player, index);
|
||||
|
||||
if(this.springJumpsLeft > 0 && --this.springJumpsLeft == 0) this.player.removeEffect(PotionEffect.JUMP_BOOST);
|
||||
|
||||
if(index < this.bridgeStart || index > this.bridgeEnd) {
|
||||
this.combo++;
|
||||
this.player.playSound(Sound.sound(SoundEvent.BLOCK_NOTE_BLOCK_PLING, Sound.Source.MASTER, 0.1f, 1 + this.combo * 0.025f));
|
||||
this.player.showTitle(Title.title(Component.empty(), Component.text(this.combo, COMBO_COLOR)));
|
||||
}
|
||||
this.blocks.clear();
|
||||
*/
|
||||
|
||||
// auch ein übersprungener Checkpoint zählt: der letzte zwischen altem und neuem Stand
|
||||
int reachedCheckpoint = index / 10 * 10;
|
||||
if(reachedCheckpoint > previous) {
|
||||
this.checkpoint = this.step(reachedCheckpoint).pos();
|
||||
Progress.save(this.player.getUuid(), reachedCheckpoint);
|
||||
this.unloadChunksBehind();
|
||||
this.player.playSound(CHECKPOINT_SOUND);
|
||||
this.player.showTitle(CHECKPOINT_TITLE);
|
||||
this.player.sendPacket(new ParticlePacket(Particle.CRIT, this.checkpoint, this.checkpoint.add(0.5), 1.5f, 50));
|
||||
}
|
||||
|
||||
// neuer Abschnitt: Stimmung wechseln (die Actionbar zeigt der Abschnitt selbst dauerhaft)
|
||||
BlockPalette.Stage stage = BlockPalette.stageAt(index);
|
||||
if(stage != BlockPalette.stageAt(previous)) {
|
||||
this.atmosphere.enter(stage);
|
||||
this.player.playSound(STAGE_SOUND);
|
||||
}
|
||||
|
||||
if(previous < FINALE_AT && index >= FINALE_AT) this.celebrate();
|
||||
|
||||
this.ensureGenerated(index + PREVIEW + 1, 0);
|
||||
}
|
||||
|
||||
/** Durchgespielt (Beginn von „Unendlich“): Feuerwerk rund um den Spieler. */
|
||||
private void celebrate() {
|
||||
this.player.showTitle(FINALE_TITLE);
|
||||
|
||||
// in Blickrichtung: ein paar Raketen nah vor dem Spieler, die meisten in der Ferne links, mittig und rechts
|
||||
Pos origin = this.player.getPosition();
|
||||
Vec forward = origin.direction().withY(0).normalize();
|
||||
Vec right = new Vec(-forward.z(), 0, forward.x());
|
||||
for(int i = 0; i < FIREWORKS_NEAR + FIREWORKS_FAR; i++) {
|
||||
boolean near = i < FIREWORKS_NEAR;
|
||||
double distance = near ? this.random.nextDouble(3, 10) : this.random.nextDouble(20, 45);
|
||||
double side = near ? this.random.nextDouble(-6, 6) : this.random.nextDouble(-25, 25);
|
||||
Pos at = origin.add(forward.mul(distance)).add(right.mul(side)).add(0, near ? 0 : 4, 0);
|
||||
this.world.scheduler().buildTask(() -> Fireworks.launch(this.world, at, this.random))
|
||||
.delay(TaskSchedule.tick(1 + this.random.nextInt(FIREWORKS_SPAN_TICKS)))
|
||||
.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Strecke wandert immer in +z. Chunks hinter dem Checkpoint und außerhalb der Sichtweite
|
||||
* werden nie wieder gebraucht (Reset und Rettung liegen davor): freigeben, sonst sammeln sich
|
||||
* bei langen Läufen tausende an.
|
||||
*/
|
||||
private void unloadChunksBehind() {
|
||||
int playerChunk = this.player.getPosition().blockZ() >> 4;
|
||||
int checkpointChunk = this.checkpoint.blockZ() >> 4;
|
||||
int keepFrom = Math.min(checkpointChunk, playerChunk - this.player.effectiveViewDistance()) - 1;
|
||||
for(Chunk chunk : List.copyOf(this.world.getChunks())) {
|
||||
if(chunk.getChunkZ() < keepFrom) this.world.unloadChunk(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureGenerated(int upTo, int stepTicks) {
|
||||
int delay = 0;
|
||||
while(this.generated() <= upTo) {
|
||||
this.generateStep(delay, null);
|
||||
delay += stepTicks;
|
||||
}
|
||||
}
|
||||
|
||||
private void addStep(Step step) {
|
||||
this.courseIndex.put(BlockKey.of(step.pos()), this.generated());
|
||||
this.course.add(step);
|
||||
}
|
||||
|
||||
/** Erzeugt den nächsten Streckenblock; {@code afterArrival} läuft, sobald er tatsächlich steht. */
|
||||
private void generateStep(int animationDelayTicks, Runnable afterArrival) {
|
||||
int index = this.generated();
|
||||
Step previous = this.course.getLast();
|
||||
Block block = isMilestone(index) ? Block.DIAMOND_BLOCK : isCheckpoint(index) ? Block.GOLD_BLOCK : BlockPalette.getRandom(index, this.random);
|
||||
Pos target = previous.pos().add(this.nextOffset(index, previous.block(), block));
|
||||
this.addStep(new Step(target, block));
|
||||
|
||||
BlockTransition.animate(this.world, previous.pos(), target, block, animationDelayTicks, () -> {
|
||||
this.world.setBlock(target, block);
|
||||
this.world.setBlock(target.add(0, 1, 0), Block.LIGHT);
|
||||
this.player.sendPacket(new ParticlePacket(Particle.CRIT, target, target.add(0.5), 1, 10));
|
||||
this.player.sendPacket(new ParticlePacket(Particle.FALLING_NECTAR, target, target.add(0.5), 0.3f, 10));
|
||||
if(isMilestone(index) || (isCheckpoint(index) && this.random.nextDouble() < CHECKPOINT_JOKER_CHANCE)) this.dropJoker(target);
|
||||
if(afterArrival != null) afterArrival.run();
|
||||
});
|
||||
}
|
||||
|
||||
/** Auf jedem Diamantblock (und manchem Checkpoint) liegt ein zufälliger Joker zum Einsammeln. */
|
||||
private void dropJoker(Pos target) {
|
||||
ItemEntity item = new ItemEntity(Jokers.random(this.random).item());
|
||||
item.setPickupDelay(Duration.ZERO);
|
||||
item.setInstance(this.world, target.add(0.5, 1, 0.5));
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstand zum nächsten Block. Sprünge sind immer mindestens 3 weit (natürliche Sprint-Weite,
|
||||
* kürzere werden überjumpt). Über die ersten {@link #FULL_DIFFICULTY_AT} Sprünge steigen der
|
||||
* Anteil an 4er-Sprüngen, seitlichen Versätzen und Sprüngen nach oben bis zum bisherigen
|
||||
* Niveau und bleiben danach dort.
|
||||
*/
|
||||
private Vec nextOffset(int index, Block from, Block to) {
|
||||
double difficulty = Math.min(1.0, (double) index / FULL_DIFFICULTY_AT);
|
||||
|
||||
int dz = this.random.nextDouble() < 0.1 + 0.4 * difficulty ? 4 : 3;
|
||||
int dx = this.random.nextDouble() < 0.1 + 0.57 * difficulty ? (this.random.nextBoolean() ? 1 : -1) : 0;
|
||||
|
||||
double y = this.player.getPosition().y();
|
||||
double roll = this.random.nextDouble();
|
||||
int dy = 0;
|
||||
if(roll < 0.05 + 0.28 * difficulty && y <= 120) dy = 1;
|
||||
else if(roll > 1 - (0.2 + 0.13 * difficulty) && y >= 80) dy = -1;
|
||||
|
||||
// von niedrigen Blöcken (z. B. Endportalrahmen) nie nach oben, und die Oberkante darf
|
||||
// höchstens einen Block höher liegen
|
||||
if(dy == 1 && (height(from) < 1 || height(to) > height(from))) dy = 0;
|
||||
|
||||
// weite Sprünge nach oben erst ab mittlerer, diagonal erst ab hoher Schwierigkeit
|
||||
if(dy == 1 && dz == 4 && difficulty < 0.4) dz = 3;
|
||||
if(dy == 1 && dz == 4 && dx != 0 && difficulty < 0.7) dx = 0;
|
||||
|
||||
return new Vec(dx, dy, dz);
|
||||
}
|
||||
|
||||
/** Rechtsklick mit einem Joker; true, wenn er verbraucht wurde. Das Totem wirkt nur automatisch. */
|
||||
public boolean useJoker(Jokers joker) {
|
||||
boolean used = switch(joker) {
|
||||
case BRIDGE -> this.useBridge();
|
||||
case FEATHER_FALL -> {
|
||||
this.player.addEffect(new Potion(PotionEffect.SLOW_FALLING, 0, FEATHER_FALL_TICKS));
|
||||
yield true;
|
||||
}
|
||||
case SPRING -> {
|
||||
this.springJumpsLeft = SPRING_JUMPS;
|
||||
this.player.addEffect(new Potion(PotionEffect.JUMP_BOOST, 0, Potion.INFINITE_DURATION));
|
||||
yield true;
|
||||
}
|
||||
case TOTEM -> false;
|
||||
};
|
||||
if(used) this.player.playSound(JOKER_SOUND);
|
||||
return used;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joker „Brücke“: verbindet die nächsten {@link #BRIDGE_LENGTH} Blöcke ab der aktuellen Position
|
||||
* mit einem begehbaren Weg, auch über früher schon geschaffte Stücke (z. B. nach einem Reset).
|
||||
* Diese Blöcke zählen nicht zur Combo. Ist der Bereich schon überbrückt, passiert nichts.
|
||||
*/
|
||||
private boolean useBridge() {
|
||||
int target = this.standing + BRIDGE_LENGTH;
|
||||
if(this.rescueSafe != null || (this.standing >= this.bridgeStart && target <= this.bridgeEnd)) return false;
|
||||
this.bridgeStart = this.standing;
|
||||
this.bridgeEnd = target;
|
||||
|
||||
// bereits stehende Abschnitte sofort, neue Blöcke bekommen ihren Weg bei Ankunft
|
||||
for(int i = this.standing + 1; i < this.generated() && i <= target; i++) {
|
||||
this.buildPath(this.step(i - 1).pos(), this.step(i).pos());
|
||||
}
|
||||
int delay = 0;
|
||||
while(this.generated() <= target) {
|
||||
Pos from = this.course.getLast().pos();
|
||||
int index = this.generated();
|
||||
this.generateStep(delay, () -> this.buildPath(from, this.step(index).pos()));
|
||||
delay += BRIDGE_STEP_TICKS;
|
||||
}
|
||||
|
||||
this.player.playSound(BRIDGE_SOUND);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Weg auf Höhe des Startblocks; bei seitlichem Versatz zwei Blöcke breit, damit er durchgehend ist. */
|
||||
private void buildPath(Pos from, Pos to) {
|
||||
int minX = Math.min(from.blockX(), to.blockX());
|
||||
int maxX = Math.max(from.blockX(), to.blockX());
|
||||
for(int z = from.blockZ() + 1; z < to.blockZ(); z++) {
|
||||
for(int x = minX; x <= maxX; x++) {
|
||||
Pos pos = new Pos(x, from.blockY(), z);
|
||||
if(this.world.getBlock(pos).compare(Block.AIR)) this.world.setBlock(pos, BRIDGE_BLOCK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkFall() {
|
||||
double y = this.player.getPosition().y();
|
||||
|
||||
if(this.rescueSafe != null) {
|
||||
if(y >= this.rescueSafe.y() + 0.9) this.rescueSafe = null;
|
||||
else if(y < this.rescueSafe.blockY() - RESCUE_DEPTH - 3) {
|
||||
this.rescueSafe = null;
|
||||
this.reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(y < this.landing().y() - 5 && y < this.checkpoint.y()) {
|
||||
if(Jokers.TOTEM.take(this.player)) this.rescue();
|
||||
else this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Joker „Rettungstotem“: Plattform unter dem zuletzt erreichten Block, davor eine Stützsäule
|
||||
* mit Leiter, über die man direkt wieder auf den Block klettert. Die Strecke geht immer
|
||||
* mindestens 3 Blöcke nach vorne, die Rettung baut nur dahinter und darunter.
|
||||
*/
|
||||
private void rescue() {
|
||||
Pos safe = this.step(this.standing).pos();
|
||||
int x = safe.blockX();
|
||||
int z = safe.blockZ();
|
||||
int floorY = safe.blockY() - RESCUE_DEPTH;
|
||||
this.rescueSafe = safe;
|
||||
|
||||
for(int dx = -1; dx <= 1; dx++) {
|
||||
for(int dz = -3; dz <= -1; dz++) this.placeRescue(new Pos(x + dx, floorY, z + dz), RESCUE_BLOCK);
|
||||
}
|
||||
// Säule direkt hinter dem Block bis auf seine Höhe, Leiter davor (hängt an der Säule)
|
||||
for(int y = floorY + 1; y <= safe.blockY(); y++) {
|
||||
this.placeRescue(new Pos(x, y, z - 1), RESCUE_BLOCK);
|
||||
this.placeRescue(new Pos(x, y, z - 2), Block.LADDER.withProperty("facing", "north"));
|
||||
}
|
||||
|
||||
this.player.setVelocity(Vec.ZERO);
|
||||
this.player.teleport(new Pos(x + 0.5, floorY + 1, z - 1.5, 0, 0));
|
||||
this.player.triggerStatus((byte) 35);
|
||||
this.player.playSound(RESCUE_SOUND);
|
||||
}
|
||||
|
||||
/** Brückenglas darf überschrieben werden, sonst könnte ein früherer Brückenweg die Leiter versperren. */
|
||||
private void placeRescue(Pos pos, Block block) {
|
||||
Block existing = this.world.getBlock(pos);
|
||||
if(existing.compare(Block.AIR) || existing.compare(BRIDGE_BLOCK)) this.world.setBlock(pos, block);
|
||||
}
|
||||
|
||||
/** Beendet alles, was an den Spieler gebunden weiterläuft (Partikel, Musik, Joker-Effekte). */
|
||||
public void close() {
|
||||
this.atmosphere.stop();
|
||||
this.player.removeEffect(PotionEffect.SLOW_FALLING);
|
||||
this.player.removeEffect(PotionEffect.JUMP_BOOST);
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
this.player.teleport(this.checkpoint.add(0.5, 1, 0.5));
|
||||
this.combo = 0;
|
||||
this.player.showTitle(Title.title(Component.text(""), Component.text("")));
|
||||
|
||||
//generateStart(last);
|
||||
this.player.playSound(Sound.sound(SoundEvent.BLOCK_GLASS_BREAK, Sound.Source.BLOCK, 0.3f, 1f));
|
||||
|
||||
}
|
||||
|
||||
public Pos getCheckpoint() {
|
||||
return checkpoint;
|
||||
}
|
||||
|
||||
public ArrayList<Pos> getBlocks() {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
public int getPreview() {
|
||||
return preview;
|
||||
this.player.clearTitle();
|
||||
this.player.playSound(RESET_SOUND);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package eu.mhsl.minecraft.endlessjumper.generator;
|
||||
|
||||
import net.kyori.adventure.sound.Sound;
|
||||
import net.kyori.adventure.sound.SoundStop;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.instance.Instance;
|
||||
import net.minestom.server.network.packet.server.play.ParticlePacket;
|
||||
import net.minestom.server.timer.Task;
|
||||
import net.minestom.server.timer.TaskSchedule;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Stimmung des aktuellen Abschnitts: Tageszeit (fließender Übergang), Musik, Partikel auf der
|
||||
* Strecke voraus und der Abschnittsname dauerhaft in der Actionbar.
|
||||
*/
|
||||
class StageAtmosphere {
|
||||
private static final int TIME_TRANSITION_TICKS = 100;
|
||||
// die Strecke läuft immer in +z: Partikel vom Spieler bis etwa 10 Sprünge voraus
|
||||
private static final Vec AMBIENT_AHEAD = new Vec(0, 1.5, 14);
|
||||
private static final Vec AMBIENT_SPREAD = new Vec(7, 4, 16);
|
||||
private static final int AMBIENT_EVERY_TICKS = 4;
|
||||
// die Actionbar blendet nach ~3 s aus: jede Sekunde erneuern, nach einem Wechsel 5 s golden
|
||||
private static final int ACTION_BAR_TICKS = 20;
|
||||
private static final int HIGHLIGHT_TICKS = 100;
|
||||
private static final Component ENDLESS_HINT = Component.text(" · /reset für einen Neustart", NamedTextColor.DARK_GRAY);
|
||||
|
||||
private final Player player;
|
||||
private final Instance world;
|
||||
private BlockPalette.Stage stage;
|
||||
private Task timeTransition;
|
||||
private final Task ambient;
|
||||
private int ticks = 0;
|
||||
private int highlightUntil = 0;
|
||||
|
||||
StageAtmosphere(Player player, Instance world, BlockPalette.Stage initial) {
|
||||
this.player = player;
|
||||
this.world = world;
|
||||
this.stage = initial;
|
||||
this.world.setTime(initial.time());
|
||||
this.world.refreshTime();
|
||||
this.playMusic();
|
||||
// an den Spieler gebunden, endet also mit ihm
|
||||
this.ambient = player.scheduler().buildTask(this::tick).repeat(TaskSchedule.tick(AMBIENT_EVERY_TICKS)).schedule();
|
||||
}
|
||||
|
||||
void stop() {
|
||||
this.ambient.cancel();
|
||||
if(this.timeTransition != null) this.timeTransition.cancel();
|
||||
this.player.stopSound(SoundStop.source(Sound.Source.MUSIC));
|
||||
}
|
||||
|
||||
void enter(BlockPalette.Stage next) {
|
||||
if(next == this.stage) return;
|
||||
this.stage = next;
|
||||
this.highlightUntil = this.ticks + HIGHLIGHT_TICKS;
|
||||
this.playMusic();
|
||||
this.fadeTimeTo(next.time());
|
||||
this.showStage();
|
||||
}
|
||||
|
||||
private void playMusic() {
|
||||
this.player.stopSound(SoundStop.source(Sound.Source.MUSIC));
|
||||
this.player.playSound(Sound.sound(this.stage.music(), Sound.Source.MUSIC, 1f, 1f));
|
||||
}
|
||||
|
||||
/** Läuft immer vorwärts zur Zielzeit, Tick für Tick, damit Himmel und Licht weich wechseln. */
|
||||
private void fadeTimeTo(long target) {
|
||||
if(this.timeTransition != null) this.timeTransition.cancel();
|
||||
long start = this.world.getTime();
|
||||
long delta = Math.floorMod(target - start, 24000L);
|
||||
if(delta == 0) return;
|
||||
|
||||
this.timeTransition = this.world.scheduler().submitTask(new Supplier<>() {
|
||||
private int tick = 0;
|
||||
|
||||
@Override
|
||||
public TaskSchedule get() {
|
||||
this.tick++;
|
||||
StageAtmosphere.this.world.setTime(start + delta * this.tick / TIME_TRANSITION_TICKS);
|
||||
StageAtmosphere.this.world.refreshTime();
|
||||
return this.tick < TIME_TRANSITION_TICKS ? TaskSchedule.tick(1) : TaskSchedule.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
this.ticks += AMBIENT_EVERY_TICKS;
|
||||
if(this.ticks % ACTION_BAR_TICKS == 0) this.showStage();
|
||||
if(this.stage.ambient() != null) {
|
||||
this.player.sendPacket(new ParticlePacket(this.stage.ambient(), this.player.getPosition().add(AMBIENT_AHEAD), AMBIENT_SPREAD, 0.02f, 8));
|
||||
}
|
||||
}
|
||||
|
||||
private void showStage() {
|
||||
Component text = Component.text(this.stage.name(), this.ticks < this.highlightUntil ? NamedTextColor.GOLD : NamedTextColor.GRAY);
|
||||
if(BlockPalette.isEndless(this.stage)) text = text.append(ENDLESS_HINT);
|
||||
this.player.sendActionBar(text);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +0,0 @@
|
||||
#Generated by Maven
|
||||
#Fri Dec 19 20:45:21 CET 2025
|
||||
artifactId=EndlessJumper
|
||||
groupId=org.example
|
||||
version=1.0-SNAPSHOT
|
||||
@@ -1,5 +0,0 @@
|
||||
eu/mhsl/minecraft/endlessjumper/generator/BlockPalette.class
|
||||
eu/mhsl/minecraft/endlessjumper/TerrainGenerator.class
|
||||
eu/mhsl/minecraft/endlessjumper/generator/JumpGenerator.class
|
||||
eu/mhsl/minecraft/endlessjumper/BlockMath.class
|
||||
eu/mhsl/minecraft/endlessjumper/EndlessJumper.class
|
||||
@@ -1,5 +0,0 @@
|
||||
/home/elias/projects/Minecraft/EndlessJumper/src/main/java/eu/mhsl/minecraft/endlessjumper/EndlessJumper.java
|
||||
/home/elias/projects/Minecraft/EndlessJumper/src/main/java/eu/mhsl/minecraft/endlessjumper/BlockMath.java
|
||||
/home/elias/projects/Minecraft/EndlessJumper/src/main/java/eu/mhsl/minecraft/endlessjumper/TerrainGenerator.java
|
||||
/home/elias/projects/Minecraft/EndlessJumper/src/main/java/eu/mhsl/minecraft/endlessjumper/generator/JumpGenerator.java
|
||||
/home/elias/projects/Minecraft/EndlessJumper/src/main/java/eu/mhsl/minecraft/endlessjumper/generator/BlockPalette.java
|
||||
Binary file not shown.
Reference in new issue
Block a user