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: - ![Combat log anti logout feature](https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/combatlog.gif?raw=true) + Combat log anti logout feature - **Customize combat experience** Add custom effects to players in combat or death. Everything should be configurable and user-friendly: - ![Lightning strikes when players die](https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/lightning.gif?raw=true) + Lightning strikes when players die + Flare indicates players death location - **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: - ![Border around protected region](https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/border.gif?raw=true) + Border around protected region - **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: - ![Crystal PvP showcase](https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/crystals.gif?raw=true) + everyone in the game until the last anchor hit. Check it out: + Crystal PvP showcase - **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; + + } }