Git Fixer

This commit is contained in:
2025-05-14 13:17:26 -05:00
commit 1fccfad0a5
70 changed files with 11750 additions and 0 deletions

119
.gitignore vendored Normal file
View File

@@ -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

60
build.gradle Normal file
View File

@@ -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
}
}

0
gradle.properties Normal file
View File

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -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

249
gradlew vendored Normal file
View File

@@ -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" "$@"

92
gradlew.bat vendored Normal file
View File

@@ -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

1
settings.gradle Normal file
View File

@@ -0,0 +1 @@
rootProject.name = 'TrimServer'

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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<Config> {
public boolean debugMode = false;
public List<String> 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 = "&#00ffaa";
public String prefix = "&9TrimServer> &7";
public String pluginName = "TrimServer";
public boolean fancyAlerts = true;
}
}

View File

@@ -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<Storage> {
@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<String, Set<String>> playerTrust = new HashMap<>();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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 <trim> [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 <toggle|include|exclude>");
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 <method>");
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 <method>");
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();
}
}

View File

@@ -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()));
}
}

View File

@@ -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 <player>");
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 <player>");
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<String> trustees = main.man().trustBackend.getTrustees((Player) sender);
Set<String> 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")
);
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<TrimPattern, AbstractAbility> 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<AbilityCooldown> 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<String> abilities() {
List<String> 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;
}
}
}

View File

@@ -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<String> 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<String> 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<String> 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<String> 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);
}
}

View File

@@ -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<String, List<Map.Entry<String, Integer>>> groupedInfo = new LinkedHashMap<>();
List<TrimMaterial> 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<String, List<Map.Entry<String, Integer>>> entry : groupedInfo.entrySet()) {
String description = entry.getKey();
List<Map.Entry<String, Integer>> 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<String, Integer> 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";
}
}

View File

@@ -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;
}

View File

@@ -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.";
}

View File

@@ -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<BlockDisplay> 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<BlockDisplay> 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<List<BlockDisplay>> teethRows = new ArrayList<>();
for (int row = 0; row < 4; row++) {
List<BlockDisplay> 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<BlockDisplay> allEntities; // All worm parts
private final List<BlockDisplay> bodyEntities;
private final List<List<BlockDisplay>> 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<BlockDisplay> allEntities, List<BlockDisplay> bodyEntities, List<List<BlockDisplay>> 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<BlockDisplay> 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<LivingEntity> 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<BlockDisplay> 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<BlockDisplay> 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();
}
}
}

View File

@@ -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<Player> 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);
}
}

View File

@@ -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<UUID, BukkitTask> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<UUID, BukkitTask> activeRiders = new HashMap<>();
private final Map<UUID, Breeze> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<UUID, BukkitTask> 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);
}
}

View File

@@ -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<BlockDisplay> 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;
}
}

View File

@@ -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<Entity> 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<BlockDisplay> 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<Player> 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<Entity> 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;
}
}

View File

@@ -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<UUID, BukkitTask> activeShellTasks = new ConcurrentHashMap<>();
private final Map<UUID, List<BlockDisplay>> activeShellDisplays = new ConcurrentHashMap<>();
private final Map<UUID, AttributeModifier> knockbackModifiers = new ConcurrentHashMap<>();
private final Map<UUID, Integer> 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<BlockDisplay> 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<BlockDisplay> 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<BlockDisplay> 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;
}
}

View File

@@ -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<Player> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<UUID, List<Entity>> activeSpires = new HashMap<>();
private final Map<UUID, BukkitTask> activeTasks = new HashMap<>();
private final Set<UUID> 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<Entity> 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<Location> 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<Player> 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<Entity> 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<Entity> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<UUID, DisguiseData> 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<UUID, DisguiseData> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<BlockDisplay> 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;
}
}

View File

