Minecarts.java
AKL City ยท akl-city.git
Branches > main > de59059 > files / src / main / java / dev / grahp / aklcity / Minecarts.java
package dev.grahp.aklcity;
import static dev.grahp.aklcity.PlayerUtils.isHoldingFist;
import org.bukkit.*;
import org.bukkit.event.*;
import org.bukkit.event.vehicle.*;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.block.Action;
import org.bukkit.block.Block;
import org.bukkit.block.data.Powerable;
import org.bukkit.entity.*;
import org.bukkit.util.Vector;
import org.bukkit.scheduler.BukkitTask;
import java.util.*;
import net.kyori.adventure.text.Component;
// TODO:
// - Stops mobs from pathfinding onto rails? Or... Just kill them when they do smiemsie, keep momentum
// Somehow tell whether the player is going the wrong way?
public class Minecarts implements Listener {
private static final Material stopperRailMaterial = Material.DETECTOR_RAIL;
private static final long timeToWaitForMinecartsToBeKilledAfterBeingDismounted = 5L;
private static final double minecartVelocityThreshold = 0.001;
private static final long ticksToStopAtStops = 20L;
private static final long extraTicksToDismountAfterDeparting = 10L;
/// Returns whether a player is in a Minecart
private boolean isPlayerInMinecart(Minecart cart) {
return !cart.getPassengers().isEmpty() && cart.getPassengers().get(0) instanceof Player;
}
// TODO should find a safe point to dismount the player and tp them there
private void dismount(Minecart cart) {
cart.eject();
}
private void killMinecart(Minecart cart) {
dismount(cart);
Timers.schedule(timeToWaitForMinecartsToBeKilledAfterBeingDismounted, () -> {
if (!cart.isDead()) {
cart.remove();
}
});
}
/// Returns whether a Minecart is on top of a rail that it should slow down and allow the player to exit for
private boolean isOnStopperRail(Minecart cart) {
return cart.getLocation().getBlock().getType() == stopperRailMaterial;
}
/// Returns whether a Minecart is stopped, or moving so slow that it's basically not moving at all
private boolean isCartStopped(Minecart cart) {
return cart.getVelocity().lengthSquared() < minecartVelocityThreshold;
}
@EventHandler
public void onEnterMinecart(VehicleEnterEvent e) {
if (e.getVehicle() instanceof Minecart && e.getEntered() instanceof Player p) {
// Needs to be delayed a tick to override the normal message
Timers.runNextTick(() -> {
p.sendActionBar(
Component.text("Press ")
.append(Component.keybind("key.sneak"))
.append(Component.text(" to exit at the next stop"))
);
});
}
}
@EventHandler
public void onExitingMinecart(VehicleExitEvent e) {
if (e.getVehicle() instanceof Minecart cart && cart.getPassengers().get(0) instanceof Player p) {
if (isCartStopped(cart)) {
killMinecart(cart);
if (stopLocations.containsKey(cart)) {
p.teleport(stopLocations.get(cart));
}
} else {
e.setCancelled(true);
p.sendActionBar(Component.text("You may only exit at a stop!"));
}
stopLocations.remove(cart);
}
}
private Map<Minecart, Location> stopLocations = new HashMap();
private Map<Minecart, Vector> previousSpeeds = new HashMap();
private Set<Minecart> cartsAtStops = new HashSet();
private enum StopType {
NORMAL, // Waited for the minecart to stop at its stop and then continue on
SKIPPED // Manually pressed W to move on to the next stop
}
/// Departs the Minecart from the stop it's stopped at.
/// When called after a cart has already departed, does nothing.
/// After a delay, a stopped minecart will automatically go forward,
/// but the player may choose to do this manually. When skipping a stop,
/// extra leeway is not given to allow the player to exit the stop after the
/// cart has resumed motion.
private void departFromStop(Minecart cart, StopType type) {
if (!cart.isDead() && cart.isValid() && cartsAtStops.contains(cart) && previousSpeeds.containsKey(cart)) {
cart.setVelocity(previousSpeeds.get(cart).normalize().multiply(0.4));
}
switch (type) {
case NORMAL -> Timers.schedule(extraTicksToDismountAfterDeparting, () -> {
stopLocations.remove(cart);
});
case SKIPPED -> stopLocations.remove(cart);
}
previousSpeeds.remove(cart);
}
public void onEnterStop(Minecart cart) {
if (!isPlayerInMinecart(cart)) {
killMinecart(cart);
} else {
cartsAtStops.add(cart);
previousSpeeds.put(cart, cart.getVelocity().clone());
stopLocations.put(cart, cart.getLocation());
cart.setVelocity(new Vector(0, 0, 0));
Timers.schedule(ticksToStopAtStops, () -> {
departFromStop(cart, StopType.NORMAL);
});
}
}
// TODO Being block-based has the fundamental problem that travelling too fast can cause you to skip over a block entirely
@EventHandler
public void onMove(VehicleMoveEvent e) {
if (e.getVehicle() instanceof Minecart cart && isPlayerInMinecart(cart)) {
var movingAtStop = isOnStopperRail(cart);
var alreadyAtStop = cartsAtStops.contains(cart);
if (movingAtStop && !alreadyAtStop) {
onEnterStop(cart);
}
if (movingAtStop && alreadyAtStop) {
departFromStop(cart, StopType.SKIPPED);
}
if (!movingAtStop) {
cartsAtStops.remove(cart);
}
}
}
/// Spawns a Minecart properly on a block and returns the new Minecart entity
public Minecart spawnMinecartOnBlock(Block b) {
var spawnLocation = b.getLocation().add(0.5, 0.1, 0.5);
var cart = (Minecart) b.getWorld().spawn(spawnLocation, Minecart.class);
return cart;
}
public boolean isRightClickingOnPoweredRail(PlayerInteractEvent e) {
Block b = e.getClickedBlock();
return b != null && e.getAction().isRightClick() && b.getType() == Material.POWERED_RAIL;
}
@EventHandler
public void onRailClick(PlayerInteractEvent e) {
if (isRightClickingOnPoweredRail(e)) {
var p = e.getPlayer();
if (isHoldingFist(p)) {
e.setCancelled(true);
var b = e.getClickedBlock();
spawnMinecartOnBlock(b).addPassenger(p);
}
}
}
}