Skip to content

Commit

Permalink
Implement time penalties for switching
Browse files Browse the repository at this point in the history
Signed-off-by: Pablo Herrera <[email protected]>
  • Loading branch information
Pablete1234 committed Oct 24, 2024
1 parent 852bf3d commit 4ae0486
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 35 deletions.
13 changes: 13 additions & 0 deletions core/src/main/java/tc/oc/pgm/PGMConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
Expand Down Expand Up @@ -81,6 +82,7 @@ public final class PGMConfig implements Config {
private final boolean woolRefill;
private final int griefScore;
private final float assistPercent;
private final Map<TimePenalty, Duration> timePenalties;

// votes.*
private final boolean allowExtraVotes;
Expand Down Expand Up @@ -183,6 +185,12 @@ public final class PGMConfig implements Config {
parseInteger(config.getString("gameplay.grief-score", "-10"), Range.atMost(0));
this.assistPercent =
parseFloat(config.getString("gameplay.assist-percent", "0.3"), Range.openClosed(0f, 1f));
this.timePenalties = new EnumMap<>(TimePenalty.class);
for (TimePenalty penalty : TimePenalty.values()) {
String key = penalty.name().toLowerCase(Locale.ROOT).replace('_', '-');
timePenalties.put(
penalty, parseDuration(config.getString("gameplay.time-penalties." + key, "0")));
}

this.allowExtraVotes = parseBoolean(config.getString("votes.allow-extra-votes", "true"));
this.maxExtraVotes = parseInteger(config.getString("votes.max-extra-votes", "5"));
Expand Down Expand Up @@ -569,6 +577,11 @@ public float getAssistPercent() {
return assistPercent;
}

@Override
public Duration getTimePenalty(TimePenalty penalty) {
return timePenalties.get(penalty);
}

@Override
public boolean showSideBar() {
return showSideBar;
Expand Down
16 changes: 16 additions & 0 deletions core/src/main/java/tc/oc/pgm/api/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,22 @@ public interface Config {
*/
float getAssistPercent();

/**
* Gets how long to penalize players for certain actions
*
* @return time to make them sit out for
*/
Duration getTimePenalty(TimePenalty penalty);

enum TimePenalty {
FFA_FULL_REJOIN,
STACKED,
FULL_REJOIN,
REJOIN_MULTIPLIER,
REJOIN_MAX,
SWITCH
}

/**
* Gets if extra votes are allowed based on the "pgm.vote.extra.#" permission.
*
Expand Down
64 changes: 64 additions & 0 deletions core/src/main/java/tc/oc/pgm/spawns/ParticipationData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package tc.oc.pgm.spawns;

import java.time.Duration;
import tc.oc.pgm.api.Config.TimePenalty;
import tc.oc.pgm.api.PGM;
import tc.oc.pgm.api.match.Match;
import tc.oc.pgm.api.party.Competitor;
import tc.oc.pgm.teams.Team;
import tc.oc.pgm.util.TimeUtils;

class ParticipationData {
private Team lastTeam;
private long lastLeaveTick;
private boolean wasFull;
private boolean wasStacked;
private int rejoins;

public void trackLeave(Match match, Competitor competitor) {
if (competitor instanceof Team team) {
if (lastTeam == team) rejoins++;
lastTeam = team;
wasFull = team.getSize() >= team.getMaxPlayers() - 1;
wasStacked = team.isStacked();
} else {
lastTeam = null;
wasFull = match.getParticipants().size() >= match.getMaxPlayers();
wasStacked = false;
}
lastLeaveTick = match.getTick().tick;
}

public long getJoinTick(Team newTeam) {
TimePenalty penalty = getPenalty(newTeam);
Duration timeOff = getDuration(penalty);
if (penalty == TimePenalty.REJOIN_MULTIPLIER) {
timeOff = TimeUtils.min(timeOff.multipliedBy(rejoins), getDuration(TimePenalty.REJOIN_MAX));
}
return lastLeaveTick + TimeUtils.toTicks(timeOff);
}

static Duration getDuration(TimePenalty penalty) {
return penalty == null ? Duration.ZERO : PGM.get().getConfiguration().getTimePenalty(penalty);
}

private TimePenalty getPenalty(Team newTeam) {
if (lastTeam == null || newTeam == null) { // FFA
return wasFull ? TimePenalty.FFA_FULL_REJOIN : null;
}
// Left and rejoined to get the team stacked
if (newTeam.isStacked()) return TimePenalty.STACKED;

if (lastTeam == newTeam) { // Team rejoin
// Making space for someone else to join
if (wasFull && newTeam.getSize() >= newTeam.getMaxPlayers()) return TimePenalty.FULL_REJOIN;
// Rejoin spam for resources
return TimePenalty.REJOIN_MULTIPLIER;
} else {
// Explicitly forgive switching to un-stack
if (wasStacked && !newTeam.isStacked()) return null;

return TimePenalty.STACKED;
}
}
}
42 changes: 32 additions & 10 deletions core/src/main/java/tc/oc/pgm/spawns/SpawnMatchModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,13 @@
import tc.oc.pgm.api.time.Tick;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.PlayerJoinPartyEvent;
import tc.oc.pgm.events.PlayerParticipationStopEvent;
import tc.oc.pgm.events.PlayerPartyChangeEvent;
import tc.oc.pgm.join.JoinRequest;
import tc.oc.pgm.spawns.states.Joining;
import tc.oc.pgm.spawns.states.Observing;
import tc.oc.pgm.spawns.states.State;
import tc.oc.pgm.teams.Team;
import tc.oc.pgm.util.event.PlayerItemTransferEvent;
import tc.oc.pgm.util.event.player.PlayerAttackEntityEvent;

Expand All @@ -64,6 +67,8 @@ public class SpawnMatchModule implements MatchModule, Listener, Tickable {
private final ObserverToolFactory observerToolFactory;
private final Cache<UUID, Long> deathTicks =
CacheBuilder.newBuilder().expireAfterWrite(60, TimeUnit.SECONDS).build();
private final Cache<UUID, ParticipationData> participationData =
CacheBuilder.newBuilder().expireAfterWrite(120, TimeUnit.SECONDS).build();

public SpawnMatchModule(Match match, SpawnModule module) {
this.match = match;
Expand Down Expand Up @@ -180,27 +185,44 @@ public long getDeathTick(MatchPlayer player) {
return deathTick != null ? deathTick : 0;
}

public long getJoinPenalty(PlayerPartyChangeEvent event) {
if (event.getRequest().has(JoinRequest.Flag.FORCE)) return 0;
if (event.getNewParty() == null || !event.getNewParty().isParticipating()) return 0;

ParticipationData data = participationData.getIfPresent(event.getPlayer().getId());
return data == null ? 0 : data.getJoinTick(event.getNewParty() instanceof Team t ? t : null);
}

@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerLeave(final PlayerParticipationStopEvent event) {
MatchPlayer player = event.getPlayer();
// Match not started, or you never fully joined, or forced move
if (!event.getMatch().isRunning() || states.get(player) instanceof Joining) return;
if (event.getRequest().has(JoinRequest.Flag.FORCE) && event.getNextParty() != null) return;

ParticipationData data = participationData.getIfPresent(event.getPlayer().getId());
(data == null ? data = new ParticipationData() : data).trackLeave(match, event.getCompetitor());
participationData.put(event.getPlayer().getId(), data);
}

@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPartyChange(final PlayerPartyChangeEvent event) {
MatchPlayer player = event.getPlayer();
if (event.getOldParty() == null) {
// Join match
if (event.getNewParty().isParticipating()) {
transition(
event.getPlayer(),
null,
new Joining(this, event.getPlayer(), getDeathTick(event.getPlayer())));
transition(player, null, new Joining(this, player, getJoinPenalty(event)));
} else {
transition(event.getPlayer(), null, new Observing(this, event.getPlayer(), true, true));
transition(player, null, new Observing(this, player, true, true));
}
} else if (event.getNewParty() == null) {
// Leave match
transition(event.getPlayer(), states.get(event.getPlayer()), null);
transition(player, states.get(player), null);
} else {
// Party change during match
State state = states.get(event.getPlayer());
if (state != null)
state.onEvent((PlayerJoinPartyEvent)
event); // Should always be PlayerPartyJoinEvent if getNewParty() != null
State state = states.get(player);
// Should always be PlayerPartyJoinEvent if getNewParty() != null
if (state != null) state.onEvent((PlayerJoinPartyEvent) event);
}
}

Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/tc/oc/pgm/spawns/states/Alive.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public void onEvent(PlayerJoinPartyEvent event) {
super.onEvent(event);

if (event.getNewParty() instanceof Competitor) {
transition(new Joining(smm, player));
transition(new Joining(smm, player, smm.getJoinPenalty(event)));
} else {
transition(new Observing(smm, player, true, true));
}
Expand Down
17 changes: 7 additions & 10 deletions core/src/main/java/tc/oc/pgm/spawns/states/Dead.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class Dead extends Spawning {
private boolean kitted, rotted;

public Dead(SpawnMatchModule smm, MatchPlayer player) {
super(smm, player, player.getMatch().getTick().tick);
super(smm, player, player.getMatch().getTick().tick, 0);
}

@Override
Expand All @@ -55,15 +55,12 @@ public void enterState() {
// Flash/wobble the screen. If we don't delay this then the client glitches out
// when the player dies from a potion effect. I have no idea why it happens,
// but this fixes it. We could investigate a better fix at some point.
smm.getMatch()
.getExecutor(MatchScope.LOADED)
.execute(
() -> {
if (isCurrent() && bukkit.isOnline()) {
bukkit.addPotionEffect(options.blackout ? BLINDNESS_LONG : BLINDNESS_SHORT, true);
bukkit.addPotionEffect(CONFUSION, true);
}
});
smm.getMatch().getExecutor(MatchScope.LOADED).execute(() -> {
if (isCurrent() && bukkit.isOnline()) {
bukkit.addPotionEffect(options.blackout ? BLINDNESS_LONG : BLINDNESS_SHORT, true);
bukkit.addPotionEffect(CONFUSION, true);
}
});
}

@Override
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/java/tc/oc/pgm/spawns/states/Joining.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ public Joining(SpawnMatchModule smm, MatchPlayer player) {
this(smm, player, 0);
}

public Joining(SpawnMatchModule smm, MatchPlayer player, long deathTick) {
super(smm, player, deathTick);
public Joining(SpawnMatchModule smm, MatchPlayer player, long minSpawnTick) {
super(smm, player, smm.getDeathTick(player), minSpawnTick);
this.spawnRequested = true;
}

Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/tc/oc/pgm/spawns/states/Observing.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public void onEvent(MatchStartEvent event) {
@Override
public void onEvent(PlayerJoinPartyEvent event) {
if (event.getNewParty() instanceof Competitor && event.getMatch().isRunning()) {
transition(new Joining(smm, player, smm.getDeathTick(event.getPlayer())));
transition(new Joining(smm, player, smm.getJoinPenalty(event)));
}
}

Expand Down
14 changes: 8 additions & 6 deletions core/src/main/java/tc/oc/pgm/spawns/states/Spawning.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ public abstract class Spawning extends Participating {

protected final RespawnOptions options;
protected boolean spawnRequested;
protected final long deathTick;
protected final long startTick;
protected final long spawnAtTick;

public Spawning(SpawnMatchModule smm, MatchPlayer player, long deathTick) {
public Spawning(SpawnMatchModule smm, MatchPlayer player, long deathTick, long minSpawnTick) {
super(smm, player);
this.options = smm.getRespawnOptions(player);
this.spawnRequested = options.auto;
this.deathTick = deathTick;
this.startTick = player.getMatch().getTick().tick;
this.spawnAtTick = Math.max(deathTick + options.delayTicks, minSpawnTick);
}

@Override
Expand Down Expand Up @@ -73,7 +75,7 @@ public void onEvent(EntityDamageEvent event) {
}

protected long age() {
return player.getMatch().getTick().tick - deathTick;
return player.getMatch().getTick().tick - startTick;
}

@Override
Expand All @@ -100,7 +102,7 @@ protected boolean trySpawn() {
}

protected long ticksUntilRespawn() {
return Math.max(0, options.delayTicks - age());
return spawnAtTick - player.getMatch().getTick().tick;
}

public @Nullable Spawn chooseSpawn() {
Expand Down Expand Up @@ -144,7 +146,7 @@ protected Component getSubtitle(boolean spectator) {
}

public void sendMessage() {
long ticks = options.delayTicks - age();
long ticks = ticksUntilRespawn();
if (ticks % (ticks > 0 ? 20 : 100) == 0) {
player.sendMessage(getSubtitle(false));
}
Expand Down
12 changes: 9 additions & 3 deletions core/src/main/java/tc/oc/pgm/teams/Team.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ public class Team extends PartyImpl implements Competitor, Feature<TeamFactory>
// The maximum allowed ratio between the "fullness" of any two teams in a match,
// as measured by the Team.getFullness method. An imbalance of one player is
// always allowed, even if it exceeds this ratio.
public static final float MAX_IMBALANCE = 1.2f;
public static final float MAX_IMBALANCE = 1.25f;
// Same as above, but for "standard" 2-team same max-players matches
public static final float MAX_STANDARD_IMBALANCE = 1.1f;

private final TeamFactory info;
private int min, max, overfill;
Expand Down Expand Up @@ -186,14 +188,17 @@ public float getFullnessAfterJoin(int players) {
public int getMaxBalancedSize() {
// Find the minimum fullness among other teams
float minFullness = 1f;
boolean isStandard = true;
for (Team team : module().getParticipatingTeams()) {
if (team != this) {
minFullness = Math.min(minFullness, team.getFullness());
isStandard &= team.getMaxOverfill() == this.getMaxOverfill();
}
}

// Calculate the dynamic limit to maintain balance with other teams (this can be zero)
int slots = (int) Math.ceil(Math.min(1f, minFullness * MAX_IMBALANCE) * this.getMaxOverfill());
float maxImbalance = isStandard ? MAX_STANDARD_IMBALANCE : MAX_IMBALANCE;
int slots = (int) Math.ceil(Math.min(1f, minFullness * maxImbalance) * this.getMaxOverfill());

// Clamp to the static limit defined for this team (cannot be zero unless the static limit is
// zero)
Expand All @@ -217,7 +222,8 @@ public int getOpenSlots(JoinRequest request, boolean priorityKick) {
} else {
// Subtract all players who cannot be kicked
JoinMatchModule jmm = join();
slots -= this.getPlayers().stream().filter(pl -> !jmm.canBePriorityKicked(pl)).count();
slots -=
this.getPlayers().stream().filter(pl -> !jmm.canBePriorityKicked(pl)).count();
}
return Math.max(0, slots);
}
Expand Down
11 changes: 9 additions & 2 deletions core/src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,16 @@ join:

# Changes various gameplay mechanics.
gameplay:
refill-wool: true # Should wool in wool rooms be automatically refilled?
grief-score: -10 # Score under which players should be kept out of the match
refill-wool: true # Should wool in wool rooms be automatically refilled?
grief-score: -10 # Score under which players should be kept out of the match
assist-percent: 0.3 # What percent of the damage is required for an assist 0.3 = 30% of max hp
time-penalties:
ffa-full-rejoin: 10s # Rejoining on ffa when it's full
stacked: 30s # Rejoining into a stacked team
full-rejoin: 30s # Rejoining when your team was full and is still full
rejoin-multiplier: 10s # Increased penalty for every additional rejoin, up to max
rejoin-max: 30s # Max time penalty for repeated rejoins
switch: 30s # Plainly switching teams

# Changes map voting mechanics.
votes:
Expand Down
8 changes: 8 additions & 0 deletions util/src/main/java/tc/oc/pgm/util/TimeUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,12 @@ public static boolean isShorterThan(Duration a, Duration b) {
public static boolean isLongerThan(Duration a, Duration b) {
return a.compareTo(b) > 0;
}

public static Duration max(Duration a, Duration b) {
return a.compareTo(b) > 0 ? a : b;
}

public static Duration min(Duration a, Duration b) {
return a.compareTo(b) < 0 ? a : b;
}
}

0 comments on commit 4ae0486

Please sign in to comment.