Compare commits

...
Author SHA1 Message Date
Martin Sulikowski bc11f46275 Respect ignored worlds for void combat 2026-08-17 21:32:18 +02:00
Martin Sulikowski 35e19d0eed Keep combat tag active below void height to prevent evasion 2026-08-17 21:24:24 +02:00
renovate[bot] b2776936ed dependency: Update junit-framework monorepo to v6.1.3 (#418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-17 20:58:09 +02:00
Martin Sulikowski d2aa5dfa84 GH-395 Add combat sign editing blocker (#395)
* Add combat sign editing blocker

* Fix.
2026-08-17 20:57:17 +02:00
renovate[bot] 5c88af692d dependency: Update Gradle to v9.7.0 (#419)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-08-17 20:50:56 +02:00
renovate[bot] 390195edb2 dependency: Update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.6.1 (#413)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-22 10:11:56 +02:00
renovate[bot] 8808331f0a dependency: Update junit-framework monorepo to v6.1.2 (#411)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-14 17:14:08 +02:00
renovate[bot] 99b3a5aa27 dependency: Update dependency org.jetbrains.kotlin:kotlin-stdlib-jdk8 to v2.4.10 (#412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-14 17:14:00 +02:00
renovate[bot] 9d1716d9db dependency: Update Gradle to v9.6.1 (#405)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 22:52:33 +02:00
renovate[bot] 7b94ab4b7a dependency: Update Versions.PACKETS_EVENTS (#402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 22:52:13 +02:00
renovate[bot] daee189865 dependency: Update dependency me.clip:placeholderapi to v2.12.3 (#407)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 22:51:59 +02:00
renovate[bot] 1aef1e6f1d dependency: Update junit-framework monorepo to v6.1.1 (#408)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 22:51:49 +02:00
renovate[bot] 788a2a668c dependency: Update dependency com.gradleup.shadow:com.gradleup.shadow.gradle.plugin to v9.5.1 (#409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-08 22:51:41 +02:00
Nico SendanandMartin Sulikowski 93d7ffdd0f GH-400 Resolve EntityMountEvent registration failure on modern Paper (#400)
* Fix: Resolve EntityMountEvent registration failure on modern Paper

Newer Paper builds (e.g. 26.1.2) removed org.spigotmc.event.entity.EntityMountEvent, causing KnockbackRegionController registration to fail and region knockback protection to be disabled.

Resolve EntityMountEvent at runtime (checking Bukkit, Paper, and Spigot packages) and register it reflectively while preserving the 1.19.3 compile target for compatibility.

* Optimize dynamic mount event handling

---------

Co-authored-by: Martin Sulikowski <vluckyyy.biznes@gmail.com>
2026-06-21 15:30:55 +02:00
renovate[bot] fb6db7a008 dependency: Update Gradle to v9.6.0 (#397)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-20 14:14:16 +02:00
renovate[bot] 7d8a01b7d7 dependency: Update Versions.LITE_COMMANDS to v3.11.0 (#398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-20 14:13:50 +02:00
renovate[bot] 08c7482cee dependency: Update actions/checkout action to v7 (#399)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-20 14:12:36 +02:00
7e24e5afa6 GH-266 New features for Trident and Pearls (#303)
* I've added new options to Pearl and Trident that reset the timer when we use them

* I made the corrections that gemini noticed

* The improvements suggested by the team have been implemented

* Implemented suggestions from the team

* Make tridents throw check work on all tridents, regardless of enchantment

* Implemented suggestions from the team and fixed `FightTridentController` logic

* Corrected comments and field names

* Implemented CitralFlo suggestions

* Implemented CitralFlo suggestions again

* Follow igoyek's review from 21.10.25

* CR

* Simplify naming - extract logic to service for Pearls

* make it more logical

* Refactor trident-related classes and services for improved clarity and consistency

* Rename trident cooldown methods to improve clarity and update comments for riptide usage

* Fixes after tests - everything should be working now!

* Remove commented line

* Add cooldown to enderpearl - hack by:https://www.spigotmc.org/threads/modify-vanilla-ender-pearl-cooldown.629232/

* Bump minecraft version in runServer task to 1.21.11

* Add combat restrictions for Riptide tridents and improve event handling

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/trident/TridentSettings.java

* Apply suggestion from @Jakubk15

* Migrate to commons delay

* Resolve conflicts

* CR

* resolve merge

* resolve merge conflicts and update

---------

Co-authored-by: Jakubk15 <77227023+Jakubk15@users.noreply.github.com>
Co-authored-by: Michał Wojtas <wojtas.michal90@gmail.com>
Co-authored-by: Rollczi <ndejlich5@gmail.com>
Co-authored-by: Martin Sulikowski <vluckyyy.biznes@gmail.com>
2026-06-07 22:36:05 +02:00
bb130ae10e GH-373 Add opponent placeholder in combat notification (#373)
* Add opponent placeholder in combat notification

Signed-off-by: Michał Wojtas <wojtas.michal90@gmail.com>

* Update eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/FightTask.java

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Signed-off-by: Michał Wojtas <wojtas.michal90@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Norbert Dejlich <ndejlich5@gmail.com>
2026-06-07 21:46:10 +02:00
Igor MichalskiandRollczi 2c5b007d02 GH-331 Add pearl teleport (#331)
* Add pearl stasis prevention feature

Took 27 minutes

* Add message to pearl stasis prevention feature

Took 8 minutes

* CR

---------

Co-authored-by: Rollczi <ndejlich5@gmail.com>
2026-06-07 21:11:41 +02:00
Martin Sulikowski 51b365df1b bump version to 2.5.2-SNAPSHOT for further development 2026-06-06 21:25:32 +02:00
Martin Sulikowski 4f23105ba1 Release: bump version to 2.5.1 for official release 2026-06-06 21:21:25 +02:00
Martin Sulikowski d0c221c184 Fix: Add PlaceholderAPI dependency and ensure safe API initialization/deinitialization 2026-06-06 21:18:29 +02:00
Martin Sulikowski bf529dce32 Readme: Extend supported Minecraft versions to 26.1.2 2026-06-06 02:04:05 +02:00
Martin Sulikowski 74ec795581 Fix: bump version to 2.5.1-SNAPSHOT for further development 2026-06-06 02:03:30 +02:00
Martin Sulikowski b3550110c8 bump version to 2.5.1-SNAPSHOT for further development 2026-06-06 01:57:48 +02:00
36 changed files with 805 additions and 181 deletions

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
contents: read
if: (github.event_name == 'push' && !contains(github.event.head_commit.message, '[ci skip]')) || github.event_name == 'release'
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up JDK 21
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
contents: read
if: (github.event_name == 'push' && !contains(github.event.head_commit.message, '[ci skip]')) || github.event_name == 'release'
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up JDK 21
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
environment: deployment
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up JDK
uses: actions/setup-java@v5
+3 -3
View File
@@ -25,7 +25,7 @@
## EternalCombat requires PacketEvents plugin to work.
You can download it from [here](https://modrinth.com/plugin/packetevents)
EternalCombat 2.0 has been tested on Minecraft versions **1.17.1 to 1.21.11**, but it should work seamlessly on most
EternalCombat 2.0 has been tested on Minecraft versions **1.17.1 to 26.1.2**, but it should work seamlessly on most
other versions too.
If you run into any compatibility issues, please let us know in
the [Issues tab](https://github.com/EternalCodeTeam/EternalCombat/issues).
@@ -139,7 +139,7 @@ With Maven:
With Gradle:
```kts
compileOnly("com.eternalcode:eternalcombat-api:2.5.0-SNAPSHOT")
compileOnly("com.eternalcode:eternalcombat-api:2.5.2-SNAPSHOT")
```
With Maven:
@@ -149,7 +149,7 @@ With Maven:
<dependency>
<groupId>com.eternalcode</groupId>
<artifactId>eternalcombat-api</artifactId>
<version>2.5.0-SNAPSHOT</version>
<version>2.5.2-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
```
+2 -2
View File
@@ -8,10 +8,10 @@ repositories {
}
dependencies {
implementation("com.gradleup.shadow:com.gradleup.shadow.gradle.plugin:9.4.2")
implementation("com.gradleup.shadow:com.gradleup.shadow.gradle.plugin:9.6.1")
implementation("com.modrinth.minotaur:com.modrinth.minotaur.gradle.plugin:2.9.0")
implementation("io.papermc.hangar-publish-plugin:io.papermc.hangar-publish-plugin.gradle.plugin:0.1.4")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.4.0")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.4.10")
implementation("net.minecrell:plugin-yml:0.6.0")
implementation("xyz.jpenilla:run-task:3.0.2")
}
+6 -6
View File
@@ -3,17 +3,17 @@ object Versions {
// Keep this version for best compatibility.
const val PAPER_API = "1.19.3-R0.1-SNAPSHOT"
const val JUNIT_JUPITER_API = "6.1.0"
const val JUNIT_JUPITER_PARAMS = "6.1.0"
const val JUNIT_JUPITER_ENGINE = "6.1.0"
const val JUNIT_JUPITER_API = "6.1.3"
const val JUNIT_JUPITER_PARAMS = "6.1.3"
const val JUNIT_JUPITER_ENGINE = "6.1.3"
const val JETBRAINS_ANNOTATIONS = "26.1.0"
const val ETERNALCODE_COMMONS = "1.4.1"
const val MULTIFICATION = "1.2.5-SNAPSHOT"
const val PACKETS_EVENTS = "2.12.2"
const val PACKETS_EVENTS = "2.13.0"
const val LITE_COMMANDS = "3.10.9"
const val LITE_COMMANDS = "3.11.0"
const val OKAERI_CONFIGS_SERDES_COMMONS = "5.0.13"
const val OKAERI_CONFIGS_SERDES_BUKKIT = "5.0.13"
@@ -24,7 +24,7 @@ object Versions {
const val B_STATS_BUKKIT = "3.2.1"
const val WORLD_GUARD_BUKKIT = "7.0.17"
const val PLACEHOLDER_API = "2.12.2"
const val PLACEHOLDER_API = "2.12.3"
const val LANDS_API = "7.25.4"
const val WORLDEDIT = "3ISh7ADm" //cannot use numeric version bc of duplicated version on modrinth
const val PACKETEVENTS = "2.11.1"
@@ -3,7 +3,7 @@ plugins {
}
group = "com.eternalcode"
version = "2.5.0-SNAPSHOT"
version = "2.5.2-SNAPSHOT"
tasks.compileJava {
options.compilerArgs = listOf("-Xlint:deprecation", "-parameters")
@@ -4,7 +4,7 @@ plugins {
}
group = "com.eternalcode"
version = "2.5.0"
version = "2.5.2-SNAPSHOT"
java {
withSourcesJar()
@@ -4,7 +4,7 @@ import com.eternalcode.combat.fight.drop.DropKeepInventoryService;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.drop.DropService;
import com.eternalcode.combat.fight.effect.FightEffectService;
import com.eternalcode.combat.fight.pearl.FightPearlService;
import com.eternalcode.combat.fight.pearl.PearlService;
import com.eternalcode.combat.region.RegionProvider;
import com.eternalcode.combat.fight.tagout.FightTagOutService;
@@ -14,7 +14,7 @@ public interface EternalCombatApi {
RegionProvider getRegionProvider();
FightPearlService getFightPearlService();
PearlService getFightPearlService();
FightTagOutService getFightTagOutService();
@@ -23,6 +23,22 @@ public enum CauseOfTag {
*/
CRYSTAL,
/**
* Trident usage extending combat tag.
*/
TRIDENT,
/**
* Ender pearl usage extending combat tag.
*/
ENDER_PEARL,
/**
* The player's combat tag was kept active because they were below the void combat height.
*/
VOID,
/**
* A custom cause, typically defined by external plugins or systems, applied the combat tag.
*/
@@ -1,16 +1,16 @@
package com.eternalcode.combat.fight.pearl;
import java.time.Duration;
import java.time.Instant;
import java.util.UUID;
import org.bukkit.entity.Player;
public interface FightPearlService {
public interface PearlService {
Instant getDelay(UUID uuid);
boolean shouldCancelEvent(UUID playerId);
Duration getRemainingDelay(UUID uuid);
boolean hasDelay(UUID uuid);
void markDelay(UUID uuid);
void handleDelay(Player uuid);
}
@@ -0,0 +1,31 @@
package com.eternalcode.combat.fight.trident;
import java.time.Duration;
import java.util.UUID;
public interface TridentService {
/**
* returns remaining delay for the player to use trident again
* @param uuid unique id of the player
* @return remaining duration left to use trident again by the player or zero
*/
Duration getRemainingDelay(UUID uuid);
/**
* checks if player still has delay left to use trident
* @param uuid unique id of the player
* @return true if user still has cooldown left to use trident
*/
boolean hasDelay(UUID uuid);
/**
* marks start of the delay for the user
* @param uuid unique id of the player
*/
void markDelay(UUID uuid);
void removeDelay(UUID playerId);
}
+7 -2
View File
@@ -1,6 +1,6 @@
import net.minecrell.pluginyml.bukkit.BukkitPluginDescription
import net.minecrell.pluginyml.paper.PaperPluginDescription
import org.gradle.kotlin.dsl.shadowJar
import net.minecrell.pluginyml.bukkit.BukkitPluginDescription
plugins {
`eternalcombat-java`
@@ -95,6 +95,11 @@ paper {
required = false
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
}
register("PlaceholderAPI") {
required = false
load = PaperPluginDescription.RelativeLoadOrder.BEFORE
}
}
}
@@ -1,13 +1,20 @@
package com.eternalcode.combat;
import com.eternalcode.combat.border.BorderTriggerController;
import com.eternalcode.combat.border.BorderService;
import com.eternalcode.combat.border.BorderServiceImpl;
import com.eternalcode.combat.border.BorderTriggerController;
import com.eternalcode.combat.border.animation.block.BorderBlockController;
import com.eternalcode.combat.border.animation.particle.ParticleController;
import com.eternalcode.combat.bridge.BridgeService;
import com.eternalcode.combat.crystalpvp.RespawnAnchorListener;
import com.eternalcode.combat.config.ConfigService;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.crystalpvp.EndCrystalListener;
import com.eternalcode.combat.crystalpvp.RespawnAnchorListener;
import com.eternalcode.combat.event.EventManager;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.FightManagerImpl;
import com.eternalcode.combat.fight.FightTagCommand;
import com.eternalcode.combat.fight.FightTask;
import com.eternalcode.combat.fight.blocker.CommandsBlocker;
import com.eternalcode.combat.fight.blocker.ElytraBlocker;
import com.eternalcode.combat.fight.blocker.ElytraEquipBlocker;
@@ -16,44 +23,42 @@ import com.eternalcode.combat.fight.controller.FightBypassAdminController;
import com.eternalcode.combat.fight.controller.FightBypassCreativeController;
import com.eternalcode.combat.fight.controller.FightBypassPermissionController;
import com.eternalcode.combat.fight.blocker.InventoryContainersBlocker;
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;
import com.eternalcode.combat.fight.effect.FightEffectService;
import com.eternalcode.combat.fight.firework.FireworkController;
import com.eternalcode.combat.fight.knockback.KnockbackService;
import com.eternalcode.combat.fight.tagout.FightTagOutService;
import com.eternalcode.combat.fight.pearl.FightPearlService;
import com.eternalcode.combat.handler.InvalidUsageHandlerImpl;
import com.eternalcode.combat.handler.MissingPermissionHandlerImpl;
import com.eternalcode.combat.config.ConfigService;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.drop.DropController;
import com.eternalcode.combat.fight.drop.DropKeepInventoryServiceImpl;
import com.eternalcode.combat.fight.drop.DropServiceImpl;
import com.eternalcode.combat.fight.drop.impl.PercentDropModifier;
import com.eternalcode.combat.fight.drop.impl.PlayersHealthDropModifier;
import com.eternalcode.combat.fight.FightTagCommand;
import com.eternalcode.combat.fight.blocker.PlaceBlockBlocker;
import com.eternalcode.combat.fight.controller.FightMessageController;
import com.eternalcode.combat.fight.controller.FightTagController;
import com.eternalcode.combat.fight.controller.FightUnTagController;
import com.eternalcode.combat.fight.death.DeathFlareController;
import com.eternalcode.combat.fight.death.DeathLightningController;
import com.eternalcode.combat.fight.drop.DropController;
import com.eternalcode.combat.fight.drop.DropKeepInventoryService;
import com.eternalcode.combat.fight.drop.DropKeepInventoryServiceImpl;
import com.eternalcode.combat.fight.drop.DropService;
import com.eternalcode.combat.fight.drop.DropServiceImpl;
import com.eternalcode.combat.fight.drop.impl.PercentDropModifier;
import com.eternalcode.combat.fight.drop.impl.PlayersHealthDropModifier;
import com.eternalcode.combat.fight.blocker.PlaceBlockBlocker;
import com.eternalcode.combat.fight.blocker.SignEditingBlocker;
import com.eternalcode.combat.fight.effect.FightEffectController;
import com.eternalcode.combat.event.EventManager;
import com.eternalcode.combat.fight.FightManagerImpl;
import com.eternalcode.combat.fight.FightTask;
import com.eternalcode.combat.fight.effect.FightEffectService;
import com.eternalcode.combat.fight.effect.FightEffectServiceImpl;
import com.eternalcode.combat.fight.firework.FireworkController;
import com.eternalcode.combat.fight.knockback.KnockbackMountController;
import com.eternalcode.combat.fight.knockback.KnockbackRegionController;
import com.eternalcode.combat.fight.knockback.KnockbackService;
import com.eternalcode.combat.fight.logout.LogoutController;
import com.eternalcode.combat.fight.logout.LogoutService;
import com.eternalcode.combat.fight.pearl.FightPearlController;
import com.eternalcode.combat.fight.pearl.FightPearlServiceImpl;
import com.eternalcode.combat.fight.tagout.FightTagOutController;
import com.eternalcode.combat.fight.tagout.FightTagOutServiceImpl;
import com.eternalcode.combat.fight.pearl.PearlController;
import com.eternalcode.combat.fight.pearl.PearlService;
import com.eternalcode.combat.fight.pearl.PearlServiceImpl;
import com.eternalcode.combat.fight.tagout.FightTagOutCommand;
import com.eternalcode.combat.fight.tagout.FightTagOutController;
import com.eternalcode.combat.fight.tagout.FightTagOutService;
import com.eternalcode.combat.fight.tagout.FightTagOutServiceImpl;
import com.eternalcode.combat.fight.trident.TridentController;
import com.eternalcode.combat.fight.trident.TridentService;
import com.eternalcode.combat.fight.trident.TridentServiceImpl;
import com.eternalcode.combat.handler.InvalidUsageHandlerImpl;
import com.eternalcode.combat.handler.MissingPermissionHandlerImpl;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.fight.knockback.KnockbackRegionController;
import com.eternalcode.combat.region.RegionProvider;
import com.eternalcode.combat.updater.UpdaterNotificationController;
import com.eternalcode.combat.updater.UpdaterService;
@@ -85,7 +90,8 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
private static final int BSTATS_METRICS_ID = 17803;
private FightManager fightManager;
private FightPearlService fightPearlService;
private PearlService pearlService;
private TridentService tridentService;
private FightTagOutService fightTagOutService;
private FightEffectService fightEffectService;
@@ -95,6 +101,7 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
private RegionProvider regionProvider;
private LiteCommands<CommandSender> liteCommands;
private boolean apiInitialized;
@Override
@@ -112,7 +119,8 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
MinecraftScheduler scheduler = CombatSchedulerAdapter.getAdaptiveScheduler(this);
this.fightManager = new FightManagerImpl(eventManager);
this.fightPearlService = new FightPearlServiceImpl(pluginConfig.pearl);
this.pearlService = new PearlServiceImpl(this.fightManager, pluginConfig, scheduler);
this.tridentService = new TridentServiceImpl(pluginConfig);
this.fightTagOutService = new FightTagOutServiceImpl();
this.fightEffectService = new FightEffectServiceImpl();
@@ -182,7 +190,8 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
new FightBypassPermissionController(server, pluginConfig),
new FightBypassCreativeController(server, pluginConfig),
new PlaceBlockBlocker(this.fightManager, noticeService, pluginConfig),
new FightPearlController(pluginConfig.pearl, noticeService, this.fightManager, this.fightPearlService),
new PearlController(pluginConfig, this.pearlService, noticeService, fightManager),
new TridentController(pluginConfig, noticeService, this.fightManager, this.tridentService, server),
new DeathFlareController(pluginConfig, server, scheduler, this),
new DeathLightningController(pluginConfig, server),
new UpdaterNotificationController(updaterService, pluginConfig, miniMessage),
@@ -197,6 +206,7 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
new RespawnAnchorListener(this, this.fightManager, pluginConfig),
new FireworkController(this.fightManager, pluginConfig, noticeService),
new InventoryContainersBlocker(this.fightManager, pluginConfig, noticeService),
new SignEditingBlocker(this.fightManager, pluginConfig),
new CommandsBlocker(this.fightManager, noticeService, pluginConfig),
new ElytraBlocker(this.fightManager, pluginConfig),
new ElytraEquipBlocker(this.fightManager, noticeService, pluginConfig, server),
@@ -204,6 +214,8 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
new PlaceBlockBlocker(this.fightManager, noticeService, pluginConfig)
);
new KnockbackMountController(noticeService, this.regionProvider, this.fightManager).register(this);
eventManager.subscribe(
PlayerDeathEvent.class,
pluginConfig.drop.dropEventPriority,
@@ -217,6 +229,7 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
);
EternalCombatProvider.initialize(this);
this.apiInitialized = true;
long millis = started.elapsed(TimeUnit.MILLISECONDS);
this.getLogger().info("Successfully loaded EternalCombat in " + millis + "ms");
@@ -230,7 +243,11 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
if (this.fightManager != null) {
this.fightManager.untagAll();
}
if (this.apiInitialized) {
EternalCombatProvider.deinitialize();
this.apiInitialized = false;
}
}
@@ -245,8 +262,8 @@ public final class CombatPlugin extends JavaPlugin implements EternalCombatApi {
}
@Override
public FightPearlService getFightPearlService() {
return this.fightPearlService;
public PearlService getFightPearlService() {
return this.pearlService;
}
@Override
@@ -90,6 +90,19 @@ public class CombatSettings extends OkaeriConfig {
})
public EventPriority quitPunishmentEventPriority = EventPriority.NORMAL;
@Comment({
"# Keep a player's combat tag continuously active while they are below 'voidCombatHeight'.",
"# This prevents players from evading combat by falling into the void (e.g. in the End).",
"# Set to 'true' to keep resetting the combat timer to its full duration while below that height."
})
public boolean keepCombatActiveInVoid = false;
@Comment({
"# The Y height below which the combat timer is kept active (see 'keepCombatActiveInVoid').",
"# Default: 0, matching the void threshold in worlds such as the End."
})
public double voidCombatHeight = 0;
@Comment({
"# List of kick reasons where players will NOT be punished for combat logging.",
"# If this list is empty, players are ALWAYS punished when kicked during combat.",
@@ -16,6 +16,7 @@ public class MessagesSettings extends OkaeriConfig {
" ",
"# Configure the combat log notification displayed to players.",
"# You can use the {TIME} variable to display the remaining combat time.",
"# Placeholder {OPPONENT} will be replaced with player name or 'unknownPlayerPlaceholder' variable",
" ",
})
public Notice combatNotification = Notice.builder()
@@ -27,6 +28,12 @@ public class MessagesSettings extends OkaeriConfig {
})
public boolean withoutMillis = true;
@Comment({
" ",
"# When player is not known this text will be used as {OPPONENT} in 'combatNotification'"
})
public String unknownPlayerPlaceholder = "Unknown";
@Comment({
"# Message displayed when a player lacks permission to execute a command.",
"# The {PERMISSION} placeholder is replaced with the required permission."
@@ -5,7 +5,8 @@ import com.eternalcode.combat.fight.death.DeathSettings;
import com.eternalcode.combat.fight.drop.DropSettings;
import com.eternalcode.combat.fight.effect.FightEffectSettings;
import com.eternalcode.combat.fight.knockback.KnockbackSettings;
import com.eternalcode.combat.fight.pearl.FightPearlSettings;
import com.eternalcode.combat.fight.pearl.PearlSettings;
import com.eternalcode.combat.fight.trident.TridentSettings;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.time.Duration;
@@ -34,7 +35,14 @@ public class PluginConfig extends OkaeriConfig {
"# Settings related to Ender Pearls.",
"# Configure cooldowns, restrictions, and other behaviors for Ender Pearls during combat."
})
public FightPearlSettings pearl = new FightPearlSettings();
public PearlSettings pearl = new PearlSettings();
@Comment({
" ",
"# Settings related to Trident",
"# Configure cooldowns, restrictions, and other behaviors for Trident during combat."
})
public TridentSettings trident = new TridentSettings();
@Comment({
" ",
@@ -119,6 +127,13 @@ public class PluginConfig extends OkaeriConfig {
})
public InventorySettings inventory = new InventorySettings();
@Comment({
" ",
"# Settings related to sign editing during combat.",
"# Created for sign traps where opening the sign editor can block ender pearl throws."
})
public SignEditingSettings signEditing = new SignEditingSettings();
@Comment({
" ",
"# Settings related to placeholders used in the plugin.",
@@ -0,0 +1,13 @@
package com.eternalcode.combat.config.implementation;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
public class SignEditingSettings extends OkaeriConfig {
@Comment({
"# Prevent players from editing signs during combat.",
"# Disabled by default; created for sign traps where sign editing blocks ender pearl throws."
})
public boolean disableSignEditingDuringCombat = false;
}
@@ -1,9 +1,12 @@
package com.eternalcode.combat.fight;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.event.CauseOfTag;
import com.eternalcode.combat.fight.event.CauseOfUnTag;
import com.eternalcode.combat.fight.event.FightTagEvent;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.util.DurationUtil;
import java.util.Optional;
import org.bukkit.Server;
import org.bukkit.entity.Player;
@@ -17,7 +20,7 @@ public class FightTask implements Runnable {
private final FightManager fightManager;
private final NoticeService noticeService;
public FightTask(Server server, PluginConfig config, FightManager fightManager, NoticeService noticeService) {
public FightTask(Server server, PluginConfig config, FightManager fightManager, NoticeService noticeService) {
this.server = server;
this.config = config;
this.fightManager = fightManager;
@@ -40,12 +43,29 @@ public class FightTask implements Runnable {
return;
}
if (this.config.combat.keepCombatActiveInVoid
&& player.getLocation().getY() < this.config.combat.voidCombatHeight
&& !this.config.settings.ignoredWorlds.contains(player.getWorld().getName())) {
FightTagEvent tagEvent = this.fightManager.tag(playerUniqueId, this.config.settings.combatTimerDuration, CauseOfTag.VOID, fightTag.getTagger());
if (!tagEvent.isCancelled()) {
fightTag = this.fightManager.getTag(playerUniqueId);
}
}
Duration remaining = fightTag.getRemainingDuration();
String opponent = Optional.ofNullable(fightTag.getTagger())
.filter(uuid -> !uuid.equals(playerUniqueId))
.map(this.server::getPlayer)
.map(Player::getName)
.orElse(this.config.messagesSettings.unknownPlayerPlaceholder);
this.noticeService.create()
.player(player.getUniqueId())
.notice(this.config.messagesSettings.combatNotification)
.placeholder("{TIME}", DurationUtil.format(remaining, this.config.messagesSettings.withoutMillis))
.placeholder("{OPPONENT}", opponent)
.send();
}
@@ -0,0 +1,63 @@
package com.eternalcode.combat.fight.blocker;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import java.util.UUID;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Event.Result;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
public class SignEditingBlocker implements Listener {
private final FightManager fightManager;
private final PluginConfig config;
public SignEditingBlocker(FightManager fightManager, PluginConfig config) {
this.fightManager = fightManager;
this.config = config;
}
@EventHandler(ignoreCancelled = true)
void onInteract(PlayerInteractEvent event) {
if (!this.config.signEditing.disableSignEditingDuringCombat) {
return;
}
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) {
return;
}
Block clickedBlock = event.getClickedBlock();
if (clickedBlock == null || !isSign(clickedBlock.getType())) {
return;
}
Player player = event.getPlayer();
UUID uniqueId = player.getUniqueId();
if (!this.fightManager.isInCombat(uniqueId)) {
return;
}
event.setUseInteractedBlock(Result.DENY);
ItemStack item = event.getItem();
if (item != null && isEnderPearl(item.getType()) && event.useItemInHand() != Result.DENY) {
event.setUseItemInHand(Result.ALLOW);
}
}
static boolean isSign(Material material) {
return material.name().endsWith("_SIGN");
}
static boolean isEnderPearl(Material material) {
return material == Material.ENDER_PEARL;
}
}
@@ -0,0 +1,129 @@
package com.eternalcode.combat.fight.knockback;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.region.RegionProvider;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityEvent;
import org.bukkit.plugin.Plugin;
// EntityMountEvent moved between org.spigotmc, io.papermc.paper and org.bukkit across versions,
// so we resolve it at runtime to stay compatible without bumping the compile API.
public final class KnockbackMountController implements Listener {
private static final String[] EVENT_CLASS_CANDIDATES = {
"org.bukkit.event.entity.EntityMountEvent",
"io.papermc.paper.event.entity.EntityMountEvent",
"org.spigotmc.event.entity.EntityMountEvent",
};
private final NoticeService noticeService;
private final RegionProvider regionProvider;
private final FightManager fightManager;
private final AtomicBoolean mountFailureLogged = new AtomicBoolean();
public KnockbackMountController(NoticeService noticeService, RegionProvider regionProvider, FightManager fightManager) {
this.noticeService = noticeService;
this.regionProvider = regionProvider;
this.fightManager = fightManager;
}
public void register(Plugin plugin) {
Class<? extends Event> eventClass = null;
for (String candidate : EVENT_CLASS_CANDIDATES) {
try {
eventClass = Class.forName(candidate).asSubclass(Event.class);
break;
} catch (ClassNotFoundException | ClassCastException ignored) {
}
}
if (eventClass == null) {
plugin.getLogger().warning("Could not find any EntityMountEvent class; mount protection in regions is disabled.");
return;
}
MethodHandle getMountHandle;
try {
getMountHandle = this.resolveGetMountHandle(eventClass);
} catch (NoSuchMethodException | IllegalAccessException exception) {
plugin.getLogger().log(Level.WARNING, "EntityMountEvent has incompatible getMount() method; mount protection in regions is disabled.", exception);
return;
}
plugin.getServer().getPluginManager().registerEvent(
eventClass,
this,
EventPriority.HIGHEST,
(listener, event) -> this.handle(event, getMountHandle, plugin),
plugin,
true
);
}
private MethodHandle resolveGetMountHandle(Class<? extends Event> eventClass) throws NoSuchMethodException, IllegalAccessException {
if (!EntityEvent.class.isAssignableFrom(eventClass) || !Cancellable.class.isAssignableFrom(eventClass)) {
throw new NoSuchMethodException(eventClass.getName() + " is not a cancellable entity event");
}
Method getMount = eventClass.getMethod("getMount");
if (!Entity.class.isAssignableFrom(getMount.getReturnType())) {
throw new NoSuchMethodException(eventClass.getName() + "#getMount() does not return Entity");
}
return MethodHandles.publicLookup()
.unreflect(getMount)
.asType(MethodType.methodType(Entity.class, Event.class));
}
private void handle(Event event, MethodHandle getMountHandle, Plugin plugin) {
if (!(event instanceof EntityEvent entityEvent) || !(event instanceof Cancellable cancellable)) {
return;
}
if (!(entityEvent.getEntity() instanceof Player player)) {
return;
}
if (!this.fightManager.isInCombat(player.getUniqueId())) {
return;
}
Entity mount;
try {
mount = (Entity) getMountHandle.invokeExact(event);
} catch (Throwable exception) {
this.logMountFailure(plugin, exception);
return;
}
if (mount == null || !this.regionProvider.isInRegion(mount.getLocation())) {
return;
}
cancellable.setCancelled(true);
this.noticeService.create()
.player(player.getUniqueId())
.notice(config -> config.messagesSettings.cantEnterOnRegion)
.send();
}
private void logMountFailure(Plugin plugin, Throwable exception) {
if (!this.mountFailureLogged.compareAndSet(false, true)) {
return;
}
plugin.getLogger().log(Level.WARNING, "Could not read EntityMountEvent mount; mount protection in regions is skipped.", exception);
}
}
@@ -17,7 +17,6 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.vehicle.VehicleMoveEvent;
import org.spigotmc.event.entity.EntityMountEvent;
public class KnockbackRegionController implements Listener {
@@ -132,27 +131,6 @@ public class KnockbackRegionController implements Listener {
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
void onEntityMount(EntityMountEvent event) {
if (!(event.getEntity() instanceof Player player)) {
return;
}
if (!this.fightManager.isInCombat(player.getUniqueId())) {
return;
}
if (!this.regionProvider.isInRegion(event.getMount().getLocation())) {
return;
}
event.setCancelled(true);
this.noticeService.create()
.player(player.getUniqueId())
.notice(config -> config.messagesSettings.cantEnterOnRegion)
.send();
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
void onTag(FightTagEvent event) {
Player player = this.server.getPlayer(event.getPlayer());
@@ -1,50 +0,0 @@
package com.eternalcode.combat.fight.pearl;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.time.Duration;
import java.time.Instant;
import java.util.UUID;
public class FightPearlServiceImpl implements FightPearlService {
private final FightPearlSettings pearlSettings;
private final Cache<UUID, Instant> pearlStartTimes;
public FightPearlServiceImpl(FightPearlSettings pearlSettings) {
this.pearlSettings = pearlSettings;
this.pearlStartTimes = Caffeine.newBuilder()
.expireAfterWrite(pearlSettings.pearlThrowDelay)
.build();
}
@Override
public void markDelay(UUID uuid) {
this.pearlStartTimes.put(uuid, Instant.now());
}
@Override
public boolean hasDelay(UUID uuid) {
return this.pearlStartTimes.getIfPresent(uuid) != null;
}
@Override
public Duration getRemainingDelay(UUID uuid) {
Instant startTime = this.pearlStartTimes.getIfPresent(uuid);
if (startTime == null) {
return Duration.ZERO;
}
Duration elapsed = Duration.between(startTime, Instant.now());
Duration remaining = this.pearlSettings.pearlThrowDelay.minus(elapsed);
return remaining.isNegative() ? Duration.ZERO : remaining;
}
@Override
public Instant getDelay(UUID uuid) {
Instant startTime = this.pearlStartTimes.getIfPresent(uuid);
return startTime != null ? startTime.plus(this.pearlSettings.pearlThrowDelay) : Instant.MIN;
}
}
@@ -1,11 +1,11 @@
package com.eternalcode.combat.fight.pearl;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.util.DurationUtil;
import java.time.Duration;
import java.util.UUID;
import org.bukkit.Material;
import org.bukkit.entity.EnderPearl;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -14,28 +14,26 @@ import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.ProjectileLaunchEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.event.player.PlayerTeleportEvent;
public class FightPearlController implements Listener {
public class PearlController implements Listener {
private final FightPearlSettings settings;
private final PluginConfig pluginConfig;
private final PearlService pearlService;
private final NoticeService noticeService;
private final FightManager fightManager;
private final FightPearlService fightPearlService;
public FightPearlController(
FightPearlSettings settings,
NoticeService noticeService,
FightManager fightManager,
FightPearlService fightPearlService
public PearlController(
PluginConfig pluginConfig,
PearlService pearlService, NoticeService noticeService, FightManager fightManager
) {
this.settings = settings;
this.pluginConfig = pluginConfig;
this.pearlService = pearlService;
this.noticeService = noticeService;
this.fightManager = fightManager;
this.fightPearlService = fightPearlService;
}
@EventHandler(priority = EventPriority.HIGHEST)
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPearlThrow(ProjectileLaunchEvent event) {
if (!(event.getEntity() instanceof EnderPearl)) {
return;
@@ -47,58 +45,68 @@ public class FightPearlController implements Listener {
UUID playerId = player.getUniqueId();
if (!this.fightManager.isInCombat(playerId)) {
return;
}
if (this.settings.pearlThrowDisabledDuringCombat) {
if (this.pearlService.shouldCancelEvent(playerId)) {
event.setCancelled(true);
if (this.pluginConfig.pearl.pearlThrowDisabledDuringCombat) {
this.noticeService.create()
.player(playerId)
.notice(this.pluginConfig.pearl.pearlThrowBlockedDuringCombat)
.send();
return;
}
Duration remainingDelay = this.pearlService.getRemainingDelay(playerId);
this.noticeService.create()
.player(playerId)
.notice(this.settings.pearlThrowBlockedDuringCombat)
.notice(this.pluginConfig.pearl.pearlThrowBlockedDelayDuringCombat)
.placeholder("{TIME}", DurationUtil.format(remainingDelay))
.send();
return;
}
if (this.settings.pearlCooldownEnabled) {
handlePearlCooldown(event, player, playerId);
}
this.pearlService.handleDelay(player);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPearlDamage(EntityDamageByEntityEvent event) {
if (this.settings.pearlThrowDamageEnabled) {
if (this.pluginConfig.pearl.pearlThrowDamageEnabled) {
return;
}
if (!(event.getEntity() instanceof Player) ||
!(event.getDamager() instanceof EnderPearl) ||
event.getCause() != EntityDamageEvent.DamageCause.FALL) {
event.getCause() != EntityDamageEvent.DamageCause.PROJECTILE) {
return;
}
event.setDamage(0.0);
}
private void handlePearlCooldown(ProjectileLaunchEvent event, Player player, UUID playerId) {
if (this.settings.pearlThrowDelay.isZero()) {
@EventHandler(priority = EventPriority.HIGHEST)
public void onPearlTeleport(PlayerTeleportEvent event) {
if (event.getCause() != PlayerTeleportEvent.TeleportCause.ENDER_PEARL) {
return;
}
if (this.fightPearlService.hasDelay(playerId)) {
event.setCancelled(true);
Duration remainingDelay = this.fightPearlService.getRemainingDelay(playerId);
this.noticeService.create()
.player(playerId)
.notice(this.settings.pearlThrowBlockedDelayDuringCombat)
.placeholder("{TIME}", DurationUtil.format(remainingDelay))
.send();
if (!this.pluginConfig.pearl.pearlThrowDisabledDuringCombat) {
return;
}
this.fightPearlService.markDelay(playerId);
int cooldownTicks = (int) (this.settings.pearlThrowDelay.toMillis() / 50);
player.setCooldown(Material.ENDER_PEARL, cooldownTicks);
Player player = event.getPlayer();
UUID playerId = player.getUniqueId();
if (!this.fightManager.isInCombat(playerId)) {
return;
}
event.setCancelled(true);
this.noticeService.create()
.player(playerId)
.notice(this.pluginConfig.pearl.pearlThrowBlockedDuringCombat)
.send();
}
}
@@ -0,0 +1,81 @@
package com.eternalcode.combat.fight.pearl;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.event.CauseOfTag;
import com.eternalcode.commons.delay.Delay;
import com.eternalcode.commons.scheduler.Scheduler;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.time.Duration;
import java.util.UUID;
public class PearlServiceImpl implements PearlService {
private final FightManager fightManager;
private final PluginConfig pluginConfig;
private final Scheduler scheduler;
private final Delay<UUID> pearlStartTimes;
public PearlServiceImpl(FightManager fightManager, PluginConfig pluginConfig, Scheduler scheduler) {
this.fightManager = fightManager;
this.pluginConfig = pluginConfig;
this.pearlStartTimes = Delay.withDefault(() -> pluginConfig.pearl.pearlThrowDelay);
this.scheduler = scheduler;
}
@Override
public boolean shouldCancelEvent(UUID playerId) {
if (fightManager.isInCombat(playerId)) {
if (this.pluginConfig.pearl.pearlCooldownEnabled) {
return this.pearlStartTimes.hasDelay(playerId);
}
return pluginConfig.pearl.pearlThrowDisabledDuringCombat;
}
return false;
}
@Override
public void handleDelay(Player player) {
UUID uniqueId = player.getUniqueId();
if (this.hasDelay(uniqueId)) {
return;
}
if (this.fightManager.isInCombat(uniqueId)) {
if (this.pluginConfig.pearl.pearlExtendsCombatTag) {
Duration combatTime = this.pluginConfig.settings.combatTimerDuration;
this.fightManager.tag(uniqueId, combatTime, CauseOfTag.ENDER_PEARL);
}
if (this.pluginConfig.pearl.pearlCooldownEnabled && !this.pluginConfig.pearl.pearlThrowDelay.isZero()) {
this.pearlStartTimes.markDelay(uniqueId);
this.scheduler.runLater(
() -> player.setCooldown(
Material.ENDER_PEARL,
(int) this.pluginConfig.pearl.pearlThrowDelay.toMillis() / 50
), Duration.ofMillis(50)
);
}
}
}
@Override
public boolean hasDelay(UUID uuid) {
return this.pearlStartTimes.hasDelay(uuid);
}
@Override
public Duration getRemainingDelay(UUID uuid) {
return this.pearlStartTimes.getRemaining(uuid);
}
}
@@ -6,7 +6,7 @@ import eu.okaeri.configs.annotation.Comment;
import java.time.Duration;
public class FightPearlSettings extends OkaeriConfig {
public class PearlSettings extends OkaeriConfig {
@Comment({ "# Is pearl damage to be enabled?", "# This will work globally" })
public boolean pearlThrowDamageEnabled = true;
@@ -20,6 +20,9 @@ public class FightPearlSettings extends OkaeriConfig {
@Comment("# Set true, If you want add cooldown to pearls")
public boolean pearlCooldownEnabled = false;
@Comment("# Set true, If you want to reset timer when player throws ender pearl")
public boolean pearlExtendsCombatTag = false;
@Comment({
"# Block throwing pearls with delay?",
"# If you set this to for example 3s, player will have to wait 3 seconds before throwing another pearl"
@@ -0,0 +1,146 @@
package com.eternalcode.combat.fight.trident;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.combat.fight.event.CauseOfTag;
import com.eternalcode.combat.fight.event.FightUntagEvent;
import com.eternalcode.combat.notification.NoticeService;
import com.eternalcode.combat.util.DurationUtil;
import java.time.Duration;
import java.util.UUID;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerRiptideEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
public class TridentController implements Listener {
private final PluginConfig pluginConfig;
private final NoticeService noticeService;
private final FightManager fightManager;
private final TridentService tridentService;
private final Server server;
public TridentController(
PluginConfig pluginConfig,
NoticeService noticeService,
FightManager fightManager,
TridentService tridentService, Server server
) {
this.pluginConfig = pluginConfig;
this.noticeService = noticeService;
this.fightManager = fightManager;
this.tridentService = tridentService;
this.server = server;
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onRiptideInteract(PlayerInteractEvent event) {
if (!this.isRiptideInteract(event)) {
return;
}
Player player = event.getPlayer();
UUID playerId = player.getUniqueId();
if (!this.fightManager.isInCombat(playerId)) {
return;
}
if (this.pluginConfig.trident.tridentRiptideDisabledDuringCombat) {
event.setCancelled(true);
this.noticeService.create()
.player(playerId)
.notice(this.pluginConfig.trident.tridentRiptideBlocked)
.send();
return;
}
if (!this.tridentService.hasDelay(playerId)) {
return;
}
event.setCancelled(true);
}
// we need to send message without ignoring canceled event because of riptide cooldown
@EventHandler(priority = EventPriority.LOWEST)
public void messageOnRiptideCooldown(PlayerInteractEvent event) {
if (!this.isRiptideInteract(event)) {
return;
}
UUID playerId = event.getPlayer().getUniqueId();
if (!this.tridentService.hasDelay(playerId)) {
return;
}
Duration remainingDelay = this.tridentService.getRemainingDelay(playerId);
this.noticeService.create()
.player(playerId)
.notice(this.pluginConfig.trident.tridentRiptideOnCooldown)
.placeholder("{TIME}", DurationUtil.format(remainingDelay))
.send();
}
@EventHandler(ignoreCancelled = true)
public void onRiptide(PlayerRiptideEvent event) {
Player player = event.getPlayer();
UUID uniqueId = player.getUniqueId();
if (this.pluginConfig.trident.tridentRiptideDisabledDuringCombat) {
return;
}
if (!this.fightManager.isInCombat(uniqueId)) {
return;
}
if (this.tridentService.hasDelay(uniqueId)) {
return;
}
this.tridentService.markDelay(player.getUniqueId());
player.setCooldown(Material.TRIDENT, (int) this.pluginConfig.trident.tridentRiptideDelay.toMillis() / 50);
if (this.pluginConfig.trident.tridentRiptideExtendsCombatTag) {
this.fightManager.tag(uniqueId, this.pluginConfig.settings.combatTimerDuration, CauseOfTag.TRIDENT);
}
}
@EventHandler(ignoreCancelled = true)
public void onUntag(FightUntagEvent event) {
Player player = server.getPlayer(event.getPlayer());
player.setCooldown(Material.TRIDENT, 0);
this.tridentService.removeDelay(player.getUniqueId());
}
private boolean isRiptideInteract(PlayerInteractEvent event) {
EquipmentSlot hand = event.getHand();
if (hand != EquipmentSlot.HAND && hand != EquipmentSlot.OFF_HAND) {
return false;
}
Action action = event.getAction();
if (action != Action.RIGHT_CLICK_AIR && action != Action.RIGHT_CLICK_BLOCK) {
return false;
}
return this.isRiptideTrident(event.getItem());
}
private boolean isRiptideTrident(ItemStack itemStack) {
return itemStack != null
&& itemStack.getType() == Material.TRIDENT
&& itemStack.containsEnchantment(Enchantment.RIPTIDE);
}
}
@@ -0,0 +1,41 @@
package com.eternalcode.combat.fight.trident;
import com.eternalcode.combat.config.implementation.PluginConfig;
import com.eternalcode.combat.fight.FightManager;
import com.eternalcode.commons.delay.Delay;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.time.Duration;
import java.util.UUID;
public class TridentServiceImpl implements TridentService {
private final Delay<UUID> delay;
public TridentServiceImpl(PluginConfig pluginConfig) {
this.delay = Delay.withDefault(() -> pluginConfig.trident.tridentRiptideDelay);
}
@Override
public void markDelay(UUID uuid) {
this.delay.markDelay(uuid);
}
@Override
public boolean hasDelay(UUID uuid) {
return this.delay.hasDelay(uuid);
}
@Override
public void removeDelay(UUID playerId) {
this.delay.unmarkDelay(playerId);
}
@Override
public Duration getRemainingDelay(UUID uuid) {
return this.delay.getRemaining(uuid);
}
}
@@ -0,0 +1,39 @@
package com.eternalcode.combat.fight.trident;
import com.eternalcode.multification.notice.Notice;
import eu.okaeri.configs.OkaeriConfig;
import eu.okaeri.configs.annotation.Comment;
import java.time.Duration;
public class TridentSettings extends OkaeriConfig {
@Comment({
"# Set to true to disable riptide usage during combat",
"# This setting works globally, but can be overridden by region settings"
})
public boolean tridentRiptideDisabledDuringCombat = false;
@Comment("# Set to true so then riptide usage during combat will extend combat tag duration, works only if riptide usage is not disabled")
public boolean tridentRiptideExtendsCombatTag = false;
@Comment({
"# Should riptide enchantment be on cooldown during combat?",
"# Setting this option to 3s will make players wait 3 seconds between trident throws",
"# Setting this to 0s or below will remove cooldown"
})
public Duration tridentRiptideDelay = Duration.ofSeconds(10);
@Comment("# Message shown when riptide is blocked during combat")
public Notice tridentRiptideBlocked = Notice.builder()
.chat("<red>Using riptide is prohibited during combat!")
.build();
@Comment({
"# Message sent to the player when riptide is on cooldown",
"# Available placeholder: {TIME} - remaining time left to use riptide again"
})
public Notice tridentRiptideOnCooldown = Notice.builder()
.chat("<red>You must wait {TIME} before next riptide!")
.build();
}
@@ -0,0 +1,15 @@
package com.eternalcode.combat.config.implementation;
import static org.junit.jupiter.api.Assertions.assertFalse;
import org.junit.jupiter.api.Test;
class SignEditingSettingsTest {
@Test
void shouldDisableSignEditingBlockerByDefault() {
SignEditingSettings signEditingSettings = new SignEditingSettings();
assertFalse(signEditingSettings.disableSignEditingDuringCombat);
}
}
@@ -0,0 +1,34 @@
package com.eternalcode.combat.fight.blocker;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.bukkit.Material;
import org.junit.jupiter.api.Test;
class SignEditingBlockerTest {
@Test
void shouldRecognizeStandingSignMaterials() {
assertTrue(SignEditingBlocker.isSign(Material.OAK_SIGN));
assertTrue(SignEditingBlocker.isSign(Material.CRIMSON_SIGN));
}
@Test
void shouldRecognizeWallSignMaterials() {
assertTrue(SignEditingBlocker.isSign(Material.OAK_WALL_SIGN));
assertTrue(SignEditingBlocker.isSign(Material.WARPED_WALL_SIGN));
}
@Test
void shouldIgnoreNonSignMaterials() {
assertFalse(SignEditingBlocker.isSign(Material.CHEST));
assertFalse(SignEditingBlocker.isSign(Material.ENDER_PEARL));
}
@Test
void shouldRecognizeEnderPearlMaterial() {
assertTrue(SignEditingBlocker.isEnderPearl(Material.ENDER_PEARL));
assertFalse(SignEditingBlocker.isEnderPearl(Material.OAK_SIGN));
}
}
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
Vendored
+2 -2
View File
@@ -20,7 +20,7 @@
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
Vendored
+2 -2
View File
@@ -19,7 +19,7 @@
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2
@rem Execute Gradle
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel