package tc.oc.pgm.controlpoint; import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.Objects; import javax.annotation.Nullable; import net.md_5.bungee.api.ChatColor; import org.bukkit.event.HandlerList; import org.bukkit.util.Vector; import tc.oc.api.docs.virtual.MatchDoc; import tc.oc.commons.core.util.Comparables; import tc.oc.commons.core.util.DefaultMapAdapter; import tc.oc.commons.core.util.TimeUtils; import tc.oc.pgm.controlpoint.events.CapturingTeamChangeEvent; import tc.oc.pgm.controlpoint.events.CapturingTimeChangeEvent; import tc.oc.pgm.controlpoint.events.ControllerChangeEvent; import tc.oc.pgm.goals.IncrementalGoal; import tc.oc.pgm.goals.SimpleGoal; import tc.oc.pgm.goals.events.GoalCompleteEvent; 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.Party; import tc.oc.pgm.regions.Region; import tc.oc.pgm.score.ScoreMatchModule; import tc.oc.pgm.teams.TeamMatchModule; import tc.oc.pgm.utils.Strings; public class ControlPoint extends SimpleGoal<ControlPointDefinition> implements IncrementalGoal<ControlPointDefinition> { public static final ChatColor COLOR_NEUTRAL_TEAM = ChatColor.WHITE; public static final String SYMBOL_CP_INCOMPLETE = "\u29be"; // ⦾ public static final String SYMBOL_CP_COMPLETE = "\u29bf"; // ⦿ protected final ControlPointPlayerTracker playerTracker; protected final ControlPointBlockDisplay blockDisplay; protected final Vector centerPoint; // This is set false after the first state change if definition.permanent == true protected boolean capturable = true; // The team that currently owns the point. The goal is completed for this team. // If this is null then the point is unowned, either because it is in the // neutral state, or because it has no initial owner and has not yet been captured. protected Competitor owner = null; // The team that will own the CP if the current capture is successful. // If this is null then either the point is not being captured or it is // being "uncaptured" toward the neutral state. protected Competitor capturer = null; // Time accumulated towards the next owner change. When this passes timeToCapture, // it is reset to zero and the owner changes to the capturer (which may be null, // if changing to the neutral state). When this is zero, the capturer is null. protected Duration progress = Duration.ZERO; public ControlPoint(Match match, ControlPointDefinition definition) { super(definition, match); if(this.definition.getInitialOwner() != null) { this.owner = match.needMatchModule(TeamMatchModule.class).team(this.definition.getInitialOwner()); } this.centerPoint = this.getCaptureRegion().getBounds().center(); this.playerTracker = new ControlPointPlayerTracker(match, this.getCaptureRegion()); this.blockDisplay = new ControlPointBlockDisplay(match, this); } public void registerEvents() { this.match.registerEvents(this.playerTracker); this.match.registerEvents(this.blockDisplay); this.blockDisplay.render(); } public void unregisterEvents() { HandlerList.unregisterAll(this.blockDisplay); HandlerList.unregisterAll(this.playerTracker); } public ControlPointBlockDisplay getBlockDisplay() { return blockDisplay; } public ControlPointPlayerTracker getPlayerTracker() { return playerTracker; } public Region getCaptureRegion() { return definition.getCaptureRegion(); } public Duration getTimeToCapture() { return definition.getTimeToCapture(); } /** * Point that can be used as the location of the ControlPoint */ public Vector getCenterPoint() { return centerPoint.clone(); } /** * The team that owns (is receiving points from) this ControlPoint, * or null if the ControlPoint is unowned. */ public Competitor getOwner() { return this.owner; } /** * The team that is "capturing" the ControlPoint. This is the team * that the current capturingTime counts towards. The capturingTime * goes up whenever this team has the most players on the point, * and goes down when any other team has the most players on the point. * If capturingTime reaches timeToCapture, this team will take * ownership of the point, if they don't own it already. When capturingTime * goes below zero, the capturingTeam changes to the team with the most * players on the point, and the point becomes unowned. */ public Competitor getCapturer() { return this.capturer; } /** * The partial owner of the ControlPoint. The "partial owner" is defined in * three scenarios. If the ControlPoint is owned and has a neutral state, the * partial owner is the owner of the ControlPoint. If the ControlPoint is in * contest, the partial owner is the team that is currently capturing the * ControlPoint. Lastly, if the ControlPoint is un-owned and not in contest, * the progressingTeam is null. * * @return The team that should be displayed as having partial ownership of * the point, if any. */ public Competitor getPartialOwner() { return this.definition.hasNeutralState() && this.getOwner() != null ? this.getOwner() : this.getCapturer(); } /** * Progress towards "capturing" the ControlPoint for the current capturingTeam */ public Duration getProgress() { return this.progress; } /** * Progress toward "capturing" the ControlPoint for the current capturingTeam, * as a real number from 0 to 1. */ @Override public double getCompletion() { return (double) this.progress.toMillis() / (double) this.definition.getTimeToCapture().toMillis(); } @Override public String renderCompletion() { return Strings.progressPercentage(this.getCompletion()); } @Override public @Nullable String renderPreciseCompletion() { return null; } @Override public ChatColor renderSidebarStatusColor(@Nullable Competitor competitor, Party viewer) { return this.capturer == null ? COLOR_NEUTRAL_TEAM : this.capturer.getColor(); } @Override public String renderSidebarStatusText(@Nullable Competitor competitor, Party viewer) { if(Duration.ZERO.equals(this.progress)) { return this.owner == null ? SYMBOL_CP_INCOMPLETE : SYMBOL_CP_COMPLETE; } else { return this.renderCompletion(); } } @Override public ChatColor renderSidebarLabelColor(@Nullable Competitor competitor, Party viewer) { return this.owner == null ? COLOR_NEUTRAL_TEAM : this.owner.getColor(); } /** * Ownership of the ControlPoint for a specific team given as a real number from * 0 to 1. */ public double getCompletion(Competitor team) { if (this.getOwner() == team) { return 1 - this.getCompletion(); } else if (this.getCapturer() == team) { return this.getCompletion(); } else { return 0; } } @Override public boolean getShowProgress() { return this.definition.getShowProgress(); } @Override public boolean canComplete(Competitor team) { return this.canCapture(team); } @Override public boolean isCompleted() { return this.owner != null; } @Override public boolean isCompleted(Competitor team) { return this.owner != null && this.owner == team; } private boolean canCapture(Competitor team) { return this.definition.getCaptureFilter() == null || this.definition.getCaptureFilter().query(team).isAllowed(); } private boolean canDominate(MatchPlayer player) { return this.definition.getPlayerFilter() == null || this.definition.getPlayerFilter().query(player).isAllowed(); } private Duration calculateDominateTime(int lead, Duration duration) { // Don't scale time if only one player is present, don't zero duration if multiplier is zero return TimeUtils.multiply(duration, 1 + (lead - 1) * definition.getTimeMultiplier()); } public void tick(Duration duration) { this.tickCapture(duration); this.tickScore(duration); } /** * Do a scoring cycle on this ControlPoint over the given duration. */ protected void tickScore(Duration duration) { if(this.getOwner() != null && this.getDefinition().affectsScore()) { ScoreMatchModule scoreMatchModule = this.getMatch().getMatchModule(ScoreMatchModule.class); if(scoreMatchModule != null) { float seconds = this.getMatch().getLength().getSeconds(); float initial = this.getDefinition().getPointsPerSecond(); float growth = this.getDefinition().getPointsGrowth(); float rate = (float) (initial * Math.pow(2, seconds / growth)); scoreMatchModule.incrementScore(this.getOwner(), rate * duration.toMillis() / 1000d); } } } /** * Do a capturing cycle on this ControlPoint over the given duration. */ protected void tickCapture(Duration duration) { Map<Competitor, Integer> playerCounts = new DefaultMapAdapter<>(new HashMap<>(), 0); // The teams with the most and second-most capturing players on the point, respectively Competitor leader = null, runnerUp = null; // The total number of players on the point who are allowed to dominate and not on the leading team int defenderCount = 0; for(MatchPlayer player : this.playerTracker.getPlayersOnPoint()) { Competitor team = player.getCompetitor(); if(this.canDominate(player)) { defenderCount++; int playerCount = playerCounts.get(team) + 1; playerCounts.put(team, playerCount); if(team != leader) { if(leader == null || playerCount > playerCounts.get(leader)) { runnerUp = leader; leader = team; } else if(team != runnerUp && (runnerUp == null || playerCount > playerCounts.get(runnerUp))) { runnerUp = team; } } } } int lead = 0; if(leader != null) { lead = playerCounts.get(leader); defenderCount -= lead; switch(this.definition.getCaptureCondition()) { case EXCLUSIVE: if(defenderCount > 0) { lead = 0; } break; case MAJORITY: lead = Math.max(0, lead - defenderCount); break; case LEAD: if(runnerUp != null) { lead -= playerCounts.get(runnerUp); } break; } } if(lead > 0) { this.dominateAndFireEvents(leader, calculateDominateTime(lead, duration)); } else { this.dominateAndFireEvents(null, duration); } } /** * Do a cycle of domination on this ControlPoint for the given team over the given duration. The team can be null, * which means no team is dominating the point, which can cause the state to change in some configurations. */ private void dominateAndFireEvents(@Nullable Competitor dominator, Duration duration) { final Duration oldProgress = progress; final Competitor oldCapturer = capturer; final Competitor oldOwner = owner; dominate(dominator, duration); if(!Objects.equals(oldCapturer, capturer) || !oldProgress.equals(progress)) { match.callEvent(new CapturingTimeChangeEvent(match, this)); match.callEvent(new GoalStatusChangeEvent(this)); } if(!Objects.equals(oldCapturer, capturer)) { match.callEvent(new CapturingTeamChangeEvent(match, this, oldCapturer, capturer)); } if(!Objects.equals(oldOwner, owner)) { match.callEvent(new ControllerChangeEvent(match, this, oldOwner, owner)); match.callEvent(new GoalCompleteEvent(this, owner != null, c -> c.equals(oldOwner), c -> c.equals(owner))); ScoreMatchModule smm = this.getMatch().getMatchModule(ScoreMatchModule.class); if (smm != null) { if (oldOwner != null) smm.incrementScore(oldOwner, getDefinition().getPointsOwned() * -1); if (owner != null) smm.incrementScore(owner, getDefinition().getPointsOwned()); } } } /** * If there is a neutral state, then the point cannot be owned and captured * at the same time. This means that at least one of controllingTeam or capturingTeam * must be null at any particular time. * * If controllingTeam is non-null, the point is owned, and it must be "uncaptured" * before any other team can capture it. In this state, capturingTeam is null, * the controlling team will decrease capturingTimeMillis, and all other teams will * increase it. * * If controllingTeam is null, then the point is in the neutral state. If capturingTeam * is also null, then the point is not being captured, and capturingTimeMillis is * zero. If capturingTeam is non-null, then that is the only team that will increase * capturingTimeMillis. All other teams will decrease it. * * If there is no neutral state, then the point is always either being captured * by a specific team, or not being captured at all. * * If incremental capturing is disabled, then capturingTimeMillis is reset to * zero whenever it stops increasing. */ private void dominate(@Nullable Competitor dominator, Duration duration) { if(!capturable || Comparables.lessOrEqual(duration, Duration.ZERO)) { return; } if(owner != null && definition.hasNeutralState()) { // Point is owned and has a neutral state if(Objects.equals(dominator, owner)) { // Owner is recovering the point recover(duration, dominator); } else if(dominator != null) { // Non-owner is uncapturing the point uncapture(duration, dominator); } else { // Point is decaying towards the owner decay(duration); } } else if(capturer != null) { // Point is partly captured by someone if(Objects.equals(dominator, capturer)) { // Capturer is making progress capture(duration); } else if(dominator != null) { // Non-capturer is reversing progress recover(duration, dominator); } else { // Point is decaying towards owner or neutral decay(duration); } } else if(dominator != null && !Objects.equals(dominator, owner) && canCapture(dominator)) { // Point is not being captured and there is a dominant team that is not the owner, so they start capturing capturer = dominator; dominate(dominator, duration); } } private @Nullable Duration addCaptureTime(final Duration duration) { progress = progress.plus(duration); if(Comparables.lessThan(progress, definition.getTimeToCapture())) { return null; } else { final Duration remainder = progress.minus(definition.getTimeToCapture()); progress = Duration.ZERO; return remainder; } } private @Nullable Duration subtractCaptureTime(final Duration duration) { if(Comparables.greaterThan(progress, duration)) { progress = progress.minus(duration); return null; } else { final Duration remainder = duration.minus(progress); progress = Duration.ZERO; return remainder; } } /** * Point is owned, and a non-owner is pushing it towards neutral */ private void uncapture(Duration duration, Competitor dominator) { duration = addCaptureTime(duration); if(duration != null) { // If uncapture is complete, recurse with the dominant team's remaining time owner = null; dominate(dominator, duration); } } /** * Point is either owned or neutral, and someone is pushing it towards themselves */ private void capture(Duration duration) { duration = addCaptureTime(duration); if(duration != null) { owner = capturer; capturer = null; if(definition.isPermanent()) { // The objective is permanent, so the first capture disables it capturable = false; } } } /** * Point is being pulled back towards its current state */ private void recover(Duration duration, Competitor dominator) { duration = TimeUtils.multiply(duration, definition.recoveryRate()); duration = subtractCaptureTime(duration); if(duration != null) { capturer = null; if(!Objects.equals(dominator, owner)) { // If the dominant team is not the controller, recurse with the remaining time dominate(dominator, TimeUtils.multiply(duration, 1D / definition.recoveryRate())); } } } /** * Point is decaying back towards its current state */ private void decay(Duration duration) { duration = TimeUtils.multiply(duration, definition.decayRate()); duration = subtractCaptureTime(duration); if(duration != null) { capturer = null; } } @Override public MatchDoc.IncrementalGoal getDocument() { return new Document(); } class Document extends SimpleGoal.Document implements MatchDoc.IncrementalGoal { @Override public double completion() { return getCompletion(); } } }