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);
      }
    }
  }
}