diff --git a/README.md b/README.md index 134bf2f..b55e094 100644 --- a/README.md +++ b/README.md @@ -1,168 +1,30 @@ -
+# EternalCombat-OG -![Readme banner](https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/readme-banner.png?raw=true) +A [TrueOG Network](https://true-og.net) fork of [EternalCombat](https://github.com/EternalCodeTeam/EternalCombat) +by the EternalCode Team — a combat-tag / combat-logging plugin for Paper. -[![Available on SpigotMC](https://raw.githubusercontent.com/vLuckyyy/badges/main/available-on-spigotmc.svg)](https://www.spigotmc.org/resources/eternalcombat-%E2%9C%94%EF%B8%8F-enchance-your-combat-system-with-eternalcombat.109056/) -[![Available on Modrinth](https://raw.githubusercontent.com/vLuckyyy/badges/main/avaiable-on-modrinth.svg)](https://modrinth.com/plugin/eternalcombat) -[![Available on Hangar](https://raw.githubusercontent.com/vLuckyyy/badges/main/avaiable-on-hangar.svg)](https://hangar.papermc.io/EternalCodeTeam/eternalcombat) +## License & attribution -[![Chat on Discord](https://raw.githubusercontent.com/vLuckyyy/badges/main//chat-with-us-on-discord.svg)](https://discord.com/invite/FQ7jmGBd6c) -[![Read the Docs](https://raw.githubusercontent.com/vLuckyyy/badges/main/read-the-documentation.svg)](https://docs.eternalcode.pl/eternalcombat/introduction) -[![Available on BStats](https://raw.githubusercontent.com/vLuckyyy/badges/main/available-on-bstats.svg)](https://bstats.org/plugin/bukkit/EternalCombat/17803) +EternalCombat-OG is a derivative work of **EternalCombat** by the **EternalCode Team**, distributed +under the **Apache License 2.0**. The original copyright and license are retained; see the +[`LICENSE`](LICENSE) file. Upstream project: https://github.com/EternalCodeTeam/EternalCombat -[Report Bug](https://github.com/EternalCodeTeam/EternalCombat/issues) • [Request Feature](https://github.com/EternalCodeTeam/EternalCombat/issues) • [Join Discord](https://discord.com/invite/FQ7jmGBd6c) +## Changes from upstream -
+This fork diverges from upstream EternalCombat in the following ways: - - Support us on Ko-fi - +- Renamed the project and plugin to `EternalCombat-OG`. +- Fixed combat knockback flinging players across the map or into the sky near safe zones. +- Players in combat can move between the warzone and wilderness, but not into safe areas. +- Mounted players are pushed along with their mount instead of being thrown off. +- Closed gaps that let combat-tagged players slip into shops and the market. +- Removed chat gradients. +- Added a command whitelist for "The Herobrine" minigames. +- Dropped Lands support. +- Removed the upstream GitHub Actions, plus build and dependency cleanup. -
+## Requirements -### Information - -## 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 -other versions too. -If you run into any compatibility issues, please let us know in -the [Issues tab](https://github.com/EternalCodeTeam/EternalCombat/issues). -The plugin requires **Java 21 or later**, so -ensure your server is ready. -Ready for action? -Install EternalCombat and dive in now! - -### How Does EternalCombat Work? - -Take your server’s PvP experience to epic heights with **EternalCombat 2.0**! Our robust combat logging system ensures -fair, heart-pounding battles that keep players on their toes. Here’s a rundown of what makes it stand out: - - -- **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 - -- **Customize combat experience** - Add custom effects to players in combat or death. Everything should be configurable and user-friendly: - 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 - -- **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 - -- **Fully Customizable Combat** - Tailor the combat experience to your liking with a ton of options! From disabling elytra to setting drop rates for - defeated players, you’re in control. Here’s what you can tweak: - - | Feature | Description | - |----------------------|-----------------------------------------------------------------| - | Elytra & Inventory | Disable elytra or inventory access during combat. | - | Commands | Whitelist or blacklist specific commands in combat. | - | Damage & Projectiles | Customize damage causes and projectile tagging settings. | - | Ender Pearls | Add cooldowns to pearl usage in fights. | - | Block Placement | Enable or disable block placement during combat. | - | Drop Rates | Set a percentage of items dropped by defeated players. | - | Temporary Effects | Apply special effects to players in combat for added intensity. | - | And More! | Dive into the config for even more fine-tuning options! | - -Curious for more? Check out our quick and exciting YouTube presentation [here](https://youtu.be/5pELO5B0Hhk) to see -EternalCombat in full swing and learn why it’s a game-changer for your server! - -### Permissions for EternalCombat - -Control who can use EternalCombat’s powerful features with these permissions: - -| Permission | Description | -|--------------------------------|-------------------------------------------------------------------------------------------------------| -| `eternalcombat.status` | Check a player’s combat status with `/combatlog status `. | -| `eternalcombat.tag` | Start a fight between players using `/combatlog tag [player2]`. | -| `eternalcombat.untag` | Remove a player from combat with `/combatlog untag `. | -| `eternalcombat.reload` | Reload the plugin with `/combatlog reload`. | -| `eternalcombat.receiveupdates` | Receive notifications about new plugin versions on join. | -| `eternalcombat.bypass` | When exclude `excludeAdminsFromCombat` is set to `true` users with this permission will not be tagged | - -## PlaceholderAPI - -EternalCombat 2.0 fully supports **PlaceholderAPI**, letting you integrate placeholders into other compatible plugins. -Follow the [PlaceholderAPI instructions](https://wiki.placeholderapi.com/users/) to get started. -Here are the available -placeholders: - -| Placeholder | Description | -|-------------------------------------|---------------------------------------------------------------| -| `%eternalcombat_opponent%` | Returns the name of the player you’re fighting. | -| `%eternalcombat_opponent_health%` | Returns the opponent’s health in `00.00` format. | -| `%eternalcombat_remaining_seconds%` | Returns seconds remaining until the player exits combat. | -| `%eternalcombat_remaining_millis%` | Returns milliseconds remaining until the player exits combat. | - -If a player isn’t in combat, placeholders return an empty string. -If combat wasn’t triggered by another player, -opponent-related placeholders will also return empty. - -### Developer API - -#### 1. Add repository: - -With Gradle: - -```kts -maven { - url = uri("https://repo.eternalcode.pl/releases") -} -``` - -With Maven: - -```xml - - - eternalcode-reposilite-releases - EternalCode Repository - https://repo.eternalcode.pl/releases - -``` - -#### 2. Add dependency: - -With Gradle: - -```kts -compileOnly("com.eternalcode:eternalcombat-api:2.5.0-SNAPSHOT") -``` - -With Maven: - -```xml - - - com.eternalcode - eternalcombat-api - 2.5.0-SNAPSHOT - provided - -``` - -### Contributing - -We’d love your help to make EternalCombat even better! -Check out our [contributing guide](https://github.com/EternalCodeTeam/EternalCombat/blob/master/.github/CONTRIBUTING.md) for -details on how to get involved and our [code of conduct](https://github.com/EternalCodeTeam/EternalCombat/blob/master/.github/CODE_OF_CONDUCT.md). - -### Reporting Issues - -Found a bug? -Report it in the [Issues tab](https://github.com/eternalcodeteam/eternalcombat/issues). -Please include as much detail as possible, like your Minecraft and plugin -versions, along with any error messages or logs. -Ready to transform your server’s combat experience? -Download EternalCombat 2.0 now and let the battles begin! +- Paper (Java 17+ toolchain) +- [PacketEvents](https://modrinth.com/plugin/packetevents) (required) +- WorldGuard (optional, for region-based combat restrictions) diff --git a/build.gradle.kts b/build.gradle.kts index 997ba6b..c674054 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ val pluginProject = project(":eternalcombat-plugin") val pluginJar by tasks.registering(Copy::class) { dependsOn(":eternalcombat-plugin:shadowJar") from(pluginProject.provider { - pluginProject.layout.buildDirectory.file("libs/EternalCombat-${pluginProject.version}.jar").get().asFile + pluginProject.layout.buildDirectory.file("libs/EternalCombat-OG-${pluginProject.version}.jar").get().asFile }) into(layout.buildDirectory.dir("libs")) } diff --git a/eternalcombat-plugin/build.gradle.kts b/eternalcombat-plugin/build.gradle.kts index 4ed3f66..f69d56c 100644 --- a/eternalcombat-plugin/build.gradle.kts +++ b/eternalcombat-plugin/build.gradle.kts @@ -155,13 +155,12 @@ dependencies { } bukkit { - name = "EternalCombat" + name = "EternalCombat-OG" main = "com.eternalcode.combat.CombatPlugin" version = project.version.toString() apiVersion = "1.19" - authors = listOf("EternalCodeTeam") - prefix = "EternalCombat" - name = "EternalCombat" + authors = listOf("EternalCodeTeam", "true-og") + prefix = "EternalCombat-OG" load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD softDepend = listOf( "WorldGuard" @@ -185,7 +184,7 @@ tasks { } tasks.shadowJar { - archiveFileName.set("EternalCombat-${project.version}.jar") + archiveFileName.set("EternalCombat-OG-${project.version}.jar") exclude( "org/intellij/lang/annotations/**", diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackOutsideRegionGenerator.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackOutsideRegionGenerator.java deleted file mode 100644 index 75cc380..0000000 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackOutsideRegionGenerator.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.eternalcode.combat.fight.knockback; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.NavigableMap; -import java.util.Set; -import java.util.TreeMap; -import org.bukkit.Location; - -class KnockbackOutsideRegionGenerator { - - static Location generate(Point2D min, Point2D max, Location playerLocation) { - NavigableMap> points = generatePoints(min, max, Point2D.from(playerLocation)); - NavigableMap distances = new TreeMap<>(); - double totalWeight = 0; - - Double maxDistance = points.lastKey(); - - for (double distance : points.keySet()) { - double weight = createWeight(distance, maxDistance); - distances.put(distance, weight); - totalWeight += weight; - } - - double rand = Math.random() * totalWeight; - double cumulativeWeight = 0; - - for (Map.Entry entry : distances.entrySet()) { - double distance = entry.getKey(); - double weight = entry.getValue(); - - cumulativeWeight += weight; - if (rand <= cumulativeWeight) { - return getRandom(points.get(distance)) - .toLocation(playerLocation); - } - } - - return getRandom(points.firstEntry().getValue()) - .toLocation(playerLocation); - } - - private static Point2D getRandom(List points) { - return points.get((int) (Math.random() * points.size())); - } - - private static double createWeight(double distance, double maxDistance) { - double last = Math.log(maxDistance); - double weight = last - Math.log(distance); - return Math.pow(weight, 10); - } - - private static NavigableMap> generatePoints(Point2D min, Point2D max, Point2D location) { - Set points = new HashSet<>(); - - for (int i = min.x() - 1; i <= max.x() + 1; i++) { - points.add(new Point2D(i, min.z() - 1)); - points.add(new Point2D(i, max.z() + 1)); - } - for (int i = min.z(); i <= max.z(); i++) { - points.add(new Point2D(min.x() - 1, i)); - points.add(new Point2D(max.x() + 1, i)); - } - - TreeMap> result = new TreeMap<>(); - - for (Point2D point : points) { - double distance = distance(location, point); - result.computeIfAbsent(distance, k -> new ArrayList<>()).add(point); - } - - return result; - } - - private static double distance(Point2D p1, Point2D p2) { - return Math.sqrt(Math.pow(p2.x() - p1.x(), 2) + Math.pow(p2.z() - p1.z(), 2)); - } - - -} diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackRegionController.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackRegionController.java index 109e8cd..d37f08c 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackRegionController.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackRegionController.java @@ -17,6 +17,7 @@ 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.bukkit.util.Vector; import org.spigotmc.event.entity.EntityMountEvent; public class KnockbackRegionController implements Listener { @@ -55,16 +56,22 @@ public class KnockbackRegionController implements Listener { if (xTo != xFrom || yTo != yFrom || zTo != zFrom) { Optional regionOptional = this.regionProvider.getRegion(locationTo); if (regionOptional.isEmpty()) { + // Destination is not a safe zone (wilderness or warzone) -> allow the move. return; } Region region = regionOptional.get(); - if (region.contains(locationFrom)) { + if (this.regionProvider.isInRegion(locationFrom)) { + // Origin is itself a safe zone (player was already inside it, e.g. tagged there) + // -> eject them out through the nearest wall. this.knockbackService.knockback(region, player); this.knockbackService.forceKnockbackLater(player, region); } else { + // Origin is a legal area (wilderness or warzone) and they tried to step into a safe + // zone -> cancel the move and bounce them straight back the way they came. event.setCancelled(true); - this.knockbackService.knockbackLater(region, player, Duration.ofMillis(50)); + Vector back = locationFrom.toVector().subtract(locationTo.toVector()); + this.knockbackService.knockbackDirectionLater(player, back, Duration.ofMillis(50)); } this.noticeService.create() @@ -118,11 +125,16 @@ public class KnockbackRegionController implements Listener { } Region region = regionOptional.get(); - if (region.contains(locationFrom)) { + if (this.regionProvider.isInRegion(locationFrom)) { this.knockbackService.knockback(region, player); this.knockbackService.forceKnockbackLater(player, region); } else { - this.knockbackService.knockbackLater(region, player, Duration.ofMillis(50)); + // VehicleMoveEvent cannot be cancelled, so bounce the mount back the way it came and + // schedule an eject as a fallback if it is still inside after the delay. The vehicle + // is moved with its rider rather than dismounting. + Vector back = locationFrom.toVector().subtract(locationTo.toVector()); + this.knockbackService.knockbackDirectionLater(player, back, Duration.ofMillis(50)); + this.knockbackService.forceKnockbackLater(player, region); } this.noticeService.create() diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackService.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackService.java index bae8afa..f3624d7 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackService.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackService.java @@ -1,7 +1,6 @@ package com.eternalcode.combat.fight.knockback; import com.eternalcode.combat.config.implementation.PluginConfig; -import com.eternalcode.combat.region.Point; import com.eternalcode.combat.region.Region; import com.eternalcode.combat.region.RegionProvider; import com.eternalcode.commons.bukkit.scheduler.MinecraftScheduler; @@ -9,15 +8,18 @@ import io.papermc.lib.PaperLib; import java.time.Duration; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import java.util.UUID; import org.bukkit.Location; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.bukkit.util.Vector; public final class KnockbackService { + // Pushed just past the nearest region wall so the player lands outside the cuboid. + private static final double EJECT_MARGIN = 1.5; + private final PluginConfig config; private final MinecraftScheduler scheduler; private final RegionProvider regionProvider; @@ -34,6 +36,25 @@ public final class KnockbackService { this.scheduler.runLater(() -> this.knockback(region, player), duration); } + // Push the player along the given horizontal direction. Used when a move into a safe zone is + // cancelled: the direction is (from - to), i.e. straight back the way the player came, so they + // bounce back into the legal area (wilderness or warzone) regardless of the safe region's shape + // or size. Applied a tick later so it survives the move cancellation. + public void knockbackDirectionLater(Player player, Vector horizontalDirection, Duration duration) { + Vector direction = new Vector(horizontalDirection.getX(), 0, horizontalDirection.getZ()); + if (direction.lengthSquared() < 1.0E-6) { + return; + } + + double multiplier = this.config.knockback.multiplier; + Vector velocity = direction.normalize().multiply(multiplier); + + this.scheduler.runLater( + () -> applyVelocity(player, new Vector(velocity.getX(), 0.5, velocity.getZ())), + duration + ); + } + public void forceKnockbackLater(Player player, Region region) { if (insideRegion.containsKey(player.getUniqueId())) { return; @@ -49,39 +70,113 @@ public final class KnockbackService { return; } - if (player.isInsideVehicle()) { - player.leaveVehicle(); + // Eject through the nearest wall, keeping the player's own Y. No highest-block lookup + // (which launched players onto walls/treetops) and no bounding-box perimeter teleport + // (which threw them dozens of blocks across large regions). A mounted player is moved + // by teleporting their vehicle, so the mount travels with them instead of dismounting. + Location target = ejectLocation(region, playerLocation); + Entity vehicle = topVehicle(player); + if (vehicle != null) { + PaperLib.teleportAsync(vehicle, target, TeleportCause.PLUGIN); + } else { + PaperLib.teleportAsync(player, target, TeleportCause.PLUGIN); } - - Location location = generate(playerLocation, Point2D.from(region.getMin()), Point2D.from(region.getMax())); - - PaperLib.teleportAsync(player, location, TeleportCause.PLUGIN); }, this.config.knockback.forceDelay); } - private Location generate(Location playerLocation, Point2D minX, Point2D maxX) { - Location location = KnockbackOutsideRegionGenerator.generate(minX, maxX, playerLocation); - Optional otherRegion = regionProvider.getRegion(location); - if (otherRegion.isPresent()) { - Region region = otherRegion.get(); - return generate(playerLocation, minX.min(region.getMin()), maxX.max(region.getMax())); - } - - return location; - } - public void knockback(Region region, Player player) { - if (player.isInsideVehicle()) { - player.leaveVehicle(); + Vector outward = outwardDirection(region, player.getLocation()); + double multiplier = this.config.knockback.multiplier; + + applyVelocity(player, new Vector(outward.getX() * multiplier, 0.5, outward.getZ() * multiplier)); + } + + // Apply velocity to the player, or to the vehicle they are riding so the mount (and its rider) + // move together. Never dismounts the player. + private static void applyVelocity(Player player, Vector velocity) { + Entity vehicle = topVehicle(player); + if (vehicle != null) { + vehicle.setVelocity(velocity); + } else { + player.setVelocity(velocity); + } + } + + // The root of the player's mount stack (the entity actually being moved), or null when the + // player is on foot. + private static Entity topVehicle(Player player) { + Entity vehicle = player.getVehicle(); + if (vehicle == null) { + return null; + } + while (vehicle.getVehicle() != null) { + vehicle = vehicle.getVehicle(); + } + return vehicle; + } + + // Unit vector pointing straight out of the closest vertical face of the region. For a player + // crossing a wall this is the wall they crossed, so the push always sends them back the way + // they came regardless of how large the region is. + private static Vector outwardDirection(Region region, Location location) { + Face face = nearestFace(region, location); + return switch (face.side) { + case WEST -> new Vector(-1, 0, 0); + case EAST -> new Vector(1, 0, 0); + case NORTH -> new Vector(0, 0, -1); + case SOUTH -> new Vector(0, 0, 1); + }; + } + + private static Location ejectLocation(Region region, Location location) { + Face face = nearestFace(region, location); + + double minX = region.getMin().getX(); + double maxX = region.getMax().getX() + 1; + double minZ = region.getMin().getZ(); + double maxZ = region.getMax().getZ() + 1; + + double x = location.getX(); + double z = location.getZ(); + + switch (face.side) { + case WEST -> x = minX - EJECT_MARGIN; + case EAST -> x = maxX + EJECT_MARGIN; + case NORTH -> z = minZ - EJECT_MARGIN; + case SOUTH -> z = maxZ + EJECT_MARGIN; } - Point point = region.getCenter(); - Location subtract = player.getLocation().subtract(point.x(), 0, point.z()); - - Vector knockbackVector = new Vector(subtract.getX(), 0, subtract.getZ()).normalize(); - double multiplier = this.config.knockback.multiplier; - Vector configuredVector = new Vector(multiplier, 0.5, multiplier); - - player.setVelocity(knockbackVector.multiply(configuredVector)); + return new Location(location.getWorld(), x, location.getY(), z, location.getYaw(), location.getPitch()); } + + private static Face nearestFace(Region region, Location location) { + double minX = region.getMin().getX(); + double maxX = region.getMax().getX() + 1; + double minZ = region.getMin().getZ(); + double maxZ = region.getMax().getZ() + 1; + + double px = location.getX(); + double pz = location.getZ(); + + double west = px - minX; + double east = maxX - px; + double north = pz - minZ; + double south = maxZ - pz; + + double nearest = Math.min(Math.min(west, east), Math.min(north, south)); + if (nearest == west) { + return new Face(Side.WEST, west); + } + if (nearest == east) { + return new Face(Side.EAST, east); + } + if (nearest == north) { + return new Face(Side.NORTH, north); + } + return new Face(Side.SOUTH, south); + } + + private enum Side { WEST, EAST, NORTH, SOUTH } + + private record Face(Side side, double distance) {} } diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/Point2D.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/Point2D.java deleted file mode 100644 index ec31f56..0000000 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/Point2D.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.eternalcode.combat.fight.knockback; - -import org.bukkit.Location; -import org.bukkit.World; - -record Point2D(int x, int z) { - - Location toLocation(Location location) { - World world = location.getWorld(); - int y = world.getHighestBlockYAt(x, z) + 1; - - return new Location(world, x, y, z, location.getYaw(), location.getPitch()); - } - - static Point2D from(Location location) { - return new Point2D(location.getBlockX(), location.getBlockZ()); - } - - public Point2D min(Location min) { - return new Point2D(Math.min(this.x, min.getBlockX()), Math.min(this.z, min.getBlockZ())); - } - - public Point2D max(Location max) { - return new Point2D(Math.max(this.x, max.getBlockX()), Math.max(this.z, max.getBlockZ())); - } - -} diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/region/worldguard/WorldGuardRegionProvider.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/region/worldguard/WorldGuardRegionProvider.java index fdc44c8..26c2e31 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/region/worldguard/WorldGuardRegionProvider.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/region/worldguard/WorldGuardRegionProvider.java @@ -38,17 +38,24 @@ public class WorldGuardRegionProvider implements RegionProvider { RegionQuery regionQuery = this.regionContainer.createQuery(); ApplicableRegionSet applicableRegions = regionQuery.getApplicableRegions(BukkitAdapter.adapt(location)); - ProtectedRegion highestPriorityRegion = this.highestPriorityRegion(applicableRegions); - - if (highestPriorityRegion == null) { + if (applicableRegions.size() == 0) { return Optional.empty(); } - if (this.isCombatRegion(highestPriorityRegion)) { - return Optional.of(new WorldGuardRegion(location.getWorld(), highestPriorityRegion)); + if (!this.isRestricted(applicableRegions)) { + return Optional.empty(); } - return Optional.empty(); + // Eject relative to the innermost (highest-priority) region the player is standing in, not + // whichever region happens to set the PVP flag. The innermost region is the tightest box, so + // the knockback/teleport distance stays local instead of being measured against a giant + // world-spanning spawn region. + ProtectedRegion bounds = this.innermostRegion(applicableRegions); + if (bounds == null) { + return Optional.empty(); + } + + return Optional.of(new WorldGuardRegion(location.getWorld(), bounds)); } @Override @@ -61,11 +68,40 @@ public class WorldGuardRegionProvider implements RegionProvider { return regionManager.getRegions() .values() .stream() - .filter(region -> this.isCombatRegion(region)) + .filter(this::isCombatRegion) .map(region -> (Region) new WorldGuardRegion(world, region)) .toList(); } + // Whether the player's current location is a no-PvP / restricted spot. Uses WorldGuard's own flag + // resolution (queryState) so inherited and priority-overridden PVP flags are honoured. This closes + // the holes where a child region (e.g. a shop) without its own PVP flag was treated as unprotected. + private boolean isRestricted(ApplicableRegionSet applicableRegions) { + StateFlag.State pvpState = this.pluginConfig.regions.preventPvpInRegions + ? applicableRegions.queryState(null, Flags.PVP) + : null; + + // Effective PvP is denied here (inheritance/priority aware) -> safe zone, block entry. + if (pvpState == StateFlag.State.DENY) { + return true; + } + + // A region in the blockedRegions list applies here, but never treat a spot as restricted when + // a higher-priority region explicitly allows PvP. Otherwise a large umbrella region (e.g. a + // world-spanning "spawn" listed in blockedRegions) would also block the warzone/arenas nested + // inside it. + if (pvpState != StateFlag.State.ALLOW) { + for (ProtectedRegion region : applicableRegions.getRegions()) { + if (this.regions.contains(region.getId())) { + return true; + } + } + } + + return false; + } + + // Per-region check used only for border rendering, where there is no single query point. private boolean isCombatRegion(ProtectedRegion region) { if (this.regions.contains(region.getId())) { return true; @@ -83,7 +119,7 @@ public class WorldGuardRegionProvider implements RegionProvider { } @Nullable - private ProtectedRegion highestPriorityRegion(ApplicableRegionSet applicableRegions) { + private ProtectedRegion innermostRegion(ApplicableRegionSet applicableRegions) { return applicableRegions.getRegions().stream() .max(Comparator.comparingInt(ProtectedRegion::getPriority)) .orElse(null); diff --git a/settings.gradle.kts b/settings.gradle.kts index 3063fb7..67e595d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ -rootProject.name = "EternalCombat" +rootProject.name = "EternalCombat-OG" include("eternalcombat-api") include("eternalcombat-plugin")