package com.supaham.commons.bukkit.players;
import com.supaham.commons.bukkit.TickerTask;
import com.supaham.commons.bukkit.listeners.PlayerListeners;
import com.supaham.commons.bukkit.modules.CommonModule;
import com.supaham.commons.bukkit.modules.ModuleContainer;
import com.supaham.commons.bukkit.potion.Potion;
import com.supaham.commons.bukkit.potion.PotionEffectManager;
import com.supaham.commons.bukkit.potion.Potions;
import com.supaham.commons.bukkit.utils.LocationUtils;
import com.supaham.commons.state.State;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerToggleFlightEvent;
import org.bukkit.plugin.Plugin;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nonnull;
/**
* Supa-awesome player freezing class. This class utilizes flight and flight speed especially for
* handling players midair and stop them from moving by setting them as flying and their flight
* speed.
*
* <p/>
* Since this code does modify a player's walk speed when they get frozen, if for whatever case
* the player retains the zero walk speed, you may use {@link PlayerListeners#defaultSpeeds(Plugin)}
* in order to reset their default walk and flight speed on join.
*
* <p/>
* This effect does not carry across sessions as the player is immediately unfrozen upon quitting
* the game.
*
* @see #Freeze(ModuleContainer)
* @see #freeze(Player)
* @since 0.2
*/
public class Freeze extends CommonModule {
private static final Potion NO_JUMP = Potions.noJump().infinite();
private final Map<Player, PlayerData> frozenPlayers = new HashMap<>();
private final Listener listener = new PlayerListener();
private final TickerTask expiryTask;
private final PotionEffectManager potionEffectManager;
/**
* Constructs a new {@link Freeze}. This is equivalent to calling {@link #Freeze(ModuleContainer,
* long)} with the {@code long} parameter as 1.
*
* @param container module container to own this module.
*
* @see #Freeze(ModuleContainer, long)
* @see #freeze(Player, int)
*/
public Freeze(@Nonnull ModuleContainer container) {
this(container, 1);
}
/**
* Constructs a new {@link Freeze}. The long parameter is how often (in ticks) the expiry
* checking task should run, by default it's 1.
*
* @param container module container to own this module
* @param interval interval of the expiry checking task
*
* @see #freeze(Player, int)
*/
public Freeze(@Nonnull ModuleContainer container, long interval) {
super(container);
this.expiryTask = new ExpiryTask(0, interval);
PotionEffectManager pem = this.container.getModule(PotionEffectManager.class);
this.potionEffectManager = pem == null ? new PotionEffectManager(container) : pem;
if (pem == null) { // we created our own freeze.
this.potionEffectManager.setState(State.ACTIVE);
}
registerTask(this.expiryTask);
registerListener(this.listener);
}
/**
* Freezes a {@link Player}, completely stopping them from moving, infinitely. This is equivalent
* to calling {@link #freeze(Player, int)} with the {@code long} parameter as -1.
* <p />
* This effect does not carry across sessions as the player is immediately unfrozen upon quitting
* the game.
*
* @param player player to freeze
*
* @see #freeze(Player, int)
* @see #freeze(Player, int, boolean)
* @see #unfreeze(Player)
*/
public void freeze(@Nonnull Player player) {
freeze(player, -1);
}
/**
* Freezes a {@link Player}, completely stopping them from moving. The duration is in ticks, if
* set to anything smaller than 0, the effect will be infinite.
* This is equivalent to calling {@link #freeze(Player, int, boolean)} with the {@code boolean}
* parameter as false.
* <p />
* This effect does not carry across sessions as the player is immediately unfrozen upon quitting
* the game.
*
* @param player player to freeze
* @param duration duration (in ticks) of this effect
*
* @see #freeze(Player, int, boolean)
* @see #unfreeze(Player)
*/
public void freeze(@Nonnull Player player, int duration) {
freeze(player, duration, false);
}
/**
* Freezes a {@link Player}, completely stopping them from moving. The duration is in ticks, if
* set to anything smaller than 0, the effect will be infinite. If {@code turningAllowed} is
* true, then the player may look around them, but still be frozen in their block coordinates.
*
* <p />
* This effect does not carry across sessions as the player is immediately unfrozen upon quitting
* the game.
*
* @param player player to freeze
* @param duration duration (in ticks) of this effect
* @param turningAllowed whether to allow the player to look around, but still not move
*
* @see #unfreeze(Player)
*/
public void freeze(@Nonnull Player player, int duration, boolean turningAllowed) {
PlayerData data = this.frozenPlayers.get(player);
if (data != null) {
data.setExpires(duration);
data.turningAllowed = turningAllowed;
return;
}
this.frozenPlayers.put(player, new PlayerData(player, duration, turningAllowed));
this.potionEffectManager.apply(NO_JUMP, player);
player.setWalkSpeed(0);
player.setFlySpeed(0);
player.setAllowFlight(true);
player.setFlying(true);
}
/**
* Unfreezes a {@link Player}, undoing any changes applied by this effect.
*
* @param player player to unfreeze
*
* @return whether the player was unfrozen
*/
public boolean unfreeze(@Nonnull Player player) {
return unfreeze(player, this.frozenPlayers.remove(player));
}
private boolean unfreeze(@Nonnull Player player, PlayerData data) {
if (data != null) {
data.applyDefaults(player);
this.potionEffectManager.clear(player, NO_JUMP.getPotionId());
}
return data != null;
}
public boolean isFrozen(@Nonnull Player player) {
return this.frozenPlayers.containsKey(player);
}
private final class ExpiryTask extends TickerTask {
public ExpiryTask(long delay, long interval) {
super(Freeze.this.plugin, delay, interval);
}
@Override
public void run() {
Iterator<Entry<Player, PlayerData>> it = Freeze.this.frozenPlayers.entrySet().iterator();
while (it.hasNext()) {
Entry<Player, PlayerData> entry = it.next();
if (entry.getValue().isDone()) {
unfreeze(entry.getKey(), entry.getValue());
it.remove();
}
}
}
}
private final class PlayerListener implements Listener {
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
PlayerData data = Freeze.this.frozenPlayers.get(event.getPlayer());
if (data != null && (!data.turningAllowed
|| !LocationUtils.isSameCoordinates(event.getFrom(), event.getTo()))) {
Location tp = event.getFrom();
if (data.turningAllowed) {
tp.setYaw(event.getTo().getYaw());
tp.setPitch(event.getTo().getPitch());
}
event.getPlayer().teleport(event.getFrom());
}
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerQuit(PlayerQuitEvent event) {
unfreeze(event.getPlayer());
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onPlayerToggleFlight(PlayerToggleFlightEvent event) {
if (!event.isFlying() && Freeze.this.frozenPlayers.containsKey(event.getPlayer())) {
event.setCancelled(true);
}
}
}
private final class PlayerData {
// player's state prior to being frozen
private final float walkSpeed;
private final float flySpeed;
private final boolean allowFlight;
private final boolean flying;
private long expires = -1;
private boolean turningAllowed;
public PlayerData(Player player, int duration, boolean turningAllowed) {
this.expires = duration;
this.walkSpeed = player.getWalkSpeed();
this.flySpeed = player.getFlySpeed();
this.allowFlight = player.getAllowFlight();
this.flying = player.isFlying();
this.turningAllowed = turningAllowed;
setExpires(duration);
}
public void applyDefaults(Player player) {
player.setWalkSpeed(this.walkSpeed);
player.setFlySpeed(this.flySpeed);
player.setAllowFlight(this.allowFlight);
player.setFlying(this.flying);
}
public boolean isDone() {
return this.expires > -1 && System.currentTimeMillis() - this.expires >= 0;
}
public void setExpires(int duration) {
if (duration >= 0 && duration != Integer.MAX_VALUE) {
this.expires = System.currentTimeMillis() + ((long) duration * 50);
} else {
this.expires = -1;
}
}
}
}