Skip to main content

Per-Competition Code

Now that you have an Arena class with functioning game logic, it's time to expand on that.

Creating Competition Classes

As mentioned in the Adding Game Logic page, you will need to create a custom Competition class. The following code below is how to create a custom Competition:

public class MyCompetition extends LiveCompetition<MyCompetition> {

    public MyCompetition(MyArena arena, CompetitionType type, ArenaMap map) {
        super(arena, type, map);
    }

}

As seen above, you will need to extend the LiveCompetition class, which is created for competitions that are live on the server BattleArena is running on. On it's own, this will do nothing, so the next step is to create a custom ArenaMap which is responsible for creating the competition.

public class MyCompetitionMap extends LiveCompetitionMap {
    public static final MapFactory FACTORY = MapyFactory.create(MyCompetitionMap.class, MyCompetitionMap::new);

    public MyCompetitionMap() {
    }

    public MyCompetitionMap(String name, Arena arena, MapType type, String world, @Nullable Bounds bounds, @Nullable Spawns spawns) {
        super(name, arena, type, world, bounds, spawns);
    }

    // Override this method in order to use your custom competition class
    @Override
    public LiveCompetition<?> createCompetition(Arena arena) {
        if (!(arena instanceof MyArena myArena)) {
            throw new IllegalArgumentException("Arena must be an instance of MyArena!");
        }
        
        return new MyCompetition(myArena, arena.getType(), this);
    }
}

As mentioned in an earlier segment of this documentation, maps can exist without necessarily having a competition bound to them, meaning they are responsible for actually creating a live competition. In the map class above, it can be seen the createCompetition method is overridden to instead create an instance of our MyCompetition class.

Additionally, a MapFactory is specified at the top of the class - this is important for the next step of linking this to your MyArena so BattleArena knows which ArenaMap (and therefore, Competition) to create for your Arena. It is also very important that both constructors are specified as seen in the example above.

And finally, the last step is to override the getMapFactory method in MyArena, and specify your factory like so:

public class MyArena extends Arena {
    @ArenaOption(name = "infection-time", description = "How long a player should be infected once hit.")
    private Duration infectionTime = Duration.ofSeconds(5);

    private final Set<UUID> infectedPlayers = new HashSet<>();

    @Override
    public MapFactory getMapFactory() {
        return MyCompetitionMap.FACTORY;
    }

    ...
  }

Per-Competition Code

Now that we have all the necessary classes created, you can now start creating code that will exist on a per-competition level. If we wanted to infect a random player every minute for instance to speed up the game, the following could be done like so:

public class MyCompetition extends LiveCompetition<MyCompetition> {

    private BukkitTask tickTask;

    public MyCompetition(MyArena arena, CompetitionType type, LiveCompetitionMap map) {
        super(arena, type, map);
    }

    public void startInfectTask() {
        this.tickTask = Bukkit.getScheduler().runTaskTimer(this.getArena().getPlugin(), this::infectPlayer, 0, 60 * 60 * 20);
    }

    public void stopInfectTask() {
        if (this.tickTask != null) {
            this.tickTask.cancel();
        }

        this.tickTask = null;
    }

    private void infectPlayer() {
        MyArena arena = (MyArena) this.getArena();

        // Infect a random player
        List<ArenaPlayer> uninfectedPlayers = this.getPlayers().stream().filter(player -> !arena.isInfected(player)).toList();
        if (uninfectedPlayers.isEmpty()) {
            return;
        }

        ArenaPlayer player = uninfectedPlayers.get((int) (Math.random() * uninfectedPlayers.size()));
        arena.infect(player.getPlayer());
    }
}

And with those changes, we also need to update MyArena to actually call the start and stop methods. Here is what the updated MyArena class would look like:

public class MyArena extends Arena {
    private static final String INFECTED_METADATA = "infected";

    @ArenaOption(name = "infection-time", description = "How long a player should be infected once hit.")
    private Duration infectionTime = Duration.ofSeconds(5);

    @Override
    public MapFactory getMapFactory() {
        return MyCompetitionMap.FACTORY;
    }

    @ArenaEventHandler
    public void onDamageEntity(EntityDamageByEntityEvent event) {
        if (event.getDamager() instanceof Player damager && event.getEntity() instanceof Player player) {
            // Player is not infected, let's infect them :)
            if (!player.hasMetadata(INFECTED_METADATA)) {
                this.infect(player);

                damager.sendMessage("You have infected " + player.getName() + "!");
            }
        }
    }

    @ArenaEventHandler
    public void onMove(PlayerMoveEvent event) {
        if (event.getPlayer().hasMetadata(INFECTED_METADATA)) {
            event.getPlayer().sendMessage("You are infected! You cannot move!");
            event.setCancelled(true);
        }
    }

    @ArenaEventHandler
    public void onPhaseStart(ArenaPhaseStartEvent event, MyCompetition competition) {
        // Ensure we are ingame
        if (!CompetitionPhaseType.INGAME.equals(event.getPhase().getType())) {
            return;
        }
      
        competition.startInfectTask();
    }

    @ArenaEventHandler
    public void onPhaseComplete(ArenaPhaseCompleteEvent event, MyCompetition competition) {
        // Ensure we are ingame
        if (!CompetitionPhaseType.INGAME.equals(event.getPhase().getType())) {
            return;
        }

        competition.stopInfectTask();
    }

    public boolean isInfected(ArenaPlayer player) {
        return player.getPlayer().hasMetadata(INFECTED_METADATA);
    }

    public void infect(Player player) {
        player.sendMessage("You have been infected!");
        player.setMetadata(INFECTED_METADATA, new FixedMetadataValue(MyPlugin.getInstance(), true));

        // Infect the player for the given duration
        Bukkit.getScheduler().runTaskLater(MyPlugin.getInstance(), () -> {
            player.removeMetadata(INFECTED_METADATA, MyPlugin.getInstance());
            player.sendMessage("You are no longer infected!");
        }, this.infectionTime.toMillis() / 50);
    }
}

As seen above, when the game enters the in-game phase, we start running our task in the active MyCompetition to infect a random player every minute. Once the competition is no longer ingame, we stop the task.