ProjectAres/PGM/src/main/java/tc/oc/pgm/flag/Flag.java

556 lines
20 KiB
Java

package tc.oc.pgm.flag;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import com.google.common.collect.ImmutableSet;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.DyeColor;
import org.bukkit.FireworkEffect;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.block.Banner;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
import org.bukkit.entity.Firework;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BannerMeta;
import org.bukkit.util.BlockVector;
import tc.oc.commons.bukkit.chat.BukkitSound;
import tc.oc.commons.bukkit.chat.NameStyle;
import tc.oc.commons.bukkit.event.CoarsePlayerMoveEvent;
import tc.oc.commons.bukkit.item.BooleanItemTag;
import tc.oc.commons.bukkit.util.BukkitUtils;
import tc.oc.commons.bukkit.util.Materials;
import tc.oc.commons.bukkit.util.NMSHacks;
import tc.oc.commons.bukkit.util.materials.Banners;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.util.Lazy;
import tc.oc.commons.core.util.Optionals;
import tc.oc.pgm.events.BlockTransformEvent;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.PlayerLeavePartyEvent;
import tc.oc.pgm.filters.query.ILocationQuery;
import tc.oc.pgm.filters.query.IQuery;
import tc.oc.pgm.fireworks.FireworkUtil;
import tc.oc.pgm.flag.event.FlagCaptureEvent;
import tc.oc.pgm.flag.event.FlagStateChangeEvent;
import tc.oc.pgm.flag.state.BaseState;
import tc.oc.pgm.flag.state.Captured;
import tc.oc.pgm.flag.state.Completed;
import tc.oc.pgm.flag.state.Returned;
import tc.oc.pgm.flag.state.Spawned;
import tc.oc.pgm.flag.state.State;
import tc.oc.pgm.goals.TouchableGoal;
import tc.oc.pgm.goals.events.GoalCompleteEvent;
import tc.oc.pgm.goals.events.GoalEvent;
import tc.oc.pgm.goals.events.GoalStatusChangeEvent;
import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.match.ParticipantState;
import tc.oc.pgm.match.Party;
import tc.oc.pgm.module.ModuleLoadException;
import tc.oc.pgm.points.AngleProvider;
import tc.oc.pgm.points.PointProvider;
import tc.oc.pgm.points.StaticAngleProvider;
import tc.oc.pgm.regions.PointRegion;
import tc.oc.pgm.regions.Region;
import tc.oc.pgm.spawns.events.ParticipantDespawnEvent;
import tc.oc.pgm.teams.Team;
import tc.oc.pgm.teams.TeamMatchModule;
@ListenerScope(MatchScope.LOADED)
public class Flag extends TouchableGoal<FlagDefinition> implements Listener {
public static final String RESPAWNING_SYMBOL = "\u2690"; // ⚐
public static final String RETURNED_SYMBOL = "\u2691"; // ⚑
public static final String DROPPED_SYMBOL = "\u2691"; // ⚑
public static final String CARRIED_SYMBOL = "\u2794"; // ➔
public static final BukkitSound PICKUP_SOUND_OWN = new BukkitSound(Sound.ENTITY_WITHER_AMBIENT, 0.7f, 1.2f);
public static final BukkitSound DROP_SOUND_OWN = new BukkitSound(Sound.ENTITY_WITHER_HURT, 0.7f, 1);
public static final BukkitSound RETURN_SOUND_OWN = new BukkitSound(Sound.ENTITY_ZOMBIE_VILLAGER_CONVERTED, 1.1f, 1.2f);
public static final BukkitSound PICKUP_SOUND = new BukkitSound(Sound.ENTITY_FIREWORK_LARGE_BLAST_FAR, 1f, 0.7f);
public static final BukkitSound DROP_SOUND = new BukkitSound(Sound.ENTITY_FIREWORK_TWINKLE_FAR, 1f, 1f);
public static final BukkitSound RETURN_SOUND = new BukkitSound(Sound.ENTITY_FIREWORK_TWINKLE_FAR, 1f, 1f);
private static final BooleanItemTag FLAG_ITEM = new BooleanItemTag("flag", false);
private final ImmutableSet<Net> nets;
private final @Nullable Team owner;
private final Lazy<Set<Team>> capturers;
private final Lazy<Set<Team>> controllers;
private final Lazy<Set<Team>> completers;
private BaseState state;
private boolean transitioning;
private final BannerInfo bannerInfo;
private static class BannerInfo {
final Location location;
final BannerMeta meta;
final ItemStack item;
final AngleProvider yawProvider;
private BannerInfo(Location location, BannerMeta meta, ItemStack item, AngleProvider yawProvider) {
this.location = location;
this.meta = meta;
FLAG_ITEM.set(this.meta, true);
this.item = item;
this.yawProvider = yawProvider;
}
}
protected Flag(Match match, FlagDefinition definition, ImmutableSet<Net> nets) throws ModuleLoadException {
super(definition, match);
this.nets = nets;
final TeamMatchModule tmm = match.getMatchModule(TeamMatchModule.class);
this.owner = definition.owner()
.map(def -> tmm.team(def)) // Do not use a method ref here, it will NPE if tmm is null
.orElse(null);
this.capturers = Lazy.from(
() -> Optionals.stream(match.module(TeamMatchModule.class))
.flatMap(TeamMatchModule::teams)
.filter(team -> getDefinition().canPickup(team) && canCapture(team))
.collect(Collectors.toSet())
);
this.controllers = Lazy.from(
() -> nets.stream()
.flatMap(net -> Optionals.stream(net.returnPost()
.flatMap(Post::owner)))
.map(def -> tmm.team(def))
.collect(Collectors.toSet())
);
this.completers = Lazy.from(
() -> nets.stream()
.flatMap(net -> Optionals.stream(net.returnPost()))
.filter(Post::isPermanent)
.flatMap(post -> Optionals.stream(post.owner()))
.map(def -> tmm.team(def))
.collect(Collectors.toSet())
);
Banner banner = null;
pointLoop: for(PointProvider returnPoint : definition.getDefaultPost().getReturnPoints()) {
Region region = returnPoint.getRegion();
if(region instanceof PointRegion) {
// Do not require PointRegions to be at the exact center of the block.
// It might make sense to just override PointRegion.getBlockVectors() to
// always do this, but it does technically violate the contract of that method.
banner = toBanner(((PointRegion) region).getPosition().toLocation(match.getWorld()).getBlock());
if(banner != null) break pointLoop;
} else {
for(BlockVector pos : returnPoint.getRegion().getBlockVectors()) {
banner = toBanner(pos.toLocation(match.getWorld()).getBlock());
if(banner != null) break pointLoop;
}
}
}
if(banner == null) {
throw new ModuleLoadException("Flag '" + getName() + "' must have a banner at its default post");
}
final Location location = Banners.getLocationWithYaw(banner);
bannerInfo = new BannerInfo(location,
Banners.getItemMeta(banner),
new ItemStack(Material.BANNER),
new StaticAngleProvider(location.getYaw()));
bannerInfo.item.setItemMeta(bannerInfo.meta);
match.registerEvents(this);
this.state = new Returned(this, this.getDefinition().getDefaultPost(), bannerInfo.location);
this.state.enterState();
}
private static Banner toBanner(Block block) {
if(block == null) return null;
BlockState state = block.getState();
return state instanceof Banner ? (Banner) state : null;
}
@Override
public String toString() {
return "Flag{name=" + this.getName() + " state=" + this.state + "}";
}
public DyeColor getDyeColor() {
DyeColor color = this.getDefinition().getColor();
if(color == null) color = bannerInfo.meta.getBaseColor();
return color;
}
public net.md_5.bungee.api.ChatColor getChatColor() {
return BukkitUtils.toChatColor(this.getDyeColor());
}
public String getColoredName() {
return this.getChatColor() + this.getName();
}
public Component getComponentName() {
return new Component(getName()).color(getChatColor());
}
public ImmutableSet<Net> getNets() {
return nets;
}
public BannerMeta getBannerMeta() {
return bannerInfo.meta;
}
public ItemStack getBannerItem() {
return bannerInfo.item;
}
public State state() {
return state;
}
/**
* Owner is defined in XML, and does not change during a match
*/
public @Nullable Team getOwner() {
return owner;
}
/**
* Physical location of the flag, if any
*/
public Optional<Location> getLocation() {
return state instanceof Spawned ? Optional.of(((Spawned) state).getLocation())
: Optional.empty();
}
/**
* Controller is the owner of the {@link Post} the flag is at, which obviously can change
*/
public @Nullable Team getController() {
return this.state.getController();
}
public boolean hasMultipleControllers() {
return !controllers.get().isEmpty();
}
public boolean canDropOn(BlockState base) {
return Materials.isColliding(base.getType()) || (getDefinition().canDropOnWater() && Materials.isWater(base.getType()));
}
public boolean canDropAt(Location location) {
if(!match.getWorld().equals(location.getWorld())) return false;
Block block = location.getBlock();
Block below = block.getRelative(BlockFace.DOWN);
if(!canDropOn(below.getState())) return false;
if(block.getRelative(BlockFace.UP).getType() != Material.AIR) return false;
switch(block.getType()) {
case AIR:
case LONG_GRASS:
return true;
default:
return false;
}
}
public boolean canDrop(ILocationQuery query) {
return canDropAt(query.getLocation()) &&
getDefinition().getDropFilter().query(query).isAllowed();
}
public Location getReturnPoint(Post post) {
return post.getReturnPoint(this, bannerInfo.yawProvider).clone();
}
// Touchable
@Override
public boolean canTouch(ParticipantState player) {
MatchPlayer matchPlayer = player.getMatchPlayer();
return matchPlayer != null && canPickup(matchPlayer, state.getPost());
}
@Override
public boolean showEnemyTouches() {
return true;
}
@Override
public BaseComponent getTouchMessage(ParticipantState toucher, boolean self) {
if(self) {
return new TranslatableComponent("match.flag.pickup.you", getComponentName());
} else {
return new TranslatableComponent("match.flag.pickup", getComponentName(), toucher.getStyledName(NameStyle.COLOR));
}
}
// Proximity
@Override
public Iterable<Location> getProximityLocations(ParticipantState player) {
return state.getProximityLocations(player);
}
@Override
public boolean isProximityRelevant(Competitor team) {
if(hasTouched(team)) {
return canCapture(team);
} else {
return canPickup(team);
}
}
// Misc
/**
* Transition to the given state. This happens immediately if not already transitioning.
* If this is called from within a transition, the state is queued and the transition
* happens after the current one completes. This allows {@link BaseState#enterState} to
* immediately transition into another state without nesting the transitions, and keeps
* the events in the correct order.
*/
public void transition(BaseState newState) {
if(this.transitioning) {
throw new IllegalStateException("Nested flag state transition");
}
BaseState oldState = this.state;
try {
logger.fine("Transitioning " + getName() + " from " + oldState + " to " + newState);
this.transitioning = true;
this.state.leaveState();
this.state = newState;
this.state.enterState();
} finally {
this.transitioning = false;
}
getMatch().callEvent(new FlagStateChangeEvent(this, oldState, this.state));
// If we are still in the state we just transitioned into, start the countdown, if any.
// We check this because the FlagStateChangeEvent may have already transitioned into another state.
if(this.state == newState) {
this.state.startCountdown();
}
// Check again, in case startCountdown transitioned. In that case, the nested
// transition will have already called these events if necessary.
if(this.state == newState) {
getMatch().callEvent(new GoalStatusChangeEvent(this));
if(isCompleted()) {
getMatch().callEvent(new GoalCompleteEvent(this,
true,
c -> false,
c -> c.equals(getController())));
}
}
}
public boolean canPickup(IQuery query, Post post) {
return getDefinition().getPickupFilter().query(query).isAllowed() &&
post.getPickupFilter().query(query).isAllowed();
}
public boolean canPickup(IQuery query) {
return canPickup(query, state.getPost());
}
public boolean canCapture(IQuery query, Net net) {
return getDefinition().getCaptureFilter().query(query).isAllowed() &&
net.getCaptureFilter().query(query).isAllowed();
}
public boolean canCapture(IQuery query) {
return getDefinition().canCapture(query, getNets());
}
public boolean isCurrent(Class<? extends State> state) {
return state.isInstance(this.state);
}
public boolean isCurrent(State state) {
return this.state == state;
}
public boolean isCarrying(ParticipantState player) {
MatchPlayer matchPlayer = player.getMatchPlayer();
return matchPlayer != null && isCarrying(matchPlayer);
}
public boolean isCarrying(MatchPlayer player) {
return this.state.isCarrying(player);
}
public boolean isCarrying(Competitor party) {
return this.state.isCarrying(party);
}
public boolean isAtPost(Post post) {
return this.state.isAtPost(post);
}
public boolean isCompletable() {
return !completers.get().isEmpty();
}
@Override
public boolean canComplete(Competitor team) {
return team instanceof Team && capturers.get().contains(team);
}
@Override
public boolean isShared() {
// Flag is shared if it has multiple capturers or no capturers
return capturers.get().size() != 1;
}
@Override
public boolean isCompleted() {
return isCurrent(Completed.class);
}
@Override
public boolean isCompleted(Competitor team) {
return isCompleted() && getController() == team;
}
public boolean isCaptured() {
return isCompleted() || isCurrent(Captured.class);
}
@Override
public String renderSidebarStatusText(@Nullable Competitor competitor, Party viewer) {
return this.state.getStatusText(viewer);
}
@Override
public ChatColor renderSidebarStatusColor(@Nullable Competitor competitor, Party viewer) {
return this.state.getStatusColor(viewer);
}
@Override
public ChatColor renderSidebarLabelColor(@Nullable Competitor competitor, Party viewer) {
return this.state.getLabelColor(viewer);
}
public void playFlareEffect() {
if(isCurrent(Spawned.class)) {
Location location = ((Spawned) this.state).getLocation();
if(location == null) return;
FireworkEffect effect = FireworkEffect.builder().with(FireworkEffect.Type.BURST).withColor(this.getDyeColor().getColor()).build();
Firework firework = FireworkUtil.spawnFirework(location, effect, 0);
NMSHacks.skipFireworksLaunch(firework);
}
}
/**
* Play one of two status sounds depending on the team of the listener.
* Owning players hear the first sound, other players hear the second.
*/
public void playStatusSound(BukkitSound ownerSound, BukkitSound otherSound) {
for(MatchPlayer listener : getMatch().getPlayers()) {
if(listener.getParty() != null && (listener.getParty() == this.getOwner() || listener.getParty() == this.getController())) {
listener.playSound(ownerSound);
} else {
listener.playSound(otherSound);
}
}
}
private boolean isFlagItem(ItemStack item) {
return FLAG_ITEM.get(item);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
private void onPlayerDeath(PlayerDeathEvent event) {
event.getDrops().removeIf(this::isFlagItem);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
private void onGoalChange(GoalEvent event) {
this.state.onEvent(event);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
private void onFlagStateChange(FlagStateChangeEvent event) {
this.state.onEvent(event);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
private void onFlagCapture(FlagCaptureEvent event) {
this.state.onEvent(event);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
private void onPlayerMove(PlayerMoveEvent event) {
if(event.getFrom().getWorld() == event.getTo().getWorld()) { // yes, this can be false
this.state.onEvent(event);
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
private void onPlayerMove(CoarsePlayerMoveEvent event) {
this.state.onEvent(event);
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
private void onBlockTransform(BlockTransformEvent event) {
this.state.onEvent(event);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
private void onItemDrop(PlayerDropItemEvent event) {
this.state.onEvent(event);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
private void onPlayerDespawn(ParticipantDespawnEvent event) {
this.state.onEvent(event);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
private void onPlayerDespawn(PlayerLeavePartyEvent event) {
this.state.onEvent(event);
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
private void onInventoryClick(InventoryClickEvent event) {
this.state.onEvent(event);
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
private void onProjectileHit(EntityDamageEvent event) {
this.state.onEvent(event);
}
}