Release EternalCombat-OG
This commit is contained in:
1 parent
cc85e0ad0c
commit
1f3b4a7215
9 files changed
+212
-317
No files matched your search
@@ -1,168 +1,30 @@
|
||||
<div align="center">
|
||||
# EternalCombat-OG
|
||||
|
||||

|
||||
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.
|
||||
|
||||
[](https://www.spigotmc.org/resources/eternalcombat-%E2%9C%94%EF%B8%8F-enchance-your-combat-system-with-eternalcombat.109056/)
|
||||
[](https://modrinth.com/plugin/eternalcombat)
|
||||
[](https://hangar.papermc.io/EternalCodeTeam/eternalcombat)
|
||||
## License & attribution
|
||||
|
||||
[](https://discord.com/invite/FQ7jmGBd6c)
|
||||
[](https://docs.eternalcode.pl/eternalcombat/introduction)
|
||||
[](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
|
||||
|
||||
<br>
|
||||
This fork diverges from upstream EternalCombat in the following ways:
|
||||
|
||||
<a href="https://ko-fi.com/eternalcodeteam">
|
||||
<img src="https://github.com/intergrav/devins-badges/blob/v3/assets/cozy/donate/kofi-plural-alt_64h.png?raw=true" height="64" alt="Support us on Ko-fi">
|
||||
</a>
|
||||
- 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.
|
||||
|
||||
</div>
|
||||
## 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:
|
||||
<img alt="Combat log anti logout feature" src="https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/combatlog.gif?raw=true" width="420">
|
||||
|
||||
- **Customize combat experience**
|
||||
Add custom effects to players in combat or death. Everything should be configurable and user-friendly:
|
||||
<img alt="Lightning strikes when players die" src="https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/lightning.gif?raw=true" width="420">
|
||||
<img alt="Flare indicates players death location" src="https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/flare.gif?raw=true" width="420">
|
||||
|
||||
- **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:
|
||||
<img alt="Border around protected region" src="https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/border.gif?raw=true" width="420">
|
||||
|
||||
- **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:
|
||||
<img alt="Crystal PvP showcase" src="https://github.com/EternalCodeTeam/EternalCombat/blob/master/assets/crystals.gif?raw=true" width="420">
|
||||
|
||||
- **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 <player>`. |
|
||||
| `eternalcombat.tag` | Start a fight between players using `/combatlog tag <player> [player2]`. |
|
||||
| `eternalcombat.untag` | Remove a player from combat with `/combatlog untag <player>`. |
|
||||
| `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
|
||||
|
||||
<repository>
|
||||
<id>eternalcode-reposilite-releases</id>
|
||||
<name>EternalCode Repository</name>
|
||||
<url>https://repo.eternalcode.pl/releases</url>
|
||||
</repository>
|
||||
```
|
||||
|
||||
#### 2. Add dependency:
|
||||
|
||||
With Gradle:
|
||||
|
||||
```kts
|
||||
compileOnly("com.eternalcode:eternalcombat-api:2.5.0-SNAPSHOT")
|
||||
```
|
||||
|
||||
With Maven:
|
||||
|
||||
```xml
|
||||
|
||||
<dependency>
|
||||
<groupId>com.eternalcode</groupId>
|
||||
<artifactId>eternalcombat-api</artifactId>
|
||||
<version>2.5.0-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### 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)
|
||||
+1
-1
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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/**",
|
||||
|
||||
-82
@@ -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<Double, List<Point2D>> points = generatePoints(min, max, Point2D.from(playerLocation));
|
||||
NavigableMap<Double, Double> 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<Double, Double> 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<Point2D> 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<Double, List<Point2D>> generatePoints(Point2D min, Point2D max, Point2D location) {
|
||||
Set<Point2D> 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<Double, List<Point2D>> 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));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+16
-4
@@ -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<Region> 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()
|
||||
|
||||
+124
-29
@@ -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<Region> 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) {}
|
||||
}
|
||||
-27
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
+44
-8
@@ -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);
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
rootProject.name = "EternalCombat"
|
||||
rootProject.name = "EternalCombat-OG"
|
||||
include("eternalcombat-api")
|
||||
include("eternalcombat-plugin")
|
||||
|
||||
Reference in new issue
Block a user