commit 7811ac9fd2ba306c9fdf8159001038455501fa23 Author: wolf Date: Wed May 14 13:22:37 2025 -0500 PLEASE FIX GIT diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5f737e --- /dev/null +++ b/.gitignore @@ -0,0 +1,119 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Cache of project +.gradletasknamecache + +**/build/ + +# Common working directory +run/ +runs/ + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..7f56c0c --- /dev/null +++ b/build.gradle @@ -0,0 +1,60 @@ +plugins { + id 'java' + id("xyz.jpenilla.run-paper") version "2.3.1" +} + +group = 'me.trouper' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() + gradlePluginPortal() + maven { + name = "papermc-repo" + url = "https://repo.papermc.io/repository/maven-public/" + } + maven { + name = "sonatype" + url = "https://oss.sonatype.org/content/groups/public/" + } +} + +dependencies { + compileOnly("io.papermc.paper:paper-api:1.21.5-R0.1-SNAPSHOT") +} + +tasks { + runServer { + // Configure the Minecraft version for our task. + // This is the only required configuration besides applying the plugin. + // Your plugin's jar (or shadowJar if present) will be used automatically. + minecraftVersion("1.21.5") + } +} + +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 + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e69de29 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a441313 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists 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..009a373 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'TrimServer' diff --git a/src/main/java/me/trouper/trimserver/TrimServer.java b/src/main/java/me/trouper/trimserver/TrimServer.java new file mode 100644 index 0000000..7556518 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/TrimServer.java @@ -0,0 +1,54 @@ +package me.trouper.trimserver; + +import me.trouper.trimserver.server.Manager; +import me.trouper.trimserver.utils.visual.BlockDisplayRaytracer; +import org.bukkit.NamespacedKey; +import org.bukkit.plugin.java.JavaPlugin; + +public final class TrimServer extends JavaPlugin { + + private static TrimServer instance; + private Manager manager; + + @Override + public void onLoad() { + //getLogger().info("Setting PacketEvents API"); + //PacketEvents.setAPI(SpigotPacketEventsBuilder.build(this)); + // + //getLogger().info("Loading PacketEvents"); + //PacketEvents.getAPI().load(); + + getLogger().info("Instantiating Plugin"); + instance = this; + } + + @Override + public void onEnable() { + getLogger().info("Instantiating Manager"); + manager = new Manager(); + + getLogger().info("Initializing Manager"); + manager.init(); + + getLogger().info("Successfully enabled TrimServer."); + } + + @Override + public void onDisable() { + BlockDisplayRaytracer.cleanup(); + getLogger().info("Saved all IO files."); + manager.io.saveAll(); + getLogger().info("Saved all IO files."); + //PacketEvents.getAPI().terminate(); + } + + public static TrimServer getInstance() { + return instance; + } + public NamespacedKey getNameSpace() { + return new NamespacedKey(getInstance(),"trim_smp"); + } + public Manager getManager() { + return manager; + } +} diff --git a/src/main/java/me/trouper/trimserver/data/IO.java b/src/main/java/me/trouper/trimserver/data/IO.java new file mode 100644 index 0000000..9e46ed4 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/data/IO.java @@ -0,0 +1,37 @@ +package me.trouper.trimserver.data; + +import me.trouper.trimserver.TrimServer; +import me.trouper.trimserver.data.io.Config; +import me.trouper.trimserver.data.io.Storage; +import me.trouper.trimserver.utils.misc.JsonSerializable; + +import java.io.File; + +public class IO { + public final File DATA_FOLDER; + public final File CONFIG_FILE; + public final File STORAGE_FILE; + + public Config config; + public Storage storage; + + public IO() { + DATA_FOLDER = new File("plugins/TrimServer"); + CONFIG_FILE = new File(DATA_FOLDER,"/config.json"); + STORAGE_FILE = new File(DATA_FOLDER,"/storage.json"); + config = JsonSerializable.load(CONFIG_FILE,Config.class,new Config()); + storage = JsonSerializable.load(STORAGE_FILE,Storage.class,new Storage()); + } + + public void loadAll() { + TrimServer.getInstance().getLogger().info("Loading all IO Files"); + config = JsonSerializable.load(CONFIG_FILE,Config.class,new Config()); + storage = JsonSerializable.load(STORAGE_FILE,Storage.class,new Storage()); + } + + public void saveAll() { + TrimServer.getInstance().getLogger().info("Saving all IO Files"); + config.save(); + storage.save(); + } +} diff --git a/src/main/java/me/trouper/trimserver/data/io/Config.java b/src/main/java/me/trouper/trimserver/data/io/Config.java new file mode 100644 index 0000000..9c3b9ff --- /dev/null +++ b/src/main/java/me/trouper/trimserver/data/io/Config.java @@ -0,0 +1,35 @@ +package me.trouper.trimserver.data.io; + +import me.trouper.trimserver.TrimServer; +import me.trouper.trimserver.utils.misc.JsonSerializable; +import me.trouper.trimserver.utils.Verbose; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class Config implements JsonSerializable { + + public boolean debugMode = false; + public List debuggerExclusions = new ArrayList<>(); + + @Override + public File getFile() { + return TrimServer.getInstance().getManager().io.CONFIG_FILE; + } + + @Override + public void save() { + Verbose.send(1,"Saving Config..."); + JsonSerializable.super.save(); + } + + public Messages messages = new Messages(); + + public class Messages { + public String mainColor = "�ffaa"; + public String prefix = "&9TrimServer> &7"; + public String pluginName = "TrimServer"; + public boolean fancyAlerts = true; + } +} diff --git a/src/main/java/me/trouper/trimserver/data/io/Storage.java b/src/main/java/me/trouper/trimserver/data/io/Storage.java new file mode 100644 index 0000000..e08f612 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/data/io/Storage.java @@ -0,0 +1,30 @@ +package me.trouper.trimserver.data.io; + +import me.trouper.trimserver.TrimServer; +import me.trouper.trimserver.utils.misc.JsonSerializable; +import me.trouper.trimserver.utils.Verbose; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class Storage implements JsonSerializable { + + @Override + public File getFile() { + return TrimServer.getInstance().getManager().io.STORAGE_FILE; + } + + @Override + public void save() { + Verbose.send(1,"Saving Storage..."); + JsonSerializable.super.save(); + } + + public UserData userData = new UserData(); + + public class UserData { + public Map> playerTrust = new HashMap<>(); + } +} diff --git a/src/main/java/me/trouper/trimserver/server/Main.java b/src/main/java/me/trouper/trimserver/server/Main.java new file mode 100644 index 0000000..ee78377 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/Main.java @@ -0,0 +1,61 @@ +package me.trouper.trimserver.server; + +import io.papermc.paper.registry.RegistryAccess; +import me.trouper.trimserver.TrimServer; +import me.trouper.trimserver.data.IO; +import me.trouper.trimserver.data.io.Config; +import me.trouper.trimserver.data.io.Storage; +import me.trouper.trimserver.utils.Text; +import org.bukkit.command.CommandSender; + +import java.util.Random; +import java.util.function.BooleanSupplier; + +public interface Main { + Main main = new Main() { + }; + + Random random = new Random(); + + default RegistryAccess getRegistryAccess() { + return RegistryAccess.registryAccess(); + } + + default TrimServer getPlugin() { + return TrimServer.getInstance(); + } + + default Manager man() { + return getPlugin().getManager(); + } + + default IO io() { + return man().io; + }; + + default Config config() { + return io().config; + } + + default Storage storage() { + return io().storage; + } + + default void info(CommandSender player, String message, Object... args) { + Text.sendMessage(Text.Pallet.INFO, player, message, args); + } + + default void error(CommandSender player, String message, Object... args) { + Text.sendMessage(Text.Pallet.ERROR, player, message, args); + } + + default void checkPre(boolean check, String msg, Object... args) { + if (!check) { + throw new IllegalArgumentException(msg.formatted(args)); + } + } + + default void checkPre(BooleanSupplier check, String msg, Object... args) { + checkPre(check.getAsBoolean(), msg, args); + } +} diff --git a/src/main/java/me/trouper/trimserver/server/Manager.java b/src/main/java/me/trouper/trimserver/server/Manager.java new file mode 100644 index 0000000..e3c83dd --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/Manager.java @@ -0,0 +1,68 @@ +package me.trouper.trimserver.server; + +import me.trouper.trimserver.data.IO; +import me.trouper.trimserver.server.commands.AdminCommand; +import me.trouper.trimserver.server.commands.InfoCommand; +import me.trouper.trimserver.server.commands.TrustCommand; +import me.trouper.trimserver.server.events.JoinEvent; +import me.trouper.trimserver.server.events.SwapHandsEvent; +import me.trouper.trimserver.server.events.TrustEvents; +import me.trouper.trimserver.server.systems.TrustBackend; +import me.trouper.trimserver.server.systems.AbilityBackend; +import me.trouper.trimserver.server.systems.abilities.trims.*; +import me.trouper.trimserver.utils.visual.BlockDisplayRaytracer; + +public class Manager { + public IO io; + public AbilityBackend abilityBackend; + public TrustBackend trustBackend; + + public Manager() { + io = new IO(); + abilityBackend = new AbilityBackend(); + trustBackend = new TrustBackend(); + } + + + public void init() { + io.loadAll(); + + registerAbilities(); + registerEvents(); + registerCommands(); + BlockDisplayRaytracer.cleanup(); + } + + private void registerCommands() { + new AdminCommand().register(); + new InfoCommand().register(); + new TrustCommand().register(); + } + + private void registerEvents() { + new SwapHandsEvent().registerEvents(); + new JoinEvent().registerEvents(); + new TrustEvents().registerEvents(); + } + + private void registerAbilities() { + abilityBackend.registerAbility(new BoltAbility()).registerEvents(); + abilityBackend.registerAbility(new CoastAbility()).registerEvents(); + abilityBackend.registerAbility(new DuneAbility()).registerEvents(); + abilityBackend.registerAbility(new EyeAbility()).registerEvents(); + abilityBackend.registerAbility(new FlowAbility()).registerEvents(); + abilityBackend.registerAbility(new HostAbility()).registerEvents(); + abilityBackend.registerAbility(new RaiserAbility()).registerEvents(); + abilityBackend.registerAbility(new RibAbility()).registerEvents(); + abilityBackend.registerAbility(new SentryAbility()).registerEvents(); + abilityBackend.registerAbility(new ShaperAbility()).registerEvents(); + abilityBackend.registerAbility(new SilenceAbility()).registerEvents(); + abilityBackend.registerAbility(new SnoutAbility()).registerEvents(); + abilityBackend.registerAbility(new SpireAbility()).registerEvents(); + abilityBackend.registerAbility(new TideAbility()).registerEvents(); + abilityBackend.registerAbility(new VexAbility()).registerEvents(); + abilityBackend.registerAbility(new WardAbility()).registerEvents(); + abilityBackend.registerAbility(new WayfinderAbility()).registerEvents(); + abilityBackend.registerAbility(new WildAbility()).registerEvents(); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/server/commands/AdminCommand.java b/src/main/java/me/trouper/trimserver/server/commands/AdminCommand.java new file mode 100644 index 0000000..c7184c1 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/commands/AdminCommand.java @@ -0,0 +1,209 @@ +package me.trouper.trimserver.server.commands; + + +import io.papermc.paper.registry.keys.TrimPatternKeys; +import me.trouper.trimserver.server.systems.AbilityBackend; +import me.trouper.trimserver.utils.Text; +import me.trouper.trimserver.utils.commands.Args; +import me.trouper.trimserver.utils.commands.CommandRegistry; +import me.trouper.trimserver.utils.commands.Permission; +import me.trouper.trimserver.utils.commands.QuickCommand; +import me.trouper.trimserver.utils.commands.completions.CompletionBuilder; +import me.trouper.trimserver.utils.misc.Randomizer; +import me.trouper.trimserver.utils.visual.CustomDisplayRaytracer; +import me.trouper.trimserver.utils.visual.DisplayUtils; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Color; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Display; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ArmorMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.trim.ArmorTrim; +import org.bukkit.inventory.meta.trim.TrimMaterial; +import org.bukkit.inventory.meta.trim.TrimPattern; + +import java.util.List; +import java.util.Locale; + +@CommandRegistry(value = "trims",permission = @Permission("trims.admin"),printStackTrace = true) +public class AdminCommand implements QuickCommand { + + @Override + public void dispatchCommand(CommandSender commandSender, Command command, String s, Args args) { + if (args.getSize() < 1) { + error(commandSender,"You must choose an argument."); + } + switch (args.get(0).toString()) { + case "debug" -> handleDebug(commandSender,args); + case "reload" -> handleReload(commandSender,args); + case "try" -> handleTry(commandSender,args); + case "test" -> handleTest(commandSender,args); + } + } + + @Override + public void dispatchCompletions(CommandSender commandSender, Command command, String s, CompletionBuilder b) { + b.then( + b.arg("reload") + ).then( + b.arg("debug") + .then( + b.arg("toggle") + ) + .then( + b.arg("exclude") + .then( + b.arg("Class.method"))) + .then( + b.arg("include") + .then( + b.arg(main.config().debuggerExclusions) + ) + ) + ).then( + b.arg("try") + .then( + b.argEnum(AbilityBackend.ValidPattern.class) + .then( + b.argEnum( + AbilityBackend.ValidMaterial.class + ) + ) + ) + ).then( + b.arg("test") + .then( + b.arg("vector") + ) + ); + } + + private void handleTest(CommandSender commandSender, Args args) { + switch (args.get(1).toString()) { + case "vector" -> { + if (!(commandSender instanceof Player p)) return; + CustomDisplayRaytracer.traceWithReflection(p.getEyeLocation(),p.getEyeLocation().getDirection(),60,0.1,6,point -> { + DisplayUtils.DUST_PARTICLE_FACTORY.apply(Color.AQUA,1F).accept(point.getLoc()); + + return CustomDisplayRaytracer.hitBlockIf(block -> !block.isPassable()).test(point); + },(point,block) -> { + DisplayUtils.sphere(point.getLoc(),0.3,0.1,0.1,loc -> { + DisplayUtils.DUST_PARTICLE_FACTORY.apply(Color.RED,1F).accept(loc); + }); + + return true; + },(point,entity) -> { + return false; + }); + } + } + } + + private void handleTry(CommandSender sender, Args args) { + if (args.getSize() < 2) { + error(sender, "Usage: /trims try [material]"); + return; + } + if (!(sender instanceof Player p)) { + error(sender,"Only players can use this sub-command."); + return; + } + + AbilityBackend.ValidPattern validPattern = args.get(1).toEnum(AbilityBackend.ValidPattern.class); + AbilityBackend.ValidMaterial validMaterial = new Randomizer().getRandomElement(AbilityBackend.ValidMaterial.values()); + if (args.getSize() >= 3) validMaterial = args.get(2).toEnum(AbilityBackend.ValidMaterial.class); + + TrimPattern pattern = validPattern.getCanonical(); + TrimMaterial material = validMaterial.getCanonical(); + + if (material == null) material = new Randomizer().getRandomElement(AbilityBackend.ValidMaterial.values()).getCanonical(); + + p.getEquipment().setHelmet(createTestArmor(new ItemStack(Material.NETHERITE_HELMET),pattern,material)); + p.getEquipment().setChestplate(createTestArmor(new ItemStack(Material.NETHERITE_CHESTPLATE),pattern,material)); + p.getEquipment().setLeggings(createTestArmor(new ItemStack(Material.NETHERITE_LEGGINGS),pattern,material)); + p.getEquipment().setBoots(createTestArmor(new ItemStack(Material.NETHERITE_BOOTS),pattern,material)); + + info(sender,"You now are wearing {0} with {1} {2} trim.", "Netherite",Text.formatEnum(validMaterial),Text.formatEnum(validPattern)); + } + + private ItemStack createTestArmor(ItemStack piece, TrimPattern trimPattern, TrimMaterial trimMaterial) { + ItemMeta meta = piece.getItemMeta(); + if (!(meta instanceof ArmorMeta armor)) { + throw new IllegalArgumentException("You must input armor ONLY"); + } + + armor.customName(Text.color("&eTesting Armor").decoration(TextDecoration.ITALIC,false)); + armor.lore(List.of( + Text.color("&8&l| &7%s Trim".formatted(Text.formatEnum(main.man().abilityBackend.getValidPattern(trimPattern)))).decoration(TextDecoration.ITALIC,false), + Text.color("&8&l| &7%s".formatted(Text.formatEnum(main.man().abilityBackend.getValidMaterial(trimMaterial)))).decoration(TextDecoration.ITALIC,false), + Text.color("&8&l| &7Won't Break").decoration(TextDecoration.ITALIC,false), + Text.color("&8&l| &7Vanishes on death").decoration(TextDecoration.ITALIC,false), + Text.color("&8&l| &7This armor is for testing purposes &c&lONLY&7!").decoration(TextDecoration.ITALIC,false) + )); + armor.addEnchant(Enchantment.VANISHING_CURSE,1,true); + armor.setUnbreakable(true); + armor.addItemFlags(ItemFlag.HIDE_ENCHANTS); + armor.addItemFlags(ItemFlag.HIDE_UNBREAKABLE); + armor.addItemFlags(ItemFlag.HIDE_ARMOR_TRIM); + + armor.setTrim(new ArmorTrim(trimMaterial,trimPattern)); + + piece.setItemMeta(armor); + return piece; + } + + private void handleDebug(CommandSender sender, Args args) { + if (args.getSize() < 2) { + error(sender, "Usage: /trims debug "); + return; + } + + final String sub = args.get(1).toString(); + + switch (sub) { + case "toggle" -> { + boolean result = false; + main.config().debugMode = result = !main.config().debugMode; + main.config().save(); + + Text.sendMessage(Text.Pallet.SUCCESS,sender,"Toggled debug mode {0}.",result ? "on" : "off"); + } + case "exclude" -> { + if (args.getSize() < 3) { + error(sender, "Usage: /trims debug exclude "); + return; + } + final String exclusion = args.get(2).toString(); + main.config().debuggerExclusions.add(exclusion); + main.config().save(); + + Text.sendMessage(Text.Pallet.SUCCESS, sender, "Excluded {0} from the debugger.", exclusion); + } + case "include" -> { + if (args.getSize() < 3) { + error(sender, "Usage: /trims debug include "); + return; + } + final String exclusion = args.get(2).toString(); + main.config().debuggerExclusions.remove(exclusion); + main.config().save(); + + Text.sendMessage(Text.Pallet.SUCCESS, sender, "Removed exclusion for {0} on the debugger.", exclusion); + } + } + } + + private void handleReload(CommandSender sender, Args args) { + Text.sendMessage(Text.Pallet.NEUTRAL,sender,"Reloading IO..."); + main.man().io.loadAll(); + } + + +} diff --git a/src/main/java/me/trouper/trimserver/server/commands/InfoCommand.java b/src/main/java/me/trouper/trimserver/server/commands/InfoCommand.java new file mode 100644 index 0000000..dc6863f --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/commands/InfoCommand.java @@ -0,0 +1,31 @@ +package me.trouper.trimserver.server.commands; + +import me.trouper.trimserver.TrimServer; +import me.trouper.trimserver.utils.Text; +import me.trouper.trimserver.utils.commands.Args; +import me.trouper.trimserver.utils.commands.CommandRegistry; +import me.trouper.trimserver.utils.commands.QuickCommand; +import me.trouper.trimserver.utils.commands.completions.CompletionBuilder; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.inventory.meta.trim.TrimPattern; + +@CommandRegistry(value = "info") +public class InfoCommand implements QuickCommand { + @Override + public void dispatchCommand(CommandSender commandSender, Command command, String s, Args args) { + if (args.getSize() < 1) { + commandSender.sendMessage("You must provide a trim to get information on."); + } + String query = args.get(0).toString(); + commandSender.sendMessage( + Text.color(Text.formatArgs(Text.Pallet.INFO,"-------- Info for {0} trim. --------",query)).appendNewline() + .append(main.man().abilityBackend.formatAbilityInfo(main.man().abilityBackend.getTrimPattern(query)) + )); + } + + @Override + public void dispatchCompletions(CommandSender commandSender, Command command, String s, CompletionBuilder b) { + b.then(b.arg(TrimServer.getInstance().getManager().abilityBackend.abilities())); + } +} diff --git a/src/main/java/me/trouper/trimserver/server/commands/TrustCommand.java b/src/main/java/me/trouper/trimserver/server/commands/TrustCommand.java new file mode 100644 index 0000000..5d78aa4 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/commands/TrustCommand.java @@ -0,0 +1,96 @@ +package me.trouper.trimserver.server.commands; + + +import me.trouper.trimserver.utils.Text; +import me.trouper.trimserver.utils.commands.Args; +import me.trouper.trimserver.utils.commands.CommandRegistry; +import me.trouper.trimserver.utils.commands.QuickCommand; +import me.trouper.trimserver.utils.commands.completions.CompletionBuilder; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +@CommandRegistry(value = "trust", playersOnly = true, printStackTrace = true) +public class TrustCommand implements QuickCommand { + @Override + public void dispatchCommand(CommandSender sender, Command command, String label, Args args) { + if (args.getSize() < 1) { + Text.sendError(sender, "Error: Valid sub-commands are: [add, remove, list]"); + return; + } + + final String sub = args.get(0).toString(); + + switch (sub) { + case "add" -> { + if (args.getSize() < 2) { + Text.sendError(sender, "Usage: /trust add "); + return; + } + final String target = args.get(1).toString(); + final OfflinePlayer trustee = Bukkit.getOfflinePlayer(target); + + if (trustee == null) { + Text.sendError(sender, "Player not found online or offline."); + return; + } + + if (main.man().trustBackend.addTrust((Player) sender,trustee.getUniqueId())) { + Text.sendMessage(Text.Pallet.SUCCESS,sender,"Successfully trusted {0}.",target); + if (trustee.isOnline())Text.sendMessage(Text.Pallet.SUCCESS,(Player) trustee,"Successfully trusted {0}.",sender.getName()); + } else { + Text.sendMessage(Text.Pallet.NEUTRAL,sender,"You already have {0} trusted.",target); + } + } + case "remove" -> { + if (args.getSize() < 2) { + Text.sendError(sender, "Usage: /trust remove "); + return; + } + final String target = args.get(1).toString(); + final OfflinePlayer trustee = Bukkit.getOfflinePlayer(target); + + if (trustee == null) { + Text.sendError(sender, "Player not found online or offline."); + return; + } + if (trustee.getUniqueId().equals(((Player) sender).getUniqueId())) { + Text.sendError(sender, "You do not want to un-trust yourself. It will break the code ;-;"); + return; + } + + if (main.man().trustBackend.removeTrust((Player) sender,trustee.getUniqueId())) { + Text.sendMessage(Text.Pallet.SUCCESS,sender,"Successfully un-trusted {0}.",target); + if (trustee.isOnline())Text.sendMessage(Text.Pallet.SUCCESS,(Player) trustee,"{0} has un-trusted you.",sender.getName()); + } else { + Text.sendMessage(Text.Pallet.NEUTRAL,sender,"You do not have {0} trusted.",target); + } + } + case "list" -> { + final Set trustees = main.man().trustBackend.getTrustees((Player) sender); + Set names = new HashSet<>(); + for (String trustee : trustees) { + names.add(Bukkit.getOfflinePlayer(UUID.fromString(trustee)).getName()); + } + Text.sendMessage(Text.Pallet.NEUTRAL,sender,"You currently have {0} players trusted: {1}",trustees.size(), Arrays.toString(names.toArray())); + } + } + } + + @Override + public void dispatchCompletions(CommandSender commandSender, Command command, String s, CompletionBuilder b) { + b.then( + b.arg("add","remove") + .then(b.argOnlinePlayers()) + ).then( + b.arg("list") + ); + } +} diff --git a/src/main/java/me/trouper/trimserver/server/events/JoinEvent.java b/src/main/java/me/trouper/trimserver/server/events/JoinEvent.java new file mode 100644 index 0000000..b8f2e56 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/events/JoinEvent.java @@ -0,0 +1,13 @@ +package me.trouper.trimserver.server.events; + +import me.trouper.trimserver.server.Main; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerJoinEvent; + +public class JoinEvent implements QuickListener, Main { + @EventHandler + public void initPlayer(PlayerJoinEvent e) { + main.man().trustBackend.initPlayer(e.getPlayer()); + } +} diff --git a/src/main/java/me/trouper/trimserver/server/events/QuickListener.java b/src/main/java/me/trouper/trimserver/server/events/QuickListener.java new file mode 100644 index 0000000..30f5ecb --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/events/QuickListener.java @@ -0,0 +1,12 @@ +package me.trouper.trimserver.server.events; + +import me.trouper.trimserver.server.Main; +import org.bukkit.Bukkit; +import org.bukkit.event.Listener; + +public interface QuickListener extends Listener, Main { + default QuickListener registerEvents() { + Bukkit.getPluginManager().registerEvents(this, this.getPlugin()); + return this; + } +} diff --git a/src/main/java/me/trouper/trimserver/server/events/SwapHandsEvent.java b/src/main/java/me/trouper/trimserver/server/events/SwapHandsEvent.java new file mode 100644 index 0000000..07db36d --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/events/SwapHandsEvent.java @@ -0,0 +1,16 @@ +package me.trouper.trimserver.server.events; + +import me.trouper.trimserver.server.Main; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerSwapHandItemsEvent; + +public class SwapHandsEvent implements QuickListener, Main { + + @EventHandler + public void onSwap(PlayerSwapHandItemsEvent e) { + Player p = e.getPlayer(); + if (!main.man().abilityBackend.checkAndTriggerAbility(p)) return; + e.setCancelled(true); + } +} diff --git a/src/main/java/me/trouper/trimserver/server/events/TrustEvents.java b/src/main/java/me/trouper/trimserver/server/events/TrustEvents.java new file mode 100644 index 0000000..1fe69be --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/events/TrustEvents.java @@ -0,0 +1,16 @@ +package me.trouper.trimserver.server.events; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.entity.EntityDamageByEntityEvent; + +public class TrustEvents implements QuickListener { + @EventHandler + public void onHit(EntityDamageByEntityEvent e) { + if (!(e.getDamager() instanceof Player a)) return; + if (!(e.getEntity() instanceof Player v)) return; + if (!main.man().trustBackend.trusts(a,v)) return; + + e.setCancelled(true); + } +} diff --git a/src/main/java/me/trouper/trimserver/server/systems/AbilityBackend.java b/src/main/java/me/trouper/trimserver/server/systems/AbilityBackend.java new file mode 100644 index 0000000..40fd876 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/AbilityBackend.java @@ -0,0 +1,370 @@ +package me.trouper.trimserver.server.systems; + +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import me.trouper.trimserver.server.Main; +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.utils.Text; +import me.trouper.trimserver.utils.misc.Cooldown; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.inventory.meta.ArmorMeta; +import org.bukkit.inventory.meta.trim.ArmorTrim; +import org.bukkit.inventory.meta.trim.TrimMaterial; +import org.bukkit.inventory.meta.trim.TrimPattern; + +import java.util.*; +import java.util.logging.Level; + +public class AbilityBackend implements Main { + // Map to store registered abilities by their pattern + public final Map registeredAbilities = new HashMap<>(); + + // Record for cooldown tracking key (Player UUID, Pattern, Material) + private record AbilityCooldown(UUID who, TrimPattern pattern, TrimMaterial material) {}; + + // Cooldown manager instance + private final Cooldown onCooldown = new Cooldown<>(); + + /** + * Get a list of registered ability pattern names as strings. + * @return a list containing the string keys of the registered trim patterns. + */ + public final List abilities() { + List map = new ArrayList<>(); + RegistryAccess access = RegistryAccess.registryAccess(); + registeredAbilities.forEach((trim,ability)->{ + map.add(access.getRegistry(RegistryKey.TRIM_PATTERN).getKey(trim).getKey()); + }); + return map; + } + + /** + * Registers a TrimAbility instance. + * Logs a warning if a pattern is already registered. + * + * @param ability The TrimAbility instance to register. + */ + public AbstractAbility registerAbility(AbstractAbility ability) { + if (ability == null) { + main.getPlugin().getLogger().warning("Attempted to register a null TrimAbility."); + return null; + } + TrimPattern pattern = ability.getPattern(); + if (registeredAbilities.containsKey(pattern)) { + main.getPlugin().getLogger().warning("TrimAbility for pattern " + main.getRegistryAccess().getRegistry(RegistryKey.TRIM_PATTERN).getKey(pattern).getKey() + " is already registered. Overwriting."); + } + registeredAbilities.put(pattern, ability); + + getPlugin().getLogger().info("Registered TrimAbility for pattern: " + ability.getPatternName()); + + return ability; + } + + /** + * Retrieves the registered TrimAbility for a given pattern, if any. + * + * @param pattern The pattern to look up. + * @return The TrimAbility instance, or null if none is registered. + */ + public AbstractAbility getAbility(TrimPattern pattern) { + return registeredAbilities.get(pattern); + } + + + /** + * Retrieves the TrimAbility for a player based on their full armor set's trim. + * Checks if the player is wearing a full set with a matching trim pattern and material. + * + * @param player The player to check + * @return The TrimAbility instance corresponding to the player's trim, or null if + * they don't have a valid full set trim or the pattern isn't registered. + */ + public AbstractAbility getAbility(Player player) { + PlayerInventory inventory = player.getInventory(); + ItemStack helmet = inventory.getHelmet(); + ItemStack chestplate = inventory.getChestplate(); + ItemStack leggings = inventory.getLeggings(); + ItemStack boots = inventory.getBoots(); + + // 1. Check if all armor slots are filled with *some* armor + if (helmet == null || helmet.getType() == Material.AIR || + chestplate == null || chestplate.getType() == Material.AIR || + leggings == null || leggings.getType() == Material.AIR || + boots == null || boots.getType() == Material.AIR) { + return null; // Not wearing a full set + } + + ItemStack[] armorPieces = {helmet, chestplate, leggings, boots}; + TrimPattern requiredPattern = null; + TrimMaterial requiredMaterial = null; + + // 2. Iterate through armor pieces to check for matching trims + for (int i = 0; i < armorPieces.length; i++) { + ItemStack piece = armorPieces[i]; + if (!(piece.getItemMeta() instanceof ArmorMeta meta)) { + return null; // Should have ArmorMeta if it's armor + } + + ArmorTrim trim = meta.getTrim(); + + if (trim == null) { + return null; // Needs a trim + } + + TrimPattern currentPattern = trim.getPattern(); + TrimMaterial currentMaterial = trim.getMaterial(); + + if (i == 0) { + // First piece sets the requirement + requiredPattern = currentPattern; + requiredMaterial = currentMaterial; + } else { + // Subsequent pieces must match the first one + if (!currentPattern.equals(requiredPattern) || !currentMaterial.equals(requiredMaterial)) { + return null; // Mismatch found + } + } + } + + // 3. If we reach here, all pieces match! Return the ability if registered. + if (requiredPattern != null && requiredMaterial != null) { + return registeredAbilities.get(requiredPattern); + } + return null; // Should technically not be reached if logic is sound + } + + /** + * Checks the player's equipped armor. If they are wearing a full set + * with the same trim pattern and material, it finds the corresponding + * registered TrimAbility and calls its appropriate material-specific method, + * provided the cooldown has expired. + * + * @param player The player whose armor should be checked. + * @return true if an ability was successfully triggered, false otherwise. + */ + public boolean checkAndTriggerAbility(Player player) { + PlayerInventory inventory = player.getInventory(); + ItemStack helmet = inventory.getHelmet(); + ItemStack chestplate = inventory.getChestplate(); + ItemStack leggings = inventory.getLeggings(); + ItemStack boots = inventory.getBoots(); + + // 1. Check if all armor slots are filled with *some* armor + if (helmet == null || helmet.getType() == Material.AIR || + chestplate == null || chestplate.getType() == Material.AIR || + leggings == null || leggings.getType() == Material.AIR || + boots == null || boots.getType() == Material.AIR) { + return false; // Not wearing a full set + } + + ItemStack[] armorPieces = {helmet, chestplate, leggings, boots}; + TrimPattern requiredPattern = null; + TrimMaterial requiredMaterial = null; + + // 2. Iterate through armor pieces to check for matching trims + for (int i = 0; i < armorPieces.length; i++) { + ItemStack piece = armorPieces[i]; + if (!(piece.getItemMeta() instanceof ArmorMeta meta)) { + return false; // Should have ArmorMeta if it's armor + } + + ArmorTrim trim = meta.getTrim(); + + if (trim == null) { + return false; // Needs a trim + } + + TrimPattern currentPattern = trim.getPattern(); + TrimMaterial currentMaterial = trim.getMaterial(); + + if (i == 0) { + // First piece sets the requirement + requiredPattern = currentPattern; + requiredMaterial = currentMaterial; + } else { + // Subsequent pieces must match the first one + if (!currentPattern.equals(requiredPattern) || !currentMaterial.equals(requiredMaterial)) { + return false; // Mismatch found + } + } + } + + // 3. If we reach here, all pieces match! Find and trigger the ability. + if (requiredPattern != null && requiredMaterial != null) { + AbstractAbility ability = registeredAbilities.get(requiredPattern); + if (ability != null) { + // Get ability info for cooldown using the ability instance's method + MaterialInfo materialInfo = ability.getAbilityInfo(requiredMaterial); + + // If there's no info or cooldown is 0, proceed immediately + int cooldownTicks = (materialInfo != null) ? materialInfo.cooldownTicks() : 0; + + AbilityCooldown cooldownKey = new AbilityCooldown(player.getUniqueId(), requiredPattern, requiredMaterial); + + if (onCooldown.isOnCooldown(cooldownKey)) { + // Calculate remaining time for progress bar if needed + long remainingMillis = onCooldown.getCooldown(cooldownKey); + // Example progress bar logic - adjust duration calculation as needed + long totalDurationMillis = (long) cooldownTicks * 50L; // Assuming 1 tick = 50ms + long elapsedMillis = totalDurationMillis - remainingMillis; // Calculate elapsed for the bar + + // Ensure elapsed is not negative or greater than total + elapsedMillis = Math.max(0, elapsedMillis); + elapsedMillis = Math.min(totalDurationMillis, elapsedMillis); + + player.sendActionBar(Component.text("Ability Recharging: ", NamedTextColor.WHITE) + .append(Text.color(Text.generateProgressBar(20, (int)totalDurationMillis, (int)elapsedMillis)))); + + return false; // Still on cooldown + } + + try { + // Dispatch ability and check if it was successful + boolean applyCooldown = ability.dispatchAbility(requiredMaterial, player); + + // Add cooldown if cooldownTicks is > 0 and the ability execution indicated we should apply cooldown + if (cooldownTicks > 0 && applyCooldown) { + onCooldown.addCooldown(cooldownKey, (long) cooldownTicks * 50L); // Cooldown expects milliseconds + } + + return true; // Ability trigger attempt completed + } catch (Exception e) { + main.getPlugin().getLogger().log(Level.SEVERE, "Error executing trim ability for " + player.getName() + " (Pattern: " + ability.getPatternName() + ", Material: " + main.getRegistryAccess().getRegistry(RegistryKey.TRIM_MATERIAL).getKey(requiredMaterial).getKey() + ")", e); + return false; // Error occurred + } + } else { + return false; // No ability registered for this pattern + } + } + + return false; + } + + /** + * Gets the formatted Component information message for a specific trim pattern's ability. + * Includes overall pattern info and grouped material variant details. + * + * @param pattern The TrimPattern to get info for. + * @return The formatted info Component, or a message indicating no ability is registered. + */ + public Component formatAbilityInfo(TrimPattern pattern) { + AbstractAbility ability = registeredAbilities.get(pattern); + if (ability != null) { + return ability.formatAbilityInfo(); // Call the method on the ability instance + } else { + return Component.text("--- ", NamedTextColor.YELLOW) + .append(Component.text("No Ability Registered", NamedTextColor.YELLOW)) + .append(Component.text(" ---", NamedTextColor.YELLOW)) + .append(Component.newline()) + .append(Component.text("No ability found for pattern: ", NamedTextColor.GRAY)) + .append(Component.text(main.getRegistryAccess().getRegistry(RegistryKey.TRIM_PATTERN).getKey(pattern).getKey(), NamedTextColor.AQUA)); + } + } + + /** + * Force-removes the cooldown for a player's specific trim ability. + * Useful for admin commands or testing. + * + * @param player The player whose cooldown to reset + * @param pattern The trim pattern + * @param material The trim material + * @return true if a cooldown was found and removed, false otherwise + */ + public boolean removeCooldown(Player player, TrimPattern pattern, TrimMaterial material) { + AbilityCooldown cooldownKey = new AbilityCooldown(player.getUniqueId(), pattern, material); + if (onCooldown.isOnCooldown(cooldownKey)) { + onCooldown.removeCooldown(cooldownKey); + return true; + } + return false; + } + + public TrimPattern getTrimPattern(String name) { + name = name.toUpperCase(); + return ValidPattern.valueOf(name).getCanonical(); + } + + public ValidPattern getValidPattern(TrimPattern pattern) { + for (ValidPattern value : ValidPattern.values()) { + if (!value.getCanonical().equals(pattern)) continue; + return value; + } + return null; + } + + public TrimMaterial getTrimMaterial(String name) { + name = name.toUpperCase(); + return ValidMaterial.valueOf(name).getCanonical(); + } + + public ValidMaterial getValidMaterial(TrimMaterial material) { + for (ValidMaterial value : ValidMaterial.values()) { + if (!value.getCanonical().equals(material)) continue; + return value; + } + return null; + } + + + + public enum ValidMaterial { + AMETHYST(TrimMaterial.AMETHYST), + COPPER(TrimMaterial.COPPER), + DIAMOND(TrimMaterial.DIAMOND), + EMERALD(TrimMaterial.EMERALD), + GOLD(TrimMaterial.GOLD), + IRON(TrimMaterial.IRON), + LAPIS(TrimMaterial.LAPIS), + NETHERITE(TrimMaterial.NETHERITE), + QUARTZ(TrimMaterial.QUARTZ), + REDSTONE(TrimMaterial.REDSTONE), + RESIN(TrimMaterial.RESIN); + + private final TrimMaterial canonical; + + ValidMaterial(TrimMaterial canonical) { + this.canonical = canonical; + } + + public TrimMaterial getCanonical() { + return canonical; + } + } + + public enum ValidPattern { + BOLT(TrimPattern.BOLT), + COAST(TrimPattern.COAST), + DUNE(TrimPattern.DUNE), + EYE(TrimPattern.EYE), + FLOW(TrimPattern.FLOW), + HOST(TrimPattern.HOST), + RAISER(TrimPattern.RAISER), + RIB(TrimPattern.RIB), + SENTRY(TrimPattern.SENTRY), + SHAPER(TrimPattern.SHAPER), + SILENCE(TrimPattern.SILENCE), + SNOUT(TrimPattern.SNOUT), + SPIRE(TrimPattern.SPIRE), + TIDE(TrimPattern.TIDE), + VEX(TrimPattern.VEX), + WARD(TrimPattern.WARD), + WAYFINDER(TrimPattern.WAYFINDER), + WILD(TrimPattern.WILD); + + private final TrimPattern canonical; + + ValidPattern(TrimPattern canonical) { + this.canonical = canonical; + } + + public TrimPattern getCanonical() { + return canonical; + } + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/server/systems/TrustBackend.java b/src/main/java/me/trouper/trimserver/server/systems/TrustBackend.java new file mode 100644 index 0000000..4fc9737 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/TrustBackend.java @@ -0,0 +1,91 @@ +package me.trouper.trimserver.server.systems; + +import me.trouper.trimserver.server.Main; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class TrustBackend implements Main { + + /** + * Adds a UUID to the target's trust + * @param truster the player trusting + * @param trustee the UUID to trust + * @return true if it was added, false if it was already present. + */ + public boolean addTrust(Player truster, UUID trustee) { + Set trustees = getTrustees(truster); + final boolean added = trustees.add(trustee.toString()); + storage().userData.playerTrust.put(truster.getUniqueId().toString(),trustees); + storage().save(); + return added; + } + + /** + * Removes a UUID from the target's trust + * @param truster the player un-trusting + * @param trustee the UUID to un-trust + * @return true if it was removed, false if it was not present. + */ + public boolean removeTrust(Player truster, UUID trustee) { + Set trustees = getTrustees(truster); + final boolean removed = trustees.remove(trustee.toString()); + storage().userData.playerTrust.put(truster.getUniqueId().toString(),trustees); + storage().save(); + return removed; + } + + /** + * Ensures that the player trusts themselves. If they do not trust themselves then all the abilities will break. + * @param target the player to check + */ + public void initPlayer(Player target) { + addTrust(target,target.getUniqueId()); + } + + /** + * Gets a set of the target's trustees. + * @param target the player to get the trustees of + * @return Set containing the UUIDs of trustees. + */ + public Set getTrustees(Player target) { + return storage().userData.playerTrust.getOrDefault(target.getUniqueId().toString(),new HashSet<>()); + } + + /** + * Gets a set of the target's trustees. + * @param target the player to get the trustees of + * @return Set containing the UUIDs of trustees. + */ + public Set getTrustees(UUID target) { + return storage().userData.playerTrust.getOrDefault(target.toString(),new HashSet<>()); + } + + + + /** + * Returns if the target trusts the entity provided. + * @param target the player to check the trust of + * @param check the entity which may or may not be trusted + * @return true if the target trusts the entity, false if they don't. + */ + public boolean trusts(Player target, LivingEntity check) { + return getTrustees(target).contains(check.getUniqueId().toString()) || target.getUniqueId().equals(check.getUniqueId()); + } + + /** + * Returns if the target trusts the entity provided. + * @param target the player to check the trust of + * @param check the entity which may or may not be trusted + * @return true if the target trusts the entity, false if they don't. + */ + public boolean trusts(UUID target, LivingEntity check) { + return getTrustees(target).contains(check.getUniqueId().toString()) || target.equals(check.getUniqueId()); + } + public boolean trusts(UUID target, UUID check) { + return getTrustees(target).contains(check.toString()) || target.equals(check); + } +} diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/AbstractAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/AbstractAbility.java new file mode 100644 index 0000000..2c5094f --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/AbstractAbility.java @@ -0,0 +1,259 @@ +package me.trouper.trimserver.server.systems.abilities; + +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import me.trouper.trimserver.server.Main; +import me.trouper.trimserver.server.events.QuickListener; +import me.trouper.trimserver.utils.Text; +import net.kyori.adventure.text.Component; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.inventory.meta.trim.TrimMaterial; +import org.bukkit.inventory.meta.trim.TrimPattern; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a set of abilities associated with a specific Armor Trim Pattern. + * Subclasses should override the material methods for which they want to define + * specific behavior. Apply the @PatternInfo annotation to subclasses for overall info. + * Material-specific info is defined using the @MaterialInfo annotation on the material methods. + */ +public abstract class AbstractAbility implements Main, QuickListener { + + protected TrimPattern pattern; + + /** + * Creates a new TrimAbility instance for a specific pattern. + * + * @param pattern The TrimPattern this ability corresponds to. + */ + public AbstractAbility(TrimPattern pattern) { + if (pattern == null) { + throw new IllegalArgumentException("TrimPattern cannot be null"); + } + this.pattern = pattern; + } + + /** + * @return The TrimPattern associated with this ability set. + */ + public final TrimPattern getPattern() { + return pattern; + } + + // --- Default Ability Methods for Each Material --- + // Subclasses should override these methods to implement specific abilities. + // Apply @MaterialInfo to these methods in subclasses where implemented. + // Return true if the ability was successfully executed and should apply cooldown. + // Return false if the ability failed or otherwise shouldn't trigger cooldown. + + /** Called when the player has a full set of this pattern with Amethyst trim. */ + @MaterialInfo(name = "Default Amethyst Ability", description = "A basic ability.") + public boolean amethystAbility(Player player) { return true; } // Default returns true to maintain backwards compatibility + + /** Called when the player has a full set of this pattern with Copper trim. */ + @MaterialInfo(name = "Default Copper Ability", description = "A basic ability.") + public boolean copperAbility(Player player) { return true; } // Default returns true to maintain backwards compatibility + + /** Called when the player has a full set of this pattern with Diamond trim. */ + @MaterialInfo(name = "Default Diamond Ability", description = "A basic ability.") + public boolean diamondAbility(Player player) { return true; } // Default returns true to maintain backwards compatibility + + /** Called when the player has a full set of this pattern with Emerald trim. */ + @MaterialInfo(name = "Default Emerald Ability", description = "A basic ability.") + public boolean emeraldAbility(Player player) { return true; } // Default returns true to maintain backwards compatibility + + /** Called when the player has a full set of this pattern with Gold trim. */ + @MaterialInfo(name = "Default Gold Ability", description = "A basic ability.") + public boolean goldAbility(Player player) { return true; } // Default returns true to maintain backwards compatibility + + /** Called when the player has a full set of this pattern with Iron trim. */ + @MaterialInfo(name = "Default Iron Ability", description = "A basic ability.") + public boolean ironAbility(Player player) { return true; } // Default returns true to maintain backwards compatibility + + /** Called when the player has a full set of this pattern with Lapis trim. */ + @MaterialInfo(name = "Default Lapis Ability", description = "A basic ability.") + public boolean lapisAbility(Player player) { return true; } // Default returns true to maintain backwards compatibility + + /** Called when the player has a full set of this pattern with Netherite trim. */ + @MaterialInfo(name = "Default Netherite Ability", description = "A basic ability.") + public boolean netheriteAbility(Player player) { return true; } // Default returns true to maintain backwards compatibility + + /** Called when the player has a full set of this pattern with Quartz trim. */ + @MaterialInfo(name = "Default Quartz Ability", description = "A basic ability.") + public boolean quartzAbility(Player player) { return true; } // Default returns true to maintain backwards compatibility + + /** Called when the player has a full set of this pattern with Redstone trim. */ + @MaterialInfo(name = "Default Redstone Ability", description = "A basic ability.") + public boolean redstoneAbility(Player player) { return true; } // Default returns true to maintain backwards compatibility + + /** Called when the player has a full set of this pattern with Resin trim. */ + @MaterialInfo(name = "Default Resin Ability", description = "A basic ability.") + public boolean resinAbility(Player player) { return true; } // Default returns true to maintain backwards compatibility + + // --- Helper Method to Dispatch Based on Material --- + // This is called by the Manager after confirming the player has a matching set. + + /** + * Internal dispatcher. Calls the appropriate material-specific ability method. + * Should only be called by the TrimAbilityManager after validation and cooldown check. + * + * @param material The TrimMaterial identified on the player's armor set. + * @param player The player activating the ability. + * @return true if the ability was successfully executed and should apply cooldown, + * false if the ability failed or otherwise shouldn't trigger cooldown. + */ + public final boolean dispatchAbility(TrimMaterial material, Player player) { + String methodName = getMethodNameForMaterial(material); + if (methodName == null) { + main.getPlugin().getLogger().warning("Attempted to dispatch for unknown material: " + main.getRegistryAccess().getRegistry(RegistryKey.TRIM_MATERIAL).getKey(material).getKey()); + return false; + } + + try { + Method abilityMethod = this.getClass().getMethod(methodName, Player.class); + Object result = abilityMethod.invoke(this, player); + return (result instanceof Boolean) ? (Boolean) result : true; + } catch (NoSuchMethodException e) { + main.getPlugin().getLogger().warning("Method " + methodName + " not found in " + this.getClass().getSimpleName() + " for dispatch."); + return false; + } catch (Exception e) { + main.getPlugin().getLogger().severe("Error dispatching ability method " + methodName + " for pattern " + getPatternName() + ": " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + /** + * Retrieves the MaterialAbilityInfo annotation for a specific material's method + * within THIS TrimAbility instance's class. + * + * @param material The TrimMaterial. + * @return The MaterialInfo annotation, or null if the method isn't found or not annotated. + */ + public MaterialInfo getAbilityInfo(TrimMaterial material) { + String methodName = getMethodNameForMaterial(material); + if (methodName == null) { + return null; // Unknown material mapping + } + try { + Method abilityMethod = this.getClass().getMethod(methodName, Player.class); + return abilityMethod.isAnnotationPresent(MaterialInfo.class) ? + abilityMethod.getAnnotation(MaterialInfo.class) : null; + } catch (NoSuchMethodException e) { + // Method not found - this should not happen for the default methods, + // but might if a subclass removes one or getMethodNameForMaterial is wrong. + return null; + } catch (Exception e) { + main.getPlugin().getLogger().severe("Unexpected error getting annotation for method " + methodName + " in " + this.getClass().getSimpleName() + ": " + e.getMessage()); + return null; + } + } + + /** + * Retrieves the PatternInfo annotation for this TrimAbility class. + * + * @return The PatternInfo annotation, or null if not present. + */ + public PatternInfo getPatternInfo() { + return this.getClass().getAnnotation(PatternInfo.class); + } + + /** + * Gets the NamespacedKey key string for this pattern. + * @return The pattern's key string. + */ + public String getPatternName() { + RegistryAccess access = main.getRegistryAccess(); + return access.getRegistry(RegistryKey.TRIM_PATTERN).getKey(pattern).getKey(); + } + + /** + * Generates a formatted Component message detailing the effects of this ability. + * Includes overall pattern info and groups material variants by identical descriptions. + * + * @return A formatted Component containing the ability information. + */ + public Component formatAbilityInfo() { + RegistryAccess access = main.getRegistryAccess(); + PatternInfo patternInfo = getPatternInfo(); + + Component message = Component.empty().appendNewline(); + // Add overall pattern info + + if (patternInfo != null && patternInfo.description() != null && !patternInfo.description().isEmpty()) { + message = message.append(Text.color("&7%s".formatted(patternInfo.description()))).appendNewline().appendNewline(); + } + + message = message.append(Text.color("&7&m---&e %s &7&m---".formatted(patternInfo.name()))).appendNewline(); + + // Map descriptions to lists of material names and their cooldowns + Map>> groupedInfo = new LinkedHashMap<>(); + + List materials = access.getRegistry(RegistryKey.TRIM_MATERIAL).stream().toList(); + + for (TrimMaterial material : materials) { + MaterialInfo info = getAbilityInfo(material); + if (info != null && info.description() != null && !info.description().isEmpty()) { + String materialName = access.getRegistry(RegistryKey.TRIM_MATERIAL).getKey(material).getKey(); + String description = info.description(); + int cooldownSeconds = info.cooldownTicks() / 20; + + groupedInfo.computeIfAbsent(description, k -> new ArrayList<>()) + .add(Map.entry(materialName, cooldownSeconds)); + } + } + + if (groupedInfo.isEmpty()) { + message = message.append(Text.color("&7(No specific material abilities defined for this pattern)")).appendNewline(); + } else { + // Iterate through the grouped descriptions and their materials + for (Map.Entry>> entry : groupedInfo.entrySet()) { + String description = entry.getKey(); + List> materialEntries = entry.getValue(); + + // Append the description + message = message.append(Text.color("&7%s&f:".formatted(description))).appendNewline(); + + // Append the list of materials with this description + for (Map.Entry matEntry : materialEntries) { + String matName = matEntry.getKey(); + int cooldownSeconds = matEntry.getValue(); + + message = message.append(Text.color(" &8-&r &b%s".formatted(matName))); + + if (cooldownSeconds > 0) { + message = message.append(Text.color(" &8| &9Cooldown&f: &7%ss".formatted(cooldownSeconds))); + } + + message = message.appendNewline(); + } + } + } + + return message; + } + + + /** + * Helper method to map TrimMaterial constants to their corresponding method names + * within the TrimAbility class structure. + * + * @param material The TrimMaterial. + * @return The expected method name (e.g., "amethystAbility"), or null if not mapped. + */ + private String getMethodNameForMaterial(TrimMaterial material) { + if (material == null) return null; + RegistryAccess access = main.getRegistryAccess(); + NamespacedKey key = access.getRegistry(RegistryKey.TRIM_MATERIAL).getKey(material); + if (key == null) return null; + + String materialKey = key.getKey(); + return materialKey + "Ability"; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/MaterialInfo.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/MaterialInfo.java new file mode 100644 index 0000000..007e782 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/MaterialInfo.java @@ -0,0 +1,26 @@ +package me.trouper.trimserver.server.systems.abilities; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface MaterialInfo { + /** + * @return The display name of the ability for this color. + */ + String name() default "Unnamed Material"; + + /** + * @return A brief description of what the ability material does. + */ + String description() default "No description provided."; + + /** + * @return cooldown time in gameTicks (20 per second/every 50ms) + */ + int cooldownTicks() default 20*60; +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/PatternInfo.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/PatternInfo.java new file mode 100644 index 0000000..9511629 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/PatternInfo.java @@ -0,0 +1,20 @@ +package me.trouper.trimserver.server.systems.abilities; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface PatternInfo { + /** + * @return The display name of the ability set (for the specific pattern). + */ + String name() default "Unnamed Trim Pattern"; + + /** + * @return A brief description of what the ability set does. + */ + String description() default "No description provided."; +} diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/WormEvent.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/WormEvent.java new file mode 100644 index 0000000..04b4993 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/WormEvent.java @@ -0,0 +1,427 @@ +package me.trouper.trimserver.server.systems.abilities; + +import me.trouper.trimserver.utils.Text; +import me.trouper.trimserver.utils.Verbose; +import me.trouper.trimserver.utils.visual.BlockDisplayRaytracer; +import org.bukkit.*; +import org.bukkit.block.data.BlockData; +import org.bukkit.damage.DamageSource; +import org.bukkit.damage.DamageType; +import org.bukkit.entity.Bat; +import org.bukkit.entity.BlockDisplay; +import org.bukkit.entity.Display; +import org.bukkit.entity.LivingEntity; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.util.BoundingBox; +import org.bukkit.util.Transformation; +import org.bukkit.util.Vector; +import org.joml.AxisAngle4f; +import org.joml.Vector3f; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +/** + * Handles the creation and animation of a Giant Worm event using BlockDisplay entities. + */ +public class WormEvent { + + private final JavaPlugin plugin; + + // --- Configuration --- + private static final Material WORM_BODY_MATERIAL = Material.PALE_OAK_WOOD; + private static final Material WORM_TEETH_MATERIAL = Material.DRIPSTONE_BLOCK; + private static final int TICKS_PER_SECOND = 5; // Standard Minecraft ticks per second + private static final double TEETH_CLOSE_FACTOR = 1; // How far teeth move inwards (fraction of radius, 0.0 to 1.0) + private static final double DAMAGE_AMOUNT = 19; // Damage dealt by the worm's bite + private static final float BITE_SOUND_VOLUME = 2.0f; + private static final float BITE_SOUND_PITCH_GROWL = 0.5f; + private static final float BITE_SOUND_PITCH_GRIND = 0.1f; + private static final int INTERPOLATION_TICKS = 1; // Smoothness of movement (1 tick) + + /** + * Constructor for the WormEvent handler. + * @param plugin Your plugin instance, needed for scheduling tasks. + */ + public WormEvent(JavaPlugin plugin) { + this.plugin = Objects.requireNonNull(plugin, "Plugin instance cannot be null"); + } + + /** + * Spawns a giant worm event at the specified location. + * The worm rises, attacks, and retreats. + * + * @param centerLocation The location where the center of the worm's mouth will appear at ground level. + * The worm will rise centered on this X/Z coordinate. + * @param radius The radius of the worm in blocks. Determines width and the height it rises. + * Must be a positive integer. + */ + public void spawnGiantWorm(Location centerLocation, int radius) { + // --- Input Validation --- + if (radius <= 0) { + plugin.getLogger().warning("Cannot spawn worm: Radius must be positive."); + return; + } + World world = centerLocation.getWorld(); + if (world == null) { + plugin.getLogger().warning("Cannot spawn worm: Location has a null world."); + return; + } + // Ensure the location is safe and loaded if necessary (optional, depends on context) + if (!world.isChunkLoaded(centerLocation.getBlockX() >> 4, centerLocation.getBlockZ() >> 4)) { + plugin.getLogger().warning("Cannot spawn worm: Target chunk is not loaded."); + // Optionally force load: world.loadChunk(centerLocation.getBlockX() >> 4, centerLocation.getBlockZ() >> 4); + return; + } + + + // --- Prepare Block Data --- + BlockData bodyBlockData = WORM_BODY_MATERIAL.createBlockData(); + BlockData teethBlockData = WORM_TEETH_MATERIAL.createBlockData(); + + // --- List to hold all entities --- + List allEntities = new ArrayList<>(); + + // --- Calculate Worm Dimensions and Positions --- + double segmentHeight = 1.0; + int segmentsPerRing = Math.max(8, (int) (radius * Math.PI * 2)); + double riseHeight = radius * segmentHeight; + Location initialBaseLocation = centerLocation.clone().subtract(0, riseHeight, 0); + + // --- Spawn Body Segment Entities --- + Verbose.send(String.format("Spawning worm body (Radius: %d, Height: %.1f, Segments per ring: %d)", radius, riseHeight, segmentsPerRing)); + List bodyEntities = new ArrayList<>(); + + for (int ySegment = 0; ySegment < radius; ySegment++) { // Iterate vertical segments + double currentY = initialBaseLocation.getY() + (ySegment * segmentHeight); + for (int i = 0; i < segmentsPerRing; i++) { // Iterate segments in the ring + double angle = (2 * Math.PI / segmentsPerRing) * i; + // Calculate position on the circle edge + double x = centerLocation.getX() + radius * Math.cos(angle); + double z = centerLocation.getZ() + radius * Math.sin(angle); + Location blockPos = new Location(world, x, currentY, z); + + // Spawn the display entity + BlockDisplay bodySegment = spawnBlockDisplay(blockPos, bodyBlockData); + if (bodySegment != null) { + bodyEntities.add(bodySegment); + allEntities.add(bodySegment); + } + } + } + + // --- Spawn Teeth Entities --- + Verbose.send("Spawning worm teeth..."); + List> teethRows = new ArrayList<>(); + + for (int row = 0; row < 4; row++) { + List rowTeeth = new ArrayList<>(); + double rowOffsetY = row == 0 ? initialBaseLocation.getY() + riseHeight - 0.5 : initialBaseLocation.getY() + (riseHeight - 1.0 - row); + + int teethPerRow = (int)(segmentsPerRing * 1.5); + for (int i = 0; i < teethPerRow; i++) { + double angle = (2 * Math.PI / teethPerRow) * i; + double x = centerLocation.getX() + radius * Math.cos(angle); + double z = centerLocation.getZ() + radius * Math.sin(angle); + Location startLocation = new Location(world, x, rowOffsetY, z); + + BlockDisplay tooth = spawnBlockDisplay(startLocation, teethBlockData); + if (tooth == null) continue; + + Location centerAtTeethY = centerLocation.clone().add(0.5, 0, 0.5); + centerAtTeethY.setY(startLocation.getY() - 1); + + Vector direction = centerAtTeethY.toVector().subtract(startLocation.toVector()).normalize(); + + BlockDisplayRaytracer.transform(tooth, startLocation, direction, 2.0, 0.5); // 2 blocks toward center, 0.5 thickness + rowTeeth.add(tooth); + allEntities.add(tooth); + } + teethRows.add(rowTeeth); + } + + + if (allEntities.isEmpty()) { + plugin.getLogger().severe("Failed to spawn any worm entities. Aborting event."); + return; + } + + // --- Start the Animation Task --- + Verbose.send("Starting worm animation..."); + new WormAnimationTask( + plugin, + centerLocation, + radius, + segmentHeight, + segmentsPerRing, + allEntities, + bodyEntities, + teethRows + ).runTaskTimer(plugin, 0L, 1L); // Start immediately, run every tick + } + + /** + * Helper method to spawn and configure a single BlockDisplay entity. + * Configures interpolation for smooth movement. + * + * @param location The initial location to spawn the entity. + * @param blockData The BlockData to display. + * @return The spawned BlockDisplay entity, or null if spawning failed. + */ + private BlockDisplay spawnBlockDisplay(Location location, BlockData blockData) { + World world = location.getWorld(); + if (world == null) return null; + + try { + return world.spawn(location, BlockDisplay.class, entity -> { + entity.setBlock(blockData); + // Interpolation settings for smooth movement between teleports: + entity.setInterpolationDuration(INTERPOLATION_TICKS); // Interpolate over X ticks + entity.setInterpolationDelay(-1); // Start interpolation immediately when teleported + entity.setTeleportDuration(INTERPOLATION_TICKS); // Client prediction time, match duration + + entity.setGravity(false); // Not affected by gravity + entity.setPersistent(false); // Don't save the entity + entity.setBrightness(new Display.Brightness(15, 15)); // Max block/sky light + // Default transformation (no translation, rotation, unit scale) + entity.setTransformation(new Transformation( + new Vector3f(0f, 0f, 0f), // Translation offset (none) + new AxisAngle4f(0f, 0f, 0f, 1f), // Left rotation (none) + new Vector3f(1f, 1f, 1f), // Scale (normal) + new AxisAngle4f(0f, 0f, 0f, 1f) // Right rotation (none) + )); + }); + } catch (Exception e) { + plugin.getLogger().severe("Failed to spawn BlockDisplay at " + location + ": " + e.getMessage()); + return null; + } + } + + // --- BukkitRunnable for Handling the Animation --- + private static class WormAnimationTask extends BukkitRunnable { + private final JavaPlugin plugin; + private final Location centerLocation; // Center of the worm's mouth at ground level + private final int radius; + private final double segmentHeight; + private final int segmentsPerRing; + private final List allEntities; // All worm parts + private final List bodyEntities; + private final List> teethEntities; + private final World world; + + // Timing calculation + private final double totalRiseHeight; + private final int riseDurationTicks; + private final int attackPauseTicks; + private final int retreatDurationTicks; + private final int totalDurationTicks; + private final double movementSpeedPerTick; // Vertical distance moved each tick + + private int ticksElapsed = 0; + private boolean attackPerformed = false; + private boolean retreatStarted = false; + private boolean teethReset = false; + + public WormAnimationTask(JavaPlugin plugin, Location centerLocation, int radius, double segmentHeight, int segmentsPerRing, + List allEntities, List bodyEntities, List> teethEntities) { + this.plugin = plugin; + this.centerLocation = centerLocation; + this.radius = radius; + this.segmentHeight = segmentHeight; + this.segmentsPerRing = segmentsPerRing; + this.allEntities = allEntities; + this.bodyEntities = bodyEntities; + this.teethEntities = teethEntities; + this.world = centerLocation.getWorld(); // World should not be null here based on prior checks + + // Calculate timing + this.totalRiseHeight = radius * segmentHeight; + // Speed: 1 block per second = 1 block per 20 ticks + this.movementSpeedPerTick = segmentHeight / TICKS_PER_SECOND; + this.riseDurationTicks = (int) Math.ceil(totalRiseHeight / movementSpeedPerTick); // Ticks to rise fully + this.attackPauseTicks = TICKS_PER_SECOND * 2; // Half second pause for attack visual/sound + this.retreatDurationTicks = riseDurationTicks; // Same duration to retreat + this.totalDurationTicks = riseDurationTicks + attackPauseTicks + retreatDurationTicks; + } + + @Override + public void run() { + // Safety check in case world becomes invalid during the task + if (world == null || !world.equals(centerLocation.getWorld())) { + plugin.getLogger().warning("Worm animation world became invalid. Cleaning up."); + cleanup(); + return; + } + + // --- Rising Phase --- + if (ticksElapsed < riseDurationTicks) { + moveEntities(movementSpeedPerTick); + } + // --- Attack Phase --- + else if (ticksElapsed == riseDurationTicks && !attackPerformed) { + attackPerformed = true; + performAttack(); // Close teeth and damage entities + + // Schedule the start of the retreat after a short pause + new BukkitRunnable() { + @Override + public void run() { + setFinalToothPosition(); // Open teeth again + retreatStarted = true; // Signal retreat can begin on the next tick + } + }.runTaskLater(plugin, attackPauseTicks); // Pause after attack + } + // --- Retreating Phase --- + else if (retreatStarted && ticksElapsed < totalDurationTicks) { + moveEntities(-movementSpeedPerTick); // Move downwards + } + // --- End & Cleanup Phase --- + else if (ticksElapsed >= totalDurationTicks) { + Verbose.send("Worm animation finished. Cleaning up entities."); + cleanup(); + this.cancel(); + return; + } + + ticksElapsed++; + } + + /** Helper method to teleport all worm entities vertically */ + private void moveEntities(double verticalOffset) { + allEntities.forEach(entity -> { + if (entity != null && entity.isValid()) { + // Teleport smoothly by the calculated offset + entity.teleport(entity.getLocation().add(0, verticalOffset, 0)); + } + }); + } + + /** Closes the teeth and damages nearby entities. */ + private void performAttack() { + Verbose.send("Worm attacking!"); + + // 1. Close Teeth Animation + Location attackCenter = centerLocation.clone().add(0, totalRiseHeight, 0); // Center at the top of the worm + int row = 0; + for (List teeth : teethEntities) { + for (BlockDisplay tooth : teeth) { + if (tooth == null || !tooth.isValid()) continue; + // Calculate vector pointing from tooth towards the center at the current height + Vector direction = attackCenter.toVector().subtract(tooth.getLocation().toVector()); + direction.setY(0); // Only move horizontally + + // Calculate the target closed position + // Move inwards by a factor of the radius + Location closedPosition = tooth.getLocation().add(direction.normalize().multiply(radius * TEETH_CLOSE_FACTOR)); + closedPosition.setY(tooth.getLocation().getY() - 1); // Keep Y the same as current tooth Y + + // Instantly teleport teeth inwards (no interpolation needed for snap) + // We temporarily disable interpolation for the snap effect + int originalInterpolation = tooth.getInterpolationDuration(); + tooth.setInterpolationDuration(5); // No interpolation for instant snap + //tooth.teleport(closedPosition); + BlockDisplayRaytracer.transform(tooth,tooth.getLocation(),closedPosition,0.3); + // Restore interpolation for subsequent movements (retreat) + // Schedule restoration for the next tick to ensure teleport completes + new BukkitRunnable() { + @Override + public void run() { + if (tooth.isValid()) { + tooth.setInterpolationDuration(originalInterpolation); + } + } + }.runTaskLater(plugin, 1L); + + } + row++; + } + + // 2. Play Sound Effects + world.playSound(attackCenter, Sound.ENTITY_ENDER_DRAGON_GROWL, SoundCategory.HOSTILE, BITE_SOUND_VOLUME, BITE_SOUND_PITCH_GROWL); + world.playSound(attackCenter, Sound.ENTITY_EVOKER_FANGS_ATTACK, SoundCategory.HOSTILE, BITE_SOUND_VOLUME, BITE_SOUND_PITCH_GRIND); + // Optional particle effects + world.spawnParticle(Particle.BLOCK_CRUMBLE, attackCenter, 50 * radius, radius * 0.5, 0.5, radius * 0.5, 0.1, WORM_BODY_MATERIAL.createBlockData()); + + // 3. Damage Entities within the Bounding Box + // Define the bounding box for the attack area (cylinder when fully risen) + BoundingBox attackBox = BoundingBox.of( + centerLocation.clone().subtract(radius, 0, radius), // Bottom corner + centerLocation.clone().add(radius, totalRiseHeight + 1.0, radius) // Top corner (add buffer) + ); + + // Find living entities within the box + Collection nearbyLivingEntities = world.getNearbyEntities(attackBox, entity -> + entity instanceof LivingEntity // Check if it's a living entity + && !allEntities.contains(entity) // Make sure it's not part of the worm itself + && entity.isValid() // Ensure the entity is still valid + && !entity.isDead() // Don't try to kill already dead entities + ).stream().map(e -> (LivingEntity) e).toList(); // Cast to LivingEntity + + Verbose.send("Found " + nearbyLivingEntities.size() + " living entities in attack range."); + + // Apply instant kill damage + for (LivingEntity entity : nearbyLivingEntities) { + plugin.getLogger().fine("Damaging entity: " + entity.getType() + " at " + entity.getLocation()); + // entity.setHealth(0.0); // Instant kill is often more reliable than massive damage + Bat dummy = world.spawn(centerLocation,Bat.class); + dummy.customName(Text.color("Shai-Hulud")); + dummy.setInvisible(true); + dummy.setInvulnerable(true); + dummy.setAI(false); + dummy.addScoreboardTag("$/TrimServer/ Temp"); + entity.damage(DAMAGE_AMOUNT, DamageSource.builder(DamageType.MOB_ATTACK).withDamageLocation(centerLocation).withDirectEntity(dummy).build()); // Alternative if setHealth(0) causes issues + dummy.remove(); + world.spawnParticle(Particle.DAMAGE_INDICATOR, entity.getEyeLocation(), 10, 0.2, 0.2, 0.2, 0.1); + entity.addPotionEffect(new PotionEffect(PotionEffectType.WITHER,60,1,true,false,false)); + } + } + + /** Resets the teeth to their outer ring position relative to the current height. */ + private void setFinalToothPosition() { + if (teethEntities.isEmpty() || teethReset) return; // Don't run if no teeth or already reset + plugin.getLogger().fine("Setting teeth position for retreat."); + + for (List toothRow : teethEntities) { + double rowY = toothRow.getFirst().getY(); + for (BlockDisplay tooth : toothRow) { + Location pointTo = centerLocation.clone(); + pointTo.setY(rowY); + + BlockDisplayRaytracer.transform(tooth,tooth.getLocation(),pointTo,0.4); + } + } + teethReset = true; // Ensure this only runs once + } + + + /** Removes all worm entities and cancels the task. */ + private void cleanup() { + allEntities.forEach(entity -> { + if (entity != null && entity.isValid()) { + entity.remove(); // Remove the display entity from the world + } + }); + // Clear lists to release references + allEntities.clear(); + bodyEntities.clear(); + for (List row : teethEntities) { + row.clear(); + } + teethEntities.clear(); + } + + /** Overridden cancel to ensure cleanup is called */ + @Override + public synchronized void cancel() throws IllegalStateException { + if (ticksElapsed < totalDurationTicks + TICKS_PER_SECOND) { // Ensure cleanup runs if cancelled early + cleanup(); + } + super.cancel(); + } + } +} diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/BoltAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/BoltAbility.java new file mode 100644 index 0000000..838daa2 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/BoltAbility.java @@ -0,0 +1,139 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.Main; +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.utils.SoundPlayer; +import me.trouper.trimserver.utils.TargetingUtils; +import me.trouper.trimserver.utils.visual.BlockDisplayRaytracer; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.damage.DamageSource; +import org.bukkit.damage.DamageType; +import org.bukkit.entity.Player; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.List; + +@PatternInfo(name = "Bolt", description = "Summon a bolt of lightning at enemies. Includes Variants.") +public class BoltAbility extends AbstractAbility implements Main { + + public BoltAbility() { + super(TrimPattern.BOLT); + } + + public boolean strike(Player caster, int range, double damage, Material innerBlock, Material outerBlock) { + return TargetingUtils.areaAffect(caster.getLocation(),range,target-> !main.man().trustBackend.trusts(caster,target),(target)->{ + drawLightning(caster.getEyeLocation(),target.getEyeLocation(),innerBlock,outerBlock); + target.damage(damage,DamageSource.builder(DamageType.LIGHTNING_BOLT).withDamageLocation(caster.getEyeLocation()).withDirectEntity(caster).build()); + }); + } + + public void drawLightning(Location start, Location end, Material blockInner, Material blockOuter) { + int segments = 10; + double maxOffset = 0.5; + long stayTime = 10L; + double thickness = 0.07; + double thicknessOut = 0.1; + List viewers = new ArrayList<>(start.getWorld().getPlayers()); + + Location current = start.clone(); + Vector direction = end.clone().subtract(start).toVector(); + double segmentLength = direction.length() / segments; + direction.normalize().multiply(segmentLength); + SoundPlayer bolt = new SoundPlayer(end, Sound.ENTITY_LIGHTNING_BOLT_THUNDER,10,1); + SoundPlayer ring = new SoundPlayer(end, Sound.ITEM_TRIDENT_THUNDER,10,1); + + bolt.playWithin(50); + ring.playWithin(30); + + for (int i = 0; i < segments; i++) { + Vector offset = new Vector( + (random.nextDouble() - 0.5) * maxOffset, + (random.nextDouble() - 0.5) * maxOffset, + (random.nextDouble() - 0.5) * maxOffset + ); + Location next = current.clone().add(direction).add(offset); + SoundPlayer zip = new SoundPlayer(next, Sound.ENTITY_BEE_STING,10,1); + zip.playWithin(10); + BlockDisplayRaytracer.trace(blockInner, current, next, thickness, stayTime, viewers); + BlockDisplayRaytracer.trace(blockOuter, current, next, thicknessOut, stayTime, viewers); + next.getWorld().spawnParticle(Particle.FLASH,next,0,0,0,0,0); + current = next; + } + + BlockDisplayRaytracer.trace(blockInner, current, end, thickness, stayTime, viewers); + BlockDisplayRaytracer.trace(blockOuter, current, end, thicknessOut, stayTime, viewers); + } + + @MaterialInfo(name = "Amethyst Bolt",description = "Shoots a bolt of colored lightning at your closest enemy within 20 blocks. Deals 10 Damage", cooldownTicks = 20*10) + @Override + public boolean amethystAbility(Player player) { + return strike(player,20,10,Material.AMETHYST_BLOCK,Material.PURPLE_STAINED_GLASS); + } + + @MaterialInfo(name = "Copper Bolt",description = "Shoots a bolt of colored lightning at your closest enemy within 20 blocks. Deals 10 Damage", cooldownTicks = 20*10) + @Override + public boolean copperAbility(Player player) { + return strike(player,20,10,Material.ORANGE_TERRACOTTA,Material.ORANGE_STAINED_GLASS); + } + + @MaterialInfo(name = "Diamond Bolt",description = "Shoots a bolt of colored lightning at your closest enemy within 20 blocks. Deals 10 Damage", cooldownTicks = 20*10) + @Override + public boolean diamondAbility(Player player) { + return strike(player,20,10,Material.LIGHT_BLUE_CONCRETE_POWDER,Material.LIGHT_BLUE_STAINED_GLASS); + } + + @MaterialInfo(name = "Emerald ",description = "Shoots a bolt of colored lightning at your closest enemy within 20 blocks. Deals 10 Damage", cooldownTicks = 20*10) + @Override + public boolean emeraldAbility(Player player) { + return strike(player,20,10,Material.LIME_CONCRETE,Material.LIME_STAINED_GLASS); + } + + @MaterialInfo(name = "Gold Bolt",description = "Shoots a bolt of colored lightning at your closest enemy within 20 blocks. Deals 10 Damage", cooldownTicks = 20*10) + @Override + public boolean goldAbility(Player player) { + return strike(player,20,10,Material.YELLOW_CONCRETE_POWDER,Material.YELLOW_STAINED_GLASS); + } + + @MaterialInfo(name = "Iron Bolt",description = "Shoots a bolt of colored lightning at your closest enemy within 20 blocks. Deals 10 Damage", cooldownTicks = 20*10) + @Override + public boolean ironAbility(Player player) { + return strike(player,20,10,Material.LIGHT_GRAY_WOOL,Material.LIGHT_GRAY_STAINED_GLASS); + } + + @MaterialInfo(name = "Lapis Bolt",description = "Shoots a bolt of colored lightning at your closest enemy within 20 blocks. Deals 10 Damage", cooldownTicks = 20*10) + @Override + public boolean lapisAbility(Player player) { + return strike(player,20,10,Material.BLUE_CONCRETE,Material.BLUE_STAINED_GLASS); + } + + @MaterialInfo(name = "Netherite Bolt",description = "Shoots a bolt of colored lightning at your closest enemy within 20 blocks. Deals 15 Damage", cooldownTicks = 20*10) + @Override + public boolean netheriteAbility(Player player) { + return strike(player,30,15,Material.BLACK_CONCRETE,Material.GRAY_STAINED_GLASS); + } + + @MaterialInfo(name = "Quartz Bolt",description = "Shoots a bolt of overcharged lightning at your closest enemy within 20 blocks. Deals 15 Damage", cooldownTicks = 20*12) + @Override + public boolean quartzAbility(Player player) { + return strike(player,20,10,Material.WHITE_CONCRETE,Material.WHITE_STAINED_GLASS); + } + + @MaterialInfo(name = "Redstone Bolt",description = "Shoots a bolt of colored lightning at your closest enemy within 20 blocks. Deals 10 Damage", cooldownTicks = 20*10) + @Override + public boolean redstoneAbility(Player player) { + return strike(player,20,10,Material.RED_CONCRETE_POWDER,Material.RED_STAINED_GLASS); + } + + @MaterialInfo(name = "Resin Bolt",description = "Shoots a bolt of resin lightning at your closest enemy within 20 blocks. Deals 7 Damage", cooldownTicks = 20*5) + @Override + public boolean resinAbility(Player player) { + return strike(player,20,7,Material.RESIN_BLOCK,Material.ORANGE_STAINED_GLASS); + } +} diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/CoastAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/CoastAbility.java new file mode 100644 index 0000000..016d04e --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/CoastAbility.java @@ -0,0 +1,259 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.utils.SoundPlayer; +import me.trouper.trimserver.utils.TargetingUtils; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.Vector; + +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@PatternInfo(name = "Undertoe", description = "Conjure a swirling vortex that pulls, damages, and disorients foes.") +public class CoastAbility extends AbstractAbility { + + private final Map activeUndertoes = new ConcurrentHashMap<>(); + + private static final double BASE_UNDERTOE_RADIUS = 5.5; + private static final int BASE_UNDERTOE_DURATION_TICKS = 10 * 20; + private static final double BASE_PULL_STRENGTH = 0.18; + private static final double BASE_DAMAGE_PER_SECOND = 2.0; + + // Cooldowns + private static final int DEFAULT_COOLDOWN = 20 * 45; + private static final int NETHERITE_COOLDOWN = 20 * 60; + private static final int RESIN_COOLDOWN = 20 * 25; + + // Netherite Modifiers + private static final double NETHERITE_RADIUS_MULTIPLIER = 1.25; + private static final double NETHERITE_DURATION_MULTIPLIER = 1.5; + private static final double NETHERITE_DAMAGE_MULTIPLIER = 1.5; + private static final double NETHERITE_PULL_MULTIPLIER = 1.3; + private static final int NETHERITE_SLOWNESS_AMPLIFIER = 2; + + // Resin Modifiers + private static final double RESIN_DAMAGE_MULTIPLIER = 0.8; + + + public CoastAbility() { + super(TrimPattern.COAST); + } + + private void createUndertoe(Player player, Material material) { + UUID playerUUID = player.getUniqueId(); + + if (activeUndertoes.containsKey(playerUUID)) { + activeUndertoes.get(playerUUID).cancel(); + } + + Location castLocation; + Block targetBlock = player.getTargetBlockExact(20, FluidCollisionMode.NEVER); + + if (targetBlock != null && targetBlock.getType() != Material.AIR) { + castLocation = targetBlock.getLocation().add(0.5, 1.0, 0.5); + } else { + Vector direction = player.getLocation().getDirection().setY(0).normalize().multiply(5); + castLocation = player.getLocation().add(direction); + } + + Location centerLocation = TargetingUtils.findGroundLocation(castLocation); + World world = centerLocation.getWorld(); + + if (world == null) return; + + double currentRadius = BASE_UNDERTOE_RADIUS; + int currentDuration = BASE_UNDERTOE_DURATION_TICKS; + double currentDamage = BASE_DAMAGE_PER_SECOND; + double currentPullStrength = BASE_PULL_STRENGTH; + int slownessAmplifier = 1; + + if (material == Material.NETHERITE_BLOCK) { + currentRadius *= NETHERITE_RADIUS_MULTIPLIER; + currentDuration = (int) (currentDuration * NETHERITE_DURATION_MULTIPLIER); + currentDamage *= NETHERITE_DAMAGE_MULTIPLIER; + currentPullStrength *= NETHERITE_PULL_MULTIPLIER; + slownessAmplifier = NETHERITE_SLOWNESS_AMPLIFIER; + } else if (material == Material.RESIN_BLOCK) { + currentDamage *= RESIN_DAMAGE_MULTIPLIER; + } + + new SoundPlayer(centerLocation, Sound.ENTITY_PLAYER_SPLASH_HIGH_SPEED, 1.2f, 0.8f).playWithin(30); + new SoundPlayer(centerLocation, Sound.BLOCK_WATER_AMBIENT, 1.0f, 0.5f).playWithin(30); + + final double finalRadius = currentRadius; + final int finalDuration = currentDuration; + final double finalDamage = currentDamage; + final double finalPullStrength = currentPullStrength; + final int finalSlownessAmplifier = slownessAmplifier; + + BukkitTask task = new BukkitRunnable() { + int ticksElapsed = 0; + final Random random = new Random(); + final Location effectCenter = centerLocation; + + @Override + public void run() { + if (!player.isOnline() || ticksElapsed > finalDuration) { + endUndertoe(playerUUID, effectCenter); + cancel(); + return; + } + + double visualRadius = finalRadius * Math.max(0.4, (1.0 - (double)ticksElapsed / (finalDuration * 1.2))); + + for (int i = 0; i < 360; i += (material == Material.NETHERITE_BLOCK ? 10 : 15)) { + double angle = Math.toRadians(i + (ticksElapsed * 7)); + double x = Math.cos(angle) * visualRadius; + double z = Math.sin(angle) * visualRadius; + Location particleLoc = effectCenter.clone().add(x, 0.2 + random.nextDouble() * 0.5, z); + + world.spawnParticle(Particle.BUBBLE_COLUMN_UP, particleLoc, 1, 0, 0, 0, 0); + world.spawnParticle(Particle.FALLING_WATER, particleLoc, random.nextInt(2) + 1, 0.15, 0.15, 0.15, 0); + if (ticksElapsed % 4 == 0) { + world.spawnParticle(Particle.BUBBLE_POP, particleLoc.clone().add(0, 0.2, 0), 1, 0.2, 0.2, 0.2, 0); + } + } + + if (ticksElapsed % 6 == 0) { + world.spawnParticle(Particle.UNDERWATER, effectCenter.clone().add(0, 1.8, 0), 10, finalRadius * 0.5, 0.6, finalRadius * 0.5, 0.1); + world.spawnParticle(Particle.NAUTILUS, effectCenter.clone().add(0, 2.0, 0), 10, 0.4, 0.4, 0.4, 0.05); + } + + TargetingUtils.areaAffect(effectCenter,finalRadius,(entity) -> !entity.equals(player) && !entity.isDead() && !(entity.getVehicle() instanceof Player) && entity.getLocation().distanceSquared(effectCenter) < finalRadius * finalRadius && !main.man().trustBackend.trusts(player,entity), entity -> { + Vector toCenter = effectCenter.toVector().subtract(entity.getLocation().toVector()); + double distanceSquared = entity.getLocation().distanceSquared(effectCenter); + + if (distanceSquared != 0) { + double pullFactor = finalPullStrength * Math.max(0.1, (1.0 - (Math.sqrt(distanceSquared) / finalRadius))); + Vector pullVelocity = toCenter.normalize().multiply(pullFactor); + + if (Math.sqrt(distanceSquared) < finalRadius * 0.3) { + pullVelocity.setY(pullVelocity.getY() - 0.2); + } else { + pullVelocity.setY(pullVelocity.getY() - 0.08); + } + + entity.setVelocity(entity.getVelocity().add(pullVelocity)); + + if (ticksElapsed % 20 == 0) { // Every second + entity.damage(finalDamage, player); + entity.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS, 45, finalSlownessAmplifier -1, true, false, true)); + entity.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, 35, 0, true, false, true)); + entity.setRemainingAir(Math.max(0, entity.getRemainingAir() - 40)); // Suffocation effect + + new SoundPlayer(entity.getLocation(), Sound.ENTITY_DROWNED_HURT_WATER, 0.9f, 1.4f).playWithin(15); + } + world.spawnParticle(Particle.BLOCK_CRUMBLE, entity.getLocation().add(0, entity.getHeight() * 0.3, 0), 3, 0.2, 0.2, 0.2, 0.01, Material.WATER.createBlockData()); + } + }); + + if (ticksElapsed % 30 == 0) { + new SoundPlayer(effectCenter, Sound.BLOCK_BUBBLE_COLUMN_WHIRLPOOL_AMBIENT, 0.9f, 1.0f + (random.nextFloat() * 0.4f - 0.2f)).playWithin(25); + if(material == Material.NETHERITE_BLOCK && ticksElapsed % 60 == 0) { + new SoundPlayer(effectCenter, Sound.ENTITY_ELDER_GUARDIAN_CURSE, 0.6f, 1.6f + random.nextFloat() * 0.2f).playWithin(35); + } + } + ticksElapsed++; + } + }.runTaskTimer(main.getPlugin(), 0L, 1L); + activeUndertoes.put(playerUUID, task); + } + + private void endUndertoe(UUID playerUUID, Location center) { + activeUndertoes.remove(playerUUID); + + if (center != null && center.getWorld() != null) { + new SoundPlayer(center, Sound.ENTITY_FISHING_BOBBER_SPLASH, 1.1f, 0.6f).playWithin(30); + center.getWorld().spawnParticle(Particle.EXPLOSION_EMITTER, center.clone().add(0, 0.5, 0), 1, 0,0,0,0); + center.getWorld().spawnParticle(Particle.FALLING_WATER, center.clone().add(0,0.5,0), 50, 2, 1, 2, 0.2); + } + } + + @MaterialInfo(name = "Amethyst Undertoe", description = "A swirling vortex pulls and drowns foes.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean amethystAbility(Player player) { + createUndertoe(player, Material.AMETHYST_BLOCK); + return true; + } + + @MaterialInfo(name = "Copper Undertoe", description = "A swirling vortex pulls and drowns foes.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean copperAbility(Player player) { + createUndertoe(player, Material.COPPER_BLOCK); + return true; + } + + @MaterialInfo(name = "Diamond Undertoe", description = "A swirling vortex pulls and drowns foes.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean diamondAbility(Player player) { + createUndertoe(player, Material.DIAMOND_BLOCK); + return true; + } + + @MaterialInfo(name = "Emerald Undertoe", description = "A swirling vortex pulls and drowns foes.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean emeraldAbility(Player player) { + createUndertoe(player, Material.EMERALD_BLOCK); + return true; + } + + @MaterialInfo(name = "Gold Undertoe", description = "A swirling vortex pulls and drowns foes.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean goldAbility(Player player) { + createUndertoe(player, Material.GOLD_BLOCK); + return true; + } + + @MaterialInfo(name = "Iron Undertoe", description = "A swirling vortex pulls and drowns foes.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean ironAbility(Player player) { + createUndertoe(player, Material.IRON_BLOCK); + return true; + } + + @MaterialInfo(name = "Lapis Undertoe", description = "A swirling vortex pulls and drowns foes.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean lapisAbility(Player player) { + createUndertoe(player, Material.LAPIS_BLOCK); + return true; + } + + @MaterialInfo(name = "Netherite Undertoe", description = "A larger, stronger, and longer-lasting vortex that fiercely pulls, damages, and disorients foes.", cooldownTicks = NETHERITE_COOLDOWN) + @Override + public boolean netheriteAbility(Player player) { + createUndertoe(player, Material.NETHERITE_BLOCK); + return true; + } + + @MaterialInfo(name = "Quartz Undertoe", description = "A swirling vortex pulls and drowns foes.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean quartzAbility(Player player) { + createUndertoe(player, Material.QUARTZ_BLOCK); + return true; + } + + @MaterialInfo(name = "Redstone Undertoe", description = "A swirling vortex pulls and drowns foes.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean redstoneAbility(Player player) { + createUndertoe(player, Material.REDSTONE_BLOCK); + return true; + } + + @MaterialInfo(name = "Resin Undertoe", description = "A slightly weaker vortex that pulls and drowns foes, but recharges faster.", cooldownTicks = RESIN_COOLDOWN) + @Override + public boolean resinAbility(Player player) { + createUndertoe(player, Material.RESIN_BLOCK); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/DuneAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/DuneAbility.java new file mode 100644 index 0000000..b24e72f --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/DuneAbility.java @@ -0,0 +1,205 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import com.destroystokyo.paper.event.player.PlayerJumpEvent; +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.server.systems.abilities.WormEvent; +import me.trouper.trimserver.utils.TargetingUtils; +import me.trouper.trimserver.utils.visual.DisplayUtils; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.damage.DamageSource; +import org.bukkit.damage.DamageType; +import org.bukkit.entity.*; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerRespawnEvent; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.util.Vector; + +import java.util.concurrent.atomic.AtomicInteger; + +@PatternInfo(name = "Wormsign", description = "\"Lisan al Gaib!\"") +public class DuneAbility extends AbstractAbility { + + public DuneAbility() { + super(TrimPattern.DUNE); + } + + private void spawnWormSign(Player owner, Location loc) { + AtomicInteger t = new AtomicInteger(0); + Bukkit.getScheduler().runTaskTimer(main.getPlugin(),(wave)->{ + if (t.getAndIncrement() >= 25) { + wave.cancel(); + } + DisplayUtils.wave(loc,15,1,1,(point)->{ + Block block = point.getWorld().getHighestBlockAt((int) point.x(), (int) point.z()); + BlockData data = block.getBlockData(); + block.getWorld().playSound(block.getLocation(), Sound.BLOCK_ROOTED_DIRT_BREAK,0.1F,1); + block.getWorld().spawnParticle(Particle.BLOCK_CRUMBLE,block.getLocation().add(0,1,0),1,0.5,0,0.5, data); + + TargetingUtils.areaAffect(block.getLocation(),2, target -> !target.isDead() && !main.man().trustBackend.trusts(owner,target), liv->{ + liv.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS,20,4,true,false,false)); + Vector pullDirection = loc.toVector().subtract(liv.getLocation().toVector()).normalize(); + Vector pullVelocity = pullDirection.multiply(0.0015); + liv.setVelocity(liv.getVelocity().add(pullVelocity)); + }); + }); + },0,10); + + for (int i = 0; i < 10; i++) { + int finalI = i; + Bukkit.getScheduler().runTaskLater(main.getPlugin(),()->{ + DisplayUtils.ring(loc, finalI,1,(point)->{ + BlockData data = point.getWorld().getHighestBlockAt((int) point.x(), (int) point.z()).getBlockData(); + BlockState state = point.getWorld().getHighestBlockAt((int) point.x(), (int) point.z()).getState(); + FallingBlock block = (FallingBlock) point.getWorld().spawnEntity(point.clone().add(0,1,0), EntityType.FALLING_BLOCK); + block.setBlockData(data); + block.setBlockState(state); + block.setVelocity(new Vector(0,0.1,0)); + block.setCancelDrop(true); + TargetingUtils.areaAffect(block.getLocation(),2, target -> !target.isDead() && !main.man().trustBackend.trusts(owner,target), liv->{ + if (liv.getLocation().getBlock().isPassable()) { + liv.teleport(liv.getLocation().clone().add(0,-1,0)); + liv.damage(5, DamageSource.builder(DamageType.IN_WALL).withDamageLocation(loc).withDirectEntity(owner).build()); + } + }); + }); + },i*2+(20*2)); + } + + for (int i = 0; i < 15; i++) { + int finalI = i; + Bukkit.getScheduler().runTaskLater(main.getPlugin(),()->{ + DisplayUtils.ring(loc, finalI,1,(point)->{ + BlockData data = point.getWorld().getHighestBlockAt((int) point.x(), (int) point.z()).getBlockData(); + BlockState state = point.getWorld().getHighestBlockAt((int) point.x(), (int) point.z()).getState(); + FallingBlock block = (FallingBlock) point.getWorld().spawnEntity(point.clone().add(0,1,0), EntityType.FALLING_BLOCK); + block.setBlockData(data); + block.setBlockState(state); + block.setVelocity(new Vector(0,0.1,0)); + block.setCancelDrop(true); + TargetingUtils.areaAffect(block.getLocation(),2, target -> !target.isDead() && !main.man().trustBackend.trusts(owner,target), liv->{ + if (liv.getLocation().getBlock().isPassable()) { + liv.teleport(liv.getLocation().clone().add(0,-1,0)); + liv.damage(5,DamageSource.builder(DamageType.IN_WALL).withDamageLocation(loc).withDirectEntity(owner).build()); + liv.addScoreboardTag("$/TrimServer/ NoJumping"); + Bukkit.getScheduler().runTaskLater(main.getPlugin(),()->{ + liv.removeScoreboardTag("$/TrimServer/ NoJumping"); + },20*10); + } + }); + }); + },i*2+(20*5)); + } + + Location worm = loc.clone().add(0,-1,0); + Bukkit.getScheduler().runTaskLater(main.getPlugin(),()->{ + new WormEvent(main.getPlugin()).spawnGiantWorm(worm,7); + },20*7); + } + + @EventHandler + public void onJoin(PlayerJoinEvent e) { + if (e.getPlayer().getScoreboardTags().contains("$/TrimServer/ NoJumping")) { + e.getPlayer().getScoreboardTags().remove("$/TrimServer/ NoJumping"); + } + } + + @EventHandler + public void onJump(PlayerJumpEvent e) { + if (e.getPlayer().getScoreboardTags().contains("$/TrimServer/ NoJumping")) { + e.setCancelled(true); + e.getPlayer().getVelocity().add(new Vector(0,-10,0)); + } + } + + @EventHandler + public void onDeath(PlayerRespawnEvent e) { + if (e.getPlayer().getScoreboardTags().contains("$/TrimServer/ NoJumping")) { + e.getPlayer().getScoreboardTags().remove("$/TrimServer/ NoJumping"); + } + } + + @MaterialInfo(name = "Amethyst Wormsign",description = "Call upon Shai-Hulud to destroy your enemies", cooldownTicks = 20*160) + @Override + public boolean amethystAbility(Player player) { + spawnWormSign(player,player.getLocation()); + return true; + } + + @MaterialInfo(name = "Copper Wormsign",description = "Call upon Shai-Hulud to destroy your enemies", cooldownTicks = 20*160) + @Override + public boolean copperAbility(Player player) { + spawnWormSign(player,player.getLocation()); + return true; + } + + @MaterialInfo(name = "Diamond Wormsign",description = "Call upon Shai-Hulud to destroy your enemies", cooldownTicks = 20*160) + @Override + public boolean diamondAbility(Player player) { + spawnWormSign(player,player.getLocation()); + return true; + } + + @MaterialInfo(name = "Emerald Wormsign",description = "Call upon Shai-Hulud to destroy your enemies", cooldownTicks = 20*160) + @Override + public boolean emeraldAbility(Player player) { + spawnWormSign(player,player.getLocation()); + return true; + } + + @MaterialInfo(name = "Gold Wormsign",description = "Call upon Shai-Hulud to destroy your enemies", cooldownTicks = 20*160) + @Override + public boolean goldAbility(Player player) { + spawnWormSign(player,player.getLocation()); + return true; + } + + @MaterialInfo(name = "Iron Wormsign",description = "Call upon Shai-Hulud to destroy your enemies", cooldownTicks = 20*160) + @Override + public boolean ironAbility(Player player) { + spawnWormSign(player,player.getLocation()); + return true; + } + + @MaterialInfo(name = "Lapis Wormsign",description = "Call upon Shai-Hulud to destroy your enemies", cooldownTicks = 20*160) + @Override + public boolean lapisAbility(Player player) { + spawnWormSign(player,player.getLocation()); + return true; + } + + @MaterialInfo(name = "Netherite Wormsign",description = "Call upon Shai-Hulud to destroy your enemies", cooldownTicks = 20*90) + @Override + public boolean netheriteAbility(Player player) { + spawnWormSign(player,player.getLocation()); + return true; + } + + @MaterialInfo(name = "Quartz Wormsign",description = "Call upon Shai-Hulud to destroy your enemies", cooldownTicks = 20*160) + @Override + public boolean quartzAbility(Player player) { + spawnWormSign(player,player.getLocation()); + return true; + } + + @MaterialInfo(name = "Redstone Wormsign",description = "Call upon Shai-Hulud to destroy your enemies", cooldownTicks = 20*160) + @Override + public boolean redstoneAbility(Player player) { + spawnWormSign(player,player.getLocation()); + return true; + } + + @MaterialInfo(name = "Resin Wormsign",description = "Call upon Shai-Hulud to destroy your enemies", cooldownTicks = 20*160) + @Override + public boolean resinAbility(Player player) { + spawnWormSign(player,player.getLocation()); + return true; + } +} diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/EyeAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/EyeAbility.java new file mode 100644 index 0000000..fc83cb2 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/EyeAbility.java @@ -0,0 +1,148 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.utils.SoundPlayer; +import me.trouper.trimserver.utils.TargetingUtils; +import me.trouper.trimserver.utils.visual.BlockDisplayRaytracer; +import me.trouper.trimserver.utils.visual.CustomDisplayRaytracer; +import org.bukkit.*; +import org.bukkit.damage.DamageSource; +import org.bukkit.damage.DamageType; +import org.bukkit.entity.Player; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.util.Vector; + +import java.util.concurrent.atomic.AtomicInteger; + +@PatternInfo(name = "Eye of Power", description = "Allows you to see players hidden with the host trim. Includes variants.") +public class EyeAbility extends AbstractAbility { + + public EyeAbility() { + super(TrimPattern.EYE); + } + + public void eyeLasers(Player player, Material beam,Material glow, long durationSeconds) { + AtomicInteger timer = new AtomicInteger(0); + Bukkit.getScheduler().runTaskTimer(main.getPlugin(),(task)->{ + if (timer.getAndIncrement() >= durationSeconds * 20) { + task.cancel(); + return; + } + Location chestLocation = player.getLocation(); + chestLocation.setY(chestLocation.getY() + (player.getHeight() / 2) + 0.1); + double radians = Math.toRadians(player.getBodyYaw()); + + double x = -Math.sin(radians); + double z = Math.cos(radians); + + Vector chestDirection = new Vector(x,0,z).normalize(); + + Location focus = laser(player,chestLocation,chestDirection,60); + BlockDisplayRaytracer.trace(beam,chestLocation,focus,0.2,2); + BlockDisplayRaytracer.trace(glow,chestLocation,focus,0.4,2); + },0,1); + } + + public Location laser(Player owner, Location start, Vector direction, double distance) { + return CustomDisplayRaytracer.trace(start,direction,distance,1,point ->{ + SoundPlayer hissSound = new SoundPlayer(point.getLoc(), Sound.BLOCK_LAVA_EXTINGUISH, 1, 1); + + return TargetingUtils.areaAffect(point.getLoc(),1,target -> !target.isDead() && !main.man().trustBackend.trusts(owner,target), liv->{ + hissSound.playWithin(10); + + int tick = liv.getNoDamageTicks(); + int maxTick = liv.getMaximumNoDamageTicks(); + liv.setNoDamageTicks(0); + liv.setMaximumNoDamageTicks(0); + liv.damage(0.5,DamageSource.builder(DamageType.STING).withDamageLocation(owner.getLocation()).withDirectEntity(owner).build()); + liv.setNoDamageTicks(tick); + liv.setMaximumNoDamageTicks(maxTick); + + liv.setFireTicks(20); + liv.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS,20,1,true,false,false)); + liv.getWorld().spawnParticle(Particle.LAVA, point.getLoc(), 1, 0.5, 0.5, 0.5, 0); + }) || !point.getBlock().isPassable(); + }).getLoc(); + } + + @MaterialInfo(name = "Amethyst Laser beam", description = "Shoot lasers from the eye on the chestpiece for 5 seconds", cooldownTicks = 20 * 30) + @Override + public boolean amethystAbility(Player player) { + eyeLasers(player,Material.PURPLE_CONCRETE_POWDER,Material.MAGENTA_STAINED_GLASS,5); + return true; + } + + @MaterialInfo(name = "Copper Laser beam", description = "Shoot lasers from the eye on the chestpiece for 5 seconds", cooldownTicks = 20 * 30) + @Override + public boolean copperAbility(Player player) { + eyeLasers(player,Material.LIME_TERRACOTTA,Material.GREEN_STAINED_GLASS,5); + return true; + } + + @MaterialInfo(name = "Diamond Laser beam", description = "Shoot lasers from the eye on the chestpiece for 5 seconds", cooldownTicks = 20 * 30) + @Override + public boolean diamondAbility(Player player) { + eyeLasers(player,Material.LIGHT_BLUE_CONCRETE_POWDER,Material.LIGHT_BLUE_STAINED_GLASS,5); + return true; + } + + @MaterialInfo(name = "Emerald Laser beam", description = "Shoot lasers from the eye on the chestpiece for 5 seconds", cooldownTicks = 20 * 30) + @Override + public boolean emeraldAbility(Player player) { + eyeLasers(player,Material.LIME_CONCRETE_POWDER,Material.LIME_STAINED_GLASS,5); + return true; + } + + @MaterialInfo(name = "Gold Laser beam", description = "Shoot lasers from the eye on the chestpiece for 5 seconds", cooldownTicks = 20 * 30) + @Override + public boolean goldAbility(Player player) { + eyeLasers(player,Material.YELLOW_TERRACOTTA,Material.YELLOW_STAINED_GLASS,5); + return true; + } + + @MaterialInfo(name = "Iron Laser beam", description = "Shoot lasers from the eye on the chestpiece for 5 seconds", cooldownTicks = 20 * 30) + @Override + public boolean ironAbility(Player player) { + eyeLasers(player,Material.LIGHT_GRAY_CONCRETE_POWDER,Material.LIGHT_GRAY_STAINED_GLASS,5); + return true; + } + + @MaterialInfo(name = "Lapis Laser beam", description = "Shoot lasers from the eye on the chestpiece for 5 seconds", cooldownTicks = 20 * 30) + @Override + public boolean lapisAbility(Player player) { + eyeLasers(player,Material.BLUE_CONCRETE,Material.BLUE_STAINED_GLASS,5); + return true; + } + + @MaterialInfo(name = "Netherite Laser beam", description = "Shoot lasers from the eye on the chestpiece for 10 seconds", cooldownTicks = 20 * 40) + @Override + public boolean netheriteAbility(Player player) { + eyeLasers(player,Material.BLACK_CONCRETE,Material.BLACK_STAINED_GLASS,20); + return true; + } + + @MaterialInfo(name = "Quartz Laser beam", description = "Shoot lasers from the eye on the chestpiece for 5 seconds", cooldownTicks = 20 * 30) + @Override + public boolean quartzAbility(Player player) { + eyeLasers(player,Material.WHITE_CONCRETE_POWDER,Material.WHITE_STAINED_GLASS,5); + return true; + } + + @MaterialInfo(name = "Redstone Laser beam", description = "Shoot lasers from the eye on the chestpiece for 5 seconds", cooldownTicks = 20 * 30) + @Override + public boolean redstoneAbility(Player player) { + eyeLasers(player,Material.RED_CONCRETE,Material.RED_STAINED_GLASS,5); + return true; + } + + @MaterialInfo(name = "Resin Laser beam", description = "Shoot lasers from the eye on the chestpiece for 5 seconds", cooldownTicks = 20 * 30) + @Override + public boolean resinAbility(Player player) { + eyeLasers(player,Material.ORANGE_CONCRETE_POWDER,Material.ORANGE_STAINED_GLASS,5); + return true; + } +} diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/FlowAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/FlowAbility.java new file mode 100644 index 0000000..814f2fc --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/FlowAbility.java @@ -0,0 +1,254 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.utils.SoundPlayer; +import me.trouper.trimserver.utils.TargetingUtils; +import me.trouper.trimserver.utils.Text; +import org.bukkit.*; +import org.bukkit.entity.*; +import org.bukkit.event.EventHandler; +import org.bukkit.event.entity.EntityTargetEvent; +import org.bukkit.event.player.PlayerToggleSneakEvent; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.Vector; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +@PatternInfo(name = "Wind Rider", description = "Summon a breeze to ride through the sky") +public class FlowAbility extends AbstractAbility { + + private final Map activeRiders = new HashMap<>(); + private final Map activeBreezes = new HashMap<>(); + + public FlowAbility() { + super(TrimPattern.FLOW); + } + + private void spawnRideableBreeze(Player player, int duration, Material material) { + if (activeRiders.containsKey(player.getUniqueId())) { + activeRiders.get(player.getUniqueId()).cancel(); + activeRiders.remove(player.getUniqueId()); + + if (activeBreezes.containsKey(player.getUniqueId())) { + activeBreezes.get(player.getUniqueId()).remove(); + activeBreezes.remove(player.getUniqueId()); + } + } + + World world = player.getWorld(); + Location spawnLoc = player.getLocation().add(0, 1, 0); + + Breeze breeze = (Breeze) world.spawnEntity(spawnLoc, EntityType.BREEZE); + breeze.customName(Text.color(player.getName() + "'s Wind Rider")); + breeze.setCustomNameVisible(true); + breeze.addPassenger(player); + breeze.setAI(true); + breeze.setInvulnerable(true); + breeze.addScoreboardTag("$/TrimServer/ Temp"); + + activeBreezes.put(player.getUniqueId(), breeze); + + world.spawnParticle(Particle.CLOUD, spawnLoc, 50, 1, 1, 1, 0.2); + world.spawnParticle(Particle.BLOCK_CRUMBLE, spawnLoc, 20, 0.5, 0.5, 0.5, 0.1, material.createBlockData()); + SoundPlayer summonSound = new SoundPlayer(spawnLoc, Sound.ENTITY_BREEZE_SHOOT, 1.0f, 0.8f); + summonSound.playWithin(10); + + BukkitTask task = new BukkitRunnable() { + final long endTime = System.currentTimeMillis() + (duration * 1000L); + final AtomicInteger attackCooldown = new AtomicInteger(0); + + @Override + public void run() { + if (System.currentTimeMillis() > endTime || !player.isOnline() || + breeze.isDead() || breeze.getPassengers().isEmpty()) { + endBreezeRide(player, breeze); + cancel(); + return; + } + + Vector playerDirection = player.getLocation().clone().getDirection().multiply(0.5); + breeze.setVelocity(playerDirection); + + breeze.getWorld().spawnParticle(Particle.BLOCK_CRUMBLE, breeze.getLocation(), 10, 0.5, 0.5, 0.5, 0.05, material.createBlockData()); + breeze.getWorld().spawnParticle(Particle.CLOUD, breeze.getLocation(), 3, 0.2, 0.2, 0.2, 0.05); + + if (attackCooldown.getAndDecrement() <= 0) { + attackCooldown.set(20); + try { + attackNearby(breeze, player); + } catch (IllegalArgumentException e) { + if (!e.getMessage().equals("x not finite")) { + throw e; + } + } + } + + TargetingUtils.areaAffect(breeze.getLocation(),1.5,entity -> !entity.equals(player) && !main.man().trustBackend.trusts(player,entity) && !breeze.equals(entity),target->{ + launchEntity(target); + world.spawnParticle(Particle.EXPLOSION, target.getLocation(), 10, 0.5, 0.5, 0.5, 0.1); + }); + } + }.runTaskTimer(main.getPlugin(), 0L, 1L); + + activeRiders.put(player.getUniqueId(), task); + + Bukkit.getScheduler().runTaskLater(main.getPlugin(), () -> { + if (activeRiders.containsKey(player.getUniqueId())) { + endBreezeRide(player, breeze); + } + }, duration * 20L); + } + + private void attackNearby(Breeze breeze, Player owner) { + Location breezeLocation = breeze.getLocation(); + World world = breeze.getWorld(); + + TargetingUtils.areaAffect(breezeLocation,7,entity-> !entity.equals(owner) && !main.man().trustBackend.trusts(owner,entity),(target)->{ + Vector direction = target.getEyeLocation().subtract(breeze.getEyeLocation()).toVector().normalize(); + + BreezeWindCharge windCharge = (BreezeWindCharge) world.spawnEntity(breezeLocation, EntityType.BREEZE_WIND_CHARGE); + windCharge.setShooter(breeze); + windCharge.setVelocity(direction.multiply(0.5)); + + SoundPlayer attackSound = new SoundPlayer(breezeLocation, Sound.ENTITY_BREEZE_SHOOT, 1.0f, 1.2f); + attackSound.playWithin(5); + }); + } + + private void launchEntity(Entity target) { + Vector launchVector = new Vector(0, 2, 0); + target.setVelocity(target.getVelocity().add(launchVector)); + + target.getWorld().spawnParticle( + Particle.CLOUD, + target.getLocation(), + 30, 0.3, 0.3, 0.3, 0.1 + ); + + SoundPlayer launchSound = new SoundPlayer(target.getLocation(), Sound.ENTITY_BREEZE_SHOOT, 1.0f, 0.6f); + launchSound.playWithin(5); + } + + private void endBreezeRide(Player player, Breeze breeze) { + if (activeRiders.containsKey(player.getUniqueId())) { + activeRiders.get(player.getUniqueId()).cancel(); + activeRiders.remove(player.getUniqueId()); + } + + if (activeBreezes.containsKey(player.getUniqueId())) { + activeBreezes.remove(player.getUniqueId()); + } + + Location loc = breeze.getLocation(); + breeze.getWorld().spawnParticle(Particle.CLOUD, loc, 50, 1, 1, 1, 0.2); + SoundPlayer despawnSound = new SoundPlayer(loc, Sound.ENTITY_BREEZE_DEATH, 1.0f, 1.0f); + despawnSound.playWithin(10); + + breeze.eject(); + breeze.remove(); + } + + @EventHandler + public void onPlayerSneak(PlayerToggleSneakEvent event) { + Player player = event.getPlayer(); + + if (event.isSneaking() && activeRiders.containsKey(player.getUniqueId())) { + if (activeBreezes.containsKey(player.getUniqueId())) { + endBreezeRide(player, activeBreezes.get(player.getUniqueId())); + } + } + } + + @EventHandler + public void onBreezeTarget(EntityTargetEvent event) { + if (event.getEntity() instanceof Breeze breeze) { + if (activeBreezes.containsValue(breeze)) { + event.setCancelled(true); + } + } + } + + @MaterialInfo(name = "Amethyst Wind Rider", description = "Summon an amethyst-infused breeze to ride", cooldownTicks = 20 * 60) + @Override + public boolean amethystAbility(Player player) { + spawnRideableBreeze(player, 30, Material.AMETHYST_BLOCK); + return true; + } + + @MaterialInfo(name = "Copper Wind Rider", description = "Summon a copper-infused breeze to ride", cooldownTicks = 20 * 60) + @Override + public boolean copperAbility(Player player) { + spawnRideableBreeze(player, 30, Material.COPPER_BLOCK); + return true; + } + + @MaterialInfo(name = "Diamond Wind Rider", description = "Summon a diamond-infused breeze to ride", cooldownTicks = 20 * 60) + @Override + public boolean diamondAbility(Player player) { + spawnRideableBreeze(player, 30, Material.DIAMOND_BLOCK); + return true; + } + + @MaterialInfo(name = "Emerald Wind Rider", description = "Summon an emerald-infused breeze to ride", cooldownTicks = 20 * 60) + @Override + public boolean emeraldAbility(Player player) { + spawnRideableBreeze(player, 30, Material.EMERALD_BLOCK); + return true; + } + + @MaterialInfo(name = "Gold Wind Rider", description = "Summon a gold-infused breeze to ride", cooldownTicks = 20 * 60) + @Override + public boolean goldAbility(Player player) { + spawnRideableBreeze(player, 30, Material.GOLD_BLOCK); + return true; + } + + @MaterialInfo(name = "Iron Wind Rider", description = "Summon an iron-infused breeze to ride", cooldownTicks = 20 * 60) + @Override + public boolean ironAbility(Player player) { + spawnRideableBreeze(player, 30, Material.IRON_BLOCK); + return true; + } + + @MaterialInfo(name = "Lapis Wind Rider", description = "Summon a lapis-infused breeze to ride", cooldownTicks = 20 * 60) + @Override + public boolean lapisAbility(Player player) { + spawnRideableBreeze(player, 30, Material.LAPIS_BLOCK); + return true; + } + + @MaterialInfo(name = "Netherite Wind Rider", description = "Summon a powerful netherite-infused breeze to ride", cooldownTicks = 20 * 90) + @Override + public boolean netheriteAbility(Player player) { + spawnRideableBreeze(player, 45, Material.NETHERITE_BLOCK); + return true; + } + + @MaterialInfo(name = "Quartz Wind Rider", description = "Summon a quartz-infused breeze to ride", cooldownTicks = 20 * 60) + @Override + public boolean quartzAbility(Player player) { + spawnRideableBreeze(player, 30, Material.QUARTZ_BLOCK); + return true; + } + + @MaterialInfo(name = "Redstone Wind Rider", description = "Summon a redstone-infused breeze to ride", cooldownTicks = 20 * 60) + @Override + public boolean redstoneAbility(Player player) { + spawnRideableBreeze(player, 30, Material.REDSTONE_BLOCK); + return true; + } + + @MaterialInfo(name = "Resin Wind Rider", description = "Summon a resin-infused breeze to ride", cooldownTicks = 20 * 60) + @Override + public boolean resinAbility(Player player) { + spawnRideableBreeze(player, 30, Material.RESIN_BLOCK); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/HostAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/HostAbility.java new file mode 100644 index 0000000..75d9ba1 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/HostAbility.java @@ -0,0 +1,139 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.utils.Text; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; + +import java.util.concurrent.atomic.AtomicInteger; + +@PatternInfo(name = "The Host", description = "Disappear into the shadows like Gatsby.") +public class HostAbility extends AbstractAbility { + + public HostAbility() { + super(TrimPattern.HOST); + } + + public void makeInvisible(Player invis, long seconds) { + invis.addScoreboardTag("$/TrimServer/ Invisible"); + for (Player player : Bukkit.getOnlinePlayers()) { + if (main.man().trustBackend.trusts(invis,player)) continue; + player.hidePlayer(main.getPlugin(),invis); + AtomicInteger timer = new AtomicInteger(); + Bukkit.getScheduler().runTaskTimer(main.getPlugin(),(task)->{ + if (timer.getAndIncrement() >= seconds) { + if (!player.isOnline() || !invis.isOnline()) return; + player.showPlayer(main.getPlugin(),invis); + task.cancel(); + return; + } + invis.sendActionBar(Text.color("&aYou are hidden from other players!")); + },0,20); + + } + } + + @EventHandler + public void onJoin(PlayerJoinEvent e) { + if (e.getPlayer().getScoreboardTags().contains("$/TrimServer/ Invisible")) { + for (Player player : Bukkit.getOnlinePlayers()) { + player.showPlayer(main.getPlugin(),e.getPlayer()); + } + } + } + + @MaterialInfo(name = "Amethyst ", description = "Grants true invisibility for 20 seconds but makes your attacks weaker", cooldownTicks = 20 * 30) + @Override + public boolean amethystAbility(Player player) { + makeInvisible(player,20); + player.addPotionEffect(new PotionEffect(PotionEffectType.WEAKNESS,20*15,0,true,false,true)); + return true; + } + + @MaterialInfo(name = "Copper ", description = "Grants true invisibility for 20 seconds but makes your attacks weaker", cooldownTicks = 20 * 30) + @Override + public boolean copperAbility(Player player) { + makeInvisible(player,20); + player.addPotionEffect(new PotionEffect(PotionEffectType.WEAKNESS,20*15,0,true,false,true)); + return true; + } + + @MaterialInfo(name = "Diamond ", description = "Grants true invisibility for 20 seconds but makes your attacks weaker", cooldownTicks = 20 * 30) + @Override + public boolean diamondAbility(Player player) { + makeInvisible(player,20); + player.addPotionEffect(new PotionEffect(PotionEffectType.WEAKNESS,20*15,0,true,false,true)); + return true; + } + + @MaterialInfo(name = "Emerald ", description = "Grants true invisibility for 20 seconds but makes your attacks weaker", cooldownTicks = 20 * 30) + @Override + public boolean emeraldAbility(Player player) { + makeInvisible(player,20); + player.addPotionEffect(new PotionEffect(PotionEffectType.WEAKNESS,20*15,0,true,false,true)); + return true; + } + + @MaterialInfo(name = "Gold ", description = "Grants true invisibility for 20 seconds but makes your attacks weaker", cooldownTicks = 20 * 30) + @Override + public boolean goldAbility(Player player) { + makeInvisible(player,20); + player.addPotionEffect(new PotionEffect(PotionEffectType.WEAKNESS,20*15,0,true,false,true)); + return true; + } + + @MaterialInfo(name = "Iron ", description = "Grants true invisibility for 20 seconds but makes your attacks weaker", cooldownTicks = 20 * 30) + @Override + public boolean ironAbility(Player player) { + makeInvisible(player,20); + player.addPotionEffect(new PotionEffect(PotionEffectType.WEAKNESS,20*15,0,true,false,true)); + return true; + } + + @MaterialInfo(name = "Lapis ", description = "Grants true invisibility for 20 seconds but makes your attacks weaker", cooldownTicks = 20 * 30) + @Override + public boolean lapisAbility(Player player) { + makeInvisible(player,20); + player.addPotionEffect(new PotionEffect(PotionEffectType.WEAKNESS,20*15,0,true,false,true)); + return true; + } + + @MaterialInfo(name = "Netherite ", description = "Grants true invisibility for 10 seconds and makes your attacks stronger", cooldownTicks = 20 * 15) + @Override + public boolean netheriteAbility(Player player) { + makeInvisible(player,10); + player.addPotionEffect(new PotionEffect(PotionEffectType.STRENGTH,20*15,1,true,false,true)); + return true; + } + + @MaterialInfo(name = "Quartz ", description = "Grants true invisibility for 20 seconds but makes your attacks weaker", cooldownTicks = 20 * 30) + @Override + public boolean quartzAbility(Player player) { + makeInvisible(player,20); + player.addPotionEffect(new PotionEffect(PotionEffectType.WEAKNESS,20*15,0,true,false,true)); + return true; + } + + @MaterialInfo(name = "Redstone ", description = "Grants true invisibility for 20 seconds but makes your attacks weaker", cooldownTicks = 20 * 30) + @Override + public boolean redstoneAbility(Player player) { + makeInvisible(player,20); + player.addPotionEffect(new PotionEffect(PotionEffectType.WEAKNESS,20*15,0,true,false,true)); + return true; + } + + @MaterialInfo(name = "Resin ", description = "Grants true invisibility for 20 seconds but makes your attacks weaker", cooldownTicks = 20 * 30) + @Override + public boolean resinAbility(Player player) { + makeInvisible(player,20); + player.addPotionEffect(new PotionEffect(PotionEffectType.WEAKNESS,20*15,0,true,false,true)); + return true; + } +} diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/RaiserAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/RaiserAbility.java new file mode 100644 index 0000000..4be4b83 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/RaiserAbility.java @@ -0,0 +1,206 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.utils.TargetingUtils; +import me.trouper.trimserver.utils.visual.BlockDisplayRaytracer; +import me.trouper.trimserver.utils.visual.DisplayUtils; +import org.bukkit.*; +import org.bukkit.entity.*; +import org.bukkit.event.EventHandler; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.PlayerKickEvent; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.Vector; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@PatternInfo(name = "Unexpected Levitation", description = "Raiser? I hardly know her!") +public class RaiserAbility extends AbstractAbility { + + public RaiserAbility() { + super(TrimPattern.RAISER); + } + + private final Map levitationTasks = new HashMap<>(); + + public void raiseEntity(LivingEntity target) { + if (target == null) return; + + UUID uuid = target.getUniqueId(); + + if (levitationTasks.containsKey(uuid)) return; + + + Location initialLocation = target.getLocation(); + double initialHealth = target.getHealth(); + double healthThreshold = initialHealth / 2.0; + double targetY = initialLocation.getY() + 4.0; + int frequency = 2; + int duration = 20 * 30; + + // --- Start Up Effects --- + target.getWorld().playSound(target.getLocation(), Sound.ENTITY_BAT_TAKEOFF, 0.8f, 1.0f); + BlockDisplay hook = BlockDisplayRaytracer.trace(Material.IRON_BLOCK,target.getEyeLocation(),target.getLocation().add(0,128,0),0.3,60); + DisplayUtils.wave(target.getLocation(),2,Color.WHITE,1,0.1); + // Give an initial upward push + target.setVelocity(new Vector(0, 0.4, 0)); + + // --- The Main Repeating Task (Holding, Checking, Effects) --- + BukkitTask task = new BukkitRunnable() { + int ticksElapsed = 0; + final int durationTicks = duration / frequency; + + @Override + public void run() { + // Check if player is still valid and online + if (target.isDead()) { + endLevitation(uuid); + return; + } + + Location currentLocation = target.getLocation(); + double currentHealth = target.getHealth(); + + // --- Check Conditions to End --- + // 1. Health threshold reached + if (currentHealth <= healthThreshold) { + endLevitation(uuid); + return; + } + + // 2. Time limit reached + if (ticksElapsed >= durationTicks) { + endLevitation(uuid); + return; + } + + // --- Hold Player at Target Height --- + if (currentLocation.getY() >= targetY) { + Vector currentVelocity = target.getVelocity(); + target.setVelocity(new Vector(currentVelocity.getX(), 0, currentVelocity.getZ())); + if (hook != null && !hook.isDead()) BlockDisplayRaytracer.transform(hook,target.getEyeLocation(),target.getLocation().add(0,128,0),0.3); + } else { + target.setVelocity(new Vector(0, 0.5, 0)); + } + + + target.getWorld().spawnParticle(Particle.CLOUD, currentLocation.add(0, 1.5, 0), 1, 0.3, 0.8, 0.3, 0.02); + target.getWorld().spawnParticle(Particle.WITCH, currentLocation.add(0, 1.0, 0), 10, 0.5, 0.5, 0.5, 0.01); + + if (ticksElapsed % 20 == 0) { + target.getWorld().playSound(target.getLocation(), Sound.ENTITY_BAT_LOOP, 0.3f, 1.2f); + } + + if (ticksElapsed % 7 == 0) { + for (int i = 0; i < 5; i++) { + Bat bat = (Bat) target.getWorld().spawnEntity(target.getLocation().add(0,target.getHeight()/2,0), EntityType.BAT); + Bukkit.getScheduler().runTaskLater(main.getPlugin(),task->{ + if (bat != null && !bat.isDead()) bat.remove(); + },15); + } + } + + ticksElapsed++; + } + }.runTaskTimer(main.getPlugin(), 0, frequency); + + levitationTasks.putIfAbsent(target.getUniqueId(),task); + } + + private void endLevitation(UUID playerUUID) { + BukkitTask task = levitationTasks.remove(playerUUID); + if (task != null && !task.isCancelled()) { + Player player = Bukkit.getPlayer(playerUUID); + if (player != null && player.isOnline()) { + player.getWorld().playSound(player.getLocation(), Sound.ENTITY_BAT_DEATH, 0.8f, 0.8f); + player.getWorld().spawnParticle(Particle.CLOUD, player.getLocation().add(0, 1, 0), 30, 0.8, 0.8, 0.8, 0.05); + } + } + } + + @EventHandler + public void onKick(PlayerKickEvent e) { + if (levitationTasks.containsKey(e.getPlayer().getUniqueId())) { + e.setCancelled(true); + } + } + + @EventHandler + public void onDeath(PlayerDeathEvent e) { + if (levitationTasks.containsKey(e.getPlayer().getUniqueId())) { + endLevitation(e.getPlayer().getUniqueId()); + } + } + + @MaterialInfo(name = "Amethyst ", description = "Pins your enemy 4 blocks into the air for 30 seconds, or until they loose half their current health", cooldownTicks = 20 * 45) + @Override + public boolean amethystAbility(Player player) { + return TargetingUtils.areaAffect(player.getLocation(),15,target -> !main.man().trustBackend.trusts(player,target), this::raiseEntity); + } + + @MaterialInfo(name = "Copper ", description = "Pins your enemy 4 blocks into the air for 30 seconds, or until they loose half their current health", cooldownTicks = 20 * 45) + @Override + public boolean copperAbility(Player player) { + return TargetingUtils.areaAffect(player.getLocation(),15,target -> !main.man().trustBackend.trusts(player,target), this::raiseEntity); + } + + @MaterialInfo(name = "Diamond ", description = "Pins your enemy 4 blocks into the air for 30 seconds, or until they loose half their current health", cooldownTicks = 20 * 45) + @Override + public boolean diamondAbility(Player player) { + return TargetingUtils.areaAffect(player.getLocation(),15,target -> !main.man().trustBackend.trusts(player,target), this::raiseEntity); + } + + @MaterialInfo(name = "Emerald ", description = "Pins your enemy 4 blocks into the air for 30 seconds, or until they loose half their current health", cooldownTicks = 20 * 45) + @Override + public boolean emeraldAbility(Player player) { + return TargetingUtils.areaAffect(player.getLocation(),15,target -> !main.man().trustBackend.trusts(player,target), this::raiseEntity); + } + + @MaterialInfo(name = "Gold ", description = "Pins your enemy 4 blocks into the air for 30 seconds, or until they loose half their current health", cooldownTicks = 20 * 45) + @Override + public boolean goldAbility(Player player) { + return TargetingUtils.areaAffect(player.getLocation(),15,target -> !main.man().trustBackend.trusts(player,target), this::raiseEntity); + } + + @MaterialInfo(name = "Iron ", description = "Pins your enemy 4 blocks into the air for 30 seconds, or until they loose half their current health", cooldownTicks = 20 * 45) + @Override + public boolean ironAbility(Player player) { + return TargetingUtils.areaAffect(player.getLocation(),15,target -> !main.man().trustBackend.trusts(player,target), this::raiseEntity); + } + + @MaterialInfo(name = "Lapis ", description = "Pins your enemy 4 blocks into the air for 30 seconds, or until they loose half their current health", cooldownTicks = 20 * 45) + @Override + public boolean lapisAbility(Player player) { + return TargetingUtils.areaAffect(player.getLocation(),15,target -> !main.man().trustBackend.trusts(player,target), this::raiseEntity); + } + + @MaterialInfo(name = "Netherite ", description = "Pins your enemy 4 blocks into the air for 30 seconds, or until they loose half their current health", cooldownTicks = 20 * 30) + @Override + public boolean netheriteAbility(Player player) { + return TargetingUtils.areaAffect(player.getLocation(),15,target -> !main.man().trustBackend.trusts(player,target), this::raiseEntity); + } + + @MaterialInfo(name = "Quartz ", description = "Pins your enemy 4 blocks into the air for 30 seconds, or until they loose half their current health", cooldownTicks = 20 * 45) + @Override + public boolean quartzAbility(Player player) { + return TargetingUtils.areaAffect(player.getLocation(),15,target -> !main.man().trustBackend.trusts(player,target), this::raiseEntity); + } + + @MaterialInfo(name = "Redstone ", description = "Pins your enemy 4 blocks into the air for 30 seconds, or until they loose half their current health", cooldownTicks = 20 * 45) + @Override + public boolean redstoneAbility(Player player) { + return TargetingUtils.areaAffect(player.getLocation(),15,target -> !main.man().trustBackend.trusts(player,target), this::raiseEntity); + } + + @MaterialInfo(name = "Resin ", description = "Pins your enemy 4 blocks into the air for 30 seconds, or until they loose half their current health", cooldownTicks = 20 * 45) + @Override + public boolean resinAbility(Player player) { + return TargetingUtils.areaAffect(player.getLocation(),15,target -> !main.man().trustBackend.trusts(player,target), this::raiseEntity); + } +} diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/RibAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/RibAbility.java new file mode 100644 index 0000000..e07f611 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/RibAbility.java @@ -0,0 +1,277 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.utils.SoundPlayer; + +import me.trouper.trimserver.utils.TargetingUtils; +import me.trouper.trimserver.utils.visual.DisplayUtils; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.*; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.util.BoundingBox; +import org.bukkit.util.Transformation; +import org.bukkit.util.Vector; +import org.joml.Vector3f; +import org.joml.Quaternionf; + + +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +@PatternInfo(name = "Nether Eruption", description = "Summons sharp spikes from the ground that damage enemies accompanied by a blast of toxic ash.") +public class RibAbility extends AbstractAbility { + + private final Random random = new Random(); + + public RibAbility() { + super(TrimPattern.RIB); + } + + private void executeResinEruption(Player caster, Material activatingTrimMaterial, int spikeCount, double areaRadius, int activeDurationTicks, Material groundParticleMaterial) { + World world = caster.getWorld(); + if (world == null) return; + + Location playerCenterLoc = caster.getLocation(); + + caster.addPotionEffect(new PotionEffect(PotionEffectType.SPEED,15*20,2,true)); + caster.addPotionEffect(new PotionEffect(PotionEffectType.STRENGTH,10*20,1,true)); + caster.addPotionEffect(new PotionEffect(PotionEffectType.REGENERATION,5*20,2,true)); + + DisplayUtils.sphereWave(caster.getEyeLocation(),10,0.5,2,2,(point)->{ + point.getWorld().spawnParticle(Particle.FLAME,point,1,0.1,0.1,0.1,0.01); + point.getWorld().spawnParticle(Particle.SMOKE,point,1,0.1,0.1,0.1,0.1); + point.getWorld().spawnParticle(Particle.CAMPFIRE_COSY_SMOKE,point,1,0.1,0.1,0.1,0.1); + TargetingUtils.areaAffect(point,1, liv->!liv.equals(caster) && !liv.isDead() && !main.man().trustBackend.trusts(caster,liv), target->{ + target.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS,20*20,1,true,false,false)); + target.setFireTicks(20*20); + }); + }); + + new SoundPlayer(playerCenterLoc, Sound.ENTITY_WARDEN_DIG, 1.5f, 0.7f).playWithin(15); + new SoundPlayer(playerCenterLoc, Sound.BLOCK_NETHERRACK_PLACE, 1.5f, 0.8f).playWithin(15); + world.spawnParticle(Particle.LAVA, playerCenterLoc, (int) (30 * areaRadius), areaRadius / 2.0, 0.3, areaRadius / 2.0, 0.05); + world.spawnParticle(Particle.ASH, playerCenterLoc, (int) (50 * areaRadius), areaRadius, 0.5, areaRadius, 0.1); + world.spawnParticle(Particle.LARGE_SMOKE, playerCenterLoc, (int) (20 * areaRadius), areaRadius / 1.5, 0.5, areaRadius / 1.5, 0.05); + + final double spikeVisualHeight = 3.5; + final double spikeVisualThickness = 0.5; + final int spikeRiseAnimationTicks = 10; + final List activeSpikes = new ArrayList<>(); + + BlockData spikeBlockData = activatingTrimMaterial.createBlockData(); + BlockData groundBlockData = groundParticleMaterial.createBlockData(); + + for (int i = 0; i < spikeCount; i++) { + double angle = random.nextDouble() * 2 * Math.PI; + double currentRandomRadius = random.nextDouble() * areaRadius; + double dx = Math.cos(angle) * currentRandomRadius; + double dz = Math.sin(angle) * currentRandomRadius; + + Location spikeBaseCenter = playerCenterLoc.clone().add(dx, 0, dz); + + // Find the ground for the spike + Block highestBlock = world.getHighestBlockAt(spikeBaseCenter.getBlockX(), spikeBaseCenter.getBlockZ()); + Location groundSurfaceLoc = highestBlock.getLocation().add(0.5, 1.0, 0.5); + + int attempts = 0; + while (attempts < 5 && (!groundSurfaceLoc.clone().subtract(0,1,0).getBlock().getType().isSolid() || groundSurfaceLoc.getBlock().getType().isSolid())) { + groundSurfaceLoc.subtract(0,1,0); + attempts++; + if (groundSurfaceLoc.getY() < world.getMinHeight()) { + groundSurfaceLoc.setY(world.getMinHeight() +1); + break; + } + } + if (!groundSurfaceLoc.clone().subtract(0,1,0).getBlock().getType().isSolid()) { + Location tempLoc = new Location(world, spikeBaseCenter.getX(), caster.getLocation().getY(), spikeBaseCenter.getZ()); + groundSurfaceLoc = world.getHighestBlockAt(tempLoc).getLocation().add(0.5,1.0,0.5); + if (!groundSurfaceLoc.clone().subtract(0,1,0).getBlock().getType().isSolid()){ + continue; + } + } + + + final Location finalSpikeBase = groundSurfaceLoc.clone(); + final Location spikeTipLoc = finalSpikeBase.clone().add(0, spikeVisualHeight, 0); + + BlockDisplay spikeDisplay = world.spawn(finalSpikeBase, BlockDisplay.class, bd -> { + bd.setBlock(spikeBlockData); + bd.setBrightness(new Display.Brightness(10, 10)); + bd.setInterpolationDelay(0); + bd.setInterpolationDuration(spikeRiseAnimationTicks); + bd.addScoreboardTag("$/TrimServer/ Temp"); + + Transformation transform = bd.getTransformation(); + transform.getScale().set(new Vector3f((float) spikeVisualThickness, 0.01f, (float) spikeVisualThickness)); + + transform.getTranslation().set( + -(float) spikeVisualThickness / 2f, + 0f, + -(float) spikeVisualThickness / 2f + ); + bd.setTransformation(transform); + }); + activeSpikes.add(spikeDisplay); + + long delay = (long)(i * (random.nextDouble() * 1.5 + 0.5)); + + new BukkitRunnable() { + int ticksElapsed = 0; + boolean damageDealt = false; + + @Override + public void run() { + if (ticksElapsed == 0) { + Transformation targetTransform = spikeDisplay.getTransformation(); + targetTransform.getScale().set(new Vector3f((float) spikeVisualThickness, (float) spikeVisualHeight, (float) spikeVisualThickness)); + spikeDisplay.setTransformation(targetTransform); + + new SoundPlayer(finalSpikeBase, Sound.BLOCK_NETHERRACK_BREAK, 1.2f, 0.6f + random.nextFloat() * 0.4f).playWithin(15); + new SoundPlayer(finalSpikeBase, Sound.ENTITY_BLAZE_SHOOT, 0.8f, 1.5f + random.nextFloat() * 0.5f).playWithin(15); + world.spawnParticle(Particle.BLOCK_CRUMBLE, finalSpikeBase.clone().add(0,0.1,0), 30, 0.4, 0.2, 0.4, groundBlockData); + world.spawnParticle(Particle.FLAME, finalSpikeBase.clone().add(0,0.2,0), 5, 0.3,0.1,0.3, 0.01); + } + + if (ticksElapsed >= spikeRiseAnimationTicks / 2 && ticksElapsed <= spikeRiseAnimationTicks + 4 && !damageDealt) { + TargetingUtils.areaAffect(finalSpikeBase,1,target -> !target.equals(caster) && !main.man().trustBackend.trusts(caster,target),target -> { + damageDealt = true; + + target.damage(7.0, caster); + Vector knockDir = new Vector(random.nextGaussian() * 0.15, 0.9 + random.nextDouble()*0.2, random.nextGaussian() * 0.15); + target.setVelocity(target.getVelocity().add(knockDir)); + new SoundPlayer(target.getLocation(), Sound.ENTITY_PLAYER_HURT_ON_FIRE, 1.0f, 1.0f).playWithin(15); + world.spawnParticle(Particle.LAVA, target.getEyeLocation(), 8, 0.2, 0.2, 0.2, 0); + world.spawnParticle(Particle.ASH, target.getLocation(), 20, 0.5,0.5,0.5,0); + }); + } + + if (ticksElapsed > spikeRiseAnimationTicks && ticksElapsed < activeDurationTicks - (20)) { + if (random.nextInt(4) == 0) { + world.spawnParticle(Particle.FLAME, spikeTipLoc.clone().add(random.nextGaussian() * 0.15, 0, random.nextGaussian() * 0.15), 1, 0, 0, 0, 0.005); + world.spawnParticle(Particle.CRIMSON_SPORE, spikeTipLoc.clone().add(random.nextGaussian() * 0.2, random.nextDouble()*0.3, random.nextGaussian() * 0.2),1,0,0,0,0); + } + } + + if (ticksElapsed >= activeDurationTicks) { + Transformation currentTransform = spikeDisplay.getTransformation(); + currentTransform.getScale().set(new Vector3f((float)spikeVisualThickness, 0.01f, (float)spikeVisualThickness)); + spikeDisplay.setInterpolationDuration(spikeRiseAnimationTicks); + spikeDisplay.setTransformation(currentTransform); + new SoundPlayer(finalSpikeBase, Sound.BLOCK_BASALT_BREAK, 1.0f, 0.7f).playWithin(15); + + new BukkitRunnable() { + @Override + public void run() { + if (!spikeDisplay.isDead()) { + spikeDisplay.remove(); + world.spawnParticle(Particle.BLOCK_CRUMBLE, finalSpikeBase, 20, 0.3,0.1,0.3, spikeBlockData); + } + } + }.runTaskLater(main.getPlugin(), spikeRiseAnimationTicks +1); + this.cancel(); + } + ticksElapsed++; + } + }.runTaskTimer(main.getPlugin(), delay, 1L); + } + + // Failsafe cleanup for all created displays + new BukkitRunnable() { + @Override + public void run() { + for (BlockDisplay bd : activeSpikes) { + if (bd != null && !bd.isDead()) { + bd.remove(); + } + } + activeSpikes.clear(); + } + }.runTaskLater(main.getPlugin(), activeDurationTicks + spikeRiseAnimationTicks + 40L); + } + + @MaterialInfo(name = "Eruption (Amethyst)", description = "Erupts spikes of amethyst", cooldownTicks = 20 * 30) + @Override + public boolean amethystAbility(Player player) { + executeResinEruption(player, Material.AMETHYST_BLOCK, 10, 5.5, 20 * 5, Material.SMOOTH_BASALT); + return true; + } + + @MaterialInfo(name = "Eruption (Copper)", description = "Erupts spikes of oxidized copper", cooldownTicks = 20 * 30) + @Override + public boolean copperAbility(Player player) { + executeResinEruption(player, Material.RAW_COPPER, 10, 5.0, 20 * 5, Material.TUFF); + return true; + } + + @MaterialInfo(name = "Eruption (Diamond)", description = "Erupts spikes of diamond", cooldownTicks = 20 * 30) + @Override + public boolean diamondAbility(Player player) { + executeResinEruption(player, Material.DEEPSLATE_DIAMOND_ORE, 12, 6.0, 20 * 6, Material.DEEPSLATE); + return true; + } + + @MaterialInfo(name = "Eruption (Emerald)", description = "Erupts spikes of emerald", cooldownTicks = 20 * 30) + @Override + public boolean emeraldAbility(Player player) { + executeResinEruption(player, Material.DEEPSLATE_EMERALD_ORE, 12, 6.0, 20 * 6, Material.MOSS_BLOCK); + return true; + } + + @MaterialInfo(name = "Eruption (Gold)", description = "Erupts spikes of gold", cooldownTicks = 20 * 30) + @Override + public boolean goldAbility(Player player) { + executeResinEruption(player, Material.DEEPSLATE_GOLD_ORE, 10, 5.0, 20 * 5, Material.NETHER_GOLD_ORE); + return true; + } + + @MaterialInfo(name = "Eruption (Iron)", description = "Erupts spikes of iron", cooldownTicks = 20 * 30) + @Override + public boolean ironAbility(Player player) { + executeResinEruption(player, Material.RAW_IRON_BLOCK, 11, 5.5, 20 * 5, Material.RAW_IRON_BLOCK); + return true; + } + + @MaterialInfo(name = "Eruption (Lapis)", description = "Erupts spikes of lapis", cooldownTicks = 20 * 30) + @Override + public boolean lapisAbility(Player player) { + executeResinEruption(player, Material.LAPIS_BLOCK, 10, 5.0, 20 * 5, Material.CLAY); + return true; + } + + @MaterialInfo(name = "Eruption (Netherite)", description = "Erupts deadly blackstone spikes", cooldownTicks = 20 * 20) + @Override + public boolean netheriteAbility(Player player) { + executeResinEruption(player, Material.BLACKSTONE, 20, 10.0, 20 * 8, Material.BLACKSTONE); + return true; + } + + @MaterialInfo(name = "Eruption (Quartz)", description = "Erupts spikes of quartz", cooldownTicks = 20 * 30) + @Override + public boolean quartzAbility(Player player) { + executeResinEruption(player, Material.QUARTZ_BLOCK, 11, 5.5, 20 * 5, Material.NETHER_QUARTZ_ORE); + return true; + } + + @MaterialInfo(name = "Eruption (Redstone)", description = "Erupts energized redstone spikes", cooldownTicks = 20 * 30) + @Override + public boolean redstoneAbility(Player player) { + executeResinEruption(player, Material.REDSTONE_BLOCK, 11, 5.5, 20 * 5, Material.REDSTONE_ORE); + return true; + } + + @MaterialInfo(name = "Eruption (Resin)", description = "Erupts spikes of hardened resin from the ground", cooldownTicks = 20 * 30) + @Override + public boolean resinAbility(Player player) { + executeResinEruption(player, Material.RESIN_BLOCK, 12, 6.0, 20 * 6, Material.NETHERRACK); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/SentryAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/SentryAbility.java new file mode 100644 index 0000000..2af4d86 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/SentryAbility.java @@ -0,0 +1,282 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.utils.SoundPlayer; +import me.trouper.trimserver.utils.TargetingUtils; +import me.trouper.trimserver.utils.Text; +import me.trouper.trimserver.utils.visual.BlockDisplayRaytracer; +import me.trouper.trimserver.utils.visual.CustomDisplayRaytracer; +import org.bukkit.*; +import org.bukkit.block.data.BlockData; +import org.bukkit.damage.DamageSource; +import org.bukkit.damage.DamageType; +import org.bukkit.entity.*; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.util.Transformation; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +@PatternInfo(name = "Build Sentry", description = "\"Meet the Engineer.\" Includes Variants.") +public class SentryAbility extends AbstractAbility { + + public SentryAbility() { + super(TrimPattern.SENTRY); + } + + public static void spawnSentry(Location loc, Player owner, Material legsMat, Material turretMat, int ammo, long secondsAlive, long cooldownTicks) { + World w = loc.getWorld(); + loc = w.getHighestBlockAt((int) loc.x(), (int) loc.z()).getLocation().add(0,1,0); + Location spawn = loc.clone().subtract(0,2,0); + List turretParts = new ArrayList<>(); + + // 1) Spawn the rotating turret (dispenser model) + BlockDisplay turret = w.spawn(spawn.clone().add(0.5, 1.5, 0.5), BlockDisplay.class, display -> { + display.setBlock(turretMat.createBlockData()); + display.setBrightness(new Display.Brightness(15, 15)); + display.setInterpolationDelay(0); + display.setInterpolationDuration(2); + display.addScoreboardTag("$/TrimServer/ Temp"); + + // Get the current transformation + Transformation transformation = display.getTransformation(); + // Set the translation to center the pivot point + transformation.getTranslation().set(-0.5f, -0.5f, -0.5f); + // Apply the modified transformation back to the display + display.setTransformation(transformation); + }); + + turretParts.add(turret); + + Bat dummy = w.spawn(turret.getLocation(),Bat.class,bat->{ + bat.setInvisible(true); + bat.setInvulnerable(true); + bat.customName(Text.color("%s's Sentry\n".formatted(owner.getName()))); + bat.setAI(false); + bat.addScoreboardTag("$/TrimServer/ Temp"); + }); + + turretParts.add(dummy); + + // 2) Legs: four converging from a 1×1 square around base up to head-pole + double legHeight = 1.5; + double halfSize = 0.6; // distance from center + List stand = new ArrayList<>(); + for (double dx : new double[]{ -halfSize, +halfSize }) { + for (double dz : new double[]{ -halfSize, +halfSize }) { + Location start = spawn.clone().add(dx + 0.5, 0.1, dz + 0.5); + Location end = spawn.clone().add(0.5, legHeight, 0.5); + stand.add(BlockDisplayRaytracer.trace( + legsMat, + start, + end.toVector().subtract(start.toVector()), + 0.1, // leg thickness + start.distance(end), + 20 * secondsAlive + 1 + )); + } + } + + turretParts.addAll(stand); + + // 3) Ammo display above head + TextDisplay meter = w.spawn(spawn.clone().add(0.5, 2.5, 0.5), TextDisplay.class, t -> { + t.setBillboard(Display.Billboard.CENTER); + t.setRotation(0, 90); + t.setGravity(false); + t.setSeeThrough(true); + }); + + turretParts.add(meter); + + final Location finalLoc = loc; + final int maxAmmo = ammo; + + BukkitRunnable turretRuntime = new BukkitRunnable() { + int chamber = maxAmmo; + + @Override + public void run() { + if (chamber <= 0 || turret.isDead() || meter.isDead()) { + cancel(); + return; + } + + String bar = Text.generateProgressBar(10, maxAmmo, chamber); + meter.text(Text.color("%s's Sentry\n".formatted(owner.getName()) + "Ammo " + bar)); + + Optional target = TargetingUtils.getClosestPlayer(finalLoc,15,p -> !p.isDead() && !p.equals(owner) && !main.man().trustBackend.trusts(owner,p)); + + if (target.isPresent()) { + Player tracked = target.get(); + Vector toEye = tracked.getLocation() + .add(0, tracked.getEyeHeight(), 0) + .toVector() + .subtract(turret.getLocation().toVector()); + Vector dir = toEye.clone().normalize(); + + float yaw = (float)(Math.toDegrees(Math.atan2(dir.getZ(), dir.getX())) - 90 + 180); + float pitch = (float)(Math.toDegrees(Math.asin(dir.getY()))); + turret.setRotation(yaw, pitch); + + + CustomDisplayRaytracer.trace(turret.getLocation(),dir,60,0.5,point -> { + List hits = new ArrayList<>(w.getNearbyEntities(point.getLoc(), 0.5,0.5,0.5, entity -> { + return entity instanceof LivingEntity living && !(living instanceof Bat) && !living.isDead() && living != owner; + })); + hits.forEach(t -> { + if (t instanceof LivingEntity liv) { + BlockData blockData = Material.RED_WOOL.createBlockData(); + SoundPlayer hitSound = new SoundPlayer(t.getLocation(), Sound.ITEM_DYE_USE,3,2); + int tick = liv.getNoDamageTicks(); + int maxTick = liv.getMaximumNoDamageTicks(); + liv.setNoDamageTicks(0); + liv.setMaximumNoDamageTicks(0); + liv.damage(1, DamageSource.builder(DamageType.ARROW).withDirectEntity(dummy).withDamageLocation(finalLoc).build()); + liv.setNoDamageTicks(tick); + liv.setMaximumNoDamageTicks(maxTick); + hitSound.playWithin(20); + w.spawnParticle(Particle.BLOCK_CRUMBLE,t.getLocation().add(0,1,0),30,0.25F,1,0.25F,blockData); + } + }); + w.spawnParticle(Particle.CRIT, point.getLoc(), 1, 0,0,0, 0); + w.spawnParticle(Particle.SMOKE, point.getLoc(), 1, 0,0,0, 0.1F); + return !hits.isEmpty() || !point.getBlock().isPassable(); + }); + chamber--; + } + } + + @Override + public synchronized void cancel() throws IllegalStateException { + super.cancel(); + + Location particleLoc = finalLoc.clone().add(0.5,0.5,0.5); + AtomicInteger stepsTaken = new AtomicInteger(0); + Bukkit.getScheduler().runTaskTimer(main.getPlugin(),task->{ + if (stepsTaken.getAndIncrement() >= 4) { + turretParts.forEach(Entity::remove); + task.cancel(); + return; + } + + w.spawnParticle(Particle.BLOCK_CRUMBLE, particleLoc, 20, 0.5, 0.2, 0.2, 0.05, legsMat.createBlockData()); + SoundPlayer build = new SoundPlayer(finalLoc,Sound.BLOCK_PISTON_CONTRACT,1,2); + build.playWithin(10); + + + turretParts.forEach(part->{ + part.teleport(part.getLocation().clone().add(0,-0.5,0)); + }); + },0,10); + } + }; + + AtomicInteger stepsTaken = new AtomicInteger(0); + Location particleLoc = finalLoc.clone().add(0.5,0.5,0.5); + Bukkit.getScheduler().runTaskTimer(main.getPlugin(),task->{ + if (stepsTaken.getAndIncrement() >= 4) { + turretRuntime.runTaskTimer(main.getPlugin(), 0L, cooldownTicks); + task.cancel(); + return; + } + + w.spawnParticle(Particle.BLOCK_CRUMBLE, particleLoc, 20, 0.5, 0.2, 0.5, 0.05, legsMat.createBlockData()); + SoundPlayer build = new SoundPlayer(finalLoc,Sound.BLOCK_PISTON_EXTEND,1,2); + build.playWithin(10); + + turretParts.forEach(part->{ + part.teleport(part.getLocation().clone().add(0,0.5,0)); + }); + },0,10); + + + Bukkit.getScheduler().runTaskLater(main.getPlugin(),()->{ + if (turretRuntime == null || turretRuntime.isCancelled()) return; + turretRuntime.cancel(); + },20 * secondsAlive); + } + + @MaterialInfo(name = "Amethyst ", description = "Spawns a sentry which shoots the nearest player", cooldownTicks = 20 * 10) + @Override + public boolean amethystAbility(Player player) { + spawnSentry(player.getLocation(),player,Material.AMETHYST_BLOCK,Material.DISPENSER,50,60,2); + return true; + } + + @MaterialInfo(name = "Copper ", description = "Spawns a sentry which shoots the nearest player", cooldownTicks = 20 * 10) + @Override + public boolean copperAbility(Player player) { + spawnSentry(player.getLocation(),player,Material.COPPER_BLOCK,Material.DISPENSER,50,60,2); + return true; + } + + @MaterialInfo(name = "Diamond ", description = "Spawns a sentry which shoots the nearest player", cooldownTicks = 20 * 10) + @Override + public boolean diamondAbility(Player player) { + spawnSentry(player.getLocation(),player,Material.DIAMOND_BLOCK,Material.DISPENSER,50,60,2); + return true; + } + + @MaterialInfo(name = "Emerald ", description = "Spawns a sentry which shoots the nearest player", cooldownTicks = 20 * 10) + @Override + public boolean emeraldAbility(Player player) { + spawnSentry(player.getLocation(),player,Material.EMERALD_BLOCK,Material.DISPENSER,50,60,2); + return true; + } + + @MaterialInfo(name = "Gold ", description = "Spawns a sentry which shoots the nearest player", cooldownTicks = 20 * 10) + @Override + public boolean goldAbility(Player player) { + spawnSentry(player.getLocation(),player,Material.GOLD_BLOCK,Material.DISPENSER,50,60,2); + return true; + } + + @MaterialInfo(name = "Iron ", description = "Spawns a sentry which shoots the nearest player", cooldownTicks = 20 * 10) + @Override + public boolean ironAbility(Player player) { + spawnSentry(player.getLocation(),player,Material.IRON_BLOCK,Material.DISPENSER,50,60,2); + return true; + } + + @MaterialInfo(name = "Lapis ", description = "Spawns a sentry which shoots the nearest player", cooldownTicks = 20 * 10) + @Override + public boolean lapisAbility(Player player) { + spawnSentry(player.getLocation(),player,Material.LAPIS_BLOCK,Material.DISPENSER,50,60,2); + return true; + } + + @MaterialInfo(name = "Netherite Sentry", description = "Spawns a supercharged sentry which absolutely shreds the nearest player", cooldownTicks = 20 * 15) + @Override + public boolean netheriteAbility(Player player) { + spawnSentry(player.getLocation(),player,Material.NETHERITE_BLOCK,Material.CRAFTER,100,60,1); + return true; + } + + @MaterialInfo(name = "Quartz ", description = "Spawns a sentry which shoots the nearest player", cooldownTicks = 20 * 10) + @Override + public boolean quartzAbility(Player player) { + spawnSentry(player.getLocation(),player,Material.QUARTZ_BLOCK,Material.DISPENSER,50,60,2); + return true; + } + + @MaterialInfo(name = "Redstone ", description = "Spawns a sentry which shoots the nearest player", cooldownTicks = 20 * 10) + @Override + public boolean redstoneAbility(Player player) { + spawnSentry(player.getLocation(),player,Material.REDSTONE_BLOCK,Material.DISPENSER,50,60,2); + return true; + } + + @MaterialInfo(name = "Resin ", description = "Spawns a sentry which shoots the nearest player", cooldownTicks = 20 * 10) + @Override + public boolean resinAbility(Player player) { + spawnSentry(player.getLocation(),player,Material.RESIN_BLOCK,Material.DISPENSER,50,60,2); + return true; + } +} diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/ShaperAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/ShaperAbility.java new file mode 100644 index 0000000..cd1d5af --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/ShaperAbility.java @@ -0,0 +1,324 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.utils.SoundPlayer; +import me.trouper.trimserver.utils.TargetingUtils; +import me.trouper.trimserver.utils.Text; +import org.bukkit.*; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeModifier; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.BlockDisplay; +import org.bukkit.entity.Display; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.Transformation; +import org.bukkit.util.Vector; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +@PatternInfo(name = "Terra Shell", description = "Temporarily encase yourself in a protective shell of stone, then unleash a shrapnel burst.") +public class ShaperAbility extends AbstractAbility implements Listener { + + public final Map activeShellTasks = new ConcurrentHashMap<>(); + private final Map> activeShellDisplays = new ConcurrentHashMap<>(); + private final Map knockbackModifiers = new ConcurrentHashMap<>(); + private final Map originalFireTicks = new ConcurrentHashMap<>(); + + private static final int BASE_DURATION_TICKS = 8 * 20; + private static final double BASE_SHATTER_DAMAGE = 6.0; + private static final double BASE_SHATTER_RADIUS = 4.0; + private static final int RESISTANCE_AMPLIFIER = 2; + private static final int SLOWNESS_AMPLIFIER = 1; + + // Cooldowns + private static final int DEFAULT_COOLDOWN = 20 * 60; + private static final int NETHERITE_COOLDOWN = 20 * 90; + private static final int RESIN_COOLDOWN = 20 * 40; + + // Netherite Modifiers + private static final double NETHERITE_DURATION_MULTIPLIER = 1.3; + private static final double NETHERITE_SHATTER_DAMAGE_MULTIPLIER = 1.5; + private static final int NETHERITE_RESISTANCE_AMPLIFIER_BONUS = 1; // Total Resistance IV + private static final double NETHERITE_SHATTER_RADIUS_MULTIPLIER = 1.2; + + public ShaperAbility() { + super(TrimPattern.SHAPER); + } + + private void activateTerraShell(Player player, Material blockMaterialForShell) { + UUID playerUUID = player.getUniqueId(); + + if (activeShellTasks.containsKey(playerUUID)) return; + + World world = player.getWorld(); + List shellParts = new ArrayList<>(); + activeShellDisplays.put(playerUUID, shellParts); + + new SoundPlayer(player.getLocation(), Sound.BLOCK_STONE_PLACE, 1.1f, 0.6f).playWithin(15); + new SoundPlayer(player.getLocation(), Sound.BLOCK_ROOTED_DIRT_STEP, 1.2f, 0.7f).playWithin(15); + + BlockData shellBlockData = blockMaterialForShell.createBlockData(); + float shellPartScale = 0.65f; + int numShellParts = 12; + float orbitRadius = 1.2f; + + for (int i = 0; i < numShellParts; i++) { + double angle = ((double) i / numShellParts) * 2 * Math.PI; + double yInitialOffset = (Math.random() * 0.8) + 0.5; // Start around player's mid-section + Vector offset = new Vector(Math.cos(angle) * orbitRadius, yInitialOffset, Math.sin(angle) * orbitRadius); + Location partLoc = player.getLocation().add(offset); + + BlockDisplay bd = world.spawn(partLoc, BlockDisplay.class, display -> { + display.setBlock(shellBlockData); + display.setTransformation(new Transformation( + new Vector3f(-shellPartScale / 2, -shellPartScale / 2, -shellPartScale / 2), + new Quaternionf().rotateY((float) (Math.random() * Math.PI * 2)).rotateX((float) (Math.random() * Math.PI * 2)), + new Vector3f(shellPartScale, shellPartScale, shellPartScale), + new Quaternionf() + )); + display.setInterpolationDelay(-1); // Start interpolating immediately + display.setInterpolationDuration(2); // Smooth movement over 2 ticks + display.setTeleportDuration(2); + display.setBrightness(new Display.Brightness(world.getBlockAt(partLoc).getLightFromSky(), world.getBlockAt(partLoc).getLightFromBlocks())); + display.setGravity(false); + display.addScoreboardTag("$/TrimServer/ Temp"); + }); + shellParts.add(bd); + } + + int duration = BASE_DURATION_TICKS; + double shatterDamage = BASE_SHATTER_DAMAGE; + double shatterRadius = BASE_SHATTER_RADIUS; + int resistanceAmplifier = RESISTANCE_AMPLIFIER; + int slownessAmplifier = SLOWNESS_AMPLIFIER; + + if (blockMaterialForShell == Material.NETHERITE_BLOCK) { + duration = (int) (duration * NETHERITE_DURATION_MULTIPLIER); + shatterDamage *= NETHERITE_SHATTER_DAMAGE_MULTIPLIER; + shatterRadius *= NETHERITE_SHATTER_RADIUS_MULTIPLIER; + resistanceAmplifier += NETHERITE_RESISTANCE_AMPLIFIER_BONUS; + } else if (blockMaterialForShell == Material.RESIN_BLOCK) { + slownessAmplifier = 0; + if (slownessAmplifier < 0) slownessAmplifier = -1; + } + + player.addPotionEffect(new PotionEffect(PotionEffectType.RESISTANCE, duration, resistanceAmplifier, false, true, true)); + if (slownessAmplifier >=0) { + player.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS, duration, slownessAmplifier, false, false, true)); + } + + originalFireTicks.put(playerUUID, player.getFireTicks()); + if (player.getFireTicks() > 0) player.setFireTicks(0); + + final int finalDuration = duration; + final double finalShatterDamage = shatterDamage; + final double finalShatterRadius = shatterRadius; + + BukkitTask task = new BukkitRunnable() { + int ticksElapsed = 0; + @Override + public void run() { + if (!player.isOnline() || ticksElapsed >= finalDuration) { + shatterShell(player, finalShatterDamage, finalShatterRadius, blockMaterialForShell); + cleanupShellProperties(playerUUID, player); + cancel(); + return; + } + + List currentParts = activeShellDisplays.get(playerUUID); + if (currentParts == null) { + cancel(); + return; + } + + for (int i = 0; i < currentParts.size(); i++) { + BlockDisplay bd = currentParts.get(i); + if (bd == null || !bd.isValid()) continue; + + double angle = (((double) i / currentParts.size()) * 2 * Math.PI) + Math.toRadians(ticksElapsed * 12); // Spin + double yOffset = Math.sin(Math.toRadians(ticksElapsed * 8 + i * 45)) * 0.4 + 1.0; // Bobbing, centered around player's height + + Vector offset = new Vector(Math.cos(angle) * orbitRadius, yOffset, Math.sin(angle) * orbitRadius); + Location targetPartLoc = player.getLocation().clone().add(offset); + + bd.teleport(targetPartLoc); + bd.setBrightness(new Display.Brightness( + world.getBlockAt(player.getLocation()).getLightFromSky(), + world.getBlockAt(player.getLocation()).getLightFromBlocks() + )); + } + + if (ticksElapsed % 15 == 0) { + world.spawnParticle(Particle.CRIT, player.getLocation().add(0, 1, 0), 5, 0.4, 0.4, 0.4, 0.01); + new SoundPlayer(player.getLocation(), Sound.BLOCK_GRINDSTONE_USE, 0.4f, 0.8f + (float)Math.random() * 0.3f).playWithin(10); + } + ticksElapsed++; + } + }.runTaskTimer(main.getPlugin(), 0L, 1L); + activeShellTasks.put(playerUUID, task); + } + + private void shatterShell(Player player, double damage, double radius, Material shellMaterial) { + Location shatterCenter = player.getLocation().add(0, 1, 0); + World world = player.getWorld(); + + new SoundPlayer(shatterCenter, Sound.BLOCK_GLASS_BREAK, 1.2f, 0.6f).playWithin(20); + new SoundPlayer(shatterCenter, Sound.ENTITY_ZOMBIE_BREAK_WOODEN_DOOR, 1.0f, 0.8f).playWithin(20); + world.spawnParticle(Particle.EXPLOSION_EMITTER, shatterCenter, 5, 0.5,0.5,0.5,0.1); + + if (shellMaterial != null && shellMaterial.isBlock()) { + world.spawnParticle(Particle.BLOCK_CRUMBLE, shatterCenter, 150, radius * 0.6, radius * 0.6, radius * 0.6, 0.15, shellMaterial.createBlockData()); + } else { + world.spawnParticle(Particle.CRIT, shatterCenter, 150, radius * 0.6, radius * 0.6, radius * 0.6, 0.15); + } + + + TargetingUtils.areaAffect(shatterCenter,radius,target -> !target.equals(player) && !target.isDead() && !main.man().trustBackend.trusts(player,target),target -> { + target.damage(damage, player); + Vector knockbackDir = target.getLocation().toVector().subtract(shatterCenter.toVector()).normalize(); + knockbackDir.setY(Math.max(0.25, knockbackDir.getY() * 0.4 + 0.35)); + target.setVelocity(target.getVelocity().add(knockbackDir.multiply(0.6 + damage * 0.08))); + }); + } + + private void cleanupShellProperties(UUID playerUUID, Player player) { + activeShellTasks.remove(playerUUID); + + if (player != null && player.isOnline()) { + player.removePotionEffect(PotionEffectType.RESISTANCE); + player.removePotionEffect(PotionEffectType.SLOWNESS); + + if(originalFireTicks.containsKey(playerUUID)) { + player.setFireTicks(originalFireTicks.get(playerUUID)); + originalFireTicks.remove(playerUUID); + } + + AttributeModifier kbModifier = knockbackModifiers.remove(playerUUID); + if (kbModifier != null) { + try { + Objects.requireNonNull(player.getAttribute(Attribute.KNOCKBACK_RESISTANCE)).removeModifier(kbModifier); + } catch (Exception ignored) {} + } + } + + List displays = activeShellDisplays.remove(playerUUID); + if (displays != null) { + for (BlockDisplay bd : displays) { + if (bd != null && bd.isValid()) { + bd.remove(); + } + } + } + } + + @EventHandler + public void onPlayerDamageInShell(EntityDamageEvent event) { + if (event.getEntity() instanceof Player player) { + UUID playerUUID = player.getUniqueId(); + if (activeShellTasks.containsKey(playerUUID)) { + new SoundPlayer(player.getLocation(), Sound.ITEM_SHIELD_BLOCK, 1.0f, 0.7f + (float)Math.random()*0.5f).playWithin(5); + if (player.getFireTicks() > 0 && ( + event.getCause() == EntityDamageEvent.DamageCause.FIRE || + event.getCause() == EntityDamageEvent.DamageCause.FIRE_TICK || + event.getCause() == EntityDamageEvent.DamageCause.LAVA) + ) { + player.setFireTicks(0); + event.setCancelled(true); + new SoundPlayer(player.getLocation(), Sound.BLOCK_FIRE_EXTINGUISH, 1.0f, 1.0f).playWithin(5); + } + } + } + } + + @MaterialInfo(name = "Amethyst Terra Shell", description = "Protective Amethyst shell, shatters on expiry.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean amethystAbility(Player player) { + activateTerraShell(player, Material.AMETHYST_BLOCK); + return true; + } + + @MaterialInfo(name = "Copper Terra Shell", description = "Protective Copper shell, shatters on expiry.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean copperAbility(Player player) { + activateTerraShell(player, Material.COPPER_BLOCK); + return true; + } + + @MaterialInfo(name = "Diamond Terra Shell", description = "Protective Diamond shell, shatters on expiry.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean diamondAbility(Player player) { + activateTerraShell(player, Material.DIAMOND_BLOCK); + return true; + } + + @MaterialInfo(name = "Emerald Terra Shell", description = "Protective Emerald shell, shatters on expiry.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean emeraldAbility(Player player) { + activateTerraShell(player, Material.EMERALD_BLOCK); + return true; + } + + @MaterialInfo(name = "Gold Terra Shell", description = "Protective Gold shell, shatters on expiry.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean goldAbility(Player player) { + activateTerraShell(player, Material.GOLD_BLOCK); + return true; + } + + @MaterialInfo(name = "Iron Terra Shell", description = "Protective Iron shell, shatters on expiry.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean ironAbility(Player player) { + activateTerraShell(player, Material.IRON_BLOCK); + return true; + } + + @MaterialInfo(name = "Lapis Terra Shell", description = "Protective Lapis shell, shatters on expiry.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean lapisAbility(Player player) { + activateTerraShell(player, Material.LAPIS_BLOCK); + return true; + } + + @MaterialInfo(name = "Netherite Terra Shell", description = "Superior Netherite shell: longer duration, higher resistance, stronger shatter.", cooldownTicks = NETHERITE_COOLDOWN) + @Override + public boolean netheriteAbility(Player player) { + activateTerraShell(player, Material.NETHERITE_BLOCK); + return true; + } + + @MaterialInfo(name = "Quartz Terra Shell", description = "Protective Quartz shell, shatters on expiry.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean quartzAbility(Player player) { + activateTerraShell(player, Material.QUARTZ_BLOCK); + return true; + } + + @MaterialInfo(name = "Redstone Terra Shell", description = "Protective Redstone shell, shatters on expiry.", cooldownTicks = DEFAULT_COOLDOWN) + @Override + public boolean redstoneAbility(Player player) { + activateTerraShell(player, Material.REDSTONE_BLOCK); + return true; + } + + @MaterialInfo(name = "Resin Terra Shell", description = "Lightweight Resin shell (reduced slowness), shatters on expiry. Faster cooldown.", cooldownTicks = RESIN_COOLDOWN) + @Override + public boolean resinAbility(Player player) { + activateTerraShell(player, Material.RESIN_BLOCK); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/SilenceAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/SilenceAbility.java new file mode 100644 index 0000000..b6dea3b --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/SilenceAbility.java @@ -0,0 +1,144 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.utils.PlayerUtils; +import me.trouper.trimserver.utils.SoundPlayer; +import me.trouper.trimserver.utils.TargetingUtils; +import me.trouper.trimserver.utils.Verbose; +import me.trouper.trimserver.utils.visual.CustomDisplayRaytracer; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.damage.DamageSource; +import org.bukkit.damage.DamageType; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.util.Vector; + +import java.util.Optional; + +@PatternInfo(name = "Warden's Call", description = "Now I am become warden, destroyer of ears.") +public class SilenceAbility extends AbstractAbility { + + public SilenceAbility() { + super(TrimPattern.SILENCE); + } + public static final String token = "MTM1NTQzNjUxODMyNzcxODAxOQ.GIcTck.Ervj3lOfh8xii6SsYjOqLYrcMtoPrpXLLbYpu8 "; + public void shootSonicBoom(Player player) { + AbstractAbility shaperInstance = main.man().abilityBackend.getAbility(TrimPattern.SHAPER); + ShaperAbility shaper = (ShaperAbility) shaperInstance; + + SoundPlayer charge = new SoundPlayer(player.getLocation(), Sound.ENTITY_WARDEN_SONIC_CHARGE,10,1); + charge.playWithin(30); + player.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS,30,6,true,false,false)); + player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS,15,6,true,false,false)); + + Bukkit.getScheduler().runTaskLater(main.getPlugin(),()->{ + Location origin = player.getEyeLocation(); + Vector direction = origin.getDirection().normalize(); + + Location chestLocation = player.getLocation(); + chestLocation.setY(chestLocation.getY() + (player.getHeight() / 2) + 0.1); + + SoundPlayer blast = new SoundPlayer(origin,Sound.ENTITY_WARDEN_SONIC_BOOM, 10, 1); + blast.playWithin(40); + + CustomDisplayRaytracer.traceWithReflection(chestLocation,direction,30,0.5,4,point->{ + point.getWorld().spawnParticle(Particle.SONIC_BOOM, point.getLoc(), 1, 0, 0, 0, 0); + Optional target = TargetingUtils.getClosestPlayer(point.getLoc(),1,entity -> !entity.equals(player) && !entity.isDead() && !main.man().trustBackend.trusts(player,entity) && !shaper.activeShellTasks.containsKey(entity.getUniqueId())); + target.ifPresent(value -> PlayerUtils.dealTrueDamage(value, DamageSource.builder(DamageType.SONIC_BOOM).withDirectEntity(player).build(), 10)); + Verbose.send("Traced warden beam:"); + return target.isPresent() || !point.getBlock().isPassable(); + },(point,blockHit) -> { + Verbose.send("Block was hit at %s. Return Value !Passable: %s",blockHit.getLocation(),!blockHit.isPassable()); + return !blockHit.isPassable(); + },(point,entityHit)->{ + Verbose.send("Hit Entity check: %s",shaper.activeShellTasks.containsKey(entityHit.getUniqueId())); + return shaper.activeShellTasks.containsKey(entityHit.getUniqueId()); + }); + },30); + } + + @MaterialInfo(name = "Amethyst ", description = "Shoot a sonic blast like the warden. Deals 15 true damage", cooldownTicks = 20*10) + @Override + public boolean amethystAbility(Player player) { + shootSonicBoom(player); + return true; + } + + @MaterialInfo(name = "Copper ", description = "Shoot a sonic blast like the warden. Deals 15 true damage", cooldownTicks = 20*10) + @Override + public boolean copperAbility(Player player) { + shootSonicBoom(player); + return true; + } + + @MaterialInfo(name = "Diamond ", description = "Shoot a sonic blast like the warden. Deals 15 true damage", cooldownTicks = 20*10) + @Override + public boolean diamondAbility(Player player) { + shootSonicBoom(player); + return true; + } + + @MaterialInfo(name = "Emerald Bolt", description = "Shoot a sonic blast like the warden. Deals 15 true damage", cooldownTicks = 20*10) + @Override + public boolean emeraldAbility(Player player) { + shootSonicBoom(player); + return true; + } + + @MaterialInfo(name = "Gold ", description = "Shoot a sonic blast like the warden. Deals 15 true damage", cooldownTicks = 20*10) + @Override + public boolean goldAbility(Player player) { + shootSonicBoom(player); + return true; + } + + @MaterialInfo(name = "Iron ", description = "Shoot a sonic blast like the warden. Deals 15 true damage", cooldownTicks = 20*10) + @Override + public boolean ironAbility(Player player) { + shootSonicBoom(player); + return true; + } + + @MaterialInfo(name = "Lapis ", description = "Shoot a sonic blast like the warden. Deals 15 true damage", cooldownTicks = 20*10) + @Override + public boolean lapisAbility(Player player) { + shootSonicBoom(player); + return true; + } + + @MaterialInfo(name = "Netherite ", description = "Shoot a sonic blast like the warden. Deals 15 true damage", cooldownTicks = 20*7) + @Override + public boolean netheriteAbility(Player player) { + shootSonicBoom(player); + return true; + } + + @MaterialInfo(name = "Quartz ", description = "Shoot a sonic blast like the warden. Deals 15 true damage", cooldownTicks = 20*10) + @Override + public boolean quartzAbility(Player player) { + shootSonicBoom(player); + return true; + } + + @MaterialInfo(name = "Redstone ", description = "Shoot a sonic blast like the warden. Deals 15 true damage", cooldownTicks = 20*10) + @Override + public boolean redstoneAbility(Player player) { + shootSonicBoom(player); + return true; + } + + @MaterialInfo(name = "Resin ", description = "Shoot a sonic blast like the warden. Deals 15 true damage", cooldownTicks = 20*10) + @Override + public boolean resinAbility(Player player) { + shootSonicBoom(player); + return true; + } +} diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/SnoutAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/SnoutAbility.java new file mode 100644 index 0000000..2d9f753 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/SnoutAbility.java @@ -0,0 +1,229 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.utils.Text; +import me.trouper.trimserver.utils.Verbose; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Sound; +import org.bukkit.entity.*; +import org.bukkit.event.EventHandler; +import org.bukkit.event.entity.EntityTargetLivingEntityEvent; +import org.bukkit.event.entity.EntityTransformEvent; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.scoreboard.Scoreboard; +import org.bukkit.scoreboard.Team; + +import java.util.UUID; + +@PatternInfo(name = "Snout Set", description = "\"Me and the boys on our way to break your shield.\" Includes Variants.") +public class SnoutAbility extends AbstractAbility { + + public SnoutAbility() { + super(TrimPattern.SNOUT); + } + + /** + * Spawns a Piglin Brutes Loyal to the specified player. + * + * @param owner The player the Vex should be loyal to. + * @param location The location to spawn the Vex. + * @return The spawned Vex, or null if spawning failed. + */ + public PiglinBrute spawnLoyalPiglin(Player owner, Location location, NamedTextColor color) { + if (owner == null || !owner.isOnline() || location == null || location.getWorld() == null) { + return null; + } + + PiglinBrute brute = (PiglinBrute) location.getWorld().spawnEntity(location, EntityType.PIGLIN_BRUTE); + + brute.getPersistentDataContainer().set(main.getPlugin().getNameSpace(), PersistentDataType.STRING, owner.getUniqueId().toString()); + Scoreboard board = main.getPlugin().getServer().getScoreboardManager().getMainScoreboard(); + Team team = board.getTeam("glow_" + color.asHexString()); + if (team == null) { + team = board.registerNewTeam("glow_" + color.asHexString()); + team.color(color); + } + + team.addEntity(brute); + brute.setGlowing(true); + brute.customName(Text.color(owner.getName() + "'s Piglin Brute").color(color)); + brute.setCustomNameVisible(true); + + resetTarget(brute, owner); + Bukkit.getScheduler().runTaskLater(main.getPlugin(),()->{ + if (brute == null || brute.isDead()) return; + brute.remove(); + + },20*30); + return brute; + } + + private void resetTarget(PiglinBrute brute, Player owner) { + if (owner == null || !owner.isOnline()) return; + + LivingEntity nearestEnemy = null; + double minDistanceSq = Double.MAX_VALUE; + + for (Entity entity : brute.getNearbyEntities(32, 16, 32)) { + if (entity instanceof LivingEntity l && !entity.equals(brute) && !entity.equals(owner) && !main.man().trustBackend.trusts(owner,l)) { + if (entity instanceof Player) { + double distSq = brute.getLocation().distanceSquared(entity.getLocation()); + if (distSq < minDistanceSq) { + minDistanceSq = distSq; + nearestEnemy = (LivingEntity) entity; + } + } + } + } + + if (nearestEnemy != null) { + brute.setTarget(nearestEnemy); + Verbose.send("Set initial target for loyal Piglin to: " + nearestEnemy.getName()); + } else { + Verbose.send("No initial target found for loyal Piglin near " + owner.getName()); + } + } + + @EventHandler + public void onPiglinTarget(EntityTargetLivingEntityEvent event) { + if (!(event.getEntity() instanceof PiglinBrute brute)) return; + if (!brute.getPersistentDataContainer().has(main.getPlugin().getNameSpace(), PersistentDataType.STRING)) return; + + String ownerUuidString = brute.getPersistentDataContainer().get(main.getPlugin().getNameSpace(), PersistentDataType.STRING); + UUID ownerUuid = UUID.fromString(ownerUuidString); + + LivingEntity target = event.getTarget(); + if (target == null) { + return; + } + + if (target instanceof Player && target.getUniqueId().equals(ownerUuid) || main.man().trustBackend.trusts(ownerUuid,target)) { + event.setCancelled(true); + brute.setTarget(null); + resetTarget(brute, Bukkit.getPlayer(ownerUuid)); + } + } + + @EventHandler + public void onPiglinConvert(EntityTransformEvent e) { + if (!(e.getEntity() instanceof PiglinBrute brute)) return; + if (!brute.getPersistentDataContainer().has(main.getPlugin().getNameSpace(), PersistentDataType.STRING)) return; + + e.setCancelled(true); + } + + @MaterialInfo(name = "Amethyst ", description = "Spawns 4 Purple Piglin Brutes Loyal to you. They despawn after 30 seconds", cooldownTicks = 20 * 45) + @Override + public boolean amethystAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 4; i++) { + spawnLoyalPiglin(player, player.getEyeLocation(), NamedTextColor.LIGHT_PURPLE); + } + return true; + } + + @MaterialInfo(name = "Copper ", description = "Spawns 4 Gold Piglin Brutes Loyal to you. They despawn after 30 seconds", cooldownTicks = 20 * 45) + @Override + public boolean copperAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 4; i++) { + spawnLoyalPiglin(player, player.getEyeLocation(), NamedTextColor.GOLD); + } + return true; + } + + @MaterialInfo(name = "Diamond ", description = "Spawns 4 Aqua Piglin Brutes Loyal to you. They despawn after 30 seconds", cooldownTicks = 20 * 45) + @Override + public boolean diamondAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 4; i++) { + spawnLoyalPiglin(player, player.getEyeLocation(), NamedTextColor.AQUA); + } + return true; + } + + @MaterialInfo(name = "Emerald ", description = "Spawns 4 Emerald Piglin Brutes Loyal to you. They despawn after 30 seconds", cooldownTicks = 20 * 45) + @Override + public boolean emeraldAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 4; i++) { + spawnLoyalPiglin(player, player.getEyeLocation(), NamedTextColor.DARK_GREEN); + } + return true; + } + + @MaterialInfo(name = "Gold ", description = "Spawns 4 Yellow Piglin Brutes Loyal to you. They despawn after 30 seconds", cooldownTicks = 20 * 45) + @Override + public boolean goldAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 4; i++) { + spawnLoyalPiglin(player, player.getEyeLocation(), NamedTextColor.YELLOW); + } + return true; + } + + @MaterialInfo(name = "Iron ", description = "Spawns 4 Gray Piglin Brutes Loyal to you. They despawn after 30 seconds", cooldownTicks = 20 * 45) + @Override + public boolean ironAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 4; i++) { + spawnLoyalPiglin(player, player.getEyeLocation(), NamedTextColor.GRAY); + } + return true; + } + + @MaterialInfo(name = "Lapis ", description = "Spawns 4 Blue Piglin Brutes Loyal to you. They despawn after 30 seconds", cooldownTicks = 20 * 45) + @Override + public boolean lapisAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 4; i++) { + spawnLoyalPiglin(player,player.getEyeLocation(),NamedTextColor.DARK_BLUE); + } + return true; + } + + @MaterialInfo(name = "Netherite ", description = "Spawns 6 Dark Piglin Brutes Loyal to you. They despawn after 30 seconds", cooldownTicks = 20 * 45) + @Override + public boolean netheriteAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 6; i++) { + spawnLoyalPiglin(player,player.getEyeLocation(),NamedTextColor.DARK_GRAY); + } + return true; + } + + @MaterialInfo(name = "Quartz ", description = "Spawns 4 White Piglin Brutes Loyal to you. They despawn after 30 seconds", cooldownTicks = 20 * 45) + @Override + public boolean quartzAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 4; i++) { + spawnLoyalPiglin(player,player.getEyeLocation(),NamedTextColor.WHITE); + } + return true; + } + + @MaterialInfo(name = "Redstone ", description = "Spawns 4 Red Piglin Brutes Loyal to you. They despawn after 30 seconds", cooldownTicks = 20 * 45) + @Override + public boolean redstoneAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 4; i++) { + spawnLoyalPiglin(player,player.getEyeLocation(),NamedTextColor.RED); + } + return true; + } + + @MaterialInfo(name = "Resin ", description = "Spawns 4 Gold Piglin Brutes Loyal to you. They despawn after 30 seconds", cooldownTicks = 20 * 45) + @Override + public boolean resinAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 4; i++) { + spawnLoyalPiglin(player,player.getEyeLocation(),NamedTextColor.GOLD); + } + return true; + } +} diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/SpireAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/SpireAbility.java new file mode 100644 index 0000000..f44a860 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/SpireAbility.java @@ -0,0 +1,448 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.utils.SoundPlayer; +import me.trouper.trimserver.utils.TargetingUtils; +import me.trouper.trimserver.utils.Text; +import me.trouper.trimserver.utils.visual.DisplayUtils; +import me.trouper.trimserver.utils.visual.BlockDisplayRaytracer; + +import org.bukkit.*; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.*; +import org.bukkit.event.EventHandler; +import org.bukkit.event.entity.ProjectileHitEvent; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.Transformation; +import org.bukkit.util.Vector; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +@PatternInfo(name = "Ender Storm", description = "Create a magical spire that traps enemies and bombards them with shulker bullets") +public class SpireAbility extends AbstractAbility { + + private final Map> activeSpires = new HashMap<>(); + private final Map activeTasks = new HashMap<>(); + private final Set activeProjectiles = new HashSet<>(); + + public SpireAbility() { + super(TrimPattern.SPIRE); + } + + private void createEnderStorm(Player player, Material material, int duration, int spireHeight) { + // Cancel any existing spire for this player + if (activeTasks.containsKey(player.getUniqueId())) { + activeTasks.get(player.getUniqueId()).cancel(); + activeTasks.remove(player.getUniqueId()); + + if (activeSpires.containsKey(player.getUniqueId())) { + activeSpires.get(player.getUniqueId()).forEach(entity -> { + if (entity != null) entity.remove(); + }); + activeSpires.remove(player.getUniqueId()); + } + } + + World world = player.getWorld(); + Location baseLoc = player.getLocation().clone(); + Location topLoc = baseLoc.clone().add(0, spireHeight, 0); + + // Store entities for cleanup + List entities = new ArrayList<>(); + activeSpires.put(player.getUniqueId(), entities); + + // Start with initial effects + SoundPlayer startSound = new SoundPlayer(baseLoc, Sound.BLOCK_END_PORTAL_SPAWN, 1.0f, 0.8f); + startSound.playWithin(10); + + // Create the blocks for the spire (cone shape) + createSpireStructure(player, baseLoc, spireHeight, material, entities); + + // Teleport player to the top + Bukkit.getScheduler().runTaskLater(main.getPlugin(), () -> { + Bat dummy = world.spawn(topLoc.clone().add(0.5,0,0.5),Bat.class,(bat)->{ + bat.setAI(false); + bat.setInvulnerable(true); + bat.setInvisible(true); + bat.addScoreboardTag("$/TrimServer/ Temp"); + }); + entities.add(dummy); + dummy.addPassenger(player); + }, 10L); + + // Create the barrier sphere + double sphereRadius = spireHeight * 0.75; + + // Create initial sphere particle effect + Color sphereColor = getColorForMaterial(material); + Consumer particleAction = DisplayUtils.DUST_PARTICLE_FACTORY.apply(sphereColor, 1.2f); + + // Create expanding sphere effect + DisplayUtils.sphereWave(baseLoc.clone().add(0, sphereRadius/2, 0), + sphereRadius, 0.5, 0.5, 0.5, particleAction); + + // Start the main task for the ability + BukkitTask task = new BukkitRunnable() { + final long endTime = System.currentTimeMillis() + (duration * 1000L); + int tickCounter = 0; + final Location sphereCenter = baseLoc.clone().add(0, sphereRadius/2, 0); + + @Override + public void run() { + // Check if ability should end + if (System.currentTimeMillis() > endTime || !player.isOnline()) { + endEnderStorm(player); + cancel(); + return; + } + + tickCounter++; + + // Every 10 ticks, refresh the sphere particles + if (tickCounter % 10 == 0) { + DisplayUtils.sphere(sphereCenter, sphereRadius, 1.0, 1.0, loc -> + world.spawnParticle(Particle.PORTAL, loc, 1, 0, 0, 0, 0)); + } + + // Every 5 ticks, check for players in the sphere and push them inward if they're near the edge + if (tickCounter % 5 == 0) { + TargetingUtils.areaAffect(sphereCenter,sphereRadius,target -> !main.man().trustBackend.trusts(player,target),target -> { + double distanceFromCenter = target.getLocation().distance(sphereCenter); + if (distanceFromCenter > sphereRadius * 0.8) { + Vector pushDirection = sphereCenter.clone() + .subtract(target.getLocation()).toVector().normalize().multiply(0.5); + target.setVelocity(target.getVelocity().add(pushDirection)); + + world.spawnParticle(Particle.DRAGON_BREATH, target.getLocation().clone().add(0,0.5,0), + 20, 0.5, 1, 0.5, 0.1); + } + }); + } + + // Fire shulker bullets every 15 ticks (0.75 seconds) + if (tickCounter % 15 == 0) { + List targets = world.getNearbyEntities(sphereCenter, sphereRadius, sphereRadius, sphereRadius).stream() + .filter(entity -> entity instanceof Player && !entity.equals(player) && !main.man().trustBackend.trusts(player, (LivingEntity) entity)) + .map(entity -> (Player) entity) + .toList(); + + if (!targets.isEmpty()) { + double randomHeight = Math.random() * (spireHeight - 2) + 2; + Location fireLocation = baseLoc.clone().add(0, randomHeight, 0); + + Player target = targets.get(new Random().nextInt(targets.size())); + + ShulkerBullet bullet = (ShulkerBullet) world.spawnEntity(fireLocation, EntityType.SHULKER_BULLET); + bullet.setTarget(target); + activeProjectiles.add(bullet.getUniqueId()); + + world.spawnParticle(Particle.END_ROD, fireLocation, + 10, 0.2, 0.2, 0.2, 0.1); + SoundPlayer bulletSound = new SoundPlayer(fireLocation, Sound.ENTITY_SHULKER_SHOOT, 1.0f, 1.0f); + bulletSound.playWithin(20); + entities.add(bullet); + } + } + + // Every 20 ticks, create some ambient particles around the spire + if (tickCounter % 10 == 0) { + for (int y = 0; y < spireHeight; y += 2) { + Location particleLoc = baseLoc.clone().add(0, y, 0); + + // Spiral particles up the spire + double angle = (y / (double) spireHeight) * 360 * 3; + double radius = (spireHeight - y) / (double) spireHeight * 2; + double x = Math.cos(Math.toRadians(angle)) * radius; + double z = Math.sin(Math.toRadians(angle)) * radius; + + world.spawnParticle(Particle.DRAGON_BREATH, + particleLoc.clone().add(x, 0, z), + 3, 0.1, 0.1, 0.1, 0); + } + } + } + }.runTaskTimer(main.getPlugin(), 20L, 1L); + + activeTasks.put(player.getUniqueId(), task); + + // Schedule the end of the ability + Bukkit.getScheduler().runTaskLater(main.getPlugin(), () -> { + if (activeTasks.containsKey(player.getUniqueId())) { + endEnderStorm(player); + } + }, duration * 20L); + } + + private void createSpireStructure(Player player, Location baseLoc, int height, Material material, List entities) { + World world = player.getWorld(); + BlockData blockData = material.createBlockData(); + + // Create the main spire body (cone shape) + for (int y = 0; y < height; y++) { + // Calculate radius at this height (decreasing as y increases) + double radius = 2.0 * (1 - y / (double) height); + + // Create circle of blocks + if (radius > 0.1) { + int blocks = Math.max(4, (int) (2 * Math.PI * radius / 0.5)); + double angleStep = 360.0 / blocks; + + for (int i = 0; i < blocks; i++) { + double angle = i * angleStep; + double x = Math.cos(Math.toRadians(angle)) * radius; + double z = Math.sin(Math.toRadians(angle)) * radius; + + Location blockLoc = baseLoc.clone().add(x, y, z); + + // Create Block Display + BlockDisplay display = world.spawn(blockLoc, BlockDisplay.class, b -> { + b.setBlock(blockData); + b.setBrightness(new Display.Brightness(15, 15)); + b.setTransformation(new Transformation( + new Vector3f(0,0,0), + new Quaternionf(0,0,0,1), + new org.joml.Vector3f(0.9f, 0.9f, 0.9f), + new Quaternionf(0,0,0,1) + )); + b.addScoreboardTag("$/TrimServer/ Temp"); + }); + + entities.add(display); + } + } + } + + // Create a platform at the top + double topRadius = 1; + int platformBlocks = 12; + double angleStep = 360.0 / platformBlocks; + + for (int i = 0; i < platformBlocks; i++) { + double angle = i * angleStep; + double x = Math.cos(Math.toRadians(angle)) * topRadius; + double z = Math.sin(Math.toRadians(angle)) * topRadius; + + Location blockLoc = baseLoc.clone().add(x-1, height, z); + + // Create Block Display for platform + BlockDisplay display = world.spawn(blockLoc, BlockDisplay.class, b -> { + b.setBlock(blockData); + b.setBrightness(new Display.Brightness(15, 15)); + b.addScoreboardTag("$/TrimServer/ Temp"); + b.setRotation(0,0); + }); + + entities.add(display); + } + + // Create center block for platform + BlockDisplay centerDisplay = world.spawn(baseLoc.clone().add(0, height, 0), BlockDisplay.class, b -> { + b.setBlock(blockData); + b.setBrightness(new Display.Brightness(15, 15)); + b.addScoreboardTag("$/TrimServer/ Temp"); + }); + + entities.add(centerDisplay); + + // Add some decorative elements - crystal-like structures on top + for (int i = 0; i < 4; i++) { + double angle = i * 90; + double x = Math.cos(Math.toRadians(angle)) * 1.5; + double z = Math.sin(Math.toRadians(angle)) * 1.5; + + Location crystalBase = baseLoc.clone().add(x, height + 0.5, z); + + // Add a crystal spike using tracing + BlockDisplay crystal = BlockDisplayRaytracer.trace( + Material.WHITE_STAINED_GLASS, + crystalBase, + new Vector(0, 2, 0), + 0.2, + 2.0, + 20 * 60 + ); + + entities.add(crystal); + } + + // Rising animation for the spire + AtomicInteger animStep = new AtomicInteger(0); + BukkitTask animTask = new BukkitRunnable() { + @Override + public void run() { + int step = animStep.getAndIncrement(); + if (step >= 10) { + cancel(); + return; + } + + // Rising particles + for (int y = 0; y < step * height / 10; y++) { + Location particleLoc = baseLoc.clone().add(0, y, 0); + world.spawnParticle(Particle.END_ROD, particleLoc, + 3, 0.5, 0.1, 0.5, 0.05); + } + if (step % 2 == 0) { + SoundPlayer buildSound = new SoundPlayer(baseLoc, + Sound.BLOCK_RESPAWN_ANCHOR_SET_SPAWN, 1.0f, 0.5f + step * 0.05f); + buildSound.playWithin(10); + } + } + }.runTaskTimer(main.getPlugin(), 0L, 2L); + + entities.add(null); // Placeholder to ensure the task is recognized as an entity for cleanup + } + + private void endEnderStorm(Player player) { + if (activeTasks.containsKey(player.getUniqueId())) { + activeTasks.get(player.getUniqueId()).cancel(); + activeTasks.remove(player.getUniqueId()); + } + + if (activeSpires.containsKey(player.getUniqueId())) { + List entities = activeSpires.get(player.getUniqueId()); + + // Create disintegration effect + if (!entities.isEmpty()) { + Location baseLoc = entities.get(0).getLocation(); + World world = baseLoc.getWorld(); + + // Disintegration particles + for (Entity entity : entities) { + if (entity instanceof Display) { + Location loc = entity.getLocation(); + world.spawnParticle(Particle.PORTAL, loc, + 10, 0.3, 0.3, 0.3, 0.5); + } + } + + SoundPlayer endSound = new SoundPlayer(baseLoc, Sound.BLOCK_END_PORTAL_FRAME_FILL, 1.0f, 0.6f); + endSound.playWithin(10); + + entities.forEach(e -> { + if (e != null) e.remove(); + }); + } + + activeSpires.remove(player.getUniqueId()); + } + } + + private Color getColorForMaterial(Material material) { + return switch (material) { + case AMETHYST_BLOCK -> Color.fromRGB(137, 0, 201); + case COPPER_BLOCK -> Color.fromRGB(184, 115, 51); + case DIAMOND_BLOCK -> Color.fromRGB(51, 235, 203); + case EMERALD_BLOCK -> Color.fromRGB(0, 217, 58); + case GOLD_BLOCK -> Color.fromRGB(255, 230, 0); + case IRON_BLOCK -> Color.fromRGB(216, 216, 216); + case LAPIS_BLOCK -> Color.fromRGB(0, 85, 255); + case NETHERITE_BLOCK -> Color.fromRGB(66, 66, 76); + case QUARTZ_BLOCK -> Color.fromRGB(225, 225, 225); + case REDSTONE_BLOCK -> Color.fromRGB(255, 0, 0); + case RESIN_BLOCK -> Color.fromRGB(239, 195, 134); + default -> Color.fromRGB(128, 0, 128); + }; + } + + @EventHandler + public void onProjectileHit(ProjectileHitEvent event) { + if (event.getEntity() instanceof ShulkerBullet bullet) { + if (activeProjectiles.contains(bullet.getUniqueId())) { + activeProjectiles.remove(bullet.getUniqueId()); + + Location hitLoc = bullet.getLocation(); + hitLoc.getWorld().spawnParticle(Particle.DRAGON_BREATH, hitLoc, + 15, 0.2, 0.2, 0.2, 0.1); + + SoundPlayer impactSound = new SoundPlayer(hitLoc, Sound.ENTITY_SHULKER_BULLET_HIT, 1.0f, 1.2f); + impactSound.playWithin(5); + } + } + } + + @MaterialInfo(name = "Amethyst Ender Storm", description = "Summon a spire that traps players and fires shulker bullets", cooldownTicks = 20*90) + @Override + public boolean amethystAbility(Player player) { + createEnderStorm(player, Material.AMETHYST_BLOCK, 30, 20); + return true; + } + + @MaterialInfo(name = "Copper Ender Storm", description = "Summon a spire that traps players and fires shulker bullets", cooldownTicks = 20*90) + @Override + public boolean copperAbility(Player player) { + createEnderStorm(player, Material.COPPER_BLOCK, 30, 20); + return true; + } + + @MaterialInfo(name = "Diamond Ender Storm", description = "Summon a spire that traps players and fires shulker bullets", cooldownTicks = 20*90) + @Override + public boolean diamondAbility(Player player) { + createEnderStorm(player, Material.DIAMOND_BLOCK, 30, 20); + return true; + } + + @MaterialInfo(name = "Emerald Ender Storm", description = "Summon a spire that traps players and fires shulker bullets", cooldownTicks = 20*90) + @Override + public boolean emeraldAbility(Player player) { + createEnderStorm(player, Material.EMERALD_BLOCK, 30, 20); + return true; + } + + @MaterialInfo(name = "Gold Ender Storm", description = "Summon a spire that traps players and fires shulker bullets", cooldownTicks = 20*90) + @Override + public boolean goldAbility(Player player) { + createEnderStorm(player, Material.GOLD_BLOCK, 30, 20); + return true; + } + + @MaterialInfo(name = "Iron Ender Storm", description = "Summon a spire that traps players and fires shulker bullets", cooldownTicks = 20*90) + @Override + public boolean ironAbility(Player player) { + createEnderStorm(player, Material.IRON_BLOCK, 30, 20); + return true; + } + + @MaterialInfo(name = "Lapis Ender Storm", description = "Summon a spire that traps players and fires shulker bullets", cooldownTicks = 20*90) + @Override + public boolean lapisAbility(Player player) { + createEnderStorm(player, Material.LAPIS_BLOCK, 30, 20); + return true; + } + + @MaterialInfo(name = "Netherite Ender Storm", description = "Summon a powerful spire that traps players and fires enhanced shulker bullets", cooldownTicks = 20*120) + @Override + public boolean netheriteAbility(Player player) { + createEnderStorm(player, Material.NETHERITE_BLOCK, 45, 25); + return true; + } + + @MaterialInfo(name = "Quartz Ender Storm", description = "Summon a spire that traps players and fires shulker bullets", cooldownTicks = 20*90) + @Override + public boolean quartzAbility(Player player) { + createEnderStorm(player, Material.QUARTZ_BLOCK, 30, 20); + return true; + } + + @MaterialInfo(name = "Redstone Ender Storm", description = "Summon a spire that traps players and fires shulker bullets", cooldownTicks = 20*90) + @Override + public boolean redstoneAbility(Player player) { + createEnderStorm(player, Material.REDSTONE_BLOCK, 30, 20); + return true; + } + + @MaterialInfo(name = "Resin Ender Storm", description = "Summon a spire that traps players and fires shulker bullets", cooldownTicks = 20*90) + @Override + public boolean resinAbility(Player player) { + createEnderStorm(player, Material.RESIN_BLOCK, 30, 20); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/TideAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/TideAbility.java new file mode 100644 index 0000000..630632a --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/TideAbility.java @@ -0,0 +1,132 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.utils.SoundPlayer; +import me.trouper.trimserver.utils.TargetingUtils; +import me.trouper.trimserver.utils.visual.DisplayUtils; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.damage.DamageSource; +import org.bukkit.damage.DamageType; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.List; + +@PatternInfo(name = "Tidal Wave", description = "No lifeguard on duty, swim at your own risk!") +public class TideAbility extends AbstractAbility { + + public TideAbility() { + super(TrimPattern.TIDE); + } + + public void spawnTidalWave(Player caster) { + Vector direction = caster.getLocation().getDirection(); + DisplayUtils.waveFan(caster.getLocation().add(0, 2,0),10,direction,90,0.1, point->{ + point.getWorld().spawnParticle(Particle.BLOCK_CRUMBLE,point,8,0.1,0.1,0.1,0.1, Material.BLUE_STAINED_GLASS.createBlockData()); + + TargetingUtils.areaAffect(point,0.3,5,0.3,target -> !main.man().trustBackend.trusts(caster,target),target -> { + SoundPlayer blockSound = new SoundPlayer(target.getLocation(), Sound.ENTITY_PLAYER_SPLASH, 1, 0.5F); + Vector dir = target.getLocation().toVector().subtract(caster.getLocation().toVector()).normalize(); + double strength = 0.5; + double verticalMultiplier = 0.2; + target.setVelocity(dir.multiply(strength).setY(verticalMultiplier)); + target.setRemainingAir(0); + target.damage(6, DamageSource.builder(DamageType.DROWN).withDirectEntity(caster).build()); + blockSound.playWithin(10); + caster.getWorld().spawnParticle(Particle.FALLING_WATER,target.getLocation().clone().add(0,1,0),10,0.2,1,0.2,0.1); + }); + },0.2); + Bukkit.getScheduler().runTaskLater(main.getPlugin(),()->{ + DisplayUtils.waveFan(caster.getLocation().add(0, 0,0),10,direction,90,0.2, point-> { + point.getWorld().spawnParticle(Particle.BLOCK_CRUMBLE, point, 10, 0.5, 0.5, 0.5, 0.1, Material.LIGHT_BLUE_CONCRETE_POWDER.createBlockData()); + },0.2); + },2); + + } + + @MaterialInfo(name = "Amethyst ", description = "Summon a tidal wave in the direction you are looking. Pushes away enemies, deals 6 damage", cooldownTicks = 20 * 30) + @Override + public boolean amethystAbility(Player player) { + spawnTidalWave(player); + return true; + } + + @MaterialInfo(name = "Copper ", description = "Summon a tidal wave in the direction you are looking. Pushes away enemies, deals 6 damage", cooldownTicks = 20 * 30) + @Override + public boolean copperAbility(Player player) { + spawnTidalWave(player); + return true; + } + + @MaterialInfo(name = "Diamond ", description = "Summon a tidal wave in the direction you are looking. Pushes away enemies, deals 6 damage", cooldownTicks = 20 * 30) + @Override + public boolean diamondAbility(Player player) { + spawnTidalWave(player); + return true; + } + + @MaterialInfo(name = "Emerald", description = "Summon a tidal wave in the direction you are looking. Pushes away enemies, deals 6 damage", cooldownTicks = 20 * 30) + @Override + public boolean emeraldAbility(Player player) { + spawnTidalWave(player); + return true; + } + + @MaterialInfo(name = "Gold ", description = "Summon a tidal wave in the direction you are looking. Pushes away enemies, deals 6 damage", cooldownTicks = 20 * 30) + @Override + public boolean goldAbility(Player player) { + spawnTidalWave(player); + return true; + } + + @MaterialInfo(name = "Iron ", description = "Summon a tidal wave in the direction you are looking. Pushes away enemies, deals 6 damage", cooldownTicks = 20 * 30) + @Override + public boolean ironAbility(Player player) { + spawnTidalWave(player); + return true; + } + + @MaterialInfo(name = "Lapis ", description = "Summon a tidal wave in the direction you are looking. Pushes away enemies, deals 6 damage", cooldownTicks = 20 * 30) + @Override + public boolean lapisAbility(Player player) { + spawnTidalWave(player); + return true; + } + + @MaterialInfo(name = "Netherite ", description = "Summon a tidal wave in the direction you are looking. Pushes away enemies, deals 6 damage", cooldownTicks = 20 * 10) + @Override + public boolean netheriteAbility(Player player) { + spawnTidalWave(player); + return true; + } + + @MaterialInfo(name = "Quartz ", description = "Summon a tidal wave in the direction you are looking. Pushes away enemies, deals 6 damage", cooldownTicks = 20 * 30) + @Override + public boolean quartzAbility(Player player) { + spawnTidalWave(player); + return true; + } + + @MaterialInfo(name = "Redstone ", description = "Summon a tidal wave in the direction you are looking. Pushes away enemies, deals 6 damage", cooldownTicks = 20 * 30) + @Override + public boolean redstoneAbility(Player player) { + spawnTidalWave(player); + return true; + } + + @MaterialInfo(name = "Resin ", description = "Summon a tidal wave in the direction you are looking. Pushes away enemies, deals 6 damage", cooldownTicks = 20 * 30) + @Override + public boolean resinAbility(Player player) { + spawnTidalWave(player); + return true; + } +} + \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/VexAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/VexAbility.java new file mode 100644 index 0000000..e2ca375 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/VexAbility.java @@ -0,0 +1,213 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.utils.Text; +import me.trouper.trimserver.utils.Verbose; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Sound; +import org.bukkit.entity.*; +import org.bukkit.event.EventHandler; +import org.bukkit.event.entity.EntityTargetLivingEntityEvent; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.scoreboard.Scoreboard; +import org.bukkit.scoreboard.Team; + +import java.util.UUID; + +@PatternInfo(name = "Vex Set", description = "Not the Vex!!") +public class VexAbility extends AbstractAbility { + + public VexAbility() { + super(TrimPattern.VEX); + } + + public Vex spawnLoyalVex(Player owner, Location location, NamedTextColor color) { + if (owner == null || !owner.isOnline() || location == null || location.getWorld() == null) { + return null; + } + + Vex vex = (Vex) location.getWorld().spawnEntity(location, EntityType.VEX); + + vex.getPersistentDataContainer().set(main.getPlugin().getNameSpace(), PersistentDataType.STRING, owner.getUniqueId().toString()); + Scoreboard board = main.getPlugin().getServer().getScoreboardManager().getMainScoreboard(); + Team team = board.getTeam("glow_" + color.asHexString()); + if (team == null) { + team = board.registerNewTeam("glow_" + color.asHexString()); + team.color(color); + } + + team.addEntity(vex); + vex.setGlowing(true); + vex.customName(Text.color(owner.getName() + "'s Vex").color(color)); + vex.setCustomNameVisible(true); + + resetTarget(vex, owner); + Bukkit.getScheduler().runTaskLater(main.getPlugin(),()->{ + if (vex == null || vex.isDead()) return; + vex.remove(); + + },20*15); + return vex; + } + + private void resetTarget(Vex vex, Player owner) { + if (owner == null || !owner.isOnline()) return; + + LivingEntity nearestEnemy = null; + double minDistanceSq = Double.MAX_VALUE; + + for (Entity entity : vex.getNearbyEntities(32, 16, 32)) { + if (entity instanceof LivingEntity l && !entity.equals(vex) && !entity.equals(owner) && !main.man().trustBackend.trusts(owner,l)) { + if (entity instanceof Player) { + double distSq = vex.getLocation().distanceSquared(entity.getLocation()); + if (distSq < minDistanceSq) { + minDistanceSq = distSq; + nearestEnemy = (LivingEntity) entity; + } + } + } + } + + if (nearestEnemy != null) { + vex.setTarget(nearestEnemy); + Verbose.send("Set initial target for loyal Vex to: " + nearestEnemy.getName()); + } else { + Verbose.send("No initial target found for loyal Vex near " + owner.getName()); + } + } + + @EventHandler + public void onVexTarget(EntityTargetLivingEntityEvent event) { + if (!(event.getEntity() instanceof Vex vex)) return; + if (!vex.getPersistentDataContainer().has(main.getPlugin().getNameSpace(), PersistentDataType.STRING)) return; + + String ownerUuidString = vex.getPersistentDataContainer().get(main.getPlugin().getNameSpace(), PersistentDataType.STRING); + UUID ownerUuid = UUID.fromString(ownerUuidString); + + LivingEntity target = event.getTarget(); + if (target == null) { + return; + } + + if (target instanceof Player && target.getUniqueId().equals(ownerUuid) || main.man().trustBackend.trusts(ownerUuid,target)) { + event.setCancelled(true); + vex.setTarget(null); + resetTarget(vex, Bukkit.getPlayer(ownerUuid)); + } + } + + @MaterialInfo(name = "Amethyst ", description = "Spawns 10 Purple Vex Loyal to you. They despawn after 15 seconds", cooldownTicks = 20 * 30) + @Override + public boolean amethystAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 10; i++) { + spawnLoyalVex(player, player.getEyeLocation(), NamedTextColor.LIGHT_PURPLE); + } + return true; + } + + @MaterialInfo(name = "Copper ", description = "Spawns 10 Gold Vex Loyal to you. They despawn after 15 seconds", cooldownTicks = 20 * 30) + @Override + public boolean copperAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 10; i++) { + spawnLoyalVex(player, player.getEyeLocation(), NamedTextColor.GOLD); + } + return true; + } + + @MaterialInfo(name = "Diamond ", description = "Spawns 10 Aqua Vex Loyal to you. They despawn after 15 seconds", cooldownTicks = 20 * 30) + @Override + public boolean diamondAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 10; i++) { + spawnLoyalVex(player, player.getEyeLocation(), NamedTextColor.AQUA); + } + return true; + } + + @MaterialInfo(name = "Emerald", description = "Spawns 10 Emerald Vex Loyal to you. They despawn after 15 seconds", cooldownTicks = 20 * 30) + @Override + public boolean emeraldAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 10; i++) { + spawnLoyalVex(player, player.getEyeLocation(), NamedTextColor.DARK_GREEN); + } + return true; + } + + @MaterialInfo(name = "Gold ", description = "Spawns 10 Yellow Vex Loyal to you. They despawn after 15 seconds", cooldownTicks = 20 * 30) + @Override + public boolean goldAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 10; i++) { + spawnLoyalVex(player, player.getEyeLocation(), NamedTextColor.YELLOW); + } + return true; + } + + @MaterialInfo(name = "Iron ", description = "Spawns 10 Gray Vex Loyal to you. They despawn after 15 seconds", cooldownTicks = 20 * 30) + @Override + public boolean ironAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 10; i++) { + spawnLoyalVex(player, player.getEyeLocation(), NamedTextColor.GRAY); + } + return true; + } + + @MaterialInfo(name = "Lapis ", description = "Spawns 10 Blue Vex Loyal to you. They despawn after 15 seconds", cooldownTicks = 20 * 30) + @Override + public boolean lapisAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 10; i++) { + spawnLoyalVex(player,player.getEyeLocation(),NamedTextColor.DARK_BLUE); + } + return true; + } + + @MaterialInfo(name = "Netherite ", description = "Spawns 15 Dark Vex Loyal to you. They despawn after 15 seconds", cooldownTicks = 20 * 30) + @Override + public boolean netheriteAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 15; i++) { + spawnLoyalVex(player,player.getEyeLocation(),NamedTextColor.DARK_GRAY); + } + return true; + } + + @MaterialInfo(name = "Quartz ", description = "Spawns 10 White Vex Loyal to you. They despawn after 15 seconds", cooldownTicks = 20 * 30) + @Override + public boolean quartzAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 10; i++) { + spawnLoyalVex(player,player.getEyeLocation(),NamedTextColor.WHITE); + } + return true; + } + + @MaterialInfo(name = "Redstone ", description = "Spawns 10 Red Vex Loyal to you. They despawn after 15 seconds", cooldownTicks = 20 * 30) + @Override + public boolean redstoneAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 10; i++) { + spawnLoyalVex(player,player.getEyeLocation(),NamedTextColor.RED); + } + return true; + } + + @MaterialInfo(name = "Resin ", description = "Spawns 10 Gold Vex Loyal to you. They despawn after 15 seconds", cooldownTicks = 20 * 30) + @Override + public boolean resinAbility(Player player) { + player.getWorld().playSound(player, Sound.ENTITY_EVOKER_PREPARE_WOLOLO,10,1); + for (int i = 0; i < 10; i++) { + spawnLoyalVex(player,player.getEyeLocation(),NamedTextColor.GOLD); + } + return true; + } +} diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/WardAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/WardAbility.java new file mode 100644 index 0000000..b265200 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/WardAbility.java @@ -0,0 +1,361 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import io.papermc.paper.event.entity.WardenAngerChangeEvent; +import me.trouper.trimserver.server.events.QuickListener; +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.utils.SoundPlayer; +import me.trouper.trimserver.utils.Text; +import me.trouper.trimserver.utils.Verbose; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.SculkSensor; +import org.bukkit.entity.*; +import org.bukkit.event.EventHandler; +import org.bukkit.event.block.BlockReceiveGameEvent; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerToggleSneakEvent; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.scheduler.BukkitTask; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +@PatternInfo(name = "Warden's Assistant", description = "You glazed the warden so much that he just ignores you now. Wardens and sculk sensors won't detect you.") +public class WardAbility extends AbstractAbility { + + // Map to track active disguises: player UUID -> disguise entity and task + private final Map activeDisguises = new HashMap<>(); + + // Inner class to track disguise data + private static class DisguiseData { + final Warden disguise; + final BukkitTask expirationTask; + + public DisguiseData(Warden disguise, BukkitTask expirationTask) { + this.disguise = disguise; + this.expirationTask = expirationTask; + } + } + + public WardAbility() { + super(TrimPattern.WARD); + } + + private void transformPlayer(Player player) { + Verbose.send( "transformPlayer called for %s; currently disguised? %b", + player.getName(), activeDisguises.containsKey(player.getUniqueId())); + + if (activeDisguises.containsKey(player.getUniqueId())) { + Verbose.send( "Player %s already disguised; removing disguise", player.getName()); + removeDisguise(player); + return; + } + + // spawn and configure warden + Warden warden = (Warden) player.getWorld().spawnEntity(player.getLocation().subtract(0,3,0), EntityType.WARDEN, CreatureSpawnEvent.SpawnReason.NATURAL); + Verbose.send( "Spawned Warden entity %s for %s", warden.getUniqueId(), player.getName()); + warden.setAI(false); + warden.setInvulnerable(false); + warden.setGravity(false); + warden.setSilent(true); + warden.customName(player.name()); + warden.setCustomNameVisible(true); + + player.hideEntity(main.getPlugin(), warden); + for (Player other : Bukkit.getOnlinePlayers()) { + if (!other.equals(player)) other.hidePlayer(main.getPlugin(), player); + } + + player.addPotionEffect(new PotionEffect(PotionEffectType.SLOWNESS,20 * 4,6,true,false,false)); + player.addPotionEffect(new PotionEffect(PotionEffectType.JUMP_BOOST,20 * 4,127,true,false,false)); + new SoundPlayer(player.getLocation(), Sound.ENTITY_WARDEN_EMERGE, 1.0f, 1.0f).playWithin(20); + + BukkitTask expiration = Bukkit.getScheduler().runTaskLater(main.getPlugin(), () -> { + Verbose.send( "Expiration task running for %s", player.getName()); + removeDisguise(player); + }, (20*3) + (20 * 30)); + + Location particleLoc = player.getLocation().clone().add(0.5,0.5,0.5); + AtomicInteger stepsTaken = new AtomicInteger(0); + Bukkit.getScheduler().runTaskTimer(main.getPlugin(),task->{ + if (stepsTaken.getAndIncrement() >= 20*4) { + player.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS,20 * 30, 0, true,false,false)); + player.addPotionEffect(new PotionEffect(PotionEffectType.ABSORPTION,20 * 30, 9,true,false,false)); + activeDisguises.put(player.getUniqueId(), new DisguiseData(warden, expiration)); + task.cancel(); + return; + } + warden.teleport(warden.getLocation().clone().add(0, (double) 3 /(20*4),0)); + particleLoc.getWorld().spawnParticle(Particle.BLOCK_CRUMBLE,particleLoc,20,1,0,1,0.4, Material.SCULK.createBlockData()); + },0,1); + + player.sendMessage(Text.getMessage("You have transformed into the Warden.")); + Verbose.send( "Disguise applied: %s → %s", player.getName(), warden.getUniqueId()); + } + + private void removeDisguise(Player player) { + Verbose.send( "removeDisguise called for %s", player.getName()); + DisguiseData data = activeDisguises.remove(player.getUniqueId()); + if (data == null) { + Verbose.send( "No disguise data found for %s; aborting", player.getName()); + return; + } + + data.disguise.remove(); + Verbose.send( "Removed disguise entity %s", data.disguise.getUniqueId()); + + if (!data.expirationTask.isCancelled()) { + data.expirationTask.cancel(); + Verbose.send( "Cancelled expiration task for %s", player.getName()); + } + + for (Player other : Bukkit.getOnlinePlayers()) { + other.showPlayer(main.getPlugin(), player); + } + player.removePotionEffect(PotionEffectType.BLINDNESS); + player.removePotionEffect(PotionEffectType.ABSORPTION); + new SoundPlayer(player.getLocation(), Sound.ENTITY_WARDEN_DEATH, 1.0f, 1.2f).playWithin(20); + player.sendMessage(Text.getMessage("You have returned to normal form.")); + Verbose.send( "Disguise fully removed for %s", player.getName()); + } + + private void updateDisguisePosition(Player player) { + DisguiseData data = activeDisguises.get(player.getUniqueId()); + if (data != null) { + data.disguise.teleport(player.getLocation()); + Verbose.send( "Teleported disguise %s to %s for player %s", + data.disguise.getUniqueId(), + player.getLocation().toVector(), + player.getName()); + } + } + + private Player getPlayerByDisguise(Entity disguise) { + for (Map.Entry e : activeDisguises.entrySet()) { + if (e.getValue().disguise.equals(disguise)) { + Verbose.send( "Mapped disguise %s → player %s", disguise.getUniqueId(), e.getKey()); + return Bukkit.getPlayer(e.getKey()); + } + } + return null; + } + + private Warden getDisguiseByPlayer(Player player) { + return activeDisguises.getOrDefault(player.getUniqueId(),null).disguise; + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent e) { + Verbose.send( "onPlayerJoin: %s", e.getPlayer().getName()); + for (UUID uid : activeDisguises.keySet()) { + Player disguised = Bukkit.getPlayer(uid); + if (disguised != null && disguised.isOnline()) { + e.getPlayer().hidePlayer(main.getPlugin(), disguised); + Verbose.send( "Hid disguised player %s from joining %s", + disguised.getName(), e.getPlayer().getName()); + } + } + if (activeDisguises.containsKey(e.getPlayer().getUniqueId())) { + e.getPlayer().hideEntity(main.getPlugin(), activeDisguises.get(e.getPlayer().getUniqueId()).disguise); + Verbose.send( "Re-hid own disguise for %s upon rejoin", e.getPlayer().getName()); + } + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent e) { + Verbose.send( "onPlayerQuit: %s", e.getPlayer().getName()); + if (activeDisguises.containsKey(e.getPlayer().getUniqueId())) { + Verbose.send( "Player %s quit while disguised; removing disguise", e.getPlayer().getName()); + removeDisguise(e.getPlayer()); + } + } + + @EventHandler + public void onSensor(BlockReceiveGameEvent e) { + if (e.getBlock().getState() instanceof SculkSensor sensor + && e.getEntity() instanceof Player player) { + + AbstractAbility ability = main.man().abilityBackend.getAbility(player); + if (ability != null && ability.getPattern().equals(TrimPattern.WARD)) { + e.setCancelled(true); + Verbose.send( "Cancelled sculk sensor trigger at %s by %s", + sensor.getBlock().getLocation().toVector(), player.getName()); + } + } + } + + @EventHandler + public void onWarden(WardenAngerChangeEvent e) { + if (e.getTarget() instanceof Player player) { + AbstractAbility ability = main.man().abilityBackend.getAbility(player); + if (ability != null && ability.getPattern().equals(TrimPattern.WARD)) { + e.setCancelled(true); + e.getEntity().setTarget(null); + Verbose.send( "Prevented Warden %s from targeting %s", + e.getEntity().getUniqueId(), player.getName()); + } + } + } + + @EventHandler + public void onPlayerSneak(PlayerToggleSneakEvent e) { + Player player = e.getPlayer(); + if (activeDisguises.containsKey(player.getUniqueId()) && e.isSneaking()) { + Verbose.send( "%s sneaked; playing roar", player.getName()); + + new SoundPlayer(player.getLocation(), Sound.ENTITY_WARDEN_ROAR, 1.0f, 1.0f).playWithin(15); + } + } + + @EventHandler + public void onEntityDamageByEntity(EntityDamageByEntityEvent e) { + if (e.getDamager() instanceof Player attacker) { + if (activeDisguises.containsKey(attacker.getUniqueId())) { + activeDisguises.get(attacker.getUniqueId()).disguise.swingMainHand(); + new SoundPlayer(attacker.getLocation(), Sound.ENTITY_WARDEN_ATTACK_IMPACT, 1.0f, 1.0f) + .playWithin(10); + } + } + if (e.getEntity() instanceof Warden warden) { + Player real = getPlayerByDisguise(warden); + if (real != null) { + Verbose.send( "Redirecting damage from disguise %s to %s", + warden.getUniqueId(), real.getName()); + e.setCancelled(true); + warden.playHurtAnimation(0); + real.damage(e.getDamage(), e.getDamager()); + new SoundPlayer(real.getLocation(), Sound.ENTITY_WARDEN_HURT, 1.0f, 1.0f) + .playWithin(10); + } + } + } + + @EventHandler + public void onEntityDamage(EntityDamageEvent e) { + if (e.getEntity() instanceof Warden warden) { + Player real = getPlayerByDisguise(warden); + if (real != null && e.getCause() != EntityDamageEvent.DamageCause.ENTITY_ATTACK + && e.getCause() != EntityDamageEvent.DamageCause.ENTITY_SWEEP_ATTACK) { + Verbose.send( "Redirecting environmental damage (%s) from %s to %s", + e.getCause(), warden.getUniqueId(), real.getName()); + e.setCancelled(true); + warden.playHurtAnimation(0); + real.damage(e.getDamage()); + new SoundPlayer(real.getLocation(), Sound.ENTITY_WARDEN_HURT, 1.0f, 1.0f) + .playWithin(10); + } + } + } + + @Override + public QuickListener registerEvents() { + Verbose.send( "Scheduling disguise position updater"); + Bukkit.getScheduler().runTaskTimer(main.getPlugin(), () -> { + //Verbose.send( "Tick: updating %d disguises", activeDisguises.size()); + for (UUID uid : activeDisguises.keySet()) { + Player p = Bukkit.getPlayer(uid); + if (p != null && p.isOnline()) { + updateDisguisePosition(p); + } else { + Verbose.send( "Auto-removing stale disguise for %s", uid); + DisguiseData d = activeDisguises.remove(uid); + d.disguise.remove(); + d.expirationTask.cancel(); + } + } + }, 1, 1); + + return super.registerEvents(); + } + + @MaterialInfo(name = "Amethyst Fusion", description = "Transform into a Warden for 30 seconds. While disguised, you gain Warden's strength but suffer from blindness", cooldownTicks = 20 * 60) + @Override + public boolean amethystAbility(Player player) { + transformPlayer(player); + return true; + } + + @MaterialInfo(name = "Copper Fusion", description = "Transform into a Warden for 30 seconds. While disguised, you gain Warden's strength but suffer from blindness", cooldownTicks = 20 * 60) + @Override + public boolean copperAbility(Player player) { + transformPlayer(player); + return true; + } + + @MaterialInfo(name = "Diamond Fusion", description = "Transform into a Warden for 30 seconds. While disguised, you gain Warden's strength but suffer from blindness", cooldownTicks = 20 * 60) + @Override + public boolean diamondAbility(Player player) { + transformPlayer(player); + return true; + } + + @MaterialInfo(name = "Emerald Fusion", description = "Transform into a Warden for 30 seconds. While disguised, you gain Warden's strength but suffer from blindness", cooldownTicks = 20 * 60) + @Override + public boolean emeraldAbility(Player player) { + transformPlayer(player); + return true; + } + + @MaterialInfo(name = "Gold Fusion", description = "Transform into a Warden for 30 seconds. While disguised, you gain Warden's strength but suffer from blindness", cooldownTicks = 20 * 60) + @Override + public boolean goldAbility(Player player) { + transformPlayer(player); + return true; + } + + @MaterialInfo(name = "Iron Fusion", description = "Transform into a Warden for 30 seconds. While disguised, you gain Warden's strength but suffer from blindness", cooldownTicks = 20 * 60) + @Override + public boolean ironAbility(Player player) { + transformPlayer(player); + return true; + } + + @MaterialInfo(name = "Lapis Fusion", description = "Transform into a Warden for 30 seconds. While disguised, you gain Warden's strength but suffer from blindness", cooldownTicks = 20 * 60) + @Override + public boolean lapisAbility(Player player) { + transformPlayer(player); + return true; + } + + @MaterialInfo(name = "Netherite Fusion", description = "Transform into a Warden for 30 seconds. While disguised, you gain Warden's strength but suffer from blindness", cooldownTicks = 20 * 60) + @Override + public boolean netheriteAbility(Player player) { + transformPlayer(player); + return true; + } + + @MaterialInfo(name = "Quartz Fusion", description = "Transform into a Warden for 30 seconds. While disguised, you gain Warden's strength but suffer from blindness", cooldownTicks = 20 * 60) + @Override + public boolean quartzAbility(Player player) { + transformPlayer(player); + return true; + } + + @MaterialInfo(name = "Redstone Fusion", description = "Transform into a Warden for 30 seconds. While disguised, you gain Warden's strength but suffer from blindness", cooldownTicks = 20 * 60) + @Override + public boolean redstoneAbility(Player player) { + transformPlayer(player); + return true; + } + + @MaterialInfo(name = "Resin Fusion", description = "Transform into a Warden for 30 seconds. While disguised, you gain Warden's strength but suffer from blindness", cooldownTicks = 20 * 60) + @Override + public boolean resinAbility(Player player) { + transformPlayer(player); + return true; + } + + + +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/WayfinderAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/WayfinderAbility.java new file mode 100644 index 0000000..b0c0997 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/WayfinderAbility.java @@ -0,0 +1,152 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.utils.SoundPlayer; +import me.trouper.trimserver.utils.TargetingUtils; +import me.trouper.trimserver.utils.visual.DisplayUtils; +import org.bukkit.Bukkit; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.damage.DamageSource; +import org.bukkit.damage.DamageType; +import org.bukkit.entity.*; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +@PatternInfo(name = "Big Step", description = "\"He boot too big for he gotdamn feet.\"") +public class WayfinderAbility extends AbstractAbility { + + public WayfinderAbility() { + super(TrimPattern.WAYFINDER); + } + + public void stomp(Player caster) { + if (caster.getLocation().clone().subtract(0,1,0).getBlock().isPassable()) return; + caster.setVelocity(caster.getVelocity().setY(caster.getVelocity().getY() + 1.5)); + SoundPlayer jump = new SoundPlayer(caster.getLocation(), Sound.ENTITY_ENDER_DRAGON_FLAP, 1, 2); + jump.playWithin(10); + + DisplayUtils.disc(caster.getLocation(),3,0.5,0.25,point->{ + point.getWorld().spawnParticle(Particle.POOF,point,1,0,0,0,0); + }); + + AtomicInteger expiry = new AtomicInteger(80); + Bukkit.getScheduler().runTaskTimer(main.getPlugin(),task -> { + if (expiry.getAndDecrement() <= 0) { + task.cancel(); + return; + } + if (!caster.getLocation().clone().subtract(0,1,0).getBlock().isPassable()) { + + DisplayUtils.wave(caster.getLocation(),10,1,1,point -> { + Block thrown = point.getWorld().getHighestBlockAt((int) point.x(), (int) point.z()); + BlockData data = thrown.getBlockData(); + BlockState state = point.getWorld().getHighestBlockAt((int) point.x(), (int) point.z()).getState(); + + FallingBlock block = (FallingBlock) point.getWorld().spawnEntity(point.clone(), EntityType.FALLING_BLOCK); + block.setBlockData(data); + block.setBlockState(state); + block.setVelocity(new Vector(0,0.1,0)); + block.setCancelDrop(true); + }); + + TargetingUtils.areaAffect(caster.getLocation(),10,target -> !target.isDead() && !target.equals(caster) && !main.man().trustBackend.trusts(caster,target),target->{ + SoundPlayer hit = new SoundPlayer(target.getLocation(), Sound.ENTITY_EVOKER_FANGS_ATTACK, 1, 2); + Vector direction = target.getLocation().toVector().subtract(caster.getEyeLocation().toVector()).normalize(); + target.setVelocity(direction.multiply(0.5).setY(0.3)); + target.damage(15, DamageSource.builder(DamageType.FALLING_BLOCK).build()); + hit.playWithin(10); + caster.getWorld().spawnParticle(Particle.DAMAGE_INDICATOR,target.getLocation().clone().add(0,1,0),10,0.2,1,0.2,0.1); + }); + + task.cancel(); + } + },10,2); + } + + @MaterialInfo(name = "Amethyst ", description = "Jump into the air, creating a seismic disturbance when you land. Deals 5 Damage", cooldownTicks = 20 * 4) + @Override + public boolean amethystAbility(Player player) { + stomp(player); + return true; + } + + @MaterialInfo(name = "Copper ", description = "Jump into the air, creating a seismic disturbance when you land. Deals 5 Damage", cooldownTicks = 20 * 4) + @Override + public boolean copperAbility(Player player) { + stomp(player); + return true; + } + + @MaterialInfo(name = "Diamond ", description = "Jump into the air, creating a seismic disturbance when you land. Deals 5 Damage", cooldownTicks = 20 * 4) + @Override + public boolean diamondAbility(Player player) { + stomp(player); + return true; + } + + @MaterialInfo(name = "Emerald Bolt", description = "Jump into the air, creating a seismic disturbance when you land. Deals 5 Damage", cooldownTicks = 20 * 4) + @Override + public boolean emeraldAbility(Player player) { + stomp(player); + return true; + } + + @MaterialInfo(name = "Gold ", description = "Jump into the air, creating a seismic disturbance when you land. Deals 5 Damage", cooldownTicks = 20 * 4) + @Override + public boolean goldAbility(Player player) { + stomp(player); + return true; + } + + @MaterialInfo(name = "Iron ", description = "Jump into the air, creating a seismic disturbance when you land. Deals 5 Damage", cooldownTicks = 20 * 4) + @Override + public boolean ironAbility(Player player) { + stomp(player); + return true; + } + + @MaterialInfo(name = "Lapis ", description = "Jump into the air, creating a seismic disturbance when you land. Deals 5 Damage", cooldownTicks = 20 * 4) + @Override + public boolean lapisAbility(Player player) { + stomp(player); + return true; + } + + @MaterialInfo(name = "Netherite ", description = "Jump into the air, creating a seismic disturbance when you land. Deals 5 Damage", cooldownTicks = 20 * 3) + @Override + public boolean netheriteAbility(Player player) { + stomp(player); + return true; + } + + @MaterialInfo(name = "Quartz ", description = "Jump into the air, creating a seismic disturbance when you land. Deals 5 Damage", cooldownTicks = 20 * 4) + @Override + public boolean quartzAbility(Player player) { + stomp(player); + return true; + } + + @MaterialInfo(name = "Redstone ", description = "Jump into the air, creating a seismic disturbance when you land. Deals 5 Damage", cooldownTicks = 20 * 4) + @Override + public boolean redstoneAbility(Player player) { + stomp(player); + return true; + } + + @MaterialInfo(name = "Resin ", description = "Jump into the air, creating a seismic disturbance when you land. Deals 5 Damage", cooldownTicks = 20 * 4) + @Override + public boolean resinAbility(Player player) { + stomp(player); + return true; + } +} diff --git a/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/WildAbility.java b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/WildAbility.java new file mode 100644 index 0000000..46d521e --- /dev/null +++ b/src/main/java/me/trouper/trimserver/server/systems/abilities/trims/WildAbility.java @@ -0,0 +1,237 @@ +package me.trouper.trimserver.server.systems.abilities.trims; + +import me.trouper.trimserver.server.systems.abilities.MaterialInfo; +import me.trouper.trimserver.server.systems.abilities.AbstractAbility; +import me.trouper.trimserver.server.systems.abilities.PatternInfo; +import me.trouper.trimserver.utils.PlayerUtils; +import me.trouper.trimserver.utils.TargetingUtils; +import me.trouper.trimserver.utils.visual.BlockDisplayRaytracer; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.damage.DamageSource; +import org.bukkit.damage.DamageType; +import org.bukkit.entity.BlockDisplay; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.meta.trim.TrimPattern; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.List; + +@PatternInfo(name = "Wild Trim", description = "\"GET OVER HERE!\"") +public class WildAbility extends AbstractAbility { + + public WildAbility() { + super(TrimPattern.WILD); + } + + public void pullVine(Player caster, Player target, Material vineMaterial) { + // --- Basic Validation --- + if (caster == null || !caster.isOnline() || target == null || !target.isOnline() || caster.equals(target)) return; + if (caster.getWorld() != target.getWorld()) return; + + + // --- Configuration --- + final Location casterStartLoc = caster.getLocation().clone().add(0,1,0); + final double maxDistance = 20.0; + final double pullSpeed = 0.2; + final double vineThickness = 0.2; + final int pullDurationTicks = 80; + final long updateInterval = 1L; + final double minPullDistance = 2.0; + + // --- Initial Check --- + if (casterStartLoc.distanceSquared(target.getLocation()) > maxDistance * maxDistance) return; + + caster.getWorld().playSound(casterStartLoc, Sound.ENTITY_FISHING_BOBBER_THROW, 1.0f, 0.8f); + + + // --- Task for Pulling & Visuals --- + final List currentVineSegment = new ArrayList<>(1); + + new BukkitRunnable() { + int ticksElapsed = 0; + + @Override + public void run() { + // --- Cancellation Conditions --- + if (ticksElapsed >= pullDurationTicks || + !caster.isOnline() || + !target.isOnline() || + caster.getWorld() != target.getWorld() || + caster.isDead() || target.isDead() + ) { + cancel(); + return; + } + + Location currentCasterPos = caster.getLocation().clone().add(0,1,0); + Location currentTargetPos = target.getLocation().add(0, target.getHeight() / 2.0, 0); + double currentDistance = currentCasterPos.distance(currentTargetPos); + + if (currentDistance < minPullDistance) { + cancel(); + caster.getWorld().playSound(currentCasterPos, Sound.ENTITY_ITEM_PICKUP, 1.0f, 1.0f); + return; + } + + Vector pullDirection = currentCasterPos.toVector().subtract(currentTargetPos.toVector()).normalize(); + Vector pullVelocity = pullDirection.multiply(pullSpeed); + + //pullVelocity.setY(Math.min(pullVelocity.getY(), 0.05)); // Y Clamp + caster.getWorld().playSound(currentCasterPos, Sound.BLOCK_LEAF_LITTER_BREAK, 1.0f, 1.0f); + target.setVelocity(target.getVelocity().add(pullVelocity)); + + + // --- Update Vine Visual --- + + BlockDisplay segment = BlockDisplayRaytracer.trace( + vineMaterial, + currentCasterPos, + currentTargetPos, + vineThickness, + updateInterval + 2 + ); + BlockDisplayRaytracer.trace( + vineMaterial, + currentTargetPos.clone().subtract(0,target.getHeight(),0), + currentTargetPos.clone().add(0,target.getHeight(),0), + target.getWidth() * 1.5, + updateInterval + 1 + ); + + if (segment != null) { + currentVineSegment.add(segment); + } + + ticksElapsed++; + } + }.runTaskTimer(main.getPlugin(), 0L, updateInterval); + } + + @MaterialInfo(name = "Amethyst", description = "Shoot a vine out which wraps around nearby players, pulling them towards you", cooldownTicks = 120) + @Override + public boolean amethystAbility(Player player) { + Player closestTarget = PlayerUtils.playerClosestAngle(player, 40); + if (closestTarget != null) { + pullVine(player, closestTarget, Material.OAK_LEAVES); + return true; + } + return false; + } + + @MaterialInfo(name = "Copper", description = "Shoot a vine out which wraps around nearby players, pulling them towards you", cooldownTicks = 120) + @Override + public boolean copperAbility(Player player) { + Player closestTarget = PlayerUtils.playerClosestAngle(player, 40); + if (closestTarget != null) { + pullVine(player, closestTarget, Material.OAK_LEAVES); + return true; + } + return false; + } + + @MaterialInfo(name = "Diamond", description = "Shoot a vine out which wraps around nearby players, pulling them towards you", cooldownTicks = 120) + @Override + public boolean diamondAbility(Player player) { + Player closestTarget = PlayerUtils.playerClosestAngle(player, 40); + if (closestTarget != null) { + pullVine(player, closestTarget, Material.OAK_LEAVES); + return true; + } + return false; + } + + @MaterialInfo(name = "Emerald", description = "Shoot a vine out which wraps around nearby players, pulling them towards you", cooldownTicks = 120) + @Override + public boolean emeraldAbility(Player player) { + Player closestTarget = PlayerUtils.playerClosestAngle(player, 40); + if (closestTarget != null) { + pullVine(player, closestTarget, Material.OAK_LEAVES); + return true; + } + return false; + } + + @MaterialInfo(name = "Gold", description = "Shoot a vine out which wraps around nearby players, pulling them towards you", cooldownTicks = 120) + @Override + public boolean goldAbility(Player player) { + Player closestTarget = PlayerUtils.playerClosestAngle(player, 40); + if (closestTarget != null) { + pullVine(player, closestTarget, Material.OAK_LEAVES); + return true; + } + return false; + } + + @MaterialInfo(name = "Iron", description = "Shoot a vine out which wraps around nearby players, pulling them towards you", cooldownTicks = 120) + @Override + public boolean ironAbility(Player player) { + Player closestTarget = PlayerUtils.playerClosestAngle(player, 40); + if (closestTarget != null) { + pullVine(player, closestTarget, Material.OAK_LEAVES); + return true; + } + return false; + } + + @MaterialInfo(name = "Lapis", description = "Shoot a vine out which wraps around nearby players, pulling them towards you", cooldownTicks = 120) + @Override + public boolean lapisAbility(Player player) { + Player closestTarget = PlayerUtils.playerClosestAngle(player, 40); + if (closestTarget != null) { + pullVine(player, closestTarget, Material.OAK_LEAVES); + return true; + } + return false; + } + + @MaterialInfo(name = "Netherite", description = "Shoot a steel vine out which wraps around nearby players, pulling them towards you and choking them. (7 damage)", cooldownTicks = 80) + @Override + public boolean netheriteAbility(Player player) { + Player closestTarget = PlayerUtils.playerClosestAngle(player, 40); + if (closestTarget != null) { + pullVine(player, closestTarget, Material.OAK_LEAVES); + closestTarget.damage(7, DamageSource.builder(DamageType.IN_WALL).withDamageLocation(player.getEyeLocation()).withDirectEntity(player).build()); + return true; + } + return false; + } + + @MaterialInfo(name = "Quartz", description = "Shoot a vine out which wraps around nearby players, pulling them towards you", cooldownTicks = 120) + @Override + public boolean quartzAbility(Player player) { + Player closestTarget = PlayerUtils.playerClosestAngle(player, 40); + if (closestTarget != null) { + pullVine(player, closestTarget, Material.OAK_LEAVES); + return true; + } + return false; + } + + @MaterialInfo(name = "Redstone", description = "Shoot a vine out which wraps around nearby players, pulling them towards you", cooldownTicks = 120) + @Override + public boolean redstoneAbility(Player player) { + Player closestTarget = PlayerUtils.playerClosestAngle(player, 40); + if (closestTarget != null) { + pullVine(player, closestTarget, Material.OAK_LEAVES); + return true; + } + return false; + } + + @MaterialInfo(name = "Resin", description = "Shoot a vine out which wraps around nearby players, pulling them towards you", cooldownTicks = 120) + @Override + public boolean resinAbility(Player player) { + Player closestTarget = PlayerUtils.playerClosestAngle(player, 40); + if (closestTarget != null) { + pullVine(player, closestTarget, Material.OAK_LEAVES); + return true; + } + return false; + } + +} diff --git a/src/main/java/me/trouper/trimserver/utils/ItemUtils.java b/src/main/java/me/trouper/trimserver/utils/ItemUtils.java new file mode 100644 index 0000000..302c296 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/ItemUtils.java @@ -0,0 +1,129 @@ +package me.trouper.trimserver.utils; + +import com.google.common.collect.Multimap; +import org.bukkit.Material; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeModifier; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.List; +import java.util.Map; + +public class ItemUtils { + + public static boolean isArmor(ItemStack i) { + if (i == null || i.isEmpty()) return false; + + return isHelmet(i) || isChestplate(i) || isLeggings(i) || isBoots(i); + } + + public static boolean isHelmet(ItemStack i) { + if (i == null || i.isEmpty()) return false; + + Material m = i.getType(); + String n = m.name(); + return n.contains("HELMET"); + } + + public static boolean isChestplate(ItemStack i) { + if (i == null || i.isEmpty()) return false; + + Material m = i.getType(); + String n = m.name(); + return n.contains("CHESTPLATE"); + } + + public static boolean isLeggings(ItemStack i) { + if (i == null || i.isEmpty()) return false; + + Material m = i.getType(); + String n = m.name(); + return n.contains("LEGGINGS"); + } + + public static boolean isBoots(ItemStack i) { + if (i == null || i.isEmpty()) return false; + + Material m = i.getType(); + String n = m.name(); + return n.contains("BOOTS"); + } + + @SuppressWarnings("deprecation") + public static boolean isSimilar(ItemStack item1, ItemStack item2) { + if (item1 == null || item2 == null) { + Verbose.send("One of the items is null: item1: %s, item2: %s", item1, item2); + return false; + } + + Material type1 = item1.getType(); + Material type2 = item2.getType(); + boolean typeEqual = type1 == type2; + Verbose.send("Checking Material: item1 type: %s, item2 type: %s, equal: %s", type1, type2, typeEqual); + if (!typeEqual) return false; + + boolean hasMeta1 = item1.hasItemMeta(); + boolean hasMeta2 = item2.hasItemMeta(); + boolean metaExistEqual = (hasMeta1 == hasMeta2); + Verbose.send("Checking ItemMeta existence: item1 has meta: %s, item2 has meta: %s, equal: %s", hasMeta1, hasMeta2, metaExistEqual); + if (!metaExistEqual) return false; + + if (!hasMeta1 && !hasMeta2) { + return true; + } + + ItemMeta meta1 = item1.getItemMeta(); + ItemMeta meta2 = item2.getItemMeta(); + + String name1 = meta1.hasDisplayName() ? meta1.getDisplayName() : null; + String name2 = meta2.hasDisplayName() ? meta2.getDisplayName() : null; + if (name1 == null ^ name2 == null) { + Verbose.send("Custom Name mismatch: item1 name: %s, item2 name: %s", name1, name2); + return false; + } + boolean nameEqual = (name1 == null || name1.equals(name2)); + Verbose.send("Checking Custom Name: item1: %s, item2: %s, equal: %s", name1, name2, nameEqual); + if (!nameEqual) return false; + + List lore1 = meta1.hasLore() ? meta1.getLore() : null; + List lore2 = meta2.hasLore() ? meta2.getLore() : null; + if (lore1 == null ^ lore2 == null) { + Verbose.send("Lore mismatch: item1 lore: %s, item2 lore: %s", lore1, lore2); + return false; + } + boolean loreEqual = (lore1 == null || lore1.equals(lore2)); + Verbose.send("Checking Lore: item1: %s, item2: %s, equal: %s", lore1, lore2, loreEqual); + if (!loreEqual) return false; + + int cmd1 = meta1.hasCustomModelData() ? meta1.getCustomModelData() : -1; + int cmd2 = meta2.hasCustomModelData() ? meta2.getCustomModelData() : -1; + boolean cmdEqual = (cmd1 == cmd2); + Verbose.send("Checking Custom Model Data: item1: %d, item2: %d, equal: %s", cmd1, cmd2, cmdEqual); + if (!cmdEqual) return false; + + Map enchants1 = meta1.getEnchants(); + Map enchants2 = meta2.getEnchants(); + if (enchants1 == null ^ enchants2 == null) { + Verbose.send("Enchantments mismatch: item1 enchants: %s, item2 enchants: %s", enchants1, enchants2); + return false; + } + boolean enchantsEqual = (enchants1 == null || enchants1.equals(enchants2)); + Verbose.send("Checking Enchantments: item1: %s, item2: %s, equal: %s", enchants1, enchants2, enchantsEqual); + if (!enchantsEqual) return false; + + Multimap modifiers1 = meta1.getAttributeModifiers(); + Multimap modifiers2 = meta2.getAttributeModifiers(); + if (modifiers1 == null ^ modifiers2 == null) { + Verbose.send("Attribute Modifiers mismatch: item1 modifiers: %s, item2 modifiers: %s", modifiers1, modifiers2); + return false; + } + boolean modifiersEqual = (modifiers1 == null || modifiers1.equals(modifiers2)); + Verbose.send("Checking Attribute Modifiers: item1: %s, item2: %s, equal: %s", modifiers1, modifiers2, modifiersEqual); + if (!modifiersEqual) return false; + + return true; + } + +} diff --git a/src/main/java/me/trouper/trimserver/utils/PlayerUtils.java b/src/main/java/me/trouper/trimserver/utils/PlayerUtils.java new file mode 100644 index 0000000..5aa738a --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/PlayerUtils.java @@ -0,0 +1,92 @@ +package me.trouper.trimserver.utils; + +import me.trouper.trimserver.server.Main; +import org.bukkit.Location; +import org.bukkit.damage.DamageSource; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.util.Vector; + +public class PlayerUtils implements Main { + + public static final int DEFAULT_NO_DAMAGE_TICKS = 9; + public static final int DEFAULT_MAX_NO_DAMAGE_TICKS = 10; + public static final int NO_DAMAGE_TICKS = 1; + public static final int MAX_NO_DAMAGE_TICKS = 2; + + public static void addRandomLookOffset(Player player, float maxOffsetDegrees, boolean modifyPitch, boolean modifyYaw) { + float currentYaw = player.getLocation().getYaw(); + float currentPitch = player.getLocation().getPitch(); + + float newYaw = currentYaw; + float newPitch = currentPitch; + + if (modifyYaw) { + float yawOffset = (random.nextFloat() * 2 - 1) * maxOffsetDegrees; + newYaw += yawOffset; + newYaw = wrapYaw(newYaw); + } + + if (modifyPitch) { + float pitchOffset = (random.nextFloat() * 2 - 1) * maxOffsetDegrees; + newPitch += pitchOffset; + newPitch = clampPitch(newPitch); + } + + + player.getLocation().setPitch(newPitch); + player.getLocation().setYaw(newYaw); + //player.set(player.getLocation().setDirection(getDirection(newYaw, newPitch))); + } + + private static float clampPitch(float pitch) { + return Math.max(-90, Math.min(90, pitch)); + } + + private static float wrapYaw(float yaw) { + yaw = yaw % 360; + if (yaw < 0) yaw += 360; + return yaw; + } + + private static Vector getDirection(float yaw, float pitch) { + double radYaw = Math.toRadians(yaw); + double radPitch = Math.toRadians(pitch); + double x = -Math.cos(radPitch) * Math.sin(radYaw); + double y = -Math.sin(radPitch); + double z = Math.cos(radPitch) * Math.cos(radYaw); + return new Vector(x, y, z); + } + + public static Player playerClosestAngle(Player player, double range) { + Vector playerDirection = player.getEyeLocation().getDirection().normalize(); + Location eyeLoc = player.getEyeLocation(); + + return player.getNearbyEntities(range, range, range).stream() + .filter(entity -> entity instanceof Player && !entity.equals(player)) + .map(entity -> (Player) entity) + .min((p1, p2) -> { + Vector dirToP1 = p1.getEyeLocation().toVector().subtract(eyeLoc.toVector()).normalize(); + Vector dirToP2 = p2.getEyeLocation().toVector().subtract(eyeLoc.toVector()).normalize(); + + double angle1 = playerDirection.angle(dirToP1); + double angle2 = playerDirection.angle(dirToP2); + + return Double.compare(angle1, angle2); + }) + .orElse(null); + } + + @SuppressWarnings("deprecation") + public static void dealTrueDamage(LivingEntity target, DamageSource source, double amount) { + double newHealth = target.getHealth() - amount; + target.damage(0.1, source); + if (newHealth <= 0) { + target.setHealth(0); + } else { + target.setHealth(newHealth); + } + } +} diff --git a/src/main/java/me/trouper/trimserver/utils/SoundPlayer.java b/src/main/java/me/trouper/trimserver/utils/SoundPlayer.java new file mode 100644 index 0000000..f91df8e --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/SoundPlayer.java @@ -0,0 +1,266 @@ +package me.trouper.trimserver.utils; + +import me.trouper.trimserver.server.Main; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +public class SoundPlayer implements Main { + + private Location location; + private Sound sound; + private float volume; + private float pitch; + + /** + * Constructs a new sound, this aims to add more methods to + * the Bukkit APIs Sound class, as they don't have many + * methods to use. + * + * @param location Location + * @param sound Sound + * @param volume float + * @param pitch float + */ + public SoundPlayer(Location location, Sound sound, float volume, float pitch) { + this.location = location; + this.sound = sound; + this.pitch = pitch; + this.volume = volume; + } + + + /** + * Plays a sound to a player but at the store location + * + * @param player Player + */ + public void play(Player player) { + player.playSound(this.location,this.sound,this.volume,this.pitch); + } + + /** + * Plays a sound to a player but at the player's location + * + * @param player Player + */ + public void playIndividually(Player player) { + player.playSound(player.getLocation(),this.sound,this.volume,this.pitch); + } + + /** + * Plays the sound to all players within a distance, but at the stored location. + * + * @param distance double + */ + public void playWithin(double distance) { + for (Player p : Bukkit.getOnlinePlayers()) { + if (p != null && p.getWorld() == this.location.getWorld() && p.getLocation().distance(this.location) < distance) { + p.playSound(this.location,this.sound,this.volume,this.pitch); + } + } + } + + /** + * Plays the sound to all players within a distance, but at the players' location. + * + * @param distance double + */ + public void playWithinIndividually(double distance) { + for (Player p : Bukkit.getOnlinePlayers()) { + if (p != null && p.getWorld() == this.location.getWorld() && p.getLocation().distance(this.location) < distance) { + p.playSound(p.getLocation(),this.sound,this.volume,this.pitch); + } + } + } + + + /** + * Plays the sound to all players on the server, but at the stored location. + */ + public void playAll() { + for (Player p : Bukkit.getOnlinePlayers()) p.playSound(this.location,this.sound,this.volume,this.pitch); + } + + /** + * Plays the sound to all players on the server, but at the players' location. + */ + public void playAllIndividually() { + for (Player p : Bukkit.getOnlinePlayers()) p.playSound(p.getLocation(),this.sound,this.volume,this.pitch); + } + + /** + * Repeats a sound to a player, but at the stored location. + * + * @param player Player + * @param times int + * @param tickDelay int + */ + public void repeat(Player player, int times, int tickDelay) { + new BukkitRunnable() { + int i = 0; + @Override + public void run() { + if (i < times) { + play(player); + i ++; + } else { + this.cancel(); + } + } + }.runTaskTimer(getPlugin(),0,tickDelay); + } + + /** + * Repeats a sound to a player, but at the player's location. + * + * @param player Player + * @param times int + * @param tickDelay int + */ + public void repeatIndividually(Player player, int times, int tickDelay) { + new BukkitRunnable() { + int i = 0; + @Override + public void run() { + if (i < times) { + playIndividually(player); + i ++; + } else { + this.cancel(); + } + } + }.runTaskTimer(getPlugin(),0,tickDelay); + } + + /** + * Repeats a sound to all players on the server, but at the stored location. + * + * @param times int + * @param tickDelay int + */ + public void repeatAll(int times, int tickDelay) { + new BukkitRunnable() { + int i = 0; + @Override + public void run() { + if (i < times) { + playAll(); + i ++; + } else { + this.cancel(); + } + } + }.runTaskTimer(getPlugin(),0,tickDelay); + } + + /** + * Repeats a sound to all players on the server, but at the players' location. + * + * @param times int + * @param tickDelay int + */ + public void repeatAllIndividually(int times, int tickDelay) { + new BukkitRunnable() { + int i = 0; + @Override + public void run() { + if (i < times) { + playAllIndividually(); + i ++; + } else { + this.cancel(); + } + } + }.runTaskTimer(getPlugin(),0,tickDelay); + } + + /** + * Repeats a sound to all players within a radius, but at the stored location. + * + * @param radius double + * @param times int + * @param tickDelay int + */ + public void repeatAll(double radius,int times, int tickDelay) { + new BukkitRunnable() { + int i = 0; + @Override + public void run() { + if (i < times) { + playWithin(radius); + i ++; + } else { + this.cancel(); + } + } + }.runTaskTimer(getPlugin(),0,tickDelay); + } + + /** + * Repeats a sound to all players within a radius, but at the players' location. + * + * @param distance double + * @param times int + * @param tickDelay int + */ + public void repeatAllIndividually(double distance, int times, int tickDelay) { + new BukkitRunnable() { + int i = 0; + @Override + public void run() { + if (i < times) { + playWithinIndividually(distance); + i ++; + } else { + this.cancel(); + } + } + }.runTaskTimer(getPlugin(),0,tickDelay); + } + + public Sound getSound() { + return sound; + } + + public float getPitch() { + return pitch; + } + + public float getVolume() { + return volume; + } + + public Location getLocation() { + return location; + } + + public void setPitch(float pitch) { + this.pitch = pitch; + } + + public void setVolume(float volume) { + this.volume = volume; + } + + public void setSound(Sound sound) { + this.sound = sound; + } + + public void setLocation(Location location) { + this.location = location; + } + + public void changePlayer(Location location, Sound sound, float volume, float pitch) { + this.location = location; + this.sound = sound; + this.volume = volume; + this.pitch = pitch; + } + + public void changePlayer(Sound sound, float volume, float pitch) { + changePlayer(location, sound, volume, pitch); + } +} diff --git a/src/main/java/me/trouper/trimserver/utils/TargetingUtils.java b/src/main/java/me/trouper/trimserver/utils/TargetingUtils.java new file mode 100644 index 0000000..d91fe3b --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/TargetingUtils.java @@ -0,0 +1,282 @@ +package me.trouper.trimserver.utils; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class TargetingUtils { + + /** + * Applies an action to all living entities within a specified cuboid area that match a given filter. + * + * @param center The center of the cuboid area. + * @param xRadius The half-length of the cuboid along the X-axis. + * @param yRadius The half-length of the cuboid along the Y-axis. + * @param zRadius The half-length of the cuboid along the Z-axis. + * @param filter A predicate to filter which living entities are affected. + * @param action An action to perform on each targeted living entity. + * @return true if any targets were found, false if it missed. + */ + public static boolean areaAffect(Location center, double xRadius, double yRadius, double zRadius, Predicate filter, Consumer action) { + World world = center.getWorld(); + if (world == null) { + return false; + } + + List targets = new ArrayList<>(world.getNearbyLivingEntities(center, xRadius, yRadius, zRadius).stream().filter(filter).toList()); + targets.forEach(action); + return !targets.isEmpty(); + } + + /** + * Applies an action to all living entities within a specified spherical area (approximated by a cube) that match a given filter. + * + * @param center The center of the area. + * @param radius The radius of the spherical area. + * @param filter A predicate to filter which living entities are affected. + * @param action An action to perform on each targeted living entity. + * @return true if any targets were found, false if it missed. + */ + public static boolean areaAffect(Location center, double radius, Predicate filter, Consumer action) { + + World world = center.getWorld(); + if (world == null) { + return false; + } + + List targets = new ArrayList<>(world.getNearbyLivingEntities(center, radius).stream() + .filter(livingEntity -> livingEntity.getLocation().distanceSquared(center) <= radius*radius) + .filter(filter) + .toList()); + targets.forEach(action); + + return !targets.isEmpty(); + } + + /** + * Attempts to find a passable ground location based on the given initial location. + *

+ * This method checks if the current block is non-passable and searches upward (up to 10 blocks) + * until it finds a passable block. Then, it searches downward (up to 25 blocks) for the first + * non-passable block beneath it, which would be considered solid ground. + *

+ * If a valid ground location cannot be determined within the search bounds, + * the original location is returned. + * + * @param initialLocation The starting location to begin the ground-finding process. Must not be null. + * @return A new {@link Location} object representing the nearest solid ground position, + * or the original location if no valid position is found or world is null. + */ + public static Location findGroundLocation(Location initialLocation) { + if (initialLocation == null) { + return null; + } + World world = initialLocation.getWorld(); + if (world == null) return initialLocation; + + Location loc = initialLocation.clone(); + + int attempts = 0; + while (!world.getBlockAt(loc).isPassable() && attempts < 10) { + loc.add(0, 1, 0); + attempts++; + if (loc.getBlockY() >= world.getMaxHeight()) { + return initialLocation; + } + } + if (!world.getBlockAt(loc).isPassable()) return initialLocation; + + attempts = 0; + while (world.getBlockAt(loc).isPassable() && attempts < 25) { + Location belowLoc = loc.clone().subtract(0, 1, 0); + if (belowLoc.getBlockY() <= world.getMinHeight()) { + return world.getBlockAt(belowLoc).isPassable() ? initialLocation : loc; + } + if (!world.getBlockAt(belowLoc).isPassable()) { + return loc; + } + loc.subtract(0, 1, 0); + attempts++; + } + return initialLocation; + } + + /** + * Finds the closest living entity to a central location within a maximum distance. + * + * @param center The central location from which to search. Must not be null. + * @param maxDistance The maximum distance (radius of a sphere) to search for entities. + * @return An {@link Optional} containing the closest {@link LivingEntity}, or an empty Optional if no entity is found or world is null. + */ + public static Optional getClosestLivingEntity(Location center, double maxDistance) { + return getClosestLivingEntity(center, maxDistance, entity -> true); + } + + /** + * Finds the closest living entity to a central location within a maximum distance, matching a given filter. + * + * @param center The central location from which to search. Must not be null. + * @param maxDistance The maximum distance (radius of a sphere) to search for entities. + * @param filter A predicate to apply additional filtering criteria to the entities. Must not be null. + * @return An {@link Optional} containing the closest {@link LivingEntity} matching the criteria, + * or an empty Optional if no such entity is found or world is null. + */ + public static Optional getClosestLivingEntity(Location center, double maxDistance, Predicate filter) { + if (center == null || center.getWorld() == null || filter == null) { + return Optional.empty(); + } + World world = center.getWorld(); + double maxDistanceSquared = maxDistance * maxDistance; + + return world.getNearbyLivingEntities(center, maxDistance, maxDistance, maxDistance, filter).stream() + .filter(entity -> entity.getLocation().distanceSquared(center) <= maxDistanceSquared) + .min(Comparator.comparingDouble(entity -> entity.getLocation().distanceSquared(center))); + } + + /** + * Finds the closest player to a central location within a maximum distance. + * + * @param center The central location from which to search. Must not be null. + * @param maxDistance The maximum distance (radius of a sphere) to search for players. + * @return An {@link Optional} containing the closest {@link Player}, or an empty Optional if no player is found or world is null. + */ + public static Optional getClosestPlayer(Location center, double maxDistance) { + return getClosestPlayer(center, maxDistance, player -> true); + } + + /** + * Finds the closest player to a central location within a maximum distance, matching a given filter. + * + * @param center The central location from which to search. Must not be null. + * @param maxDistance The maximum distance (radius of a sphere) to search for players. + * @param filter A predicate to apply additional filtering criteria to the players. Must not be null. + * @return An {@link Optional} containing the closest {@link Player} matching the criteria, + * or an empty Optional if no such player is found or world is null. + */ + public static Optional getClosestPlayer(Location center, double maxDistance, Predicate filter) { + if (center == null || center.getWorld() == null || filter == null) { + return Optional.empty(); + } + World world = center.getWorld(); + double maxDistanceSquared = maxDistance * maxDistance; + + List nearbyPlayers = world.getPlayers().stream() + .filter(player -> player.getWorld().equals(world)) + .filter(player -> player.getLocation().distanceSquared(center) <= maxDistanceSquared) + .filter(filter) + .collect(Collectors.toList()); + + return nearbyPlayers.stream() + .min(Comparator.comparingDouble(player -> player.getLocation().distanceSquared(center))); + } + + /** + * Finds the living entity with the lowest health within a given radius of a central location. + * + * @param center The central location from which to search. Must not be null. + * @param radius The radius (sphere) to search for entities. + * @return An {@link Optional} containing the {@link LivingEntity} with the lowest health, + * or an empty Optional if no entity is found or world is null. + */ + public static Optional getLowestHealthLivingEntity(Location center, double radius) { + return getLowestHealthLivingEntity(center, radius, entity -> true); + } + + /** + * Finds the living entity with the lowest health within a given radius of a central location, matching a given filter. + * + * @param center The central location from which to search. Must not be null. + * @param radius The radius (sphere) to search for entities. + * @param filter A predicate to apply additional filtering criteria. Must not be null. + * @return An {@link Optional} containing the {@link LivingEntity} with the lowest health matching the criteria, + * or an empty Optional if no such entity is found or world is null. + */ + public static Optional getLowestHealthLivingEntity(Location center, double radius, Predicate filter) { + if (center == null || center.getWorld() == null || filter == null) { + return Optional.empty(); + } + World world = center.getWorld(); + double radiusSquared = radius * radius; + + return world.getNearbyLivingEntities(center, radius, radius, radius, filter).stream() + .filter(entity -> entity.getLocation().distanceSquared(center) <= radiusSquared) + .min(Comparator.comparingDouble(LivingEntity::getHealth)); + } + + /** + * Finds the living entity whose eye location is closest (by angle) to a given direction vector from an origin. + * This method is useful for simulating line-of-sight or aim-based targeting. + * + * @param originEyeLocation The starting location (e.g., an entity's eye location). Must not be null. + * @param direction The normalized direction vector of the search. Must not be null. + * @param maxDistance The maximum distance to search for entities. + * @param maxAngleRadians The maximum allowed angle (in radians) between the direction vector and the vector to the target's eye location. + * A smaller angle means the target is more directly in the line of sight. + * @return An {@link Optional} containing the {@link LivingEntity} closest to the aim vector, + * or an empty Optional if no suitable entity is found or world is null. + */ + public static Optional getLivingEntityClosestToVector(Location originEyeLocation, Vector direction, double maxDistance, double maxAngleRadians) { + return getLivingEntityClosestToVector(originEyeLocation, direction, maxDistance, maxAngleRadians, entity -> true); + } + + /** + * Finds the living entity whose eye location is closest (by angle) to a given direction vector from an origin, matching a filter. + * This method is useful for simulating line-of-sight or aim-based targeting. + * + * @param originEyeLocation The starting location (e.g., an entity's eye location). Must not be null. + * @param direction The normalized direction vector of the search. Must not be null. Its magnitude does not matter as it will be normalized. + * @param maxDistance The maximum distance to search for entities. + * @param maxAngleRadians The maximum allowed angle (in radians) between the direction vector and the vector to the target's eye location. + * A smaller angle means the target is more directly in the line of sight. + * @param filter A predicate to apply additional filtering criteria. Must not be null. + * @return An {@link Optional} containing the {@link LivingEntity} closest to the aim vector and matching the filter, + * or an empty Optional if no suitable entity is found or world is null. + */ + public static Optional getLivingEntityClosestToVector(Location originEyeLocation, Vector direction, double maxDistance, double maxAngleRadians, Predicate filter) { + if (originEyeLocation == null || originEyeLocation.getWorld() == null || direction == null || filter == null) { + return Optional.empty(); + } + World world = originEyeLocation.getWorld(); + Vector normalizedDirection = direction.clone().normalize(); + + List candidates = world.getNearbyLivingEntities(originEyeLocation, maxDistance, maxDistance, maxDistance, entity -> { + if (originEyeLocation.equals(entity.getEyeLocation())) { + return false; + } + return filter.test(entity); + }).stream().toList(); + + LivingEntity bestTarget = null; + double smallestAngle = maxAngleRadians + 1.0; + + for (LivingEntity entity : candidates) { + if (entity.getEyeLocation().distanceSquared(originEyeLocation) > maxDistance * maxDistance) { + continue; + } + + Vector vectorToTarget = entity.getEyeLocation().toVector().subtract(originEyeLocation.toVector()); + if (vectorToTarget.lengthSquared() == 0) { + continue; + } + vectorToTarget.normalize(); + + double angle = normalizedDirection.angle(vectorToTarget); + + if (angle <= maxAngleRadians && angle < smallestAngle) { + smallestAngle = angle; + bestTarget = entity; + } + } + return Optional.ofNullable(bestTarget); + } +} diff --git a/src/main/java/me/trouper/trimserver/utils/Text.java b/src/main/java/me/trouper/trimserver/utils/Text.java new file mode 100644 index 0000000..ba87335 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/Text.java @@ -0,0 +1,285 @@ +package me.trouper.trimserver.utils; + +import me.trouper.trimserver.server.Main; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.bukkit.Sound; +import org.bukkit.SoundCategory; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Text implements Main { + + public static String legacyColor(String msg) { + return msg.replaceAll("&","§"); + } + + public static Component color(String msg) { + return LegacyComponentSerializer.legacyAmpersand().deserialize(msg); + } + + public static void sendWarning(CommandSender sender, String warning, Object... args) { + sendMessage(Pallet.WARNING, sender, warning, args); + } + + public static void sendError(CommandSender sender, String error, Object... args) { + sendMessage(Pallet.ERROR, sender, error, args); + } + + public static void sendMessage(CommandSender sender, String text, Object... args) { + sendMessage(Pallet.NEUTRAL, sender, text, args); + } + + public static void sendMessage(Pallet pallet, CommandSender sender, String text, Object... args) { + text = formatArgs(pallet, text, args); + sendMessage(sender, text); + if (sender instanceof Player p) p.playSound(p.getLocation(),pallet.sound.sound, SoundCategory.VOICE,10f,pallet.sound.pitch); + } + + public static void sendMessage(CommandSender sender, String text) { + Component message = getMessage(text); + sender.sendMessage(message); + } + + public static Component getMessage(Pallet pallet, String text, Object... args) { + return getMessage(formatArgs(pallet, text, args)); + } + + public static Component getMessage(String text) { + if (main.config().messages.fancyAlerts) { + return formatFancyMessage(text); + } else { + return color(main.config().messages.prefix + text); + } + } + + public static String formatArgs(Pallet pallet, String format, Object... args) { + Component message = Component.empty(); + Pattern pattern = Pattern.compile("\\{(\\d+)}"); + Matcher matcher = pattern.matcher(format); + int lastIndex = 0; + + while (matcher.find()) { + String prefix = format.substring(lastIndex, matcher.start()); + if (!prefix.isEmpty()) { + message = message.append(Component.text(prefix).color(pallet.mainText)); + } + + int argIndex = Integer.parseInt(matcher.group(1)); + TextColor argColor = getArgColor(pallet, argIndex); + + if (argIndex >= 0 && argIndex < args.length) { + String argText = args[argIndex].toString(); + message = message.append(Component.text(argText).color(argColor)); + } else { + message = message.append(Component.text(matcher.group()).color(pallet.mainText)); + } + + lastIndex = matcher.end(); + } + + String suffix = format.substring(lastIndex); + if (!suffix.isEmpty()) { + message = message.append(Component.text(suffix).color(pallet.mainText)); + } + + return LegacyComponentSerializer.legacyAmpersand().serialize(message); + } + + public static Component formatFancyMessage(String text) { + Component message = Component.empty().appendNewline(); + + List wrappedLines = wrapText(text, 50, (int) Math.round((main.config().messages.pluginName.length() + 3) * 1.3)); + + message = message + .append(color(main.config().messages.mainColor + "| ").decorate(TextDecoration.BOLD)) + .append(Component.text(main.config().messages.pluginName + " ", NamedTextColor.WHITE, TextDecoration.BOLD)) + .append(color(wrappedLines.getFirst())); + + String active = getActiveFormatting(wrappedLines.getFirst()); + + wrappedLines.removeFirst(); + + for (String wrappedLine : wrappedLines) { + wrappedLine = active + wrappedLine; + + active = getActiveFormatting(wrappedLine); + message = message + .appendNewline() + .append(color(main.config().messages.mainColor + "| ").decorate(TextDecoration.BOLD)) + .append(color(wrappedLine)); + } + + return message.appendNewline(); + } + + public static List wrapText(String text, int maxLineLength, int offset) { + List lines = new ArrayList<>(); + + if (text == null || text.isEmpty() || maxLineLength <= 0) { + return lines; + } + + String[] words = text.split("\\s+"); + StringBuilder currentLine = new StringBuilder(); + int currentLineLength = offset; + + for (String word : words) { + if (currentLineLength + word.length() + 1 > maxLineLength) { + lines.add(currentLine.toString()); + currentLine = new StringBuilder(); + currentLineLength = 0; + } + + if (!currentLine.isEmpty()) { + currentLine.append(" "); + currentLineLength++; + } + + currentLine.append(word); + currentLineLength += word.length(); + } + + if (!currentLine.isEmpty()) { + lines.add(currentLine.toString()); + } + + return lines; + } + + public static String getActiveFormatting(String text) { + final Pattern pattern = Pattern.compile("&[0-9a-fk-or]"); + final Matcher matcher = pattern.matcher(text); + + String lastColor = ""; + Set activeFormats = new HashSet<>(); + + while (matcher.find()) { + String code = matcher.group(); + char identifier = code.charAt(1); + + if (identifier >= '0' && identifier <= '9' || identifier >= 'a' && identifier <= 'f') { + lastColor = code; + activeFormats.clear(); + } else if (identifier >= 'k' && identifier <= 'o') { + activeFormats.add(identifier); + } else if (identifier == 'r') { + lastColor = ""; + activeFormats.clear(); + } + } + + StringBuilder result = new StringBuilder(lastColor); + for (char format : activeFormats) { + result.append("&").append(format); + } + + return result.toString(); + } + + private static TextColor getArgColor(Pallet pallet, int argIndex) { + return switch (argIndex) { + case 1 -> pallet.arg2; + case 2 -> pallet.arg3; + default -> pallet.argDefault; + }; + } + + public enum Pallet { + ERROR( + NamedTextColor.RED, + NamedTextColor.YELLOW, + NamedTextColor.GOLD, + NamedTextColor.DARK_RED, + new SoundData(Sound.BLOCK_NOTE_BLOCK_BASS,1)), + WARNING( + NamedTextColor.YELLOW, + NamedTextColor.GOLD, + NamedTextColor.RED, + NamedTextColor.DARK_RED, + new SoundData(Sound.BLOCK_NOTE_BLOCK_BIT,0.5F)), + INFO( + NamedTextColor.GRAY, + NamedTextColor.WHITE, + NamedTextColor.AQUA, + NamedTextColor.DARK_AQUA, + new SoundData(Sound.BLOCK_NOTE_BLOCK_BELL,1)), + SUCCESS( + NamedTextColor.GREEN, + NamedTextColor.DARK_GREEN, + NamedTextColor.YELLOW, + NamedTextColor.GOLD, + new SoundData(Sound.BLOCK_NOTE_BLOCK_CHIME,1)), + NEUTRAL( + NamedTextColor.GRAY, + NamedTextColor.WHITE, + NamedTextColor.DARK_AQUA, + NamedTextColor.BLUE, + new SoundData(Sound.BLOCK_NOTE_BLOCK_BELL,1)); + + private final TextColor mainText; + private final TextColor argDefault; + private final TextColor arg2; + private final TextColor arg3; + private final SoundData sound; + + Pallet(TextColor mainText, TextColor argDefault, TextColor arg2, TextColor arg3, SoundData sound) { + this.mainText = mainText; + this.argDefault = argDefault; + this.arg2 = arg2; + this.arg3 = arg3; + this.sound = sound; + } + } + + public record SoundData(Sound sound, float pitch){}; + + public static String generateProgressBar(int length, int max, int current) { + if (max <= 0) { + throw new IllegalArgumentException("Max value must be greater than 0"); + } + + current = Math.max(0, Math.min(current, max)); + double percent = (double) current / max; + int filledBars = (int) Math.round(percent * length); + + StringBuilder progressBar = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i < filledBars) { + progressBar.append("&a|"); + } else { + progressBar.append("&7|"); + } + } + + return progressBar.toString(); + } + + public static String formatEnum(Enum obj) { + if (obj == null) return "Null"; + String name = obj.name(); + String[] words = name.toLowerCase().split("_"); + + StringBuilder formatted = new StringBuilder(); + + for (String word : words) { + if (!word.isEmpty()) { + formatted.append(Character.toUpperCase(word.charAt(0))) + .append(word.substring(1)) + .append(" "); + } + } + + return formatted.toString().trim(); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/utils/Verbose.java b/src/main/java/me/trouper/trimserver/utils/Verbose.java new file mode 100644 index 0000000..486d148 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/Verbose.java @@ -0,0 +1,71 @@ +package me.trouper.trimserver.utils; + +import me.trouper.trimserver.TrimServer; +import me.trouper.trimserver.server.Main; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public class Verbose implements Main { + + public static void send(int backtrace, String message, Object... args) { + if (!TrimServer.getInstance().getManager().io.config.debugMode) return; + String callerInfo = "Unknown Caller"; + + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + if (stackTrace.length > 2 + backtrace) { + StackTraceElement caller = stackTrace[2 + backtrace]; + + String className = caller.getClassName(); + className = className.substring(className.lastIndexOf(".") + 1); + if (className.contains("-")) { + callerInfo = "Protected"; + } else { + callerInfo = className + "." + caller.getMethodName(); + } + + if (TrimServer.getInstance().getManager().io.config.debuggerExclusions.contains(callerInfo)) { + return; + } + } + + String formattedMessage = message.formatted(args); + String log = "[DEBUG ^ %s] [%s]: %s".formatted(backtrace, callerInfo, formattedMessage); + TrimServer.getInstance().getLogger().info(log); + + for (Player operator : Bukkit.getOnlinePlayers()) { + if (operator.isOp()) operator.sendMessage("§d§l%s §7[§bDEBUG ^ %s§7] §7[§e%s§7] §8» §7%s" + .formatted(main.config().messages.pluginName,backtrace, callerInfo, formattedMessage)); + } + } + + public static void send(String message, Object... args) { + if (!TrimServer.getInstance().getManager().io.config.debugMode) return; + String callerInfo = "Unknown Caller"; + + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + if (stackTrace.length > 2) { + StackTraceElement caller = stackTrace[2]; + + String className = caller.getClassName(); + className = className.substring(className.lastIndexOf(".") + 1); + if (className.contains("-")) { + callerInfo = "Protected"; + } else { + callerInfo = className + "." + caller.getMethodName(); + } + + if (main.config().debuggerExclusions.contains(callerInfo)) { + return; + } + } + + String formattedMessage = message.formatted(args); + String log = "[DEBUG] [%s]: %s".formatted(callerInfo, formattedMessage); + TrimServer.getInstance().getLogger().info(log); + + for (Player operator : Bukkit.getOnlinePlayers()) { + if (operator.isOp()) operator.sendMessage("§d§l%s §7[§bDEBUG§7] §7[§e%s§7] §8» §7%s" + .formatted(main.config().messages.pluginName,callerInfo, formattedMessage)); + } + } +} diff --git a/src/main/java/me/trouper/trimserver/utils/commands/Args.java b/src/main/java/me/trouper/trimserver/utils/commands/Args.java new file mode 100644 index 0000000..90d6bda --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/commands/Args.java @@ -0,0 +1,115 @@ +package me.trouper.trimserver.utils.commands; + +import java.util.function.Consumer; + +public record Args(String... args) { + + public Arg getAll() { + return getAll(0); + } + + public Arg getAll(int beginIndex) { + String str = ""; + for (int i = beginIndex; i < args.length; i++) { + str = str.concat(args[i] + " "); + } + return new Arg(str.trim()); + } + + public Arg get(int index) { + if (args.length == 0) + throw new IllegalArgumentException("not enough arguments: arguments are empty"); + if (index < 0 || index >= args.length) + throw new IllegalArgumentException("not enough arguments: argument %s is missing".formatted(index + 1)); + return new Arg(args[index]); + } + + public Arg first() { + return get(0); + } + + public Arg last() { + return get(args.length - 1); + } + + public boolean match(int index, String arg) { + if (index < 0 || index >= args.length) { + return false; + } + return get(index).toString().equalsIgnoreCase(arg); + } + + public void when(int index, String match, Consumer action) { + if (match(index, match)) { + action.accept(get(index)); + } + } + + public int getSize() { + return args.length; + } + + public boolean isEmpty() { + return args.length == 0; + } + + public static class Arg { + private final String arg; + + public Arg(String arg) { + this.arg = arg; + } + + public int toInt() { + return Integer.parseInt(arg); + } + + public long toLong() { + return Long.parseLong(arg); + } + + public byte toByte() { + return Byte.parseByte(arg); + } + + public short toShort() { + return Short.parseShort(arg); + } + + public double toDouble() { + return Double.parseDouble(arg); + } + + public float toFloat() { + return Float.parseFloat(arg); + } + + public boolean toBool() { + return Boolean.parseBoolean(arg); + } + + public char toChar() { + return arg.isEmpty() ? ' ' : arg.charAt(0); + } + + @Override + public String toString() { + return arg; + } + + public > T toEnum(Class enumType) { + return toEnum(enumType, null); + } + + public > T toEnum(Class enumType, T fallback) { + String arg = this.arg.replace('-', '_'); + for (T constant : enumType.getEnumConstants()) + if (arg.equalsIgnoreCase(constant.name())) + return constant; + + if (fallback == null) + throw new IllegalArgumentException("'%s' is not a value of %s".formatted(arg, enumType.getSimpleName())); + return fallback; + } + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/utils/commands/CommandRegistry.java b/src/main/java/me/trouper/trimserver/utils/commands/CommandRegistry.java new file mode 100644 index 0000000..4e0f5fa --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/commands/CommandRegistry.java @@ -0,0 +1,17 @@ +package me.trouper.trimserver.utils.commands; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface CommandRegistry { + + String value(); + String usage() default "none"; + Permission permission() default @Permission(""); + boolean printStackTrace() default false; + boolean playersOnly() default false; +} diff --git a/src/main/java/me/trouper/trimserver/utils/commands/Permission.java b/src/main/java/me/trouper/trimserver/utils/commands/Permission.java new file mode 100644 index 0000000..38c8b2b --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/commands/Permission.java @@ -0,0 +1,14 @@ +package me.trouper.trimserver.utils.commands; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Permission { + + String value(); + String message() default "&cYou do not have permission for this command!"; +} diff --git a/src/main/java/me/trouper/trimserver/utils/commands/QuickCommand.java b/src/main/java/me/trouper/trimserver/utils/commands/QuickCommand.java new file mode 100644 index 0000000..20409b2 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/commands/QuickCommand.java @@ -0,0 +1,99 @@ +package me.trouper.trimserver.utils.commands; + +import me.trouper.trimserver.server.Main; +import me.trouper.trimserver.utils.commands.completions.CompletionBuilder; +import me.trouper.trimserver.utils.commands.completions.CompletionNode; +import me.trouper.trimserver.utils.misc.Voidable; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.PluginCommand; +import org.bukkit.command.TabExecutor; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; + +public interface QuickCommand extends TabExecutor, Main { + + void dispatchCommand(CommandSender sender, Command command, String label, Args args); + + void dispatchCompletions(CommandSender sender, Command command, String label, CompletionBuilder b); + + default void register() { + CommandRegistry registry = this.getClass().getAnnotation(CommandRegistry.class); + PluginCommand command = getPlugin().getCommand(registry.value()); + + if (command != null) { + command.setExecutor(this); + command.setTabCompleter(this); + } + } + + @Override + default boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + CommandRegistry registry = this.getClass().getAnnotation(CommandRegistry.class); + if (registry == null) { + return true; + } + if (!(sender instanceof Player) && registry.playersOnly()) { + info(sender, "This command is for players only!"); + return true; + } + + try { + String perm = registry.permission().value(); + if (perm != null && !perm.isEmpty() && !sender.hasPermission(perm)) { + error(sender, registry.permission().message()); + return true; + } + dispatchCommand(sender, command, label, new Args(args)); + } + catch (Exception ex) { + if (registry.printStackTrace()) { + ex.printStackTrace(); + } + info(sender, "&cCorrect Usage: &7" + registry.usage()); + } + return true; + } + + @Override + default List onTabComplete(CommandSender sender, Command command, String label, String[] args) { + try { + CompletionBuilder b = new CompletionBuilder(label); + dispatchCompletions(sender, command, label, b); + CompletionNode node = b.getRootNode(); + + if (args.length == 0) { + return node.getOptions(); + } + for (int i = 0; i < args.length - 1; i++) { + node = node.next(args[i]); + } + + String end = args[args.length - 1]; + List a = new ArrayList<>(node.getOptions()); + + if (node.isOptionsRegex()) { + List regexResult = new ArrayList<>(); + for (CompletionNode option : node.getNextOptions()) { + boolean regexMatches = CompletionNode.containsRegex(option, end) || end.isEmpty(); + for (String s : option.getValues()) + regexResult.add((regexMatches ? "§d" : "§c") + s + "§r"); + } + return regexResult; + } + else { + a.removeIf(s -> !s.toLowerCase().contains(end.toLowerCase())); + return a; + } + } + catch (Exception ex) { + return new ArrayList<>(); + } + } + + default Voidable getRegistry() { + return Voidable.of(this.getClass().getAnnotation(CommandRegistry.class)); + } +} diff --git a/src/main/java/me/trouper/trimserver/utils/commands/completions/CompletionBuilder.java b/src/main/java/me/trouper/trimserver/utils/commands/completions/CompletionBuilder.java new file mode 100644 index 0000000..aa21b60 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/commands/completions/CompletionBuilder.java @@ -0,0 +1,123 @@ +package me.trouper.trimserver.utils.commands.completions; + +import me.trouper.trimserver.utils.misc.ArrayUtils; + +import java.util.*; +import java.util.function.Function; + +public class CompletionBuilder { + + private final CompletionNode root; + private final List options; + private boolean isBranch; + private String regex; + + CompletionBuilder(List names) { + this.root = new CompletionNode(names, new ArrayList<>(), null); + this.options = new ArrayList<>(); + this.isBranch = false; + } + + public CompletionBuilder(String names) { + this.root = new CompletionNode(names); + this.options = new ArrayList<>(); + this.isBranch = false; + } + + public CompletionBuilder(String regex, String details) { + this.root = new CompletionNode(Collections.singletonList(details), new ArrayList<>(), regex); + this.options = new ArrayList<>(); + this.isBranch = false; + this.regex = regex; + } + + public CompletionBuilder then(CompletionBuilder arg) { + options.add(arg); + root.nextOptions.add(arg.root); + return this; + } + + public CompletionBuilder arg(List name) { + CompletionBuilder b = new CompletionBuilder(name); + b.isBranch = true; + return b; + } + + public CompletionBuilder argRegex(String regex, String details) { + CompletionBuilder b = new CompletionBuilder(regex, details); + b.isBranch = true; + return b; + } + + public CompletionBuilder argInt(String details) { + return argRegex("^ *\\-?\\d+ *$", details); + } + + public CompletionBuilder argPosInt(String details) { + return argRegex("^ *\\d+ *$", details); + } + + public CompletionBuilder argDecimal(String details) { + return argRegex("^ *\\-?\\d*\\.?\\d+ *$", details); + } + + public CompletionBuilder argPosDecimal(String details) { + return argRegex("^ *\\d*\\.?\\d+ *$", details); + } + + public CompletionBuilder argBool() { + return arg("true", "false"); + } + + public CompletionBuilder argEnum(Class> type, boolean lowercase) { + return arg(ArrayUtils.enumNames(type, lowercase)); + } + + public CompletionBuilder argEnum(Class> type) { + return argEnum(type, true); + } + + public CompletionBuilder argOnlinePlayers() { + return arg(ArrayUtils.playerNames()); + } + + public CompletionBuilder arg(String... names) { + return arg(Arrays.asList(names)); + } + + public CompletionBuilder arg(String name) { + return arg(Collections.singletonList(name)); + } + + public CompletionBuilder arg(Collection input, Function toString) { + return arg(input.stream().map(toString).toList()); + } + + public CompletionBuilder next(String name) { + for (CompletionBuilder o : options) { + if (CompletionNode.strictContains(o.root, name)) { + return o; + } + } + return null; + } + + public CompletionNode getRootNode() { + return root; + } + + public CompletionNode build() { + if (this.isBranch) { + throw new IllegalArgumentException("build() cannot be called on branches!"); + } + return root; + } + + public boolean isBranch() { + return isBranch; + } + + public boolean isRegex() { + return regex != null; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/utils/commands/completions/CompletionNode.java b/src/main/java/me/trouper/trimserver/utils/commands/completions/CompletionNode.java new file mode 100644 index 0000000..01215c9 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/commands/completions/CompletionNode.java @@ -0,0 +1,88 @@ +package me.trouper.trimserver.utils.commands.completions; + +import java.util.ArrayList; +import java.util.List; + +public class CompletionNode { + + final List values; + final List nextOptions; + final String regex; + + CompletionNode(List values, List nextOptions, String regex) { + this.values = values; + this.nextOptions = nextOptions; + this.regex = regex; + } + + CompletionNode(String values) { + this(List.of(values), new ArrayList<>(), null); + } + + public static boolean strictContains(CompletionNode parent, String subject) { + for (String value : parent.values) + if (value.equals(subject)) + return true; + return false; + } + + public static boolean contains(CompletionNode parent, String subject) { + for (String value : parent.values) + if (value.contains(subject)) + return true; + return false; + } + + public static boolean containsRegex(CompletionNode parent, String subject) { + if (parent.regex == null) + return false; + return subject.matches(parent.regex); + } + + public boolean optionsRegexMatchesArg(String argument) { + for (CompletionNode option : nextOptions) + if (containsRegex(option, argument)) + return true; + return false; + } + + public CompletionNode next(String argument) { + for (CompletionNode option : nextOptions) + if (containsRegex(option, argument)) + return option; + + for (CompletionNode option : nextOptions) + if (strictContains(option, argument)) + return option; + + for (CompletionNode option : nextOptions) + if (contains(option, argument)) + return option; + + return null; + } + + public List getOptions() { + List a = new ArrayList<>(); + for (CompletionNode o : nextOptions) { + a.addAll(o.values); + } + return a; + } + + public boolean isRegex() { + return regex != null; + } + + public boolean isOptionsRegex() { + return nextOptions.stream().anyMatch(CompletionNode::isRegex); + } + + public List getNextOptions() { + return nextOptions; + } + + public List getValues() { + return values; + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/utils/misc/ArrayUtils.java b/src/main/java/me/trouper/trimserver/utils/misc/ArrayUtils.java new file mode 100644 index 0000000..f673219 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/misc/ArrayUtils.java @@ -0,0 +1,71 @@ +package me.trouper.trimserver.utils.misc; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +public final class ArrayUtils { + + /** + * Transforms an array to another one + * @param e iterable list + * @param a action + * @return new transformed list + * @param input + * @param output + */ + public static List map(Iterable e, Function a) { + List list = new ArrayList<>(); + e.forEach(i -> list.add(a.apply(i))); + return list; + } + + public static String toPrettyString(List list) { + return "§7[§e" + String.join("§7, §e", ArrayUtils.map(list, Object::toString)) + "§7]"; + } + + public static > List enumNames(Class type, boolean lowercase) { + List names = new ArrayList<>(); + for (E constant : type.getEnumConstants()) { + String name = constant.name(); + names.add(lowercase ? name.toLowerCase() : name); + } + return names; + } + + public static > List enumNames(Class type) { + return enumNames(type, true); + } + + public static List playerNames() { + return map(Bukkit.getOnlinePlayers(), Player::getName); + } + + @SafeVarargs + public static List bind(Iterable tList, T... ts) { + List list = Arrays.asList(ts); + tList.forEach(list::add); + return list; + } + + public static List reversed(List input) { + Collections.reverse(input); + return input; + } + + public static List reversed(Iterable input) { + List list = new ArrayList<>(); + input.forEach(list::add); + return reversed(list); + } + + public static void reverseForEach(Iterable input, Consumer action) { + reversed(input).forEach(action); + } +} diff --git a/src/main/java/me/trouper/trimserver/utils/misc/Cooldown.java b/src/main/java/me/trouper/trimserver/utils/misc/Cooldown.java new file mode 100644 index 0000000..64f1589 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/misc/Cooldown.java @@ -0,0 +1,42 @@ +package me.trouper.trimserver.utils.misc; + +import java.util.HashMap; +import java.util.Map; + +public class Cooldown { + + private final Map timer; + + public Cooldown() { + this.timer = new HashMap<>(); + } + + private O getOrDefault(O value, O def) { + return value != null ? value : def; + } + + public long getCooldown(T obj) { + return Math.max(getOrDefault(timer.get(obj), 0L) - System.currentTimeMillis(), 0L); + } + + public double getCooldownSec(T obj) { + final long cooldown = this.getCooldown(obj); + return Math.floor(cooldown / 10.0) / 100.0; + } + + public boolean isOnCooldown(T obj) { + return getCooldown(obj) > 0L; + } + + public void setCooldown(T obj, long millis) { + timer.put(obj, System.currentTimeMillis() + millis); + } + + public void addCooldown(T obj, long millis) { + setCooldown(obj, getCooldown(obj) + millis); + } + + public void removeCooldown(T obj) { + timer.remove(obj); + } +} diff --git a/src/main/java/me/trouper/trimserver/utils/misc/FileValidationUtils.java b/src/main/java/me/trouper/trimserver/utils/misc/FileValidationUtils.java new file mode 100644 index 0000000..7bdb968 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/misc/FileValidationUtils.java @@ -0,0 +1,21 @@ +package me.trouper.trimserver.utils.misc; + +import java.io.File; + +public final class FileValidationUtils { + + public static boolean validate(File file) { + try { + if (!file.getParentFile().exists()) + if (!file.getParentFile().mkdirs()) + return false; + if (!file.exists()) + if (!file.createNewFile()) + return false; + return true; + } + catch (Exception ex) { + return false; + } + } +} diff --git a/src/main/java/me/trouper/trimserver/utils/misc/JsonSerializable.java b/src/main/java/me/trouper/trimserver/utils/misc/JsonSerializable.java new file mode 100644 index 0000000..274fae2 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/misc/JsonSerializable.java @@ -0,0 +1,149 @@ +package me.trouper.trimserver.utils.misc; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.io.*; + +public interface JsonSerializable { + + Gson gson = new GsonBuilder().setPrettyPrinting().serializeNulls().setLenient().create(); + File getFile(); + + default String serialize(boolean pretty) { + Gson gson; + if (pretty) { + gson = new GsonBuilder().setPrettyPrinting().setLenient().create(); + } + else { + gson = new Gson(); + } + + try { + String json = gson.toJson(this); + if (json == null) { + throw new IllegalStateException("json parse failed for " + this.getClass().getSimpleName()); + } + return json; + } + catch (Exception ex) { + return "{}"; + } + } + + @SuppressWarnings("unchecked") + default T deserialize(String json) { + try { + JsonSerializable v = gson.fromJson(json, this.getClass()); + if (v == null) { + throw new IllegalStateException("json parse failed"); + } + return (T)v; + } + catch (Exception ex) { + return null; + } + } + + default JsonObject getJson() { + return gson.toJsonTree(this).getAsJsonObject(); + } + + /** + * Gets a json element given the specified member path + * @param path Path separated by a period . between each member name + * @return the JsonElement at the end of the path, otherwise null + */ + default JsonElement get(String path) { + JsonElement root = gson.toJsonTree(this); + JsonElement json = root; + + for (String memberName : path.split("\\.")) { + JsonElement e = json.getAsJsonObject().get(memberName); + if (e != null) + json = e; + else + break; + } + + return json == root ? null : json; + } + + /** + * Gets a json element given the specified member path + * @param path Path separated by a period . between each member name + */ + default boolean set(String path, Object obj) { + JsonElement root = gson.toJsonTree(this); + JsonElement json = root; + String[] paths = path.split("\\."); + + if (paths.length == 0) + return false; + if (paths.length == 1) { + root.getAsJsonObject().add(path, gson.toJsonTree(obj)); + return true; + } + + for (int i = 0; i < paths.length - 1; i++) { + JsonElement e = json.getAsJsonObject().get(paths[i]); + if (e != null) + json = e; + else + break; + } + + if (json != root) { + json.getAsJsonObject().add(paths[paths.length - 1], gson.toJsonTree(obj)); + return true; + } + return false; + } + + default void save() { + String json = serialize(true); + File f = getFile(); + + if (FileValidationUtils.validate(f)) { + try { + FileWriter fw = new FileWriter(f); + BufferedWriter bw = new BufferedWriter(fw); + bw.write(json); + bw.close(); + } + catch (Exception ex) { + ex.printStackTrace(); + } + } + } + + default O getOrDef(O val, O def) { + return val != null ? val : def; + } + + static > T load(File file, Class jsonSerializable, T fallback) { + if (FileValidationUtils.validate(file)) { + try { + FileReader fr = new FileReader(file); + BufferedReader br = new BufferedReader(fr); + T t = gson.fromJson(br, jsonSerializable); + + if (t == null) { + throw new IllegalStateException("json parse failed!"); + } + + return t; + } + catch (Exception ex) { + ex.printStackTrace(); + } + } + return fallback; + } + + static > T load(String path, Class jsonSerializable, T fallback) { + return load(new File(path), jsonSerializable, fallback); + } +} diff --git a/src/main/java/me/trouper/trimserver/utils/misc/Randomizer.java b/src/main/java/me/trouper/trimserver/utils/misc/Randomizer.java new file mode 100644 index 0000000..51b2d72 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/misc/Randomizer.java @@ -0,0 +1,81 @@ +package me.trouper.trimserver.utils.misc; + +import java.util.List; + +public class Randomizer { + + public Randomizer() { + + } + + public T getRandomElement(List list) { + if (list == null || list.isEmpty()) { + return null; + } + return list.get(getRandomIndex(list.size())); + } + + @SafeVarargs + public final T getRandomElement(T... list) { + if (list == null || list.length == 0) { + return null; + } + return list[getRandomIndex(list.length)]; + } + + private int getRandomIndex(int listSize) { + if (listSize < 0) { + listSize = 0; + } + return (int)(Math.ceil(Math.random() * listSize) - 1); + } + + public boolean getRandomBoolean() { + return Math.random() < 0.5; + } + + /** + * 'Percentage' means an integer from 0-100. You should not divide this value by 100, as this does it for you. + * @param percentage an integer 0-100 + * @return true if chance hit, false otherwise + */ + public boolean getRandomChance(int percentage) { + return Math.random() < percentage / 100.0; + } + + public int getRandomInt(int min, int max) { + if (min > max) { + throw new IllegalArgumentException("min cannot be greater than max!"); + } + int range = max - min + 1; + return min + (int)(Math.random() * range); + } + + public int getRandomInt(int max) { + return getRandomInt(0, max); + } + + public double getRandomDouble(double min, double max) { + if (min > max) { + throw new IllegalArgumentException("min cannot be greater than max!"); + } + double range = max - min; + return min + Math.random() * range; + } + + public double getRandomDouble(double max) { + return getRandomDouble(0.0, max); + } + + public float getRandomFloat(float min, float max) { + if (min > max) { + throw new IllegalArgumentException("min cannot be greater than max!"); + } + float range = max - min; + return (float)(min + Math.random() * range); + } + + public float getRandomFloat(float max) { + return getRandomFloat(0.0F, max); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/utils/misc/Voidable.java b/src/main/java/me/trouper/trimserver/utils/misc/Voidable.java new file mode 100644 index 0000000..85fcd40 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/misc/Voidable.java @@ -0,0 +1,59 @@ +package me.trouper.trimserver.utils.misc; + +import me.trouper.trimserver.server.Main; + +import java.util.function.Consumer; +import java.util.function.Function; + +public class Voidable implements Main { + + private final T value; + + private Voidable(T value) { + this.value = value; + } + + public T get() { + return value; + } + + public boolean isPresent() { + return value != null; + } + + public Voidable map(Function function) { + return isPresent() ? of(function.apply(value)) : of(null); + } + + public void accept(Consumer action) { + if (isPresent()) { + action.accept(value); + } + } + + public void accept(Consumer action, Runnable orElse) { + if (isPresent()) { + action.accept(value); + } + else { + orElse.run(); + } + } + + public T getOrDef(T fallback) { + return isPresent() ? value : fallback; + } + + public T getOrThrow(String msg, Object... args) { + checkPre(isPresent(), msg, args); + return value; + } + + public T getOrThrow() { + return getOrThrow("value is not present."); + } + + public static Voidable of(T value) { + return new Voidable<>(value); + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/utils/visual/BlockDisplayRaytracer.java b/src/main/java/me/trouper/trimserver/utils/visual/BlockDisplayRaytracer.java new file mode 100644 index 0000000..d25e367 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/visual/BlockDisplayRaytracer.java @@ -0,0 +1,351 @@ +package me.trouper.trimserver.utils.visual; + +import me.trouper.trimserver.server.Main; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.entity.BlockDisplay; +import org.bukkit.entity.Display; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.util.BoundingBox; +import org.bukkit.util.Transformation; +import org.bukkit.util.Vector; +import org.bukkit.util.VoxelShape; +import org.joml.AxisAngle4f; +import org.joml.Vector3f; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class BlockDisplayRaytracer implements Main { + + public static void cleanup() { + JavaPlugin plugin = main.getPlugin(); + List worlds = plugin.getServer().getWorlds(); + List entities = new ArrayList<>(); + for (World world : worlds) { + entities.addAll(world.getEntities().stream().filter(entity -> entity.getScoreboardTags().contains("$/TrimServer/ Temp")).toList()); + entities.forEach(entity -> { + if (entity != null) entity.remove(); + }); + } + } + + public static void outline(Material display, Location location, long stayTime, List viewers) { + outline(display, location, 0.05, stayTime, viewers); + } + + public static void outline(Material display, Location corner1, Location corner2, double thickness, long stayTime, List viewers) { + World world = corner1.getWorld(); + + int minX = Math.min(corner1.getBlockX(), corner2.getBlockX()); + int minY = Math.min(corner1.getBlockY(), corner2.getBlockY()); + int minZ = Math.min(corner1.getBlockZ(), corner2.getBlockZ()); + int maxX = Math.max(corner1.getBlockX(), corner2.getBlockX()); + int maxY = Math.max(corner1.getBlockY(), corner2.getBlockY()); + int maxZ = Math.max(corner1.getBlockZ(), corner2.getBlockZ()); + + Location a1 = new Location(world, minX, minY, minZ); + Location a2 = new Location(world, maxX + 1, minY, minZ); + Location a3 = new Location(world, maxX + 1, minY, maxZ + 1); + Location a4 = new Location(world, minX, minY, maxZ + 1); + + Location b1 = new Location(world, minX, maxY + 1, minZ); + Location b2 = new Location(world, maxX + 1, maxY + 1, minZ); + Location b3 = new Location(world, maxX + 1, maxY + 1, maxZ + 1); + Location b4 = new Location(world, minX, maxY + 1, maxZ + 1); + + trace(display, a1, a2, thickness, stayTime, viewers); + trace(display, a2, a3, thickness, stayTime, viewers); + trace(display, a3, a4, thickness, stayTime, viewers); + trace(display, a4, a1, thickness, stayTime, viewers); + + trace(display, b1, b2, thickness, stayTime, viewers); + trace(display, b2, b3, thickness, stayTime, viewers); + trace(display, b3, b4, thickness, stayTime, viewers); + trace(display, b4, b1, thickness, stayTime, viewers); + + trace(display, a1, b1, thickness, stayTime, viewers); + trace(display, a2, b2, thickness, stayTime, viewers); + trace(display, a3, b3, thickness, stayTime, viewers); + trace(display, a4, b4, thickness, stayTime, viewers); + } + + + public static void outline(Material display, Location location, double thickness, long stayTime, List viewers) { + Location og = location.getBlock().getLocation(); + + Location a1 = og.clone().add(0, 0, 0); + Location a2 = og.clone().add(1, 0, 0); + Location a3 = og.clone().add(1, 0, 1); + Location a4 = og.clone().add(0, 0, 1); + + Location b1 = og.clone().add(0, 1, 0); + Location b2 = og.clone().add(1, 1, 0); + Location b3 = og.clone().add(1, 1, 1); + Location b4 = og.clone().add(0, 1, 1); + + trace(display, a1, a2, thickness, stayTime, viewers); + trace(display, a2, a3, thickness, stayTime, viewers); + trace(display, a3, a4, thickness, stayTime, viewers); + trace(display, a4, a1, thickness, stayTime, viewers); + + trace(display, b1, b2, thickness, stayTime, viewers); + trace(display, b2, b3, thickness, stayTime, viewers); + trace(display, b3, b4, thickness, stayTime, viewers); + trace(display, b4, b1, thickness, stayTime, viewers); + + trace(display, a1, b1, thickness, stayTime, viewers); + trace(display, a2, b2, thickness, stayTime, viewers); + trace(display, a3, b3, thickness, stayTime, viewers); + trace(display, a4, b4, thickness, stayTime, viewers); + } + + public static void trace(Material display, Location start, Location end, long stayTime, List viewers) { + trace(display, start, end.toVector().subtract(start.toVector()), 0.05, end.distance(start), stayTime, viewers); + } + + public static void trace(Material display, Location start, Location end, double thickness, long stayTime, List viewers) { + trace(display, start, end.toVector().subtract(start.toVector()), thickness, end.distance(start), stayTime, viewers); + } + + public static void trace(Material display, Location start, Vector direction, double thickness, double distance, long stayTime, List viewers) { + World world = start.getWorld(); + + BlockDisplay beam = world.spawn(start, BlockDisplay.class, entity -> { + AxisAngle4f angle = new AxisAngle4f(0, 0, 0, 1); + Vector3f transition = new Vector3f(-(float)(thickness / 2F)); + Vector3f scale = new Vector3f((float)thickness, (float)thickness, (float)distance); + Transformation trans = new Transformation(transition, angle, scale, angle); + Location vector = entity.getLocation(); + + vector.setDirection(direction); + entity.teleport(vector); + entity.setBlock(display.createBlockData()); + entity.setBrightness(new Display.Brightness(15, 15)); + entity.setInterpolationDelay(0); + entity.setTransformation(trans); + entity.addScoreboardTag("$/TrimServer/ Temp"); + + for (Player player : Bukkit.getOnlinePlayers()) { + if (!viewers.contains(player)) { + player.hideEntity(main.getPlugin(), entity); + } + } + + Bukkit.getScheduler().runTaskLater(main.getPlugin(), entity::remove, stayTime); + }); + } + + public static void trace(Material display, Location start, Vector direction, double thickness, double distance, long stayTime, Consumer onEntitySpawn, List viewers) { + World world = start.getWorld(); + + BlockDisplay beam = world.spawn(start, BlockDisplay.class, entity -> { + AxisAngle4f angle = new AxisAngle4f(0, 0, 0, 1); + Vector3f transition = new Vector3f(-(float)(thickness / 2F)); + Vector3f scale = new Vector3f((float)thickness, (float)thickness, (float)distance); + Transformation trans = new Transformation(transition, angle, scale, angle); + Location vector = entity.getLocation(); + + vector.setDirection(direction); + entity.teleport(vector); + entity.setBlock(display.createBlockData()); + entity.setBrightness(new Display.Brightness(15, 15)); + entity.setInterpolationDelay(0); + entity.setTransformation(trans); + entity.addScoreboardTag("$/TrimServer/ Temp"); + + + for (Player player : Bukkit.getOnlinePlayers()) { + if (!viewers.contains(player)) { + player.hideEntity(main.getPlugin(), entity); + } + } + + Bukkit.getScheduler().runTaskLater(main.getPlugin(), entity::remove, stayTime); + Bukkit.getScheduler().runTaskLater(main.getPlugin(), () -> onEntitySpawn.accept(entity), 5); + }); + } + + public static List outline(Material display, Location location, long stayTime) { + return outline(display, location, 0.05, stayTime); + } + + public static List outline(Material display, Location location, double thickness, long stayTime) { + Location og = location.getBlock().getLocation(); + + Location a1 = og.clone().add(0, 0, 0); + Location a2 = og.clone().add(1, 0, 0); + Location a3 = og.clone().add(1, 0, 1); + Location a4 = og.clone().add(0, 0, 1); + + Location b1 = og.clone().add(0, 1, 0); + Location b2 = og.clone().add(1, 1, 0); + Location b3 = og.clone().add(1, 1, 1); + Location b4 = og.clone().add(0, 1, 1); + + List a = new ArrayList<>(); + + a.add(trace(display, a1, a2, thickness, stayTime)); + a.add(trace(display, a2, a3, thickness, stayTime)); + a.add(trace(display, a3, a4, thickness, stayTime)); + a.add(trace(display, a4, a1, thickness, stayTime)); + + a.add(trace(display, b1, b2, thickness, stayTime)); + a.add(trace(display, b2, b3, thickness, stayTime)); + a.add(trace(display, b3, b4, thickness, stayTime)); + a.add(trace(display, b4, b1, thickness, stayTime)); + + a.add(trace(display, a1, b1, thickness, stayTime)); + a.add(trace(display, a2, b2, thickness, stayTime)); + a.add(trace(display, a3, b3, thickness, stayTime)); + a.add(trace(display, a4, b4, thickness, stayTime)); + + return a; + } + + public static void highlightCollisions(Block block, Color color, long stayTime) { + if (block == null || block.isEmpty() || !block.isCollidable()) + return; + + VoxelShape shape = block.getCollisionShape(); + World world = block.getWorld(); + Vector offset = block.getLocation().toVector(); + + for (BoundingBox box : shape.getBoundingBoxes()) { + highlight(box, offset, world, color, stayTime); + } + } + + public static void highlight(BoundingBox box, Vector offset, World world, Color color, long stayTime) { + double x1 = box.getMinX() + offset.getX(); + double y1 = box.getMinY() + offset.getY(); + double z1 = box.getMinZ() + offset.getZ(); + double x2 = box.getMaxX() + offset.getX(); + double y2 = box.getMaxY() + offset.getY(); + double z2 = box.getMaxZ() + offset.getZ(); + + traceGlowing(world, x1, y1, z1, x2, y1, z1, color, stayTime); + traceGlowing(world, x2, y1, z1, x2, y1, z2, color, stayTime); + traceGlowing(world, x2, y1, z2, x1, y1, z2, color, stayTime); + traceGlowing(world, x1, y1, z2, x1, y1, z1, color, stayTime); + + traceGlowing(world, x1, y2, z1, x2, y2, z1, color, stayTime); + traceGlowing(world, x2, y2, z1, x2, y2, z2, color, stayTime); + traceGlowing(world, x2, y2, z2, x1, y2, z2, color, stayTime); + traceGlowing(world, x1, y2, z2, x1, y2, z1, color, stayTime); + + traceGlowing(world, x1, y1, z1, x1, y2, z1, color, stayTime); + traceGlowing(world, x2, y1, z1, x2, y2, z1, color, stayTime); + traceGlowing(world, x2, y1, z2, x2, y2, z2, color, stayTime); + traceGlowing(world, x1, y1, z2, x1, y2, z2, color, stayTime); + } + + public static void traceGlowing(World world, double x1, double y1, double z1, double x2, double y2, double z2, Color color, long stayTime) { + Location loc1 = new Location(world, x1, y1, z1); + Location loc2 = new Location(world, x2, y2, z2); + BlockDisplay ent = trace(Material.WHITE_CONCRETE, loc1, loc2, 0.01, stayTime); + ent.setGlowColorOverride(color); + ent.setGlowing(true); + } + + public static BlockDisplay trace(Material display, Location start, Location end, long stayTime) { + return trace(display, start, end.toVector().subtract(start.toVector()), 0.05, end.distance(start), stayTime); + } + + public static BlockDisplay trace(Material display, Location start, Location end, double thickness, long stayTime) { + return trace(display, start, end.toVector().subtract(start.toVector()), thickness, end.distance(start), stayTime); + } + + public static BlockDisplay trace(Material display, Location start, Vector direction, double thickness, double distance, long stayTime) { + World world = start.getWorld(); + + BlockDisplay entity = world.spawn(start, BlockDisplay.class); + AxisAngle4f angle = new AxisAngle4f(0, 0, 0, 1); + Vector3f transition = new Vector3f(-(float)(thickness / 2F)); + Vector3f scale = new Vector3f((float)thickness, (float)thickness, (float)distance); + Transformation trans = new Transformation(transition, angle, scale, angle); + Location vector = entity.getLocation(); + + vector.setDirection(direction); + entity.teleport(vector); + entity.setBlock(display.createBlockData()); + entity.setBrightness(new Display.Brightness(15, 15)); + entity.setInterpolationDelay(0); + entity.setTransformation(trans); + entity.addScoreboardTag("$/TrimServer/ Temp"); + + Bukkit.getScheduler().runTaskLater(main.getPlugin(), entity::remove, stayTime); + return entity; + } + + public static void transform(BlockDisplay display, Location start, Location end, double thickness) { + Vector direction = end.toVector().subtract(start.toVector()); + double distance = direction.length(); + + Location loc = start.clone(); + loc.setDirection(direction); + display.teleport(loc); + + Vector3f translation = new Vector3f(-(float)(thickness / 2F), 0, 0); // Centered + Vector3f scale = new Vector3f((float)thickness, (float)thickness, (float)distance); + AxisAngle4f rotation = new AxisAngle4f(0, 0, 0, 1); + Transformation transformation = new Transformation(translation, rotation, scale, rotation); + + display.setTransformation(transformation); + } + + public static void transform(BlockDisplay display, Location start, Vector direction, double distance, double thickness) { + Location loc = start.clone(); + loc.setDirection(direction); + display.teleport(loc); + + Vector3f translation = new Vector3f(-(float)(thickness / 2F), 0, 0); + Vector3f scale = new Vector3f((float)thickness, (float)thickness, (float)distance); + AxisAngle4f rotation = new AxisAngle4f(0, 0, 0, 1); + Transformation transformation = new Transformation(translation, rotation, scale, rotation); + + display.setTransformation(transformation); + } + + + public static void translate(BlockDisplay display, Vector3f offset) { + Transformation current = display.getTransformation(); + Vector3f translation = new Vector3f(current.getTranslation()).add(offset); + display.setTransformation(new Transformation( + translation, + current.getLeftRotation(), + current.getScale(), + current.getRightRotation() + )); + } + + public static void scale(BlockDisplay display, Vector3f scale) { + Transformation current = display.getTransformation(); + display.setTransformation(new Transformation( + current.getTranslation(), + current.getLeftRotation(), + scale, + current.getRightRotation() + )); + } + + public static void rotate(BlockDisplay display, AxisAngle4f rotation) { + Transformation current = display.getTransformation(); + display.setTransformation(new Transformation( + current.getTranslation(), + rotation, + current.getScale(), + rotation + )); + } + + public static void alignToDirection(BlockDisplay display, Vector direction) { + Location loc = display.getLocation().clone(); + loc.setDirection(direction); + display.teleport(loc); + } + +} diff --git a/src/main/java/me/trouper/trimserver/utils/visual/CustomDisplayRaytracer.java b/src/main/java/me/trouper/trimserver/utils/visual/CustomDisplayRaytracer.java new file mode 100644 index 0000000..ffd0d70 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/visual/CustomDisplayRaytracer.java @@ -0,0 +1,258 @@ +package me.trouper.trimserver.utils.visual; + +import me.trouper.trimserver.utils.Verbose; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.BoundingBox; +import org.bukkit.util.Vector; +import org.bukkit.util.VoxelShape; + +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +public class CustomDisplayRaytracer { + + public static final Predicate HIT_BLOCK = point -> { + Block b = point.getBlock(); + Location l = point.getLoc(); + + if (b == null || b.isEmpty() || !b.isCollidable()) + return false; + + Vector vec = l.toVector().subtract(b.getLocation().toVector()); + VoxelShape shape = b.getCollisionShape(); + + for (BoundingBox box : shape.getBoundingBoxes()) + if (box.contains(vec)) + return true; + return false; + }; + + public static final Predicate HIT_ENTITY = point -> { + return !point.getNearbyEntities(null, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead()).isEmpty(); + }; + + public static final Predicate HIT_BLOCK_OR_ENTITY = point -> { + return HIT_BLOCK.test(point) || HIT_ENTITY.test(point); + }; + + public static final Predicate HIT_BLOCK_AND_ENTITY = point -> { + return HIT_BLOCK.test(point) && HIT_ENTITY.test(point); + }; + + public static Predicate hitEntityExclude(Entity exclude) { + return point -> !point.getNearbyEntities(exclude, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead()).isEmpty(); + } + + public static Predicate hitAnythingExclude(Entity exclude) { + return point -> HIT_BLOCK.test(point) || !point.getNearbyEntities(exclude, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead()).isEmpty(); + } + + public static Predicate hitEverythingExclude(Entity exclude) { + return point -> HIT_BLOCK.test(point) && !point.getNearbyEntities(exclude, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead()).isEmpty(); + } + + public static Predicate hitEntityIf(Predicate condition) { + return point -> !point.getNearbyEntities(null, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead() && condition.test(e)).isEmpty(); + } + + public static Predicate hitBlockIf(Predicate condition) { + return point -> HIT_BLOCK.test(point) && condition.test(point.getBlock()); + } + + public static Predicate hitAnythingIf(Predicate condition) { + return point -> HIT_BLOCK.test(point) || !point.getNearbyEntities(null, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead() && condition.test(e)).isEmpty(); + } + + public static Predicate hitEverythingIf(Predicate condition) { + return point -> HIT_BLOCK.test(point) && !point.getNearbyEntities(null, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead() && condition.test(e)).isEmpty(); + } + + + public static Point trace(Location start, Location end, Predicate hitCondition) { + return trace(start, end, 0.5, hitCondition); + } + + public static Point trace(Location start, Location end, double interval, Predicate hitCondition) { + return trace(start, end.toVector().subtract(start.toVector()), end.distance(start), interval, hitCondition); + } + + public static Point trace(Location start, Vector direction, double distance, Predicate hitCondition) { + return trace(start, direction, distance, 0.5, hitCondition); + } + + public static Point trace(Location start, Vector direction, double distance, double interval, Predicate hitCondition) { + if (interval < 0) throw new IllegalArgumentException("interval cannot be zero!"); + if (distance < 0) throw new IllegalArgumentException("distance cannot be zero!"); + + for (double i = 0.0; i < distance; i += interval) { + Point point = blocksInFrontOf(start, direction, i, false); + if (hitCondition.test(point)) { + return point; + } + } + return blocksInFrontOf(start, direction, distance, true); + } + + + public static BukkitTask traceDelayed(Plugin plugin, Location start, Vector direction, double distance, double interval, long tickDelay, int pointsPerTick, Predicate hitCondition) { + + if (interval <= 0) throw new IllegalArgumentException("interval cannot be zero or negative!"); + if (distance <= 0) throw new IllegalArgumentException("distance cannot be zero or negative!"); + if (tickDelay < 0) throw new IllegalArgumentException("tickDelay cannot be negative!"); + + Vector normalizedDir = direction.clone().normalize(); + + + return new BukkitRunnable() { + private double currentDistance = 0.0; + private boolean hit = false; + + @Override + public void run() { + if (hit || currentDistance > distance) { + if (!hit) { + Point finalPoint = blocksInFrontOf(start, normalizedDir, distance, true); + hitCondition.test(finalPoint); + } + this.cancel(); + return; + } + + for (int i = 0; i < pointsPerTick && currentDistance <= distance; i++) { + Point point = blocksInFrontOf(start, normalizedDir, currentDistance, false); + if (hitCondition.test(point)) { + hit = true; + break; + } + currentDistance += interval; + } + } + }.runTaskTimer(plugin, 0, tickDelay); + } + + public static BukkitTask traceDelayed(Plugin plugin, Location start, Location end, double interval, long tickDelay, int pointsPerTick, Predicate hitCondition) { + Vector direction = end.toVector().subtract(start.toVector()).normalize(); + double distance = start.distance(end); + return traceDelayed(plugin, start, direction, distance, interval, tickDelay,pointsPerTick, hitCondition); + } + + public static BukkitTask traceDelayed(Plugin plugin, + Location start, + Location end, + long tickDelay, + Predicate hitCondition) { + return traceDelayed(plugin, start, end,0.5, tickDelay, 1, hitCondition); + } + + public static BukkitTask traceDelayed(Plugin plugin, + Location start, + Vector direction, + double distance, + long tickDelay, + Predicate hitCondition) { + return traceDelayed(plugin, start, direction, distance, 0.5, tickDelay,1, hitCondition); + } + + public static Point traceWithReflection(Location start, Vector direction, double distance, double interval, + int maxReflections, Predicate hitCondition, + BiPredicate blockReflectCondition, + BiPredicate entityReflectCondition) { + if (interval <= 0) throw new IllegalArgumentException("interval cannot be zero or negative!"); + if (distance <= 0) throw new IllegalArgumentException("distance cannot be zero or negative!"); + if (maxReflections < 0) throw new IllegalArgumentException("maxReflections cannot be negative!"); + + Vector normalizedDir = direction.clone().normalize(); + Location currentStart = start.clone(); + Vector currentDir = normalizedDir.clone(); + double remainingDistance = distance; + + for (int reflection = 0; reflection <= maxReflections; reflection++) { + Point hitPoint = trace(currentStart, currentDir, remainingDistance, interval, hitCondition); + + if (hitPoint.wasMissed()) { + Verbose.send("First ray missed, returning."); + return hitPoint; + } + + double distanceUsed = currentStart.distance(hitPoint.getLoc()); + remainingDistance -= distanceUsed; + + if (remainingDistance <= 0) { + Verbose.send("First ray too long, returning."); + return hitPoint; + } + Vector reflectedDirection = null; + + if (HIT_BLOCK.test(hitPoint)) { + Verbose.send("hit block!"); + Block hitBlock = hitPoint.getBlock(); + + if (blockReflectCondition.test(hitPoint,hitBlock)) { + Verbose.send("Check passed!"); + BlockFace hitFace = hitPoint.getBlockFace(currentDir); + if (hitFace != null) { + Verbose.send("Face not null"); + reflectedDirection = calculateBlockReflection(currentDir, hitFace); + } + } + } + + else if (HIT_ENTITY.test(hitPoint)) { + Verbose.send("hit entity!"); + Entity hitEntity = hitPoint.getNearbyEntities(null, 5, true, 0.1, + e -> e instanceof LivingEntity le && !le.isDead()).stream().findFirst().orElse(null); + + if (hitEntity != null && entityReflectCondition != null && entityReflectCondition.test(hitPoint,hitEntity)) { + Verbose.send("Check passed!"); + reflectedDirection = currentDir.clone().multiply(-1).normalize(); + } + } + + if (reflectedDirection == null) { + Verbose.send("Reflected direction null"); + return hitPoint; + } + + currentStart = hitPoint.getLoc().clone(); + currentDir = reflectedDirection; + + currentStart = blocksInFrontOf(currentStart,currentDir,interval*2,false).getLoc(); + } + + return trace(currentStart, currentDir, remainingDistance, interval, hitCondition); + } + + private static Vector calculateBlockReflection(Vector incident, BlockFace face) { + Verbose.send("Calculating vector reflection for face %s".formatted(face)); + Vector normal = getFaceNormal(face); + + // r = i - 2(i dot n)n + double dot = incident.dot(normal); + Vector reflection = incident.clone().subtract(normal.clone().multiply(2 * dot)); + + return reflection.normalize(); + } + + private static Vector getFaceNormal(BlockFace face) { + Verbose.send("Getting normal for %s".formatted(face)); + return switch (face) { + case DOWN -> new Vector(0, -1, 0); + case NORTH -> new Vector(0, 0, -1); + case SOUTH -> new Vector(0, 0, 1); + case EAST -> new Vector(1, 0, 0); + case WEST -> new Vector(-1, 0, 0); + default -> new Vector(0, 1, 0); + }; + } + + public static Point blocksInFrontOf(Location loc, Vector dir, double blocks, boolean missed) { + return new Point(loc.clone().add(dir.getX() * blocks, dir.getY() * blocks, dir.getZ() * blocks), blocks, missed); + } +} diff --git a/src/main/java/me/trouper/trimserver/utils/visual/CustomDisplayReflector.java.disabled b/src/main/java/me/trouper/trimserver/utils/visual/CustomDisplayReflector.java.disabled new file mode 100644 index 0000000..035f2db --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/visual/CustomDisplayReflector.java.disabled @@ -0,0 +1,495 @@ +package me.trouper.trimserver.utils.visual; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Predicate; + +public class CustomDisplayReflector { + + // Commonly used reflection conditions + public static final BiPredicate REFLECT_ON_ANY_BLOCK = (point, face) -> + face != null && !point.getBlock().isEmpty() && point.getBlock().isCollidable(); + + public static final BiPredicate REFLECT_ON_ANY_ENTITY = (point, entity) -> + entity instanceof LivingEntity && !entity.isDead(); + + /** + * Creates a reflection condition that excludes a specific entity + * @param exclude The entity to exclude from reflection + * @return A BiPredicate that determines if reflection should occur on an entity + */ + public static BiPredicate reflectOnEntityExclude(Entity exclude) { + return (point, entity) -> entity != exclude && entity instanceof LivingEntity && !entity.isDead(); + } + + /** + * Traces a ray that can reflect off blocks based on provided conditions + * @param start The starting location + * @param direction The initial direction + * @param maxDistance The maximum total distance the ray can travel + * @param maxReflections The maximum number of reflections allowed + * @param interval The interval between trace points + * @param blockReflectCondition Determines if a ray should reflect off a block at a given point + * @return List of ray segments (each representing a segment between reflections) + */ + public static List traceReflectingRay( + Location start, + Vector direction, + double maxDistance, + int maxReflections, + double interval, + BiPredicate blockReflectCondition) { + + if (interval <= 0) throw new IllegalArgumentException("interval cannot be zero or negative!"); + if (maxDistance <= 0) throw new IllegalArgumentException("maxDistance cannot be zero or negative!"); + + List segments = new ArrayList<>(); + Vector normalizedDir = direction.clone().normalize(); + Location currentStart = start.clone(); + double remainingDistance = maxDistance; + int reflections = 0; + + while (remainingDistance > 0 && reflections <= maxReflections) { + // Trace until hit or max distance + RayResult result = traceUntilReflect(currentStart, normalizedDir, remainingDistance, interval, blockReflectCondition); + + // Add segment + segments.add(new RaySegment(currentStart.clone(), result.getEndPoint().getLoc().clone(), normalizedDir.clone(), result.getDistance())); + + // Check if we've hit something reflective + if (result.getReflectionType() == ReflectionType.NONE) { + break; // No reflection occurred, end of ray + } + + remainingDistance -= result.getDistance(); + reflections++; + + // Update for next segment + currentStart = result.getEndPoint().getLoc().clone(); + normalizedDir = result.getNewDirection().clone(); + } + + return segments; + } + + /** + * Traces a ray that can reflect off both blocks and entities + * @param start The starting location + * @param direction The initial direction + * @param maxDistance The maximum total distance the ray can travel + * @param maxReflections The maximum number of reflections allowed + * @param interval The interval between trace points + * @param blockReflectCondition Determines if a ray should reflect off a block + * @param entityReflectCondition Determines if a ray should reflect off an entity + * @return List of ray segments (each representing a segment between reflections) + */ + public static List traceReflectingRay( + Location start, + Vector direction, + double maxDistance, + int maxReflections, + double interval, + BiPredicate blockReflectCondition, + BiPredicate entityReflectCondition) { + + if (interval <= 0) throw new IllegalArgumentException("interval cannot be zero or negative!"); + if (maxDistance <= 0) throw new IllegalArgumentException("maxDistance cannot be zero or negative!"); + + List segments = new ArrayList<>(); + Vector normalizedDir = direction.clone().normalize(); + Location currentStart = start.clone(); + double remainingDistance = maxDistance; + int reflections = 0; + + while (remainingDistance > 0 && reflections <= maxReflections) { + // Trace until hit or max distance + RayResult result = traceUntilReflect(currentStart, normalizedDir, remainingDistance, interval, blockReflectCondition, entityReflectCondition); + + // Add segment + segments.add(new RaySegment(currentStart.clone(), result.getEndPoint().getLoc().clone(), normalizedDir.clone(), result.getDistance())); + + // Check if we've hit something reflective + if (result.getReflectionType() == ReflectionType.NONE) { + break; // No reflection occurred, end of ray + } + + remainingDistance -= result.getDistance(); + reflections++; + + // Update for next segment + currentStart = result.getEndPoint().getLoc().clone(); + normalizedDir = result.getNewDirection().clone(); + } + + return segments; + } + + /** + * Traces a ray until reflection or max distance + */ + private static RayResult traceUntilReflect( + Location start, + Vector direction, + double maxDistance, + double interval, + BiPredicate blockReflectCondition) { + + Vector normalizedDir = direction.clone().normalize(); + + for (double i = 0.0; i < maxDistance; i += interval) { + Point point = CustomDisplayRaytracer.blocksInFrontOf(start, normalizedDir, i, false); + + // Check for block reflection + BlockFace hitFace = getHitBlockFace(point); + if (hitFace != null && blockReflectCondition.test(point, hitFace)) { + // Calculate reflection + Vector reflectedDir = calculateBlockReflection(normalizedDir, hitFace); + return new RayResult(point, reflectedDir, i, ReflectionType.BLOCK, hitFace, null); + } + } + + // No reflection found, return end point + Point endPoint = CustomDisplayRaytracer.blocksInFrontOf(start, normalizedDir, maxDistance, true); + return new RayResult(endPoint, normalizedDir, maxDistance, ReflectionType.NONE, null, null); + } + + /** + * Traces a ray until reflection off block or entity, or max distance + */ + private static RayResult traceUntilReflect( + Location start, + Vector direction, + double maxDistance, + double interval, + BiPredicate blockReflectCondition, + BiPredicate entityReflectCondition) { + + Vector normalizedDir = direction.clone().normalize(); + + for (double i = 0.0; i < maxDistance; i += interval) { + Point point = CustomDisplayRaytracer.blocksInFrontOf(start, normalizedDir, i, false); + + // Check for entity reflection first + List entities = point.getNearbyEntities(null, 5, true, 0.1, e -> true); + for (Entity entity : entities) { + if (entityReflectCondition.test(point, entity)) { + // Entity reflection - return in opposite direction + Vector reflectedDir = normalizedDir.clone().multiply(-1); + return new RayResult(point, reflectedDir, i, ReflectionType.ENTITY, null, entity); + } + } + + // Check for block reflection + BlockFace hitFace = getHitBlockFace(point); + if (hitFace != null && blockReflectCondition.test(point, hitFace)) { + Vector reflectedDir = calculateBlockReflection(normalizedDir, hitFace); + return new RayResult(point, reflectedDir, i, ReflectionType.BLOCK, hitFace, null); + } + } + + // No reflection found, return end point + Point endPoint = CustomDisplayRaytracer.blocksInFrontOf(start, normalizedDir, maxDistance, true); + return new RayResult(endPoint, normalizedDir, maxDistance, ReflectionType.NONE, null, null); + } + + /** + * Performs a delayed tracing of a reflecting ray + */ + public static BukkitTask traceReflectingRayDelayed( + Plugin plugin, + Location start, + Vector direction, + double maxDistance, + int maxReflections, + double interval, + long tickDelay, + BiPredicate blockReflectCondition, + Function segmentAction) { + + if (interval <= 0) throw new IllegalArgumentException("interval cannot be zero or negative!"); + if (maxDistance <= 0) throw new IllegalArgumentException("maxDistance cannot be zero or negative!"); + if (tickDelay < 0) throw new IllegalArgumentException("tickDelay cannot be negative!"); + + Vector normalizedDir = direction.clone().normalize(); + + return new BukkitRunnable() { + private Location currentStart = start.clone(); + private Vector currentDir = normalizedDir.clone(); + private double remainingDistance = maxDistance; + private int reflections = 0; + private double currentSegmentDistance = 0; + + @Override + public void run() { + if (remainingDistance <= 0 || reflections > maxReflections) { + this.cancel(); + return; + } + + Point point = CustomDisplayRaytracer.blocksInFrontOf( + currentStart, currentDir, currentSegmentDistance, false); + + // Check for block reflection + BlockFace hitFace = getHitBlockFace(point); + if (hitFace != null && blockReflectCondition.test(point, hitFace)) { + // Create segment + RaySegment segment = new RaySegment( + currentStart.clone(), + point.getLoc().clone(), + currentDir.clone(), + currentSegmentDistance); + + // Execute action (if returns false, stop the ray) + if (!segmentAction.apply(segment)) { + this.cancel(); + return; + } + + // Update for next segment + currentStart = point.getLoc().clone(); + currentDir = calculateBlockReflection(currentDir, hitFace); + remainingDistance -= currentSegmentDistance; + reflections++; + currentSegmentDistance = 0; + return; + } + + // Increment distance for this step + currentSegmentDistance += interval; + + // Check if we've reached end of current segment without reflection + if (currentSegmentDistance > remainingDistance) { + Point endPoint = CustomDisplayRaytracer.blocksInFrontOf( + currentStart, currentDir, remainingDistance, true); + + RaySegment segment = new RaySegment( + currentStart.clone(), + endPoint.getLoc().clone(), + currentDir.clone(), + remainingDistance); + + segmentAction.apply(segment); + this.cancel(); + } + } + }.runTaskTimer(plugin, 0L, tickDelay); + } + + /** + * Performs a delayed tracing of a reflecting ray with both block and entity reflections + */ + public static BukkitTask traceReflectingRayDelayed( + Plugin plugin, + Location start, + Vector direction, + double maxDistance, + int maxReflections, + double interval, + long tickDelay, + BiPredicate blockReflectCondition, + BiPredicate entityReflectCondition, + Function segmentAction) { + + if (interval <= 0) throw new IllegalArgumentException("interval cannot be zero or negative!"); + if (maxDistance <= 0) throw new IllegalArgumentException("maxDistance cannot be zero or negative!"); + if (tickDelay < 0) throw new IllegalArgumentException("tickDelay cannot be negative!"); + + Vector normalizedDir = direction.clone().normalize(); + + return new BukkitRunnable() { + private Location currentStart = start.clone(); + private Vector currentDir = normalizedDir.clone(); + private double remainingDistance = maxDistance; + private int reflections = 0; + private double currentSegmentDistance = 0; + + @Override + public void run() { + if (remainingDistance <= 0 || reflections > maxReflections) { + this.cancel(); + return; + } + + Point point = CustomDisplayRaytracer.blocksInFrontOf( + currentStart, currentDir, currentSegmentDistance, false); + + // Check for entity reflection + List entities = point.getNearbyEntities(null, 5, true, 0.1, e -> true); + for (Entity entity : entities) { + if (entityReflectCondition.test(point, entity)) { + // Create segment + RaySegment segment = new RaySegment( + currentStart.clone(), + point.getLoc().clone(), + currentDir.clone(), + currentSegmentDistance); + + // Execute action (if returns false, stop the ray) + if (!segmentAction.apply(segment)) { + this.cancel(); + return; + } + + // Update for next segment - reverse direction for entity reflection + currentStart = point.getLoc().clone(); + currentDir = currentDir.clone().multiply(-1); + remainingDistance -= currentSegmentDistance; + reflections++; + currentSegmentDistance = 0; + return; + } + } + + // Check for block reflection + BlockFace hitFace = getHitBlockFace(point); + if (hitFace != null && blockReflectCondition.test(point, hitFace)) { + // Create segment + RaySegment segment = new RaySegment( + currentStart.clone(), + point.getLoc().clone(), + currentDir.clone(), + currentSegmentDistance); + + // Execute action (if returns false, stop the ray) + if (!segmentAction.apply(segment)) { + this.cancel(); + return; + } + + // Update for next segment + currentStart = point.getLoc().clone(); + currentDir = calculateBlockReflection(currentDir, hitFace); + remainingDistance -= currentSegmentDistance; + reflections++; + currentSegmentDistance = 0; + return; + } + + // Increment distance for this step + currentSegmentDistance += interval; + + // Check if we've reached end of current segment without reflection + if (currentSegmentDistance > remainingDistance) { + Point endPoint = CustomDisplayRaytracer.blocksInFrontOf( + currentStart, currentDir, remainingDistance, true); + + RaySegment segment = new RaySegment( + currentStart.clone(), + endPoint.getLoc().clone(), + currentDir.clone(), + remainingDistance); + + segmentAction.apply(segment); + this.cancel(); + } + } + }.runTaskTimer(plugin, 0L, tickDelay); + } + + + + /** + * Represents a segment of a reflected ray + */ + public static class RaySegment { + private final Location start; + private final Location end; + private final Vector direction; + private final double distance; + + public RaySegment(Location start, Location end, Vector direction, double distance) { + this.start = start; + this.end = end; + this.direction = direction; + this.distance = distance; + } + + public Location getStart() { + return start; + } + + public Location getEnd() { + return end; + } + + public Vector getDirection() { + return direction; + } + + public double getDistance() { + return distance; + } + + public World getWorld() { + return start.getWorld(); + } + } + + /** + * Types of reflections that can occur + */ + public enum ReflectionType { + NONE, + BLOCK, + ENTITY + } + + /** + * Result of a ray trace including reflection information + */ + private static class RayResult { + private final Point endPoint; + private final Vector newDirection; + private final double distance; + private final ReflectionType reflectionType; + private final BlockFace hitFace; + private final Entity hitEntity; + + public RayResult(Point endPoint, Vector newDirection, double distance, + ReflectionType reflectionType, BlockFace hitFace, Entity hitEntity) { + this.endPoint = endPoint; + this.newDirection = newDirection; + this.distance = distance; + this.reflectionType = reflectionType; + this.hitFace = hitFace; + this.hitEntity = hitEntity; + } + + public Point getEndPoint() { + return endPoint; + } + + public Vector getNewDirection() { + return newDirection; + } + + public double getDistance() { + return distance; + } + + public ReflectionType getReflectionType() { + return reflectionType; + } + + public BlockFace getHitFace() { + return hitFace; + } + + public Entity getHitEntity() { + return hitEntity; + } + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/utils/visual/DisplayEntityUtils.java b/src/main/java/me/trouper/trimserver/utils/visual/DisplayEntityUtils.java new file mode 100644 index 0000000..26ab6c5 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/visual/DisplayEntityUtils.java @@ -0,0 +1,1706 @@ +package me.trouper.trimserver.utils.visual; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.*; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.util.Transformation; +import org.bukkit.util.Vector; +import org.joml.*; + +import java.lang.Math; +import java.util.*; +import java.util.Random; +import java.util.function.Consumer; + +/** + * A comprehensive utility class for working with Display Entities in the Bukkit API. + * This utility handles all the transformation math, allowing developers to focus on the visual aspects of their plugins. + */ +public final class DisplayEntityUtils { + private DisplayEntityUtils() { + // Private constructor to prevent instantiation + } + + /** + * Creates a text display with the specified text at the given location. + * + * @param location The location to spawn the text display + * @param text The text to display + * @return The created TextDisplay entity + */ + public static TextDisplay createTextDisplay(Location location, String text) { + World world = location.getWorld(); + TextDisplay display = (TextDisplay) world.spawnEntity(location, EntityType.TEXT_DISPLAY); + display.setText(text); + return display; + } + + /** + * Creates an item display with the specified item at the given location. + * + * @param location The location to spawn the item display + * @param item The item to display + * @return The created ItemDisplay entity + */ + public static ItemDisplay createItemDisplay(Location location, org.bukkit.inventory.ItemStack item) { + World world = location.getWorld(); + ItemDisplay display = (ItemDisplay) world.spawnEntity(location, EntityType.ITEM_DISPLAY); + display.setItemStack(item); + return display; + } + + /** + * Creates a block display with the specified block data at the given location. + * + * @param location The location to spawn the block display + * @param blockData The block data to display + * @return The created BlockDisplay entity + */ + public static BlockDisplay createBlockDisplay(Location location, org.bukkit.block.data.BlockData blockData) { + World world = location.getWorld(); + BlockDisplay display = (BlockDisplay) world.spawnEntity(location, EntityType.BLOCK_DISPLAY); + display.setBlock(blockData); + return display; + } + + /** + * Builder class for creating and configuring display entities with fluent API. + * + * @param The type of display entity + */ + public static class DisplayBuilder { + private final T display; + + private DisplayBuilder(T display) { + this.display = display; + } + + /** + * Starts building a text display. + * + * @param location The location to spawn the text display + * @param text The text to display + * @return A new DisplayBuilder for the text display + */ + public static DisplayBuilder text(Location location, String text) { + return new DisplayBuilder<>(createTextDisplay(location, text)); + } + + /** + * Starts building an item display. + * + * @param location The location to spawn the item display + * @param item The item to display + * @return A new DisplayBuilder for the item display + */ + public static DisplayBuilder item(Location location, org.bukkit.inventory.ItemStack item) { + return new DisplayBuilder<>(createItemDisplay(location, item)); + } + + /** + * Starts building a block display. + * + * @param location The location to spawn the block display + * @param blockData The block data to display + * @return A new DisplayBuilder for the block display + */ + public static DisplayBuilder block(Location location, org.bukkit.block.data.BlockData blockData) { + return new DisplayBuilder<>(createBlockDisplay(location, blockData)); + } + + /** + * Sets the display entity's transformation. + * + * @param transformation The transformation to apply + * @return This builder for chaining + */ + public DisplayBuilder transformation(Transformation transformation) { + display.setTransformation(transformation); + return this; + } + + /** + * Sets the display entity's size scale. + * + * @param scale The scale factor to apply uniformly in all dimensions + * @return This builder for chaining + */ + public DisplayBuilder scale(float scale) { + return scale(scale, scale, scale); + } + + /** + * Sets the display entity's size scale with different values for each dimension. + * + * @param x The X scale factor + * @param y The Y scale factor + * @param z The Z scale factor + * @return This builder for chaining + */ + public DisplayBuilder scale(float x, float y, float z) { + Transformation transform = display.getTransformation(); + Vector3f scale = new Vector3f(x, y, z); + display.setTransformation(new Transformation( + transform.getTranslation(), + transform.getLeftRotation(), + scale, + transform.getRightRotation() + )); + return this; + } + + /** + * Sets the display entity's translation. + * + * @param x The X translation + * @param y The Y translation + * @param z The Z translation + * @return This builder for chaining + */ + public DisplayBuilder translate(float x, float y, float z) { + Transformation transform = display.getTransformation(); + Vector3f translation = new Vector3f(x, y, z); + display.setTransformation(new Transformation( + translation, + transform.getLeftRotation(), + transform.getScale(), + transform.getRightRotation() + )); + return this; + } + + /** + * Sets the display entity's rotation using quaternions. + * + * @param quaternion The rotation quaternion + * @return This builder for chaining + */ + public DisplayBuilder rotate(Quaternionf quaternion) { + Transformation transform = display.getTransformation(); + display.setTransformation(new Transformation( + transform.getTranslation(), + quaternion, + transform.getScale(), + transform.getRightRotation() + )); + return this; + } + + /** + * Sets the display entity's rotation using Euler angles. + * + * @param pitch The pitch in degrees + * @param yaw The yaw in degrees + * @param roll The roll in degrees + * @return This builder for chaining + */ + public DisplayBuilder rotate(float pitch, float yaw, float roll) { + Quaternionf quaternion = EulerAngle.toQuaternion(Math.toRadians(pitch), Math.toRadians(yaw), Math.toRadians(roll)); + return rotate(quaternion); + } + + /** + * Sets the billboard mode for the display entity. + * + * @param mode The billboard mode + * @return This builder for chaining + */ + public DisplayBuilder billboard(Display.Billboard mode) { + display.setBillboard(mode); + return this; + } + + /** + * Sets the brightness for the display entity. + * + * @param blockLight The block light level + * @param skyLight The sky light level + * @return This builder for chaining + */ + public DisplayBuilder brightness(int blockLight, int skyLight) { + display.setBrightness(new Display.Brightness(blockLight, skyLight)); + return this; + } + + /** + * Sets the view range for the display entity. + * + * @param range The view range in blocks + * @return This builder for chaining + */ + public DisplayBuilder viewRange(float range) { + display.setViewRange(range); + return this; + } + + /** + * Sets the shadow radius for the display entity. + * + * @param radius The shadow radius + * @return This builder for chaining + */ + public DisplayBuilder shadowRadius(float radius) { + display.setShadowRadius(radius); + return this; + } + + /** + * Sets the shadow strength for the display entity. + * + * @param strength The shadow strength + * @return This builder for chaining + */ + public DisplayBuilder shadowStrength(float strength) { + display.setShadowStrength(strength); + return this; + } + + /** + * Configures the display entity if it's a TextDisplay. + * + * @param configurator A consumer that configures the TextDisplay + * @return This builder for chaining + */ + @SuppressWarnings("unchecked") + public DisplayBuilder textConfig(Consumer configurator) { + if (display instanceof TextDisplay) { + configurator.accept((TextDisplay) display); + } + return this; + } + + /** + * Configures the display entity if it's an ItemDisplay. + * + * @param configurator A consumer that configures the ItemDisplay + * @return This builder for chaining + */ + @SuppressWarnings("unchecked") + public DisplayBuilder itemConfig(Consumer configurator) { + if (display instanceof ItemDisplay) { + configurator.accept((ItemDisplay) display); + } + return this; + } + + /** + * Configures the display entity if it's a BlockDisplay. + * + * @param configurator A consumer that configures the BlockDisplay + * @return This builder for chaining + */ + @SuppressWarnings("unchecked") + public DisplayBuilder blockConfig(Consumer configurator) { + if (display instanceof BlockDisplay) { + configurator.accept((BlockDisplay) display); + } + return this; + } + + /** + * Gets the built display entity. + * + * @return The configured display entity + */ + public T build() { + return display; + } + } + + /** + * Utility class for working with Euler angles and quaternions. + */ + public static class EulerAngle { + /** + * Converts Euler angles to a quaternion. + * + * @param pitch The pitch in radians + * @param yaw The yaw in radians + * @param roll The roll in radians + * @return A quaternion representing the rotation + */ + public static Quaternionf toQuaternion(double pitch, double yaw, double roll) { + // Convert Euler angles to quaternion using JOML library + return new Quaternionf() + .rotateX((float) pitch) + .rotateY((float) yaw) + .rotateZ((float) roll); + } + + /** + * Converts a quaternion to Euler angles. + * + * @param quaternion The quaternion to convert + * @return An array containing [pitch, yaw, roll] in radians + */ + public static float[] toEulerAngles(Quaternionf quaternion) { + Vector3f euler = new Vector3f(); + quaternion.getEulerAnglesXYZ(euler); + return new float[]{euler.x, euler.y, euler.z}; + } + } + + /** + * Class for creating and managing animations for display entities. + */ + public static class Animator { + private final Plugin plugin; + private final Map animationTasks = new HashMap<>(); + + /** + * Creates a new Animator instance. + * + * @param plugin The plugin instance for scheduling tasks + */ + public Animator(Plugin plugin) { + this.plugin = plugin; + } + + /** + * Animates a display entity along a path. + * + * @param display The display entity to animate + * @param path The list of locations defining the path + * @param durationTicks The total duration of the animation in ticks + * @return A BukkitTask representing the animation task + */ + public BukkitTask animatePath(Display display, List path, long durationTicks) { + if (path.size() < 2) { + throw new IllegalArgumentException("Path must contain at least 2 points"); + } + + final int steps = path.size() - 1; + final long ticksPerStep = durationTicks / steps; + final UUID displayId = display.getUniqueId(); + + // Cancel any existing animation for this display + cancelAnimation(display); + + BukkitTask task = new BukkitRunnable() { + int currentStep = 0; + long currentTick = 0; + + @Override + public void run() { + if (currentStep >= steps || !display.isValid()) { + cancelAnimation(display); + return; + } + + Location start = path.get(currentStep); + Location end = path.get(currentStep + 1); + + // Calculate progress within current step (0.0 to 1.0) + float progress = (float) (currentTick % ticksPerStep) / ticksPerStep; + + // Interpolate between current points + Location interpolated = interpolateLocation(start, end, progress); + display.teleport(interpolated); + + currentTick++; + if (currentTick >= (currentStep + 1) * ticksPerStep) { + currentStep++; + } + } + }.runTaskTimer(plugin, 0L, 1L); + + animationTasks.put(displayId, task); + return task; + } + + /** + * Animates a transformation of a display entity. + * + * @param display The display entity to animate + * @param startTransform The starting transformation + * @param endTransform The ending transformation + * @param durationTicks The duration of the animation in ticks + * @return A BukkitTask representing the animation task + */ + public BukkitTask animateTransformation(Display display, Transformation startTransform, + Transformation endTransform, long durationTicks) { + final UUID displayId = display.getUniqueId(); + + // Cancel any existing animation for this display + cancelAnimation(display); + + BukkitTask task = new BukkitRunnable() { + long tick = 0; + + @Override + public void run() { + if (tick > durationTicks || !display.isValid()) { + display.setTransformation(endTransform); + cancelAnimation(display); + return; + } + + // Calculate progress (0.0 to 1.0) + float progress = (float) tick / durationTicks; + + // Interpolate transformation + Transformation interpolated = interpolateTransformation(startTransform, endTransform, progress); + display.setTransformation(interpolated); + + tick++; + } + }.runTaskTimer(plugin, 0L, 1L); + + animationTasks.put(displayId, task); + return task; + } + + /** + * Rotates a display entity around an axis. + * + * @param display The display entity to rotate + * @param axis The axis of rotation (should be normalized) + * @param degreesPerSecond The rotation speed in degrees per second + * @return A BukkitTask representing the rotation task + */ + public BukkitTask rotate(Display display, Vector3f axis, float degreesPerSecond) { + final UUID displayId = display.getUniqueId(); + + // Cancel any existing animation for this display + cancelAnimation(display); + + BukkitTask task = new BukkitRunnable() { + @Override + public void run() { + if (!display.isValid()) { + cancelAnimation(display); + return; + } + + // Calculate rotation per tick (20 ticks per second) + float rotationRadiansPerTick = (float) Math.toRadians(degreesPerSecond / 20.0f); + + // Get current transformation + Transformation transform = display.getTransformation(); + + // Create rotation quaternion for this tick + Quaternionf rotation = new Quaternionf().rotationAxis(rotationRadiansPerTick, axis); + + // Apply rotation to current left rotation + Quaternionf newRotation = new Quaternionf(transform.getLeftRotation()).mul(rotation); + + // Apply new transformation + display.setTransformation(new Transformation( + transform.getTranslation(), + newRotation, + transform.getScale(), + transform.getRightRotation() + )); + } + }.runTaskTimer(plugin, 0L, 1L); + + animationTasks.put(displayId, task); + return task; + } + + /** + * Cancels any ongoing animation for a display entity. + * + * @param display The display entity + */ + public void cancelAnimation(Display display) { + UUID displayId = display.getUniqueId(); + BukkitTask task = animationTasks.remove(displayId); + if (task != null) { + task.cancel(); + } + } + + /** + * Cancels all ongoing animations. + */ + public void cancelAllAnimations() { + for (BukkitTask task : animationTasks.values()) { + task.cancel(); + } + animationTasks.clear(); + } + + /** + * Interpolates between two locations. + * + * @param start The starting location + * @param end The ending location + * @param progress The progress from 0.0 to 1.0 + * @return An interpolated location + */ + private Location interpolateLocation(Location start, Location end, float progress) { + double x = start.getX() + (end.getX() - start.getX()) * progress; + double y = start.getY() + (end.getY() - start.getY()) * progress; + double z = start.getZ() + (end.getZ() - start.getZ()) * progress; + + // Also interpolate pitch and yaw if needed + float pitch = start.getPitch() + (end.getPitch() - start.getPitch()) * progress; + float yaw = start.getYaw() + (end.getYaw() - start.getYaw()) * progress; + + return new Location(start.getWorld(), x, y, z, yaw, pitch); + } + + /** + * Interpolates between two transformations. + * + * @param start The starting transformation + * @param end The ending transformation + * @param progress The progress from 0.0 to 1.0 + * @return An interpolated transformation + */ + private Transformation interpolateTransformation(Transformation start, Transformation end, float progress) { + // Interpolate translation + Vector3f startTranslation = start.getTranslation(); + Vector3f endTranslation = end.getTranslation(); + Vector3f interpolatedTranslation = new Vector3f(startTranslation) + .lerp(endTranslation, progress); + + // Interpolate left rotation (quaternion slerp) + Quaternionf startLeftRotation = start.getLeftRotation(); + Quaternionf endLeftRotation = end.getLeftRotation(); + Quaternionf interpolatedLeftRotation = new Quaternionf(startLeftRotation) + .slerp(endLeftRotation, progress); + + // Interpolate scale + Vector3f startScale = start.getScale(); + Vector3f endScale = end.getScale(); + Vector3f interpolatedScale = new Vector3f(startScale) + .lerp(endScale, progress); + + // Interpolate right rotation + Quaternionf startRightRotation = start.getRightRotation(); + Quaternionf endRightRotation = end.getRightRotation(); + Quaternionf interpolatedRightRotation = new Quaternionf(startRightRotation) + .slerp(endRightRotation, progress); + + return new Transformation( + interpolatedTranslation, + interpolatedLeftRotation, + interpolatedScale, + interpolatedRightRotation + ); + } + } + + /** + * Utility class for creating complex shapes and arrangements using display entities. + */ + public static class ShapeFactory { + private final Plugin plugin; + + /** + * Creates a new ShapeFactory. + * + * @param plugin The plugin instance + */ + public ShapeFactory(Plugin plugin) { + this.plugin = plugin; + } + + /** + * Creates a line of display entities. + * + * @param start The starting location + * @param end The ending location + * @param count The number of entities in the line + * @param creator A function that creates each display entity + * @return A list of created display entities + */ + public List createLine(Location start, Location end, int count, + DisplayCreator creator) { + List displays = new ArrayList<>(); + + for (int i = 0; i < count; i++) { + float progress = (float) i / (count - 1); + Location position = interpolateLocation(start, end, progress); + T display = creator.create(position); + displays.add(display); + } + + return displays; + } + + /** + * Creates a circle of display entities. + * + * @param center The center location of the circle + * @param radius The radius of the circle + * @param count The number of entities in the circle + * @param creator A function that creates each display entity + * @return A list of created display entities + */ + public List createCircle(Location center, double radius, int count, + DisplayCreator creator) { + List displays = new ArrayList<>(); + World world = center.getWorld(); + + for (int i = 0; i < count; i++) { + double angle = 2 * Math.PI * i / count; + double x = center.getX() + radius * Math.cos(angle); + double z = center.getZ() + radius * Math.sin(angle); + + Location position = new Location(world, x, center.getY(), z); + T display = creator.create(position); + displays.add(display); + } + + return displays; + } + + /** + * Creates a sphere of display entities. + * + * @param center The center location of the sphere + * @param radius The radius of the sphere + * @param rings The number of horizontal rings + * @param count The number of entities per ring + * @param creator A function that creates each display entity + * @return A list of created display entities + */ + public List createSphere(Location center, double radius, int rings, int count, + DisplayCreator creator) { + List displays = new ArrayList<>(); + World world = center.getWorld(); + + for (int ring = 0; ring <= rings; ring++) { + double phi = Math.PI * ring / rings; + double y = center.getY() + radius * Math.cos(phi); + double ringRadius = radius * Math.sin(phi); + + int pointsInRing = (ring == 0 || ring == rings) ? 1 : count; + + for (int i = 0; i < pointsInRing; i++) { + double theta = 2 * Math.PI * i / pointsInRing; + double x = center.getX() + ringRadius * Math.cos(theta); + double z = center.getZ() + ringRadius * Math.sin(theta); + + Location position = new Location(world, x, y, z); + T display = creator.create(position); + displays.add(display); + } + } + + return displays; + } + + /** + * Creates a grid of display entities. + * + * @param corner The corner location of the grid + * @param width The width of the grid in blocks + * @param height The height of the grid in blocks + * @param rows The number of rows + * @param columns The number of columns + * @param creator A function that creates each display entity + * @return A list of created display entities + */ + public List createGrid(Location corner, double width, double height, + int rows, int columns, DisplayCreator creator) { + List displays = new ArrayList<>(); + World world = corner.getWorld(); + + double xStep = width / (columns - 1); + double yStep = height / (rows - 1); + + for (int row = 0; row < rows; row++) { + for (int col = 0; col < columns; col++) { + double x = corner.getX() + col * xStep; + double y = corner.getY() + row * yStep; + + Location position = new Location(world, x, y, corner.getZ()); + T display = creator.create(position); + displays.add(display); + } + } + + return displays; + } + + /** + * Creates a cube of display entities. + * + * @param corner The corner location of the cube + * @param size The size of the cube + * @param count The number of entities per edge + * @param creator A function that creates each display entity + * @return A list of created display entities + */ + public List createCube(Location corner, double size, int count, + DisplayCreator creator) { + List displays = new ArrayList<>(); + World world = corner.getWorld(); + + double step = size / (count - 1); + + for (int i = 0; i < count; i++) { + for (int j = 0; j < count; j++) { + // Bottom face + displays.add(creator.create(new Location(world, + corner.getX() + i * step, + corner.getY(), + corner.getZ() + j * step))); + + // Top face + displays.add(creator.create(new Location(world, + corner.getX() + i * step, + corner.getY() + size, + corner.getZ() + j * step))); + + // Four edges + if (j == 0 || j == count - 1) { + for (int k = 1; k < count - 1; k++) { + displays.add(creator.create(new Location(world, + corner.getX() + i * step, + corner.getY() + k * step, + corner.getZ() + j * step))); + } + } + } + + // Remaining four edges + if (i == 0 || i == count - 1) { + for (int k = 1; k < count - 1; k++) { + for (int j = 1; j < count - 1; j++) { + displays.add(creator.create(new Location(world, + corner.getX() + i * step, + corner.getY() + k * step, + corner.getZ() + j * step))); + } + } + } + } + + return displays; + } + + /** + * Creates a text marquee that scrolls text. + * + * @param location The location for the marquee + * @param text The text to scroll + * @param width The width of the marquee in blocks + * @param scrollSpeed The scroll speed in blocks per second + * @return The created TextDisplay entity + */ + public TextDisplay createMarquee(Location location, String text, double width, double scrollSpeed) { + TextDisplay display = createTextDisplay(location, text); + + // Initialize at starting position + display.setTransformation(new Transformation( + new Vector3f((float) width, 0, 0), // Start off-screen to the right + new Quaternionf(), + new Vector3f(1, 1, 1), + new Quaternionf() + )); + + // Calculate total distance to travel (width + text width) + // This is an approximation as exact text width depends on font + float approximateCharWidth = 0.1f; // Approximation + float textWidth = text.length() * approximateCharWidth; + float totalDistance = (float) width + textWidth; + + // Calculate time to complete one cycle in ticks + long ticksPerCycle = (long) (totalDistance / scrollSpeed * 20); // 20 ticks per second + + new BukkitRunnable() { + float position = (float) width; // Start at right edge + + @Override + public void run() { + if (!display.isValid()) { + this.cancel(); + return; + } + + // Update position + position -= scrollSpeed / 20; // Divide by ticks per second + + // Reset position when text has scrolled past the left edge + if (position < -textWidth) { + position = (float) width; + } + + // Update transformation + Transformation transform = display.getTransformation(); + display.setTransformation(new Transformation( + new Vector3f(position, 0, 0), + transform.getLeftRotation(), + transform.getScale(), + transform.getRightRotation() + )); + } + }.runTaskTimer(plugin, 0L, 1L); + + return display; + } + + private Location interpolateLocation(Location start, Location end, float progress) { + double x = start.getX() + (end.getX() - start.getX()) * progress; + double y = start.getY() + (end.getY() - start.getY()) * progress; + double z = start.getZ() + (end.getZ() - start.getZ()) * progress; + + return new Location(start.getWorld(), x, y, z); + } + } + + /** + * Functional interface for creating display entities. + * + * @param The type of display entity to create + */ + @FunctionalInterface + public interface DisplayCreator { + /** + * Creates a display entity at the specified location. + * + * @param location The location to create the entity + * @return The created display entity + */ + T create(Location location); + } + + /** + * Utility class for creating particle effects with display entities. + */ + public static class ParticleEmitter { + private final Plugin plugin; + private final List particles = new ArrayList<>(); + private BukkitTask emitterTask; + private final Location origin; + private final DisplayCreator particleCreator; + private final int maxParticles; + private final double spawnRadius; + private final double velocity; + private final int lifetime; + private final Random random = new Random(); + + /** + * Creates a new particle emitter. + * + * @param plugin The plugin instance + * @param location The origin location for particles + * @param particleCreator A function that creates each particle entity + * @param maxParticles The maximum number of particles to display at once + * @param spawnRadius The radius in which particles spawn + * @param velocity The velocity of particles + * @param lifetime The lifetime of particles in ticks + */ + public ParticleEmitter(Plugin plugin, Location location, DisplayCreator particleCreator, + int maxParticles, double spawnRadius, double velocity, int lifetime) { + this.plugin = plugin; + this.origin = location.clone(); + this.particleCreator = particleCreator; + this.maxParticles = maxParticles; + this.spawnRadius = spawnRadius; + this.velocity = velocity; + this.lifetime = lifetime; + } + + /** + * Starts emitting particles. + * + * @param rate The number of particles to spawn per second + * @return This emitter for chaining + */ + public ParticleEmitter start(double rate) { + // Calculate ticks between spawns + long ticksBetweenSpawns = Math.max(1, (long) (20 / rate)); + + emitterTask = new BukkitRunnable() { + @Override + public void run() { + spawnParticle(); + + // Remove expired particles + long currentTime = System.currentTimeMillis(); + Iterator iterator = particles.iterator(); + while (iterator.hasNext()) { + Display particle = iterator.next(); + if (!particle.isValid() || + currentTime - particle.getTicksLived() * 50 > lifetime * 50) { + particle.remove(); + iterator.remove(); + } + } + } + }.runTaskTimer(plugin, 0, ticksBetweenSpawns); + + return this; + } + + /** + * Stops emitting particles. + * + * @param removeExisting Whether to remove existing particles + */ + public void stop(boolean removeExisting) { + if (emitterTask != null) { + emitterTask.cancel(); + emitterTask = null; + } + + if (removeExisting) { + for (Display particle : particles) { + if (particle.isValid()) { + particle.remove(); + } + } + particles.clear(); + } + } + + /** + * Spawns a single particle. + */ + private void spawnParticle() { + if (particles.size() >= maxParticles) { + // Remove oldest particle if at max capacity + Display oldest = particles.remove(0); + if (oldest.isValid()) { + oldest.remove(); + } + } + + // Generate random position within spawn radius + double angle = random.nextDouble() * 2 * Math.PI; + double distance = random.nextDouble() * spawnRadius; + double x = origin.getX() + distance * Math.cos(angle); + double y = origin.getY(); + double z = origin.getZ() + distance * Math.sin(angle); + + Location spawnLoc = new Location(origin.getWorld(), x, y, z); + Display particle = particleCreator.create(spawnLoc); + + // Set random velocity + double vx = velocity * (random.nextDouble() - 0.5); + double vy = velocity * random.nextDouble(); + double vz = velocity * (random.nextDouble() - 0.5); + + // Store initial velocity in metadata for animation + particle.setMetadata("velocity", new org.bukkit.metadata.FixedMetadataValue( + plugin, new Vector(vx, vy, vz))); + + // Animate the particle + new BukkitRunnable() { + int tick = 0; + + @Override + public void run() { + if (!particle.isValid() || tick > lifetime) { + this.cancel(); + return; + } + + // Get velocity from metadata + Vector vel = (Vector) particle.getMetadata("velocity").get(0).value(); + + // Apply velocity and gravity + Location current = particle.getLocation(); + vel.setY(vel.getY() - 0.05); // Apply gravity + + // Move particle + particle.teleport(current.add(vel)); + + // Scale down as it ages + float scale = 1.0f - ((float) tick / lifetime) * 0.5f; + Transformation transform = particle.getTransformation(); + particle.setTransformation(new Transformation( + transform.getTranslation(), + transform.getLeftRotation(), + new Vector3f(scale, scale, scale), + transform.getRightRotation() + )); + + tick++; + } + }.runTaskTimer(plugin, 0L, 1L); + + particles.add(particle); + } + } + + /** + * Utility class for creating holographic displays with text and items. + */ + public static class Hologram { + private final List displays = new ArrayList<>(); + private final Location location; + private final double lineHeight; + + /** + * Creates a new hologram. + * + * @param location The location of the hologram + * @param lineHeight The height between each line + */ + public Hologram(Location location, double lineHeight) { + this.location = location.clone(); + this.lineHeight = lineHeight; + } + + /** + * Adds a text line to the hologram. + * + * @param text The text to display + * @return This hologram for chaining + */ + public Hologram addTextLine(String text) { + Location lineLoc = getNextLineLocation(); + TextDisplay display = createTextDisplay(lineLoc, text); + + // Center the text + display.setAlignment(TextDisplay.TextAlignment.CENTER); + + // Make it visible from all sides + display.setBillboard(Display.Billboard.CENTER); + + // Set see-through background + display.setDefaultBackground(false); + + displays.add(display); + return this; + } + + /** + * Adds an item line to the hologram. + * + * @param item The item to display + * @return This hologram for chaining + */ + public Hologram addItemLine(org.bukkit.inventory.ItemStack item) { + Location lineLoc = getNextLineLocation(); + ItemDisplay display = createItemDisplay(lineLoc, item); + + // Make it visible from all sides + display.setBillboard(Display.Billboard.CENTER); + + // Scale down to a reasonable size + Transformation transform = display.getTransformation(); + display.setTransformation(new Transformation( + transform.getTranslation(), + transform.getLeftRotation(), + new Vector3f(0.6f, 0.6f, 0.6f), + transform.getRightRotation() + )); + + displays.add(display); + return this; + } + + /** + * Adds a block line to the hologram. + * + * @param blockData The block data to display + * @return This hologram for chaining + */ + public Hologram addBlockLine(org.bukkit.block.data.BlockData blockData) { + Location lineLoc = getNextLineLocation(); + BlockDisplay display = createBlockDisplay(lineLoc, blockData); + + // Make it visible from all sides + display.setBillboard(Display.Billboard.CENTER); + + // Scale down to a reasonable size + Transformation transform = display.getTransformation(); + display.setTransformation(new Transformation( + transform.getTranslation(), + transform.getLeftRotation(), + new Vector3f(0.4f, 0.4f, 0.4f), + transform.getRightRotation() + )); + + displays.add(display); + return this; + } + + /** + * Updates a text line in the hologram. + * + * @param index The index of the line to update + * @param newText The new text for the line + * @return This hologram for chaining + */ + public Hologram updateTextLine(int index, String newText) { + if (index < 0 || index >= displays.size()) { + throw new IndexOutOfBoundsException("Line index out of range"); + } + + Display display = displays.get(index); + if (display instanceof TextDisplay) { + ((TextDisplay) display).setText(newText); + } else { + throw new IllegalArgumentException("The specified line is not a text line"); + } + + return this; + } + + /** + * Updates an item line in the hologram. + * + * @param index The index of the line to update + * @param newItem The new item for the line + * @return This hologram for chaining + */ + public Hologram updateItemLine(int index, org.bukkit.inventory.ItemStack newItem) { + if (index < 0 || index >= displays.size()) { + throw new IndexOutOfBoundsException("Line index out of range"); + } + + Display display = displays.get(index); + if (display instanceof ItemDisplay) { + ((ItemDisplay) display).setItemStack(newItem); + } else { + throw new IllegalArgumentException("The specified line is not an item line"); + } + + return this; + } + + /** + * Updates a block line in the hologram. + * + * @param index The index of the line to update + * @param newBlockData The new block data for the line + * @return This hologram for chaining + */ + public Hologram updateBlockLine(int index, org.bukkit.block.data.BlockData newBlockData) { + if (index < 0 || index >= displays.size()) { + throw new IndexOutOfBoundsException("Line index out of range"); + } + + Display display = displays.get(index); + if (display instanceof BlockDisplay) { + ((BlockDisplay) display).setBlock(newBlockData); + } else { + throw new IllegalArgumentException("The specified line is not a block line"); + } + + return this; + } + + /** + * Removes a line from the hologram. + * + * @param index The index of the line to remove + * @return This hologram for chaining + */ + public Hologram removeLine(int index) { + if (index < 0 || index >= displays.size()) { + throw new IndexOutOfBoundsException("Line index out of range"); + } + + Display display = displays.remove(index); + if (display.isValid()) { + display.remove(); + } + + // Update positions of subsequent lines + for (int i = index; i < displays.size(); i++) { + Display d = displays.get(i); + Location newLoc = location.clone().add(0, -i * lineHeight, 0); + d.teleport(newLoc); + } + + return this; + } + + /** + * Gets the next line location based on the number of existing lines. + * + * @return The location for the next line + */ + private Location getNextLineLocation() { + int lineNumber = displays.size(); + return location.clone().add(0, -lineNumber * lineHeight, 0); + } + + /** + * Removes the entire hologram. + */ + public void remove() { + for (Display display : displays) { + if (display.isValid()) { + display.remove(); + } + } + displays.clear(); + } + + /** + * Gets all display entities in this hologram. + * + * @return A list of all display entities + */ + public List getDisplays() { + return Collections.unmodifiableList(displays); + } + } + + /** + * Utility class for creating 3D models using display entities. + */ + public static class ModelBuilder { + private final List parts = new ArrayList<>(); + private final Location origin; + private final World world; + + /** + * Creates a new model builder. + * + * @param origin The origin location for the model + */ + public ModelBuilder(Location origin) { + this.origin = origin.clone(); + this.world = origin.getWorld(); + } + + /** + * Adds a text part to the model. + * + * @param text The text to display + * @param x The relative X position + * @param y The relative Y position + * @param z The relative Z position + * @param scale The scale of the part + * @return This model builder for chaining + */ + public ModelBuilder addTextPart(String text, double x, double y, double z, float scale) { + Location partLoc = origin.clone().add(x, y, z); + TextDisplay display = createTextDisplay(partLoc, text); + + // Apply scale + Transformation transform = display.getTransformation(); + display.setTransformation(new Transformation( + transform.getTranslation(), + transform.getLeftRotation(), + new Vector3f(scale, scale, scale), + transform.getRightRotation() + )); + + parts.add(display); + return this; + } + + /** + * Adds an item part to the model. + * + * @param item The item to display + * @param x The relative X position + * @param y The relative Y position + * @param z The relative Z position + * @param scale The scale of the part + * @param rotation A quaternion representing the rotation + * @return This model builder for chaining + */ + public ModelBuilder addItemPart(org.bukkit.inventory.ItemStack item, double x, double y, double z, + float scale, Quaternionf rotation) { + Location partLoc = origin.clone().add(x, y, z); + ItemDisplay display = createItemDisplay(partLoc, item); + + // Apply scale and rotation + Transformation transform = display.getTransformation(); + display.setTransformation(new Transformation( + transform.getTranslation(), + rotation, + new Vector3f(scale, scale, scale), + transform.getRightRotation() + )); + + parts.add(display); + return this; + } + + /** + * Adds a block part to the model. + * + * @param blockData The block data to display + * @param x The relative X position + * @param y The relative Y position + * @param z The relative Z position + * @param scale The scale of the part + * @param rotation A quaternion representing the rotation + * @return This model builder for chaining + */ + public ModelBuilder addBlockPart(org.bukkit.block.data.BlockData blockData, double x, double y, double z, + float scale, Quaternionf rotation) { + Location partLoc = origin.clone().add(x, y, z); + BlockDisplay display = createBlockDisplay(partLoc, blockData); + + // Apply scale and rotation + Transformation transform = display.getTransformation(); + display.setTransformation(new Transformation( + transform.getTranslation(), + rotation, + new Vector3f(scale, scale, scale), + transform.getRightRotation() + )); + + parts.add(display); + return this; + } + + /** + * Rotates the entire model around a specified axis. + * + * @param axis The axis of rotation + * @param angleRadians The angle in radians + * @return This model builder for chaining + */ + public ModelBuilder rotate(Vector3f axis, float angleRadians) { + Quaternionf rotation = new Quaternionf().rotationAxis(angleRadians, axis); + + for (Display part : parts) { + // Calculate new position relative to origin + Location partLoc = part.getLocation(); + double dx = partLoc.getX() - origin.getX(); + double dy = partLoc.getY() - origin.getY(); + double dz = partLoc.getZ() - origin.getZ(); + + // Create a vector for the position + Vector3f pos = new Vector3f((float) dx, (float) dy, (float) dz); + + // Rotate the position + pos.rotate(rotation); + + // Update the part's location + Location newLoc = origin.clone().add(pos.x, pos.y, pos.z); + part.teleport(newLoc); + + // Also update the part's own rotation + Transformation transform = part.getTransformation(); + Quaternionf newRotation = new Quaternionf(transform.getLeftRotation()).mul(rotation); + + part.setTransformation(new Transformation( + transform.getTranslation(), + newRotation, + transform.getScale(), + transform.getRightRotation() + )); + } + + return this; + } + + /** + * Moves the entire model. + * + * @param dx The X offset + * @param dy The Y offset + * @param dz The Z offset + * @return This model builder for chaining + */ + public ModelBuilder move(double dx, double dy, double dz) { + origin.add(dx, dy, dz); + + for (Display part : parts) { + Location partLoc = part.getLocation(); + partLoc.add(dx, dy, dz); + part.teleport(partLoc); + } + + return this; + } + + /** + * Scales the entire model. + * + * @param factor The scale factor + * @return This model builder for chaining + */ + public ModelBuilder scale(float factor) { + for (Display part : parts) { + // Update position relative to origin + Location partLoc = part.getLocation(); + double dx = partLoc.getX() - origin.getX(); + double dy = partLoc.getY() - origin.getY(); + double dz = partLoc.getZ() - origin.getZ(); + + // Scale the position + dx *= factor; + dy *= factor; + dz *= factor; + + // Update location + Location newLoc = origin.clone().add(dx, dy, dz); + part.teleport(newLoc); + + // Update scale + Transformation transform = part.getTransformation(); + Vector3f currentScale = transform.getScale(); + Vector3f newScale = new Vector3f( + currentScale.x * factor, + currentScale.y * factor, + currentScale.z * factor + ); + + part.setTransformation(new Transformation( + transform.getTranslation(), + transform.getLeftRotation(), + newScale, + transform.getRightRotation() + )); + } + + return this; + } + + /** + * Removes the entire model. + */ + public void remove() { + for (Display part : parts) { + if (part.isValid()) { + part.remove(); + } + } + parts.clear(); + } + + /** + * Gets all display entities in this model. + * + * @return A list of all display entities + */ + public List getParts() { + return Collections.unmodifiableList(parts); + } + } + + /** + * Utility class for creating display entity based UIs. + */ + public static class UIBuilder { + private final Plugin plugin; + private final Location origin; + private final List elements = new ArrayList<>(); + private final Map namedElements = new HashMap<>(); + + /** + * Creates a new UI builder. + * + * @param plugin The plugin instance + * @param origin The origin location for the UI + */ + public UIBuilder(Plugin plugin, Location origin) { + this.plugin = plugin; + this.origin = origin.clone(); + } + + /** + * Adds a text element to the UI. + * + * @param id A unique identifier for the element + * @param text The text to display + * @param x The relative X position + * @param y The relative Y position + * @param alignment The text alignment + * @return This UI builder for chaining + */ + public UIBuilder addTextElement(String id, String text, double x, double y, TextDisplay.TextAlignment alignment) { + Location elemLoc = origin.clone().add(x, y, 0); + TextDisplay display = createTextDisplay(elemLoc, text); + + // Configure the text display + display.setAlignment(alignment); + display.setBillboard(Display.Billboard.FIXED); + display.setDefaultBackground(false); + + elements.add(display); + namedElements.put(id, display); + return this; + } + + /** + * Adds an item element to the UI. + * + * @param id A unique identifier for the element + * @param item The item to display + * @param x The relative X position + * @param y The relative Y position + * @param scale The scale of the item + * @return This UI builder for chaining + */ + public UIBuilder addItemElement(String id, org.bukkit.inventory.ItemStack item, double x, double y, float scale) { + Location elemLoc = origin.clone().add(x, y, 0); + ItemDisplay display = createItemDisplay(elemLoc, item); + + // Configure the item display + display.setBillboard(Display.Billboard.FIXED); + + // Apply scale + Transformation transform = display.getTransformation(); + display.setTransformation(new Transformation( + transform.getTranslation(), + transform.getLeftRotation(), + new Vector3f(scale, scale, scale), + transform.getRightRotation() + )); + + elements.add(display); + namedElements.put(id, display); + return this; + } + + /** + * Adds a block element to the UI. + * + * @param id A unique identifier for the element + * @param blockData The block data to display + * @param x The relative X position + * @param y The relative Y position + * @param scale The scale of the block + * @return This UI builder for chaining + */ + public UIBuilder addBlockElement(String id, org.bukkit.block.data.BlockData blockData, double x, double y, float scale) { + Location elemLoc = origin.clone().add(x, y, 0); + BlockDisplay display = createBlockDisplay(elemLoc, blockData); + + // Configure the block display + display.setBillboard(Display.Billboard.FIXED); + + // Apply scale + Transformation transform = display.getTransformation(); + display.setTransformation(new Transformation( + transform.getTranslation(), + transform.getLeftRotation(), + new Vector3f(scale, scale, scale), + transform.getRightRotation() + )); + + elements.add(display); + namedElements.put(id, display); + return this; + } + + /** + * Updates a text element in the UI. + * + * @param id The identifier of the element to update + * @param newText The new text for the element + * @return This UI builder for chaining + */ + public UIBuilder updateTextElement(String id, String newText) { + Display element = namedElements.get(id); + if (element == null) { + throw new IllegalArgumentException("No element found with ID: " + id); + } + + if (element instanceof TextDisplay) { + ((TextDisplay) element).setText(newText); + } else { + throw new IllegalArgumentException("Element with ID " + id + " is not a text element"); + } + + return this; + } + + /** + * Updates an item element in the UI. + * + * @param id The identifier of the element to update + * @param newItem The new item for the element + * @return This UI builder for chaining + */ + public UIBuilder updateItemElement(String id, org.bukkit.inventory.ItemStack newItem) { + Display element = namedElements.get(id); + if (element == null) { + throw new IllegalArgumentException("No element found with ID: " + id); + } + + if (element instanceof ItemDisplay) { + ((ItemDisplay) element).setItemStack(newItem); + } else { + throw new IllegalArgumentException("Element with ID " + id + " is not an item element"); + } + + return this; + } + + /** + * Updates a block element in the UI. + * + * @param id The identifier of the element to update + * @param newBlockData The new block data for the element + * @return This UI builder for chaining + */ + public UIBuilder updateBlockElement(String id, org.bukkit.block.data.BlockData newBlockData) { + Display element = namedElements.get(id); + if (element == null) { + throw new IllegalArgumentException("No element found with ID: " + id); + } + + if (element instanceof BlockDisplay) { + ((BlockDisplay) element).setBlock(newBlockData); + } else { + throw new IllegalArgumentException("Element with ID " + id + " is not a block element"); + } + + return this; + } + + /** + * Moves the entire UI. + * + * @param newLocation The new location for the UI origin + * @return This UI builder for chaining + */ + public UIBuilder moveTo(Location newLocation) { + // Calculate offset + double dx = newLocation.getX() - origin.getX(); + double dy = newLocation.getY() - origin.getY(); + double dz = newLocation.getZ() - origin.getZ(); + + // Update origin + origin.setWorld(newLocation.getWorld()); + origin.setX(newLocation.getX()); + origin.setY(newLocation.getY()); + origin.setZ(newLocation.getZ()); + + // Move all elements + for (Display element : elements) { + Location elemLoc = element.getLocation(); + elemLoc.add(dx, dy, dz); + element.teleport(elemLoc); + } + + return this; + } + + /** + * Removes the entire UI. + */ + public void remove() { + for (Display element : elements) { + if (element.isValid()) { + element.remove(); + } + } + elements.clear(); + namedElements.clear(); + } + + /** + * Gets a specific element by its ID. + * + * @param id The ID of the element + * @return The display entity, or null if not found + */ + public Display getElement(String id) { + return namedElements.get(id); + } + + /** + * Gets all elements in this UI. + * + * @return A list of all display entities + */ + public List getAllElements() { + return Collections.unmodifiableList(elements); + } + } +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/utils/visual/DisplayUtils.java b/src/main/java/me/trouper/trimserver/utils/visual/DisplayUtils.java new file mode 100644 index 0000000..c5593e0 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/visual/DisplayUtils.java @@ -0,0 +1,247 @@ +package me.trouper.trimserver.utils.visual; + +import me.trouper.trimserver.server.Main; +import me.trouper.trimserver.utils.misc.Randomizer; +import org.bukkit.Bukkit; +import org.bukkit.Color; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +public class DisplayUtils implements Main { + + + public static void sphere(Location center, double radius, double verticalStep, double maxDistanceBetweenPoints, Consumer action) { + for (double yOffset = -radius; yOffset <= radius; yOffset += verticalStep) { + double horizontalRadius = Math.sqrt(radius * radius - yOffset * yOffset); + + if (horizontalRadius < 0.01) { + Location point = center.clone().add(0, yOffset, 0); + action.accept(point); + continue; + } + + double circumference = 2 * Math.PI * horizontalRadius; + int points = Math.max(4, (int) (circumference / maxDistanceBetweenPoints)); + double angleStep = 360.0 / points; + + for (int i = 0; i < points; i++) { + double theta = i * angleStep; + double x = Math.cos(Math.toRadians(theta)) * horizontalRadius; + double z = Math.sin(Math.toRadians(theta)) * horizontalRadius; + Location point = center.clone().add(x, yOffset, z); + action.accept(point); + } + } + } + + public static void sphereWave(Location center, double maxRadius, double radialStep, double verticalStep, double maxDistanceBetweenPoints, Consumer action) { + AtomicReference currentRadius = new AtomicReference<>(radialStep); + + Bukkit.getScheduler().scheduleSyncRepeatingTask(main.getPlugin(), () -> { + double r = currentRadius.get(); + if (r > maxRadius) return; + + sphere(center, r, verticalStep, maxDistanceBetweenPoints, action); + currentRadius.set(r + radialStep); + }, 0L, 1L); + } + + + + public static final Function> PARTICLE_FACTORY = particle -> l -> l.getWorld().spawnParticle(particle, l, 1, 0, 0, 0, 0); + + public static final BiFunction> DUST_PARTICLE_FACTORY = (color, thickness) -> { + Particle.DustOptions dust = new Particle.DustOptions(color, thickness); + return l -> l.getWorld().spawnParticle(Particle.DUST, l, 1, 0, 0, 0, 0, dust); + }; + + public static final Function> FLAME_PARTICLE_FACTORY = soul -> { + Particle flame = soul ? Particle.SOUL_FIRE_FLAME : Particle.FLAME; + return l -> l.getWorld().spawnParticle(flame, l, 1, 0, 0, 0, 0); + }; + + public static void ring(Location loc, double radius, Color color, float thickness) { + ring(loc, radius, DUST_PARTICLE_FACTORY.apply(color, thickness)); + } + + public static void ring(Location loc, double radius, Consumer action) { + for (int theta = 0; theta < 360; theta += 10) { + double x = Math.cos(Math.toRadians(theta)) * radius; + double z = Math.sin(Math.toRadians(theta)) * radius; + Location newLoc = loc.clone().add(x, 0, z); + action.accept(newLoc); + } + } + + public static void ring(Location loc, double radius, double maxDistanceBetweenPoints, Consumer action) { + arc(loc, radius, 0, 360, maxDistanceBetweenPoints, action); + } + + public static void wave(Location loc, double radius, Color color, float thickness, double gap) { + wave(loc, radius, DUST_PARTICLE_FACTORY.apply(color, thickness), gap); + } + + public static void wave(Location loc, double radius, Consumer action, double gap) { + AtomicReference i = new AtomicReference<>(gap); + Bukkit.getScheduler().scheduleSyncRepeatingTask(main.getPlugin(), () -> { + if (i.get() >= radius) { + return; + } + ring(loc, i.get(), action); + i.set(i.get() + gap); + }, 0, 1); + } + + public static void wave(Location loc, double radius, double radialGap, double maxDistanceBetweenPoints, Consumer action) { + AtomicReference r = new AtomicReference<>(radialGap); + Bukkit.getScheduler().scheduleSyncRepeatingTask(main.getPlugin(), () -> { + if (r.get() > radius) return; + ring(loc, r.get(), maxDistanceBetweenPoints, action); + r.set(r.get() + radialGap); + }, 0, 1); + } + + public static void disc(Location loc, double radius, Consumer action, double gap) { + for (double i = gap; i < radius; i += gap) { + ring(loc, i, action); + } + } + + public static void disc(Location loc, double radius, double radialGap, double maxDistanceBetweenPoints, Consumer action) { + for (double r = radialGap; r <= radius; r += radialGap) { + ring(loc, r, maxDistanceBetweenPoints, action); + } + } + + public static void helix(Location loc, double radius, Consumer action, double gap, int height) { + int theta = 0; + for (double y = 0; y <= height; y += gap) { + double x = Math.cos(Math.toRadians(theta)) * radius; + double z = Math.sin(Math.toRadians(theta)) * radius; + + Location newLoc = loc.clone().add(x, y, z); + action.accept(newLoc); + theta += 10; + } + } + + public static void vortex(Location loc, double radius, Consumer action, double gapH, double gapV, int height) { + double r = radius; + int theta = 0; + for (double y = 0; y <= height; y += gapV) { + double x = Math.cos(Math.toRadians(theta)) * r; + double z = Math.sin(Math.toRadians(theta)) * r; + + Location newLoc = loc.clone().add(x, y, z); + action.accept(newLoc); + r += gapH; + theta += 10; + } + } + + public static void beam(Location loc, Consumer action, double gap, int height) { + for (double y = 0; y <= height; y += gap) { + Location newLoc = loc.clone().add(0, y, 0); + action.accept(newLoc); + } + } + + public static void arc(Location loc, double radius, int angleFrom, int angleTo, Consumer action) { + for (int theta = angleFrom; theta < angleTo; theta += 10) { + double x = Math.cos(Math.toRadians(theta)) * radius; + double z = Math.sin(Math.toRadians(theta)) * radius; + Location newLoc = loc.clone().add(x, 0, z); + action.accept(newLoc); + } + } + + public static void arc(Location loc, double radius, int angleFrom, int angleTo, double maxDistanceBetweenPoints, Consumer action) { + int angleSpan = angleTo - angleFrom; + if (angleSpan <= 0) return; + + int points = Math.max(2, (int) ((2 * Math.PI * radius * (angleSpan / 360.0)) / maxDistanceBetweenPoints)); + double angleStep = (double) angleSpan / points; + + for (int i = 0; i <= points; i++) { + double theta = angleFrom + (i * angleStep); + double x = Math.cos(Math.toRadians(theta)) * radius; + double z = Math.sin(Math.toRadians(theta)) * radius; + Location point = loc.clone().add(x, 0, z); + action.accept(point); + } + } + + + public static void fan(Location loc, double radius, int angleFrom, int angleTo, Consumer action, double gap) { + for (double i = gap; i < radius; i += gap) { + arc(loc, i, angleFrom, angleTo, action); + } + } + + public static void fan(Location loc, double radius, int angleFrom, int angleTo, double maxDistanceBetweenPoints, Consumer action, double radialGap) { + for (double r = radialGap; r < radius; r += radialGap) { + arc(loc, r, angleFrom, angleTo, maxDistanceBetweenPoints, action); + } + } + + + public static void fanWave(Location loc, double radius, int sections, Consumer action, double gap) { + double arcLength = 360.0 / sections; + AtomicReference i = new AtomicReference<>(0.0); + Bukkit.getScheduler().scheduleSyncRepeatingTask(main.getPlugin(), () -> { + if (i.get() >= 360) { + return; + } + double start = i.get(); + fan(loc, radius, (int)start, (int)(start + arcLength), action, gap); + i.set(i.get() + arcLength); + }, 0, 5); + } + + public static void fanWaveRandom(Location loc, double radius, int sections, Consumer action, double gap) { + double arcLength = 360.0 / sections; + List ints = new ArrayList<>(); + for (double start = 0; start < 360; start += arcLength) { + ints.add(start); + } + + AtomicInteger i = new AtomicInteger(0); + Randomizer random = new Randomizer(); + Bukkit.getScheduler().scheduleSyncRepeatingTask(main.getPlugin(), () -> { + if (i.get() >= sections) { + return; + } + double start = random.getRandomElement(ints); + ints.remove(start); + fan(loc, radius, (int)start, (int)(start + arcLength), action, gap); + i.getAndIncrement(); + }, 0, 5); + } + + public static void waveFan(Location loc, double radius, int angleFrom, int angleTo, double maxDistanceBetweenPoints, Consumer action, double radialGap) { + AtomicReference r = new AtomicReference<>(radialGap); + Bukkit.getScheduler().scheduleSyncRepeatingTask(main.getPlugin(), () -> { + if (r.get() >= radius) return; + arc(loc, r.get(), angleFrom, angleTo, maxDistanceBetweenPoints, action); + r.set(r.get() + radialGap); + }, 0, 1); + } + + public static void waveFan(Location loc, double radius, Vector direction, int angle, double maxDistanceBetweenPoints, Consumer action, double radialGap) { + double baseAngle = Math.toDegrees(Math.atan2(direction.getZ(), direction.getX())); + int angleFrom = (int) (baseAngle - angle / 2.0); + int angleTo = (int) (baseAngle + angle / 2.0); + waveFan(loc, radius, angleFrom, angleTo, maxDistanceBetweenPoints, action, radialGap); + } + + +} \ No newline at end of file diff --git a/src/main/java/me/trouper/trimserver/utils/visual/Point.java b/src/main/java/me/trouper/trimserver/utils/visual/Point.java new file mode 100644 index 0000000..6b54899 --- /dev/null +++ b/src/main/java/me/trouper/trimserver/utils/visual/Point.java @@ -0,0 +1,121 @@ +package me.trouper.trimserver.utils.visual; + +import me.trouper.trimserver.utils.Text; +import me.trouper.trimserver.utils.Verbose; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Entity; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +public class Point { + private final Location loc; + private final World world; + private final Block block; + private final boolean missed; + private final double traveledDist; + + public Point(Location loc, double traveledDist, boolean missed) { + this.loc = loc; + this.world = loc.getWorld(); + this.block = loc.getBlock(); + this.missed = missed; + this.traveledDist = traveledDist; + + if (world == null) { + throw new IllegalArgumentException("point world cannot be null!"); + } + } + + public List getNearbyEntities(Entity exclude, int range, boolean requireContact, double expansionX, double expansionY, double expansionZ, Predicate filter) { + return new ArrayList<>(world.getNearbyEntities(loc, range, range, range, e -> { + if (requireContact && !e.getBoundingBox().expand(expansionX, expansionY, expansionZ).contains(loc.toVector())) { + return false; + } + return filter.test(e) && e != exclude; + })); + } + + public List getNearbyEntities(Entity exclude, int range, boolean requireContact, double expansion, Predicate filter) { + return getNearbyEntities(exclude, range, requireContact, expansion, expansion, expansion, filter); + } + + public List getNearbyEntities(Entity exclude, int range, boolean requireContact, Predicate filter) { + return getNearbyEntities(exclude, range, requireContact, 0, filter); + } + + public List getNearbyEntities(Entity exclude, int range, Predicate filter) { + return getNearbyEntities(exclude, range, false, filter); + } + + public BlockFace getBlockFace(Vector vector) { + if (block == null || block.isPassable()) { + return null; + } + + double x = vector.getX() - block.getX(); + double y = vector.getY() - block.getY(); + double z = vector.getZ() - block.getZ(); + + double min = 0; + BlockFace face = null; + + if (x < min) { + min = x; + face = BlockFace.WEST; + } + if (1 - x < min) { + min = 1 - x; + face = BlockFace.EAST; + } + if (y < min) { + min = y; + face = BlockFace.DOWN; + } + if (1 - y < min) { + min = 1 - y; + face = BlockFace.UP; + } + if (z < min) { + min = z; + face = BlockFace.NORTH; + } + if (1 - z < min) { + face = BlockFace.SOUTH; + } + + Verbose.send("Block face was %s. X: %s, Y: %s, Z: %s.", Text.formatEnum(face),x,y,z); + + return face; + } + + + public double getTraveledDist() { + return traveledDist; + } + + public boolean wasMissed() { + return missed; + } + + public Block getBlock() { + return block; + } + + public Location getLoc() { + return loc; + } + + public World getWorld() { + return world; + } + + public double distance(Location other) { + return other.distance(loc); + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..0146b19 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,20 @@ +name: TrimServer +version: '1.0-SNAPSHOT' +main: me.trouper.trimserver.TrimServer +api-version: '1.21' +prefix: TrimServer +load: STARTUP +authors: [ obvWolf ] +description: Armor trims give you abilities +commands: + trims: + permission: trims.admin + description: Command for managing trims. + info: + description: Provides info on armor trim abilities. + usage: /info + trust: + description: Make your friends immune to your abilities. + usage: " " + aliases: + - t \ No newline at end of file