commit b9a3f4f0dfd066706aa2d8ca3edd327f93592029 Author: Patrick <147879351+WinniePatGG@users.noreply.github.com> Date: Fri May 1 18:43:38 2026 +0200 init commit 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