@@ -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<String> lore1 = meta1.hasLore() ? meta1.getLore() : null;
List<String> 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<Enchantment, Integer> enchants1 = meta1.getEnchants();
Map<Enchantment, Integer> 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<Attribute, AttributeModifier> modifiers1 = meta1.getAttributeModifiers();
Multimap<Attribute, AttributeModifier> 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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<LivingEntity> filter, Consumer<LivingEntity> action) {
World world = center.getWorld();
if (world == null) {
return false;
}
List<LivingEntity> 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<LivingEntity> filter, Consumer<LivingEntity> action) {
World world = center.getWorld();
if (world == null) {
return false;
}
List<LivingEntity> 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.
* <p>
* 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.
* <p>
* 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<LivingEntity> 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<LivingEntity> getClosestLivingEntity(Location center, double maxDistance, Predicate<LivingEntity> 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<Player> 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<Player> getClosestPlayer(Location center, double maxDistance, Predicate<Player> filter) {
if (center == null || center.getWorld() == null || filter == null) {
return Optional.empty();
}
World world = center.getWorld();
double maxDistanceSquared = maxDistance * maxDistance;
List<Player> 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<LivingEntity> 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<LivingEntity> getLowestHealthLivingEntity(Location center, double radius, Predicate<LivingEntity> 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<LivingEntity> 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<LivingEntity> getLivingEntityClosestToVector(Location originEyeLocation, Vector direction, double maxDistance, double maxAngleRadians, Predicate<LivingEntity> filter) {
if (originEyeLocation == null || originEyeLocation.getWorld() == null || direction == null || filter == null) {
return Optional.empty();
}
World world = originEyeLocation.getWorld();
Vector normalizedDirection = direction.clone().normalize();
List<LivingEntity> 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);
}
}

View File

@@ -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<String> 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<String> wrapText(String text, int maxLineLength, int offset) {
List<String> 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<Character> 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();
}
}

View File

@@ -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));
}
}
}

View File

@@ -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<Arg> 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 extends Enum<?>> T toEnum(Class<T> enumType) {
return toEnum(enumType, null);
}
public <T extends Enum<?>> T toEnum(Class<T> 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;
}
}
}

View File

@@ -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;
}

View File

@@ -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!";
}

View File

@@ -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<String> 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<String> a = new ArrayList<>(node.getOptions());
if (node.isOptionsRegex()) {
List<String> 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<CommandRegistry> getRegistry() {
return Voidable.of(this.getClass().getAnnotation(CommandRegistry.class));
}
}

View File

@@ -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<CompletionBuilder> options;
private boolean isBranch;
private String regex;
CompletionBuilder(List<String> 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<String> 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<? extends Enum<?>> type, boolean lowercase) {
return arg(ArrayUtils.enumNames(type, lowercase));
}
public CompletionBuilder argEnum(Class<? extends Enum<?>> 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 <T> CompletionBuilder arg(Collection<T> input, Function<T, String> 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;
}
}

View File

