Blitz overhaul

This commit is contained in:
Electroid 2017-03-31 16:20:27 -07:00
parent ee06c979b4
commit 69d41c07bf
39 changed files with 1103 additions and 364 deletions

View File

@ -124,14 +124,14 @@ public abstract class CommandUtils {
}
public static Duration getDuration(CommandContext args, int index, Duration def) throws CommandException {
return args.argsLength() > index ? getDuration(args.getString(index), null) : def;
return getDuration(args.getString(index, null), def);
}
public static @Nullable Duration getDuration(@Nullable String text) throws CommandException {
public static @Nullable Duration getDuration(String text) throws CommandException {
return getDuration(text, null);
}
public static Duration getDuration(@Nullable String text, Duration def) throws CommandException {
public static Duration getDuration(String text, Duration def) throws CommandException {
if(text == null) {
return def;
} else {
@ -143,6 +143,26 @@ public abstract class CommandUtils {
}
}
public static @Nullable <E extends Enum<E>> E getEnum(CommandContext args, CommandSender sender, int index, Class<E> type) throws CommandException {
return getEnum(args, sender, index, type, null);
}
public static <E extends Enum<E>> E getEnum(CommandContext args, CommandSender sender, int index, Class<E> type, E def) throws CommandException {
return getEnum(args.getString(index, null), sender, type, def);
}
public static <E extends Enum<E>> E getEnum(String text, CommandSender sender, Class<E> type, E def) throws CommandException {
if(text == null) {
return def;
} else {
try {
return Enum.valueOf(type, text.toUpperCase().replace(' ', '_'));
} catch(IllegalArgumentException e) {
throw newCommandException(sender, new TranslatableComponent("command.error.invalidEnum", text));
}
}
}
public static String getDisplayName(CommandSender target) {
return getDisplayName(target, null);
}

View File

@ -10,6 +10,7 @@ command.admin.cancelRestart.noActionTaken = No active or queued restart countdow
command.error.notEnoughArguments = Not enough arguments
command.error.unexpectedArgument = Unexpected argument '{0}'
command.error.invalidTimePeriod = Invalid time period '{0}'
command.error.invalidEnum = Invalid enum option '{0}'
command.error.invalidNumber = Invalid number '{0}'
command.error.invalidPage = There is no page {0}. Pages run from 1 to {1}.
command.error.emptyResult = Empty result

View File

@ -248,5 +248,26 @@ item.locked = This item cannot be removed from its slot
stats.hotbar = {0} kills ({1} streak) {2} deaths {3} K/D
announce.online = Announced server as online
announce.offline = Announced server as offline
blitz.countdown = Blitz mode will activate in {0}
blitz.activated = Blitz mode
blitz.active = Blitz mode is already enabled
blitz.queued = Blitz mode is already queued to activate
lives.change.gained.singular = You gained {0} more life
lives.change.gained.plural = You gained {0} more lives
lives.change.lost.singular = You lost {0} life
lives.change.lost.plural = You lost {0} lives
lives.remaining.individual.singular = You have {0} life left
lives.remaining.individual.plural = You have {0} lives left
lives.remaining.team.singular = Your team has {0} life left
lives.remaining.team.plural = Your team has {0} lives left
lives.status.eliminated = eliminated
lives.status.alive = {0} alive
lives.status.lives = {0} lives

View File

@ -230,12 +230,6 @@ match.score.scorebox.individual = {0} scored {1}
points.singularCompound = {0} point
points.pluralCompound = {0} points
# {0} = singular / plural substitution
match.blitz.livesRemaining.message = You have {0} remaining.
match.blitz.livesRemaining.singularLives = 1 life
# {0} = number of lives
match.blitz.livesRemaining.pluralLives = {0} lives
# {0} = time left in match
match.timeRemaining = Time Remaining: {0}

View File

@ -1,7 +1,6 @@
package tc.oc.pgm;
import tc.oc.commons.core.inject.HybridManifest;
import tc.oc.pgm.blitz.BlitzModule;
import tc.oc.pgm.blockdrops.BlockDropsModule;
import tc.oc.pgm.crafting.CraftingModule;
import tc.oc.pgm.eventrules.EventRuleModule;
@ -13,6 +12,7 @@ import tc.oc.pgm.goals.GoalModule;
import tc.oc.pgm.hunger.HungerModule;
import tc.oc.pgm.itemmeta.ItemModifyModule;
import tc.oc.pgm.killreward.KillRewardModule;
import tc.oc.pgm.blitz.BlitzModule;
import tc.oc.pgm.map.MapModuleFactory;
import tc.oc.pgm.map.StaticMethodMapModuleFactory;
import tc.oc.pgm.modules.DiscardPotionBottlesModule;
@ -55,6 +55,7 @@ public class MapModulesManifest extends HybridManifest {
install(new ProjectileModule.Factory());
install(new SpawnModule.Factory());
install(new TimeLimitModule.Factory());
install(new BlitzModule.Factory());
// MapModules with static parse methods
install(new StaticMethodMapModuleFactory<EventRuleModule>(){});
@ -68,7 +69,6 @@ public class MapModulesManifest extends HybridManifest {
install(new StaticMethodMapModuleFactory<ModifyBowProjectileModule>(){});
install(new StaticMethodMapModuleFactory<MobsModule>(){});
install(new StaticMethodMapModuleFactory<HungerModule>(){});
install(new StaticMethodMapModuleFactory<BlitzModule>(){});
install(new StaticMethodMapModuleFactory<KillRewardModule>(){});
install(new StaticMethodMapModuleFactory<GhostSquadronModule>(){});
install(new StaticMethodMapModuleFactory<RageModule>(){});

View File

@ -12,6 +12,7 @@ import tc.oc.pgm.flag.FlagManifest;
import tc.oc.pgm.itemkeep.ItemKeepManifest;
import tc.oc.pgm.kits.KitManifest;
import tc.oc.pgm.lane.LaneManifest;
import tc.oc.pgm.blitz.BlitzManifest;
import tc.oc.pgm.loot.LootManifest;
import tc.oc.pgm.modes.ObjectiveModeManifest;
import tc.oc.pgm.physics.PlayerPhysicsManifest;
@ -62,5 +63,6 @@ public class PGMModulesManifest extends HybridManifest {
install(new StatsManifest());
install(new RaindropManifest());
install(new ObjectiveModeManifest());
install(new BlitzManifest());
}
}

View File

@ -19,9 +19,9 @@ import tc.oc.api.docs.virtual.Model;
import tc.oc.api.docs.virtual.ServerDoc;
import tc.oc.commons.core.stream.Collectors;
import tc.oc.commons.core.util.Streams;
import tc.oc.pgm.blitz.BlitzMatchModule;
import tc.oc.pgm.goals.GoalMatchModule;
import tc.oc.pgm.join.JoinMatchModule;
import tc.oc.pgm.blitz.BlitzMatchModule;
import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchState;
@ -36,10 +36,10 @@ public class MatchDocument extends AbstractModel implements MatchDoc {
private final VictoryMatchModule victory;
private final Optional<MutationMatchModule> mutations;
private final Optional<GoalMatchModule> goals;
private final Optional<BlitzMatchModule> blitz;
private final BlitzMatchModule blitz;
private final Optional<JoinMatchModule> join;
@Inject MatchDocument(ServerDoc.Identity localServer, MapDoc map, Match match, VictoryMatchModule victory, Optional<MutationMatchModule> mutations, Optional<GoalMatchModule> goals, Optional<BlitzMatchModule> blitz, Optional<JoinMatchModule> join) {
@Inject MatchDocument(ServerDoc.Identity localServer, MapDoc map, Match match, VictoryMatchModule victory, Optional<MutationMatchModule> mutations, Optional<GoalMatchModule> goals, BlitzMatchModule blitz, Optional<JoinMatchModule> join) {
this.match = match;
this.localServer = localServer;
this.map = map;
@ -87,7 +87,7 @@ public class MatchDocument extends AbstractModel implements MatchDoc {
@Override
public boolean join_mid_match() {
return !blitz.isPresent() && join.isPresent() && join.get().canJoinMid();
return !blitz.activated() && join.isPresent() && join.get().canJoinMid();
}
@Override public MapDoc map() {

View File

@ -18,6 +18,7 @@ import tc.oc.pgm.events.SetNextMapEvent;
import tc.oc.pgm.ffa.events.MatchResizeEvent;
import tc.oc.pgm.goals.events.GoalCompleteEvent;
import tc.oc.pgm.goals.events.GoalTouchEvent;
import tc.oc.pgm.blitz.BlitzEvent;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchManager;
import tc.oc.pgm.match.MatchModule;
@ -62,19 +63,25 @@ public class MatchPublishingMatchModule extends MatchModule implements Listener
private final MinecraftService minecraftService;
private final UpdateService<MatchDoc> matchService;
private final MatchDoc matchDocument;
private final BlitzMatchModule blitz;
private int initialParticipants; // Number of participants at match start (for blitz)
@Inject MatchPublishingMatchModule(Match match, MatchManager mm, MinecraftService minecraftService, UpdateService<MatchDoc> matchService, MatchDoc matchDocument) {
@Inject MatchPublishingMatchModule(Match match, MatchManager mm, MinecraftService minecraftService, UpdateService<MatchDoc> matchService, MatchDoc matchDocument, BlitzMatchModule blitz) {
super(match);
this.mm = mm;
this.minecraftService = minecraftService;
this.matchService = matchService;
this.matchDocument = matchDocument;
this.blitz = blitz;
}
public boolean isBlitz() {
return match.hasMatchModule(BlitzMatchModule.class);
return blitz.activated();
}
private void countPlayers() {
this.initialParticipants = getMatch().getParticipatingPlayers().size();
}
private void update() {
@ -90,7 +97,7 @@ public class MatchPublishingMatchModule extends MatchModule implements Listener
@Override
public void enable() {
super.enable();
this.initialParticipants = getMatch().getParticipatingPlayers().size();
countPlayers();
update();
}
@ -111,7 +118,7 @@ public class MatchPublishingMatchModule extends MatchModule implements Listener
@EventHandler(priority = EventPriority.MONITOR)
public void onPartyChange(final PlayerPartyChangeEvent event) {
if(!event.getMatch().hasStarted()) {
this.initialParticipants = event.getMatch().getParticipatingPlayers().size();
countPlayers();
}
update();
}
@ -122,4 +129,5 @@ public class MatchPublishingMatchModule extends MatchModule implements Listener
@EventHandler(priority = EventPriority.MONITOR) public void onTeamResize(TeamResizeEvent event) { update(); }
@EventHandler(priority = EventPriority.MONITOR) public void onGoalComplete(GoalCompleteEvent event) { update(); }
@EventHandler(priority = EventPriority.MONITOR) public void onGoalTouch(GoalTouchEvent event) { update(); }
@EventHandler(priority = EventPriority.MONITOR) public void onBlitzEnable(BlitzEvent event) { countPlayers(); update(); }
}

View File

@ -1,31 +0,0 @@
package tc.oc.pgm.blitz;
import static com.google.common.base.Preconditions.checkArgument;
/**
* Represents information needed to run the Blitz game type.
*/
public class BlitzConfig {
public BlitzConfig(int lives, boolean broadcastLives) {
checkArgument(lives > 0, "lives must be greater than zero");
this.lives = lives;
this.broadcastLives = broadcastLives;
}
/**
* Number of lives a player has during the match.
*
* @return Number of lives
*/
public int getNumLives() {
return this.lives;
}
public boolean getBroadcastLives() {
return this.broadcastLives;
}
final int lives;
final boolean broadcastLives;
}

View File

@ -0,0 +1,34 @@
package tc.oc.pgm.blitz;
import org.bukkit.event.HandlerList;
import tc.oc.pgm.events.MatchEvent;
import tc.oc.pgm.match.Match;
/**
* Called when {@link BlitzMatchModule} is enabled or disabled, even during a match.
*/
public class BlitzEvent extends MatchEvent {
private final BlitzMatchModule blitz;
public BlitzEvent(Match match, BlitzMatchModule blitz) {
super(match);
this.blitz = blitz;
}
public BlitzMatchModule blitz() {
return blitz;
}
private static final HandlerList handlers = new HandlerList();
public static HandlerList getHandlerList() {
return handlers;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
}

View File

@ -0,0 +1,17 @@
package tc.oc.pgm.blitz;
import tc.oc.commons.core.inject.HybridManifest;
import tc.oc.pgm.map.inject.MapBinders;
import tc.oc.pgm.match.inject.MatchBinders;
import tc.oc.pgm.match.inject.MatchModuleFixtureManifest;
public class BlitzManifest extends HybridManifest implements MapBinders, MatchBinders {
@Override
protected void configure() {
bindRootElementParser(BlitzProperties.class).to(BlitzParser.class);
bind(BlitzMatchModule.class).to(BlitzMatchModuleImpl.class);
install(new MatchModuleFixtureManifest<BlitzMatchModuleImpl>(){});
}
}

View File

@ -1,210 +1,93 @@
package tc.oc.pgm.blitz;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Nullable;
import com.google.api.client.util.Sets;
import com.google.common.collect.ImmutableSet;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.Effect;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.util.Vector;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.chat.Components;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.MatchPlayerDeathEvent;
import tc.oc.pgm.events.PartyAddEvent;
import tc.oc.pgm.events.PlayerLeavePartyEvent;
import tc.oc.pgm.join.JoinDenied;
import tc.oc.pgm.join.JoinHandler;
import tc.oc.pgm.join.JoinMatchModule;
import tc.oc.pgm.join.JoinMethod;
import tc.oc.pgm.join.JoinRequest;
import tc.oc.pgm.join.JoinResult;
import tc.oc.pgm.listeners.MatchAnnouncer;
import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.mutation.Mutation;
import tc.oc.pgm.mutation.MutationMatchModule;
import tc.oc.pgm.spawns.events.ParticipantReleaseEvent;
import tc.oc.pgm.victory.AbstractVictoryCondition;
import tc.oc.pgm.victory.VictoryCondition;
import tc.oc.pgm.victory.VictoryMatchModule;
@ListenerScope(MatchScope.LOADED)
public class BlitzMatchModule extends MatchModule implements Listener, JoinHandler {
import javax.annotation.Nullable;
import java.util.Optional;
final BlitzConfig config;
public final LifeManager lifeManager;
private final Set<UUID> eliminatedPlayers = Sets.newHashSet();
private int maxCompetitors; // Maximum number of non-empty Competitors that have been in the match at once
public interface BlitzMatchModule {
public class BlitzVictoryCondition extends AbstractVictoryCondition {
public BlitzVictoryCondition() {
super(Priority.BLITZ, new BlitzMatchResult());
}
/**
* Get the properties for the blitz module.
*
* It may change during a match from {@link #activate(BlitzProperties)}.
*/
BlitzProperties properties();
@Override public boolean isCompleted() {
// At least one competitor must be eliminated before the match can end.
// This allows maps to be tested with one or zero competitors present.
final int count = remainingCompetitors();
return count <= 1 && count < maxCompetitors;
}
/**
* Is the blitz module *really* activated?
*
* Since the module is always loaded on the chance it should
* be activated during a match, it is only actively enforcing
* its rules when this returns true.
*/
boolean activated();
/**
* Activate the blitz module with a new set of properties.
*
* If the properties are null, it will default to its current
* {@link #properties()}.
*/
void activate(@Nullable BlitzProperties properties);
default void activate() {
activate(null);
}
final VictoryCondition victoryCondition = new BlitzVictoryCondition();
/**
* Deactivate the blitz module by clearing all of its data.
*
* The module can be activated and deactivated as many times
* as you need.
*/
void deactivate();
public BlitzMatchModule(Match match, BlitzConfig config) {
super(match);
this.config = match.module(MutationMatchModule.class).get().enabled(Mutation.BLITZ) ? new BlitzConfig(1, true) : config;
this.lifeManager = new LifeManager(this.config.getNumLives());
}
/**
* Increment the number of lives for a player if
* {@link #eliminated(MatchPlayer)} is false.
*
* If notify is true, the player will get a message
* explaining how much lives they gained or lost.
*
* If immediate is true and the player's lives are empty,
* {@link #eliminate(MatchPlayer)} will be called and the
* player will be out of the game.
*
* @return Whether the player is now {@link #eliminated(MatchPlayer)}.
*/
boolean increment(MatchPlayer player, int lives, boolean notify, boolean immediate);
@Override
public boolean shouldLoad() {
return super.shouldLoad() && config.lives != Integer.MAX_VALUE;
}
/**
* Is the player eliminated from the match?
*
* This value can change back to false if the player were
* forced onto another team after being eliminated.
*/
boolean eliminated(MatchPlayer player);
@Override
public void load() {
super.load();
match.needMatchModule(JoinMatchModule.class).registerHandler(this);
match.needMatchModule(VictoryMatchModule.class).setVictoryCondition(victoryCondition);
}
/**
* Eliminate the player from this match by moving
* them to the default party and preventing them from respawning.
*/
void eliminate(MatchPlayer player);
@Override
public void enable() {
super.enable();
updateMaxCompetitors();
}
/**
* Try to get the lives for this player.
*/
Optional<Lives> lives(MatchPlayer player);
private int remainingCompetitors() {
return (int) match.getCompetitors()
.stream()
.filter(c -> !c.getPlayers().isEmpty())
.count();
}
/**
* Get the amount of lives a player has left.
*
* @throws IllegalStateException if {@link #lives(MatchPlayer)} is not present.
*/
int livesCount(MatchPlayer player);
@EventHandler
public void onPartyAdd(PartyAddEvent event) {
if(event.getParty() instanceof Competitor) {
updateMaxCompetitors();
}
}
private void updateMaxCompetitors() {
maxCompetitors = Math.max(maxCompetitors, remainingCompetitors());
}
public BlitzConfig getConfig() {
return this.config;
}
/** Whether or not the player participated in the match and was eliminated. */
public boolean isPlayerEliminated(UUID player) {
return this.eliminatedPlayers.contains(player);
}
public int getRemainingPlayers(Competitor competitor) {
// TODO: this becomes a bit more complex when eliminated players are not forced to observers
return competitor.getPlayers().size();
}
@Override
public @Nullable JoinResult queryJoin(MatchPlayer joining, JoinRequest request) {
if(getMatch().hasStarted() && request.method() != JoinMethod.FORCE) {
// This message should NOT look like an error, because remotely joining players will see it often.
// It also should not say "Blitz" because not all maps that use this module want to be labelled "Blitz".
return JoinDenied.friendly("command.gameplay.join.matchStarted");
}
return null;
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void handleDeath(final MatchPlayerDeathEvent event) {
MatchPlayer victim = event.getVictim();
if(victim.getParty() instanceof Competitor) {
int lives = this.lifeManager.addLives(event.getVictim().getPlayerId(), -1);
if(lives <= 0) {
this.handleElimination(victim);
}
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void handleLeave(final PlayerLeavePartyEvent event) {
if(!match.isRunning()) return;
int lives = this.lifeManager.getLives(event.getPlayer().getPlayerId());
if (event.getOldParty() instanceof Competitor && lives > 0) {
// Player switching teams, check if match needs to end
if (event.getNewParty() instanceof Competitor) checkEnd();
// Player is going to obs, eliminate it
else handleElimination(event.getPlayer());
}
}
@EventHandler
public void handleSpawn(final ParticipantReleaseEvent event) {
if(this.config.broadcastLives) {
int lives = this.lifeManager.getLives(event.getPlayer().getPlayerId());
event.getPlayer().showTitle(
// Fake the "Go!" title at match start
event.wasFrozen() ? MatchAnnouncer.GO : Components.blank(),
new Component(
new TranslatableComponent(
"match.blitz.livesRemaining.message",
new Component(
new TranslatableComponent(
lives == 1 ? "match.blitz.livesRemaining.singularLives"
: "match.blitz.livesRemaining.pluralLives",
Integer.toString(lives)
),
ChatColor.AQUA
)
),
ChatColor.RED
),
0, 60, 20
);
}
}
private void handleElimination(final MatchPlayer player) {
if (!eliminatedPlayers.add(player.getBukkit().getUniqueId())) return;
World world = player.getMatch().getWorld();
Location death = player.getBukkit().getLocation();
double radius = 0.1;
int n = 8;
for(int i = 0; i < 6; i++) {
double angle = 2 * Math.PI * i / n;
Location base = death.clone().add(new Vector(radius * Math.cos(angle), 0, radius * Math.sin(angle)));
for(int j = 0; j <= 8; j++) {
world.playEffect(base, Effect.SMOKE, j);
}
}
checkEnd();
}
private void checkEnd() {
// Process eliminations within the same tick simultaneously, so that ties are properly detected
getMatch().getScheduler(MatchScope.RUNNING).debounceTask(() -> {
ImmutableSet.copyOf(getMatch().getParticipatingPlayers())
.stream()
.filter(participating -> eliminatedPlayers.contains(participating.getBukkit().getUniqueId()))
.forEach(participating -> match.setPlayerParty(participating, match.getDefaultParty()));
match.needMatchModule(VictoryMatchModule.class).invalidateAndCheckEnd();
});
}
/**
* Try to get the team lives for this competitor.
*/
Optional<Lives> lives(Competitor competitor);
}

View File

@ -0,0 +1,283 @@
package tc.oc.pgm.blitz;
import com.google.common.collect.ImmutableSet;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.Particle;
import org.bukkit.World;
import org.bukkit.event.EventException;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import tc.oc.api.docs.PlayerId;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.chat.Components;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.MatchPlayerDeathEvent;
import tc.oc.pgm.events.PartyAddEvent;
import tc.oc.pgm.events.PlayerChangePartyEvent;
import tc.oc.pgm.join.JoinDenied;
import tc.oc.pgm.join.JoinHandler;
import tc.oc.pgm.join.JoinMatchModule;
import tc.oc.pgm.join.JoinMethod;
import tc.oc.pgm.join.JoinRequest;
import tc.oc.pgm.join.JoinResult;
import tc.oc.pgm.listeners.MatchAnnouncer;
import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.spawns.events.ParticipantReleaseEvent;
import tc.oc.pgm.teams.TeamMatchModule;
import tc.oc.pgm.victory.VictoryMatchModule;
import javax.annotation.Nullable;
import javax.inject.Inject;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
@ListenerScope(MatchScope.LOADED)
public class BlitzMatchModuleImpl extends MatchModule implements BlitzMatchModule, Listener, JoinHandler {
private final Match match;
private final World world;
private final JoinMatchModule join;
private final VictoryMatchModule victory;
private final Optional<TeamMatchModule> teams;
private BlitzProperties properties;
private final Set<Lives> lives = new HashSet<>();
private final Set<PlayerId> eliminated = new HashSet<>();
private boolean activated = false;
private int competitors = 0;
@Inject BlitzMatchModuleImpl(Match match, World world, JoinMatchModule join, VictoryMatchModule victory, Optional<TeamMatchModule> teams, BlitzProperties properties) {
this.match = match;
this.world = world;
this.join = join;
this.victory = victory;
this.teams = teams;
this.properties = properties;
}
private void preload() {
if(!properties().empty()) {
activate();
}
}
protected int competitors() {
return competitors;
}
protected int remainingCompetitors() {
return (int) match.getCompetitors()
.stream()
.filter(c -> !c.getPlayers().isEmpty())
.count();
}
private void updateCompetitors() {
competitors = Math.max(competitors(), remainingCompetitors());
}
private void setup(MatchPlayer player, boolean force) {
if(force) {
eliminated.remove(player.getPlayerId());
lives.removeIf(life -> life.owner(player.getPlayerId()));
}
switch(properties().type) {
case INDIVIDUAL:
properties().individuals.forEach((filter, count) -> {
if(filter.allows(player)) {
lives.add(new LivesIndividual(player, count));
}
}); break;
case TEAM:
properties().teams.forEach((teamFactory, count) -> {
if(teams.get().team(teamFactory).equals(player.getCompetitor())) {
lives.add(new LivesTeam(player.getCompetitor(), count));
}
}); break;
}
}
private void showLives(MatchPlayer player, boolean release, boolean activate) {
final Optional<Lives> lives = lives(player);
if(activated() && lives.isPresent()) {
player.showTitle(
release ? MatchAnnouncer.GO
: activate ? new Component(new TranslatableComponent("blitz.activated"), ChatColor.GREEN)
: Components.blank(),
lives.get().remaining(),
0, 60, 20
);
}
}
@Override
public boolean activated() {
return activated;
}
@Override
public void activate(@Nullable BlitzProperties newProperties) {
if(!activated) {
activated = true;
if(newProperties != null) {
properties = newProperties;
}
load();
if(match.hasStarted()) {
enable();
}
}
}
@Override
public void deactivate() {
activated = false;
lives.clear();
eliminated.clear();
}
@Override
public void load() {
if(activated()) {
join.registerHandler(this);
victory.setVictoryCondition(new BlitzVictoryCondition(this));
} else {
preload();
}
}
@Override
public void enable() {
if(activated()) {
updateCompetitors();
match.participants().forEach(player -> {
setup(player, false);
if(match.hasStarted()) {
showLives(player, false, true);
}
});
match.callEvent(new BlitzEvent(match, this));
}
}
@Override
public BlitzProperties properties() {
return properties;
}
@Override
public boolean increment(MatchPlayer player, int lives, boolean notify, boolean immediate) {
if(!eliminated(player)) {
return lives(player).map(life -> {
life.add(player.getPlayerId(), lives);
if(notify) {
player.showTitle(Components.blank(), life.change(lives), 0, 40, 10);
}
if(life.empty() && immediate) {
eliminate(player);
return true;
}
return false;
}).orElse(false);
}
return true;
}
@Override
public int livesCount(MatchPlayer player) {
return lives(player).map(Lives::current)
.orElseThrow(() -> new IllegalStateException(player + " has no lives present to count"));
}
@Override
public Optional<Lives> lives(MatchPlayer player) {
return lives.stream()
.filter(lives -> lives.applicableTo(player.getPlayerId()))
.findFirst();
}
@Override
public Optional<Lives> lives(Competitor competitor) {
return lives.stream()
.filter(lives -> lives.type().equals(Lives.Type.TEAM) && lives.competitor().equals(competitor))
.findFirst();
}
@Override
public boolean eliminated(MatchPlayer player) {
return eliminated.contains(player.getPlayerId());
}
@Override
public void eliminate(MatchPlayer player) {
if(activated() && !eliminated(player)) {
eliminated.add(player.getPlayerId());
// Process eliminations within the same tick simultaneously, so that ties are properly detected
match.getScheduler(MatchScope.RUNNING).debounceTask(() -> {
ImmutableSet.copyOf(getMatch().getParticipatingPlayers())
.stream()
.filter(this::eliminated)
.forEach(participating -> {
match.setPlayerParty(participating, match.getDefaultParty());
world.spawnParticle(Particle.SMOKE_LARGE, player.getLocation(), 5);
});
victory.invalidateAndCheckEnd();
});
}
}
@Override
public JoinResult queryJoin(MatchPlayer joining, JoinRequest request) {
if(activated() &&
match.hasStarted() &&
!EnumSet.of(JoinMethod.FORCE, JoinMethod.REMOTE).contains(request.method())) {
// This message should NOT look like an error, because remotely joining players will see it often.
// It also should not say "Blitz" because not all maps that use this module want to be labelled "Blitz".
return JoinDenied.friendly("command.gameplay.join.matchStarted");
}
return null;
}
@EventHandler
public void onPartyAdd(PartyAddEvent event) {
if(event.getParty() instanceof Competitor) {
updateCompetitors();
}
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onPartyChange(PlayerChangePartyEvent event) throws EventException {
final MatchPlayer player = event.getPlayer();
if(event.getNewParty() == null) {
if(event.getOldParty() instanceof Competitor && match.hasStarted() && !increment(player, -1, false, true)) {
eliminate(player);
}
} else if(event.getNewParty() instanceof Competitor) {
event.yield();
setup(player, true);
updateCompetitors();
}
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onDeath(MatchPlayerDeathEvent event) {
final MatchPlayer player = event.getVictim();
if(player.competitor().isPresent()) {
increment(player, -1, false, true);
}
}
@EventHandler
public void onRelease(ParticipantReleaseEvent event) {
showLives(event.getPlayer(), event.wasFrozen(), false);
}
}

View File

@ -6,6 +6,7 @@ import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.victory.MatchResult;
public class BlitzMatchResult implements MatchResult {
@Override
public int compare(Competitor a, Competitor b) {
return Integer.compare(b.getPlayers().size(), a.getPlayers().size());
@ -15,4 +16,5 @@ public class BlitzMatchResult implements MatchResult {
public BaseComponent describeResult() {
return new Component("most survivors");
}
}

View File

@ -1,85 +1,60 @@
package tc.oc.pgm.blitz;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import com.google.common.collect.Range;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.jdom2.Document;
import org.jdom2.Element;
import tc.oc.api.docs.virtual.MapDoc;
import tc.oc.pgm.ffa.FreeForAllModule;
import tc.oc.pgm.map.MapModule;
import tc.oc.pgm.map.MapModuleContext;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchModuleFactory;
import tc.oc.pgm.module.ModuleDescription;
import tc.oc.pgm.mutation.MutationMapModule;
import tc.oc.pgm.mutation.MutationMatchModule;
import tc.oc.pgm.teams.TeamModule;
import tc.oc.pgm.utils.XMLUtils;
import tc.oc.pgm.map.MapModuleFactory;
import tc.oc.pgm.xml.InvalidXMLException;
import tc.oc.pgm.xml.Node;
import static com.google.common.base.Preconditions.checkNotNull;
import javax.inject.Inject;
import javax.inject.Provider;
public class BlitzModule implements MapModule {
@ModuleDescription(name = "Blitz", follows = MutationMapModule.class)
public class BlitzModule implements MapModule, MatchModuleFactory<BlitzMatchModule> {
final BlitzConfig config;
private final BlitzProperties properties;
public BlitzModule(BlitzConfig config) {
this.config = checkNotNull(config);
public BlitzModule(BlitzProperties properties) {
this.properties = properties;
}
public boolean active() {
return !properties.empty();
}
@Override
public Set<MapDoc.Gamemode> getGamemodes(MapModuleContext context) {
return isEnabled() ? Collections.singleton(MapDoc.Gamemode.blitz) : Collections.emptySet();
return active() ? Collections.singleton(MapDoc.Gamemode.blitz) : Collections.emptySet();
}
@Override
public BaseComponent getGameName(MapModuleContext context) {
if(!isEnabled()) return null;
if (context.hasModule(TeamModule.class)) {
if(!active()) {
return null;
} else if(!properties.multipleLives()) {
return new TranslatableComponent("match.scoreboard.playersRemaining.title");
} else if (context.hasModule(FreeForAllModule.class) && config.getNumLives() > 1) {
} else if(properties.teams.isEmpty()) {
return new TranslatableComponent("match.scoreboard.livesRemaining.title");
} else {
return new TranslatableComponent("match.scoreboard.blitz.title");
}
}
@Override
public BlitzMatchModule createMatchModule(Match match) {
return new BlitzMatchModule(match, this.config);
}
public static class Factory extends MapModuleFactory<BlitzModule> {
/**
* In order to support {@link MutationMatchModule}, this module
* will always create a {@link BlitzMatchModule}. However, if the lives are set to
* {@link Integer#MAX_VALUE}, then it will fail to load on {@link BlitzMatchModule#shouldLoad()}.
*/
public boolean isEnabled() {
return config.lives != Integer.MAX_VALUE;
}
@Inject Provider<BlitzProperties> propertiesProvider;
// ---------------------
// ---- XML Parsing ----
// ---------------------
public static BlitzModule parse(MapModuleContext context, Logger logger, Document doc) throws InvalidXMLException {
List<Element> blitzElements = doc.getRootElement().getChildren("blitz");
BlitzConfig config = new BlitzConfig(Integer.MAX_VALUE, false);
for(Element blitzEl : blitzElements) {
boolean broadcastLives = XMLUtils.parseBoolean(blitzEl.getChild("broadcastLives"), true);
int lives = XMLUtils.parseNumber(Node.fromChildOrAttr(blitzEl, "lives"), Integer.class, Range.atLeast(1), 1);
config = new BlitzConfig(lives, broadcastLives);
@Override
public BlitzModule parse(MapModuleContext context, Logger logger, Document doc) throws InvalidXMLException {
return new BlitzModule(propertiesProvider.get());
}
return new BlitzModule(config);
}
}

View File

@ -0,0 +1,72 @@
package tc.oc.pgm.blitz;
import com.google.common.collect.Range;
import com.google.inject.Provider;
import org.jdom2.Element;
import tc.oc.pgm.filters.Filter;
import tc.oc.pgm.filters.matcher.StaticFilter;
import tc.oc.pgm.filters.parser.FilterParser;
import tc.oc.pgm.teams.TeamFactory;
import tc.oc.pgm.utils.XMLUtils;
import tc.oc.pgm.xml.InvalidXMLException;
import tc.oc.pgm.xml.Node;
import tc.oc.pgm.xml.parser.ElementParser;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import static tc.oc.pgm.blitz.BlitzProperties.*;
public class BlitzParser implements ElementParser<BlitzProperties> {
private final FilterParser filters;
private final Provider<List<TeamFactory>> factories;
@Inject private BlitzParser(FilterParser filters, Provider<List<TeamFactory>> factories) {
this.filters = filters;
this.factories = factories;
}
@Override
public BlitzProperties parseElement(Element element) throws InvalidXMLException {
boolean broadcast = true;
int global = -1;
Map<Filter, Integer> individuals = new HashMap<>();
Map<TeamFactory, Integer> teams = factories.get().stream()
.filter(team -> team.getLives().isPresent())
.collect(Collectors.toMap(Function.identity(), team -> team.getLives().get()));
for(Element el : XMLUtils.getChildren(element, "blitz")) {
broadcast = XMLUtils.parseBoolean(Node.fromChildOrAttr(el, "broadcast", "broadcastLives"), broadcast);
global = XMLUtils.parseNumber(Node.fromChildOrAttr(el, "lives"), Integer.class, Range.atLeast(1), global);
if(global != -1) {
individuals.put(StaticFilter.ALLOW, global);
} else {
for(Element e : XMLUtils.getChildren(el, "rule")) {
individuals.put(
filters.parse(Node.fromChildOrAttr(e, "filter")),
XMLUtils.parseNumber(Node.fromChildOrAttr(e, "lives"), Integer.class, Range.atLeast(1), 1)
);
}
}
}
if(!individuals.isEmpty() && teams.isEmpty()) {
return individuals(individuals, broadcast);
} else if(individuals.isEmpty() && !teams.isEmpty()) {
return teams(teams, broadcast);
} else if(!individuals.isEmpty() && !teams.isEmpty()) {
throw new InvalidXMLException("Cannot define both team respawns and blitz");
} else {
return none();
}
}
}

View File

@ -0,0 +1,64 @@
package tc.oc.pgm.blitz;
import tc.oc.commons.core.IterableUtils;
import tc.oc.commons.core.util.MapUtils;
import tc.oc.pgm.filters.Filter;
import tc.oc.pgm.filters.matcher.StaticFilter;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.teams.Team;
import tc.oc.pgm.teams.TeamFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public class BlitzProperties {
public final Map<TeamFactory, Integer> teams;
public final Map<Filter, Integer> individuals;
public final Lives.Type type;
public final boolean broadcast;
private final boolean multi;
private final boolean empty;
public BlitzProperties(Map<TeamFactory, Integer> teams, Map<Filter, Integer> individuals, Lives.Type type, boolean broadcast) {
this.teams = teams;
this.individuals = individuals;
this.type = type;
this.broadcast = broadcast;
this.multi = IterableUtils.any(IterableUtils.concat(teams.values(), individuals.values()), i -> i != 1);
this.empty = teams.isEmpty() && individuals.isEmpty();
}
public static BlitzProperties none() {
return new BlitzProperties(new HashMap<>(), new HashMap<>(), Lives.Type.INDIVIDUAL, false);
}
public static BlitzProperties individuals(Map<Filter, Integer> individuals, boolean broadcast) {
return new BlitzProperties(new HashMap<>(), individuals, Lives.Type.INDIVIDUAL, broadcast);
}
public static BlitzProperties teams(Map<TeamFactory, Integer> teams, boolean broadcast) {
return new BlitzProperties(teams, new HashMap<>(), Lives.Type.TEAM, broadcast);
}
public static BlitzProperties create(Match match, int lives, Lives.Type type) {
return new BlitzProperties(
match.competitors().filter(c -> c instanceof Team).map(c -> ((Team) c).getDefinition()).collect(Collectors.toMap(Function.identity(), c -> lives)),
MapUtils.merge(new HashMap<>(), StaticFilter.ALLOW, lives),
type,
true
);
}
public boolean multipleLives() {
return multi;
}
public boolean empty() {
return empty;
}
}

View File

@ -0,0 +1,22 @@
package tc.oc.pgm.blitz;
import tc.oc.pgm.victory.AbstractVictoryCondition;
public class BlitzVictoryCondition extends AbstractVictoryCondition {
private final BlitzMatchModuleImpl blitz;
protected BlitzVictoryCondition(BlitzMatchModuleImpl blitz) {
super(Priority.BLITZ, new BlitzMatchResult());
this.blitz = blitz;
}
@Override
public boolean isCompleted() {
// At least one competitor must be eliminated before the match can end.
// This allows maps to be tested with one or zero competitors present.
final int count = blitz.remainingCompetitors();
return blitz.activated() && count <= 1 && count < blitz.competitors();
}
}

View File

@ -1,45 +0,0 @@
package tc.oc.pgm.blitz;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.util.Map;
import com.google.common.collect.Maps;
import tc.oc.api.docs.PlayerId;
public class LifeManager {
final int lives;
final Map<PlayerId, Integer> livesLeft = Maps.newHashMap();
public LifeManager(int lives) {
checkArgument(lives > 0, "lives must be greater than zero");
this.lives = lives;
}
public int getLives() {
return this.lives;
}
public int getLives(PlayerId player) {
checkNotNull(player, "player id");
Integer livesLeft = this.livesLeft.get(player);
if(livesLeft != null) {
return livesLeft;
} else {
return this.lives;
}
}
public int addLives(PlayerId player, int dlives) {
checkNotNull(player, "player id");
int lives = Math.max(0, this.getLives(player) + dlives);
this.livesLeft.put(player, lives);
return lives;
}
}

View File

@ -0,0 +1,73 @@
package tc.oc.pgm.blitz;
import net.md_5.bungee.api.chat.BaseComponent;
import tc.oc.api.docs.PlayerId;
import tc.oc.pgm.match.Competitor;
import javax.annotation.Nullable;
public interface Lives {
/**
* Original amount of lives.
*/
int original();
/**
* Current amount of lives (may be larger than {@link #original()},
* due to players getting addition lives via kits).
*/
int current();
/**
* Add more to the current lives and include the player that
* caused this change if applicable.
*/
void add(@Nullable PlayerId cause, int delta);
/**
* Get the delta number of life changes this player has caused.
*/
int changesBy(PlayerId player);
/**
* Are the amount of lives reduced when this player dies?
*/
boolean applicableTo(PlayerId player);
/**
* Is this player the sole owner of these lives?
*/
boolean owner(PlayerId playerId);
/**
* Are there no lives left?
*/
boolean empty();
/**
* Get the competitor relation of these lives.
*/
Competitor competitor();
/**
* Message sent to players notifying them how many lives they have left.
*/
BaseComponent remaining();
/**
* Sidebar status of how many respawns a competitor has left.
*/
BaseComponent status();
/**
* Message sent when a player gains or loses lives.
*/
BaseComponent change(int delta);
/**
* Implementations of lives as an enum.
*/
Type type(); enum Type { TEAM, INDIVIDUAL }
}

View File

@ -0,0 +1,113 @@
package tc.oc.pgm.blitz;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import tc.oc.api.docs.PlayerId;
import tc.oc.commons.core.chat.Component;
import tc.oc.pgm.match.Competitor;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
public abstract class LivesBase implements Lives {
private final Map<PlayerId, Integer> deltas;
private final Competitor competitor;
private final int original;
private int current;
public LivesBase(int lives, Competitor competitor) {
this.deltas = new HashMap<>();
this.competitor = competitor;
this.original = lives;
this.current = lives;
update();
}
private void update() {
competitor().getMatch().callEvent(new LivesEvent(this));
}
@Override
public Competitor competitor() {
return competitor;
}
@Override
public int original() {
return original;
}
@Override
public int current() {
return current;
}
@Override
public boolean empty() {
return current() <= 0;
}
@Override
public void add(@Nullable PlayerId cause, int delta) {
current = Math.max(0, current() + delta);
deltas.put(cause, changesBy(cause) + delta);
update();
}
@Override
public int changesBy(PlayerId player) {
return deltas.getOrDefault(player, 0);
}
@Override
public BaseComponent remaining() {
return new Component(
new TranslatableComponent(
"lives.remaining." + type().name().toLowerCase() + "." + (current() == 1 ? "singular" : "plural"),
new Component(current(), ChatColor.YELLOW)
),
ChatColor.AQUA
);
}
@Override
public BaseComponent status() {
int alive = (int) competitor().players().count();
return new Component(
Stream.of(
new Component("("),
new TranslatableComponent(
empty() ? alive == 0 ? "lives.status.eliminated"
: "lives.status.alive"
: "lives.status.lives",
new Component(
empty() ? alive : current(),
ChatColor.WHITE
)
),
new Component(")")
),
ChatColor.GRAY,
ChatColor.ITALIC
);
}
@Override
public BaseComponent change(int delta) {
int absDelta = Math.abs(delta);
return new Component(
new TranslatableComponent(
(delta > 0 ? "lives.change.gained."
: "lives.change.lost.") + (absDelta == 1 ? "singular"
: "plural"),
new Component(absDelta, ChatColor.AQUA)
),
ChatColor.WHITE
);
}
}

View File

@ -0,0 +1,32 @@
package tc.oc.pgm.blitz;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
/**
* Called when the amount of {@link Lives} changes.
*/
public class LivesEvent extends Event {
private final Lives lives;
public LivesEvent(Lives lives) {
this.lives = lives;
}
public Lives lives() {
return lives;
}
private static final HandlerList handlers = new HandlerList();
public static HandlerList getHandlerList() {
return handlers;
}
@Override
public HandlerList getHandlers() {
return handlers;
}
}

View File

@ -0,0 +1,46 @@
package tc.oc.pgm.blitz;
import tc.oc.api.docs.PlayerId;
import tc.oc.pgm.match.MatchPlayer;
public class LivesIndividual extends LivesBase {
private final PlayerId player;
public LivesIndividual(MatchPlayer player, int lives) {
super(lives, player.getCompetitor());
this.player = player.getPlayerId();
}
public PlayerId player() {
return player;
}
@Override
public Type type() {
return Type.INDIVIDUAL;
}
@Override
public boolean applicableTo(PlayerId player) {
return player().equals(player);
}
@Override
public boolean owner(PlayerId playerId) {
return player().equals(playerId);
}
@Override
public int hashCode() {
return player().hashCode();
}
@Override
public boolean equals(Object obj) {
return obj != null &&
obj instanceof LivesIndividual &&
player().equals(((LivesIndividual) obj).player());
}
}

View File

@ -0,0 +1,20 @@
package tc.oc.pgm.blitz;
import tc.oc.pgm.kits.ItemKitApplicator;
import tc.oc.pgm.kits.Kit;
import tc.oc.pgm.match.MatchPlayer;
public class LivesKit extends Kit.Impl {
private final int lives;
public LivesKit(int lives) {
this.lives = lives;
}
@Override
public void apply(MatchPlayer player, boolean force, ItemKitApplicator items) {
player.getMatch().module(BlitzMatchModuleImpl.class).ifPresent(blitz -> blitz.increment(player, lives, true, force));
}
}

View File

@ -0,0 +1,39 @@
package tc.oc.pgm.blitz;
import tc.oc.api.docs.PlayerId;
import tc.oc.pgm.match.Competitor;
public class LivesTeam extends LivesBase {
public LivesTeam(Competitor competitor, int lives) {
super(lives, competitor);
}
@Override
public Type type() {
return Type.TEAM;
}
@Override
public boolean applicableTo(PlayerId player) {
return competitor().players().anyMatch(matchPlayer -> matchPlayer.getPlayerId().equals(player));
}
@Override
public boolean owner(PlayerId playerId) {
return false;
}
@Override
public int hashCode() {
return competitor().hashCode();
}
@Override
public boolean equals(Object obj) {
return obj != null &&
obj instanceof LivesTeam &&
competitor().equals(((LivesTeam) obj).competitor());
}
}

View File

@ -110,6 +110,10 @@ public abstract class CountdownContext implements Listener {
return countdowns().anyMatch(test);
}
public boolean anyRunning(Class<? extends Countdown> countdownClass) {
return anyRunning(countdown -> countdown.getClass().equals(countdownClass));
}
public void cancelAll() {
cancelAll(false);
}

View File

@ -9,9 +9,9 @@ import net.md_5.bungee.api.chat.TranslatableComponent;
import org.jdom2.Document;
import org.jdom2.Element;
import tc.oc.api.docs.virtual.MapDoc;
import tc.oc.pgm.blitz.BlitzModule;
import tc.oc.pgm.classes.ClassMatchModule;
import tc.oc.pgm.classes.ClassModule;
import tc.oc.pgm.blitz.BlitzModule;
import tc.oc.pgm.map.MapModule;
import tc.oc.pgm.map.MapModuleContext;
import tc.oc.pgm.match.Match;

View File

@ -38,17 +38,17 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.potion.PotionEffect;
import tc.oc.api.bukkit.users.Users;
import tc.oc.commons.bukkit.util.BukkitUtils;
import tc.oc.commons.core.commands.CommandBinder;
import tc.oc.pgm.PGMTranslations;
import tc.oc.pgm.blitz.BlitzMatchModule;
import tc.oc.pgm.blitz.BlitzMatchModuleImpl;
import tc.oc.pgm.doublejump.DoubleJumpMatchModule;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.ObserverInteractEvent;
import tc.oc.pgm.events.PlayerBlockTransformEvent;
import tc.oc.pgm.events.PlayerPartyChangeEvent;
import tc.oc.pgm.kits.WalkSpeedKit;
import tc.oc.pgm.blitz.BlitzMatchModule;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchScope;
@ -269,9 +269,9 @@ public class ViewInventoryMatchModule extends MatchModule implements Listener {
MatchPlayer matchHolder = this.match.getPlayer(holder);
if (matchHolder != null && matchHolder.isParticipating()) {
BlitzMatchModule module = matchHolder.getMatch().getMatchModule(BlitzMatchModule.class);
if (module != null) {
int livesLeft = module.lifeManager.getLives(Users.playerId(holder));
BlitzMatchModule module = matchHolder.getMatch().getMatchModule(BlitzMatchModuleImpl.class);
if(module != null) {
int livesLeft = module.livesCount(matchHolder);
ItemStack lives = new ItemStack(Material.EGG, livesLeft);
ItemMeta lifeMeta = lives.getItemMeta();
lifeMeta.addItemFlags(ItemFlag.values());

View File

@ -10,6 +10,7 @@ import org.bukkit.inventory.ItemStack;
import org.jdom2.Element;
import tc.oc.commons.bukkit.inventory.ArmorType;
import tc.oc.commons.bukkit.inventory.Slot;
import tc.oc.pgm.blitz.LivesKit;
import tc.oc.pgm.compose.CompositionParser;
import tc.oc.pgm.doublejump.DoubleJumpKit;
import tc.oc.pgm.features.FeatureDefinitionContext;
@ -254,4 +255,9 @@ public class KitDefinitionParser extends MagicMethodFeatureParser<Kit> implement
return new ForceKit(XMLUtils.parseVector(new Node(el)),
parseRelativeFlags(el));
}
@MethodParser
private Kit lives(Element el) throws InvalidXMLException {
return new LivesKit(XMLUtils.parseNumber(new Node(el), Integer.class, false, +1));
}
}

View File

@ -37,9 +37,9 @@ import tc.oc.commons.core.commands.CommandFutureCallback;
import tc.oc.commons.core.formatting.StringUtils;
import tc.oc.commons.core.util.Comparables;
import tc.oc.pgm.PGMTranslations;
import tc.oc.pgm.blitz.BlitzMatchModule;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.PlayerLeaveMatchEvent;
import tc.oc.pgm.blitz.BlitzMatchModule;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchExecutor;
import tc.oc.pgm.match.MatchModule;
@ -98,6 +98,7 @@ public class MapRatingsMatchModule extends MatchModule implements Listener {
@Inject private MapRatingsConfiguration config;
@Inject private MapService mapService;
@Inject private MatchExecutor matchExecutor;
@Inject private BlitzMatchModule blitz;
private final Map<MatchPlayer, Integer> playerRatings = new HashMap<>();
@ -140,10 +141,8 @@ public class MapRatingsMatchModule extends MatchModule implements Listener {
return PGMTranslations.t("noPermission", player);
}
BlitzMatchModule blitz = player.getMatch().getMatchModule(BlitzMatchModule.class);
if(Comparables.lessThan(player.getCumulativeParticipationTime(), MIN_PARTICIPATION_TIME) &&
!(blitz != null && blitz.isPlayerEliminated(player.getBukkit().getUniqueId())) &&
!(blitz.eliminated(player)) &&
!(this.getMatch().isFinished() && player.getCumulativeParticipationPercent() > MIN_PARTICIPATION_PERCENT)) {
return PGMTranslations.t("rating.lowParticipation", player);
}

View File

@ -23,6 +23,7 @@ import tc.oc.pgm.mutation.types.kit.MobsMutation;
import tc.oc.pgm.mutation.types.kit.PotionMutation;
import tc.oc.pgm.mutation.types.kit.ProjectileMutation;
import tc.oc.pgm.mutation.types.kit.StealthMutation;
import tc.oc.pgm.mutation.types.other.BlitzMutation;
import tc.oc.pgm.mutation.types.other.RageMutation;
import tc.oc.pgm.mutation.types.targetable.ApocalypseMutation;
import tc.oc.pgm.mutation.types.targetable.BomberMutation;
@ -32,7 +33,7 @@ import java.util.stream.Stream;
public enum Mutation {
BLITZ (null),
BLITZ (BlitzMutation.class),
RAGE (RageMutation.class),
HARDCORE (HardcoreMutation.class),
JUMP (JumpMutation.class),

View File

@ -0,0 +1,47 @@
package tc.oc.pgm.mutation.types.other;
import com.google.common.collect.Range;
import org.apache.commons.lang.math.Fraction;
import tc.oc.commons.core.random.RandomUtils;
import tc.oc.pgm.blitz.BlitzMatchModuleImpl;
import tc.oc.pgm.blitz.BlitzProperties;
import tc.oc.pgm.blitz.Lives;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchScope;
import tc.oc.pgm.mutation.types.MutationModule;
import tc.oc.pgm.teams.TeamMatchModule;
public class BlitzMutation extends MutationModule {
final static Range<Integer> LIVES = Range.closed(1, 3);
final static Fraction TEAM_CHANCE = Fraction.ONE_QUARTER;
public BlitzMutation(Match match) {
super(match);
}
@Override
public void enable() {
super.enable();
int lives = match.entropyForTick().randomInt(LIVES);
Lives.Type type;
if(match.module(TeamMatchModule.class).isPresent() && RandomUtils.nextBoolean(random, TEAM_CHANCE)) {
type = Lives.Type.TEAM;
lives *= match.module(TeamMatchModule.class).get().getFullestTeam().getSize();
} else {
type = Lives.Type.INDIVIDUAL;
}
match.module(BlitzMatchModuleImpl.class).get().activate(BlitzProperties.create(match, lives, type));
}
@Override
public void disable() {
match.getScheduler(MatchScope.LOADED).createTask(() -> {
if(!match.isFinished()) {
match.module(BlitzMatchModuleImpl.class).get().deactivate();
}
});
super.disable();
}
}

View File

@ -40,7 +40,7 @@ import tc.oc.commons.core.chat.ChatUtils;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.formatting.StringUtils;
import tc.oc.pgm.PGMTranslations;
import tc.oc.pgm.blitz.BlitzModule;
import tc.oc.pgm.blitz.BlitzEvent;
import tc.oc.pgm.classes.ClassMatchModule;
import tc.oc.pgm.classes.ClassModule;
import tc.oc.pgm.classes.PlayerClass;
@ -53,6 +53,7 @@ import tc.oc.pgm.events.PlayerPartyChangeEvent;
import tc.oc.pgm.join.JoinMatchModule;
import tc.oc.pgm.join.JoinRequest;
import tc.oc.pgm.join.JoinResult;
import tc.oc.pgm.blitz.BlitzMatchModule;
import tc.oc.pgm.match.MatchModule;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchScope;
@ -94,18 +95,18 @@ public class PickerMatchModule extends MatchModule implements Listener {
private final ComponentRenderContext renderer;
private final JoinMatchModule jmm;
private final BlitzMatchModule bmm;
private final boolean hasTeams;
private final boolean hasClasses;
private final boolean isBlitz;
private final Set<MatchPlayer> picking = new HashSet<>();
@Inject PickerMatchModule(ComponentRenderContext renderer, JoinMatchModule jmm, Optional<TeamModule> teamModule, Optional<ClassModule> classModule, Optional<BlitzModule> blitzModule) {
@Inject PickerMatchModule(ComponentRenderContext renderer, JoinMatchModule jmm, BlitzMatchModule bmm, Optional<TeamModule> teamModule, Optional<ClassModule> classModule) {
this.renderer = renderer;
this.jmm = jmm;
this.bmm = bmm;
this.hasTeams = teamModule.isPresent();
this.hasClasses = classModule.isPresent();
this.isBlitz = blitzModule.filter(BlitzModule::isEnabled).isPresent();
}
protected boolean settingEnabled(MatchPlayer player) {
@ -142,7 +143,7 @@ public class PickerMatchModule extends MatchModule implements Listener {
if(player == null) return false;
// Player is eliminated from Blitz
if(isBlitz && getMatch().isRunning()) return false;
if(bmm.activated() && getMatch().isRunning()) return false;
// Player is not observing or dead
if(!(player.isObserving() || player.isDead())) return false;
@ -346,6 +347,12 @@ public class PickerMatchModule extends MatchModule implements Listener {
refreshKitAll();
}
@EventHandler
public void blitzEnable(final BlitzEvent event) {
refreshCountsAll();
refreshKitAll();
}
/**
* Open the window for the given player, or refresh its contents
* if they already have it open, and return the current contents.

View File

@ -47,7 +47,7 @@ public class RageModule implements MapModule, MatchModuleFactory<RageMatchModule
public static RageModule parse(MapModuleContext context, Logger logger, Document doc) {
if(doc.getRootElement().getChild("rage") != null) {
return new RageModule(context.hasModule(BlitzModule.class));
return new RageModule(context.module(BlitzModule.class).filter(BlitzModule::active).isPresent());
} else {
return null;
}

View File

@ -18,11 +18,11 @@ import org.jdom2.Document;
import org.jdom2.Element;
import tc.oc.api.docs.virtual.MapDoc;
import tc.oc.commons.core.util.Optionals;
import tc.oc.pgm.blitz.BlitzModule;
import tc.oc.pgm.filters.Filter;
import tc.oc.pgm.filters.matcher.StaticFilter;
import tc.oc.pgm.filters.parser.FilterParser;
import tc.oc.pgm.goals.GoalModule;
import tc.oc.pgm.blitz.BlitzModule;
import tc.oc.pgm.map.MapModule;
import tc.oc.pgm.map.MapModuleContext;
import tc.oc.pgm.map.ProtoVersions;
@ -36,7 +36,7 @@ import tc.oc.pgm.utils.XMLUtils;
import tc.oc.pgm.xml.InvalidXMLException;
import tc.oc.pgm.xml.Node;
@ModuleDescription(name = "Score", follows = {GoalModule.class, BlitzModule.class })
@ModuleDescription(name = "Score", follows = { GoalModule.class, BlitzModule.class })
public class ScoreModule implements MapModule, MatchModuleFactory<ScoreMatchModule> {
private final ScoreConfig config;

View File

@ -31,7 +31,7 @@ import tc.oc.commons.bukkit.util.NullCommandSender;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.scheduler.Task;
import tc.oc.pgm.Config;
import tc.oc.pgm.blitz.BlitzMatchModule;
import tc.oc.pgm.blitz.LivesEvent;
import tc.oc.pgm.destroyable.Destroyable;
import tc.oc.pgm.events.FeatureChangeEvent;
import tc.oc.pgm.events.ListenerScope;
@ -50,6 +50,9 @@ import tc.oc.pgm.goals.events.GoalCompleteEvent;
import tc.oc.pgm.goals.events.GoalProximityChangeEvent;
import tc.oc.pgm.goals.events.GoalStatusChangeEvent;
import tc.oc.pgm.goals.events.GoalTouchEvent;
import tc.oc.pgm.blitz.Lives;
import tc.oc.pgm.blitz.BlitzEvent;
import tc.oc.pgm.blitz.BlitzMatchModule;
import tc.oc.pgm.match.Competitor;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchModule;
@ -72,6 +75,7 @@ public class SidebarMatchModule extends MatchModule implements Listener {
public static final int MAX_SUFFIX = 16; // Max chars in a team suffix
@Inject private List<MonumentWoolFactory> wools;
@Inject private BlitzMatchModule blitz;
private final String legacyTitle;
@ -172,8 +176,8 @@ public class SidebarMatchModule extends MatchModule implements Listener {
return getMatch().getMatchModule(ScoreMatchModule.class) != null;
}
private boolean isBlitz() {
return getMatch().getMatchModule(BlitzMatchModule.class) != null;
private boolean lives(Lives.Type type) {
return blitz.activated() && blitz.properties().type.equals(type);
}
private boolean isCompactWool() {
@ -290,6 +294,16 @@ public class SidebarMatchModule extends MatchModule implements Listener {
renderSidebarDebounce();
}
@EventHandler(priority = EventPriority.MONITOR)
public void blitzEnable(BlitzEvent event) {
renderSidebarDebounce();
}
@EventHandler(priority = EventPriority.MONITOR)
public void livesChange(LivesEvent event) {
renderSidebarDebounce();
}
private String renderGoal(Goal<?> goal, @Nullable Competitor competitor, Party viewingParty) {
StringBuilder sb = new StringBuilder(" ");
@ -325,11 +339,10 @@ public class SidebarMatchModule extends MatchModule implements Listener {
}
private String renderBlitz(Competitor competitor, Party viewingParty) {
BlitzMatchModule bmm = getMatch().needMatchModule(BlitzMatchModule.class);
if(competitor instanceof tc.oc.pgm.teams.Team) {
return ChatColor.WHITE.toString() + bmm.getRemainingPlayers(competitor);
} else if(competitor instanceof Tribute && bmm.getConfig().getNumLives() > 1) {
return ChatColor.WHITE.toString() + bmm.lifeManager.getLives(competitor.getPlayers().iterator().next().getPlayerId());
return ChatColor.WHITE.toString() + competitor.getPlayers().size();
} else if(competitor instanceof Tribute && blitz.properties().multipleLives()) {
return ChatColor.WHITE.toString() + blitz.livesCount(competitor.getPlayers().iterator().next());
} else {
return "";
}
@ -341,7 +354,8 @@ public class SidebarMatchModule extends MatchModule implements Listener {
private void renderSidebar() {
final boolean hasScores = hasScores();
final boolean isBlitz = isBlitz();
final boolean hasIndividualLives = lives(Lives.Type.INDIVIDUAL);
final boolean hasTeamLives = lives(Lives.Type.TEAM);
final GoalMatchModule gmm = match.needMatchModule(GoalMatchModule.class);
Set<Competitor> competitorsWithGoals = new HashSet<>();
@ -367,7 +381,7 @@ public class SidebarMatchModule extends MatchModule implements Listener {
List<String> rows = new ArrayList<>(MAX_ROWS);
// Scores/Blitz
if(hasScores || isBlitz) {
if(hasScores || hasIndividualLives || (hasTeamLives && competitorsWithGoals.isEmpty())) {
for(Competitor competitor : getMatch().needMatchModule(VictoryMatchModule.class).rankedCompetitors()) {
String text;
if(hasScores) {
@ -419,6 +433,11 @@ public class SidebarMatchModule extends MatchModule implements Listener {
rows.add(ComponentRenderers.toLegacyText(competitor.getStyledName(NameStyle.GAME),
NullCommandSender.INSTANCE));
// Add lives status under the team name
if(hasTeamLives) {
blitz.lives(competitor).ifPresent(l -> rows.add(ComponentRenderers.toLegacyText(l.status(), NullCommandSender.INSTANCE)));
}
if(isCompactWool()) {
String woolText = " ";
boolean firstWool = true;

View File

@ -77,6 +77,9 @@ public interface TeamFactory extends SluggedFeatureDefinition, Validatable, Feat
return org.bukkit.scoreboard.Team.OptionStatus.ALWAYS;
}
@Property(name="lives")
Optional<Integer> getLives();
MapDoc.Team getDocument();
@Override
@ -84,6 +87,9 @@ public interface TeamFactory extends SluggedFeatureDefinition, Validatable, Feat
if(getMaxOverfill().isPresent() && getMaxOverfill().get() < getMaxPlayers()) {
throw new InvalidXMLException("Max overfill cannot be less than max players");
}
if(getLives().isPresent() && getLives().get() <= 0) {
throw new InvalidXMLException("Lives must be at least 1");
}
}
}

View File

@ -26,10 +26,11 @@ import tc.oc.api.docs.Entrant;
import tc.oc.api.docs.Tournament;
import tc.oc.commons.bukkit.event.UserLoginEvent;
import tc.oc.commons.core.chat.Component;
import tc.oc.pgm.blitz.BlitzMatchModule;
import tc.oc.pgm.blitz.BlitzMatchModuleImpl;
import tc.oc.pgm.channels.ChannelMatchModule;
import tc.oc.pgm.events.MatchEndEvent;
import tc.oc.pgm.events.MatchPlayerAddEvent;
import tc.oc.pgm.blitz.BlitzMatchModule;
import tc.oc.pgm.match.Match;
import tc.oc.pgm.match.MatchPlayer;
import tc.oc.pgm.match.MatchState;
@ -120,7 +121,7 @@ public class TeamListener implements Listener {
TourneyState state = event.getNewState();
if(tourney.getKDMSession() == null && state.equals(TourneyState.ENABLED_WAITING_FOR_READY)) {
Match match = matchProvider.get();
if(match.getMatchModule(ScoreMatchModule.class) != null || match.getMatchModule(BlitzMatchModule.class) != null) {
if(match.getMatchModule(ScoreMatchModule.class) != null || match.module(BlitzMatchModuleImpl.class).filter(BlitzMatchModule::activated).isPresent()) {
tourney.createKDMSession();
}
}

View File

@ -179,6 +179,10 @@ public class StringUtils {
return builder.toString();
}
public static List<String> complete(String prefix, Class<? extends Enum> enumClass) {
return complete(prefix, Stream.of(enumClass.getEnumConstants()).map(c -> c.name().toLowerCase().replace('_', ' ')));
}
public static List<String> complete(String prefix, Collection<String> options) {
return complete(prefix, options.stream());
}