diff --git a/README.md b/README.md
index b6a4ace1..9d507ca2 100644
--- a/README.md
+++ b/README.md
@@ -43,21 +43,22 @@ fair, heart-pounding battles that keep players on their toes. Here’s a rundown
- **Combat Logging**
No more dodging fights by logging out! Once players are in combat, they’re committed until the showdown ends. Watch it
in action:
- 
+
- **Customize combat experience**
Add custom effects to players in combat or death. Everything should be configurable and user-friendly:
- 
+
+
- **Spawn Protection (Configurable)**
Stop players from fleeing to safety! Block access to spawn or safe zones during combat – tweak it to fit your server’s
rules. See how it works:
- 
+
- **Crystal PvP support**
Engage in intense Crystal PvP battles without worrying about players logging out mid-fight! EternalCombat keeps
- everyone in the game until the last anchor hit. Check it out:
- 
+ everyone in the game until the last anchor hit. Check it out:
+
- **Fully Customizable Combat**
Tailor the combat experience to your liking with a ton of options! From disabling elytra to setting drop rates for
diff --git a/assets/flare.gif b/assets/flare.gif
new file mode 100644
index 00000000..e9ec622a
Binary files /dev/null and b/assets/flare.gif differ
diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt
index a5b82c3a..0468d171 100644
--- a/buildSrc/src/main/kotlin/Versions.kt
+++ b/buildSrc/src/main/kotlin/Versions.kt
@@ -19,6 +19,8 @@ object Versions {
const val OKAERI_CONFIGS_SERDES_COMMONS = "5.0.13"
const val OKAERI_CONFIGS_SERDES_BUKKIT = "5.0.13"
+ const val XSERIES = "13.6.0"
+
const val CAFFEINE = "3.2.3"
const val B_STATS_BUKKIT = "3.1.0"
diff --git a/eternalcombat-plugin/build.gradle.kts b/eternalcombat-plugin/build.gradle.kts
index fb3a8d48..b4edb3fa 100644
--- a/eternalcombat-plugin/build.gradle.kts
+++ b/eternalcombat-plugin/build.gradle.kts
@@ -44,6 +44,9 @@ dependencies {
implementation("eu.okaeri:okaeri-configs-serdes-commons:${Versions.OKAERI_CONFIGS_SERDES_COMMONS}")
implementation("eu.okaeri:okaeri-configs-serdes-bukkit:${Versions.OKAERI_CONFIGS_SERDES_BUKKIT}")
+ // XSeries
+ implementation("com.github.cryptomorin:XSeries:${Versions.XSERIES}")
+
// bstats
implementation("org.bstats:bstats-bukkit:${Versions.B_STATS_BUKKIT}")
@@ -126,7 +129,8 @@ tasks.shadowJar {
"com.github.benmanes.caffeine",
"com.eternalcode.commons",
"com.eternalcode.multification",
- "io.papermc.lib"
+ "com.github.cryptomorin",
+ "io.papermc.lib",
).forEach { pack ->
relocate(pack, "$prefix.$pack")
}
diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java
index 4b492fc1..48874171 100644
--- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java
+++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java
@@ -12,7 +12,8 @@
import com.eternalcode.combat.fight.controller.FightBypassCreativeController;
import com.eternalcode.combat.fight.controller.FightBypassPermissionController;
import com.eternalcode.combat.fight.controller.FightInventoryController;
-import com.eternalcode.combat.fight.death.DeathEffectController;
+import com.eternalcode.combat.fight.death.DeathFlareController;
+import com.eternalcode.combat.fight.death.DeathLightningController;
import com.eternalcode.combat.fight.drop.DropKeepInventoryService;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.drop.DropService;
@@ -182,7 +183,8 @@ public void onEnable() {
new FightBypassCreativeController(server, pluginConfig),
new FightActionBlockerController(this.fightManager, noticeService, pluginConfig, server),
new FightPearlController(pluginConfig.pearl, noticeService, this.fightManager, this.fightPearlService),
- new DeathEffectController(pluginConfig),
+ new DeathFlareController(pluginConfig, server, scheduler, this),
+ new DeathLightningController(pluginConfig, server),
new UpdaterNotificationController(updaterService, pluginConfig, this.audienceProvider, miniMessage),
new KnockbackRegionController(noticeService, this.regionProvider, this.fightManager, knockbackService, server),
new FightEffectController(pluginConfig.effect, this.fightEffectService, this.fightManager, server),
diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/death/DeathEffectController.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/death/DeathEffectController.java
deleted file mode 100644
index ddf198f8..00000000
--- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/death/DeathEffectController.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.eternalcode.combat.fight.death;
-
-import com.eternalcode.combat.config.implementation.PluginConfig;
-import org.bukkit.entity.Player;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.EventPriority;
-import org.bukkit.event.Listener;
-import org.bukkit.event.entity.PlayerDeathEvent;
-
-public class DeathEffectController implements Listener {
-
- private final PluginConfig pluginConfig;
-
- public DeathEffectController(PluginConfig pluginConfig) {
- this.pluginConfig = pluginConfig;
- }
-
- @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
- public void onPlayerDeathEvent(PlayerDeathEvent event) {
- if (!this.pluginConfig.death.lightning) {
- return;
- }
-
- Player player = event.getEntity();
- player.getWorld().strikeLightningEffect(player.getLocation());
-
- }
-
-}
diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/death/DeathFlareController.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/death/DeathFlareController.java
new file mode 100644
index 00000000..bd351147
--- /dev/null
+++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/death/DeathFlareController.java
@@ -0,0 +1,152 @@
+package com.eternalcode.combat.fight.death;
+
+import com.eternalcode.combat.config.implementation.PluginConfig;
+import com.eternalcode.combat.fight.event.CauseOfUnTag;
+import com.eternalcode.combat.fight.event.FightUntagEvent;
+import com.eternalcode.commons.scheduler.Scheduler;
+import java.time.Duration;
+import java.util.UUID;
+import org.bukkit.Color;
+import org.bukkit.FireworkEffect;
+import org.bukkit.Location;
+import org.bukkit.NamespacedKey;
+import org.bukkit.Server;
+import org.bukkit.World;
+import org.bukkit.entity.Firework;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityDamageByEntityEvent;
+import org.bukkit.event.entity.PlayerDeathEvent;
+import org.bukkit.inventory.meta.FireworkMeta;
+import org.bukkit.persistence.PersistentDataType;
+import org.bukkit.plugin.Plugin;
+
+public class DeathFlareController implements Listener {
+
+ private final PluginConfig pluginConfig;
+ private final Server server;
+ private final Scheduler scheduler;
+ private final NamespacedKey key;
+
+ public DeathFlareController(PluginConfig pluginConfig, Server server, Scheduler scheduler, Plugin plugin) {
+ this.pluginConfig = pluginConfig;
+ this.server = server;
+ this.scheduler = scheduler;
+ this.key = NamespacedKey.fromString("eternalcombat_firework", plugin);
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ public void onFightUntagEvent(FightUntagEvent event) {
+ CauseOfUnTag cause = event.getCause();
+ if (cause != CauseOfUnTag.DEATH && cause != CauseOfUnTag.DEATH_BY_PLAYER) {
+ return;
+ }
+
+ UUID uniqueId = event.getPlayer();
+ Player player = this.server.getPlayer(uniqueId);
+
+ if (player == null) {
+ return;
+ }
+
+ if (this.pluginConfig.death.firework.inCombat && !this.pluginConfig.death.firework.afterEveryDeath) {
+ this.spawnFlare(player);
+ }
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ public void onPlayerDeathEventLightning(PlayerDeathEvent event) {
+ Player player = event.getEntity();
+
+ if (this.pluginConfig.death.firework.afterEveryDeath) {
+ this.spawnFlare(player);
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
+ if (event.getDamager() instanceof Firework firework && firework.getPersistentDataContainer().has(key, PersistentDataType.STRING)) {
+ event.setCancelled(true);
+ }
+ }
+
+ private void spawnFlare(Player player) {
+ Location deathLocation = player.getLocation();
+ World world = deathLocation.getWorld();
+
+ Firework flare = world.spawn(deathLocation, Firework.class);
+ flare.getPersistentDataContainer().set(key, PersistentDataType.STRING, "true");
+
+ FireworkMeta meta = flare.getFireworkMeta();
+
+ Color primaryColor = this.decodeColor(this.pluginConfig.death.firework.primaryColor, "primary");
+ Color fadeColor = this.decodeColor(this.pluginConfig.death.firework.fadeColor, "fade");
+
+ FireworkEffect effect = FireworkEffect.builder()
+ .with(this.pluginConfig.death.firework.fireworkType)
+ .withColor(primaryColor)
+ .withFade(fadeColor)
+ .trail(true)
+ .flicker(true)
+ .build();
+
+ meta.addEffect(effect);
+ meta.setPower(this.pluginConfig.death.firework.power);
+ flare.setFireworkMeta(meta);
+
+ if (this.pluginConfig.death.firework.particlesEnabled) {
+ scheduleParticles(flare, world);
+ }
+ }
+
+ private void scheduleParticles(Firework flare, World world) {
+ this.scheduler.runLaterAsync(
+ () -> {
+ if (flare.isDead() || !flare.isValid()) {
+ return;
+ }
+ this.spawnParticles(world, flare);
+ this.scheduleParticles(flare, world);
+ }, Duration.ofMillis(50)
+ );
+ }
+
+ private Color decodeColor(String firework, String name) {
+ try {
+ return Color.fromRGB(Integer.decode(firework));
+ }
+ catch (NumberFormatException exception) {
+ throw new IllegalArgumentException(
+ "Invalid " + name + " format in plugin configuration" + firework,
+ exception
+ );
+ }
+ }
+
+ private void spawnParticles(World world, Firework flare) {
+ Location location = flare.getLocation();
+
+ world.spawnParticle(
+ this.pluginConfig.death.firework.mainParticle.get(),
+ location,
+ this.pluginConfig.death.firework.mainParticleCount,
+ 0.05,
+ 0.05,
+ 0.05,
+ 0.01
+ );
+
+ world.spawnParticle(
+ this.pluginConfig.death.firework.secondaryParticle.get(),
+ location,
+ this.pluginConfig.death.firework.secondaryParticleCount,
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.01
+ );
+ }
+
+}
diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/death/DeathLightningController.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/death/DeathLightningController.java
new file mode 100644
index 00000000..adf06221
--- /dev/null
+++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/death/DeathLightningController.java
@@ -0,0 +1,56 @@
+package com.eternalcode.combat.fight.death;
+
+import com.eternalcode.combat.config.implementation.PluginConfig;
+import com.eternalcode.combat.fight.event.CauseOfUnTag;
+import com.eternalcode.combat.fight.event.FightUntagEvent;
+import java.util.UUID;
+import org.bukkit.Server;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.PlayerDeathEvent;
+
+public class DeathLightningController implements Listener {
+
+ private final PluginConfig pluginConfig;
+ private final Server server;
+
+ public DeathLightningController(PluginConfig pluginConfig, Server server) {
+ this.pluginConfig = pluginConfig;
+ this.server = server;
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ public void onFightUntagEvent(FightUntagEvent event) {
+ CauseOfUnTag cause = event.getCause();
+ if (cause != CauseOfUnTag.DEATH && cause != CauseOfUnTag.DEATH_BY_PLAYER) {
+ return;
+ }
+
+ UUID uniqueId = event.getPlayer();
+ Player player = this.server.getPlayer(uniqueId);
+
+ if (player == null) {
+ return;
+ }
+
+ if (this.pluginConfig.death.lightning.inCombat && !this.pluginConfig.death.lightning.afterEveryDeath) {
+ this.lightningStrike(player);
+ }
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ public void onPlayerDeathEventLightning(PlayerDeathEvent event) {
+ Player player = event.getEntity();
+
+ if (this.pluginConfig.death.lightning.afterEveryDeath) {
+ lightningStrike(player);
+ }
+ }
+
+ private void lightningStrike(Player player) {
+ player.getWorld().strikeLightningEffect(player.getLocation());
+ }
+
+}
diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/death/DeathSettings.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/death/DeathSettings.java
index 0c791f51..dd90f5da 100644
--- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/death/DeathSettings.java
+++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/death/DeathSettings.java
@@ -1,10 +1,77 @@
package com.eternalcode.combat.fight.death;
+import com.cryptomorin.xseries.particles.XParticle;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
+import org.bukkit.FireworkEffect;
public class DeathSettings extends OkaeriConfig {
- @Comment("Should lightning strike when a player dies")
- public boolean lightning = true;
+ @Comment({
+ "Settings related to lightning effect upon death",
+ "Setting both afterEveryDeath and inCombat to false will disable this feature completely"
+ })
+ public LightningSettings lightning = new LightningSettings();
+
+ public static class LightningSettings extends OkaeriConfig {
+
+ @Comment("Should lightning spawn on every death?")
+ public boolean afterEveryDeath = false;
+
+ @Comment("Should lightning spawn on ONLY deaths in combat?")
+ public boolean inCombat = true;
+ }
+
+ @Comment({
+ "Settings for the Arc Raiders style flare (firework)",
+ "Setting both afterEveryDeath and inCombat to false will disable this feature completely"
+ })
+ public FlareSettings firework = new FlareSettings();
+
+ public static class FlareSettings extends OkaeriConfig {
+
+ @Comment("Should firework (flare) spawn on every death?")
+ public boolean afterEveryDeath = false;
+
+ @Comment("Should firework (flare) spawn on ONLY deaths in combat?")
+ public boolean inCombat = true;
+
+ @Comment("Power of firework - how long till explosion (please enter positive number)")
+ public int power = 2;
+
+ @Comment({
+ "The firework (flare) effect type (BALL, BALL_LARGE, STAR, BURST, CREEPER)",
+ "Reference: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/FireworkEffect.Type.html"
+ })
+ public FireworkEffect.Type fireworkType = FireworkEffect.Type.BALL;
+
+ @Comment("Hex color for the firework (flare)")
+ public String primaryColor = "#a80022";
+
+ @Comment("Hex color for the fade of firework (flare)")
+ public String fadeColor = "#0a0a0a";
+
+ @Comment("Toggle on/off additional particles spawned in the firework (flare) path")
+ public boolean particlesEnabled = true;
+
+ @Comment({
+ "The main trail particle (e.g., CAMPFIRE_COSY_SMOKE, SMOKE_LARGE)",
+ "Reference: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Particle.html"
+ })
+ public XParticle mainParticle = XParticle.CAMPFIRE_COSY_SMOKE;
+
+ @Comment("Count of main particles spawned on each tick")
+ public int mainParticleCount = 3;
+
+ @Comment({
+ "The secondary trail particle (e.g., CAMPFIRE_COSY_SMOKE, SMOKE_LARGE)",
+ "Reference: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Particle.html"
+ })
+
+ public XParticle secondaryParticle = XParticle.SMALL_FLAME;
+
+ @Comment("Count of secondary particles spawned on each tick")
+ public int secondaryParticleCount = 3;
+
+ }
}