Refactor control point logic into a controllable goal

This commit is contained in:
Electroid 2017-12-30 04:00:33 -07:00
parent 068489d9b0
commit 9f73d2a620
23 changed files with 879 additions and 982 deletions

View File

@ -4,7 +4,7 @@ import tc.oc.commons.core.inject.HybridManifest;
import tc.oc.pgm.animation.AnimationManifest;
import tc.oc.pgm.broadcast.BroadcastManifest;
import tc.oc.pgm.classes.ClassManifest;
import tc.oc.pgm.controlpoint.ControlPointManifest;
import tc.oc.pgm.control.ControllableGoalManifest;
import tc.oc.pgm.core.CoreManifest;
import tc.oc.pgm.damage.DamageManifest;
import tc.oc.pgm.destroyable.DestroyableManifest;
@ -65,7 +65,7 @@ public class PGMModulesManifest extends HybridManifest {
install(new CoreManifest());
install(new DestroyableManifest());
install(new WoolManifest());
install(new ControlPointManifest());
install(new ControllableGoalManifest());
install(new PayloadManifest());
install(new LaneManifest());
install(new BroadcastManifest());

View File

@ -1,21 +1,24 @@
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;
package tc.oc.pgm.control;
import com.google.common.collect.ImmutableSet;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.event.HandlerList;
import org.bukkit.util.Vector;
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.PlayerTeleportEvent;
import tc.oc.api.docs.virtual.MatchDoc;
import tc.oc.commons.bukkit.event.CoarsePlayerMoveEvent;
import tc.oc.commons.core.collection.WeakHashSet;
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.control.events.ControllableOwnerChangeEvent;
import tc.oc.pgm.control.events.ControllableTeamChangeEvent;
import tc.oc.pgm.control.events.ControllableTimeChangeEvent;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.filters.Filter;
import tc.oc.pgm.goals.IncrementalGoal;
import tc.oc.pgm.goals.SimpleGoal;
import tc.oc.pgm.goals.events.GoalCompleteEvent;
@ -23,147 +26,133 @@ 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.Party;
import tc.oc.pgm.regions.Region;
import tc.oc.pgm.score.ScoreMatchModule;
import tc.oc.pgm.spawns.events.ParticipantDespawnEvent;
import tc.oc.pgm.teams.TeamMatchModule;
import tc.oc.pgm.utils.MatchPlayers;
import tc.oc.pgm.utils.Strings;
public class ControlPoint extends SimpleGoal<ControlPointDefinition> implements IncrementalGoal<ControlPointDefinition> {
import javax.annotation.Nullable;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public static final ChatColor COLOR_NEUTRAL_TEAM = ChatColor.WHITE;
@ListenerScope(MatchScope.LOADED)
public abstract class ControllableGoal<T extends ControllableGoalDefinition> extends SimpleGoal<T> implements IncrementalGoal<T>, Listener {
public static final String SYMBOL_CP_INCOMPLETE = "\u29be"; //
public static final String SYMBOL_CP_COMPLETE = "\u29bf"; // ⦿
/**
* Set to false if the goal is defined as permanent.
*/
protected boolean capturable;
protected final ControlPointPlayerTracker playerTracker;
protected final ControlPointBlockDisplay blockDisplay;
/**
* The team that currently owns the goal. The goal is completed for this team.
* If this is null then the goal 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;
protected final Vector centerPoint;
/**
* The team that will own the goal if the current capture is successful.
* If this is null then either the goal is not being captured or it is
* being "uncaptured" toward the neutral state.
*/
protected Competitor capturer;
// This is set false after the first state change if definition.permanent == true
protected boolean capturable = true;
/**
* Time accumulated towards the next owner change. When this passes the capture time,
* 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;
// 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;
/**
* Keeps track of players that are nearby the controllable goal.
*/
protected Set<MatchPlayer> players;
// 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;
/**
* String symbols to render the completion of the goal in the sidebar.
* Incomplete symbol is at index 0, complete symbol is at index 1.
*/
protected String[] symbols;
// 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) {
public ControllableGoal(T definition, Match match, String... symbols) {
super(definition, match);
if(this.definition.getInitialOwner() != null) {
this.owner = match.needMatchModule(TeamMatchModule.class).team(this.definition.getInitialOwner());
this.capturable = true;
this.owner = definition.initialOwner().map(owner -> match.needMatchModule(TeamMatchModule.class).team(owner)).orElse(null);
this.capturer = null;
this.progress = Duration.ZERO;
this.players = new WeakHashSet<>();
this.symbols = new String[] {"\u29be", "\u29bf"};
for(int i = 0; symbols.length == 2 && i < 2; i++) {
if(symbols[i] != null) {
this.symbols[i] = symbols[i];
}
}
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
* The team that owns (is receiving points from) this goal,
* or null if the goal is unowned.
*/
public Vector getCenterPoint() {
return centerPoint.clone();
public Competitor owner() {
return owner;
}
/**
* The team that owns (is receiving points from) this ControlPoint,
* or null if the ControlPoint is unowned.
* The team that is "capturing" the goal. This is the team
* that the current progress counts towards. The progress
* goes up whenever this team has the most players on the goal,
* and goes down when any other team has the most players on the goal.
* If progress reaches captureTime, this team will take ownership of the goal,
* if they don't own it already. When progress goes below zero, the capturing team
* changes to the team with the most players on the goal, and the goal becomes unowned.
*/
public Competitor getOwner() {
return this.owner;
public Competitor capturer() {
return capturer;
}
/**
* 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.
* The partial owner of the goal is defined in three scenarios:
* 1. If the goal is owned and has a neutral state, the partial owner is the owner of the goal.
* 2. If the goal is in contest, the partial owner is the team that is currently capturing the goal.
* 3. If the goal is un-owned and not in contest, the progressingTeam is null.
*/
public Competitor getCapturer() {
return this.capturer;
public Competitor partialOwner() {
return definition.neutralState() && owner != null ? owner() : 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
* Progress towards "capturing" the goal for the current capturing team.
*/
public Duration getProgress() {
return this.progress;
return 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();
return (double) progress.toMillis() / (double) definition.captureTime().toMillis();
}
public double getCompletion(Competitor team) {
if(Objects.equals(owner, team)) {
return 1 - getCompletion();
} else if(Objects.equals(capturer, team)) {
return getCompletion();
} else {
return 0;
}
}
@Override
public String renderCompletion() {
return Strings.progressPercentage(this.getCompletion());
return Strings.progressPercentage(getCompletion());
}
@Override
@ -173,112 +162,84 @@ public class ControlPoint extends SimpleGoal<ControlPointDefinition> implements
@Override
public ChatColor renderSidebarStatusColor(@Nullable Competitor competitor, Party viewer) {
return this.capturer == null ? COLOR_NEUTRAL_TEAM : this.capturer.getColor();
return capturer != null ? capturer.getColor() : ChatColor.WHITE;
}
@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;
if(Duration.ZERO.equals(progress)) {
return symbols[owner != null ? 1 : 0];
} else {
return this.renderCompletion();
return 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;
}
return owner != null ? owner.getColor() : ChatColor.WHITE;
}
@Override
public boolean getShowProgress() {
return this.definition.getShowProgress();
return definition.showProgress();
}
@Override
public boolean canComplete(Competitor team) {
return this.canCapture(team);
return canCapture(team);
}
@Override
public boolean isCompleted() {
return this.owner != null;
return owner != null;
}
@Override
public boolean isCompleted(Competitor team) {
return this.owner != null && this.owner == team;
return isCompleted() && owner.equals(team);
}
private boolean canCapture(Competitor team) {
return this.definition.getCaptureFilter() == null ||
this.definition.getCaptureFilter().query(team).isAllowed();
Filter filter = definition.captureFilter();
return filter == null || filter.query(team).isAllowed();
}
private boolean canDominate(MatchPlayer player) {
return this.definition.getPlayerFilter() == null ||
this.definition.getPlayerFilter().query(player).isAllowed();
Filter filter = definition.defendFilter();
return filter == null || filter.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());
return TimeUtils.multiply(duration, 1 + (lead - 1) * definition.multiplierTime());
}
public void tick(Duration duration) {
this.tickCapture(duration);
this.tickScore(duration);
tickCapture(duration);
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();
if(definition.affectsScore() && isCompleted()) {
match.module(ScoreMatchModule.class).ifPresent(scores -> {
float seconds = match.getLength().getSeconds();
float initial = definition.pointsPerSecond();
float growth = definition.pointsGrowth();
float rate = (float) (initial * Math.pow(2, seconds / growth));
scoreMatchModule.incrementScore(this.getOwner(), rate * duration.toMillis() / 1000d);
}
scores.incrementScore(owner, 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()) {
for(MatchPlayer player : ImmutableSet.copyOf(players)) {
Competitor team = player.getCompetitor();
if(this.canDominate(player)) {
if(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;
@ -289,23 +250,19 @@ public class ControlPoint extends SimpleGoal<ControlPointDefinition> implements
}
}
}
int lead = 0;
if(leader != null) {
lead = playerCounts.get(leader);
defenderCount -= lead;
switch(this.definition.getCaptureCondition()) {
switch(definition.captureCondition()) {
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);
@ -313,44 +270,42 @@ public class ControlPoint extends SimpleGoal<ControlPointDefinition> implements
break;
}
}
if(lead > 0) {
this.dominateAndFireEvents(leader, calculateDominateTime(lead, duration));
dominateAndFireEvents(leader, calculateDominateTime(lead, duration));
} else {
this.dominateAndFireEvents(null, duration);
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.
* Cycle of domination on this goal for the given team over the duration.
* The team can be null, which means no team is dominating the goal,
* 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;
final Competitor oldCapturer = capturer, oldOwner = owner;
dominate(dominator, duration);
if(!Objects.equals(oldCapturer, capturer) || !oldProgress.equals(progress)) {
match.callEvent(new CapturingTimeChangeEvent(match, this));
displayProgress(owner, capturer, getCompletion());
match.callEvent(new ControllableTimeChangeEvent(match, this));
match.callEvent(new GoalStatusChangeEvent(this));
}
if(!Objects.equals(oldCapturer, capturer)) {
match.callEvent(new CapturingTeamChangeEvent(match, this, oldCapturer, capturer));
match.callEvent(new ControllableTeamChangeEvent(match, this, oldCapturer, capturer));
}
if(!Objects.equals(oldOwner, owner)) {
match.callEvent(new ControllerChangeEvent(match, this, oldOwner, owner));
displaySet(owner);
match.callEvent(new ControllableOwnerChangeEvent(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());
}
match.module(ScoreMatchModule.class).ifPresent(scores -> {
if(oldOwner != null) {
scores.incrementScore(owner, definition.pointsOwned() * -1);
}
if(owner != null) {
scores.incrementScore(owner, definition.pointsOwned());
}
});
}
}
@ -379,10 +334,9 @@ public class ControlPoint extends SimpleGoal<ControlPointDefinition> implements
if(!capturable || Comparables.lessOrEqual(duration, Duration.ZERO)) {
return;
}
if(owner != null && definition.hasNeutralState()) {
if(owner != null && definition.neutralState()) {
// Point is owned and has a neutral state
if(Objects.equals(dominator, owner)) {
if(Objects.equals(owner, dominator)) {
// Owner is recovering the point
recover(duration, dominator);
} else if(dominator != null) {
@ -394,7 +348,7 @@ public class ControlPoint extends SimpleGoal<ControlPointDefinition> implements
}
} else if(capturer != null) {
// Point is partly captured by someone
if(Objects.equals(dominator, capturer)) {
if(Objects.equals(capturer, dominator)) {
// Capturer is making progress
capture(duration);
} else if(dominator != null) {
@ -404,37 +358,15 @@ public class ControlPoint extends SimpleGoal<ControlPointDefinition> implements
// Point is decaying towards owner or neutral
decay(duration);
}
} else if(dominator != null && !Objects.equals(dominator, owner) && canCapture(dominator)) {
} else if(dominator != null && !Objects.equals(owner, dominator) && 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
* The goal is owned, and a non-owner is pushing it towards neutral.
*/
private void uncapture(Duration duration, Competitor dominator) {
duration = addCaptureTime(duration);
@ -446,14 +378,14 @@ public class ControlPoint extends SimpleGoal<ControlPointDefinition> implements
}
/**
* Point is either owned or neutral, and someone is pushing it towards themselves
* The goal 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()) {
if(definition.permanent()) {
// The objective is permanent, so the first capture disables it
capturable = false;
}
@ -461,7 +393,7 @@ public class ControlPoint extends SimpleGoal<ControlPointDefinition> implements
}
/**
* Point is being pulled back towards its current state
* The goal is being pulled back towards its current state.
*/
private void recover(Duration duration, Competitor dominator) {
duration = TimeUtils.multiply(duration, definition.recoveryRate());
@ -476,7 +408,7 @@ public class ControlPoint extends SimpleGoal<ControlPointDefinition> implements
}
/**
* Point is decaying back towards its current state
* The goal is decaying back towards its current state.
*/
private void decay(Duration duration) {
duration = TimeUtils.multiply(duration, definition.decayRate());
@ -486,6 +418,90 @@ public class ControlPoint extends SimpleGoal<ControlPointDefinition> implements
}
}
/**
* Increase the base amount of capture time by a certain amount.
*/
private @Nullable Duration addCaptureTime(final Duration duration) {
progress = progress.plus(duration);
if(Comparables.lessThan(progress, definition.captureTime())) {
return null;
} else {
final Duration remainder = progress.minus(definition.captureTime());
progress = Duration.ZERO;
return remainder;
}
}
/**
* Decrease the base amount of capture time by a certain amount.
*/
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;
}
}
/**
* Get an approximate region of where the capture/control region of the goal is.
*/
public abstract Region region();
/**
* Reset and show progress of the controllable goal in the physical world for the first time.
*/
public void display() {
displaySet(owner);
displayProgress(owner, capturer, getCompletion());
}
/**
* Display progress of the controllable goal in the physical world.
*/
protected abstract void displayProgress(Competitor controlling, Competitor capturing, double progress);
/**
* Reset progress of the controllable goal in the physical world to a new owning team.
*/
protected abstract void displaySet(Competitor owner);
/**
* Update the tracked players that are on the controllable goal
*/
protected void updatePlayers(Player bukkit, Location to) {
MatchPlayer player = match.getPlayer(bukkit);
if(!MatchPlayers.canInteract(player)) return;
if(!player.getBukkit().isDead() && tracking(player, to)) {
players.add(player);
} else {
players.remove(player);
}
}
/**
* Should the controllable goal track the given player at the given location.
*/
protected abstract boolean tracking(MatchPlayer player, Location location);
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerMove(CoarsePlayerMoveEvent event) {
updatePlayers(event.getPlayer(), event.getTo());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerTeleport(PlayerTeleportEvent event) {
updatePlayers(event.getPlayer(), event.getTo());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerDespawn(ParticipantDespawnEvent event) {
players.remove(event.getPlayer());
}
@Override
public MatchDoc.IncrementalGoal getDocument() {
return new Document();
@ -497,4 +513,5 @@ public class ControlPoint extends SimpleGoal<ControlPointDefinition> implements
return getCompletion();
}
}
}

View File

@ -1,31 +1,35 @@
package tc.oc.pgm.controlpoint;
package tc.oc.pgm.control;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import tc.oc.commons.core.chat.Component;
import tc.oc.pgm.controlpoint.events.ControllerChangeEvent;
import tc.oc.pgm.control.events.ControllableOwnerChangeEvent;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchScope;
/**
* Announces changes of state and ownership of any controllable goal.
*/
@ListenerScope(MatchScope.LOADED)
public class ControlPointAnnouncer implements Listener {
public class ControllableGoalAnnouncer implements Listener {
private final Match match;
public ControlPointAnnouncer(Match match) {
public ControllableGoalAnnouncer(Match match) {
this.match = match;
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onOwnerChange(ControllerChangeEvent event) {
if(event.getControlPoint().isVisible()) {
final Component point = new Component(event.getControlPoint().getComponentName(), ChatColor.WHITE);
public void onOwnerChange(ControllableOwnerChangeEvent event) {
if(event.controllable().isVisible()) {
final Component point = new Component(event.controllable().getComponentName(), ChatColor.WHITE);
final Component message = new Component(ChatColor.GRAY);
final Competitor before = event.getOldController();
final Competitor after = event.getNewController();
final Competitor before = event.oldController();
final Competitor after = event.newController();
if(after == null) {
message.translate("objective.lose",
before.getComponentName(),
@ -43,4 +47,5 @@ public class ControlPointAnnouncer implements Listener {
match.sendMessage(message);
}
}
}

View File

@ -0,0 +1,97 @@
package tc.oc.pgm.control;
import tc.oc.pgm.features.GamemodeFeature;
import tc.oc.pgm.filters.Filter;
import tc.oc.pgm.goals.GoalDefinition;
import tc.oc.pgm.teams.TeamFactory;
import java.time.Duration;
import java.util.Optional;
public interface ControllableGoalDefinition extends GoalDefinition, GamemodeFeature {
/**
* Which players are allowed to control the goal.
*/
Filter captureFilter();
/**
* Which players are allowed to defend (revert progress) on the goal.
*/
Filter defendFilter();
/**
* Base time for the goal to transition between states.
*/
Duration captureTime();
/**
* Multiplier applied to capture time based on the number of players dominating the goal.
*/
double multiplierTime();
/**
* Relative rate at which progress reverts while nobody is dominating the goal.
*/
double decayRate();
/**
* Relative rate at which progress reverts from players dominating the goal.
*/
double recoveryRate();
/**
* The team that initially controls the goal before the match starts.
*/
Optional<TeamFactory> initialOwner();
/**
* Condition for when the goal transitions from one controller to another.
*/
CaptureCondition captureCondition();
enum CaptureCondition {
EXCLUSIVE, // Team owns all players on the point
MAJORITY, // Team owns more than half the players on the point
LEAD; // Team owns more players on the point than any other single team
}
/**
* Whether the goal transitions to neural control before a team control.
*/
boolean neutralState();
/**
* Whether goal transitions are immutable and cannot be reverted.
*/
boolean permanent();
/**
* Whether active ownership of the goal gives points to the controlling team.
*/
default boolean affectsScore() {
return pointsPerSecond() != 0;
}
/**
* Amount of points given to a team once they control the goal.
* Those points are revoked if they loose ownership.
*/
float pointsOwned();
/**
* Amount of points given to the controlling team every second.
*/
float pointsPerSecond();
/**
* After this many seconds pass in the match, the points per second will double exponentially.
*/
float pointsGrowth();
/**
* Whether progress of controlling the goal is published and visible.
*/
boolean showProgress();
}

View File

@ -0,0 +1,145 @@
package tc.oc.pgm.control;
import tc.oc.api.docs.virtual.MapDoc;
import tc.oc.pgm.filters.Filter;
import tc.oc.pgm.goals.GoalDefinitionImpl;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.teams.TeamFactory;
import javax.annotation.Nullable;
import java.time.Duration;
import java.util.Optional;
import java.util.stream.Stream;
public abstract class ControllableGoalDefinitionImpl extends GoalDefinitionImpl implements ControllableGoalDefinition {
private final Filter captureFilter;
private final Filter defendFilter;
private final Duration captureTime;
private final double multiplierTime;
private final double recoveryRate;
private final double decayRate;
private final Optional<TeamFactory> initialOwner;
private final CaptureCondition captureCondition;
private final boolean neutralState;
private final boolean permanent;
private final float pointsOwned;
private final float pointsPerSecond;
private final float pointsGrowth;
private final boolean showProgress;
public ControllableGoalDefinitionImpl(String name,
@Nullable Boolean required,
boolean visible,
Filter captureFilter,
Filter defendFilter,
Duration captureTime,
double multiplierTime,
double recoveryRate,
double decayRate,
Optional<TeamFactory> initialOwner,
CaptureCondition captureCondition,
boolean neutralState,
boolean permanent,
float pointsOwned,
float pointsPerSecond,
float pointsGrowth,
boolean showProgress) {
super(name, required, visible);
this.captureFilter = captureFilter;
this.defendFilter = defendFilter;
this.captureTime = captureTime;
this.multiplierTime = multiplierTime;
this.recoveryRate = recoveryRate;
this.decayRate = decayRate;
this.initialOwner = initialOwner;
this.captureCondition = captureCondition;
this.neutralState = neutralState;
this.permanent = permanent;
this.pointsOwned = pointsOwned;
this.pointsPerSecond = pointsPerSecond;
this.pointsGrowth = pointsGrowth;
this.showProgress = showProgress;
}
@Override
public Stream<MapDoc.Gamemode> gamemodes() {
return Stream.of(MapDoc.Gamemode.koth);
}
@Override
public boolean isShared() {
return true;
}
@Override
public Filter captureFilter() {
return captureFilter;
}
@Override
public Filter defendFilter() {
return defendFilter;
}
@Override
public Duration captureTime() {
return captureTime;
}
@Override
public double multiplierTime() {
return multiplierTime;
}
@Override
public double decayRate() {
return decayRate;
}
@Override
public double recoveryRate() {
return recoveryRate;
}
@Override
public Optional<TeamFactory> initialOwner() {
return initialOwner;
}
@Override
public CaptureCondition captureCondition() {
return captureCondition;
}
@Override
public boolean neutralState() {
return neutralState;
}
@Override
public boolean permanent() {
return permanent;
}
@Override
public float pointsOwned() {
return pointsOwned;
}
@Override
public float pointsPerSecond() {
return pointsPerSecond;
}
@Override
public float pointsGrowth() {
return pointsGrowth;
}
@Override
public boolean showProgress() {
return showProgress;
}
}

View File

@ -1,18 +1,22 @@
package tc.oc.pgm.controlpoint;
package tc.oc.pgm.control;
import tc.oc.commons.core.inject.HybridManifest;
import tc.oc.pgm.control.point.ControlPointDefinition;
import tc.oc.pgm.control.point.ControlPointParser;
import tc.oc.pgm.control.point.ControlPointRootNodeFinder;
import tc.oc.pgm.features.FeatureBinder;
import tc.oc.pgm.map.inject.MapBinders;
import tc.oc.pgm.map.inject.MapScoped;
import tc.oc.pgm.match.inject.MatchModuleFixtureManifest;
public class ControlPointManifest extends HybridManifest implements MapBinders {
public class ControllableGoalManifest extends HybridManifest implements MapBinders {
@Override
protected void configure() {
bind(ControlPointParser.class).in(MapScoped.class);
install(new MatchModuleFixtureManifest<ControllableGoalMatchModule>(){});
final FeatureBinder<ControlPointDefinition> cp = new FeatureBinder<>(binder(), ControlPointDefinition.class);
cp.bindDefinitionParser().to(ControlPointParser.class);
cp.installMatchModule(ControlPointMatchModule.class);
cp.installRootParser(new ControlPointRootNodeFinder());
}
}

View File

@ -0,0 +1,54 @@
package tc.oc.pgm.control;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.Repeatable;
import tc.oc.time.Time;
import javax.inject.Inject;
import java.time.Duration;
import java.util.List;
import static tc.oc.commons.core.stream.Collectors.toImmutableList;
public class ControllableGoalMatchModule extends MatchModule {
private static final long MILLIS = 100;
private static final Duration INTERVAL = Duration.ofMillis(100);
private final List<ControllableGoal> controllables;
private final ControllableGoalAnnouncer announcer;
@Inject private ControllableGoalMatchModule(Match match) {
this.announcer = new ControllableGoalAnnouncer(match);
this.controllables = match.featureDefinitions()
.all(ControllableGoalDefinition.class)
.map(cp -> (ControllableGoal) cp.getGoal(match))
.collect(toImmutableList());
}
@Override
public void load() {
match.registerEvents(announcer);
for(ControllableGoal controllable : controllables) {
match.registerEventsAndRepeatables(controllable);
controllable.display();
}
}
@Override
public void unload() {
for(ControllableGoal controllable : controllables) {
match.unregisterEventsAndRepeatables(controllable);
}
match.unregisterEvents(announcer);
}
@Repeatable(interval = @Time(milliseconds = MILLIS))
public void tick() {
for(ControllableGoal controllable : controllables) {
controllable.tick(INTERVAL);
}
}
}

View File

@ -0,0 +1,26 @@
package tc.oc.pgm.control.events;
import tc.oc.pgm.control.ControllableGoal;
import tc.oc.pgm.events.MatchEvent;
import tc.oc.pgm.match.Match;
import java.util.Optional;
public abstract class ControllableGoalEvent extends MatchEvent {
protected final ControllableGoal controllable;
public ControllableGoalEvent(Match match, ControllableGoal controllable) {
super(match);
this.controllable = controllable;
}
public ControllableGoal controllable() {
return controllable;
}
public <T extends ControllableGoal> Optional<ControllableGoal> controllable(Class<T> controllableClass) {
return Optional.ofNullable(controllableClass.isInstance(controllable) ? controllable : null);
}
}

View File

@ -0,0 +1,38 @@
package tc.oc.pgm.control.events;
import org.bukkit.event.HandlerList;
import tc.oc.pgm.control.ControllableGoal;
import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.match.Match;
public class ControllableOwnerChangeEvent extends ControllableGoalEvent {
private final Competitor oldController;
private final Competitor newController;
public ControllableOwnerChangeEvent(Match match, ControllableGoal controllable, Competitor oldController, Competitor newController) {
super(match, controllable);
this.oldController = oldController;
this.newController = newController;
}
public Competitor oldController() {
return oldController;
}
public Competitor newController() {
return newController;
}
private static final HandlerList handlers = new HandlerList();
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}

View File

@ -0,0 +1,38 @@
package tc.oc.pgm.control.events;
import org.bukkit.event.HandlerList;
import tc.oc.pgm.control.ControllableGoal;
import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.match.Match;
public class ControllableTeamChangeEvent extends ControllableGoalEvent {
private final Competitor oldTeam;
private final Competitor newTeam;
public ControllableTeamChangeEvent(Match match, ControllableGoal controllable, Competitor oldTeam, Competitor newTeam) {
super(match, controllable);
this.oldTeam = oldTeam;
this.newTeam = newTeam;
}
public Competitor oldTeam() {
return oldTeam;
}
public Competitor newTeam() {
return newTeam;
}
private static final HandlerList handlers = new HandlerList();
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}

View File

@ -1,16 +1,17 @@
package tc.oc.pgm.controlpoint.events;
package tc.oc.pgm.control.events;
import org.bukkit.event.HandlerList;
import tc.oc.pgm.control.ControllableGoal;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.controlpoint.ControlPoint;
public class CapturingTimeChangeEvent extends ControlPointEvent {
private static final HandlerList handlers = new HandlerList();
public class ControllableTimeChangeEvent extends ControllableGoalEvent {
public CapturingTimeChangeEvent(Match match, ControlPoint point) {
super(match, point);
public ControllableTimeChangeEvent(Match match, ControllableGoal controllable) {
super(match, controllable);
}
private static final HandlerList handlers = new HandlerList();
@Override
public HandlerList getHandlers() {
return handlers;
@ -19,4 +20,5 @@ public class CapturingTimeChangeEvent extends ControlPointEvent {
public static HandlerList getHandlerList() {
return handlers;
}
}

View File

@ -0,0 +1,107 @@
package tc.oc.pgm.control.point;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.util.BlockVector;
import org.bukkit.util.Vector;
import tc.oc.commons.bukkit.util.BlockUtils;
import tc.oc.commons.bukkit.util.BukkitUtils;
import tc.oc.pgm.control.ControllableGoal;
import tc.oc.pgm.filters.Filter;
import tc.oc.pgm.filters.operator.AllFilter;
import tc.oc.pgm.filters.operator.InverseFilter;
import tc.oc.pgm.filters.query.BlockQuery;
import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.regions.FiniteBlockRegion;
import tc.oc.pgm.regions.Region;
import tc.oc.pgm.regions.SectorRegion;
import tc.oc.pgm.renewable.BlockImage;
public class ControlPoint extends ControllableGoal<ControlPointDefinition> {
protected final FiniteBlockRegion progressDisplayRegion;
protected final BlockImage progressDisplayImage;
protected final FiniteBlockRegion controllerDisplayRegion;
protected final BlockImage controllerDisplayImage;
public ControlPoint(Match match, ControlPointDefinition definition) {
super(definition, match);
Filter visualMaterials = definition.visualMaterials();
Region progressDisplayRegion = definition.progressDisplayRegion();
Region controllerDisplayRegion = definition.controllerDisplayRegion();
final FiniteBlockRegion.Factory regionFactory = new FiniteBlockRegion.Factory(match.getMapInfo().proto);
if(progressDisplayRegion == null) {
this.progressDisplayRegion = null;
this.progressDisplayImage = null;
} else {
this.progressDisplayRegion = regionFactory.fromWorld(progressDisplayRegion, match.getWorld(), visualMaterials);
this.progressDisplayImage = new BlockImage(match.getWorld(), this.progressDisplayRegion.getBounds());
this.progressDisplayImage.save();
}
if(controllerDisplayRegion == null) {
this.controllerDisplayRegion = null;
this.controllerDisplayImage = null;
} else {
this.controllerDisplayRegion = regionFactory.fromWorld(controllerDisplayRegion, match.getWorld(), this.progressDisplayRegion == null ? visualMaterials : AllFilter.of(visualMaterials, new InverseFilter(progressDisplayRegion)));
this.controllerDisplayImage = new BlockImage(match.getWorld(), this.controllerDisplayRegion.getBounds());
this.controllerDisplayImage.save();
}
}
@Override
public Region region() {
return definition.captureRegion();
}
@Override
protected boolean tracking(MatchPlayer player, Location location) {
return region().contains(location);
}
@Override
protected void displayProgress(Competitor controlling, Competitor capturing, double progress) {
if(progressDisplayRegion != null) {
Vector center = progressDisplayRegion.getBounds().center();
// capturingProgress can be zero, but it can never be one, so invert it to avoid
// a zero-area SectorRegion that can cause glitchy rendering
SectorRegion sectorRegion = new SectorRegion(center.getX(), center.getZ(), 0, (1 - progress) * 2 * Math.PI);
for(BlockVector pos : progressDisplayRegion.getBlockVectors()) {
if(sectorRegion.contains(pos)) {
setBlock(pos, controlling);
} else {
setBlock(pos, capturing);
}
}
}
}
@Override
protected void displaySet(Competitor owner) {
if(controllerDisplayRegion != null) {
if(owner == null) {
for(BlockVector block : controllerDisplayRegion.getBlockVectors()) {
controllerDisplayImage.restore(block);
}
} else {
byte blockData = BukkitUtils.chatColorToDyeColor(owner.getColor()).getWoolData();
for(BlockVector pos : controllerDisplayRegion.getBlockVectors()) {
BlockUtils.blockAt(match.getWorld(), pos).setData(blockData);
}
}
}
}
private void setBlock(BlockVector pos, Competitor team) {
final Block block = BlockUtils.blockAt(match.getWorld(), pos);
if(definition.visualMaterials().query(new BlockQuery(block)).isAllowed()) {
if(team != null) {
block.setData(BukkitUtils.chatColorToDyeColor(team.getColor()).getWoolData());
} else {
progressDisplayImage.restore(pos);
}
}
}
}

View File

@ -0,0 +1,84 @@
package tc.oc.pgm.control.point;
import java.time.Duration;
import java.util.Optional;
import javax.annotation.Nullable;
import tc.oc.pgm.control.ControllableGoalDefinition;
import tc.oc.pgm.control.ControllableGoalDefinitionImpl;
import tc.oc.pgm.features.Feature;
import tc.oc.pgm.features.FeatureInfo;
import tc.oc.pgm.filters.Filter;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.module.ModuleLoadException;
import tc.oc.pgm.regions.Region;
import tc.oc.pgm.teams.TeamFactory;
@FeatureInfo(name = "control-point",
plural = {"control-points", "hills"},
singular = {"control-point", "hill"})
public interface ControlPointDefinition extends ControllableGoalDefinition {
@Override
ControlPoint getGoal(Match match);
Region captureRegion();
Region progressDisplayRegion();
Region controllerDisplayRegion();
Filter visualMaterials();
}
class ControlPointDefinitionImpl extends ControllableGoalDefinitionImpl implements ControlPointDefinition {
private final Region captureRegion;
private final Region progressDisplayRegion;
private final Region controllerDisplayRegion;
private final Filter visualMaterials;
public ControlPointDefinitionImpl(String name, @Nullable Boolean required, boolean visible, Filter captureFilter, Filter defendFilter, Duration captureTime, double multiplierTime, double recoveryRate, double decayRate, Optional<TeamFactory> initialOwner, CaptureCondition captureCondition, boolean neutralState, boolean permanent, float pointsOwned, float pointsPerSecond, float pointsGrowth, boolean showProgress,
Region captureRegion,
Region progressDisplayRegion,
Region controllerDisplayRegion,
Filter visualMaterials) {
super(name, required, visible, captureFilter, defendFilter, captureTime, multiplierTime, recoveryRate, decayRate, initialOwner, captureCondition, neutralState, permanent, pointsOwned, pointsPerSecond, pointsGrowth, showProgress);
this.captureRegion = captureRegion;
this.progressDisplayRegion = progressDisplayRegion;
this.controllerDisplayRegion = controllerDisplayRegion;
this.visualMaterials = visualMaterials;
}
@Override
public Region captureRegion() {
return captureRegion;
}
@Override
public Region progressDisplayRegion() {
return progressDisplayRegion;
}
@Override
public Region controllerDisplayRegion() {
return controllerDisplayRegion;
}
@Override
public Filter visualMaterials() {
return visualMaterials;
}
@Override
public ControlPoint getGoal(Match match) {
return (ControlPoint) super.getGoal(match);
}
@Override
public Feature<?> createFeature(Match match) throws ModuleLoadException {
return new ControlPoint(match, this);
}
}

View File

@ -1,12 +1,12 @@
package tc.oc.pgm.controlpoint;
package tc.oc.pgm.control.point;
import java.time.Duration;
import java.util.Optional;
import java.util.stream.Stream;
import javax.inject.Inject;
import com.google.common.collect.Range;
import org.bukkit.Material;
import org.bukkit.util.Vector;
import org.jdom2.Element;
import tc.oc.pgm.features.FeatureDefinitionParser;
import tc.oc.pgm.features.FeatureParser;
@ -31,7 +31,6 @@ public final class ControlPointParser implements FeatureDefinitionParser<Control
Material.STAINED_GLASS,
Material.STAINED_GLASS_PANE)
.map(material -> MaterialFilter.of(MaterialPattern.accepting(material))));
private final RegionParser regionParser;
private final FilterParser filterParser;
private final FeatureParser<TeamFactory> teamParser;
@ -45,7 +44,6 @@ public final class ControlPointParser implements FeatureDefinitionParser<Control
@Override
public ControlPointDefinition parseElement(Element elControlPoint) throws InvalidXMLException {
final boolean koth = "hill".equals(elControlPoint.getName());
Region captureRegion = regionParser.property(elControlPoint, "capture-region")
.alias("capture")
.union();
@ -59,24 +57,18 @@ public final class ControlPointParser implements FeatureDefinitionParser<Control
.optionalUnion(null);
Filter captureFilter = filterParser.property(elControlPoint, "capture-filter").optional(null);
Filter playerFilter = filterParser.property(elControlPoint, "player-filter").optional(null);
Filter visualMaterials = filterParser.property(elControlPoint, "visual-materials")
.optionalMulti()
.<Filter>map(AnyFilter::new)
.orElse(VISUAL_MATERIALS);
String name = elControlPoint.getAttributeValue("name", "Hill");
TeamFactory initialOwner = teamParser.property(elControlPoint, "initial-owner").optional(null);
Vector capturableDisplayBeacon = XMLUtils.parseVector(elControlPoint.getAttribute("beacon"));
Duration timeToCapture = XMLUtils.parseDuration(elControlPoint.getAttribute("capture-time"), Duration.ofSeconds(30));
double timeMultiplier = XMLUtils.parseNumber(elControlPoint.getAttribute("time-multiplier"), Double.class, koth ? 0.1D : 0D);
final double recoveryRate, decayRate;
final Node attrIncremental = Node.fromAttr(elControlPoint, "incremental");
final Node attrRecovery = Node.fromAttr(elControlPoint, "recovery");
final Node attrDecay = Node.fromAttr(elControlPoint, "decay");
if(attrIncremental == null) {
recoveryRate = XMLUtils.parseNumber(attrRecovery, Double.class, Range.atLeast(0D), koth ? 1D : Double.POSITIVE_INFINITY);
decayRate = XMLUtils.parseNumber(attrDecay, Double.class, Range.atLeast(0D), koth ? 0D : Double.POSITIVE_INFINITY);
@ -88,7 +80,6 @@ public final class ControlPointParser implements FeatureDefinitionParser<Control
recoveryRate = incremental ? 1D : Double.POSITIVE_INFINITY;
decayRate = incremental ? 0D : Double.POSITIVE_INFINITY;
}
boolean neutralState = XMLUtils.parseBoolean(elControlPoint.getAttribute("neutral-state"), koth);
boolean permanent = XMLUtils.parseBoolean(elControlPoint.getAttribute("permanent"), false);
float pointsOwned = XMLUtils.parseNumber(elControlPoint.getAttribute("owner-points"), Float.class, 0f);
@ -97,20 +88,18 @@ public final class ControlPointParser implements FeatureDefinitionParser<Control
boolean showProgress = XMLUtils.parseBoolean(elControlPoint.getAttribute("show-progress"), koth);
boolean visible = XMLUtils.parseBoolean(elControlPoint.getAttribute("show"), true);
Boolean required = XMLUtils.parseBoolean(elControlPoint.getAttribute("required"), null);
ControlPointDefinition.CaptureCondition captureCondition =
XMLUtils.parseEnum(Node.fromAttr(elControlPoint, "capture-rule"),
ControlPointDefinition.CaptureCondition.class,
"capture rule",
ControlPointDefinition.CaptureCondition.EXCLUSIVE);
return new ControlPointDefinitionImpl(
name, required, visible,
captureRegion, captureFilter, playerFilter,
progressDisplayRegion, ownerDisplayRegion, visualMaterials,
capturableDisplayBeacon == null ? null : capturableDisplayBeacon.toBlockVector(),
timeToCapture, timeMultiplier, recoveryRate, decayRate, initialOwner, captureCondition,
neutralState, permanent, pointsOwned, pointsPerSecond, pointsGrowth, showProgress
name, required, visible, captureFilter, playerFilter,
timeToCapture, timeMultiplier, recoveryRate, decayRate,
Optional.ofNullable(initialOwner), captureCondition, neutralState, permanent,
pointsOwned, pointsPerSecond, pointsGrowth, showProgress,
captureRegion, progressDisplayRegion, ownerDisplayRegion, visualMaterials
);
}
}

View File

@ -1,15 +1,15 @@
package tc.oc.pgm.controlpoint;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
package tc.oc.pgm.control.point;
import org.jdom2.Element;
import tc.oc.pgm.utils.XMLUtils;
import tc.oc.pgm.xml.Node;
import tc.oc.pgm.xml.finder.NodeFinder;
class ControlPointRootNodeFinder implements NodeFinder {
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
public class ControlPointRootNodeFinder implements NodeFinder {
@Override
public Stream<Node> findNodes(Element parent, String name) {
final List<Element> elements = new ArrayList<>();

View File

@ -1,152 +0,0 @@
package tc.oc.pgm.controlpoint;
import org.bukkit.block.Block;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.util.BlockVector;
import org.bukkit.util.Vector;
import tc.oc.commons.bukkit.util.BlockUtils;
import tc.oc.commons.bukkit.util.BukkitUtils;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.filters.operator.AllFilter;
import tc.oc.pgm.filters.operator.InverseFilter;
import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.filters.query.BlockQuery;
import tc.oc.pgm.controlpoint.events.CapturingTimeChangeEvent;
import tc.oc.pgm.controlpoint.events.ControllerChangeEvent;
import tc.oc.pgm.filters.Filter;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.regions.FiniteBlockRegion;
import tc.oc.pgm.regions.Region;
import tc.oc.pgm.regions.SectorRegion;
import tc.oc.pgm.renewable.BlockImage;
import java.util.Objects;
/**
* Displays the status of a ControlPoint by coloring blocks in specified regions
*/
@ListenerScope(MatchScope.LOADED)
public class ControlPointBlockDisplay implements Listener {
protected final Match match;
protected final ControlPoint controlPoint;
protected final FiniteBlockRegion progressDisplayRegion;
protected final BlockImage progressDisplayImage;
protected final FiniteBlockRegion controllerDisplayRegion;
protected final BlockImage controllerDisplayImage;
protected Competitor controllingTeam;
public ControlPointBlockDisplay(Match match, ControlPoint controlPoint) {
this.match = match;
this.controlPoint = controlPoint;
Filter visualMaterials = controlPoint.getDefinition().getVisualMaterials();
Region progressDisplayRegion = controlPoint.getDefinition().getProgressDisplayRegion();
Region controllerDisplayRegion = controlPoint.getDefinition().getControllerDisplayRegion();
final FiniteBlockRegion.Factory regionFactory = new FiniteBlockRegion.Factory(match.getMapInfo().proto);
if(progressDisplayRegion == null) {
this.progressDisplayRegion = null;
this.progressDisplayImage = null;
} else {
this.progressDisplayRegion = regionFactory.fromWorld(progressDisplayRegion,
match.getWorld(),
visualMaterials);
this.progressDisplayImage = new BlockImage(match.getWorld(), this.progressDisplayRegion.getBounds());
this.progressDisplayImage.save();
}
if(controllerDisplayRegion == null) {
this.controllerDisplayRegion = null;
this.controllerDisplayImage = null;
} else {
// Ensure the controller and progress display regions do not overlap. The progress display has priority.
this.controllerDisplayRegion = regionFactory.fromWorld(
controllerDisplayRegion,
match.getWorld(),
this.progressDisplayRegion == null ? visualMaterials
: AllFilter.of(visualMaterials, new InverseFilter(progressDisplayRegion))
);
this.controllerDisplayImage = new BlockImage(match.getWorld(), this.controllerDisplayRegion.getBounds());
this.controllerDisplayImage.save();
}
}
/**
* Change the controller display to the given team's color, or reset the display if team is null
*/
@SuppressWarnings("deprecation")
public void setController(Competitor controllingTeam) {
if(!Objects.equals(this.controllingTeam, controllingTeam) && this.controllerDisplayRegion != null) {
if(controllingTeam == null) {
for(BlockVector block : this.controllerDisplayRegion.getBlockVectors()) {
this.controllerDisplayImage.restore(block);
}
} else {
byte blockData = BukkitUtils.chatColorToDyeColor(controllingTeam.getColor()).getWoolData();
for(BlockVector pos : this.controllerDisplayRegion.getBlockVectors()) {
BlockUtils.blockAt(match.getWorld(), pos).setData(blockData);
}
}
this.controllingTeam = controllingTeam;
}
}
private void setBlock(BlockVector pos, Competitor team) {
final Block block = BlockUtils.blockAt(match.getWorld(), pos);
if(this.controlPoint.getDefinition().getVisualMaterials().query(new BlockQuery(block)).isAllowed()) {
if(team != null) {
block.setData(BukkitUtils.chatColorToDyeColor(team.getColor()).getWoolData());
} else {
this.progressDisplayImage.restore(pos);
}
}
}
protected void setProgress(Competitor controllingTeam, Competitor capturingTeam, double capturingProgress) {
if(this.progressDisplayRegion != null) {
Vector center = this.progressDisplayRegion.getBounds().center();
// capturingProgress can be zero, but it can never be one, so invert it to avoid
// a zero-area SectorRegion that can cause glitchy rendering
SectorRegion sectorRegion = new SectorRegion(center.getX(), center.getZ(), 0, (1 - capturingProgress) * 2 * Math.PI);
for(BlockVector pos : this.progressDisplayRegion.getBlockVectors()) {
if(sectorRegion.contains(pos)) {
this.setBlock(pos, controllingTeam);
} else {
this.setBlock(pos, capturingTeam);
}
}
}
}
public void render() {
this.setController(this.controlPoint.getOwner());
this.setProgress(this.controlPoint.getOwner(),
this.controlPoint.getCapturer(),
this.controlPoint.getCompletion());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onTimeChange(CapturingTimeChangeEvent event) {
if(this.controlPoint == event.getControlPoint()) {
this.setProgress(event.getControlPoint().getOwner(),
event.getControlPoint().getCapturer(),
event.getControlPoint().getCompletion());
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onControllerChange(ControllerChangeEvent event) {
if(this.controlPoint == event.getControlPoint()) {
this.setController(event.getNewController());
}
}
}

View File

@ -1,316 +0,0 @@
package tc.oc.pgm.controlpoint;
import java.time.Duration;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.bukkit.util.BlockVector;
import tc.oc.api.docs.virtual.MapDoc;
import tc.oc.pgm.features.FeatureInfo;
import tc.oc.pgm.features.GamemodeFeature;
import tc.oc.pgm.filters.Filter;
import tc.oc.pgm.goals.GoalDefinition;
import tc.oc.pgm.goals.GoalDefinitionImpl;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.regions.Region;
import tc.oc.pgm.teams.TeamFactory;
@FeatureInfo(name = "control-point",
plural = {"control-points", "hills"},
singular = {"control-point", "hill"})
public interface ControlPointDefinition extends GoalDefinition, GamemodeFeature {
@Override ControlPoint getGoal(Match match);
Region getCaptureRegion();
Filter getCaptureFilter();
Filter getPlayerFilter();
Region getProgressDisplayRegion();
Region getControllerDisplayRegion();
Filter getVisualMaterials();
BlockVector getCapturableDisplayBeacon();
Duration getTimeToCapture();
double decayRate();
double recoveryRate();
double getTimeMultiplier();
TeamFactory getInitialOwner();
CaptureCondition getCaptureCondition();
boolean hasNeutralState();
boolean isPermanent();
boolean affectsScore();
float getPointsOwned();
float getPointsPerSecond();
float getPointsGrowth();
boolean getShowProgress();
// Conditions required for a team to capture:
enum CaptureCondition {
EXCLUSIVE, // Team owns all players on the point
MAJORITY, // Team owns more than half the players on the point
LEAD // Team owns more players on the point than any other single team
}
}
class ControlPointDefinitionImpl extends GoalDefinitionImpl implements ControlPointDefinition {
// Players in this region are considered "on" the point
private final Region captureRegion;
// Which players can capture the point
private final Filter captureFilter;
// Which players can prevent other teams from capturing the point
private final Filter playerFilter;
// Blocks in this region are used to show capturing progress
private final Region progressDisplayRegion;
// Blocks in this region are used to show the team that owns the point
private final Region ownerDisplayRegion;
// Block types used for the regions above (currently fixed to wool and stained clay)
private final Filter visualMaterials;
// Location of a beacon used to indicate to players that they can capture this point
private final BlockVector capturableDisplayBeacon;
// Base time for the point to transition between states
private final Duration timeToCapture;
// Capture time multiplier for increasing or decreasing capture time based on the number of players on the point
private final double timeMultiplier;
// Relative rate at which progress reverts from players dominating the point
private final double recoveryRate;
// Relative rate at which progress reverts while nobody is dominating the point
private final double decayRate;
// The team that owns the point when the match starts, null for no owner (neutral state)
private final TeamFactory initialOwner;
private final CaptureCondition captureCondition;
// true: point must transition through unowned state to change owners
// false: point transitions directly from one owner to the next
// NOTE: points always start in an unowned state, regardless of this value
private final boolean neutralState;
// If true, the point can only be captured once in the match
private final boolean permanent;
// Amount of points given to the team that owns the point, they are removed when the team looses the point
private final float pointsOwned;
// Rate that the owner's score increases, or 0 if the CP does not affect score
private final float pointsPerSecond;
// If this is less than +inf, the effective pointsPerSecond will increase over time
// at an exponential rate, such that it doubles every time this many seconds elapses.
private final float pointsGrowth;
// If true, capturing progress is displayed on the scoreboard
private final boolean showProgress;
public ControlPointDefinitionImpl(String name,
@Nullable Boolean required,
boolean visible,
Region captureRegion,
Filter captureFilter,
Filter playerFilter,
Region progressDisplayRegion,
Region ownerDisplayRegion,
Filter visualMaterials,
BlockVector capturableDisplayBeacon,
Duration timeToCapture,
double timeMultiplier,
double recoveryRate,
double decayRate,
TeamFactory initialOwner,
CaptureCondition captureCondition,
boolean neutralState,
boolean permanent,
float pointsOwned,
float pointsPerSecond,
float pointsGrowth,
boolean progress) {
super(name, required, visible);
this.captureRegion = captureRegion;
this.captureFilter = captureFilter;
this.playerFilter = playerFilter;
this.progressDisplayRegion = progressDisplayRegion;
this.ownerDisplayRegion = ownerDisplayRegion;
this.visualMaterials = visualMaterials;
this.capturableDisplayBeacon = capturableDisplayBeacon;
this.timeToCapture = timeToCapture;
this.timeMultiplier = timeMultiplier;
this.recoveryRate = recoveryRate;
this.decayRate = decayRate;
this.initialOwner = initialOwner;
this.captureCondition = captureCondition;
this.neutralState = neutralState;
this.permanent = permanent;
this.pointsOwned = pointsOwned;
this.pointsPerSecond = pointsPerSecond;
this.pointsGrowth = pointsGrowth;
this.showProgress = progress;
}
@Override
public String toString() {
return "ControlPointDefinition {name=" + this.getName() +
" timeToCapture=" + this.getTimeToCapture() +
" timeMultiplier=" + this.getTimeMultiplier() +
" initialOwner=" + this.getInitialOwner() +
" captureCondition=" + this.getCaptureCondition() +
" neutralState=" + this.hasNeutralState() +
" permanent=" + this.isPermanent() +
" captureRegion=" + this.getCaptureRegion() +
" captureFilter=" + this.getCaptureFilter() +
" playerFilter=" + this.getPlayerFilter() +
" progressDisplay=" + this.getProgressDisplayRegion() +
" ownerDisplay=" + this.getControllerDisplayRegion() +
" beacon=" + this.getCapturableDisplayBeacon() +
" visible=" + this.isVisible();
}
@Override
public Stream<MapDoc.Gamemode> gamemodes() {
return Stream.of(MapDoc.Gamemode.koth);
}
@Override
public ControlPoint getGoal(Match match) {
return (ControlPoint) super.getGoal(match);
}
@Override
public ControlPoint createFeature(Match match) {
final ControlPoint cp = new ControlPoint(match, this);
cp.registerEvents();
return cp;
}
@Override
public boolean isShared() {
return true;
}
@Override
public Region getCaptureRegion() {
return this.captureRegion;
}
@Override
public Filter getCaptureFilter() {
return this.captureFilter;
}
@Override
public Filter getPlayerFilter() {
return this.playerFilter;
}
@Override
public Region getProgressDisplayRegion() {
return this.progressDisplayRegion;
}
@Override
public Region getControllerDisplayRegion() {
return this.ownerDisplayRegion;
}
@Override
public Filter getVisualMaterials() {
return this.visualMaterials;
}
@Override
public BlockVector getCapturableDisplayBeacon() {
return this.capturableDisplayBeacon;
}
@Override
public Duration getTimeToCapture() {
return this.timeToCapture;
}
@Override
public double getTimeMultiplier() {
return this.timeMultiplier;
}
@Override
public double recoveryRate() {
return this.recoveryRate;
}
@Override
public double decayRate() {
return this.decayRate;
}
@Override
public TeamFactory getInitialOwner() {
return this.initialOwner;
}
@Override
public CaptureCondition getCaptureCondition() {
return this.captureCondition;
}
@Override
public boolean hasNeutralState() {
return this.neutralState;
}
@Override
public boolean isPermanent() {
return this.permanent;
}
@Override
public boolean affectsScore() {
return this.pointsPerSecond > 0;
}
@Override
public float getPointsOwned() {
return this.pointsOwned;
}
@Override
public float getPointsPerSecond() {
return this.pointsPerSecond;
}
@Override
public float getPointsGrowth() {
return this.pointsGrowth;
}
@Override
public boolean getShowProgress() {
return this.showProgress;
}
}

View File

@ -1,53 +0,0 @@
package tc.oc.pgm.controlpoint;
import java.time.Duration;
import java.util.List;
import javax.inject.Inject;
import org.bukkit.event.HandlerList;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.Repeatable;
import tc.oc.time.Time;
import static tc.oc.commons.core.stream.Collectors.toImmutableList;
public class ControlPointMatchModule extends MatchModule {
private static final long MILLIS = 100;
private static final Duration INTERVAL = Duration.ofMillis(MILLIS); // milliseconds, two ticks
private final List<ControlPoint> controlPoints;
private final ControlPointAnnouncer announcer;
@Inject private ControlPointMatchModule(Match match) {
this.announcer = new ControlPointAnnouncer(match);
this.controlPoints = match.featureDefinitions()
.all(ControlPointDefinition.class)
.map(cp -> cp.getGoal(match))
.collect(toImmutableList());
}
@Override
public void load() {
super.load();
getMatch().registerEvents(this.announcer);
}
@Override
public void unload() {
for(ControlPoint controlPoint : this.controlPoints) {
controlPoint.unregisterEvents();
}
HandlerList.unregisterAll(this.announcer);
super.unload();
}
@Repeatable(interval = @Time(milliseconds = MILLIS))
public void tick() {
for(ControlPoint controlPoint : controlPoints) {
controlPoint.tick(INTERVAL);
}
}
}

View File

@ -1,92 +0,0 @@
package tc.oc.pgm.controlpoint;
import com.google.common.collect.Sets;
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.PlayerTeleportEvent;
import org.bukkit.util.Vector;
import tc.oc.pgm.events.ListenerScope;
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.spawns.events.ParticipantDespawnEvent;
import tc.oc.commons.bukkit.event.CoarsePlayerMoveEvent;
import tc.oc.pgm.regions.Region;
import tc.oc.commons.core.util.DefaultMapAdapter;
import tc.oc.pgm.utils.MatchPlayers;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Tracks which players are on a control point and answers some queries about them
*/
@ListenerScope(MatchScope.LOADED)
public class ControlPointPlayerTracker implements Listener {
protected final Match match;
protected final Region captureRegion;
protected final Set<MatchPlayer> playersOnPoint = Sets.newHashSet();
public ControlPointPlayerTracker(Match match, Region captureRegion) {
this.match = match;
this.captureRegion = captureRegion;
}
public Set<MatchPlayer> getPlayersOnPoint() {
return this.playersOnPoint;
}
/**
* Get the number of players that each team in the match has on the point
*/
public Map<Competitor, Integer> getPlayerCountsByTeam() {
// calculate how many players from each team are on the hill
Map<Competitor, Integer> counts = new DefaultMapAdapter<>(new HashMap<>(), 0);
for(MatchPlayer player : this.getPlayersOnPoint()) {
Competitor team = player.getCompetitor();
counts.put(team, counts.get(team) + 1);
}
return counts;
}
/**
* Get the number of players that each team in the match has on the point, sorted from most to least
*/
public List<Map.Entry<Competitor, Integer>> getSortedPlayerCountsByTeam() {
// reverse natural ordering of value
return new ArrayList<>(this.getPlayerCountsByTeam().entrySet()).stream().sorted((o1, o2) -> Integer.compare(o2.getValue(), o1.getValue())).collect(Collectors.toList());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerMove(final CoarsePlayerMoveEvent event) {
this.handlePlayerMove(event.getPlayer(), event.getTo().toVector());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerTeleport(final PlayerTeleportEvent event) {
this.handlePlayerMove(event.getPlayer(), event.getTo().toVector());
}
private void handlePlayerMove(Player bukkit, Vector to) {
MatchPlayer player = this.match.getPlayer(bukkit);
if(!MatchPlayers.canInteract(player)) return;
if(!player.getBukkit().isDead() && this.captureRegion.contains(to.toBlockVector())) {
this.playersOnPoint.add(player);
} else {
this.playersOnPoint.remove(player);
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerDespawn(final ParticipantDespawnEvent event) {
playersOnPoint.remove(event.getPlayer());
}
}

View File

@ -1,38 +0,0 @@
package tc.oc.pgm.controlpoint.events;
import javax.annotation.Nullable;
import org.bukkit.event.HandlerList;
import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.controlpoint.ControlPoint;
public class CapturingTeamChangeEvent extends ControlPointEvent {
private static final HandlerList handlers = new HandlerList();
@Nullable private final Competitor oldTeam;
@Nullable private final Competitor newTeam;
public CapturingTeamChangeEvent(Match match, ControlPoint hill, Competitor oldTeam, Competitor newTeam) {
super(match, hill);
this.oldTeam = oldTeam;
this.newTeam = newTeam;
}
public @Nullable Competitor getOldTeam() {
return this.oldTeam;
}
public @Nullable Competitor getNewTeam() {
return this.newTeam;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}

View File

@ -1,18 +0,0 @@
package tc.oc.pgm.controlpoint.events;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.controlpoint.ControlPoint;
import tc.oc.pgm.events.MatchEvent;
public abstract class ControlPointEvent extends MatchEvent {
protected final ControlPoint controlPoint;
public ControlPointEvent(Match match, ControlPoint controlPoint) {
super(match);
this.controlPoint = controlPoint;
}
public ControlPoint getControlPoint() {
return this.controlPoint;
}
}

View File

@ -1,40 +0,0 @@
package tc.oc.pgm.controlpoint.events;
import org.bukkit.event.HandlerList;
import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.controlpoint.ControlPoint;
import javax.annotation.Nullable;
public class ControllerChangeEvent extends ControlPointEvent {
private static final HandlerList handlers = new HandlerList();
@Nullable private final Competitor oldController;
@Nullable private final Competitor newController;
public ControllerChangeEvent(Match match, ControlPoint hill, Competitor oldController, Competitor newController) {
super(match, hill);
this.oldController = oldController;
this.newController = newController;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
@Nullable
public Competitor getNewController() {
return newController;
}
@Nullable
public Competitor getOldController() {
return oldController;
}
}

View File

@ -12,7 +12,7 @@ import org.bukkit.event.Listener;
import org.bukkit.geometry.Cuboid;
import tc.oc.commons.bukkit.util.BlockUtils;
import tc.oc.commons.bukkit.util.BukkitUtils;
import tc.oc.pgm.controlpoint.events.ControllerChangeEvent;
import tc.oc.pgm.control.events.ControllableOwnerChangeEvent;
import tc.oc.pgm.core.CoreLeakEvent;
import tc.oc.pgm.destroyable.DestroyableDestroyedEvent;
import tc.oc.pgm.flag.event.FlagCaptureEvent;
@ -78,11 +78,11 @@ public class ObjectivesFireworkListener implements Listener {
}
@EventHandler(priority = EventPriority.MONITOR)
public void onHillCapture(final ControllerChangeEvent event) {
if(FireworksConfig.Goals.enabled() && event.getControlPoint().isVisible() && event.getNewController() != null) {
public void onHillCapture(final ControllableOwnerChangeEvent event) {
if(FireworksConfig.Goals.enabled() && event.controllable().isVisible() && event.newController() != null) {
this.spawnFireworkDisplay(event.getMatch().getWorld(),
event.getControlPoint().getCaptureRegion(),
BukkitUtils.colorOf(event.getNewController().getColor()),
event.controllable().region(),
BukkitUtils.colorOf(event.newController().getColor()),
8, 1, 2);
}
}