@@ -0,0 +1,88 @@
package me.trouper.trimserver.utils.commands.completions;
import java.util.ArrayList;
import java.util.List;
public class CompletionNode {
final List<String> values;
final List<CompletionNode> nextOptions;
final String regex;
CompletionNode(List<String> values, List<CompletionNode> 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<String> getOptions() {
List<String> 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<CompletionNode> getNextOptions() {
return nextOptions;
}
public List<String> getValues() {
return values;
}
}

View File

@@ -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 <I> input
* @param <O> output
*/
public static <I,O> List<O> map(Iterable<I> e, Function<I,O> a) {
List<O> list = new ArrayList<>();
e.forEach(i -> list.add(a.apply(i)));
return list;
}
public static <T> String toPrettyString(List<T> list) {
return "§7[§e" + String.join("§7, §e", ArrayUtils.map(list, Object::toString)) + "§7]";
}
public static <E extends Enum<?>> List<String> enumNames(Class<E> type, boolean lowercase) {
List<String> names = new ArrayList<>();
for (E constant : type.getEnumConstants()) {
String name = constant.name();
names.add(lowercase ? name.toLowerCase() : name);
}
return names;
}
public static <E extends Enum<?>> List<String> enumNames(Class<E> type) {
return enumNames(type, true);
}
public static List<String> playerNames() {
return map(Bukkit.getOnlinePlayers(), Player::getName);
}
@SafeVarargs
public static <T> List<T> bind(Iterable<T> tList, T... ts) {
List<T> list = Arrays.asList(ts);
tList.forEach(list::add);
return list;
}
public static <T> List<T> reversed(List<T> input) {
Collections.reverse(input);
return input;
}
public static <T> List<T> reversed(Iterable<T> input) {
List<T> list = new ArrayList<>();
input.forEach(list::add);
return reversed(list);
}
public static <T> void reverseForEach(Iterable<T> input, Consumer<T> action) {
reversed(input).forEach(action);
}
}

View File

@@ -0,0 +1,42 @@
package me.trouper.trimserver.utils.misc;
import java.util.HashMap;
import java.util.Map;
public class Cooldown<T> {
private final Map<T, Long> timer;
public Cooldown() {
this.timer = new HashMap<>();
}
private <O> 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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<T> {
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> O getOrDef(O val, O def) {
return val != null ? val : def;
}
static <T extends JsonSerializable<?>> T load(File file, Class<T> 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 extends JsonSerializable<?>> T load(String path, Class<T> jsonSerializable, T fallback) {
return load(new File(path), jsonSerializable, fallback);
}
}

View File

@@ -0,0 +1,81 @@
package me.trouper.trimserver.utils.misc;
import java.util.List;
public class Randomizer {
public Randomizer() {
}
public <T> T getRandomElement(List<T> list) {
if (list == null || list.isEmpty()) {
return null;
}
return list.get(getRandomIndex(list.size()));
}
@SafeVarargs
public final <T> T getRandomElement(T... list) {
if (list == null || list.length == 0) {
return null;
}
return list[getRandomIndex(list.length)];
}
private <T> 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);
}
}

View File

@@ -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<T> 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 <U> Voidable<U> map(Function<T, U> function) {
return isPresent() ? of(function.apply(value)) : of(null);
}
public void accept(Consumer<T> action) {
if (isPresent()) {
action.accept(value);
}
}
public void accept(Consumer<T> 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 <T> Voidable<T> of(T value) {
return new Voidable<>(value);
}
}

View File

@@ -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<World> worlds = plugin.getServer().getWorlds();
List<Entity> 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<Player> viewers) {
outline(display, location, 0.05, stayTime, viewers);
}
public static void outline(Material display, Location corner1, Location corner2, double thickness, long stayTime, List<Player> 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<Player> 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<Player> 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<Player> 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<Player> 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<BlockDisplay> onEntitySpawn, List<Player> 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<BlockDisplay> outline(Material display, Location location, long stayTime) {
return outline(display, location, 0.05, stayTime);
}
public static List<BlockDisplay> 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<BlockDisplay> 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);
}
}

View File

@@ -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<Point> 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<Point> HIT_ENTITY = point -> {
return !point.getNearbyEntities(null, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead()).isEmpty();
};
public static final Predicate<Point> HIT_BLOCK_OR_ENTITY = point -> {
return HIT_BLOCK.test(point) || HIT_ENTITY.test(point);
};
public static final Predicate<Point> HIT_BLOCK_AND_ENTITY = point -> {
return HIT_BLOCK.test(point) && HIT_ENTITY.test(point);
};
public static Predicate<Point> hitEntityExclude(Entity exclude) {
return point -> !point.getNearbyEntities(exclude, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead()).isEmpty();
}
public static Predicate<Point> 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<Point> 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<Point> hitEntityIf(Predicate<Entity> condition) {
return point -> !point.getNearbyEntities(null, 5, true, 0.1, e -> e instanceof LivingEntity le && !le.isDead() && condition.test(e)).isEmpty();
}
public static Predicate<Point> hitBlockIf(Predicate<Block> condition) {
return point -> HIT_BLOCK.test(point) && condition.test(point.getBlock());
}
public static Predicate<Point> hitAnythingIf(Predicate<Entity> 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<Point> hitEverythingIf(Predicate<Entity> 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<Point> hitCondition) {
return trace(start, end, 0.5, hitCondition);
}
public static Point trace(Location start, Location end, double interval, Predicate<Point> 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<Point> hitCondition) {
return trace(start, direction, distance, 0.5, hitCondition);
}
public static Point trace(Location start, Vector direction, double distance, double interval, Predicate<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point> hitCondition,
BiPredicate<Point,Block> blockReflectCondition,
BiPredicate<Point,Entity> 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);
}
}

