package net.citizensnpcs.trait; import java.lang.reflect.Constructor; import java.util.List; import java.util.Map; import org.bukkit.Location; import org.bukkit.entity.EnderDragon; import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.entity.Vehicle; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.block.Action; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.util.Vector; import com.google.common.collect.Maps; import net.citizensnpcs.Settings.Setting; import net.citizensnpcs.api.command.CommandConfigurable; import net.citizensnpcs.api.command.CommandContext; import net.citizensnpcs.api.event.NPCRightClickEvent; import net.citizensnpcs.api.exception.NPCLoadException; import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.api.persistence.Persist; import net.citizensnpcs.api.trait.Trait; import net.citizensnpcs.api.trait.TraitName; import net.citizensnpcs.api.trait.trait.Owner; import net.citizensnpcs.api.util.DataKey; import net.citizensnpcs.util.NMS; import net.citizensnpcs.util.Util; /** * Persists the controllable status for /npc controllable * * A controllable {@link NPC} can be mounted by a {@link Player} using right click or /npc mount and moved around using * e.g. arrow keys. */ @TraitName("controllable") public class Controllable extends Trait implements Toggleable, CommandConfigurable { private MovementController controller = new GroundController(); @Persist private boolean enabled = true; private EntityType explicitType; @Persist("owner_required") private boolean ownerRequired; public Controllable() { super("controllable"); } /** * Configures the explicit type parameter. * * @see #setExplicitType(EntityType) */ @Override public void configure(CommandContext args) { if (args.hasFlag('f')) { explicitType = EntityType.BLAZE; } else if (args.hasFlag('g')) { explicitType = EntityType.OCELOT; } else if (args.hasFlag('o')) { explicitType = EntityType.UNKNOWN; } else if (args.hasFlag('r')) { explicitType = null; } else if (args.hasValueFlag("explicittype")) { explicitType = Util.matchEnum(EntityType.values(), args.getFlag("explicittype")); } if (npc.isSpawned()) { loadController(); } } private void enterOrLeaveVehicle(Player player) { List passengers = NMS.getPassengers(player); if (passengers.size() > 0) { if (passengers.contains(player)) { player.leaveVehicle(); } return; } if (!player.hasPermission( "citizens.npc.controllable." + npc.getEntity().getType().name().toLowerCase().replace("_", "")) || !player.hasPermission("citizens.npc.controllable") || ownerRequired && !npc.getOrAddTrait(Owner.class).isOwnedBy(player)) return; NMS.mount(npc.getEntity(), player); } public boolean isEnabled() { return enabled; } @Override public void load(DataKey key) throws NPCLoadException { if (key.keyExists("explicittype")) { explicitType = Util.matchEnum(EntityType.values(), key.getString("explicittype")); } } private void loadController() { EntityType type = npc.getEntity().getType(); if (explicitType != null) { type = explicitType; } if (!(npc.getEntity() instanceof LivingEntity) && !(npc.getEntity() instanceof Vehicle) && (explicitType == null || explicitType == EntityType.UNKNOWN || npc.getEntity().getType() == explicitType)) { controller = new LookAirController(); return; } Constructor innerConstructor = CONTROLLER_TYPES.get(type); if (innerConstructor == null) { controller = new GroundController(); return; } try { if (innerConstructor.getParameterCount() == 0) { controller = innerConstructor.newInstance(); } else { controller = innerConstructor.newInstance(this); } } catch (Exception e) { e.printStackTrace(); controller = new GroundController(); } } /** * Attempts to mount the {@link Player} onto the {@link NPC}. * * @param toMount * the player to mount * @return whether the mount was successful */ public boolean mount(Player toMount) { List passengers = NMS.getPassengers(npc.getEntity()); if (passengers.size() != 0) return false; boolean found = false; for (Entity passenger : passengers) { if (passenger != null && passenger == toMount) { found = true; break; } } if (found) return false; enterOrLeaveVehicle(toMount); return true; } @EventHandler(priority = EventPriority.MONITOR) private void onPlayerInteract(PlayerInteractEvent event) { if (!npc.isSpawned() || !enabled) return; Action performed = event.getAction(); if (!NMS.getPassengers(npc.getEntity()).contains(event.getPlayer())) return; switch (performed) { case RIGHT_CLICK_BLOCK: if (event.isCancelled()) return; case RIGHT_CLICK_AIR: controller.rightClick(event); break; case LEFT_CLICK_BLOCK: if (event.isCancelled()) return; case LEFT_CLICK_AIR: controller.leftClick(event); break; default: break; } } @EventHandler private void onRightClick(NPCRightClickEvent event) { if (!enabled || !npc.isSpawned() || !event.getNPC().equals(npc)) return; controller.rightClickEntity(event); event.setDelayedCancellation(true); } @Override public void onSpawn() { loadController(); } @Override public void run() { if (!enabled || !npc.isSpawned()) return; List passengers = NMS.getPassengers(npc.getEntity()); if (passengers.size() == 0 || !(passengers.get(0) instanceof Player) || npc.getNavigator().isNavigating()) return; controller.run((Player) passengers.get(0)); } @Override public void save(DataKey key) { if (explicitType == null) { key.removeKey("explicittype"); } else { key.setString("explicittype", explicitType.name()); } } public boolean setEnabled(boolean enabled) { this.enabled = enabled; return enabled; } /** * Configures the explicit typei.e. whether the NPC should be controlled as if it was a certain {@link EntityType}. * * @param type * the explicit type */ public void setExplicitType(EntityType type) { explicitType = type; } private void setMountedYaw(Entity entity) { if (entity instanceof EnderDragon || !Setting.USE_BOAT_CONTROLS.asBoolean()) return; // EnderDragon handles this separately Location loc = entity.getLocation(); Vector vel = entity.getVelocity(); if (vel.lengthSquared() == 0) return; double tX = loc.getX() + vel.getX(); double tZ = loc.getZ() + vel.getZ(); if (loc.getZ() > tZ) { loc.setYaw((float) -Math.toDegrees(Math.atan((loc.getX() - tX) / (loc.getZ() - tZ))) + 180F); } else if (loc.getZ() < tZ) { loc.setYaw((float) -Math.toDegrees(Math.atan((loc.getX() - tX) / (loc.getZ() - tZ)))); } NMS.look(entity, loc.getYaw(), loc.getPitch()); } /** * Sets whether the {@link Player} attempting to mount the {@link NPC} must actually own the {@link NPC} to mount * it. * * @see Owner#isOwnedBy(org.bukkit.command.CommandSender) */ public void setOwnerRequired(boolean ownerRequired) { this.ownerRequired = ownerRequired; } @Override public boolean toggle() { enabled = !enabled; if (!enabled && NMS.getPassengers(npc.getEntity()).size() > 0) { NMS.getPassengers(npc.getEntity()).get(0).leaveVehicle(); } return enabled; } private double updateHorizontalSpeed(Entity handle, Entity passenger, double speed, float speedMod, double maxSpeed) { Vector vel = handle.getVelocity(); double oldSpeed = Math.sqrt(vel.getX() * vel.getX() + vel.getZ() * vel.getZ()); double horizontal = NMS.getHorizontalMovement(passenger); if (Math.abs(Math.abs(horizontal) - 0.98) > 0.02) return speed; double yaw = passenger.getLocation().getYaw(); if (horizontal > 0.0D) { double dXcos = -Math.sin(yaw * Math.PI / 180.0F); double dXsin = Math.cos(yaw * Math.PI / 180.0F); vel = vel.setX(dXcos * speed * speedMod).setZ(dXsin * speed * speedMod); } vel = vel.add(new Vector( passenger.getVelocity().getX() * speedMod * Setting.CONTROLLABLE_GROUND_DIRECTION_MODIFIER.asDouble(), 0D, passenger.getVelocity().getZ() * speedMod * Setting.CONTROLLABLE_GROUND_DIRECTION_MODIFIER.asDouble())) .multiply(0.98); double newSpeed = Math.sqrt(vel.getX() * vel.getX() + vel.getZ() * vel.getZ()); if (newSpeed > maxSpeed) { vel = vel.multiply(new Vector(maxSpeed / newSpeed, 1, maxSpeed / newSpeed)); newSpeed = maxSpeed; } handle.setVelocity(vel); if (newSpeed > oldSpeed && speed < maxSpeed) { return (float) Math.min(maxSpeed, speed + (maxSpeed - speed) / 50.0D); } else { return (float) Math.max(0, speed - speed / 50.0D); } } public class GroundController implements MovementController { private int jumpTicks = 0; private double speed = 0.07D; @Override public void leftClick(PlayerInteractEvent event) { } @Override public void rightClick(PlayerInteractEvent event) { } @Override public void rightClickEntity(NPCRightClickEvent event) { enterOrLeaveVehicle(event.getClicker()); } @Override public void run(Player rider) { boolean onGround = NMS.isOnGround(npc.getEntity()); float impulse = npc.getNavigator().getDefaultParameters() .modifiedSpeed(onGround ? GROUND_SPEED : AIR_SPEED); if (!Util.isHorse(npc.getEntity().getType())) { speed = updateHorizontalSpeed(npc.getEntity(), rider, speed, impulse, Setting.MAX_CONTROLLABLE_GROUND_SPEED.asDouble()); } if (onGround && jumpTicks <= 0 && NMS.shouldJump(rider)) { npc.getEntity().setVelocity(npc.getEntity().getVelocity().setY(JUMP_VELOCITY)); jumpTicks = 10; } jumpTicks--; setMountedYaw(npc.getEntity()); } private static final float AIR_SPEED = 0.5F; private static final float GROUND_SPEED = 0.5F; private static final float JUMP_VELOCITY = 0.5F; } public class LookAirController implements MovementController { private boolean paused = false; @Override public void leftClick(PlayerInteractEvent event) { paused = !paused; } @Override public void rightClick(PlayerInteractEvent event) { paused = !paused; } @Override public void rightClickEntity(NPCRightClickEvent event) { enterOrLeaveVehicle(event.getClicker()); } @Override public void run(Player rider) { if (paused) { npc.getEntity().setVelocity(npc.getEntity().getVelocity().setY(0.001)); return; } Vector dir = rider.getEyeLocation().getDirection(); dir.multiply(npc.getNavigator().getDefaultParameters().speedModifier()); npc.getEntity().setVelocity(dir); setMountedYaw(npc.getEntity()); } } public static interface MovementController { void leftClick(PlayerInteractEvent event); void rightClick(PlayerInteractEvent event); void rightClickEntity(NPCRightClickEvent event); void run(Player rider); } public class PlayerInputAirController implements MovementController { private boolean paused = false; private double speed; @Override public void leftClick(PlayerInteractEvent event) { paused = !paused; } @Override public void rightClick(PlayerInteractEvent event) { npc.getEntity().setVelocity(npc.getEntity().getVelocity().setY(-0.25F)); } @Override public void rightClickEntity(NPCRightClickEvent event) { enterOrLeaveVehicle(event.getClicker()); } @Override public void run(Player rider) { if (paused) { npc.getEntity().setVelocity(npc.getEntity().getVelocity().setY(0.001F)); return; } speed = updateHorizontalSpeed(npc.getEntity(), rider, speed, 1F, Setting.MAX_CONTROLLABLE_FLIGHT_SPEED.asDouble()); boolean shouldJump = NMS.shouldJump(rider); if (shouldJump) { npc.getEntity().setVelocity(npc.getEntity().getVelocity().setY(0.25F)); } npc.getEntity().setVelocity(npc.getEntity().getVelocity().multiply(new Vector(1, 0.98, 1))); setMountedYaw(npc.getEntity()); } } /** * Register a movement controller for a certain {@link EntityType} to be used for {@link NPC}s with that type. * * Default controllers are registered for BAT, BLAZE, ENDER_DRAGON, GHAST, WITHER and PARROT using * {@link PlayerInputAirController}. * * @param type * the entity type * @param clazz * the controller class */ public static void registerControllerType(EntityType type, Class clazz) { try { Constructor constructor = clazz.getConstructor(Controllable.class); constructor.setAccessible(true); CONTROLLER_TYPES.put(type, constructor); return; } catch (Exception e) { try { Constructor constructor = clazz.getConstructor(); constructor.setAccessible(true); CONTROLLER_TYPES.put(type, constructor); } catch (Exception e2) { throw new RuntimeException(e2); } } } private static Map> CONTROLLER_TYPES = Maps .newEnumMap(EntityType.class); static { for (EntityType type : EntityType.values()) { if (Util.isAlwaysFlyable(type)) { registerControllerType(type, PlayerInputAirController.class); } } registerControllerType(EntityType.UNKNOWN, LookAirController.class); } }