78 Commits

Author SHA1 Message Date
MineTec 8a9f3a8ad0 added Bacteria monster to Backrooms game with camouflage mechanics, lure sounds, and sprinting behavior 2026-08-15 16:10:12 +02:00
MineTec 817693239b refactored Backrooms game to introduce modular game systems, procedural generation, and enhanced mechanics 2026-08-09 22:05:44 +02:00
MineTec 26eff149d4 removed deprecated use of old DimensionType constructor and updated map file paths accordingly 2026-08-09 12:35:05 +02:00
jannis 7734b225eb Merge pull request 'added playerrace and updated to version 26.2' (#14) from develop-jannis into develop
Reviewed-on: #14
Reviewed-by: Lars Neuhaus <larslukasneuhaus@gmx.de>
2026-07-31 20:04:15 +00:00
Pupsi 1a9b9f5fd2 added return to player race position check 2026-07-31 13:28:27 +02:00
jannis b0cf527977 removed unnecessary comments 2026-07-29 15:59:08 +02:00
jannis 01646546a3 fixen issues with playerrace and changed the item selection in pillars 2026-07-26 22:03:11 +02:00
jannis 13a0cdde3a changed gametype of playerrace to jumpnrun and changed symbol to dirt path in playerrace 2026-07-20 21:12:33 +02:00
jannis 7d60c74d85 fixed generation ofo elytrarace 2026-07-20 13:47:25 +02:00
MineTec 0d7f1fc646 updated dependencies and updated all usages accordingly, replaced deprecated methods, and updated project to JDK 25 2026-07-19 19:37:44 +02:00
jannis ee421245d1 added translations 2026-07-19 01:50:34 +02:00
jannis d75332927c fixed that something can generate twice in playerrace 2026-07-19 01:31:53 +02:00
jannis 2c579f4afe fixen generating problem and finished logic of playerrace 2026-07-19 01:11:56 +02:00
jannis 82984cf5cf added LadderWall2 and added checkpoints 2026-07-19 00:16:12 +02:00
MineTec 14f6a38212 merged origin/develop-jannis 2026-07-18 22:15:03 +02:00
MineTec 3bdc4113c0 added 20 obstacle types, adjustable track length and forked obstacle generation 2026-07-18 22:14:29 +02:00
jannis b7e292fe0e added LadderWall 2026-07-18 21:35:00 +02:00
Pupsi 57cd5acdfd added cos function to mudpit 2026-07-18 18:57:33 +02:00
Pupsi 0f72b8049d simplified block loop from generator 2026-07-18 18:07:20 +02:00
jannis 9992b07ed5 added basic obstacle mechanic 2026-07-18 17:41:45 +02:00
jannis f55148203e started player race 2026-07-18 15:15:29 +02:00
jannis 41753f2805 Merge pull request 'Finalized ColorJump' (#13) from develop-jannis into develop
Reviewed-on: #13
Reviewed-by: Lars Neuhaus <larslukasneuhaus@gmx.de>
2026-07-15 16:15:14 +00:00
jannis 9bdb98cdc9 other way for the effective-final 2026-07-13 16:34:07 +02:00
jannis 6f0ebe4d12 moved colorjump from prototype to jumpnrun 2026-07-13 10:40:58 +02:00
jannis 58983be156 removed off-hand swap in colorjump 2026-07-13 10:30:34 +02:00
jannis ee7d434d7e perfectet colorjump timer, cancelled inventory movement 2026-02-16 22:28:32 +01:00
jannis 410f7b4027 perfekted colorjump 2026-02-16 21:07:30 +01:00
jannis e1515200ab stoped task at game end 2026-02-11 20:56:26 +01:00
jannis 27005f4fc4 Merge pull request 'added color jump' (#12) from develop-colorjump into develop
Reviewed-on: #12
Reviewed-by: Lars Neuhaus <larslukasneuhaus@gmx.de>
2026-02-10 20:24:17 +00:00
jannis 2808393c23 solved pr comments 2026-02-10 21:22:32 +01:00
jannis d0825695b3 added an end to color jump 2026-02-10 19:23:06 +01:00
jannis e822a5c5e2 fixed translations for color jump 2026-02-10 19:17:37 +01:00
jannis bf95c8dcc2 added translations to color jump 2026-02-10 19:12:36 +01:00
jannis 2f70b25e87 added ColorJump 2026-02-10 19:08:07 +01:00
Pupsi cbec1ea7f8 Merge pull request 'develop-tetris-srs' (#11) from develop-tetris-srs into develop
Reviewed-on: #11
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2026-02-06 10:47:27 +00:00
Pupsi c176cfaf2c solved pr comments; switched from hard lock task to reset counter 2026-02-04 19:25:03 +01:00
Pupsi 56f56d48b6 added airborne lock timer reset 2026-02-03 19:22:33 +01:00
Pupsi 991e51bc10 fixed ground behavior for tetrominoes 2026-02-03 19:08:05 +01:00
Pupsi ffba86ac41 removed cause of tetris collision bugs (yaw, pitch in Pos) 2026-02-03 17:18:28 +01:00
Pupsi f95c670514 removed cause of tetris collision bugs (yaw, pitch in Pos) 2026-02-03 17:18:10 +01:00
Pupsi 0b3c757ce0 fixed srs rotations 2026-02-03 15:51:41 +01:00
Pupsi 76df6643db added simplified SRS; not functional for I-Tetromino 2026-02-02 22:37:43 +01:00
jannis 7186b4dbea Merge pull request 'added pillars and block battle' (#10) from develop-pillars into develop
Reviewed-on: #10
Reviewed-by: Lars Neuhaus <larslukasneuhaus@gmx.de>
2026-02-02 19:09:44 +00:00
jannis 24d79d7a11 pull requestest changes 2026-02-02 20:04:55 +01:00
jannis 70fd5bafdb updated Block Battle description 2026-02-02 18:19:45 +00:00
jannis 11ddd01470 pull requestest changes 2026-02-02 19:15:46 +01:00
jannis 7d5fb025bd added setting, refactored code 2026-02-01 18:24:36 +01:00
jannis 05240660f2 added scores and translations 2026-02-01 17:37:37 +01:00
jannis d0031b4ea5 added BlockBattle 2026-02-01 14:14:34 +01:00
jannis 360266339d added pillars game 2026-01-23 20:26:04 +01:00
jannis 325fba2a53 Merge pull request 'added second option to game selection menu, finished stickfight(added timer to stick fight)' (#9) from develop-jannis into develop
Reviewed-on: #9
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2026-01-11 19:07:05 +00:00
jannis e760cdb2c6 removed 0 add option seconds 2026-01-10 21:41:45 +01:00
jannis c111c027ff added second option to game selection menu, finished stickfight 2026-01-10 17:41:19 +01:00
Pupsi 73ab137ae4 Merge remote-tracking branch 'origin/develop' into develop 2026-01-10 16:29:26 +01:00
Pupsi 9a9e646288 fixed Tetris camera shake 2026-01-10 16:29:12 +01:00
jannis 728dc92fc8 Merge pull request 'elytra race fix boost ring 1' (#8) from develop-jannis into develop
Reviewed-on: #8
2026-01-10 13:48:50 +00:00
jannis 2f8ff36e5e Merge branch 'develop' into develop-jannis 2026-01-10 13:48:39 +00:00
jannis 5cb71c5c32 changed respawn height on first respawn in elytrarace
added level indicator for stickfight
2026-01-10 14:47:57 +01:00
jannis 5ab42fde4b Merge pull request 'added stick to inventory for stick fight' (#7) from develop-stickfight into develop
Reviewed-on: #7
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2026-01-10 13:10:26 +00:00
jannis d5c2f06409 elytra race fix boost ring 1 2026-01-10 14:02:21 +01:00
jannis 1ff8cca7e9 added stick to inventory for stick fight 2026-01-10 00:41:24 +01:00
MineTec 47c40c4941 fixed winning condition in SpaceSnake 2026-01-07 15:06:12 +01:00
MineTec d4a7fcada7 improved chat formatting by adding username colors and better separators 2026-01-02 02:26:56 +01:00
Pupsi a0124f1bcb Merge remote-tracking branch 'origin/develop' into develop 2025-12-28 18:53:52 +01:00
Pupsi c79fbf3136 fixed tournament random order at the end 2025-12-28 18:53:14 +01:00
MineTec 660eb645e4 Merge remote-tracking branch 'origin/develop' into develop 2025-12-27 11:37:36 +01:00
MineTec 3c50aca1e8 updated reward API endpoint in PublishRewardCommand 2025-12-27 11:37:33 +01:00
Pupsi 865cdfa605 added gamemode change to survival on leaving tetris 2025-12-25 00:52:56 +01:00
Pupsi fe88e3f921 added slowness to tetris 2025-12-24 13:20:05 +01:00
MineTec 5e94479949 updated locales file with enhanced Stickfight description and added BoatRace entry 2025-12-21 10:42:23 +01:00
MineTec b15c9c97b0 finished boat race 2025-12-18 21:36:22 +01:00
MineTec 334e130cf6 fixed boat stuttering 2025-12-18 20:02:26 +01:00
MineTec 31385fbd7f cleanup 2025-12-13 15:15:23 +01:00
MineTec b1b1e24104 added BoatRace game with factory and updated GameList 2025-12-03 23:08:55 +01:00
Pupsi 294d4bde0d added dashes to allowed characters in Languages 2025-10-16 22:09:29 +02:00
MineTec 8f6950f9af configure IntelliJ project settings and add .gitignore for IDE and Gradle 2025-10-16 01:07:28 +02:00
MineTec 794dc1dbb1 reformatted project 2025-10-16 00:58:52 +02:00
Pupsi cf0499df44 Merge pull request 'develop-fairScoreboard' (#4) from develop-fairScoreboard into develop
Reviewed-on: #4
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2025-10-15 22:25:34 +00:00
323 changed files with 9534 additions and 2132 deletions
+146 -2
View File
@@ -1,3 +1,120 @@
# Created by https://www.toptal.com/developers/gitignore/api/gradle,intellij,java
# Edit at https://www.toptal.com/developers/gitignore?templates=gradle,intellij,java
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Intellij Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
### Java ###
# Compiled class file # Compiled class file
*.class *.class
@@ -23,8 +140,35 @@
hs_err_pid* hs_err_pid*
replay_pid* replay_pid*
.idea ### Gradle ###
.gradle .gradle
build/* **/build/
!src/**/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties
# Cache of project
.gradletasknamecache
# Eclipse Gradle plugin generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
### Gradle Patch ###
# Java heap dump
*.hprof
# End of https://www.toptal.com/developers/gitignore/api/gradle,intellij,java
/resources/ /resources/
/build/
/bin/
+3
View File
@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml
+16
View File
@@ -0,0 +1,16 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="JAVA">
<option name="SPACE_BEFORE_IF_PARENTHESES" value="false" />
<option name="SPACE_BEFORE_WHILE_PARENTHESES" value="false" />
<option name="SPACE_BEFORE_FOR_PARENTHESES" value="false" />
<option name="SPACE_BEFORE_TRY_PARENTHESES" value="false" />
<option name="SPACE_BEFORE_CATCH_PARENTHESES" value="false" />
<option name="SPACE_BEFORE_SWITCH_PARENTHESES" value="false" />
<option name="SPACE_BEFORE_SYNCHRONIZED_PARENTHESES" value="false" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</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>
+7
View File
@@ -0,0 +1,7 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="UnqualifiedFieldAccess" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="UnqualifiedMethodAccess" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven3" />
<option name="name" value="maven3" />
<option name="url" value="https://oss.sonatype.org/content/repositories/snapshots" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://repo.unnamed.team/repository/unnamed-public/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven4" />
<option name="name" value="maven4" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK" />
</project>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/modules/Minigames.main.iml" filepath="$PROJECT_DIR$/.idea/modules/Minigames.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/Minigames.test.iml" filepath="$PROJECT_DIR$/.idea/modules/Minigames.test.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/eu.mhsl.minenet.Minigames.main.iml" filepath="$PROJECT_DIR$/.idea/modules/eu.mhsl.minenet.Minigames.main.iml" />
</modules>
</component>
</project>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="FacetManager">
<facet type="minecraft" name="Minecraft">
<configuration>
<autoDetectTypes>
<platformType>ADVENTURE</platformType>
</autoDetectTypes>
<projectReimportVersion>1</projectReimportVersion>
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="FacetManager">
<facet type="minecraft" name="Minecraft">
<configuration>
<autoDetectTypes>
<platformType>ADVENTURE</platformType>
</autoDetectTypes>
<projectReimportVersion>1</projectReimportVersion>
</configuration>
</facet>
</component>
</module>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="FacetManager">
<facet type="minecraft" name="Minecraft">
<configuration>
<autoDetectTypes>
<platformType>ADVENTURE</platformType>
</autoDetectTypes>
<projectReimportVersion>1</projectReimportVersion>
</configuration>
</facet>
</component>
</module>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="FacetManager">
<facet type="minecraft" name="Minecraft">
<configuration>
<autoDetectTypes>
<platformType>ADVENTURE</platformType>
</autoDetectTypes>
<projectReimportVersion>1</projectReimportVersion>
</configuration>
</facet>
</component>
</module>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+17 -40
View File
@@ -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"
}
Binary file not shown.
+3 -1
View File
@@ -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
Vendored
+7 -11
View File
@@ -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
View File
@@ -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;
@@ -24,27 +24,45 @@ public class Main {
private final static Logger logger = Logger.getGlobal(); private final static Logger logger = Logger.getGlobal();
public static ConfigurationNode globalConfig; public static ConfigurationNode globalConfig;
public static void main(String[] args) throws ConfigurateException { @SuppressWarnings("ResultOfMethodCallIgnored")
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
globalConfig = YamlConfigurationLoader globalConfig = YamlConfigurationLoader
.builder() .builder()
.path(Resource.CONFIG.getPath()) .path(Resource.CONFIG.getPath())
.build() .build()
.load(); .load();
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();
@@ -56,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();
@@ -21,6 +21,7 @@ public enum Resource {
private final Path path; private final Path path;
private final String name; private final String name;
Resource(String name, boolean keepOutdated) { Resource(String name, boolean keepOutdated) {
this.name = name; this.name = name;
this.path = Path.of("resources/" + name); this.path = Path.of("resources/" + name);
@@ -28,7 +29,7 @@ public enum Resource {
try { try {
Logger.getLogger("ressource").info("extracting resource " + name + " ... "); Logger.getLogger("ressource").info("extracting resource " + name + " ... ");
ResourceUtils.extractResource(name, keepOutdated); ResourceUtils.extractResource(name, keepOutdated);
} catch (URISyntaxException | IOException e) { } catch(URISyntaxException | IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@@ -38,10 +39,10 @@ public enum Resource {
} }
public Path getPath() { public Path getPath() {
return path; return this.path;
} }
public String getName() { public String getName() {
return name; return this.name;
} }
} }
@@ -12,11 +12,11 @@ public abstract class Controller<Q, R> implements Route {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public Controller() { public Controller() {
this.requestType = ((Class<Q>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]); this.requestType = ((Class<Q>) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0]);
} }
@Override @Override
public Object handle(Request request, Response response) throws Exception { public Object handle(Request request, Response response) {
response.header("Access-Control-Allow-Origin", "*"); response.header("Access-Control-Allow-Origin", "*");
response.header("Access-Control-Allow-Methods", "*"); response.header("Access-Control-Allow-Methods", "*");
@@ -28,7 +28,7 @@ public abstract class Controller<Q, R> implements Route {
req = new Gson().fromJson(request.body(), this.requestType); req = new Gson().fromJson(request.body(), this.requestType);
} }
return new Gson().toJson(handle(req, response)); return new Gson().toJson(this.handle(req, response));
} }
public abstract R handle(Q request, Response response); public abstract R handle(Q request, Response response);
@@ -10,6 +10,7 @@ import static spark.Spark.*;
public class HttpServer { public class HttpServer {
private static final ConfigurationNode apiConfig = Main.globalConfig.node("api"); private static final ConfigurationNode apiConfig = Main.globalConfig.node("api");
public HttpServer() { public HttpServer() {
if(!apiConfig.node("enabled").getBoolean()) return; if(!apiConfig.node("enabled").getBoolean()) return;
@@ -6,8 +6,12 @@ import spark.Response;
import java.util.UUID; import java.util.UUID;
record Req(UUID room) {} record Req(UUID room) {
record Resp() {} }
record Resp() {
}
public class CloseRoom extends Controller<Req, Resp> { public class CloseRoom extends Controller<Req, Resp> {
@Override @Override
public Resp handle(Req request, Response response) { public Resp handle(Req request, Response response) {
@@ -6,8 +6,12 @@ import spark.Response;
import java.util.UUID; import java.util.UUID;
record Req() {} record Req() {
record Resp(UUID uuid) {} }
record Resp(UUID uuid) {
}
public class CreateRoom extends Controller<Req, Resp> { public class CreateRoom extends Controller<Req, Resp> {
@Override @Override
public Resp handle(Req request, Response response) { public Resp handle(Req request, Response response) {
@@ -8,8 +8,12 @@ import spark.Response;
import java.util.UUID; import java.util.UUID;
record Req(UUID player, UUID room) {} record Req(UUID player, UUID room) {
record Resp(String error) {} }
record Resp(String error) {
}
public class QueueRoom extends Controller<Req, Resp> { public class QueueRoom extends Controller<Req, Resp> {
@Override @Override
public Resp handle(Req request, Response response) { public Resp handle(Req request, Response response) {
@@ -1,9 +1,8 @@
package eu.mhsl.minenet.minigames.command; package eu.mhsl.minenet.minigames.command;
import eu.mhsl.minenet.minigames.command.privileged.SkinCommand;
import eu.mhsl.minenet.minigames.command.privileged.*;
import eu.mhsl.minenet.minigames.command.anonymous.HubCommand; import eu.mhsl.minenet.minigames.command.anonymous.HubCommand;
import eu.mhsl.minenet.minigames.command.anonymous.LeaveCommand; import eu.mhsl.minenet.minigames.command.anonymous.LeaveCommand;
import eu.mhsl.minenet.minigames.command.privileged.*;
import eu.mhsl.minenet.minigames.message.Icon; import eu.mhsl.minenet.minigames.message.Icon;
import eu.mhsl.minenet.minigames.message.type.ChatMessage; import eu.mhsl.minenet.minigames.message.type.ChatMessage;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
@@ -33,10 +32,6 @@ public enum Commands {
PLAYERLIMIT(new PlayerLimitCommand()), PLAYERLIMIT(new PlayerLimitCommand()),
SETMEMORIAL(new SetMemorialCommand()); SETMEMORIAL(new SetMemorialCommand());
Commands(Command handler) {
MinecraftServer.getCommandManager().register(handler);
}
static { static {
MinecraftServer.getCommandManager().setUnknownCommandCallback((sender, command) -> { MinecraftServer.getCommandManager().setUnknownCommandCallback((sender, command) -> {
if(command.isBlank()) return; if(command.isBlank()) return;
@@ -47,4 +42,8 @@ public enum Commands {
.send(sender); .send(sender);
}); });
} }
Commands(Command handler) {
MinecraftServer.getCommandManager().register(handler);
}
} }
@@ -11,20 +11,21 @@ import java.util.List;
public class PrivilegedCommand extends Command { public class PrivilegedCommand extends Command {
private final List<CommandCondition> conditions = new ArrayList<>(); private final List<CommandCondition> conditions = new ArrayList<>();
public PrivilegedCommand(@NotNull String name, @Nullable String... aliases) { public PrivilegedCommand(@NotNull String name, @Nullable String... aliases) {
super(name, aliases); super(name, aliases);
construct(); this.construct();
} }
public PrivilegedCommand(@NotNull String name) { public PrivilegedCommand(@NotNull String name) {
super(name); super(name);
construct(); this.construct();
} }
private void construct() { private void construct() {
addCondition(isPrivileged()); this.addCondition(this.isPrivileged());
setCondition((sender, commandString) -> conditions.parallelStream().allMatch(condition -> condition.canUse(sender, commandString))); this.setCondition((sender, commandString) -> this.conditions.parallelStream().allMatch(condition -> condition.canUse(sender, commandString)));
} }
protected CommandCondition isPrivileged() { protected CommandCondition isPrivileged() {
@@ -32,6 +33,6 @@ public class PrivilegedCommand extends Command {
} }
protected void addCondition(CommandCondition condition) { protected void addCondition(CommandCondition condition) {
conditions.add(condition); this.conditions.add(condition);
} }
} }
@@ -1,8 +1,8 @@
package eu.mhsl.minenet.minigames.command.anonymous; package eu.mhsl.minenet.minigames.command.anonymous;
import eu.mhsl.minenet.minigames.util.MoveInstance;
import eu.mhsl.minenet.minigames.instance.hub.Hub; import eu.mhsl.minenet.minigames.instance.hub.Hub;
import eu.mhsl.minenet.minigames.instance.room.Room; import eu.mhsl.minenet.minigames.instance.room.Room;
import eu.mhsl.minenet.minigames.util.MoveInstance;
import net.minestom.server.command.builder.Command; import net.minestom.server.command.builder.Command;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
@@ -10,12 +10,12 @@ public class HubCommand extends Command {
public HubCommand() { public HubCommand() {
super("hub"); super("hub");
setCondition( this.setCondition(
(sender, commandString) -> (sender, commandString) ->
((Player) sender).getInstance() instanceof Room room && !room.apiDriven ((Player) sender).getInstance() instanceof Room room && !room.apiDriven
); );
setDefaultExecutor((sender, context) -> { this.setDefaultExecutor((sender, context) -> {
if(Room.getRoom((Player) sender).orElseThrow().apiDriven) return; if(Room.getRoom((Player) sender).orElseThrow().apiDriven) return;
Room.unsetRoom((Player) sender); Room.unsetRoom((Player) sender);
MoveInstance.move((Player) sender, Hub.INSTANCE); MoveInstance.move((Player) sender, Hub.INSTANCE);
@@ -9,8 +9,8 @@ public class LeaveCommand extends Command {
public LeaveCommand() { public LeaveCommand() {
super("leave"); super("leave");
setCondition((sender, commandString) -> ((Player) sender).getInstance() instanceof Game); this.setCondition((sender, commandString) -> ((Player) sender).getInstance() instanceof Game);
setDefaultExecutor((sender, context) -> Room.setOwnRoom((Player) sender)); this.setDefaultExecutor((sender, context) -> Room.setOwnRoom((Player) sender));
} }
} }
@@ -14,26 +14,26 @@ public class DebugCommand extends PrivilegedCommand {
public DebugCommand() { public DebugCommand() {
super("debug"); super("debug");
setDefaultExecutor((sender, args) -> { this.setDefaultExecutor((sender, args) -> {
new ChatMessage(Icon.CHAT).appendTranslated("sample").send(sender); new ChatMessage(Icon.CHAT).appendTranslated("sample").send(sender);
new ActionBarMessage().appendTranslated("sample").send(sender); new ActionBarMessage().appendTranslated("sample").send(sender);
new TitleMessage().subtitle(subtitleMessage -> subtitleMessage.appendTranslated("sample")).appendTranslated("sample").send(sender); new TitleMessage().subtitle(subtitleMessage -> subtitleMessage.appendTranslated("sample")).appendTranslated("sample").send(sender);
List<String> testplayers = new ArrayList<>() { List<String> testplayers = new ArrayList<>() {
{ {
add("MineTec"); this.add("MineTec");
add("Goldi187"); this.add("Goldi187");
add("Test"); this.add("Test");
} }
}; };
new ChatMessage(Icon.STAR, true) new ChatMessage(Icon.STAR, true)
.appendTranslated("score#result") .appendTranslated("score#result")
.newLine() .newLine()
.indent() .indent()
.numberedList(testplayers) .numberedList(testplayers)
.undent() .undent()
.newLine() .newLine()
.appendTranslated("score#thanks") .appendTranslated("score#thanks")
.send(sender); .send(sender);
new ChatMessage(Icon.SCIENCE).appendStatic(((Player) sender).getUuid().toString()).send(sender); new ChatMessage(Icon.SCIENCE).appendStatic(((Player) sender).getUuid().toString()).send(sender);
}); });
@@ -8,7 +8,7 @@ public class FlyCommand extends PrivilegedCommand {
public FlyCommand() { public FlyCommand() {
super("fly"); super("fly");
setDefaultExecutor((sender, context) -> { this.setDefaultExecutor((sender, context) -> {
Player p = (Player) sender; Player p = (Player) sender;
p.setVelocity(new Vec(0, 5, 0)); p.setVelocity(new Vec(0, 5, 0));
p.setFlying(!p.isFlying()); p.setFlying(!p.isFlying());
@@ -7,11 +7,11 @@ import net.minestom.server.entity.Player;
public class GameStartCommand extends PrivilegedCommand { public class GameStartCommand extends PrivilegedCommand {
public GameStartCommand() { public GameStartCommand() {
super("gameStart"); super("gameStart");
setDefaultExecutor((sender, context) -> { this.setDefaultExecutor((sender, context) -> {
Player player = (Player) sender; Player player = (Player) sender;
if(player.getInstance() instanceof StatelessGame game) { if(player.getInstance() instanceof StatelessGame game) {
game.startAccessor(); game.startAccessor();
} }
}); });
} }
} }
@@ -7,7 +7,7 @@ import net.minestom.server.entity.Player;
public class GameStopCommand extends PrivilegedCommand { public class GameStopCommand extends PrivilegedCommand {
public GameStopCommand() { public GameStopCommand() {
super("gameStop"); super("gameStop");
setDefaultExecutor((sender, context) -> { this.setDefaultExecutor((sender, context) -> {
Player player = (Player) sender; Player player = (Player) sender;
if(player.getInstance() instanceof StatelessGame game) { if(player.getInstance() instanceof StatelessGame game) {
game.stop(); game.stop();
@@ -12,7 +12,7 @@ public class GameTimeoutCommand extends PrivilegedCommand {
ArgumentInteger timeout = ArgumentType.Integer("timeout"); ArgumentInteger timeout = ArgumentType.Integer("timeout");
addSyntax((sender, context) -> { this.addSyntax((sender, context) -> {
Player player = (Player) sender; Player player = (Player) sender;
if(player.getInstance() instanceof StatelessGame game) { if(player.getInstance() instanceof StatelessGame game) {
game.setTimeLimit(context.get(timeout)); game.setTimeLimit(context.get(timeout));
@@ -11,9 +11,9 @@ public class GamemodeCommand extends PrivilegedCommand {
super("gamemode", "gm"); super("gamemode", "gm");
addSyntax((sender, context) -> ((Player) sender).setGameMode( this.addSyntax((sender, context) -> ((Player) sender).setGameMode(
context.get("target")), context.get("target")),
ArgumentType.Enum("target", GameMode.class).setFormat(ArgumentEnum.Format.LOWER_CASED) ArgumentType.Enum("target", GameMode.class).setFormat(ArgumentEnum.Format.LOWER_CASED)
); );
} }
} }
@@ -1,17 +1,18 @@
package eu.mhsl.minenet.minigames.command.privileged; package eu.mhsl.minenet.minigames.command.privileged;
import eu.mhsl.minenet.minigames.command.PrivilegedCommand; import eu.mhsl.minenet.minigames.command.PrivilegedCommand;
import eu.mhsl.minenet.minigames.message.type.ChatMessage;
import eu.mhsl.minenet.minigames.message.Icon; import eu.mhsl.minenet.minigames.message.Icon;
import eu.mhsl.minenet.minigames.message.type.ChatMessage;
import eu.mhsl.minenet.minigames.util.Monitoring; import eu.mhsl.minenet.minigames.util.Monitoring;
public class GcCommand extends PrivilegedCommand { public class GcCommand extends PrivilegedCommand {
private static long lastRun = System.currentTimeMillis(); private static long lastRun = System.currentTimeMillis();
public GcCommand() { public GcCommand() {
super("gc"); super("gc");
setDefaultExecutor((sender, context) -> { this.setDefaultExecutor((sender, context) -> {
long nextRun = (lastRun - (System.currentTimeMillis() - 30*1000)) / 1000; long nextRun = (lastRun - (System.currentTimeMillis() - 30 * 1000)) / 1000;
if(nextRun > 0) { if(nextRun > 0) {
new ChatMessage(Icon.ERROR).appendStatic("Please wait ").appendStatic(String.valueOf(nextRun)).appendStatic(" seconds before running GC again!").send(sender); new ChatMessage(Icon.ERROR).appendStatic("Please wait ").appendStatic(String.valueOf(nextRun)).appendStatic(" seconds before running GC again!").send(sender);
return; return;
@@ -24,10 +25,10 @@ public class GcCommand extends PrivilegedCommand {
long after = Monitoring.getRamUsage(); long after = Monitoring.getRamUsage();
new ChatMessage(Icon.SUCCESS).appendStatic("Garbage collector ran successfully!").newLine() new ChatMessage(Icon.SUCCESS).appendStatic("Garbage collector ran successfully!").newLine()
.appendStatic("before: ").appendStatic(String.valueOf(before)).appendStatic("MB").newLine() .appendStatic("before: ").appendStatic(String.valueOf(before)).appendStatic("MB").newLine()
.appendStatic("now: ").appendStatic(String.valueOf(after)).appendStatic("MB").newLine() .appendStatic("now: ").appendStatic(String.valueOf(after)).appendStatic("MB").newLine()
.appendStatic("difference: ").appendStatic(String.valueOf(before-after)).appendStatic("MB") .appendStatic("difference: ").appendStatic(String.valueOf(before - after)).appendStatic("MB")
.send(sender); .send(sender);
}); });
} }
} }
@@ -13,7 +13,7 @@ public class InstanceProxyMoveCommand extends PrivilegedCommand {
ArgumentWord serverArgument = new ArgumentWord("server"); ArgumentWord serverArgument = new ArgumentWord("server");
addSyntax((sender, context) -> { this.addSyntax((sender, context) -> {
Instance room = ((Player) sender).getInstance(); Instance room = ((Player) sender).getInstance();
room.getPlayers().forEach(player -> { room.getPlayers().forEach(player -> {
Room.unsetRoom(player); Room.unsetRoom(player);
@@ -11,17 +11,17 @@ public class KickCommand extends PrivilegedCommand {
public KickCommand() { public KickCommand() {
super("kick"); super("kick");
addSyntax( this.addSyntax(
(sender, context) -> (sender, context) ->
kick(context.getRaw("player"), ""), this.kick(context.getRaw("player"), ""),
ArgumentType.Entity("player").onlyPlayers(true) ArgumentType.Entity("player").onlyPlayers(true)
); );
addSyntax( this.addSyntax(
(sender, context) -> (sender, context) ->
kick(context.getRaw("player"), context.getRaw("reason")), this.kick(context.getRaw("player"), context.getRaw("reason")),
ArgumentType.Entity("player").onlyPlayers(true), ArgumentType.Entity("player").onlyPlayers(true),
ArgumentType.String("reason") ArgumentType.String("reason")
); );
} }
@@ -1,8 +1,8 @@
package eu.mhsl.minenet.minigames.command.privileged; package eu.mhsl.minenet.minigames.command.privileged;
import eu.mhsl.minenet.minigames.command.PrivilegedCommand; import eu.mhsl.minenet.minigames.command.PrivilegedCommand;
import eu.mhsl.minenet.minigames.lang.Languages;
import eu.mhsl.minenet.minigames.lang.Lang; import eu.mhsl.minenet.minigames.lang.Lang;
import eu.mhsl.minenet.minigames.lang.Languages;
import eu.mhsl.minenet.minigames.message.Icon; import eu.mhsl.minenet.minigames.message.Icon;
import eu.mhsl.minenet.minigames.message.TranslatableMessage; import eu.mhsl.minenet.minigames.message.TranslatableMessage;
import eu.mhsl.minenet.minigames.message.type.ChatMessage; import eu.mhsl.minenet.minigames.message.type.ChatMessage;
@@ -13,11 +13,11 @@ public class LangTestCommand extends PrivilegedCommand {
public LangTestCommand() { public LangTestCommand() {
super("langtest"); super("langtest");
setDefaultExecutor((sender, context) -> sendMessage(Languages.getInstance().getLanguage((Player) sender), "sample").send(sender)); this.setDefaultExecutor((sender, context) -> this.sendMessage(Languages.getInstance().getLanguage((Player) sender), "sample").send(sender));
var targetString = ArgumentType.String("mapId"); var targetString = ArgumentType.String("mapId");
addSyntax((sender, context) -> sendMessage(Languages.getInstance().getLanguage((Player) sender), context.get("mapId")).send(sender), targetString); this.addSyntax((sender, context) -> this.sendMessage(Languages.getInstance().getLanguage((Player) sender), context.get("mapId")).send(sender), targetString);
} }
private TranslatableMessage sendMessage(Lang lang, String mapId) { private TranslatableMessage sendMessage(Lang lang, String mapId) {
@@ -11,7 +11,7 @@ public class OpCommand extends PrivilegedCommand {
public OpCommand() { public OpCommand() {
super("op"); super("op");
addSyntax((sender, context) -> { this.addSyntax((sender, context) -> {
Player target = MinecraftServer.getConnectionManager().getOnlinePlayerByUsername(context.getRaw("target")); Player target = MinecraftServer.getConnectionManager().getOnlinePlayerByUsername(context.getRaw("target"));
if(target != null) { if(target != null) {
target.setPermissionLevel(4); target.setPermissionLevel(4);
@@ -10,8 +10,9 @@ public class PlayerLimitCommand extends PrivilegedCommand {
ArgumentInteger count = ArgumentType.Integer("count"); ArgumentInteger count = ArgumentType.Integer("count");
addSyntax((sender, context) -> { this.addSyntax((sender, context) -> System.setProperty(
System.setProperty("minenet.playerlimit", String.valueOf(context.get(count))); "minenet.playerlimit",
}, count); String.valueOf(context.get(count))
), count);
} }
} }
@@ -13,10 +13,11 @@ import java.net.http.HttpResponse;
public class PublishRewardCommand extends PrivilegedCommand { public class PublishRewardCommand extends PrivilegedCommand {
private final HttpClient rewardPublishClient = HttpClient.newHttpClient(); private final HttpClient rewardPublishClient = HttpClient.newHttpClient();
public PublishRewardCommand() { public PublishRewardCommand() {
super("publishReward"); super("publishReward");
setDefaultExecutor((sender, context) -> { this.setDefaultExecutor((sender, context) -> {
try { try {
Room room = Room.getRoom((Player) sender).orElseThrow(); Room room = Room.getRoom((Player) sender).orElseThrow();
TournamentDisplay world = new TournamentDisplay(room.getTournament()); TournamentDisplay world = new TournamentDisplay(room.getTournament());
@@ -24,14 +25,14 @@ public class PublishRewardCommand extends PrivilegedCommand {
String rewardRequestJson = new Gson().toJson(room.getTournament().getRewards()); String rewardRequestJson = new Gson().toJson(room.getTournament().getRewards());
HttpRequest giveRewardsRequest = HttpRequest.newBuilder() HttpRequest giveRewardsRequest = HttpRequest.newBuilder()
.uri(new URI("http://10.20.7.1:8080/api/event/reward")) .uri(new URI("http://10.20.9.3:8080/api/event/reward"))
.POST(HttpRequest.BodyPublishers.ofString(rewardRequestJson)) .POST(HttpRequest.BodyPublishers.ofString(rewardRequestJson))
.build(); .build();
room.getTournament().getRewards(); room.getTournament().getRewards();
HttpResponse<Void> rawResponse = rewardPublishClient.send(giveRewardsRequest, HttpResponse.BodyHandlers.discarding()); HttpResponse<Void> rawResponse = this.rewardPublishClient.send(giveRewardsRequest, HttpResponse.BodyHandlers.discarding());
sender.sendMessage(String.format("Rewards published: HTTP %s", rawResponse.statusCode())); sender.sendMessage(String.format("Rewards published: HTTP %s", rawResponse.statusCode()));
} catch (Exception e) { } catch(Exception e) {
sender.sendMessage(e.getMessage()); sender.sendMessage(e.getMessage());
} }
}); });
@@ -1,8 +1,8 @@
package eu.mhsl.minenet.minigames.command.privileged; package eu.mhsl.minenet.minigames.command.privileged;
import eu.mhsl.minenet.minigames.command.PrivilegedCommand; import eu.mhsl.minenet.minigames.command.PrivilegedCommand;
import eu.mhsl.minenet.minigames.message.type.ChatMessage;
import eu.mhsl.minenet.minigames.message.Icon; import eu.mhsl.minenet.minigames.message.Icon;
import eu.mhsl.minenet.minigames.message.type.ChatMessage;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
@@ -10,7 +10,7 @@ public class RefreshCommandsCommand extends PrivilegedCommand {
public RefreshCommandsCommand() { public RefreshCommandsCommand() {
super("refreshCommands"); super("refreshCommands");
setDefaultExecutor((sender, context) -> { this.setDefaultExecutor((sender, context) -> {
MinecraftServer.getConnectionManager().getOnlinePlayers().forEach(Player::refreshCommands); MinecraftServer.getConnectionManager().getOnlinePlayers().forEach(Player::refreshCommands);
new ChatMessage(Icon.SUCCESS).appendStatic("Updated command syntax!").send(sender); new ChatMessage(Icon.SUCCESS).appendStatic("Updated command syntax!").send(sender);
}); });
@@ -1,10 +1,10 @@
package eu.mhsl.minenet.minigames.command.privileged; package eu.mhsl.minenet.minigames.command.privileged;
import eu.mhsl.minenet.minigames.command.PrivilegedCommand; import eu.mhsl.minenet.minigames.command.PrivilegedCommand;
import eu.mhsl.minenet.minigames.instance.room.Room;
import eu.mhsl.minenet.minigames.message.Icon; import eu.mhsl.minenet.minigames.message.Icon;
import eu.mhsl.minenet.minigames.message.TranslatableMessage; import eu.mhsl.minenet.minigames.message.TranslatableMessage;
import eu.mhsl.minenet.minigames.message.type.ChatMessage; import eu.mhsl.minenet.minigames.message.type.ChatMessage;
import eu.mhsl.minenet.minigames.instance.room.Room;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -13,14 +13,14 @@ public class RoomCommand extends PrivilegedCommand {
public RoomCommand() { public RoomCommand() {
super("room"); super("room");
setDefaultExecutor((sender, context) -> { this.setDefaultExecutor((sender, context) -> {
TranslatableMessage out = new ChatMessage(Icon.SCIENCE).appendStatic("Rooms:").newLine(); TranslatableMessage out = new ChatMessage(Icon.SCIENCE).appendStatic("Rooms:").newLine();
Room.getAllRooms().forEach((roomInstance) -> out Room.getAllRooms().forEach((roomInstance) -> out
.newLine() .newLine()
.appendStatic("Owner: ").appendStatic(roomInstance.getOwner() != null ? roomInstance.getOwner().getUsername() : "none").newLine() .appendStatic("Owner: ").appendStatic(roomInstance.getOwner() != null ? roomInstance.getOwner().getUsername() : "none").newLine()
.appendStatic("Players: ").appendStatic(String.valueOf(roomInstance.getAllMembers().size())).newLine() .appendStatic("Players: ").appendStatic(String.valueOf(roomInstance.getAllMembers().size())).newLine()
.list(roomInstance.getAllMembers().stream().map(Player::getUsername).collect(Collectors.toList())).newLine()); .list(roomInstance.getAllMembers().stream().map(Player::getUsername).collect(Collectors.toList())).newLine());
out.send(sender); out.send(sender);
}); });
@@ -20,37 +20,37 @@ public class SetMemorialCommand extends PrivilegedCommand {
ArgumentString titleArgument = ArgumentType.String("title"); ArgumentString titleArgument = ArgumentType.String("title");
ArgumentString loreArgument = ArgumentType.String("lore"); ArgumentString loreArgument = ArgumentType.String("lore");
materialArgument.setSuggestionCallback((sender, context, suggestion) -> { materialArgument.setSuggestionCallback(
Material (sender, context, suggestion) -> Material
.values() .values()
.stream() .stream()
.map(material -> new SuggestionEntry(material.name(), Component.text(material.name()))) .map(material -> new SuggestionEntry(material.name(), Component.text(material.name())))
.forEach(suggestion::addEntry); .forEach(suggestion::addEntry)
}); );
addSyntax((sender, context) -> { this.addSyntax((sender, context) -> {
Room Room
.getRoom((Player) sender) .getRoom((Player) sender)
.orElseThrow() .orElseThrow()
.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)
) )
); );
sender.sendMessage( sender.sendMessage(
Component.text() Component.text()
.append(Component.text("Memorial gesetzt:", NamedTextColor.GOLD)) .append(Component.text("Memorial gesetzt:", NamedTextColor.GOLD))
.appendNewline() .appendNewline()
.append(Component.text(context.get(materialArgument), NamedTextColor.AQUA)) .append(Component.text(context.get(materialArgument), NamedTextColor.AQUA))
.appendNewline() .appendNewline()
.append(Component.text(context.get(titleArgument), NamedTextColor.RED)) .append(Component.text(context.get(titleArgument), NamedTextColor.RED))
.appendNewline() .appendNewline()
.append(Component.text(context.get(loreArgument), NamedTextColor.DARK_RED)) .append(Component.text(context.get(loreArgument), NamedTextColor.DARK_RED))
.build() .build()
); );
}, materialArgument, titleArgument, loreArgument); }, materialArgument, titleArgument, loreArgument);
} }
@@ -22,34 +22,34 @@ public class SetRewardCommand extends PrivilegedCommand {
ArgumentWord materialArgument = ArgumentType.Word("material"); ArgumentWord materialArgument = ArgumentType.Word("material");
ArgumentStringArray amountsArgument = ArgumentType.StringArray("amount"); ArgumentStringArray amountsArgument = ArgumentType.StringArray("amount");
materialArgument.setSuggestionCallback((sender, context, suggestion) -> { materialArgument.setSuggestionCallback(
Material (sender, context, suggestion) -> Material
.values() .values()
.stream() .stream()
.map(material -> new SuggestionEntry(material.name(), Component.text(material.name()))) .map(material -> new SuggestionEntry(material.name(), Component.text(material.name())))
.forEach(suggestion::addEntry); .forEach(suggestion::addEntry)
}); );
addSyntax((sender, context) -> { this.addSyntax((sender, context) -> {
Room Room
.getRoom((Player) sender) .getRoom((Player) sender)
.orElseThrow() .orElseThrow()
.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())
) )
); );
sender.sendMessage( sender.sendMessage(
Component.text() Component.text()
.append(Component.text("Belohnung gesetzt:", NamedTextColor.GOLD)) .append(Component.text("Belohnung gesetzt:", NamedTextColor.GOLD))
.appendNewline() .appendNewline()
.append(Component.text(context.get(materialArgument), NamedTextColor.AQUA)) .append(Component.text(context.get(materialArgument), NamedTextColor.AQUA))
.appendNewline() .appendNewline()
.append(Component.text(String.join(", ", context.get(amountsArgument)), NamedTextColor.RED)) .append(Component.text(String.join(", ", context.get(amountsArgument)), NamedTextColor.RED))
.build() .build()
); );
}, materialArgument, amountsArgument); }, materialArgument, amountsArgument);
} }
@@ -15,16 +15,16 @@ public class SetRoomOwnerCommand extends PrivilegedCommand {
public SetRoomOwnerCommand() { public SetRoomOwnerCommand() {
super("setRoomOwner"); super("setRoomOwner");
addCondition((sender, commandString) -> ((Player) sender).getInstance() instanceof Room); this.addCondition((sender, commandString) -> ((Player) sender).getInstance() instanceof Room);
setDefaultExecutor((sender, context) -> { this.setDefaultExecutor((sender, context) -> {
if(sender instanceof Player p) { if(sender instanceof Player p) {
Room.getRoom(p).orElseThrow().setOwner(p); Room.getRoom(p).orElseThrow().setOwner(p);
new ChatMessage(Icon.SUCCESS).appendTranslated("room#ownerSelf").send(sender); new ChatMessage(Icon.SUCCESS).appendTranslated("room#ownerSelf").send(sender);
} }
}); });
addSyntax((sender, context) -> { this.addSyntax((sender, context) -> {
System.out.println("Test"); System.out.println("Test");
if(sender instanceof Player p) { if(sender instanceof Player p) {
Player newOwner = MinecraftServer.getConnectionManager().getOnlinePlayerByUsername(context.getRaw("player")); Player newOwner = MinecraftServer.getConnectionManager().getOnlinePlayerByUsername(context.getRaw("player"));
@@ -9,7 +9,7 @@ public class SkinCommand extends PrivilegedCommand {
public SkinCommand() { public SkinCommand() {
super("skin"); super("skin");
addSyntax((sender, context) -> { this.addSyntax((sender, context) -> {
if(sender instanceof Player p) { if(sender instanceof Player p) {
p.setSkin(PlayerSkin.fromUsername(context.getRaw("target"))); p.setSkin(PlayerSkin.fromUsername(context.getRaw("target")));
} }
@@ -1,6 +1,10 @@
package eu.mhsl.minenet.minigames.handler; package eu.mhsl.minenet.minigames.handler;
import eu.mhsl.minenet.minigames.handler.global.*; import eu.mhsl.minenet.minigames.handler.global.AddEntityToInstanceEventListener;
import eu.mhsl.minenet.minigames.handler.global.ChatFormatHandler;
import eu.mhsl.minenet.minigames.handler.global.PlayerFreezeHandler;
import eu.mhsl.minenet.minigames.handler.global.PlayerLeaveHandler;
import eu.mhsl.minenet.minigames.handler.global.PlayerLoginHandler;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.event.EventListener; import net.minestom.server.event.EventListener;
@@ -8,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) {
@@ -2,6 +2,8 @@ package eu.mhsl.minenet.minigames.handler.global;
import eu.mhsl.minenet.minigames.message.Icon; import eu.mhsl.minenet.minigames.message.Icon;
import eu.mhsl.minenet.minigames.message.type.ChatMessage; import eu.mhsl.minenet.minigames.message.type.ChatMessage;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.event.EventListener; import net.minestom.server.event.EventListener;
import net.minestom.server.event.player.PlayerChatEvent; import net.minestom.server.event.player.PlayerChatEvent;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -14,7 +16,14 @@ public class ChatFormatHandler implements EventListener<PlayerChatEvent> {
@Override @Override
public @NotNull Result run(@NotNull PlayerChatEvent event) { public @NotNull Result run(@NotNull PlayerChatEvent event) {
event.setFormattedMessage(new ChatMessage(Icon.CHAT).appendStatic(event.getRawMessage()).build(event.getPlayer())); NamedTextColor color = event.getPlayer().getPermissionLevel() > 0 ? NamedTextColor.AQUA : NamedTextColor.GRAY;
event.setFormattedMessage(
new ChatMessage(Icon.CHAT)
.appendStatic(Component.text(event.getPlayer().getUsername(), color))
.appendStatic(Component.text(" > ", NamedTextColor.DARK_GRAY))
.appendStatic(event.getRawMessage())
.build(event.getPlayer())
);
return Result.SUCCESS; return Result.SUCCESS;
} }
} }
@@ -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;
}
}
@@ -2,13 +2,13 @@ package eu.mhsl.minenet.minigames.handler.global;
import eu.mhsl.minenet.minigames.Main; import eu.mhsl.minenet.minigames.Main;
import eu.mhsl.minenet.minigames.api.QueuedPlayerRooms; import eu.mhsl.minenet.minigames.api.QueuedPlayerRooms;
import eu.mhsl.minenet.minigames.instance.hub.Hub;
import eu.mhsl.minenet.minigames.instance.room.Room; import eu.mhsl.minenet.minigames.instance.room.Room;
import eu.mhsl.minenet.minigames.instance.transfer.Transfer; import eu.mhsl.minenet.minigames.instance.transfer.Transfer;
import eu.mhsl.minenet.minigames.skin.SkinCache; import eu.mhsl.minenet.minigames.skin.SkinCache;
import eu.mhsl.minenet.minigames.util.MoveInstance; import eu.mhsl.minenet.minigames.util.MoveInstance;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import eu.mhsl.minenet.minigames.instance.hub.Hub;
import net.minestom.server.event.EventListener; import net.minestom.server.event.EventListener;
import net.minestom.server.event.player.AsyncPlayerConfigurationEvent; import net.minestom.server.event.player.AsyncPlayerConfigurationEvent;
import net.minestom.server.network.packet.server.play.TeamsPacket; import net.minestom.server.network.packet.server.play.TeamsPacket;
@@ -61,7 +61,8 @@ public class PlayerLoginHandler implements EventListener<AsyncPlayerConfiguratio
if(Objects.requireNonNull(Main.globalConfig.node("admins").getList(String.class)).stream().anyMatch(s -> s.equalsIgnoreCase(p.getUsername()))) { if(Objects.requireNonNull(Main.globalConfig.node("admins").getList(String.class)).stream().anyMatch(s -> s.equalsIgnoreCase(p.getUsername()))) {
p.setPermissionLevel(4); p.setPermissionLevel(4);
} }
} catch (SerializationException | NullPointerException ignored) {} } catch(SerializationException | NullPointerException ignored) {
}
Logger.getLogger("user").info(p.getUsername() + " joined"); Logger.getLogger("user").info(p.getUsername() + " joined");
@@ -1,48 +1,85 @@
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
.build() // AMBIENT_LIGHT_COLOR (Vanilla-Overworld: #0A0A0A); der alte ambientLight-2.0-Hack
// liegt ausserhalb der validen Range und bricht das Client-Rendering
.setAttribute(EnvironmentAttribute.AMBIENT_LIGHT_COLOR, NamedTextColor.WHITE)
// defaultClock noetig, damit Instance#setTime/#defaultClock funktionieren (sonst null); Timelines wie Vanilla-Overworld
.defaultClock(WorldClock.OVERWORLD)
.timelines(RegistryTag.direct(Timeline.DAY, Timeline.MOON, Timeline.EARLY_GAME, Timeline.VILLAGER_SCHEDULE))
.build()
), ),
NETHER( 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
.build() // liegt ausserhalb der validen Range und bricht das Client-Rendering
.setAttribute(EnvironmentAttribute.AMBIENT_LIGHT_COLOR, NamedTextColor.WHITE)
.skybox(DimensionType.Skybox.NONE)
.defaultClock(WorldClock.OVERWORLD)
.timelines(RegistryTag.direct(Timeline.VILLAGER_SCHEDULE))
.build()
), ),
THE_END( 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
.build() // liegt ausserhalb der validen Range und bricht das Client-Rendering
.setAttribute(EnvironmentAttribute.AMBIENT_LIGHT_COLOR, NamedTextColor.WHITE)
.skybox(DimensionType.Skybox.END)
.defaultClock(WorldClock.THE_END)
.timelines(RegistryTag.direct(Timeline.VILLAGER_SCHEDULE))
.build()
),
BACKROOMS(
Key.key("minenet:backrooms"),
DimensionType
.builder()
// Sehr dunkles Grundlicht (knapp ueber Vanilla-Overworld #0A0A0A): die eigentliche
// Helligkeit kommt aus echten Lichtquellen via LightingChunk — Blackout-Biome
// sind dadurch tatsaechlich dunkel, mit weichen Uebergaengen am Zonenrand
.setAttribute(EnvironmentAttribute.AMBIENT_LIGHT_COLOR, TextColor.color(0x16160F))
.skybox(DimensionType.Skybox.NONE)
.defaultClock(WorldClock.OVERWORLD)
.timelines(RegistryTag.direct(Timeline.VILLAGER_SCHEDULE))
.build()
); );
public final DimensionType DIMENSION; public final 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;
this.key = MinecraftServer.getDimensionTypeRegistry().register(namespaceID, DIMENSION); this.key = MinecraftServer.getDimensionTypeRegistry().register(namespaceID, this.DIMENSION);
} }
} }
@@ -1,27 +1,34 @@
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.instance.InstanceManager; import net.minestom.server.registry.RegistryKey;
import net.minestom.server.registry.DynamicRegistry;
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);
eventNode() this.eventNode()
.addListener(AddEntityToInstanceEvent.class, this::onEntityAdd) .addListener(AddEntityToInstanceEvent.class, this::onEntityAdd)
.addListener(RemoveEntityFromInstanceEvent.class, this::onEntityRemove); .addListener(RemoveEntityFromInstanceEvent.class, this::onEntityRemove);
} }
private void onEntityRemove(RemoveEntityFromInstanceEvent removeEntityFromInstanceEvent) { private void onEntityRemove(RemoveEntityFromInstanceEvent removeEntityFromInstanceEvent) {
@@ -38,6 +45,7 @@ public class MineNetInstance extends InstanceContainer {
/** /**
* Called when Player joins this instance * Called when Player joins this instance
*
* @param p player who is joining * @param p player who is joining
* @return setCanceled * @return setCanceled
*/ */
@@ -47,18 +55,15 @@ public class MineNetInstance extends InstanceContainer {
/** /**
* Called when Player leaves this instance * Called when Player leaves this instance
*
* @param p player who is leaving * @param p player who is leaving
*/ */
protected void onPlayerLeave(Player p) { protected void onPlayerLeave(Player p) {
} }
/**
*
* @param target
*/
public void destroy(Instance target) { public void destroy(Instance target) {
getPlayers().forEach(player -> { this.getPlayers().forEach(player -> {
if(target != null) if(target != null)
player.setInstance(target); player.setInstance(target);
else else
@@ -66,9 +71,9 @@ public class MineNetInstance extends InstanceContainer {
}); });
MinecraftServer.getSchedulerManager().scheduleTask( MinecraftServer.getSchedulerManager().scheduleTask(
() -> MinecraftServer.getInstanceManager().unregisterInstance(this), () -> MinecraftServer.getInstanceManager().unregisterInstance(this),
TaskSchedule.seconds(10), TaskSchedule.seconds(10),
TaskSchedule.stop() TaskSchedule.stop()
); );
} }
} }
@@ -1,14 +1,14 @@
package eu.mhsl.minenet.minigames.instance.game; package eu.mhsl.minenet.minigames.instance.game;
import eu.mhsl.minenet.minigames.instance.MineNetInstance; import eu.mhsl.minenet.minigames.instance.MineNetInstance;
import eu.mhsl.minenet.minigames.instance.Spawnable;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.config.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.room.Room;
import eu.mhsl.minenet.minigames.message.Icon; import eu.mhsl.minenet.minigames.message.Icon;
import eu.mhsl.minenet.minigames.message.type.ChatMessage; import eu.mhsl.minenet.minigames.message.type.ChatMessage;
import eu.mhsl.minenet.minigames.score.Score; import eu.mhsl.minenet.minigames.score.Score;
import eu.mhsl.minenet.minigames.util.CommonEventHandles; import eu.mhsl.minenet.minigames.util.CommonEventHandles;
import eu.mhsl.minenet.minigames.instance.Spawnable;
import eu.mhsl.minenet.minigames.instance.room.Room;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
@@ -16,7 +16,7 @@ import net.minestom.server.event.item.ItemDropEvent;
import net.minestom.server.event.player.PlayerBlockBreakEvent; import net.minestom.server.event.player.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;
@@ -28,34 +28,26 @@ import java.util.concurrent.CompletableFuture;
import java.util.logging.Logger; import java.util.logging.Logger;
public abstract class Game extends MineNetInstance implements Spawnable { public abstract class Game extends MineNetInstance implements Spawnable {
protected final Random rnd = new Random();
protected final Logger logger;
protected Room parentRoom; protected Room parentRoom;
protected boolean isRunning = false; protected boolean isRunning = false;
protected boolean isBeforeBeginning = true; protected boolean isBeforeBeginning = true;
protected final Random rnd = new Random(); //TODO better way than ths? public Game(RegistryKey<DimensionType> dimensionType) {
protected final Logger logger;
public Game(DynamicRegistry.Key<DimensionType> dimensionType) {
super(dimensionType); super(dimensionType);
MinecraftServer.getInstanceManager().registerInstance(this); MinecraftServer.getInstanceManager().registerInstance(this);
logger = Logger.getLogger("Game:" + getUuid()); this.logger = Logger.getLogger("Game:" + this.getUuid());
eventNode() this.eventNode()
.addListener(PlayerMoveEvent.class, this::onPlayerMove) .addListener(PlayerMoveEvent.class, this::onPlayerMove)
.addListener(PlayerBlockBreakEvent.class, this::onBlockBreak) .addListener(PlayerBlockBreakEvent.class, this::onBlockBreak)
.addListener(PlayerBlockPlaceEvent.class, this::onBlockPlace) .addListener(PlayerBlockPlaceEvent.class, this::onBlockPlace)
.addListener(ItemDropEvent.class, this::onItemDrop); .addListener(ItemDropEvent.class, this::onItemDrop);
} }
public Game setParent(Room parentRoom) {
this.parentRoom = parentRoom;
return this;
}
public static void initialize(GameFactory factory, List<Option<?>> options, Player owner) { public static void initialize(GameFactory factory, List<Option<?>> options, Player owner) {
try { try {
Room originRoom = Room.getRoom(owner).orElseThrow(); Room originRoom = Room.getRoom(owner).orElseThrow();
@@ -69,18 +61,23 @@ public abstract class Game extends MineNetInstance implements Spawnable {
.appendTranslated(factory.description()) .appendTranslated(factory.description())
.send(originRoom.getAllMembers()); .send(originRoom.getAllMembers());
} catch (Exception e) { } catch(Exception e) {
new ChatMessage(Icon.ERROR).appendStatic("Instance crashed: " + e.getMessage()).send(owner); new ChatMessage(Icon.ERROR).appendStatic("Instance crashed: " + e.getMessage()).send(owner);
MinecraftServer.getSchedulerManager().scheduleNextTick(() -> Room.getRoom(owner).orElseThrow().moveMembersToRoomLobby()); MinecraftServer.getSchedulerManager().scheduleNextTick(() -> Room.getRoom(owner).orElseThrow().moveMembersToRoomLobby());
e.printStackTrace(System.err); e.printStackTrace(System.err);
} }
} }
public Game setParent(Room parentRoom) {
this.parentRoom = parentRoom;
return this;
}
/** /**
* Load and start countdown * Load and start countdown
*/ */
public void load() { public void load() {
scheduler().submitTask(() -> { this.scheduler().submitTask(() -> {
CompletableFuture<Void> callback = new CompletableFuture<>(); CompletableFuture<Void> callback = new CompletableFuture<>();
this.onLoad(callback); this.onLoad(callback);
// callback.whenComplete((unused, throwable) -> this.start()); // callback.whenComplete((unused, throwable) -> this.start());
@@ -90,13 +87,13 @@ public abstract class Game extends MineNetInstance implements Spawnable {
} }
protected void start() { protected void start() {
isRunning = true; this.isRunning = true;
isBeforeBeginning = false; this.isBeforeBeginning = false;
this.onStart(); this.onStart();
} }
public void stop() { public void stop() {
isRunning = false; this.isRunning = false;
this.onStop(); this.onStop();
this.unload(); this.unload();
} }
@@ -104,12 +101,12 @@ public abstract class Game extends MineNetInstance implements Spawnable {
public void unload() { public void unload() {
this.onUnload(); this.onUnload();
getPlayers().forEach(Room::setOwnRoom); this.getPlayers().forEach(Room::setOwnRoom);
scheduler().scheduleTask(() -> { this.scheduler().scheduleTask(() -> {
logger.info("stopping game instance " + this.uuid); this.logger.info("stopping game instance " + this.uuid);
getPlayers().forEach(player -> player.kick("timeout")); this.getPlayers().forEach(player -> player.kick("timeout"));
MinecraftServer.getInstanceManager().unregisterInstance(this); MinecraftServer.getInstanceManager().unregisterInstance(this);
@@ -120,9 +117,14 @@ public abstract class Game extends MineNetInstance implements Spawnable {
callback.complete(null); callback.complete(null);
} }
protected void onStart() {} protected void onStart() {
protected void onStop() {} }
protected void onUnload() {}
protected void onStop() {
}
protected void onUnload() {
}
protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) {
@@ -146,7 +148,7 @@ public abstract class Game extends MineNetInstance implements Spawnable {
} }
protected void checkAbandoned() { protected void checkAbandoned() {
scheduleNextTick((instance) -> { this.scheduleNextTick((instance) -> {
if(instance.getPlayers().isEmpty()) this.unload(); if(instance.getPlayers().isEmpty()) this.unload();
}); });
} }
@@ -156,10 +158,10 @@ public abstract class Game extends MineNetInstance implements Spawnable {
} }
public boolean isRunning() { public boolean isRunning() {
return isRunning; return this.isRunning;
} }
public Pos getSpawn() { public Pos getSpawn() {
return new Pos(0,50,0); return new Pos(0, 50, 0);
} }
} }
@@ -1,23 +1,28 @@
package eu.mhsl.minenet.minigames.instance.game; package eu.mhsl.minenet.minigames.instance.game;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.config.GameFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.acidRain.AcidRainFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.anvilRun.AnvilRunFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.anvilRun.AnvilRunFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBreakRace.BlockBreakRaceFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.bowSpleef.BowSpleefFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.elytraRace.ElytraRaceFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms.BackroomsFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.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.acidRain.AcidRainFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBattle.BlockBattleFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.blockBreakRace.BlockBreakRaceFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.boatRace.BoatRaceFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.bowSpleef.BowSpleefFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.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.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;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.TetrisFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.sumo.SumoFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.sumo.SumoFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.tetris.TetrisFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.tntrun.TntRunFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.tntrun.TntRunFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.TowerdefenseFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.towerdefense.TowerdefenseFactory;
import eu.mhsl.minenet.minigames.instance.game.stateless.types.trafficlightrace.TrafficLightRaceFactory; import eu.mhsl.minenet.minigames.instance.game.stateless.types.trafficlightrace.TrafficLightRaceFactory;
@@ -30,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),
@@ -44,14 +49,21 @@ public enum GameList {
HIGHGROUND(new HighGroundFactory(), GameType.PVP), HIGHGROUND(new HighGroundFactory(), GameType.PVP),
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),
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;
GameList(GameFactory factory, GameType type) { GameList(GameFactory factory, GameType type) {
this.factory = factory; this.factory = factory;
this.type = type; this.type = type;
} }
public GameFactory getFactory() { public GameFactory getFactory() {
return this.factory; return this.factory;
} }
@@ -22,15 +22,15 @@ public enum GameType {
} }
public Material getIcon() { public Material getIcon() {
return icon; return this.icon;
} }
public TranslatedComponent getTitle() { public TranslatedComponent getTitle() {
return title; return this.title;
} }
public TranslatedComponent getDescription() { public TranslatedComponent getDescription() {
return description; return this.description;
} }
@@ -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;
@@ -37,7 +37,7 @@ public class StatelessGame extends Game {
} }
public String getName() { public String getName() {
return name; return this.name;
} }
public void setTimeLimit(int limit) { public void setTimeLimit(int limit) {
@@ -47,24 +47,23 @@ public class StatelessGame extends Game {
this.timePlayed = 0; this.timePlayed = 0;
} }
if(this.timeLimit > 0) { if(this.timeLimit > 0) {
this.timeLimitTask = scheduler().submitTask(() -> { this.timeLimitTask = this.scheduler().submitTask(() -> {
if(!isRunning || timeLimit == 0) return TaskSchedule.stop(); if(!this.isRunning || this.timeLimit == 0) return TaskSchedule.stop();
if(timeLimit <= timePlayed) { if(this.timeLimit <= this.timePlayed) {
stop(); this.stop();
return TaskSchedule.stop(); return TaskSchedule.stop();
} }
int timeLeft = timeLimit - timePlayed; int timeLeft = this.timeLimit - this.timePlayed;
switch (timeLeft) { switch(timeLeft) {
case 90, 60, 30, 10, 5, 4, 3, 2, 1 -> case 90, 60, 30, 10, 5, 4, 3, 2, 1 -> new ChatMessage(Icon.TIME)
new ChatMessage(Icon.TIME) .appendStatic(String.valueOf(timeLeft))
.appendStatic(String.valueOf(timeLeft)) .appendSpace()
.appendSpace() .appendTranslated(timeLeft == 1 ? "common#secondsLeft_singular" : "common#secondsLeft_plural")
.appendTranslated(timeLeft == 1 ? "common#secondsLeft_singular" : "common#secondsLeft_plural") .send(this.getPlayers());
.send(getPlayers());
} }
timePlayed++; this.timePlayed++;
return TaskSchedule.seconds(1); return TaskSchedule.seconds(1);
}, ExecutionType.TICK_START); }, ExecutionType.TICK_START);
@@ -84,7 +83,7 @@ public class StatelessGame extends Game {
Duration fadeIn = Duration.ofMillis(300); Duration fadeIn = Duration.ofMillis(300);
Duration stay = Duration.ofMillis(700); Duration stay = Duration.ofMillis(700);
return new Countdown(TitleMessage.class) return new Countdown()
.countdown( .countdown(
Audience.audience(this.getPlayers()), Audience.audience(this.getPlayers()),
5, 5,
@@ -102,26 +101,27 @@ public class StatelessGame extends Game {
public void startAccessor() { public void startAccessor() {
this.start(); this.start();
} }
@Override @Override
protected void start() { protected void start() {
score.setInstance(this); this.score.setInstance(this);
score.attachListeners(); this.score.attachListeners();
countdownStart().thenRun(super::start); this.countdownStart().thenRun(super::start);
} }
@Override @Override
public void stop() { public void stop() {
isRunning = false; this.isRunning = false;
this.onStop(); this.onStop();
getScore().insertRemainingPlayers(getPlayers()); this.getScore().insertRemainingPlayers(this.getPlayers());
this.publishScore(getScore()); this.publishScore(this.getScore());
countdownUnload(); this.countdownUnload();
} }
private void countdownUnload() { private void countdownUnload() {
new TitleMessage(Duration.ofSeconds(1)).appendTranslated("score#done").send(getPlayers()); new TitleMessage(Duration.ofSeconds(1)).appendTranslated("score#done").send(this.getPlayers());
scheduler().scheduleTask(this::unload, TaskSchedule.seconds(5), TaskSchedule.stop()); this.scheduler().scheduleTask(this::unload, TaskSchedule.seconds(5), TaskSchedule.stop());
} }
} }
@@ -5,12 +5,12 @@ import java.util.ArrayList;
public class ConfigManager { public class ConfigManager {
private final ArrayList<Option<?>> items = new ArrayList<>(); private final ArrayList<Option<?>> items = new ArrayList<>();
public ConfigManager addOption(Option option) { public ConfigManager addOption(Option<?> option) {
items.add(option); this.items.add(option);
return this; return this;
} }
public ArrayList<Option<?>> getAll() { public ArrayList<Option<?>> getAll() {
return items; return this.items;
} }
} }
@@ -4,20 +4,19 @@ import eu.mhsl.minenet.minigames.instance.game.Game;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.Restriction; import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.Restriction;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.RestrictionData; import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.RestrictionData;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.RestrictionHandler; import eu.mhsl.minenet.minigames.instance.game.stateless.config.restriction.RestrictionHandler;
import eu.mhsl.minenet.minigames.instance.room.Room;
import eu.mhsl.minenet.minigames.message.component.TranslatedComponent; import eu.mhsl.minenet.minigames.message.component.TranslatedComponent;
import eu.mhsl.minenet.minigames.shared.inventory.InteractableInventory; import eu.mhsl.minenet.minigames.shared.inventory.InteractableInventory;
import eu.mhsl.minenet.minigames.util.InventoryItemAlignment; import eu.mhsl.minenet.minigames.util.InventoryItemAlignment;
import eu.mhsl.minenet.minigames.instance.room.Room;
import net.kyori.adventure.sound.Sound; import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
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.EventListener;
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;
@@ -40,54 +39,54 @@ public class GameConfigurationInventory extends InteractableInventory {
this.factory = factory; this.factory = factory;
room.eventNode() room.eventNode()
.addListener(AddEntityToInstanceEvent.class, addEntityToInstanceEvent -> MinecraftServer.getSchedulerManager().scheduleNextTick(this::updatePlayButton)) .addListener(AddEntityToInstanceEvent.class, addEntityToInstanceEvent -> MinecraftServer.getSchedulerManager().scheduleNextTick(this::updatePlayButton))
.addListener(RemoveEntityFromInstanceEvent.class, removeEntityFromInstanceEvent -> MinecraftServer.getSchedulerManager().scheduleNextTick(this::updatePlayButton)); .addListener(RemoveEntityFromInstanceEvent.class, removeEntityFromInstanceEvent -> MinecraftServer.getSchedulerManager().scheduleNextTick(this::updatePlayButton));
ConfigManager config = factory.configuration(); ConfigManager config = factory.configuration();
setClickableItem( this.setClickableItem(
ItemStack.builder(Material.RED_WOOL) ItemStack.builder(Material.RED_WOOL)
.customName( .customName(
TranslatedComponent.byId("common#back") TranslatedComponent.byId("common#back")
.setColor(NamedTextColor.RED) .setColor(NamedTextColor.RED)
.getAssembled(p) .getAssembled(p)
) )
.build(), .build(),
0, 0,
itemClick -> itemClick.getPlayer().closeInventory(), itemClick -> itemClick.getPlayer().closeInventory(),
true true
); );
setDummyItem(Material.BLACK_STAINED_GLASS_PANE,1); this.setDummyItem(Material.BLACK_STAINED_GLASS_PANE, 1);
setDummyItem( this.setDummyItem(
ItemStack.builder(Material.NAME_TAG) ItemStack.builder(Material.NAME_TAG)
.customName( .customName(
factory.name().setColor(NamedTextColor.GOLD).getAssembled(p) factory.name().setColor(NamedTextColor.GOLD).getAssembled(p)
) )
.build(), .build(),
4 4
); );
setDummyItem(Material.BLACK_STAINED_GLASS_PANE,7); this.setDummyItem(Material.BLACK_STAINED_GLASS_PANE, 7);
updatePlayButton(); this.updatePlayButton();
for(int i = 9; i <= 17; i++) { for(int i = 9; i <= 17; i++) {
setDummyItem(Material.BLACK_STAINED_GLASS_PANE, i); this.setDummyItem(Material.BLACK_STAINED_GLASS_PANE, i);
} }
if(config == null) { if(config == null) {
setDummyItem( this.setDummyItem(
ItemStack.builder(Material.BARRIER) ItemStack.builder(Material.BARRIER)
.customName( .customName(
TranslatedComponent.byId("room#noOption").setColor(NamedTextColor.RED).getAssembled(p) TranslatedComponent.byId("room#noOption").setColor(NamedTextColor.RED).getAssembled(p)
) )
.lore( .lore(
TranslatedComponent.assemble("room#noOptionDescription", p) TranslatedComponent.assemble("room#noOptionDescription", p)
) )
.build(), .build(),
31 31
); );
} else { } else {
@@ -95,11 +94,11 @@ public class GameConfigurationInventory extends InteractableInventory {
InventoryItemAlignment itemAlignment = new InventoryItemAlignment(config.getAll().size(), 3); InventoryItemAlignment itemAlignment = new InventoryItemAlignment(config.getAll().size(), 3);
for(Option<?> item : config.getAll()) { for(Option<?> item : config.getAll()) {
InventoryItemAlignment.ItemOffset current = itemAlignment.next(); InventoryItemAlignment.ItemOffset current = itemAlignment.next();
map.put(offset + current.get(), item); this.map.put(offset + current.get(), item);
setDummyItem( this.setDummyItem(
item.getCurrent(p), item.getCurrent(p),
offset + current.get() offset + current.get()
); );
} }
} }
@@ -107,62 +106,62 @@ 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(!map.containsKey(slot)) return; if(!this.map.containsKey(slot)) return;
Option<?> item = map.get(slot); Option<?> item = this.map.get(slot);
setDummyItem( this.setDummyItem(
item.getNext(p), item.getNext(this.p),
slot slot
); );
update(); this.update();
} }
private void updatePlayButton() { private void updatePlayButton() {
RestrictionHandler restrictionHandler = factory.globalRestrictions(); RestrictionHandler restrictionHandler = this.factory.globalRestrictions();
RestrictionData restrictionData = new RestrictionData(room); RestrictionData restrictionData = new RestrictionData(this.room);
if(restrictionHandler.canPlay(restrictionData)) { if(restrictionHandler.canPlay(restrictionData)) {
setClickableItem( this.setClickableItem(
ItemStack.builder(restrictionHandler.getWarnings(restrictionData).size() > 0 ? Material.YELLOW_WOOL : Material.GREEN_WOOL) ItemStack.builder(!restrictionHandler.getWarnings(restrictionData).isEmpty() ? Material.YELLOW_WOOL : Material.GREEN_WOOL)
.customName(TranslatedComponent.byId("restriction#success").setColor(NamedTextColor.GREEN).getAssembled(p)) .customName(TranslatedComponent.byId("restriction#success").setColor(NamedTextColor.GREEN).getAssembled(this.p))
.lore(restrictionHandler.getWarnings(restrictionData).stream().map(translatedComponent -> translatedComponent.getAssembled(p)).collect(Collectors.toList())) .lore(restrictionHandler.getWarnings(restrictionData).stream().map(translatedComponent -> translatedComponent.getAssembled(this.p)).collect(Collectors.toList()))
.build(), .build(),
8, 8,
itemClick -> Game.initialize(factory, map.values().stream().toList(), itemClick.getPlayer()), itemClick -> Game.initialize(this.factory, this.map.values().stream().toList(), itemClick.getPlayer()),
true true
); );
} else { } else {
setClickableItem( this.setClickableItem(
ItemStack.builder(Material.RED_WOOL) ItemStack.builder(Material.RED_WOOL)
.customName(TranslatedComponent.byId("restriction#fail").setColor(NamedTextColor.RED).getAssembled(p)) .customName(TranslatedComponent.byId("restriction#fail").setColor(NamedTextColor.RED).getAssembled(this.p))
.lore( .lore(
restrictionHandler.getRestrictions() restrictionHandler.getRestrictions()
.stream() .stream()
.filter(iRestriction -> iRestriction.calculate(restrictionData).type().equals(Restriction.Type.FAIL)) .filter(iRestriction -> iRestriction.calculate(restrictionData).type().equals(Restriction.Type.FAIL))
.map(iRestriction -> iRestriction.calculate(restrictionData).description().getAssembled(p)) .map(iRestriction -> iRestriction.calculate(restrictionData).description().getAssembled(this.p))
.collect(Collectors.toList())) .collect(Collectors.toList()))
.build(), .build(),
8, 8,
itemClick -> { itemClick -> {
if(restrictionHandler.canPlay(restrictionData)) { if(restrictionHandler.canPlay(restrictionData)) {
updatePlayButton(); this.updatePlayButton();
return; return;
} }
itemClick.getPlayer().playSound(Sound.sound(SoundEvent.ENTITY_SILVERFISH_DEATH, Sound.Source.AMBIENT, 1f, 1f)); itemClick.getPlayer().playSound(Sound.sound(SoundEvent.ENTITY_SILVERFISH_DEATH, Sound.Source.AMBIENT, 1f, 1f));
}, },
false false
); );
} }
update(); this.update();
} }
} }
@@ -12,9 +12,11 @@ import java.util.Map;
public interface GameFactory { public interface GameFactory {
TranslatedComponent name(); TranslatedComponent name();
default ConfigManager configuration() { default ConfigManager configuration() {
return null; return null;
} }
default RestrictionHandler globalRestrictions() { default RestrictionHandler globalRestrictions() {
return new RestrictionHandler(); return new RestrictionHandler();
} }
@@ -22,6 +24,7 @@ public interface GameFactory {
default Material symbol() { default Material symbol() {
return Material.GRASS_BLOCK; return Material.GRASS_BLOCK;
} }
default TranslatedComponent description() { default TranslatedComponent description() {
return TranslatedComponent.byId("GameFactory#missingDescription"); return TranslatedComponent.byId("GameFactory#missingDescription");
} }
@@ -29,17 +32,17 @@ public interface GameFactory {
Game manufacture(Room parent, Map<String, Option<?>> configuration) throws Exception; Game manufacture(Room parent, Map<String, Option<?>> configuration) throws Exception;
default Game manufacture(Room parent, List<Option<?>> configuration) throws Exception { default Game manufacture(Room parent, List<Option<?>> configuration) throws Exception {
if(configuration == null) return manufacture(parent); if(configuration == null) return this.manufacture(parent);
Map<String, Option<?>> cnf = new HashMap<>(); Map<String, Option<?>> cnf = new HashMap<>();
configuration.forEach(option -> cnf.put(option.getId(), option)); configuration.forEach(option -> cnf.put(option.getId(), option));
return manufacture(parent, cnf); return this.manufacture(parent, cnf);
} }
default Game manufacture(Room parent) throws Exception { default Game manufacture(Room parent) throws Exception {
if(this.configuration() == null) return manufacture(parent, List.of()); if(this.configuration() == null) return this.manufacture(parent, List.of());
return manufacture(parent, this.configuration().getAll()); return this.manufacture(parent, this.configuration().getAll());
} }
} }
@@ -12,12 +12,12 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
public abstract class Option<T> { public abstract class Option<T> {
private RestrictionHandler restrictionHandler;
private final Material item; private final Material item;
private final TranslatedComponent name; private final TranslatedComponent name;
private final String id; private final String id;
protected T currentValue;
private final List<T> options; private final List<T> options;
protected T currentValue;
private RestrictionHandler restrictionHandler;
private int pointer = 0; private int pointer = 0;
public Option(String id, Material item, TranslatedComponent name, List<T> options) { public Option(String id, Material item, TranslatedComponent name, List<T> options) {
@@ -26,46 +26,45 @@ public abstract class Option<T> {
this.name = name; this.name = name;
this.options = options; this.options = options;
currentValue = options.getFirst(); this.currentValue = options.getFirst();
}
public RestrictionHandler getRestrictionHandler() {
return this.restrictionHandler;
} }
public void setRestrictionHandler(RestrictionHandler restrictionHandler) { public void setRestrictionHandler(RestrictionHandler restrictionHandler) {
this.restrictionHandler = restrictionHandler; this.restrictionHandler = restrictionHandler;
} }
public RestrictionHandler getRestrictionHandler() {
return restrictionHandler;
}
public ItemStack getNext(Player p) { public ItemStack getNext(Player p) {
if(++pointer >= options.size()) pointer = 0; if(++this.pointer >= this.options.size()) this.pointer = 0;
currentValue = options.get(pointer); this.currentValue = this.options.get(this.pointer);
return getCurrent(p); return this.getCurrent(p);
} }
public ItemStack getCurrent(Player p) { public ItemStack getCurrent(Player p) {
String value = options.get(pointer).toString(); String value = this.options.get(this.pointer).toString();
return ItemStack.builder(item) return ItemStack.builder(this.item)
.customName(name.getAssembled(p)) .customName(this.name.getAssembled(p))
.lore(TranslatedComponent.byId("optionCommon#value").setColor(NamedTextColor.GOLD).getAssembled(p) .lore(TranslatedComponent.byId("optionCommon#value").setColor(NamedTextColor.GOLD).getAssembled(p)
.append(Component.text(": ")).append(Component.text(value))) .append(Component.text(": ")).append(Component.text(value)))
.build(); .build();
} }
public int getAsInt() { public int getAsInt() {
return Integer.parseInt(getAsString()); return Integer.parseInt(this.getAsString());
} }
public boolean getAsBoolean() { public boolean getAsBoolean() {
return Objects.equals(getAsString(), "true") || Objects.equals(getAsString(), "1"); return Objects.equals(this.getAsString(), "true") || Objects.equals(this.getAsString(), "1");
} }
public String getAsString() { public String getAsString() {
return currentValue.toString(); return this.currentValue.toString();
} }
public String getId() { public String getId() {
return id; return this.id;
} }
} }
@@ -10,7 +10,7 @@ public record Restriction(TranslatedComponent name, TranslatedComponent descript
this.warnMessage = warnMessage; this.warnMessage = warnMessage;
this.type = type; this.type = type;
if (this.warnMessage == null && this.type.equals(Type.WARN)) { if(this.warnMessage == null && this.type.equals(Type.WARN)) {
throw new IllegalStateException("warnMessage cannot be null when RestrictionType is WARN"); throw new IllegalStateException("warnMessage cannot be null when RestrictionType is WARN");
} }
} }
@@ -22,18 +22,18 @@ public class RestrictionHandler {
} }
public List<IRestriction> getRestrictions() { public List<IRestriction> getRestrictions() {
return restrictions; return this.restrictions;
} }
public boolean canPlay(RestrictionData data) { public boolean canPlay(RestrictionData data) {
if(restrictions.size() < 1) return true; if(this.restrictions.isEmpty()) return true;
return this.restrictions.stream() return this.restrictions.stream()
.noneMatch(iRestriction -> iRestriction.calculate(data).type().equals(Restriction.Type.FAIL)); .noneMatch(iRestriction -> iRestriction.calculate(data).type().equals(Restriction.Type.FAIL));
} }
public List<TranslatedComponent> getWarnings(RestrictionData data) { public List<TranslatedComponent> getWarnings(RestrictionData data) {
List<TranslatedComponent> warnings = new ArrayList<>(); List<TranslatedComponent> warnings = new ArrayList<>();
for (IRestriction r : this.restrictions) { for(IRestriction r : this.restrictions) {
Restriction calculated = r.calculate(data); Restriction calculated = r.calculate(data);
if(!calculated.type().equals(Restriction.Type.WARN)) continue; if(!calculated.type().equals(Restriction.Type.WARN)) continue;
@@ -15,10 +15,10 @@ public class MaximalPlayeramountGameRestriction implements IRestriction {
@Override @Override
public Restriction calculate(RestrictionData data) { public Restriction calculate(RestrictionData data) {
return new Restriction( return new Restriction(
TranslatedComponent.byId("restriction#maxPlayersInRoom"), TranslatedComponent.byId("restriction#maxPlayersInRoom"),
TranslatedComponent.byId("restriction#maxPlayersInRoomDescription"), TranslatedComponent.byId("restriction#maxPlayersInRoomDescription"),
null, null,
data.room().getAllMembers().size() > this.max ? Restriction.Type.FAIL : Restriction.Type.OK data.room().getAllMembers().size() > this.max ? Restriction.Type.FAIL : Restriction.Type.OK
); );
} }
} }
@@ -15,10 +15,10 @@ public class MinimalPlayeramountGameRestriction implements IRestriction {
@Override @Override
public Restriction calculate(RestrictionData data) { public Restriction calculate(RestrictionData data) {
return new Restriction( return new Restriction(
TranslatedComponent.byId("restriction#minPlayersInRoom"), TranslatedComponent.byId("restriction#minPlayersInRoom"),
TranslatedComponent.byId("restriction#minPlayersInRoomDescription"), TranslatedComponent.byId("restriction#minPlayersInRoomDescription"),
null, null,
data.room().getAllMembers().size() < this.min ? Restriction.Type.FAIL : Restriction.Type.OK data.room().getAllMembers().size() < this.min ? Restriction.Type.FAIL : Restriction.Type.OK
); );
} }
} }
@@ -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;
@@ -27,26 +28,25 @@ import java.util.concurrent.CompletableFuture;
public class AcidRain extends StatelessGame { public class AcidRain extends StatelessGame {
final private int radius = 20;
private int generationOffset = 0;
final private int roofHeight = 55;
private int difficulty = 0;
final JNoise noise = JNoise.newBuilder() final JNoise noise = JNoise.newBuilder()
.fastSimplex() .fastSimplex(FastSimplexNoiseGenerator.newBuilder().setSeed(this.rnd.nextLong()).build())
.setSeed(rnd.nextLong()) .scale(0.09)
.setFrequency(0.09) .build();
.build(); final private int radius = 20;
final private int roofHeight = 55;
private int generationOffset = 0;
private int difficulty = 0;
public AcidRain() { public AcidRain() {
super(Dimension.OVERWORLD.key, "acidRain", new LastWinsScore()); super(Dimension.OVERWORLD.key, "acidRain", new LastWinsScore());
setGenerator( this.setGenerator(
new CircularPlateTerrainGenerator(radius) new CircularPlateTerrainGenerator(this.radius)
.setPlateHeight(50) .setPlateHeight(50)
.setPlatePallet(BlockPallet.STONE) .setPlatePallet(BlockPallet.STONE)
); );
eventNode().addListener(PlayerTickEvent.class, this::onPlayerTick); this.eventNode().addListener(PlayerTickEvent.class, this::onPlayerTick);
} }
@Override @Override
@@ -61,39 +61,40 @@ public class AcidRain extends StatelessGame {
MinecraftServer.getSchedulerManager().submitTask(() -> { MinecraftServer.getSchedulerManager().submitTask(() -> {
getPlayers().forEach(player -> { this.getPlayers().forEach(player -> {
player.sendPacket(new ParticlePacket(Particle.SNEEZE, 0, 60, 0, radius, radius, radius, 1f, 500)); player.sendPacket(new ParticlePacket(Particle.SNEEZE, 0, 60, 0, this.radius, this.radius, this.radius, 1f, 500));
player.sendPacket(new ParticlePacket(Particle.ITEM_SLIME, 0, 60, 0, radius, radius, radius, 1f, 500)); player.sendPacket(new ParticlePacket(Particle.ITEM_SLIME, 0, 60, 0, this.radius, this.radius, this.radius, 1f, 500));
}); });
if(!isRunning) return TaskSchedule.stop(); if(!this.isRunning) return TaskSchedule.stop();
return TaskSchedule.millis(100); return TaskSchedule.millis(100);
}, ExecutionType.TICK_END); }, ExecutionType.TICK_END);
MinecraftServer.getSchedulerManager().submitTask(() -> { MinecraftServer.getSchedulerManager().submitTask(() -> {
generationOffset++; this.generationOffset++;
generatePlatform(generationOffset); this.generatePlatform(this.generationOffset);
if(!isRunning) return TaskSchedule.stop(); if(!this.isRunning) return TaskSchedule.stop();
return TaskSchedule.millis((long) NumberUtil.map(50 - difficulty, 0, 50, 100, 1000)); return TaskSchedule.millis((long) NumberUtil.map(50 - this.difficulty, 0, 50, 100, 1000));
}, ExecutionType.TICK_END); }, ExecutionType.TICK_END);
MinecraftServer.getSchedulerManager().submitTask(() -> { MinecraftServer.getSchedulerManager().submitTask(() -> {
difficulty++; this.difficulty++;
if(difficulty >= 50) return TaskSchedule.stop(); if(this.difficulty >= 50) return TaskSchedule.stop();
return TaskSchedule.seconds(3); return TaskSchedule.seconds(3);
}); });
} }
@Override @Override
protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) {
if(playerMoveEvent.getNewPosition().y() < 50) playerDeath(playerMoveEvent.getPlayer()); if(playerMoveEvent.getNewPosition().y() < 50) this.playerDeath(playerMoveEvent.getPlayer());
} }
private void onPlayerTick(PlayerTickEvent playerTickEvent) { private void onPlayerTick(PlayerTickEvent playerTickEvent) {
if(isBeforeBeginning) return; if(this.isBeforeBeginning) return;
if(getBlock(playerTickEvent.getPlayer().getPosition().withY(roofHeight)).isAir()) playerDeath(playerTickEvent.getPlayer()); if(this.getBlock(playerTickEvent.getPlayer().getPosition().withY(this.roofHeight)).isAir())
this.playerDeath(playerTickEvent.getPlayer());
} }
private void generatePlatform(long offset) { private void generatePlatform(long offset) {
@@ -102,19 +103,20 @@ public class AcidRain extends StatelessGame {
AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); AbsoluteBlockBatch batch = new AbsoluteBlockBatch();
for(int x = -radius; x <= radius; x++) { for(int x = -radius; x <= radius; x++) {
for(int z = -radius; z <= radius; z++) { for(int z = -radius; z <= radius; z++) {
batch.setBlock(x, roofHeight, z, Block.AIR); batch.setBlock(x, this.roofHeight, z, Block.AIR);
if(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > radius) continue; if(new Pos(x, 0, z).distance(new Pos(0, 0, 0)) > radius) continue;
if(noise.getNoise(x + offset, z + offset) > 0.4) continue; if(this.noise.evaluateNoise(x + offset, z + offset) > 0.4) continue;
batch.setBlock(x, roofHeight, z, Block.OAK_PLANKS); batch.setBlock(x, this.roofHeight, z, Block.OAK_PLANKS);
} }
} }
BatchUtil.loadAndApplyBatch(batch, this, () -> {}); BatchUtil.loadAndApplyBatch(batch, this, () -> {
});
} }
private void playerDeath(Player p) { private void playerDeath(Player p) {
p.setGameMode(GameMode.SPECTATOR); p.setGameMode(GameMode.SPECTATOR);
getScore().insertResult(p); this.getScore().insertResult(p);
} }
@Override @Override
@@ -1,8 +1,8 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.anvilRun; package eu.mhsl.minenet.minigames.instance.game.stateless.types.anvilRun;
import eu.mhsl.minenet.minigames.instance.Dimension;
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame; import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import eu.mhsl.minenet.minigames.score.LastWinsScore; import eu.mhsl.minenet.minigames.score.LastWinsScore;
import eu.mhsl.minenet.minigames.instance.Dimension;
import eu.mhsl.minenet.minigames.util.BatchUtil; import eu.mhsl.minenet.minigames.util.BatchUtil;
import eu.mhsl.minenet.minigames.world.generator.terrain.CircularPlateTerrainGenerator; import eu.mhsl.minenet.minigames.world.generator.terrain.CircularPlateTerrainGenerator;
import net.minestom.server.MinecraftServer; import net.minestom.server.MinecraftServer;
@@ -27,10 +27,10 @@ class AnvilRun extends StatelessGame {
final int spawnHeight = 30; final int spawnHeight = 30;
final int radius; final int radius;
int anvilsPerSecond;
final int seconds; final int seconds;
final int anvilHeight = 200; final int anvilHeight = 200;
final List<Pos> anvilSpawnPositions = new ArrayList<>(); final List<Pos> anvilSpawnPositions = new ArrayList<>();
int anvilsPerSecond;
public AnvilRun(int radius, int seconds) { public AnvilRun(int radius, int seconds) {
super(Dimension.OVERWORLD.key, "Anvil Run", new LastWinsScore()); super(Dimension.OVERWORLD.key, "Anvil Run", new LastWinsScore());
@@ -38,25 +38,25 @@ class AnvilRun extends StatelessGame {
this.seconds = seconds; this.seconds = seconds;
this.setGenerator(new CircularPlateTerrainGenerator(radius)); this.setGenerator(new CircularPlateTerrainGenerator(radius));
eventNode().addListener(EntityTickEvent.class, this::onEntityTick); this.eventNode().addListener(EntityTickEvent.class, this::onEntityTick);
} }
@Override @Override
protected void onLoad(@NotNull CompletableFuture<Void> callback) { protected void onLoad(@NotNull CompletableFuture<Void> callback) {
AbsoluteBlockBatch batch = new AbsoluteBlockBatch(); AbsoluteBlockBatch batch = new AbsoluteBlockBatch();
for(int x = -radius; x <= radius; x++) { for(int x = -this.radius; x <= this.radius; x++) {
for (int z = -radius; z <= radius; z++) { for(int z = -this.radius; z <= this.radius; z++) {
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)) > this.radius) continue;
anvilSpawnPositions.add(new Pos(x+0.5, anvilHeight, z+0.5)); this.anvilSpawnPositions.add(new Pos(x + 0.5, this.anvilHeight, z + 0.5));
batch.setBlock(x, spawnHeight-1, z, Block.SNOW_BLOCK); batch.setBlock(x, this.spawnHeight - 1, z, Block.SNOW_BLOCK);
} }
} }
this.anvilsPerSecond = anvilSpawnPositions.size() / this.seconds; this.anvilsPerSecond = this.anvilSpawnPositions.size() / this.seconds;
Collections.shuffle(anvilSpawnPositions); Collections.shuffle(this.anvilSpawnPositions);
BatchUtil.loadAndApplyBatch(batch, this, () -> callback.complete(null)); BatchUtil.loadAndApplyBatch(batch, this, () -> callback.complete(null));
} }
@@ -72,12 +72,12 @@ class AnvilRun extends StatelessGame {
super.onStart(); super.onStart();
Scheduler scheduler = MinecraftServer.getSchedulerManager(); Scheduler scheduler = MinecraftServer.getSchedulerManager();
for(int i=0; i<this.anvilSpawnPositions.size(); i++) { for(int i = 0; i < this.anvilSpawnPositions.size(); i++) {
final int j = i; final int j = i;
scheduler.scheduleTask( scheduler.scheduleTask(
() -> spawnAnvil(this.anvilSpawnPositions.get(j)), () -> this.spawnAnvil(this.anvilSpawnPositions.get(j)),
TaskSchedule.millis( TaskSchedule.millis(
(long) Math.floor((double) i/this.anvilsPerSecond * 1000) (long) Math.floor((double) i / this.anvilsPerSecond * 1000)
), ),
TaskSchedule.stop() TaskSchedule.stop()
); );
@@ -87,25 +87,26 @@ class AnvilRun extends StatelessGame {
@Override @Override
protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) { protected void onPlayerMove(@NotNull PlayerMoveEvent playerMoveEvent) {
super.onPlayerMove(playerMoveEvent); super.onPlayerMove(playerMoveEvent);
if(isBeforeBeginning && playerMoveEvent.getNewPosition().y() < spawnHeight - 2) { if(this.isBeforeBeginning && playerMoveEvent.getNewPosition().y() < this.spawnHeight - 2) {
playerMoveEvent.setCancelled(true); playerMoveEvent.setCancelled(true);
playerMoveEvent.getPlayer().teleport(getSpawn()); playerMoveEvent.getPlayer().teleport(this.getSpawn());
return; return;
} }
if(playerMoveEvent.getNewPosition().y() < spawnHeight - 2) getScore().insertResult(playerMoveEvent.getPlayer()); if(playerMoveEvent.getNewPosition().y() < this.spawnHeight - 2)
this.getScore().insertResult(playerMoveEvent.getPlayer());
} }
protected void onEntityTick(@NotNull EntityTickEvent entityTickEvent) { protected void onEntityTick(@NotNull EntityTickEvent entityTickEvent) {
if(!entityTickEvent.getEntity().getEntityType().equals(EntityType.FALLING_BLOCK)) return; if(!entityTickEvent.getEntity().getEntityType().equals(EntityType.FALLING_BLOCK)) return;
Pos anvilPosition = entityTickEvent.getEntity().getPosition(); Pos anvilPosition = entityTickEvent.getEntity().getPosition();
if(anvilPosition.y() > spawnHeight + 0.5) return; if(anvilPosition.y() > this.spawnHeight + 0.5) return;
if(anvilPosition.y() < spawnHeight - 3) entityTickEvent.getEntity().remove(); if(anvilPosition.y() < this.spawnHeight - 3) entityTickEvent.getEntity().remove();
if(this.getBlock(anvilPosition.withY(spawnHeight-1)).isAir()) return; if(this.getBlock(anvilPosition.withY(this.spawnHeight - 1)).isAir()) return;
this.setBlock(anvilPosition.withY(spawnHeight-1), Block.AIR); this.setBlock(anvilPosition.withY(this.spawnHeight - 1), Block.AIR);
} }
@Override @Override
public Pos getSpawn() { public Pos getSpawn() {
return new Pos(0, spawnHeight, 0); return new Pos(0, this.spawnHeight, 0);
} }
} }
@@ -1,9 +1,9 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.anvilRun; package eu.mhsl.minenet.minigames.instance.game.stateless.types.anvilRun;
import eu.mhsl.minenet.minigames.instance.game.Game; import eu.mhsl.minenet.minigames.instance.game.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.ConfigManager;
import eu.mhsl.minenet.minigames.instance.game.stateless.config.common.NumericOption; 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;
@@ -1,20 +0,0 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms;
import eu.mhsl.minenet.minigames.instance.Dimension;
import eu.mhsl.minenet.minigames.instance.game.stateless.StatelessGame;
import eu.mhsl.minenet.minigames.score.NoScore;
import net.minestom.server.event.player.PlayerBlockBreakEvent;
import org.jetbrains.annotations.NotNull;
public class Backrooms extends StatelessGame {
public Backrooms() {
super(Dimension.NETHER.key, "Backrooms", new NoScore());
BackroomsGenerator generator = new BackroomsGenerator();
setGenerator(unit -> generator.generateRoom(unit, 50));
}
@Override
protected void onBlockBreak(@NotNull PlayerBlockBreakEvent playerBlockBreakEvent) {
playerBlockBreakEvent.setCancelled(false);
}
}
@@ -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);
} }
} }
@@ -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();
}
}
@@ -1,86 +0,0 @@
package eu.mhsl.minenet.minigames.instance.game.stateless.types.backrooms;
import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.generator.GenerationUnit;
import net.minestom.server.instance.generator.UnitModifier;
import java.util.concurrent.ThreadLocalRandom;
public class BackroomsGenerator {
public void generateRoom(GenerationUnit unit, int y) {
var modifier = unit.modifier();
var start = unit.absoluteStart();
var yPos = start.blockY() + y;
for (int x = 0; x < unit.size().blockX(); x++) {
for (int z = 0; z < unit.size().blockZ(); z++) {
var xPos = start.blockX() + x;
var zPos = start.blockZ() + z;
modifier.setBlock(xPos, yPos - 1, zPos, Block.BEDROCK);
modifier.setBlock(xPos, yPos + 5, zPos, Block.BEDROCK);
modifier.setBlock(xPos, yPos, zPos, Block.LIGHT_GRAY_WOOL);
modifier.setBlock(xPos, yPos + 4, zPos, Block.SMOOTH_STONE_SLAB);
// idk man... dont ask about this
if (
(x - 2) % 4 == 0 && (
((z - 2) % 8 == 0) ||
((z - 2) % 8 == 1) ||
((z - 2) % 8 == 3) ||
((z - 2) % 8 == 4)
)
) {
modifier.setBlock(xPos, yPos + 4, zPos, Block.SEA_LANTERN);
}
}
}
generateWalls(modifier, start.blockX() + 0, yPos + 1, start.blockZ() + 0);
generateWalls(modifier, start.blockX() + 8, yPos + 1, start.blockZ() + 0);
generateWalls(modifier, start.blockX() + 0, yPos + 1, start.blockZ() + 8);
generateWalls(modifier, start.blockX() + 8, yPos + 1, start.blockZ() + 8);
}
private void generateWalls(UnitModifier modifier, int xPos, int yPos, int zPos) {
generatePillar(modifier, xPos, yPos, zPos, Block.CHISELED_SANDSTONE);
var random = ThreadLocalRandom.current();
var wall1 = random.nextInt(4) != 0;
var wall2 = random.nextInt(4) != 0;
var door1 = random.nextInt(2) != 0;
var door2 = random.nextInt(2) != 0;
var door1pos = random.nextInt(2, 6);
var door2pos = random.nextInt(2, 6);
if (wall1) {
for (int x = xPos; x < xPos + 8; x++) {
generatePillar(modifier, x, yPos, zPos, Block.SMOOTH_SANDSTONE);
}
}
if (wall2) {
for (int z = zPos; z < zPos + 8; z++) {
generatePillar(modifier, xPos, yPos, z, Block.SMOOTH_SANDSTONE);
}
}
if (door1 && wall1) {
generatePillar(modifier, xPos + door1pos, yPos, zPos, Block.AIR);
generatePillar(modifier, xPos + door1pos + 1, yPos, zPos, Block.AIR);
}
if (door2 && wall2) {
generatePillar(modifier, xPos, yPos, zPos + door2pos, Block.AIR);
generatePillar(modifier, xPos, yPos, zPos + door2pos + 1, Block.AIR);
}
}
private void generatePillar(UnitModifier modifier, int xPos, int yPos, int zPos, Block material) {
for (int y = yPos; y < yPos + 3; y++) {
modifier.setBlock(xPos, y, zPos, material);
}
}
}
@@ -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;
}
}
@@ -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 &gt;= 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");
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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()
));
}
}
}
@@ -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
) {
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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));
}
}
@@ -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};
}
}
@@ -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()));
}
}
@@ -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()));
}
}
@@ -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));
}
}
@@ -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;
}
}
@@ -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
}
@@ -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 (&gt; 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]));
}
}
@@ -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
);
}
}
@@ -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
);
}
}
@@ -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
);
}
}
@@ -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
);
}
}
@@ -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
);
}
}
@@ -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
);
}
}
@@ -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
);
}
}
@@ -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)));
}
}

Some files were not shown because too many files have changed in this diff Show More