View File

@@ -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<Point, BlockFace> REFLECT_ON_ANY_BLOCK = (point, face) ->
face != null && !point.getBlock().isEmpty() && point.getBlock().isCollidable();
public static final BiPredicate<Point, Entity> 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<Point, Entity> 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<RaySegment> traceReflectingRay(
Location start,
Vector direction,
double maxDistance,
int maxReflections,
double interval,
BiPredicate<Point, BlockFace> 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<RaySegment> 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<RaySegment> traceReflectingRay(
Location start,
Vector direction,
double maxDistance,
int maxReflections,
double interval,
BiPredicate<Point, BlockFace> blockReflectCondition,
BiPredicate<Point, Entity> 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<RaySegment> 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<Point, BlockFace> 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<Point, BlockFace> blockReflectCondition,
BiPredicate<Point, Entity> 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<Entity> 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<Point, BlockFace> blockReflectCondition,
Function<RaySegment, Boolean> 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<Point, BlockFace> blockReflectCondition,
BiPredicate<Point, Entity> entityReflectCondition,
Function<RaySegment, Boolean> 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<Entity> 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;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<Location> 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<Location> action) {
AtomicReference<Double> 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, Consumer<Location>> PARTICLE_FACTORY = particle -> l -> l.getWorld().spawnParticle(particle, l, 1, 0, 0, 0, 0);
public static final BiFunction<Color, Float, Consumer<Location>> 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<Boolean, Consumer<Location>> 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<Location> 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<Location> 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<Location> action, double gap) {
AtomicReference<Double> 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<Location> action) {
AtomicReference<Double> 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<Location> 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<Location> action) {
for (double r = radialGap; r <= radius; r += radialGap) {
ring(loc, r, maxDistanceBetweenPoints, action);
}
}
public static void helix(Location loc, double radius, Consumer<Location> 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<Location> 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<Location> 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<Location> 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<Location> 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<Location> 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<Location> 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<Location> action, double gap) {
double arcLength = 360.0 / sections;
AtomicReference<Double> 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<Location> action, double gap) {
double arcLength = 360.0 / sections;
List<Double> 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<Location> action, double radialGap) {
AtomicReference<Double> 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<Location> 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);
}
}

View File

@@ -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<Entity> getNearbyEntities(Entity exclude, int range, boolean requireContact, double expansionX, double expansionY, double expansionZ, Predicate<Entity> 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<Entity> getNearbyEntities(Entity exclude, int range, boolean requireContact, double expansion, Predicate<Entity> filter) {
return getNearbyEntities(exclude, range, requireContact, expansion, expansion, expansion, filter);
}
public List<Entity> getNearbyEntities(Entity exclude, int range, boolean requireContact, Predicate<Entity> filter) {
return getNearbyEntities(exclude, range, requireContact, 0, filter);
}
public List<Entity> getNearbyEntities(Entity exclude, int range, Predicate<Entity> 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);
}
}

View File

@@ -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 <trim>
trust:
description: Make your friends immune to your abilities.
usage: "<add|remove|list> <player>"
aliases:
- t