From b9a3f4f0dfd066706aa2d8ca3edd327f93592029 Mon Sep 17 00:00:00 2001 From: Patrick <147879351+WinniePatGG@users.noreply.github.com> Date: Fri, 1 May 2026 18:43:38 +0200 Subject: [PATCH] init commit --- .gitignore | 5 + README.md | 57 + build.gradle | 57 + gradle.properties | 0 gradlew | 249 ++++ gradlew.bat | 92 ++ settings.gradle | 1 + .../Pierredufaulersack.java | 73 + .../commands/BossHuntCommand.java | 167 +++ .../commands/KitCommand.java | 126 ++ .../pierredufaulersack/game/GameManager.java | 1236 +++++++++++++++++ .../pierredufaulersack/game/GameState.java | 7 + .../game/LuckPermsService.java | 94 ++ .../pierredufaulersack/game/Role.java | 7 + .../game/SidebarService.java | 150 ++ .../pierredufaulersack/kits/KitService.java | 346 +++++ .../pierredufaulersack/kits/KitType.java | 17 + .../listeners/CobwebDecayListener.java | 59 + .../listeners/FriendlyFireListener.java | 44 + .../listeners/PlayerConnectionListener.java | 36 + .../listeners/PlayerDeathListener.java | 21 + .../listeners/PlayerJoinListener.java | 757 ++++++++++ .../listeners/RoundOutcomeListener.java | 38 + .../listeners/RoundWaterCleanupListener.java | 35 + src/main/resources/config.yml | 66 + src/main/resources/plugin.yml | 26 + 26 files changed, 3766 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/de/winniepat/pierredufaulersack/Pierredufaulersack.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/commands/BossHuntCommand.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/commands/KitCommand.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/game/GameManager.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/game/GameState.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/game/LuckPermsService.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/game/Role.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/game/SidebarService.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/kits/KitService.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/kits/KitType.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/listeners/CobwebDecayListener.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/listeners/FriendlyFireListener.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/listeners/PlayerConnectionListener.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/listeners/PlayerDeathListener.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/listeners/PlayerJoinListener.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/listeners/RoundOutcomeListener.java create mode 100644 src/main/java/de/winniepat/pierredufaulersack/listeners/RoundWaterCleanupListener.java create mode 100644 src/main/resources/config.yml create mode 100644 src/main/resources/plugin.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f9436c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.gradle +.idea +build +gradle +run \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dfc6d72 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Pierredufaulersack - Boss Hunt Plugin + +Paper plugin for a configurable Boss Hunt mode (1 boss vs hunters). + +## Features + +- Lobby flow with `/bosshunt join` and `/bosshunt leave` +- Colored, prefixed chat messages for game + kit commands +- Configurable start countdown with per-second and final beep sounds +- Admin controls: + - `/bosshunt setlobby` + - `/bosshunt setarena` + - `/bosshunt start` + - `/bosshunt stop` +- Kit management for 3 boss kits and 3 hunter kits: + - `/bosskit add|set|del|list <1-3>` + - `/hunterkit add|set|del|list <1-3>` +- Saved kits preserve item metadata from inventory, including enchantments +- Weighted random kit selection from `config.yml` (default `40/40/20`) +- Hunter kit duplication limit per round (`hunter-kit-max-duplicates`, default `2`) +- Friendly fire protection for hunter vs hunter + +## Setup + +1. Build plugin jar. +2. Put jar into your Paper server `plugins/` folder. +3. Start server once to generate `config.yml`. +4. In game as OP/admin: + - set lobby and arena + - save kits in slots 1-3 for boss and hunter + - players join lobby + - start round + +## Permissions + +- `pierredufaulersack.admin` (default: op) + +## Build + +```powershell +.\gradlew.bat build +``` + +## Quick test commands (in game) + +```text +/bosshunt setlobby +/bosshunt setarena +/bosskit set 1 +/bosskit set 2 +/bosskit set 3 +/hunterkit set 1 +/hunterkit set 2 +/hunterkit set 3 +/bosshunt join +/bosshunt start +``` diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..bd42b1f --- /dev/null +++ b/build.gradle @@ -0,0 +1,57 @@ +plugins { + id 'java' + id("xyz.jpenilla.run-paper") version "2.3.1" +} + +group = 'de.winniepat' +version = '1.17-MeinKraft' + +repositories { + mavenCentral() + maven { + name = "papermc-repo" + url = "https://repo.papermc.io/repository/maven-public/" + } + maven { + name = "luckperms-repo" + url = "https://repo.lucko.me/" + } +} + +dependencies { + compileOnly("io.papermc.paper:paper-api:1.21.10-R0.1-SNAPSHOT") + compileOnly("net.luckperms:api:5.4") +} + +tasks { + runServer { + minecraftVersion("1.21.10") + } +} + +def targetJavaVersion = 21 +java { + def javaVersion = JavaVersion.toVersion(targetJavaVersion) + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + if (JavaVersion.current() < javaVersion) { + toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { + options.release.set(targetJavaVersion) + } +} + +processResources { + def props = [version: version] + inputs.properties props + filteringCharset 'UTF-8' + filesMatching('plugin.yml') { + expand props + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e69de29 diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..b740cf1 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..3330a2c --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'Pierredufaulersack' diff --git a/src/main/java/de/winniepat/pierredufaulersack/Pierredufaulersack.java b/src/main/java/de/winniepat/pierredufaulersack/Pierredufaulersack.java new file mode 100644 index 0000000..fdda57d --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/Pierredufaulersack.java @@ -0,0 +1,73 @@ +package de.winniepat.pierredufaulersack; + +import de.winniepat.pierredufaulersack.commands.*; +import de.winniepat.pierredufaulersack.game.*; +import de.winniepat.pierredufaulersack.kits.*; +import de.winniepat.pierredufaulersack.listeners.*; +import org.bukkit.command.PluginCommand; +import org.bukkit.plugin.java.JavaPlugin; + +public final class Pierredufaulersack extends JavaPlugin { + + private KitService kitService; + private GameManager gameManager; + private SidebarService sidebarService; + private LuckPermsService luckPermsService; + + @Override + public void onEnable() { + saveDefaultConfig(); + + this.kitService = new KitService(this); + this.luckPermsService = new LuckPermsService(this); + this.gameManager = new GameManager(this, kitService, luckPermsService); + this.sidebarService = new SidebarService(this, gameManager); + + registerCommands(); + getServer().getPluginManager().registerEvents(new FriendlyFireListener(gameManager), this); + getServer().getPluginManager().registerEvents(new PlayerConnectionListener(gameManager, sidebarService), this); + getServer().getPluginManager().registerEvents(new RoundOutcomeListener(gameManager), this); + getServer().getPluginManager().registerEvents(new RoundWaterCleanupListener(gameManager), this); + getServer().getPluginManager().registerEvents(new PlayerJoinListener(this, gameManager, kitService), this); + getServer().getPluginManager().registerEvents(new CobwebDecayListener(this, gameManager), this); + getServer().getPluginManager().registerEvents(new PlayerDeathListener(), this); + + sidebarService.start(); + getLogger().info("Pierredufaulersack enabled."); + } + + @Override + public void onDisable() { + if (sidebarService != null) { + sidebarService.shutdown(); + } + if (gameManager != null) { + gameManager.shutdown(); + gameManager.stopRound(true); + } + } + + private void registerCommands() { + PluginCommand bossHunt = getCommand("bosshunt"); + PluginCommand bossKit = getCommand("bosskit"); + PluginCommand hunterKit = getCommand("hunterkit"); + + if (bossHunt != null) { + BossHuntCommand bossHuntCommand = new BossHuntCommand(gameManager); + bossHunt.setExecutor(bossHuntCommand); + bossHunt.setTabCompleter(bossHuntCommand); + } + + if (bossKit != null) { + KitCommand cmd = new KitCommand(kitService, KitType.BOSS); + bossKit.setExecutor(cmd); + bossKit.setTabCompleter(cmd); + } + + if (hunterKit != null) { + KitCommand cmd = new KitCommand(kitService, KitType.HUNTER); + hunterKit.setExecutor(cmd); + hunterKit.setTabCompleter(cmd); + } + } +} diff --git a/src/main/java/de/winniepat/pierredufaulersack/commands/BossHuntCommand.java b/src/main/java/de/winniepat/pierredufaulersack/commands/BossHuntCommand.java new file mode 100644 index 0000000..2531d4a --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/commands/BossHuntCommand.java @@ -0,0 +1,167 @@ +package de.winniepat.pierredufaulersack.commands; + +import de.winniepat.pierredufaulersack.game.GameManager; +import de.winniepat.pierredufaulersack.game.GameState; +import java.util.*; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.*; +import org.bukkit.entity.Player; + +public class BossHuntCommand implements CommandExecutor, TabCompleter { + + private final GameManager gameManager; + private static final String PREFIX = "&8[&6BossHunt&8] &r"; + + public BossHuntCommand(GameManager gameManager) { + this.gameManager = gameManager; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (args.length < 1) { + send(sender, "&cNutzung: &f/" + label + " "); + return true; + } + + String sub = args[0].toLowerCase(); + + switch (sub) { + case "queues" -> sendQueues(sender); + case "select" -> { + if (!(sender instanceof Player player)) { + send(sender, "&cNur Spieler können eine Warteschlange auswählen."); + return true; + } + if (args.length < 2) { + send(sender, "&cNutzung: &f/" + label + " select "); + return true; + } + gameManager.selectQueue(player, args[1]); + } + case "join" -> { + if (!(sender instanceof Player player)) { + send(sender, "&cNur Spieler können einer Warteschlange beitreten."); + return true; + } + gameManager.join(player, args.length >= 2 ? args[1] : null); + } + case "leave" -> { + if (!(sender instanceof Player player)) { + send(sender, "&cNur Spieler können die Warteschlange verlassen."); + return true; + } + gameManager.leave(player); + } + case "setlobby" -> { + if (!(sender instanceof Player player)) { + send(sender, "&cNur Spieler können die Lobby setzen."); + return true; + } + gameManager.setLobby(player); + } + case "setarena" -> { + if (!(sender instanceof Player player)) { + send(sender, "&cNur Spieler können Arenen setzen."); + return true; + } + if (args.length < 2) { + send(sender, "&cNutzung: &f/" + label + " setarena "); + return true; + } + gameManager.setArena(player, args[1]); + } + case "start" -> gameManager.startRound(sender, args.length >= 2 ? args[1] : null); + case "stop" -> gameManager.stopRoundByCommand(sender, args.length >= 2 ? args[1] : null); + case "status" -> sendStatus(sender, args.length >= 2 ? args[1] : null); + default -> send(sender, "&cUnbekannter Unterbefehl."); + } + + return true; + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (args.length == 1) { + return List.of("queues", "select", "join", "leave", "start", "stop", "setlobby", "setarena", "status").stream() + .filter(option -> option.startsWith(args[0].toLowerCase())) + .toList(); + } + + if (args.length == 2) { + String sub = args[0].toLowerCase(); + if (sub.equals("select") || sub.equals("join") || sub.equals("setarena") || sub.equals("start") || sub.equals("status")) { + return gameManager.queueIds().stream() + .filter(id -> id.toLowerCase().startsWith(args[1].toLowerCase())) + .sorted() + .toList(); + } + if (sub.equals("stop")) { + List options = new ArrayList<>(gameManager.queueIds()); + options.add("all"); + return options.stream() + .filter(id -> id.toLowerCase().startsWith(args[1].toLowerCase())) + .sorted() + .toList(); + } + } + + return List.of(); + } + + private void sendQueues(CommandSender sender) { + send(sender, "&fWarteschlangen:"); + for (String queueId : new TreeSet<>(gameManager.queueIds())) { + GameState state = gameManager.getState(queueId); + send(sender, "&e- " + queueId + " &7(" + stateText(state) + "&7) &f" + + gameManager.lobbySize(queueId) + "&7/&f" + gameManager.maxPlayers()); + } + } + + private void sendStatus(CommandSender sender, String queueIdArg) { + String queueId = queueIdArg; + if ((queueId == null || queueId.isBlank()) && sender instanceof Player player) { + queueId = gameManager.getSelectedQueue(player.getUniqueId()); + } + if (queueId == null || queueId.isBlank()) { + sendQueues(sender); + return; + } + + if (!gameManager.queueIds().contains(queueId)) { + send(sender, "&cUnbekannte Warteschlange. Benutze &f/bosshunt queues&c."); + return; + } + + GameState state = gameManager.getState(queueId); + send(sender, "&fWarteschlange: &e" + queueId); + send(sender, "&fStatus: " + stateText(state)); + send(sender, "&fSpieler: &b" + gameManager.lobbySize(queueId) + "&7/&b" + gameManager.maxPlayers()); + + List onlineLobbyPlayers = new ArrayList<>(); + for (UUID uuid : gameManager.lobbyPlayers(queueId)) { + Player player = Bukkit.getPlayer(uuid); + if (player != null) { + onlineLobbyPlayers.add(player.getName()); + } + } + + if (onlineLobbyPlayers.isEmpty()) { + send(sender, "&7Warteschlange ist leer."); + } else { + send(sender, "&fIn der Warteschlange: &e" + String.join("&7, &e", onlineLobbyPlayers)); + } + } + + private String stateText(GameState state) { + return switch (state) { + case WAITING -> "&7WAITING"; + case COUNTDOWN -> "&eCOUNTDOWN"; + case RUNNING -> "&aRUNNING"; + }; + } + + private void send(CommandSender sender, String message) { + sender.sendMessage(ChatColor.translateAlternateColorCodes('&', PREFIX + message)); + } +} diff --git a/src/main/java/de/winniepat/pierredufaulersack/commands/KitCommand.java b/src/main/java/de/winniepat/pierredufaulersack/commands/KitCommand.java new file mode 100644 index 0000000..06c25a1 --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/commands/KitCommand.java @@ -0,0 +1,126 @@ +package de.winniepat.pierredufaulersack.commands; + +import de.winniepat.pierredufaulersack.kits.*; +import java.util.*; +import org.bukkit.ChatColor; +import org.bukkit.command.*; +import org.bukkit.entity.Player; + +public class KitCommand implements CommandExecutor, TabCompleter { + + private static final String ADMIN_PERMISSION = "pierredufaulersack.admin"; + private static final String PREFIX = "&8[&6BossHunt&8] &r"; + + private final KitService kitService; + private final KitType type; + + public KitCommand(KitService kitService, KitType type) { + this.kitService = kitService; + this.type = type; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!sender.hasPermission(ADMIN_PERMISSION)) { + send(sender, "&cKeine Berechtigung."); + return true; + } + + if (args.length < 1) { + send(sender, "&cNutzung: &f/" + label + " [1-3]"); + return true; + } + + String action = args[0].toLowerCase(); + switch (action) { + case "list" -> handleList(sender, label); + case "add", "set", "del" -> handleKitMutation(sender, label, action, args); + default -> send(sender, "&cUnbekannter Unterbefehl. &7Nutzung: &f/" + label + " [1-3]"); + } + + return true; + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (args.length == 1) { + return List.of("add", "set", "del", "list").stream() + .filter(opt -> opt.startsWith(args[0].toLowerCase())) + .toList(); + } + + if (args.length == 2 && !"list".equalsIgnoreCase(args[0])) { + List slots = List.of("1", "2", "3"); + List filtered = new ArrayList<>(); + for (String slot : slots) { + if (slot.startsWith(args[1])) { + filtered.add(slot); + } + } + return filtered; + } + + return List.of(); + } + + private void handleList(CommandSender sender, String label) { + List slots = kitService.listKitSlots(type); + if (slots.isEmpty()) { + send(sender, "&eKeine Kits für &f/" + label + " &egespeichert."); + return; + } + + send(sender, "&aVerfügbare Kits fuer &f/" + label + "&a: &e" + slots); + } + + private void handleKitMutation(CommandSender sender, String label, String action, String[] args) { + if (args.length != 2) { + send(sender, "&cNutzung: &f/" + label + " " + action + " <1-3>"); + return; + } + + Integer slot = parseSlot(args[1]); + if (slot == null) { + send(sender, "&cSlot muss 1, 2 oder 3 sein."); + return; + } + + if ("del".equals(action)) { + boolean deleted = kitService.deleteKit(type, slot); + send(sender, deleted ? "&aKit &e" + slot + " &agelöscht." : "&eKit &f" + slot + " &eexistiert nicht."); + return; + } + + if (!(sender instanceof Player player)) { + send(sender, "&cNur Spieler können Inventar-Kits speichern."); + return; + } + + if ("add".equals(action)) { + boolean added = kitService.addKit(type, slot, player); + send(sender, added + ? "&aKit &e" + slot + " &agespeichert." + : "&eKit &f" + slot + " &eexistiert bereits. Nutze &fset &ezum Ueberschreiben."); + return; + } + + kitService.setKit(type, slot, player); + send(sender, "&aKit &e" + slot + " &agesetzt."); + } + + private Integer parseSlot(String value) { + try { + int parsed = Integer.parseInt(value); + if (parsed < 1 || parsed > 3) { + return null; + } + return parsed; + } catch (NumberFormatException ignored) { + return null; + } + } + + private void send(CommandSender sender, String message) { + sender.sendMessage(ChatColor.translateAlternateColorCodes('&', PREFIX + message)); + } +} diff --git a/src/main/java/de/winniepat/pierredufaulersack/game/GameManager.java b/src/main/java/de/winniepat/pierredufaulersack/game/GameManager.java new file mode 100644 index 0000000..4644c14 --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/game/GameManager.java @@ -0,0 +1,1236 @@ +package de.winniepat.pierredufaulersack.game; + +import de.winniepat.pierredufaulersack.kits.KitService; +import de.winniepat.pierredufaulersack.kits.KitType; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.World; +import org.bukkit.attribute.Attribute; +import org.bukkit.boss.BarColor; +import org.bukkit.boss.BarStyle; +import org.bukkit.boss.BossBar; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Item; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.potion.PotionEffect; + +public class GameManager { + + private static final String ADMIN_PERMISSION = "pierredufaulersack.admin"; + private static final String PREFIX = "&8[&6BossHunt&8] &r"; + private static final long WINNER_TELEPORT_DELAY_TICKS = 20L * 5L; + + private final JavaPlugin plugin; + private final KitService kitService; + private final LuckPermsService luckPermsService; + private final Map sessions = new LinkedHashMap<>(); + private final Map selectedQueueByPlayer = new HashMap<>(); + private final Map queuedQueueByPlayer = new HashMap<>(); + private final Map activeQueueByPlayer = new HashMap<>(); + private final Map pendingLobbyRespawns = new HashMap<>(); + private final Set warnedInvalidSounds = new HashSet<>(); + private BukkitTask queueMonitorTask; + + public GameManager(JavaPlugin plugin, KitService kitService, LuckPermsService luckPermsService) { + this.plugin = plugin; + this.kitService = kitService; + this.luckPermsService = luckPermsService; + initializeSessions(); + startQueueMonitor(); + } + + public void shutdown() { + if (queueMonitorTask != null) { + queueMonitorTask.cancel(); + queueMonitorTask = null; + } + if (luckPermsService != null) { + luckPermsService.clearAll(); + } + } + + public boolean isRunning() { + return sessions.values().stream().anyMatch(session -> session.state == GameState.RUNNING); + } + + public GameState getState() { + for (GameSession session : sessions.values()) { + if (session.state == GameState.RUNNING) { + return GameState.RUNNING; + } + if (session.state == GameState.COUNTDOWN) { + return GameState.COUNTDOWN; + } + } + return GameState.WAITING; + } + + public Set queueIds() { + return Set.copyOf(sessions.keySet()); + } + + public int maxPlayers() { + return plugin.getConfig().getInt("max-players", 5); + } + + public int lobbySize() { + return queuedQueueByPlayer.size(); + } + + public int lobbySize(String queueId) { + GameSession session = sessions.get(queueId); + return session == null ? 0 : session.lobbyPlayers.size(); + } + + public GameState getState(String queueId) { + GameSession session = sessions.get(queueId); + return session == null ? GameState.WAITING : session.state; + } + + public String getSelectedQueue(UUID playerId) { + return selectedQueueByPlayer.get(playerId); + } + + public boolean selectQueue(Player player, String queueId) { + if (!sessions.containsKey(queueId)) { + send(player, "&cUnbekannte queue. Benutze &f/bosshunt queues&c."); + return false; + } + + selectedQueueByPlayer.put(player.getUniqueId(), queueId); + return true; + } + + public boolean join(Player player) { + return join(player, null); + } + + public boolean join(Player player, String queueIdArg) { + UUID playerId = player.getUniqueId(); + String queueId = resolveQueueId(player, queueIdArg); + if (queueId == null) { + return false; + } + + if (activeQueueByPlayer.containsKey(playerId)) { + send(player, "&cDu bist bereits in einer Runde."); + return false; + } + + GameSession target = sessions.get(queueId); + if (target == null) { + send(player, "&cQueue nicht gefunden."); + return false; + } + + String currentlyQueued = queuedQueueByPlayer.get(playerId); + if (queueId.equals(currentlyQueued)) { + send(player, "&eDu bist bereits in der queue für &f" + queueId + "&e."); + return false; + } + + if (target.state == GameState.RUNNING) { + send(player, "&cQueue &f" + queueId + " &cis läuft bereits."); + return false; + } + + if (target.lobbyPlayers.size() >= maxPlayers()) { + send(player, "&cQueue &f" + queueId + " &cist voll (&f" + maxPlayers() + "&c)."); + return false; + } + + if (currentlyQueued != null) { + GameSession oldSession = sessions.get(currentlyQueued); + if (oldSession != null) { + oldSession.lobbyPlayers.remove(playerId); + if (oldSession.state == GameState.COUNTDOWN) { + ensureEnoughPlayersForCountdown(oldSession); + } + } + } + + target.lobbyPlayers.add(playerId); + queuedQueueByPlayer.put(playerId, queueId); + selectedQueueByPlayer.put(playerId, queueId); + + Location lobbyLocation = loadLocation("locations.lobby"); + if (lobbyLocation != null) { + player.teleport(lobbyLocation); + } + player.sendMessage(color("&aQueue &e" + queueId + " &7(&f" + target.lobbyPlayers.size() + "&7/&f" + maxPlayers() + "&7) beigetreten")); + + maybeStartQueueCountdown(target); + if (target.state == GameState.COUNTDOWN && !target.countdownForFullLobby && target.lobbyPlayers.size() >= maxPlayers()) { + restartCountdownForFullLobby(target); + } + return true; + } + + public boolean leave(Player player) { + UUID playerId = player.getUniqueId(); + String queueId = queuedQueueByPlayer.remove(playerId); + if (queueId == null) { + return false; + } + + GameSession session = sessions.get(queueId); + if (session != null) { + session.lobbyPlayers.remove(playerId); + if (session.state == GameState.COUNTDOWN) { + ensureEnoughPlayersForCountdown(session); + } + } + return true; + } + + public void clearSelectedQueue(Player player) { + selectedQueueByPlayer.remove(player.getUniqueId()); + player.sendMessage(color("&eQueue Auswahl gelöscht.")); + } + + public void handlePlayerQuit(Player player) { + UUID playerId = player.getUniqueId(); + pendingLobbyRespawns.remove(playerId); + selectedQueueByPlayer.remove(playerId); + + String queuedId = queuedQueueByPlayer.remove(playerId); + if (queuedId != null) { + GameSession queuedSession = sessions.get(queuedId); + if (queuedSession != null) { + queuedSession.lobbyPlayers.remove(playerId); + if (queuedSession.state == GameState.COUNTDOWN) { + ensureEnoughPlayersForCountdown(queuedSession); + } + } + } + + String activeId = activeQueueByPlayer.remove(playerId); + if (activeId == null) { + return; + } + + GameSession activeSession = sessions.get(activeId); + if (activeSession == null || activeSession.state != GameState.RUNNING) { + return; + } + + Role role = activeSession.roles.remove(playerId); + activeSession.roundParticipants.remove(playerId); + clearRoleEffects(playerId, player); + + UUID soleRemainingId = soleRemainingPlayer(activeSession); + if (soleRemainingId != null) { + Set winners = announceSoleWinner(activeSession, soleRemainingId); + stopSession(activeSession, true, winners); + return; + } + + if (role == Role.BOSS) { + stopSession(activeSession, false, Set.of()); + return; + } + + if (role == Role.HUNTER && huntersLeft(activeSession) == 0) { + stopSession(activeSession, false, Set.of()); + } + } + + private UUID soleRemainingPlayer(GameSession session) { + if (session.roles.size() != 1) { + return null; + } + return session.roles.keySet().iterator().next(); + } + + private Set announceSoleWinner(GameSession session, UUID winnerId) { + Player winner = Bukkit.getPlayer(winnerId); + if (winner == null || !winner.isOnline()) { + return Set.of(); + } + + Role winnerRole = session.roles.get(winnerId); + send(winner, "&aDu hast gewonnen!"); + showResultTitle(winner, true); + + String soundPath = winnerRole == Role.BOSS ? "sounds.boss-win.sound" : "sounds.hunter-win.sound"; + String fallbackSound = winnerRole == Role.BOSS ? "ENTITY_ENDER_DRAGON_GROWL" : "ENTITY_PLAYER_LEVELUP"; + String volumePath = winnerRole == Role.BOSS ? "sounds.boss-win.volume" : "sounds.hunter-win.volume"; + String pitchPath = winnerRole == Role.BOSS ? "sounds.boss-win.pitch" : "sounds.hunter-win.pitch"; + + playToPlayers(List.of(winner), + plugin.getConfig().getString(soundPath, fallbackSound), + (float) plugin.getConfig().getDouble(volumePath, 1.0), + (float) plugin.getConfig().getDouble(pitchPath, 1.0)); + + return Set.of(winnerId); + } + + public void handleKill(Player killer, Player victim) { + UUID killerId = killer.getUniqueId(); + UUID victimId = victim.getUniqueId(); + + String killerQueue = activeQueueByPlayer.get(killerId); + if (killerQueue == null || !killerQueue.equals(activeQueueByPlayer.get(victimId))) { + return; + } + + GameSession session = sessions.get(killerQueue); + if (session == null || session.state != GameState.RUNNING) { + return; + } + + Role killerRole = session.roles.get(killerId); + Role victimRole = session.roles.get(victimId); + + if (killerRole == Role.BOSS && victimRole == Role.HUNTER) { + pendingLobbyRespawns.put(victimId, session.id); + session.roles.remove(victimId); + session.roundParticipants.remove(victimId); + activeQueueByPlayer.remove(victimId); + clearRoleEffects(victimId, victim); + + send(victim, "&cDu wurdest getötet"); + playToPlayers(List.of(victim), + plugin.getConfig().getString("sounds.killed.sound", "ENTITY_PLAYER_HURT"), + (float) plugin.getConfig().getDouble("sounds.killed.volume", 1.0), + (float) plugin.getConfig().getDouble("sounds.killed.pitch", 1.0)); + + if (huntersLeft(session) == 0) { + send(killer, "&aDu hast gewonnen!"); + showResultTitle(killer, true); + playToPlayers(List.of(killer), + plugin.getConfig().getString("sounds.boss-win.sound", "ENTITY_ENDER_DRAGON_GROWL"), + (float) plugin.getConfig().getDouble("sounds.boss-win.volume", 1.0), + (float) plugin.getConfig().getDouble("sounds.boss-win.pitch", 1.0)); + stopSession(session, true, Set.of(killerId)); + } + return; + } + + if (killerRole == Role.HUNTER && victimRole == Role.BOSS) { + pendingLobbyRespawns.put(victimId, session.id); + Set winners = new HashSet<>(); + + for (Map.Entry entry : session.roles.entrySet()) { + Player player = Bukkit.getPlayer(entry.getKey()); + if (player == null || !player.isOnline()) { + continue; + } + + if (entry.getValue() == Role.HUNTER) { + winners.add(entry.getKey()); + send(player, "&aDu hast gewonnen!"); + showResultTitle(player, true); + playToPlayers(List.of(player), + plugin.getConfig().getString("sounds.hunter-win.sound", "ENTITY_PLAYER_LEVELUP"), + (float) plugin.getConfig().getDouble("sounds.hunter-win.volume", 1.0), + (float) plugin.getConfig().getDouble("sounds.hunter-win.pitch", 1.2)); + } else { + send(player, "&cDu hast verloren!"); + showResultTitle(player, false); + playToPlayers(List.of(player), + plugin.getConfig().getString("sounds.boss-lose.sound", "ENTITY_VILLAGER_NO"), + (float) plugin.getConfig().getDouble("sounds.boss-lose.volume", 1.0), + (float) plugin.getConfig().getDouble("sounds.boss-lose.pitch", 1.0)); + } + } + + stopSession(session, true, winners); + } + } + + public Location consumePendingLobbyRespawn(UUID playerId) { + String queueId = pendingLobbyRespawns.remove(playerId); + if (queueId == null) { + return null; + } + return loadLocation("locations.lobby"); + } + + public void startRound(CommandSender sender) { + startRound(sender, null); + } + + public void startRound(CommandSender sender, String queueIdArg) { + if (!hasAdminPermission(sender)) { + return; + } + + GameSession session = resolveSessionFromSender(sender, queueIdArg); + if (session == null) { + return; + } + + if (session.state == GameState.RUNNING) { + send(sender, "&cQueue &f" + session.id + " &cläuft bereits."); + return; + } + if (session.state == GameState.COUNTDOWN) { + send(sender, "&eQueue &f" + session.id + " &eist bereits im countdown."); + return; + } + + List players = onlineLobbyPlayers(session); + int minPlayers = Math.max(2, plugin.getConfig().getInt("countdown.min-players", 2)); + if (players.size() < minPlayers) { + send(sender, "&cQueue &f" + session.id + " &cbraucht mindestens &f" + minPlayers + "&c Spieler."); + return; + } + + Location arena = loadArena(session.id); + if (arena == null) { + send(sender, "&cArena für &f" + session.id + " &cisr nicht gesetzt. Benutze &f/bosshunt setarena " + session.id + "&c."); + return; + } + + int countdownSeconds = countdownSecondsForLobbySize(players.size()); + if (countdownSeconds == 0) { + beginRound(session, players, arena); + return; + } + + startCountdown(session, countdownSeconds, players.size() >= maxPlayers()); + } + + public void stopRound(boolean silent) { + for (GameSession session : sessions.values()) { + stopSession(session, silent, Set.of()); + } + } + + public void stopRoundByCommand(CommandSender sender) { + stopRoundByCommand(sender, null); + } + + public void stopRoundByCommand(CommandSender sender, String queueIdArg) { + if (!hasAdminPermission(sender)) { + return; + } + + if (queueIdArg == null || queueIdArg.equalsIgnoreCase("all")) { + boolean hadAny = false; + for (GameSession session : sessions.values()) { + if (session.state != GameState.WAITING) { + hadAny = true; + stopSession(session, false, Set.of()); + } + } + if (!hadAny) { + send(sender, "&eKeine queue ist gerade aktiv."); + } + return; + } + + GameSession session = sessions.get(queueIdArg); + if (session == null) { + send(sender, "&cUnbekannte queue. Benutze &f/bosshunt queues&c."); + return; + } + + if (session.state == GameState.WAITING) { + send(sender, "&eQueue &f" + session.id + " &eist bereits im idle."); + return; + } + + stopSession(session, false, Set.of()); + } + + public void setLobby(Player player) { + if (!hasAdminPermission(player)) { + return; + } + + saveLocation("locations.lobby", player.getLocation()); + send(player, "&aLobby position gespeichert."); + } + + public void setArena(Player player, String queueId) { + if (!hasAdminPermission(player)) { + return; + } + if (!sessions.containsKey(queueId)) { + send(player, "&cUnbekannte queue. Benutze &f/bosshunt queues&c."); + return; + } + + saveLocation("arenas." + queueId, player.getLocation()); + send(player, "&aArena für &e" + queueId + " &asaved."); + } + + public boolean areBothHunters(UUID attacker, UUID victim) { + String attackerQueue = activeQueueByPlayer.get(attacker); + if (attackerQueue == null || !attackerQueue.equals(activeQueueByPlayer.get(victim))) { + return false; + } + + GameSession session = sessions.get(attackerQueue); + if (session == null || session.state != GameState.RUNNING) { + return false; + } + + return session.roles.get(attacker) == Role.HUNTER && session.roles.get(victim) == Role.HUNTER; + } + + public Role getRole(UUID playerId) { + String queueId = activeQueueByPlayer.get(playerId); + if (queueId == null) { + return null; + } + GameSession session = sessions.get(queueId); + return session == null ? null : session.roles.get(playerId); + } + + public int huntersLeft(UUID playerId) { + String queueId = activeQueueByPlayer.get(playerId); + if (queueId == null) { + return 0; + } + GameSession session = sessions.get(queueId); + return session == null ? 0 : huntersLeft(session); + } + + public boolean isLobbyPlayer(UUID playerId) { + return queuedQueueByPlayer.containsKey(playerId); + } + + public String queueOfPlayer(UUID playerId) { + String running = activeQueueByPlayer.get(playerId); + return running != null ? running : queuedQueueByPlayer.get(playerId); + } + + public Collection lobbyPlayers(String queueId) { + GameSession session = sessions.get(queueId); + return session == null ? Set.of() : Set.copyOf(session.lobbyPlayers); + } + + public void trackRoundWaterPlacement(Player player, Location location) { + if (player == null || location == null || location.getWorld() == null) { + return; + } + + String queueId = activeQueueByPlayer.get(player.getUniqueId()); + if (queueId == null) { + return; + } + + GameSession session = sessions.get(queueId); + if (session == null || session.state != GameState.RUNNING) { + return; + } + + if (!session.roundParticipants.contains(player.getUniqueId())) { + return; + } + + session.placedWater.add(PlacedBlockKey.of(location)); + } + + private void initializeSessions() { + ConfigurationSection arenaSection = plugin.getConfig().getConfigurationSection("arenas"); + int maxGames = Math.max(1, plugin.getConfig().getInt("max-concurrent-games", 3)); + + List ids = new ArrayList<>(); + if (arenaSection != null) { + ids.addAll(arenaSection.getKeys(false)); + } + + if (ids.isEmpty()) { + for (int i = 1; i <= maxGames; i++) { + plugin.getConfig().set("arenas.game" + i, new HashMap<>()); + ids.add("game" + i); + } + plugin.saveConfig(); + } + + Collections.sort(ids); + for (String id : ids) { + if (sessions.size() >= maxGames) { + break; + } + sessions.put(id, new GameSession(id)); + } + + if (sessions.isEmpty()) { + sessions.put("game1", new GameSession("game1")); + } + } + + private void startQueueMonitor() { + queueMonitorTask = Bukkit.getScheduler().runTaskTimer(plugin, this::tickQueues, 20L, 20L); + } + + private void tickQueues() { + for (GameSession session : sessions.values()) { + if (session.state == GameState.WAITING) { + maybeStartQueueCountdown(session); + continue; + } + + if (session.state != GameState.COUNTDOWN) { + continue; + } + + int minPlayers = Math.max(2, plugin.getConfig().getInt("countdown.min-players", 2)); + int onlinePlayers = onlineLobbyPlayers(session).size(); + if (onlinePlayers < minPlayers) { + cancelCountdown(session, "&cCountdown abgebrochen für &f" + session.id + "&c: nicht genug Spieler."); + continue; + } + + if (!session.countdownForFullLobby && onlinePlayers >= maxPlayers()) { + restartCountdownForFullLobby(session); + } + } + } + + private String resolveQueueId(Player player, String queueIdArg) { + if (queueIdArg == null || queueIdArg.isBlank()) { + String selectedQueue = getSelectedQueue(player.getUniqueId()); + if (selectedQueue == null || selectedQueue.isBlank()) { + send(player, "&eKeine queue ausgewählt. Benutze &f/bosshunt select &eoder den queue Selector in deinem Inventar."); + return null; + } + return selectedQueue; + } + + if (!sessions.containsKey(queueIdArg)) { + send(player, "&cUnbekannte queue. Use &f/bosshunt queues&c."); + return null; + } + + return queueIdArg; + } + + private GameSession resolveSessionFromSender(CommandSender sender, String queueIdArg) { + String queueId = queueIdArg; + if (queueId == null || queueId.isBlank()) { + if (sender instanceof Player player) { + queueId = getSelectedQueue(player.getUniqueId()); + if (queueId == null || queueId.isBlank()) { + send(sender, "&eKeine queue ausgewählt. Benutze &f/bosshunt select &e."); + return null; + } + } else { + queueId = defaultQueueId(); + } + } + + GameSession session = sessions.get(queueId); + if (session == null) { + send(sender, "&cUnbekannte queue. Benutze &f/bosshunt queues&c."); + } + return session; + } + + private String defaultQueueId() { + return sessions.keySet().iterator().next(); + } + + private void stopSession(GameSession session, boolean silent, Set delayedWinners) { + if (session.state == GameState.WAITING) { + return; + } + + if (session.state == GameState.COUNTDOWN) { + cancelCountdown(session, silent ? null : "&eCountdown abgebrochen für &f" + session.id + "&e."); + return; + } + + clearCountdownBossBar(session); + session.state = GameState.WAITING; + + Set roundPlayers = Set.copyOf(session.roundParticipants); + Set affectedPlayers = new HashSet<>(roundPlayers); + affectedPlayers.addAll(session.roles.keySet()); + for (Map.Entry entry : pendingLobbyRespawns.entrySet()) { + if (session.id.equals(entry.getValue())) { + affectedPlayers.add(entry.getKey()); + } + } + Set worldsToClean = new HashSet<>(Bukkit.getWorlds()); + + for (UUID playerId : affectedPlayers) { + queuedQueueByPlayer.remove(playerId); + activeQueueByPlayer.remove(playerId); + pendingLobbyRespawns.remove(playerId); + selectedQueueByPlayer.remove(playerId); + clearRoleEffects(playerId, Bukkit.getPlayer(playerId)); + } + + session.lobbyPlayers.removeAll(affectedPlayers); + session.roundParticipants.clear(); + session.roles.clear(); + clearPlacedWater(session); + + Location lobby = loadLocation("locations.lobby"); + for (UUID uuid : roundPlayers) { + Player player = Bukkit.getPlayer(uuid); + if (player == null || !player.isOnline()) { + continue; + } + + worldsToClean.add(player.getWorld()); + resetPlayerAfterMatch(player); + + if (lobby != null && !delayedWinners.contains(uuid)) { + player.teleport(lobby); + } + } + + clearDroppedItems(worldsToClean); + + if (lobby != null && !delayedWinners.isEmpty()) { + Bukkit.getScheduler().runTaskLater(plugin, () -> { + for (UUID winnerId : delayedWinners) { + Player winner = Bukkit.getPlayer(winnerId); + if (winner != null && winner.isOnline()) { + winner.teleport(lobby); + resetPlayerAfterMatch(winner); + } + } + clearDroppedItems(worldsToClean); + }, WINNER_TELEPORT_DELAY_TICKS); + } + + if (!silent) { + playGlobalSound("sounds.stop.sound", "ENTITY_ITEM_BREAK"); + } + } + + private void clearPlayerInventory(Player player) { + player.getInventory().clear(); + player.getInventory().setArmorContents(new ItemStack[4]); + player.getInventory().setItemInOffHand(null); + } + + private void resetPlayerAfterMatch(Player player) { + clearPlayerInventory(player); + + for (PotionEffect effect : player.getActivePotionEffects()) { + player.removePotionEffect(effect.getType()); + } + + double maxHealth = 20.0; + if (player.getAttribute(Attribute.MAX_HEALTH) != null) { + maxHealth = player.getAttribute(Attribute.MAX_HEALTH).getValue(); + } + player.setHealth(Math.max(1.0, maxHealth)); + player.setFoodLevel(20); + player.setSaturation(20.0f); + player.setExhaustion(0.0f); + player.setFireTicks(0); + player.setAbsorptionAmount(0.0); + } + + private void clearDroppedItems(Set worlds) { + for (World world : worlds) { + for (Item droppedItem : world.getEntitiesByClass(Item.class)) { + droppedItem.remove(); + } + } + } + + private void clearPlacedWater(GameSession session) { + for (PlacedBlockKey key : session.placedWater) { + World world = Bukkit.getWorld(key.worldId); + if (world == null) { + continue; + } + + Material current = world.getBlockAt(key.x, key.y, key.z).getType(); + if (current == Material.WATER) { + // Use physics so adjacent flowing water recalculates and drains. + world.getBlockAt(key.x, key.y, key.z).setType(Material.AIR, true); + } + } + session.placedWater.clear(); + } + + private int huntersLeft(GameSession session) { + return (int) session.roles.values().stream().filter(role -> role == Role.HUNTER).count(); + } + + private void ensureEnoughPlayersForCountdown(GameSession session) { + int minPlayers = Math.max(2, plugin.getConfig().getInt("countdown.min-players", 2)); + if (onlineLobbyPlayers(session).size() < minPlayers) { + cancelCountdown(session, "&cCountdown abgebrochen für &f" + session.id + "&c: nicht genug Spieler."); + } + } + + private void maybeStartQueueCountdown(GameSession session) { + if (session.state != GameState.WAITING) { + return; + } + + int minPlayers = Math.max(2, plugin.getConfig().getInt("countdown.min-players", 2)); + int players = onlineLobbyPlayers(session).size(); + if (players < minPlayers) { + return; + } + + int seconds = countdownSecondsForLobbySize(players); + if (seconds == 0) { + Location arena = loadArena(session.id); + if (arena == null) { + return; + } + beginRound(session, onlineLobbyPlayers(session), arena); + return; + } + + startCountdown(session, seconds, players >= maxPlayers()); + } + + private void restartCountdownForFullLobby(GameSession session) { + int fallback = plugin.getConfig().getInt("countdown.seconds", 10); + int fullSeconds = Math.max(0, plugin.getConfig().getInt("countdown.full-seconds", fallback)); + if (fullSeconds <= 0) { + return; + } + + cancelCountdownTaskOnly(session); + clearCountdownBossBar(session); + startCountdown(session, fullSeconds, true); + } + + private int countdownSecondsForLobbySize(int playersInLobby) { + int fallback = Math.max(0, plugin.getConfig().getInt("countdown.seconds", 10)); + int full = Math.max(0, plugin.getConfig().getInt("countdown.full-seconds", fallback)); + int notFull = Math.max(0, plugin.getConfig().getInt("countdown.not-full-seconds", Math.max(full, fallback + 10))); + return playersInLobby >= maxPlayers() ? full : notFull; + } + + private void startCountdown(GameSession session, int seconds, boolean fullLobby) { + session.state = GameState.COUNTDOWN; + session.activeCountdownSeconds = Math.max(1, seconds); + session.countdownForFullLobby = fullLobby; + + createOrRefreshCountdownBossBar(session, session.activeCountdownSeconds, session.activeCountdownSeconds); + + final int[] remaining = {seconds}; + session.countdownTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { + if (session.state != GameState.COUNTDOWN) { + cancelCountdownTaskOnly(session); + clearCountdownBossBar(session); + return; + } + + List players = onlineLobbyPlayers(session); + int minPlayers = Math.max(2, plugin.getConfig().getInt("countdown.min-players", 2)); + if (players.size() < minPlayers) { + cancelCountdown(session, "&cCountdown abgebrochen für &f" + session.id + "&c: nicht genug spieler."); + return; + } + + if (session.countdownBossBar != null) { + for (Player player : players) { + session.countdownBossBar.addPlayer(player); + } + } + + if (remaining[0] <= 0) { + cancelCountdownTaskOnly(session); + clearCountdownBossBar(session); + Location arena = loadArena(session.id); + if (arena == null) { + session.state = GameState.WAITING; + return; + } + beginRound(session, players, arena); + return; + } + + createOrRefreshCountdownBossBar(session, session.activeCountdownSeconds, remaining[0]); + playToPlayers(players, + plugin.getConfig().getString("sounds.countdown.sound", "BLOCK_NOTE_BLOCK_HAT"), + (float) plugin.getConfig().getDouble("sounds.countdown.volume", 1.0), + (float) plugin.getConfig().getDouble("sounds.countdown.pitch", 1.4)); + + if (remaining[0] <= 3) { + playToPlayers(players, + plugin.getConfig().getString("sounds.countdown-final.sound", "BLOCK_NOTE_BLOCK_PLING"), + (float) plugin.getConfig().getDouble("sounds.countdown-final.volume", 1.0), + (float) plugin.getConfig().getDouble("sounds.countdown-final.pitch", 1.8)); + } + + remaining[0]--; + }, 0L, 20L); + } + + private void beginRound(GameSession session, List players, Location arena) { + clearCountdownBossBar(session); + session.roles.clear(); + session.roundParticipants.clear(); + session.placedWater.clear(); + session.state = GameState.RUNNING; + + for (Player player : players) { + UUID playerId = player.getUniqueId(); + session.roundParticipants.add(playerId); + activeQueueByPlayer.put(playerId, session.id); + queuedQueueByPlayer.remove(playerId); + player.setGameMode(GameMode.SURVIVAL); + player.teleport(arena); + } + + int bossIndex = ThreadLocalRandom.current().nextInt(players.size()); + Player boss = players.get(bossIndex); + session.roles.put(boss.getUniqueId(), Role.BOSS); + applyRoleEffects(boss, Role.BOSS); + showRoleTitle(boss, Role.BOSS); + + List hunters = new ArrayList<>(); + for (Player player : players) { + if (!player.getUniqueId().equals(boss.getUniqueId())) { + session.roles.put(player.getUniqueId(), Role.HUNTER); + applyRoleEffects(player, Role.HUNTER); + showRoleTitle(player, Role.HUNTER); + hunters.add(player); + } + } + + assignBossKit(boss); + assignHunterKits(hunters); + + playToPlayers(players, + plugin.getConfig().getString("sounds.start.sound", "ENTITY_PLAYER_LEVELUP"), + (float) plugin.getConfig().getDouble("sounds.start.volume", 1.0), + (float) plugin.getConfig().getDouble("sounds.start.pitch", 1.0)); + } + + private List onlineLobbyPlayers(GameSession session) { + List players = new ArrayList<>(); + Iterator iterator = session.lobbyPlayers.iterator(); + while (iterator.hasNext()) { + UUID uuid = iterator.next(); + Player player = Bukkit.getPlayer(uuid); + if (player == null || !player.isOnline()) { + iterator.remove(); + queuedQueueByPlayer.remove(uuid); + continue; + } + players.add(player); + } + return players; + } + + private void cancelCountdown(GameSession session, String message) { + cancelCountdownTaskOnly(session); + session.state = GameState.WAITING; + clearCountdownBossBar(session); + if (message != null && !message.isEmpty()) { + for (Player player : onlineLobbyPlayers(session)) { + send(player, message); + } + playGlobalSound("sounds.stop.sound", "ENTITY_ITEM_BREAK"); + } + } + + private void cancelCountdownTaskOnly(GameSession session) { + if (session.countdownTask != null) { + session.countdownTask.cancel(); + session.countdownTask = null; + } + } + + private void createOrRefreshCountdownBossBar(GameSession session, int totalSeconds, int remainingSeconds) { + if (session.countdownBossBar == null) { + session.countdownBossBar = Bukkit.createBossBar("", session.countdownForFullLobby ? BarColor.GREEN : BarColor.YELLOW, BarStyle.SOLID); + } + + double progress = totalSeconds <= 0 ? 1.0 : Math.max(0.0, Math.min(1.0, remainingSeconds / (double) totalSeconds)); + session.countdownBossBar.setProgress(progress); + session.countdownBossBar.setColor(session.countdownForFullLobby ? BarColor.GREEN : BarColor.YELLOW); + session.countdownBossBar.setTitle(color("&6" + session.id + " &8- &eStart in &f" + Math.max(0, remainingSeconds) + "s" + + (session.countdownForFullLobby ? " &a(VOLL)" : " &e(TEILWEISE)"))); + } + + private void clearCountdownBossBar(GameSession session) { + if (session.countdownBossBar != null) { + session.countdownBossBar.removeAll(); + session.countdownBossBar = null; + } + } + + private void assignBossKit(Player boss) { + OptionalInt bossSlot = kitService.rollKitSlot(KitType.BOSS); + if (bossSlot.isEmpty()) { + send(boss, "&cKein Kit für den Boss verfügbar. &7Inventar bleibt unverändert."); + return; + } + + applyKit(boss, KitType.BOSS, bossSlot.getAsInt()); + send(boss, "&6Du bist der Boss &7mit dem Kit &e" + bossSlot.getAsInt() + "&7."); + } + + private void assignHunterKits(List hunters) { + int maxDuplicates = plugin.getConfig().getInt("hunter-kit-max-duplicates", 2); + Map slotUsage = new HashMap<>(); + + for (Player hunter : hunters) { + OptionalInt slot = kitService.rollKitSlot(KitType.HUNTER, + candidate -> slotUsage.getOrDefault(candidate, 0) < maxDuplicates); + + if (slot.isEmpty()) { + send(hunter, "&cKein Kit für die Hunter verfügbar. Inventar bleibt unverändert."); + continue; + } + + int chosen = slot.getAsInt(); + slotUsage.put(chosen, slotUsage.getOrDefault(chosen, 0) + 1); + applyKit(hunter, KitType.HUNTER, chosen); + send(hunter, "&bDu bist ein Hunter &7mit dem Kit &e" + chosen + "&7."); + } + } + + private void applyKit(Player player, KitType type, int slot) { + Optional> maybeKit = kitService.getKitForPlayer(player.getUniqueId(), type, slot); + if (maybeKit.isEmpty()) { + return; + } + + List kit = maybeKit.get(); + ItemStack[] full = new ItemStack[41]; + ItemStack[] loaded = kit.toArray(ItemStack[]::new); + System.arraycopy(loaded, 0, full, 0, Math.min(full.length, loaded.length)); + + player.getInventory().clear(); + player.getInventory().setStorageContents(Arrays.copyOfRange(full, 0, 36)); + player.getInventory().setArmorContents(new ItemStack[]{full[36], full[37], full[38], full[39]}); + player.getInventory().setItemInOffHand(full[40]); + } + + private void applyRoleEffects(Player player, Role role) { + if (role == Role.HUNTER) { + player.setGlowing(true); + if (luckPermsService != null) { + luckPermsService.applyHunterPrefix(player); + } + return; + } + + player.setGlowing(false); + if (luckPermsService != null) { + luckPermsService.applyBossPrefix(player); + } + } + + private void clearRoleEffects(UUID playerId, Player player) { + if (player != null && player.isOnline()) { + player.setGlowing(false); + } + if (luckPermsService != null) { + luckPermsService.clearPrefix(playerId); + } + } + + private Location loadArena(String queueId) { + return loadLocation("arenas." + queueId); + } + + private void saveLocation(String path, Location location) { + FileConfiguration config = plugin.getConfig(); + config.set(path + ".world", location.getWorld() == null ? null : location.getWorld().getName()); + config.set(path + ".x", location.getX()); + config.set(path + ".y", location.getY()); + config.set(path + ".z", location.getZ()); + config.set(path + ".yaw", location.getYaw()); + config.set(path + ".pitch", location.getPitch()); + plugin.saveConfig(); + } + + private Location loadLocation(String path) { + FileConfiguration config = plugin.getConfig(); + ConfigurationSection section = config.getConfigurationSection(path); + if (section == null) { + return null; + } + + String worldName = section.getString("world"); + if (worldName == null) { + return null; + } + + World world = Bukkit.getWorld(worldName); + if (world == null) { + return null; + } + + return new Location( + world, + section.getDouble("x"), + section.getDouble("y"), + section.getDouble("z"), + (float) section.getDouble("yaw"), + (float) section.getDouble("pitch")); + } + + private boolean hasAdminPermission(CommandSender sender) { + if (sender.hasPermission(ADMIN_PERMISSION)) { + return true; + } + + send(sender, "&cDu hast keine Rechte dazu."); + return false; + } + + private void send(CommandSender sender, String message) { + sender.sendMessage(color(PREFIX + message)); + } + + private void send(Player player, String message) { + player.sendMessage(color(PREFIX + message)); + } + + private String color(String message) { + return ChatColor.translateAlternateColorCodes('&', message); + } + + private void playGlobalSound(String soundPath, String fallback) { + float volume = (float) plugin.getConfig().getDouble(soundPath.replace(".sound", ".volume"), 1.0); + float pitch = (float) plugin.getConfig().getDouble(soundPath.replace(".sound", ".pitch"), 1.0); + String configured = plugin.getConfig().getString(soundPath, fallback); + Sound sound = resolveSound(configured, fallback); + for (Player player : Bukkit.getOnlinePlayers()) { + player.playSound(player.getLocation(), sound, volume, pitch); + } + } + + private void playToPlayers(List players, String soundName, float volume, float pitch) { + Sound sound = resolveSound(soundName, "BLOCK_NOTE_BLOCK_HAT"); + for (Player player : players) { + player.playSound(player.getLocation(), sound, volume, pitch); + } + } + + private Sound resolveSound(String configuredValue, String fallbackValue) { + Sound parsed = parseSound(configuredValue); + if (parsed != null) { + return parsed; + } + + Sound fallback = parseSound(fallbackValue); + if (fallback != null) { + warnInvalidSound(configuredValue, fallbackValue); + return fallback; + } + + warnInvalidSound(configuredValue, "BLOCK_NOTE_BLOCK_HAT"); + return Sound.BLOCK_NOTE_BLOCK_HAT; + } + + private Sound parseSound(String value) { + if (value == null || value.isBlank()) { + return null; + } + + String trimmed = value.trim(); + try { + return Sound.valueOf(trimmed.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ignored) { + // Try converting namespaced/dotted keys to enum-like names. + } + + String normalized = trimmed.toUpperCase(Locale.ROOT) + .replace(':', '_') + .replace('.', '_') + .replace('/', '_') + .replace('-', '_'); + try { + return Sound.valueOf(normalized); + } catch (IllegalArgumentException ignored) { + return null; + } + } + + private void warnInvalidSound(String configured, String fallback) { + String key = String.valueOf(configured) + "->" + fallback; + if (warnedInvalidSounds.add(key)) { + plugin.getLogger().warning("Invalid sound '" + configured + "'. Falling back to '" + fallback + "'."); + } + } + + + private void showResultTitle(Player player, boolean win) { + if (win) { + player.sendTitle(color("&aDu hast gewonnen"), color("&7Du wirst in 5 Sekunden teleportiert"), 10, 60, 20); + } else { + player.sendTitle(color("&cDu hast verloren"), color("&7Viel Glück nächste Runde"), 10, 60, 20); + } + } + + private void showRoleTitle(Player player, Role role) { + if (role == Role.BOSS) { + player.sendTitle(color("&cDu bist der Boss"), color("&7Eliminiere alle Hunter"), 10, 50, 15); + return; + } + + player.sendTitle(color("&bDu bist ein Hunter"), color("&7Besiege den Boss"), 10, 50, 15); + } + + private static final class GameSession { + private final String id; + private final Set lobbyPlayers = new LinkedHashSet<>(); + private final Set roundParticipants = new HashSet<>(); + private final Set placedWater = new HashSet<>(); + private final Map roles = new HashMap<>(); + private GameState state = GameState.WAITING; + private BukkitTask countdownTask; + private BossBar countdownBossBar; + private int activeCountdownSeconds; + private boolean countdownForFullLobby; + + private GameSession(String id) { + this.id = id; + } + } + + private static final class PlacedBlockKey { + private final UUID worldId; + private final int x; + private final int y; + private final int z; + + private PlacedBlockKey(UUID worldId, int x, int y, int z) { + this.worldId = worldId; + this.x = x; + this.y = y; + this.z = z; + } + + private static PlacedBlockKey of(Location location) { + return new PlacedBlockKey( + location.getWorld().getUID(), + location.getBlockX(), + location.getBlockY(), + location.getBlockZ()); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof PlacedBlockKey that)) { + return false; + } + return x == that.x && y == that.y && z == that.z && worldId.equals(that.worldId); + } + + @Override + public int hashCode() { + return Objects.hash(worldId, x, y, z); + } + } +} diff --git a/src/main/java/de/winniepat/pierredufaulersack/game/GameState.java b/src/main/java/de/winniepat/pierredufaulersack/game/GameState.java new file mode 100644 index 0000000..a553b9c --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/game/GameState.java @@ -0,0 +1,7 @@ +package de.winniepat.pierredufaulersack.game; + +public enum GameState { + WAITING, + COUNTDOWN, + RUNNING +} diff --git a/src/main/java/de/winniepat/pierredufaulersack/game/LuckPermsService.java b/src/main/java/de/winniepat/pierredufaulersack/game/LuckPermsService.java new file mode 100644 index 0000000..426374e --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/game/LuckPermsService.java @@ -0,0 +1,94 @@ +package de.winniepat.pierredufaulersack.game; + +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import net.luckperms.api.LuckPerms; +import net.luckperms.api.LuckPermsProvider; +import net.luckperms.api.node.types.PrefixNode; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +public class LuckPermsService { + + private static final String BOSS_PREFIX = "&c[ʙᴏꜱꜱ] &r"; + private static final String HUNTER_PREFIX = "&9[ʜᴜɴᴛᴇʀ] &r"; + + private final JavaPlugin plugin; + private final Map appliedPrefixes = new ConcurrentHashMap<>(); + private LuckPerms luckPerms; + + public LuckPermsService(JavaPlugin plugin) { + this.plugin = plugin; + initialize(); + } + + public boolean isAvailable() { + return luckPerms != null; + } + + public void applyBossPrefix(Player player) { + applyPrefix(player.getUniqueId(), BOSS_PREFIX, 100); + } + + public void applyHunterPrefix(Player player) { + applyPrefix(player.getUniqueId(), HUNTER_PREFIX, 90); + } + + public void clearPrefix(Player player) { + clearPrefix(player.getUniqueId()); + } + + public void clearPrefix(UUID playerId) { + if (!isAvailable()) { + return; + } + + PrefixNode previous = appliedPrefixes.remove(playerId); + if (previous == null) { + return; + } + + luckPerms.getUserManager().modifyUser(playerId, user -> user.transientData().remove(previous)); + } + + public void clearAll() { + Set tracked = Set.copyOf(appliedPrefixes.keySet()); + for (UUID playerId : tracked) { + clearPrefix(playerId); + } + } + + private void applyPrefix(UUID playerId, String prefix, int priority) { + if (!isAvailable()) { + return; + } + + PrefixNode next = PrefixNode.builder(prefix, priority).build(); + PrefixNode previous = appliedPrefixes.put(playerId, next); + luckPerms.getUserManager().modifyUser(playerId, user -> { + if (previous != null) { + user.transientData().remove(previous); + } + user.transientData().add(next); + }); + } + + private void initialize() { + if (Bukkit.getPluginManager().getPlugin("LuckPerms") == null) { + plugin.getLogger().info("LuckPerms not found. Prefix integration disabled."); + return; + } + + try { + luckPerms = LuckPermsProvider.get(); + plugin.getLogger().info("LuckPerms detected. Prefix integration enabled."); + } catch (IllegalStateException ex) { + plugin.getLogger().warning("LuckPerms detected but API is not ready. Prefix integration disabled."); + luckPerms = null; + } + } +} + diff --git a/src/main/java/de/winniepat/pierredufaulersack/game/Role.java b/src/main/java/de/winniepat/pierredufaulersack/game/Role.java new file mode 100644 index 0000000..adf4de2 --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/game/Role.java @@ -0,0 +1,7 @@ +package de.winniepat.pierredufaulersack.game; + +public enum Role { + BOSS, + HUNTER +} + diff --git a/src/main/java/de/winniepat/pierredufaulersack/game/SidebarService.java b/src/main/java/de/winniepat/pierredufaulersack/game/SidebarService.java new file mode 100644 index 0000000..2f5fe50 --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/game/SidebarService.java @@ -0,0 +1,150 @@ +package de.winniepat.pierredufaulersack.game; + +import java.util.TreeSet; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.scoreboard.*; + +public class SidebarService { + + private static final String OBJECTIVE_ID = "bosshunt"; + private static final String TEAM_BOSS = "0_boss"; + private static final String TEAM_HUNTER_SAME_QUEUE = "1_hunter_same"; + private static final String TEAM_HUNTER_OTHER_QUEUE = "1_hunter_other"; + private static final String TEAM_NORMAL = "2_normal"; + + private final JavaPlugin plugin; + private final GameManager gameManager; + private BukkitTask refreshTask; + + public SidebarService(JavaPlugin plugin, GameManager gameManager) { + this.plugin = plugin; + this.gameManager = gameManager; + } + + public void start() { + refreshTask = Bukkit.getScheduler().runTaskTimer(plugin, this::refreshAll, 0L, 20L); + } + + public void refreshAll() { + for (Player player : Bukkit.getOnlinePlayers()) { + showFor(player); + } + } + + public void showFor(Player player) { + ScoreboardManager manager = Bukkit.getScoreboardManager(); + if (manager == null) { + return; + } + + Scoreboard board = manager.getNewScoreboard(); + Objective objective = board.registerNewObjective(OBJECTIVE_ID, "dummy", color("&6&lʙᴏꜱꜱʜᴜɴᴛ")); + objective.setDisplaySlot(DisplaySlot.SIDEBAR); + + applyTablistOrderingTeams(board, player); + + Role role = gameManager.getRole(player.getUniqueId()); + if (role != null) { + String queueId = gameManager.queueOfPlayer(player.getUniqueId()); + showRunningSession(objective, player, role, queueId); + } else { + showLobbyView(objective, player); + } + + player.setScoreboard(board); + } + + public void clearFor(Player player) { + ScoreboardManager manager = Bukkit.getScoreboardManager(); + if (manager != null) { + player.setScoreboard(manager.getMainScoreboard()); + } + } + + public void shutdown() { + if (refreshTask != null) { + refreshTask.cancel(); + refreshTask = null; + } + + for (Player player : Bukkit.getOnlinePlayers()) { + clearFor(player); + } + } + + private void showRunningSession(Objective objective, Player player, Role role, String queueId) { + int line = 9; + + objective.getScore(" ").setScore(line--); + objective.getScore(color("&7Status: &aLÄUFT")).setScore(line--); + objective.getScore(" ").setScore(line--); + objective.getScore(color("&fQueue: &e" + queueId)).setScore(line--); + objective.getScore(color("&fRolle: " + roleText(role))).setScore(line--); + objective.getScore(color("&fHunter Verbleibend: &b" + gameManager.huntersLeft(player.getUniqueId()))).setScore(line--); + objective.getScore(" ").setScore(line--); + objective.getScore(color("&eKämpft!")).setScore(line); + } + + private void applyTablistOrderingTeams(Scoreboard board, Player viewer) { + Team bosses = board.registerNewTeam(TEAM_BOSS); + Team huntersSameQueue = board.registerNewTeam(TEAM_HUNTER_SAME_QUEUE); + Team huntersOtherQueue = board.registerNewTeam(TEAM_HUNTER_OTHER_QUEUE); + Team normals = board.registerNewTeam(TEAM_NORMAL); + + huntersSameQueue.setColor(ChatColor.RED); + + String viewerQueue = gameManager.queueOfPlayer(viewer.getUniqueId()); + for (Player online : Bukkit.getOnlinePlayers()) { + Role onlineRole = gameManager.getRole(online.getUniqueId()); + if (onlineRole == Role.BOSS) { + bosses.addEntry(online.getName()); + continue; + } + + if (onlineRole == Role.HUNTER) { + String onlineQueue = gameManager.queueOfPlayer(online.getUniqueId()); + if (viewerQueue != null && viewerQueue.equals(onlineQueue)) { + huntersSameQueue.addEntry(online.getName()); + } else { + huntersOtherQueue.addEntry(online.getName()); + } + continue; + } + + normals.addEntry(online.getName()); + } + } + + private void showLobbyView(Objective objective, Player player) { + int line = 12; + + String selected = gameManager.getSelectedQueue(player.getUniqueId()); + String currentQueue = gameManager.queueOfPlayer(player.getUniqueId()); + String selectedText = (selected == null || selected.isBlank()) ? "-" : selected; + + objective.getScore(color("&7Status: &7LOBBY")).setScore(line--); + if (currentQueue != null) { + objective.getScore(color("&fQueued: &aJa (&e" + currentQueue + "&a)")).setScore(line--); + } + objective.getScore(" ").setScore(line--); + + for (String queueId : new TreeSet<>(gameManager.queueIds())) { + objective.getScore(color("&e" + queueId + ": &f" + gameManager.lobbySize(queueId) + "&7/&f" + gameManager.maxPlayers())).setScore(line--); + } + } + + private String roleText(Role role) { + if (role == Role.BOSS) { + return "&6BOSS"; + } + return "&bHUNTER"; + } + + private String color(String text) { + return ChatColor.translateAlternateColorCodes('&', text); + } +} diff --git a/src/main/java/de/winniepat/pierredufaulersack/kits/KitService.java b/src/main/java/de/winniepat/pierredufaulersack/kits/KitService.java new file mode 100644 index 0000000..397596a --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/kits/KitService.java @@ -0,0 +1,346 @@ +package de.winniepat.pierredufaulersack.kits; + +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.IntPredicate; +import java.util.UUID; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.java.JavaPlugin; + +public class KitService { + + private static final int MIN_SLOT = 1; + private static final int MAX_SLOT = 3; + private static final int KIT_CONTENT_SIZE = 41; + + private final JavaPlugin plugin; + + public KitService(JavaPlugin plugin) { + this.plugin = plugin; + } + + public boolean addKit(KitType type, int slot, Player player) { + validateSlot(slot); + String path = path(type, slot); + if (plugin.getConfig().contains(path)) { + return false; + } + setKit(type, slot, player); + return true; + } + + public void setKit(KitType type, int slot, Player player) { + setKit(type, slot, player.getInventory().getContents()); + } + + public void setKit(KitType type, int slot, ItemStack[] contents) { + validateSlot(slot); + + ItemStack[] normalizedContents = normalizeContents(contents); + List> serialized = new ArrayList<>(normalizedContents.length); + for (ItemStack item : normalizedContents) { + serialized.add(item == null ? null : item.clone().serialize()); + } + + plugin.getConfig().set(path(type, slot), serialized); + plugin.saveConfig(); + } + + public boolean deleteKit(KitType type, int slot) { + validateSlot(slot); + String path = path(type, slot); + if (!plugin.getConfig().contains(path)) { + return false; + } + plugin.getConfig().set(path, null); + plugin.saveConfig(); + return true; + } + + public List listKitSlots(KitType type) { + List slots = new ArrayList<>(); + for (int slot = MIN_SLOT; slot <= MAX_SLOT; slot++) { + if (hasKit(type, slot)) { + slots.add(slot); + } + } + return slots; + } + + public boolean hasKit(KitType type, int slot) { + validateSlot(slot); + return plugin.getConfig().contains(path(type, slot)); + } + + public Optional> getKit(KitType type, int slot) { + validateSlot(slot); + + List raw = plugin.getConfig().getList(path(type, slot)); + if (raw == null || raw.isEmpty()) { + return Optional.empty(); + } + + List saved = new ArrayList<>(KIT_CONTENT_SIZE); + boolean hasItem = false; + + for (int index = 0; index < KIT_CONTENT_SIZE; index++) { + Object element = index < raw.size() ? raw.get(index) : null; + ItemStack item = deserializeItem(element); + if (item != null) { + hasItem = true; + } + saved.add(item); + } + + if (!hasItem) { + return Optional.empty(); + } + + return Optional.of(cloneInventory(saved)); + } + + public Optional> getKitForPlayer(UUID playerId, KitType type, int slot) { + Optional> maybeBase = getKit(type, slot); + if (maybeBase.isEmpty()) { + return Optional.empty(); + } + + List base = maybeBase.get(); + Optional maybeLayout = getPlayerLayout(playerId, type, slot); + if (maybeLayout.isEmpty()) { + return Optional.of(cloneInventory(base)); + } + + return Optional.of(applyLayout(base, maybeLayout.get())); + } + + public boolean setPlayerLayout(UUID playerId, KitType type, int slot, ItemStack[] arrangedContents) { + validateSlot(slot); + + Optional> maybeBase = getKit(type, slot); + if (maybeBase.isEmpty()) { + return false; + } + + List base = maybeBase.get(); + ItemStack[] arranged = normalizeContents(arrangedContents); + int[] layout = deriveLayout(base, arranged); + + List serialized = new ArrayList<>(layout.length); + for (int value : layout) { + serialized.add(value); + } + + plugin.getConfig().set(playerLayoutPath(playerId, type, slot), serialized); + plugin.saveConfig(); + return true; + } + + private Optional getPlayerLayout(UUID playerId, KitType type, int slot) { + String path = playerLayoutPath(playerId, type, slot); + List raw = plugin.getConfig().getList(path); + if (raw == null || raw.size() != KIT_CONTENT_SIZE) { + return Optional.empty(); + } + + int[] layout = new int[KIT_CONTENT_SIZE]; + for (int i = 0; i < KIT_CONTENT_SIZE; i++) { + Object element = raw.get(i); + if (!(element instanceof Number number)) { + return Optional.empty(); + } + + int value = number.intValue(); + if (value < 0 || value >= KIT_CONTENT_SIZE) { + return Optional.empty(); + } + layout[i] = value; + } + + return Optional.of(layout); + } + + private static List applyLayout(List base, int[] layout) { + List arranged = new ArrayList<>(KIT_CONTENT_SIZE); + boolean[] consumed = new boolean[KIT_CONTENT_SIZE]; + + for (int target = 0; target < KIT_CONTENT_SIZE; target++) { + int source = target < layout.length ? layout[target] : target; + if (source < 0 || source >= KIT_CONTENT_SIZE || consumed[source]) { + source = firstUnused(consumed, target); + } + consumed[source] = true; + + ItemStack item = source < base.size() ? base.get(source) : null; + arranged.add(item == null ? null : item.clone()); + } + + return arranged; + } + + private static int[] deriveLayout(List base, ItemStack[] arranged) { + int[] layout = new int[KIT_CONTENT_SIZE]; + boolean[] used = new boolean[KIT_CONTENT_SIZE]; + + for (int target = 0; target < KIT_CONTENT_SIZE; target++) { + ItemStack desired = arranged[target]; + int matched = -1; + + for (int source = 0; source < KIT_CONTENT_SIZE; source++) { + if (used[source]) { + continue; + } + + ItemStack sourceItem = source < base.size() ? base.get(source) : null; + if (isSameStack(sourceItem, desired)) { + matched = source; + break; + } + } + + if (matched == -1) { + matched = firstUnused(used, target); + } + + used[matched] = true; + layout[target] = matched; + } + + return layout; + } + + private static int firstUnused(boolean[] used, int fallback) { + if (fallback >= 0 && fallback < used.length && !used[fallback]) { + return fallback; + } + + for (int i = 0; i < used.length; i++) { + if (!used[i]) { + return i; + } + } + + return 0; + } + + private static boolean isSameStack(ItemStack a, ItemStack b) { + if (a == null || a.getType().isAir()) { + return b == null || b.getType().isAir(); + } + if (b == null || b.getType().isAir()) { + return false; + } + return a.getAmount() == b.getAmount() && a.isSimilar(b); + } + + private static ItemStack deserializeItem(Object element) { + if (element == null) { + return null; + } + + if (element instanceof ItemStack itemStack) { + return itemStack.clone(); + } + + if (element instanceof Map map) { + Map serialized = new LinkedHashMap<>(); + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey() instanceof String key) { + serialized.put(key, entry.getValue()); + } + } + + if (!serialized.isEmpty()) { + try { + return ItemStack.deserialize(serialized); + } catch (IllegalArgumentException ignored) { + return null; + } + } + } + + return null; + } + + public OptionalInt rollKitSlot(KitType type, IntPredicate allowedSlot) { + List slots = new ArrayList<>(); + List weights = new ArrayList<>(); + int total = 0; + + for (int slot = MIN_SLOT; slot <= MAX_SLOT; slot++) { + if (!allowedSlot.test(slot) || !hasKit(type, slot)) { + continue; + } + + int weight = getChance(type, slot); + if (weight <= 0) { + continue; + } + + slots.add(slot); + weights.add(weight); + total += weight; + } + + if (total <= 0) { + return OptionalInt.empty(); + } + + int roll = ThreadLocalRandom.current().nextInt(total); + int cursor = 0; + for (int i = 0; i < slots.size(); i++) { + cursor += weights.get(i); + if (roll < cursor) { + return OptionalInt.of(slots.get(i)); + } + } + + return OptionalInt.empty(); + } + + public OptionalInt rollKitSlot(KitType type) { + return rollKitSlot(type, slot -> true); + } + + private int getChance(KitType type, int slot) { + String chancePath = "kit-chances." + type.key() + "." + slot; + return plugin.getConfig().getInt(chancePath, 0); + } + + private static ItemStack[] normalizeContents(ItemStack[] contents) { + ItemStack[] normalized = new ItemStack[KIT_CONTENT_SIZE]; + if (contents == null) { + return normalized; + } + + int copyLength = Math.min(KIT_CONTENT_SIZE, contents.length); + for (int index = 0; index < copyLength; index++) { + ItemStack item = contents[index]; + normalized[index] = item == null ? null : item.clone(); + } + return normalized; + } + + private static List cloneInventory(List contents) { + List cloned = new ArrayList<>(contents.size()); + for (ItemStack item : contents) { + cloned.add(item == null ? null : item.clone()); + } + return cloned; + } + + private static String path(KitType type, int slot) { + return "kits." + type.key() + "." + slot; + } + + private static String playerLayoutPath(UUID playerId, KitType type, int slot) { + return "player-kit-layouts." + playerId + "." + type.key() + "." + slot; + } + + private static void validateSlot(int slot) { + if (slot < MIN_SLOT || slot > MAX_SLOT) { + throw new IllegalArgumentException("Slot must be between 1 and 3"); + } + } +} diff --git a/src/main/java/de/winniepat/pierredufaulersack/kits/KitType.java b/src/main/java/de/winniepat/pierredufaulersack/kits/KitType.java new file mode 100644 index 0000000..1eaf109 --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/kits/KitType.java @@ -0,0 +1,17 @@ +package de.winniepat.pierredufaulersack.kits; + +public enum KitType { + BOSS("boss"), + HUNTER("hunter"); + + private final String key; + + KitType(String key) { + this.key = key; + } + + public String key() { + return key; + } +} + diff --git a/src/main/java/de/winniepat/pierredufaulersack/listeners/CobwebDecayListener.java b/src/main/java/de/winniepat/pierredufaulersack/listeners/CobwebDecayListener.java new file mode 100644 index 0000000..5ba1a56 --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/listeners/CobwebDecayListener.java @@ -0,0 +1,59 @@ +package de.winniepat.pierredufaulersack.listeners; + +import de.winniepat.pierredufaulersack.game.GameManager; +import java.util.UUID; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.plugin.java.JavaPlugin; + +public class CobwebDecayListener implements Listener { + + private static final long COBWEB_LIFETIME_TICKS = 20L * 10L; + + private final JavaPlugin plugin; + private final GameManager gameManager; + + public CobwebDecayListener(JavaPlugin plugin, GameManager gameManager) { + this.plugin = plugin; + this.gameManager = gameManager; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlace(BlockPlaceEvent event) { + if (event.getBlockPlaced().getType() != Material.COBWEB) { + return; + } + + Player player = event.getPlayer(); + if (gameManager.getRole(player.getUniqueId()) == null) { + return; + } + + Location location = event.getBlockPlaced().getLocation(); + UUID worldId = location.getWorld() == null ? null : location.getWorld().getUID(); + int x = location.getBlockX(); + int y = location.getBlockY(); + int z = location.getBlockZ(); + + plugin.getServer().getScheduler().runTaskLater(plugin, () -> { + if (worldId == null) { + return; + } + World world = plugin.getServer().getWorld(worldId); + if (world == null) { + return; + } + + if (world.getBlockAt(x, y, z).getType() == Material.COBWEB) { + world.getBlockAt(x, y, z).setType(Material.AIR, false); + } + }, COBWEB_LIFETIME_TICKS); + } +} + diff --git a/src/main/java/de/winniepat/pierredufaulersack/listeners/FriendlyFireListener.java b/src/main/java/de/winniepat/pierredufaulersack/listeners/FriendlyFireListener.java new file mode 100644 index 0000000..94ed0cd --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/listeners/FriendlyFireListener.java @@ -0,0 +1,44 @@ +package de.winniepat.pierredufaulersack.listeners; + +import de.winniepat.pierredufaulersack.game.GameManager; +import org.bukkit.entity.*; +import org.bukkit.event.*; +import org.bukkit.event.entity.EntityDamageByEntityEvent; + +public class FriendlyFireListener implements Listener { + + private final GameManager gameManager; + + public FriendlyFireListener(GameManager gameManager) { + this.gameManager = gameManager; + } + + @EventHandler(ignoreCancelled = true) + public void onDamage(EntityDamageByEntityEvent event) { + if (!gameManager.isRunning()) { + return; + } + + Player attacker = asPlayerDamager(event.getDamager()); + if (attacker == null || !(event.getEntity() instanceof Player victim)) { + return; + } + + if (gameManager.areBothHunters(attacker.getUniqueId(), victim.getUniqueId())) { + event.setCancelled(true); + } + } + + private Player asPlayerDamager(Entity entity) { + if (entity instanceof Player player) { + return player; + } + + if (entity instanceof Projectile projectile && projectile.getShooter() instanceof Player shooter) { + return shooter; + } + + return null; + } +} + diff --git a/src/main/java/de/winniepat/pierredufaulersack/listeners/PlayerConnectionListener.java b/src/main/java/de/winniepat/pierredufaulersack/listeners/PlayerConnectionListener.java new file mode 100644 index 0000000..b1e203e --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/listeners/PlayerConnectionListener.java @@ -0,0 +1,36 @@ +package de.winniepat.pierredufaulersack.listeners; + +import de.winniepat.pierredufaulersack.game.*; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.*; +import org.bukkit.event.player.*; + +public class PlayerConnectionListener implements Listener { + + private final GameManager gameManager; + private final SidebarService sidebarService; + + public PlayerConnectionListener(GameManager gameManager, SidebarService sidebarService) { + this.gameManager = gameManager; + this.sidebarService = sidebarService; + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + sidebarService.showFor(event.getPlayer()); + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + gameManager.handlePlayerQuit(event.getPlayer()); + sidebarService.clearFor(event.getPlayer()); + + event.setQuitMessage(null); + + for (Player p : Bukkit.getOnlinePlayers()) { + p.sendMessage(Component.text("§4- §r" + event.getPlayer().getDisplayName())); + } + } +} diff --git a/src/main/java/de/winniepat/pierredufaulersack/listeners/PlayerDeathListener.java b/src/main/java/de/winniepat/pierredufaulersack/listeners/PlayerDeathListener.java new file mode 100644 index 0000000..bdca211 --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/listeners/PlayerDeathListener.java @@ -0,0 +1,21 @@ +package de.winniepat.pierredufaulersack.listeners; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.inventory.ItemStack; + +public class PlayerDeathListener implements Listener { + + @EventHandler + public void onPlayerDeath(PlayerDeathEvent event) { + event.setDeathMessage(null); + + Player victim = event.getEntity(); + victim.getInventory().clear(); + victim.getInventory().setArmorContents(new ItemStack[0]); + victim.getInventory().setItemInOffHand(new ItemStack(Material.AIR)); + } +} diff --git a/src/main/java/de/winniepat/pierredufaulersack/listeners/PlayerJoinListener.java b/src/main/java/de/winniepat/pierredufaulersack/listeners/PlayerJoinListener.java new file mode 100644 index 0000000..6aa7454 --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/listeners/PlayerJoinListener.java @@ -0,0 +1,757 @@ +package de.winniepat.pierredufaulersack.listeners; + +import de.winniepat.pierredufaulersack.Pierredufaulersack; +import de.winniepat.pierredufaulersack.game.GameManager; +import de.winniepat.pierredufaulersack.kits.KitService; +import de.winniepat.pierredufaulersack.kits.KitType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerRespawnEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.potion.PotionEffect; + +public class PlayerJoinListener implements Listener { + + private static final String SELECTOR_KEY = "queue_selector"; + private static final String QUEUE_ITEM_KEY = "queue_item"; + private static final String LEAVE_ITEM_KEY = "leave_item"; + private static final String KIT_EDITOR_KEY = "kit_editor"; + private static final String KIT_TYPE_ITEM_KEY = "kit_editor_type"; + private static final String KIT_SLOT_ITEM_KEY = "kit_editor_slot"; + private static final String LEAVE_ACTION = "leave"; + private static final String QUEUE_MENU_TITLE = "&8Queue Auswahl"; + private static final String KIT_TYPE_MENU_TITLE = "&8Kit Editor - Rolle"; + private static final int KIT_STORAGE_SIZE = 36; + private static final int KIT_TOTAL_SIZE = 41; + private static final int EDITOR_SIZE = 54; + private static final int EDITOR_BOOTS_SLOT = 45; + private static final int EDITOR_LEGGINGS_SLOT = 46; + private static final int EDITOR_CHESTPLATE_SLOT = 47; + private static final int EDITOR_HELMET_SLOT = 48; + private static final int EDITOR_OFFHAND_SLOT = 50; + + private final Pierredufaulersack plugin; + private final GameManager gameManager; + private final KitService kitService; + private final NamespacedKey selectorItemKey; + private final NamespacedKey queueItemKey; + private final NamespacedKey leaveItemKey; + private final NamespacedKey kitEditorItemKey; + private final NamespacedKey kitTypeItemKey; + private final NamespacedKey kitSlotItemKey; + + public PlayerJoinListener(Pierredufaulersack plugin, GameManager gameManager, KitService kitService) { + this.plugin = plugin; + this.gameManager = gameManager; + this.kitService = kitService; + this.selectorItemKey = new NamespacedKey(plugin, SELECTOR_KEY); + this.queueItemKey = new NamespacedKey(plugin, QUEUE_ITEM_KEY); + this.leaveItemKey = new NamespacedKey(plugin, LEAVE_ITEM_KEY); + this.kitEditorItemKey = new NamespacedKey(plugin, KIT_EDITOR_KEY); + this.kitTypeItemKey = new NamespacedKey(plugin, KIT_TYPE_ITEM_KEY); + this.kitSlotItemKey = new NamespacedKey(plugin, KIT_SLOT_ITEM_KEY); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + + gameManager.handlePlayerQuit(player); + + event.setJoinMessage(null); + + for (Player p : Bukkit.getOnlinePlayers()) { + p.sendMessage(Component.text("§a+ §r" + player.getDisplayName())); + } + + Location lobby = loadLocation("locations.lobby"); + if (lobby != null) { + player.teleport(lobby); + } + + player.getInventory().clear(); + player.getInventory().setArmorContents(new ItemStack[4]); + player.getInventory().setItemInOffHand(null); + for (PotionEffect effect : player.getActivePotionEffects()) { + player.removePotionEffect(effect.getType()); + } + + Bukkit.getScheduler().runTask(plugin, () -> giveLobbyItems(player)); + } + + @EventHandler + public void onRespawn(PlayerRespawnEvent event) { + Player player = event.getPlayer(); + Bukkit.getScheduler().runTask(plugin, () -> { + if (gameManager.getRole(player.getUniqueId()) != null) { + return; + } + if (isLobbyLocation(player.getLocation()) || isLobbyLocation(event.getRespawnLocation())) { + giveLobbyItems(player); + } + }); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onTeleport(PlayerTeleportEvent event) { + if (isLobbyLocation(event.getTo())) { + Bukkit.getScheduler().runTask(plugin, () -> giveLobbyItems(event.getPlayer())); + } + } + + @EventHandler + public void onSelectorUse(PlayerInteractEvent event) { + if (event.getHand() != EquipmentSlot.HAND) { + return; + } + + Action action = event.getAction(); + if (action != Action.RIGHT_CLICK_AIR && action != Action.RIGHT_CLICK_BLOCK) { + return; + } + + ItemStack used = event.getItem(); + if (used == null) { + used = event.getPlayer().getInventory().getItemInMainHand(); + } + + Player player = event.getPlayer(); + if (gameManager.getRole(player.getUniqueId()) != null) { + return; + } + + if (isQueueSelector(used)) { + event.setCancelled(true); + Bukkit.getScheduler().runTask(plugin, () -> openQueueSelectorMenu(player)); + return; + } + + if (isKitEditorOpener(used)) { + event.setCancelled(true); + Bukkit.getScheduler().runTask(plugin, () -> openKitTypeMenu(player)); + } + } + + @EventHandler(ignoreCancelled = true) + public void onInventoryClick(InventoryClickEvent event) { + Inventory topInventory = event.getView().getTopInventory(); + + if (isQueueMenu(topInventory)) { + event.setCancelled(true); + if (event.getClickedInventory() == null || event.getClickedInventory() != topInventory) { + return; + } + + if (!(event.getWhoClicked() instanceof Player player)) { + return; + } + + ItemStack clicked = event.getCurrentItem(); + if (clicked == null || !clicked.hasItemMeta()) { + return; + } + + ItemMeta meta = clicked.getItemMeta(); + if (meta == null) { + return; + } + + String queueId = meta.getPersistentDataContainer().get(queueItemKey, PersistentDataType.STRING); + String leaveAction = meta.getPersistentDataContainer().get(leaveItemKey, PersistentDataType.STRING); + + if (LEAVE_ACTION.equals(leaveAction)) { + gameManager.leave(player); + gameManager.clearSelectedQueue(player); + giveLobbyItems(player); + player.closeInventory(); + return; + } + + if (queueId == null || queueId.isBlank()) { + return; + } + + gameManager.selectQueue(player, queueId); + gameManager.join(player, queueId); + giveLobbyItems(player); + player.closeInventory(); + return; + } + + if (isKitTypeMenu(topInventory)) { + event.setCancelled(true); + if (event.getClickedInventory() == null || event.getClickedInventory() != topInventory) { + return; + } + if (!(event.getWhoClicked() instanceof Player player)) { + return; + } + + ItemStack clicked = event.getCurrentItem(); + if (clicked == null || !clicked.hasItemMeta()) { + return; + } + ItemMeta meta = clicked.getItemMeta(); + if (meta == null) { + return; + } + + String rawType = meta.getPersistentDataContainer().get(kitTypeItemKey, PersistentDataType.STRING); + KitType type = parseKitType(rawType); + if (type == null) { + return; + } + + openKitSlotMenu(player, type); + return; + } + + if (topInventory != null && topInventory.getHolder() instanceof KitSlotMenuHolder slotHolder) { + event.setCancelled(true); + if (event.getClickedInventory() == null || event.getClickedInventory() != topInventory) { + return; + } + if (!(event.getWhoClicked() instanceof Player player)) { + return; + } + + ItemStack clicked = event.getCurrentItem(); + if (clicked == null || !clicked.hasItemMeta()) { + return; + } + ItemMeta meta = clicked.getItemMeta(); + if (meta == null) { + return; + } + + Integer slot = meta.getPersistentDataContainer().get(kitSlotItemKey, PersistentDataType.INTEGER); + if (slot == null || slot < 1 || slot > 3) { + return; + } + + openKitEditor(player, slotHolder.type(), slot); + return; + } + + if (topInventory.getHolder() instanceof KitEditorMenuHolder) { + if (event.getClickedInventory() == null) { + return; + } + + if (event.getClickedInventory() != topInventory) { + event.setCancelled(true); + return; + } + + if (event.isShiftClick() + || event.getAction() == InventoryAction.MOVE_TO_OTHER_INVENTORY + || event.getAction() == InventoryAction.DROP_ALL_SLOT + || event.getAction() == InventoryAction.DROP_ONE_SLOT + || event.getAction() == InventoryAction.DROP_ALL_CURSOR + || event.getAction() == InventoryAction.DROP_ONE_CURSOR + || event.getClick() == ClickType.NUMBER_KEY + || event.getClick() == ClickType.SWAP_OFFHAND + || event.getClick() == ClickType.DOUBLE_CLICK + || event.getClick() == ClickType.DROP + || event.getClick() == ClickType.CONTROL_DROP) { + event.setCancelled(true); + } + } + } + + @EventHandler(ignoreCancelled = true) + public void onInventoryDrag(InventoryDragEvent event) { + Inventory topInventory = event.getView().getTopInventory(); + if (isQueueMenu(topInventory) + || isKitTypeMenu(topInventory) + || topInventory.getHolder() instanceof KitSlotMenuHolder) { + event.setCancelled(true); + return; + } + + if (topInventory.getHolder() instanceof KitEditorMenuHolder) { + int topSize = topInventory.getSize(); + for (int rawSlot : event.getRawSlots()) { + if (rawSlot >= topSize) { + event.setCancelled(true); + return; + } + } + } + } + + @EventHandler + public void onKitEditorClose(InventoryCloseEvent event) { + Inventory inventory = event.getInventory(); + if (!(inventory.getHolder() instanceof KitEditorMenuHolder editorHolder)) { + return; + } + + if (event.getPlayer() instanceof Player player) { + ItemStack cursor = player.getItemOnCursor(); + if (hasItem(cursor)) { + ItemStack leftover = placeIntoEditableSlots(inventory, cursor.clone()); + if (hasItem(leftover)) { + player.getWorld().dropItemNaturally(player.getLocation(), leftover); + } + player.setItemOnCursor(null); + } + + boolean saved = kitService.setPlayerLayout( + player.getUniqueId(), + editorHolder.type(), + editorHolder.slot(), + extractKitFromEditor(inventory)); + + if (!saved) { + player.sendMessage(color("&8[&6BossHunt&8] &cKein Basis-Kit vorhanden. Layout nicht gespeichert.")); + return; + } + + player.sendMessage(color("&8[&6BossHunt&8] &aKit &e" + editorHolder.slot() + " &afür &e" + + typeDisplayName(editorHolder.type()) + " &aAnordnung gespeichert.")); + return; + } + + // Non-player close is rare, but keep layout persistence consistent. + kitService.setPlayerLayout( + event.getPlayer().getUniqueId(), + editorHolder.type(), + editorHolder.slot(), + extractKitFromEditor(inventory)); + } + + private void giveLobbyItems(Player player) { + if (gameManager.getRole(player.getUniqueId()) != null) { + return; + } + + ItemStack selector = new ItemStack(Material.COMPASS); + ItemMeta selectorMeta = selector.getItemMeta(); + if (selectorMeta != null) { + String selected = gameManager.getSelectedQueue(player.getUniqueId()); + String selectedText = selected == null || selected.isBlank() ? "None" : selected; + selectorMeta.setDisplayName(color("&6Queue Auswahl")); + selectorMeta.setLore(List.of( + color("&7Gerade: &e" + selectedText), + color("&7Clicke um das Menü zu öffnen"))); + selectorMeta.getPersistentDataContainer().set(selectorItemKey, PersistentDataType.BYTE, (byte) 1); + selector.setItemMeta(selectorMeta); + } + + ItemStack kitEditor = new ItemStack(Material.CHEST); + ItemMeta editorMeta = kitEditor.getItemMeta(); + if (editorMeta != null) { + editorMeta.setDisplayName(color("&dKit Editor")); + editorMeta.setLore(List.of( + color("&7Clicke um Boss/Hunter Kits"), + color("&7für Slot 1-3 zu bearbeiten"))); + editorMeta.getPersistentDataContainer().set(kitEditorItemKey, PersistentDataType.BYTE, (byte) 1); + kitEditor.setItemMeta(editorMeta); + } + + player.getInventory().setItem(4, selector); + player.getInventory().setItem(6, kitEditor); + } + + private boolean isQueueSelector(ItemStack item) { + if (item == null || item.getType() != Material.COMPASS || !item.hasItemMeta()) { + return false; + } + + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return false; + } + + Byte marker = meta.getPersistentDataContainer().get(selectorItemKey, PersistentDataType.BYTE); + if (marker != null && marker == (byte) 1) { + return true; + } + + String displayName = meta.getDisplayName(); + return displayName != null && displayName.equals(color("&6Queue Selector")); + } + + private boolean isKitEditorOpener(ItemStack item) { + if (item == null || item.getType() != Material.CHEST || !item.hasItemMeta()) { + return false; + } + + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return false; + } + + Byte marker = meta.getPersistentDataContainer().get(kitEditorItemKey, PersistentDataType.BYTE); + return marker != null && marker == (byte) 1; + } + + private boolean isLobbyLocation(Location location) { + Location lobby = loadLocation("locations.lobby"); + if (location == null || lobby == null) { + return false; + } + if (location.getWorld() == null || lobby.getWorld() == null) { + return false; + } + if (!location.getWorld().getUID().equals(lobby.getWorld().getUID())) { + return false; + } + return location.distanceSquared(lobby) <= 4.0; + } + + private void openQueueSelectorMenu(Player player) { + List queueIds = new ArrayList<>(gameManager.queueIds()); + if (queueIds.isEmpty()) { + return; + } + Collections.sort(queueIds); + + int requiredSlots = queueIds.size() + 1; + int size = 9; + while (size < requiredSlots) { + size += 9; + } + + Inventory menu = Bukkit.createInventory(new QueueMenuHolder(), size, color(QUEUE_MENU_TITLE)); + String selectedQueue = gameManager.getSelectedQueue(player.getUniqueId()); + + int slot = 0; + for (String queueId : queueIds) { + if (slot == 8) { + slot++; + } + if (slot >= size) { + break; + } + boolean selected = queueId.equals(selectedQueue); + menu.setItem(slot, createQueueMenuItem(queueId, selected)); + slot++; + } + + if (size > 8) { + menu.setItem(8, createLeaveMenuItem()); + } + + player.openInventory(menu); + } + + private void openKitTypeMenu(Player player) { + Inventory menu = Bukkit.createInventory(new KitTypeMenuHolder(), 9, color(KIT_TYPE_MENU_TITLE)); + menu.setItem(3, createKitTypeMenuItem(KitType.BOSS)); + menu.setItem(5, createKitTypeMenuItem(KitType.HUNTER)); + player.openInventory(menu); + } + + private void openKitSlotMenu(Player player, KitType type) { + Inventory menu = Bukkit.createInventory(new KitSlotMenuHolder(type), 9, + color("&8Kit Editor - " + typeDisplayName(type))); + menu.setItem(2, createKitSlotMenuItem(type, 1)); + menu.setItem(4, createKitSlotMenuItem(type, 2)); + menu.setItem(6, createKitSlotMenuItem(type, 3)); + player.openInventory(menu); + } + + private void openKitEditor(Player player, KitType type, int slot) { + Inventory editor = Bukkit.createInventory(new KitEditorMenuHolder(type, slot), EDITOR_SIZE, + color("&8Edit " + typeDisplayName(type) + " Kit " + slot)); + + kitService.getKitForPlayer(player.getUniqueId(), type, slot) + .ifPresent(kit -> editor.setContents(buildEditorContents(kit))); + + player.openInventory(editor); + player.sendMessage(color("&8[&6BossHunt&8] &7Bearbeite das Kit und schließe das Inventar zum Speichern.")); + player.sendMessage(color("&8[&6BossHunt&8] &7Armor: Slots 46-49, Offhand: Slot 51.")); + } + + private ItemStack createQueueMenuItem(String queueId, boolean selected) { + Material material = selected ? Material.LIME_WOOL : Material.GRAY_WOOL; + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return item; + } + + meta.setDisplayName(color((selected ? "&a" : "&e") + queueId)); + meta.setLore(List.of( + color("&7Spieler: &f" + gameManager.lobbySize(queueId) + "&7/&f" + gameManager.maxPlayers()), + color("&7Status: &f" + gameManager.getState(queueId).name()), + color(selected ? "&aGerade ausgewählt" : "&eClicke zum auswählen"))); + meta.getPersistentDataContainer().set(queueItemKey, PersistentDataType.STRING, queueId); + + item.setItemMeta(meta); + return item; + } + + private ItemStack createLeaveMenuItem() { + ItemStack item = new ItemStack(Material.BARRIER); + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return item; + } + + meta.setDisplayName(color("&cLeave Queue/Lobby")); + meta.setLore(List.of( + color("&7Click um die queue zu verlassen"))); + meta.getPersistentDataContainer().set(leaveItemKey, PersistentDataType.STRING, LEAVE_ACTION); + + item.setItemMeta(meta); + return item; + } + + private ItemStack createKitTypeMenuItem(KitType type) { + Material material = type == KitType.BOSS ? Material.NETHERITE_CHESTPLATE : Material.IRON_CHESTPLATE; + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return item; + } + + String display = typeDisplayName(type); + meta.setDisplayName(color("&e" + display)); + meta.setLore(List.of(color("&7Clicke um " + display + " Kits zu bearbeiten"))); + meta.getPersistentDataContainer().set(kitTypeItemKey, PersistentDataType.STRING, type.key()); + item.setItemMeta(meta); + return item; + } + + private ItemStack createKitSlotMenuItem(KitType type, int slot) { + boolean hasKit = kitService.hasKit(type, slot); + ItemStack item = new ItemStack(hasKit ? Material.LIME_SHULKER_BOX : Material.GRAY_SHULKER_BOX); + ItemMeta meta = item.getItemMeta(); + if (meta == null) { + return item; + } + + meta.setDisplayName(color("&eKit " + slot)); + meta.setLore(List.of( + color(hasKit ? "&aBereits gespeichert" : "&7Noch leer"), + color("&7Clicke zum Bearbeiten"))); + meta.getPersistentDataContainer().set(kitSlotItemKey, PersistentDataType.INTEGER, slot); + + item.setItemMeta(meta); + return item; + } + + private KitType parseKitType(String raw) { + if (raw == null) { + return null; + } + + for (KitType type : KitType.values()) { + if (type.key().equalsIgnoreCase(raw)) { + return type; + } + } + + return null; + } + + private String typeDisplayName(KitType type) { + return type == KitType.BOSS ? "Boss" : "Hunter"; + } + + private ItemStack[] buildEditorContents(List kit) { + ItemStack[] editor = new ItemStack[EDITOR_SIZE]; + for (int i = 0; i < Math.min(KIT_STORAGE_SIZE, kit.size()); i++) { + ItemStack item = kit.get(i); + editor[i] = item == null ? null : item.clone(); + } + + editor[EDITOR_BOOTS_SLOT] = cloneAt(kit, 36); + editor[EDITOR_LEGGINGS_SLOT] = cloneAt(kit, 37); + editor[EDITOR_CHESTPLATE_SLOT] = cloneAt(kit, 38); + editor[EDITOR_HELMET_SLOT] = cloneAt(kit, 39); + editor[EDITOR_OFFHAND_SLOT] = cloneAt(kit, 40); + return editor; + } + + private ItemStack[] extractKitFromEditor(Inventory inventory) { + ItemStack[] editorContents = inventory.getContents(); + ItemStack[] kit = new ItemStack[KIT_TOTAL_SIZE]; + for (int i = 0; i < KIT_STORAGE_SIZE; i++) { + ItemStack item = editorContents[i]; + kit[i] = item == null ? null : item.clone(); + } + + kit[36] = cloneAt(editorContents, EDITOR_BOOTS_SLOT); + kit[37] = cloneAt(editorContents, EDITOR_LEGGINGS_SLOT); + kit[38] = cloneAt(editorContents, EDITOR_CHESTPLATE_SLOT); + kit[39] = cloneAt(editorContents, EDITOR_HELMET_SLOT); + kit[40] = cloneAt(editorContents, EDITOR_OFFHAND_SLOT); + return kit; + } + + private ItemStack cloneAt(List items, int index) { + if (index < 0 || index >= items.size()) { + return null; + } + ItemStack item = items.get(index); + return item == null ? null : item.clone(); + } + + private ItemStack cloneAt(ItemStack[] items, int index) { + if (index < 0 || index >= items.length) { + return null; + } + ItemStack item = items[index]; + return item == null ? null : item.clone(); + } + + private boolean hasItem(ItemStack item) { + return item != null && item.getType() != Material.AIR; + } + + private ItemStack placeIntoEditableSlots(Inventory inventory, ItemStack stack) { + ItemStack remaining = stack; + + for (int slot = 0; slot < KIT_STORAGE_SIZE; slot++) { + if (remaining == null || remaining.getType() == Material.AIR) { + return null; + } + + ItemStack existing = inventory.getItem(slot); + if (existing == null || existing.getType() == Material.AIR) { + inventory.setItem(slot, remaining); + return null; + } + + if (existing.isSimilar(remaining) && existing.getAmount() < existing.getMaxStackSize()) { + int free = existing.getMaxStackSize() - existing.getAmount(); + int move = Math.min(free, remaining.getAmount()); + existing.setAmount(existing.getAmount() + move); + remaining.setAmount(remaining.getAmount() - move); + } + } + + return remaining; + } + + private boolean isQueueMenu(Inventory inventory) { + if (inventory == null) { + return false; + } + return inventory.getHolder() instanceof QueueMenuHolder; + } + + private boolean isKitTypeMenu(Inventory inventory) { + if (inventory == null) { + return false; + } + return inventory.getHolder() instanceof KitTypeMenuHolder; + } + + private static final class QueueMenuHolder implements InventoryHolder { + @Override + public Inventory getInventory() { + return null; + } + } + + private static final class KitTypeMenuHolder implements InventoryHolder { + @Override + public Inventory getInventory() { + return null; + } + } + + private static final class KitSlotMenuHolder implements InventoryHolder { + private final KitType type; + + private KitSlotMenuHolder(KitType type) { + this.type = type; + } + + private KitType type() { + return type; + } + + @Override + public Inventory getInventory() { + return null; + } + } + + private static final class KitEditorMenuHolder implements InventoryHolder { + private final KitType type; + private final int slot; + + private KitEditorMenuHolder(KitType type, int slot) { + this.type = type; + this.slot = slot; + } + + private KitType type() { + return type; + } + + private int slot() { + return slot; + } + + @Override + public Inventory getInventory() { + return null; + } + } + + private String color(String text) { + return ChatColor.translateAlternateColorCodes('&', text); + } + + private Location loadLocation(String path) { + FileConfiguration config = plugin.getConfig(); + ConfigurationSection section = config.getConfigurationSection(path); + if (section == null) { + return null; + } + + String worldName = section.getString("world"); + if (worldName == null) { + return null; + } + + World world = Bukkit.getWorld(worldName); + if (world == null) { + return null; + } + + return new Location( + world, + section.getDouble("x"), + section.getDouble("y"), + section.getDouble("z"), + (float) section.getDouble("yaw"), + (float) section.getDouble("pitch")); + } + +} diff --git a/src/main/java/de/winniepat/pierredufaulersack/listeners/RoundOutcomeListener.java b/src/main/java/de/winniepat/pierredufaulersack/listeners/RoundOutcomeListener.java new file mode 100644 index 0000000..10aab30 --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/listeners/RoundOutcomeListener.java @@ -0,0 +1,38 @@ +package de.winniepat.pierredufaulersack.listeners; + +import de.winniepat.pierredufaulersack.game.GameManager; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.*; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.PlayerRespawnEvent; + +public class RoundOutcomeListener implements Listener { + + private final GameManager gameManager; + + public RoundOutcomeListener(GameManager gameManager) { + this.gameManager = gameManager; + } + + @EventHandler(ignoreCancelled = true) + public void onDeath(PlayerDeathEvent event) { + + Player victim = event.getEntity(); + Player killer = victim.getKiller(); + if (killer == null) { + return; + } + + gameManager.handleKill(killer, victim); + } + + @EventHandler + public void onRespawn(PlayerRespawnEvent event) { + Location lobby = gameManager.consumePendingLobbyRespawn(event.getPlayer().getUniqueId()); + if (lobby != null) { + event.setRespawnLocation(lobby); + } + } +} + diff --git a/src/main/java/de/winniepat/pierredufaulersack/listeners/RoundWaterCleanupListener.java b/src/main/java/de/winniepat/pierredufaulersack/listeners/RoundWaterCleanupListener.java new file mode 100644 index 0000000..ca1dddb --- /dev/null +++ b/src/main/java/de/winniepat/pierredufaulersack/listeners/RoundWaterCleanupListener.java @@ -0,0 +1,35 @@ +package de.winniepat.pierredufaulersack.listeners; + +import de.winniepat.pierredufaulersack.game.GameManager; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerBucketEmptyEvent; + +public class RoundWaterCleanupListener implements Listener { + + private final GameManager gameManager; + + public RoundWaterCleanupListener(GameManager gameManager) { + this.gameManager = gameManager; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onWaterBucketEmpty(PlayerBucketEmptyEvent event) { + if (event.getBucket() != Material.WATER_BUCKET) { + return; + } + + Player player = event.getPlayer(); + if (gameManager.getRole(player.getUniqueId()) == null) { + return; + } + + Block placedAt = event.getBlockClicked().getRelative(event.getBlockFace()); + gameManager.trackRoundWaterPlacement(player, placedAt.getLocation()); + } +} + diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..5e3aeb6 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,66 @@ +max-players: 5 +max-concurrent-games: 3 +hunter-kit-max-duplicates: 2 + +countdown: + seconds: 10 + full-seconds: 10 + not-full-seconds: 20 + min-players: 2 + announce-every-second: true + +sounds: + countdown: + sound: BLOCK_NOTE_BLOCK_HAT + volume: 1.0 + pitch: 1.4 + countdown-final: + sound: BLOCK_NOTE_BLOCK_PLING + volume: 1.0 + pitch: 1.8 + start: + sound: ENTITY_PLAYER_LEVELUP + volume: 1.0 + pitch: 1.0 + stop: + sound: ENTITY_ITEM_BREAK + volume: 1.0 + pitch: 1.0 + killed: + sound: ENTITY_PLAYER_HURT + volume: 1.0 + pitch: 1.0 + hunter-win: + sound: ENTITY_PLAYER_LEVELUP + volume: 1.0 + pitch: 1.2 + boss-lose: + sound: ENTITY_VILLAGER_NO + volume: 1.0 + pitch: 1.0 + boss-win: + sound: ENTITY_ENDER_DRAGON_GROWL + volume: 1.0 + pitch: 1.0 + +kit-chances: + boss: + 1: 40 + 2: 40 + 3: 20 + hunter: + 1: 40 + 2: 40 + 3: 20 + +locations: + lobby: {} + +arenas: + game1: {} + game2: {} + game3: {} + +kits: + boss: {} + hunter: {} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..b65fa16 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,26 @@ +name: Pierredufaulersack +version: '${version}' +main: de.winniepat.pierredufaulersack.Pierredufaulersack +api-version: '1.21' +authors: [ WinniePatGG ] +website: https://winniepat.de +description: Plugin für Boss Hunt Minigame by WinniePatGG +depend: [ LuckPerms ] + +commands: + bosshunt: + description: Hauptbefehl für Boss Hunt + usage: /bosshunt + bosskit: + description: Verwalte Boss Kits + usage: /bosskit [1-3] + permission: pierredufaulersack.admin + hunterkit: + description: Verwalte Hunter Kits + usage: /hunterkit [1-3] + permission: pierredufaulersack.admin + +permissions: + pierredufaulersack.admin: + description: Admin Rechte für Boss Hunt + default: op