added team fights up to 4 vs 4 to the arena

This commit is contained in:
MineTec committed 2026-10-15 23:21:20 +02:00
1 parent 4add446940
commit ae3bde626f
10 files changed
+639 -338

No files matched your search

@@ -225,14 +225,13 @@ public class Arena extends Appliance {
return this.slots.stream().anyMatch(slot -> slot.state == ArenaSlot.State.FREE);
}
void startFight(Player red, Player blue, UUID challenger, FightMode mode) {
void startFight(List<List<Player>> teams, FightMode mode, Map<UUID, UUID> kits) {
ArenaSlot slot = this.slots.stream().filter(s -> s.state == ArenaSlot.State.FREE).findFirst().orElse(null);
if(slot == null) {
this.toTribune(red);
this.toTribune(blue);
teams.forEach(team -> team.forEach(this::toTribune));
return;
}
new Fight(this, slot, red, blue, challenger, mode).start();
new Fight(this, slot, teams, mode, kits).start();
}
void resetSlot(ArenaSlot slot) {
@@ -314,7 +313,7 @@ public class Arena extends Appliance {
public void handleQuit(Player player) {
Fight fight = this.fightOf(player);
if(fight != null && this.state(player) == PlayerState.FIGHT) fight.forfeit(player.getUniqueId());
if(fight != null && fight.isAlive(player.getUniqueId())) fight.forfeit(player.getUniqueId());
if(this.lobby != null) this.lobby.leave(player);
this.teams.leave(player);
this.states.remove(player.getUniqueId());
@@ -348,7 +347,7 @@ public class Arena extends Appliance {
public @Nullable Region arenaRegionOf(Player player) {
Fight fight = this.fightOf(player);
return fight == null ? null : fight.slot().settings.region();
return fight == null ? null : fight.region();
}
public @Nullable Lobby lobby() {
@@ -1,9 +1,11 @@
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.FightMode;
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.Region;
import eu.mhsl.craftattack.spawn.core.Main;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.title.Title;
import org.bukkit.Bukkit;
@@ -18,37 +20,44 @@ import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* A running fight in one arena. Fighter 0 comes from the red pad and uses spawn 1, fighter 1 the blue pad and spawn 2.
* A running fight in one arena between team 0 (red pad, spawn 1) and team 1 (blue pad, spawn 2).
* A team loses once none of its players is left in the arena.
*/
final class Fight {
private static final int CELEBRATION_TICKS = 60;
private static final double SPECTATOR_RANGE = 32;
private static final double SPAWN_SPREAD = 2;
private final Arena arena;
private final ArenaSlot slot;
private final UUID[] fighters;
private final String[] names;
private final UUID challenger;
private final List<List<UUID>> teams;
private final Map<UUID, String> names = new HashMap<>();
private final List<Set<UUID>> alive;
private final FightMode mode;
private final Map<UUID, UUID> kits;
private final BossBar bossBar;
private final Set<UUID> bossBarViewers = new HashSet<>();
private int secondsLeft;
private boolean over;
private @Nullable BukkitTask timer;
Fight(Arena arena, ArenaSlot slot, Player red, Player blue, UUID challenger, FightMode mode) {
Fight(Arena arena, ArenaSlot slot, List<List<Player>> teams, FightMode mode, Map<UUID, UUID> kits) {
this.arena = arena;
this.slot = slot;
this.fighters = new UUID[]{red.getUniqueId(), blue.getUniqueId()};
this.names = new String[]{red.getName(), blue.getName()};
this.challenger = challenger;
this.teams = teams.stream().map(team -> team.stream().map(Player::getUniqueId).toList()).toList();
teams.forEach(team -> team.forEach(player -> this.names.put(player.getUniqueId(), player.getName())));
this.alive = this.teams.stream().map(team -> (Set<UUID>) new LinkedHashSet<>(team)).toList();
this.mode = mode;
this.kits = kits;
this.secondsLeft = arena.settings().fightSeconds();
this.bossBar = BossBar.bossBar(Component.empty(), 1f, BossBar.Color.RED, BossBar.Overlay.NOTCHED_10);
}
@@ -57,44 +66,72 @@ final class Fight {
this.slot.state = ArenaSlot.State.FIGHT;
this.slot.fight = this;
FightMode.Role[] roles = new FightMode.Role[2];
for(int i = 0; i < 2; i++) {
roles[i] = this.fighters[i].equals(this.challenger) ? FightMode.Role.CHALLENGER : FightMode.Role.OPPONENT;
}
UUID challengerId = this.challenger;
UUID opponentId = this.fighters[0].equals(challengerId) ? this.fighters[1] : this.fighters[0];
for(int i = 0; i < 2; i++) {
Player player = Bukkit.getPlayer(this.fighters[i]);
for(int team = 0; team < 2; team++) {
List<UUID> members = this.teams.get(team);
for(int i = 0; i < members.size(); i++) {
Player player = Bukkit.getPlayer(members.get(i));
if(player == null) continue;
UUID kitOwner = this.mode.kitSource(roles[i]) == FightMode.Role.CHALLENGER ? challengerId : opponentId;
this.arena.setState(player, Arena.PlayerState.FIGHT);
this.arena.teams().join(player, Teams.fight(i));
this.arena.teams().join(player, Teams.fight(team));
player.closeInventory();
player.teleport(this.slot.settings.spawns()[i]);
player.teleport(this.spawnFor(team, i, members.size()));
Arena.restore(player, GameMode.SURVIVAL);
InventoryStore.apply(player, kitOwner);
InventoryStore.apply(player, this.kits.getOrDefault(player.getUniqueId(), player.getUniqueId()));
player.showTitle(Title.title(
Component.text("Kampf!", NamedTextColor.GOLD),
Component.text("gegen ", NamedTextColor.GRAY).append(Component.text(this.names[1 - i], Style.PAD_TEXT_COLORS[1 - i])),
Component.text("gegen ", NamedTextColor.GRAY).append(this.teamNames(1 - team)),
Title.Times.times(Duration.ZERO, Duration.ofMillis(1500), Duration.ofMillis(500))
));
Style.play(player, Sound.EVENT_RAID_HORN, 1.2f);
}
}
// players who left during the countdown never arrived
for(int team = 0; team < 2; team++) this.alive.get(team).removeIf(uuid -> Bukkit.getPlayer(uuid) == null);
Bukkit.broadcast(Style.message(
this.headline().append(Component.text(" in Arena " + this.slot.settings.name() + ". Modus: " + this.mode.displayName(), NamedTextColor.GRAY))
this.teamNames(0).append(Component.text(" gegen ", NamedTextColor.GRAY)).append(this.teamNames(1))
.append(Component.text(" in Arena " + this.slot.settings.name() + ". Modus: " + this.mode.displayName(this.isTeamFight()), NamedTextColor.GRAY))
));
this.updateBossBar();
this.timer = Bukkit.getScheduler().runTaskTimer(Main.instance(), this::tickSecond, 20, 20);
if(this.alive.get(0).isEmpty() || this.alive.get(1).isEmpty()) this.checkEnd();
}
/**
* Team members stand in a small circle around the spawn, facing the same way.
*/
private Location spawnFor(int team, int index, int count) {
Location center = this.slot.settings.spawns()[team];
if(count == 1) return center;
double angle = 2 * Math.PI * index / count;
Location spot = center.clone().add(Math.cos(angle) * SPAWN_SPREAD, 0, Math.sin(angle) * SPAWN_SPREAD);
boolean free = spot.getBlock().isPassable() && spot.clone().add(0, 1, 0).getBlock().isPassable()
&& !spot.clone().subtract(0, 1, 0).getBlock().isPassable();
return free ? spot : center;
}
private boolean isTeamFight() {
return this.teams.get(0).size() > 1 || this.teams.get(1).size() > 1;
}
private Component teamNames(int team) {
return Component.join(
JoinConfiguration.separator(Component.text(", ", NamedTextColor.GRAY)),
this.teams.get(team).stream().map(uuid -> Component.text(this.names.get(uuid), Style.PAD_TEXT_COLORS[team])).toList()
);
}
/**
* Short form for bossbar and displays: names in a duel, "Rot 2/3" in a team fight.
*/
Component headline() {
return Component.text(this.names[0], Style.PAD_TEXT_COLORS[0])
if(!this.isTeamFight()) {
return this.teamNames(0).append(Component.text(" gegen ", NamedTextColor.GRAY)).append(this.teamNames(1));
}
return Component.text("Rot " + this.alive.get(0).size() + "/" + this.teams.get(0).size(), Style.PAD_TEXT_COLORS[0])
.append(Component.text(" gegen ", NamedTextColor.GRAY))
.append(Component.text(this.names[1], Style.PAD_TEXT_COLORS[1]));
.append(Component.text("Blau " + this.alive.get(1).size() + "/" + this.teams.get(1).size(), Style.PAD_TEXT_COLORS[1]));
}
int secondsLeft() {
@@ -106,7 +143,20 @@ final class Fight {
}
boolean involves(UUID player) {
return this.fighters[0].equals(player) || this.fighters[1].equals(player);
return this.teams.get(0).contains(player) || this.teams.get(1).contains(player);
}
boolean isAlive(UUID player) {
return this.alive.get(0).contains(player) || this.alive.get(1).contains(player);
}
boolean sameTeam(UUID a, UUID b) {
int team = this.teamOf(a);
return team != -1 && team == this.teamOf(b);
}
Region region() {
return this.slot.settings.region();
}
ArenaSlot slot() {
@@ -116,11 +166,11 @@ final class Fight {
private void tickSecond() {
if(this.over) return;
if(--this.secondsLeft <= 0) {
this.finish(-1, Component.text("Die Zeit ist abgelaufen.", NamedTextColor.GRAY));
this.timeUp();
return;
}
if(this.secondsLeft == 60 || this.secondsLeft == 10) {
this.fighterPlayers().forEach(player -> {
this.alivePlayers().forEach(player -> {
player.sendActionBar(Component.text("Noch " + this.secondsLeft + " Sekunden", NamedTextColor.YELLOW));
Style.play(player, Sound.BLOCK_NOTE_BLOCK_HAT, 0.8f);
});
@@ -128,13 +178,40 @@ final class Fight {
this.updateBossBar();
}
/**
* More survivors win, then more hearts left, otherwise a draw.
*/
private void timeUp() {
int red = this.alive.get(0).size();
int blue = this.alive.get(1).size();
if(red != blue) {
this.finish(red > blue ? 0 : 1, Component.text("(Zeit abgelaufen, mehr Überlebende)", NamedTextColor.GRAY));
return;
}
double redHealth = this.health(0);
double blueHealth = this.health(1);
if(Math.abs(redHealth - blueHealth) >= 0.5) {
this.finish(redHealth > blueHealth ? 0 : 1, Component.text("(Zeit abgelaufen, mehr Herzen übrig)", NamedTextColor.GRAY));
return;
}
this.finish(-1, Component.text("Die Zeit ist abgelaufen.", NamedTextColor.GRAY));
}
private double health(int team) {
return this.alive.get(team).stream()
.map(Bukkit::getPlayer)
.filter(player -> player != null)
.mapToDouble(player -> player.getHealth() + player.getAbsorptionAmount())
.sum();
}
private void updateBossBar() {
this.bossBar.name(this.headline().append(Component.text(" " + Style.time(this.secondsLeft), NamedTextColor.WHITE)));
this.bossBar.progress(Math.clamp((float) this.secondsLeft / this.arena.settings().fightSeconds(), 0f, 1f));
Set<UUID> viewers = new HashSet<>();
for(Player player : this.arena.worlds().play().getPlayers()) {
if(this.involves(player.getUniqueId()) || this.isSpectating(player)) viewers.add(player.getUniqueId());
if(this.isAlive(player.getUniqueId()) || this.isSpectating(player)) viewers.add(player.getUniqueId());
}
for(UUID uuid : this.bossBarViewers) {
Player player = Bukkit.getPlayer(uuid);
@@ -150,32 +227,61 @@ final class Fight {
private boolean isSpectating(Player player) {
if(this.arena.state(player) != Arena.PlayerState.TRIBUNE && this.arena.state(player) != Arena.PlayerState.PAD) return false;
var region = this.slot.settings.region();
Region region = this.slot.settings.region();
double dx = Math.max(0, Math.max(region.minX() - player.getX(), player.getX() - (region.maxX() + 1)));
double dz = Math.max(0, Math.max(region.minZ() - player.getZ(), player.getZ() - (region.maxZ() + 1)));
return Math.hypot(dx, dz) <= SPECTATOR_RANGE;
}
/**
* The player took lethal damage.
* The player took lethal damage: out of the fight and back to the tribune.
*/
void defeated(Player loser) {
int index = this.indexOf(loser.getUniqueId());
if(index == -1 || this.over) return;
Player winner = Bukkit.getPlayer(this.fighters[1 - index]);
Component detail = winner == null
? Component.empty()
: Component.text("(" + this.mode.displayName() + ", " + Style.hearts(winner.getHealth() + winner.getAbsorptionAmount()) + " ❤ übrig)", NamedTextColor.GRAY);
this.finish(1 - index, detail);
void defeated(Player player) {
if(this.over || !this.eliminate(player.getUniqueId())) return;
player.showTitle(Title.title(Component.text("Ausgeschieden", NamedTextColor.RED), Component.empty()));
Style.play(player, Sound.BLOCK_BEACON_DEACTIVATE, 1f);
this.arena.toTribune(player);
this.announceElimination(player.getUniqueId(), "ist ausgeschieden");
this.checkEnd();
}
/**
* The player left the server during the fight.
*/
void forfeit(UUID quitter) {
int index = this.indexOf(quitter);
if(index == -1 || this.over) return;
this.finish(1 - index, Component.text("(" + this.names[index] + " hat die Arena verlassen)", NamedTextColor.GRAY));
if(this.over || !this.eliminate(quitter)) return;
this.announceElimination(quitter, "hat die Arena verlassen");
this.checkEnd();
}
private boolean eliminate(UUID player) {
int team = this.teamOf(player);
return team != -1 && this.alive.get(team).remove(player);
}
private void announceElimination(UUID player, String what) {
if(!this.isTeamFight()) return;
int team = this.teamOf(player);
Component message = Component.text(this.names.get(player), Style.PAD_TEXT_COLORS[team])
.append(Component.text(" " + what + ". ", NamedTextColor.GRAY))
.append(this.headline());
this.alivePlayers().forEach(fighter -> fighter.sendActionBar(message));
this.updateBossBar();
}
private void checkEnd() {
if(this.over) return;
boolean redOut = this.alive.get(0).isEmpty();
boolean blueOut = this.alive.get(1).isEmpty();
if(redOut && blueOut) this.finish(-1, Component.text("Niemand ist mehr übrig.", NamedTextColor.GRAY));
else if(redOut) this.finish(1, this.detail(1));
else if(blueOut) this.finish(0, this.detail(0));
}
private Component detail(int winner) {
String hearts = Style.hearts(this.health(winner));
String survivors = this.isTeamFight() ? this.alive.get(winner).size() + " übrig, " : "";
return Component.text("(" + this.mode.displayName(this.isTeamFight()) + ", " + survivors + hearts + " ❤)", NamedTextColor.GRAY);
}
/**
@@ -186,23 +292,20 @@ final class Fight {
this.over = true;
this.stopTimer();
this.hideBossBar();
this.fighterPlayers().forEach(this.arena::toTribune);
this.alivePlayers().forEach(this.arena::toTribune);
this.slot.fight = null;
}
private void finish(int winnerIndex, Component detail) {
private void finish(int winner, Component detail) {
this.over = true;
this.stopTimer();
if(winnerIndex == -1) {
if(winner == -1) {
Bukkit.broadcast(Style.message(
Component.text(this.names[0], Style.PAD_TEXT_COLORS[0])
.append(Component.text(" und ", NamedTextColor.GRAY))
.append(Component.text(this.names[1], Style.PAD_TEXT_COLORS[1]))
.append(Component.text(" trennen sich unentschieden. ", NamedTextColor.GRAY))
.append(detail)
this.teamNames(0).append(Component.text(" und ", NamedTextColor.GRAY)).append(this.teamNames(1))
.append(Component.text(" trennen sich unentschieden. ", NamedTextColor.GRAY)).append(detail)
));
this.fighterPlayers().forEach(player -> {
this.alivePlayers().forEach(player -> {
player.showTitle(Title.title(Component.text("Unentschieden", NamedTextColor.YELLOW), detail));
Style.play(player, Sound.BLOCK_BEACON_DEACTIVATE, 1f);
this.arena.toTribune(player);
@@ -211,33 +314,31 @@ final class Fight {
return;
}
int loserIndex = 1 - winnerIndex;
int loser = 1 - winner;
boolean plural = this.teams.get(winner).size() > 1;
Bukkit.broadcast(Style.message(
Component.text(this.names[winnerIndex], Style.PAD_TEXT_COLORS[winnerIndex])
.append(Component.text(" hat gegen ", NamedTextColor.GRAY))
.append(Component.text(this.names[loserIndex], Style.PAD_TEXT_COLORS[loserIndex]))
.append(Component.text(" gewonnen ", NamedTextColor.GRAY))
this.teamNames(winner)
.append(Component.text(plural ? " gewinnen gegen " : " hat gegen ", NamedTextColor.GRAY))
.append(this.teamNames(loser))
.append(Component.text(plural ? " " : " gewonnen ", NamedTextColor.GRAY))
.append(detail)
));
Player loser = Bukkit.getPlayer(this.fighters[loserIndex]);
if(loser != null) {
loser.showTitle(Title.title(
Component.text("Verloren", NamedTextColor.RED),
Component.text(this.names[winnerIndex] + " hat gewonnen", NamedTextColor.GRAY)
));
Style.play(loser, Sound.BLOCK_BEACON_DEACTIVATE, 1f);
this.arena.toTribune(loser);
for(UUID uuid : this.teams.get(loser)) {
Player player = Bukkit.getPlayer(uuid);
if(player == null) continue;
player.showTitle(Title.title(Component.text("Verloren", NamedTextColor.RED), Component.empty()));
if(this.alive.get(loser).contains(uuid)) this.arena.toTribune(player);
}
Player winner = Bukkit.getPlayer(this.fighters[winnerIndex]);
if(winner != null) {
winner.showTitle(Title.title(
for(UUID uuid : this.teams.get(winner)) {
Player player = Bukkit.getPlayer(uuid);
if(player == null) continue;
player.showTitle(Title.title(
Component.text("Gewonnen!", NamedTextColor.GOLD),
Component.text("gegen " + this.names[loserIndex], NamedTextColor.GRAY)
Component.text("gegen ", NamedTextColor.GRAY).append(this.teamNames(loser))
));
Style.play(winner, Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f);
this.celebrate(winner.getLocation(), Style.PAD_PARTICLE_COLORS[winnerIndex]);
Style.play(player, Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f);
if(this.alive.get(winner).contains(uuid)) this.celebrate(player.getLocation(), Style.PAD_PARTICLE_COLORS[winner]);
}
this.afterCelebration();
}
@@ -261,9 +362,8 @@ final class Fight {
private void afterCelebration() {
this.hideBossBar();
Bukkit.getScheduler().runTaskLater(Main.instance(), () -> {
for(UUID uuid : this.fighters) {
Player player = Bukkit.getPlayer(uuid);
if(player != null && this.arena.fightOf(player) == this) this.arena.toTribune(player);
for(Player player : this.alivePlayers()) {
if(this.arena.fightOf(player) == this && this.arena.state(player) == Arena.PlayerState.FIGHT) this.arena.toTribune(player);
}
this.slot.fight = null;
this.arena.resetSlot(this.slot);
@@ -275,18 +375,20 @@ final class Fight {
this.timer = null;
}
private int indexOf(UUID player) {
if(this.fighters[0].equals(player)) return 0;
if(this.fighters[1].equals(player)) return 1;
private int teamOf(UUID player) {
if(this.teams.get(0).contains(player)) return 0;
if(this.teams.get(1).contains(player)) return 1;
return -1;
}
private List<Player> fighterPlayers() {
private List<Player> alivePlayers() {
List<Player> players = new ArrayList<>();
for(UUID uuid : this.fighters) {
for(Set<UUID> team : this.alive) {
for(UUID uuid : team) {
Player player = Bukkit.getPlayer(uuid);
if(player != null) players.add(player);
}
}
return players;
}
}
@@ -22,19 +22,23 @@ import org.bukkit.inventory.ItemStack;
class FightListener extends ApplianceListener<Arena> {
private boolean isFighting(Player player) {
Fight fight = this.getAppliance().fightOf(player);
return fight != null && !fight.isOver() && this.getAppliance().state(player) == Arena.PlayerState.FIGHT;
return fight != null && !fight.isOver() && fight.isAlive(player.getUniqueId());
}
/**
* Only fighters hit each other, and never their own team directly. Explosions still hurt everyone.
*/
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onDamageByPlayer(EntityDamageByEntityEvent event) {
if(!(event.getEntity() instanceof Player)) return;
if(!(event.getEntity() instanceof Player victim)) return;
Player attacker = switch(event.getDamager()) {
case Player player -> player;
case Projectile projectile when projectile.getShooter() instanceof Player shooter -> shooter;
default -> null;
};
if(attacker == null || attacker == event.getEntity()) return;
if(!this.isFighting(attacker)) event.setCancelled(true);
if(attacker == null || attacker == victim) return;
Fight fight = this.getAppliance().fightOf(attacker);
if(!this.isFighting(attacker) || fight.sameTeam(attacker.getUniqueId(), victim.getUniqueId())) event.setCancelled(true);
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
@@ -5,6 +5,7 @@ import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.PadLobby;
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.Region;
import eu.mhsl.craftattack.spawn.core.util.world.InteractSounds;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.title.Title;
import org.bukkit.Bukkit;
@@ -15,11 +16,12 @@ import org.bukkit.Sound;
import org.bukkit.entity.Player;
import org.bukkit.entity.TextDisplay;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
/**
@@ -69,7 +71,7 @@ final class Lobby implements PadLobby.Listener {
private void checkEnter(Player player) {
for(int pad = 0; pad < PadLobby.PADS; pad++) {
if(this.pads.occupant(pad) != null) {
if(this.pads.isPadFull(pad)) {
if(this.isOn(player, pad, PUSH_MARGIN)) this.pushAway(player, pad);
continue;
}
@@ -79,6 +81,10 @@ final class Lobby implements PadLobby.Listener {
this.arena.setState(player, Arena.PlayerState.PAD);
this.arena.teams().join(player, Teams.pad(pad));
Style.play(player, Sound.BLOCK_NOTE_BLOCK_PLING, 1.4f);
if(!player.getUniqueId().equals(this.pads.challenger())) {
player.sendActionBar(Component.text(Style.PAD_NAMES[pad] + " " + this.pads.players(pad).size() + "/" + this.pads.size(pad)
+ ", Modus: " + this.modeName(), Style.PAD_TEXT_COLORS[pad]));
}
return;
}
}
@@ -94,14 +100,42 @@ final class Lobby implements PadLobby.Listener {
this.arena.setState(player, Arena.PlayerState.TRIBUNE);
}
void chooseMode(Player player, FightMode mode) {
if(!this.pads.setMode(player.getUniqueId(), mode)) {
void handleMenuClick(Player player, ModeMenu menu, ModeMenu.Click click) {
if(!this.pads.canConfigure(player.getUniqueId())) {
player.closeInventory();
return;
}
InteractSounds.of(player).click();
int ownSize = menu.ownSize;
int opponentSize = menu.opponentSize;
FightMode mode = menu.mode;
switch(click) {
case ModeMenu.OwnSize(int size) -> ownSize = size;
case ModeMenu.OpponentSize(int size) -> opponentSize = size;
case ModeMenu.Mode(FightMode chosen) -> mode = chosen;
case ModeMenu.Done() -> {
InteractSounds.of(player).success();
player.closeInventory();
player.sendActionBar(Component.text("Modus: " + mode.displayName(), NamedTextColor.GOLD));
player.sendActionBar(Component.text(this.pads.size(this.pads.challengerPad()) + " gegen " + this.pads.size(1 - this.pads.challengerPad())
+ ", Modus: " + this.modeName(), NamedTextColor.GOLD));
return;
}
}
// switching to teams drops the opponent's inventory mode
if(!mode.isAllowed(ownSize, opponentSize)) mode = FightMode.OWN;
if(!this.pads.configure(player.getUniqueId(), ownSize, opponentSize, mode)) return;
menu.ownSize = ownSize;
menu.opponentSize = opponentSize;
menu.mode = mode;
menu.draw();
InteractSounds.of(player).click();
}
private boolean isTeamFight() {
return this.pads.size(0) > 1 || this.pads.size(1) > 1;
}
private String modeName() {
return this.pads.mode().displayName(this.isTeamFight());
}
boolean isIdle() {
@@ -109,11 +143,7 @@ final class Lobby implements PadLobby.Listener {
}
void shutdown() {
for(int pad = 0; pad < PadLobby.PADS; pad++) {
UUID occupant = this.pads.occupant(pad);
Player player = occupant == null ? null : Bukkit.getPlayer(occupant);
if(player != null) this.leave(player);
}
for(int pad = 0; pad < PadLobby.PADS; pad++) this.playersOn(pad).forEach(this::leave);
for(TextDisplay display : this.displays) display.remove();
}
@@ -134,121 +164,119 @@ final class Lobby implements PadLobby.Listener {
if(now - this.lastPushSound.getOrDefault(player.getUniqueId(), 0L) < 1000) return;
this.lastPushSound.put(player.getUniqueId(), now);
Style.play(player, Sound.BLOCK_NOTE_BLOCK_BASS, 0.6f);
player.sendActionBar(Component.text("Dieses Feld ist besetzt", NamedTextColor.RED));
player.sendActionBar(Component.text("Dieses Feld ist voll", NamedTextColor.RED));
}
@Override
public void challengerSelected(UUID challenger) {
public void challengerSelected(UUID challenger, boolean canConfigure) {
Player player = Bukkit.getPlayer(challenger);
if(player == null) return;
player.sendMessage(Style.message("Du forderst heraus. Wähle den Modus. Wer sich auf das andere Feld stellt, nimmt an."));
if(!canConfigure) {
player.sendMessage(Style.message("Du führst jetzt die Herausforderung an. Teams und Modus bleiben wie gewählt."));
return;
}
player.sendMessage(Style.message("Du forderst heraus. Wähle Teamgröße und Modus. Wer sich auf die Felder stellt, macht mit."));
InteractSounds.of(player).open();
ModeMenu.open(player, this.pads.mode());
int pad = this.pads.challengerPad();
ModeMenu.open(player, pad, this.pads.size(pad), this.pads.size(1 - pad), this.pads.mode());
}
@Override
public void kitsApplied(UUID challenger, UUID opponent, FightMode mode) {
Player challengerPlayer = Bukkit.getPlayer(challenger);
Player opponentPlayer = Bukkit.getPlayer(opponent);
if(challengerPlayer == null || opponentPlayer == null) return;
if(ModeMenu.isOpen(challengerPlayer)) challengerPlayer.closeInventory();
InventoryStore.apply(challengerPlayer, mode.kitSource(FightMode.Role.CHALLENGER) == FightMode.Role.CHALLENGER ? challenger : opponent);
InventoryStore.apply(opponentPlayer, mode.kitSource(FightMode.Role.OPPONENT) == FightMode.Role.CHALLENGER ? challenger : opponent);
String kitText = switch(mode) {
case OWN -> "Jeder kämpft mit seinem eigenen Inventar.";
case CHALLENGER -> "Ihr kämpft beide mit dem Inventar von " + challengerPlayer.getName() + ".";
case OPPONENT -> "Ihr kämpft beide mit dem Inventar von " + opponentPlayer.getName() + ".";
};
opponentPlayer.sendMessage(Style.message("Du nimmst die Herausforderung von " + challengerPlayer.getName() + " an. " + kitText + " Verlasse das Feld, um abzubrechen."));
challengerPlayer.sendMessage(Style.message(opponentPlayer.getName() + " nimmt deine Herausforderung an. " + kitText + " Verlasse das Feld, um abzubrechen."));
for(Player player : new Player[]{challengerPlayer, opponentPlayer}) {
Style.play(player, Sound.ITEM_ARMOR_EQUIP_NETHERITE, 1f);
}
}
@Override
public void kitReverted(UUID uuid) {
public void kitAssigned(UUID uuid, UUID kitOwner) {
Player player = Bukkit.getPlayer(uuid);
if(player != null) InventoryStore.apply(player, uuid);
if(player == null) return;
if(ModeMenu.isOpen(player)) player.closeInventory();
InventoryStore.apply(player, kitOwner);
if(uuid.equals(kitOwner)) return;
Player owner = Bukkit.getPlayer(kitOwner);
player.sendMessage(Style.message("Du kämpfst mit dem Inventar von " + (owner == null ? "jemand anderem" : owner.getName()) + ". Verlasse das Feld, um abzubrechen."));
Style.play(player, Sound.ITEM_ARMOR_EQUIP_NETHERITE, 1f);
}
@Override
public void countdown(int secondsLeft) {
for(int pad = 0; pad < PadLobby.PADS; pad++) {
Player player = this.occupantPlayer(pad);
Player other = this.occupantPlayer(1 - pad);
if(player == null || other == null) continue;
Component opponents = this.namesOn(1 - pad);
for(Player player : this.playersOn(pad)) {
player.showTitle(Title.title(
Component.text(secondsLeft, NamedTextColor.GOLD),
Component.text("Kampf gegen ", NamedTextColor.GRAY).append(Component.text(other.getName(), Style.PAD_TEXT_COLORS[1 - pad])),
Component.text("Kampf gegen ", NamedTextColor.GRAY).append(opponents),
Title.Times.times(Duration.ZERO, Duration.ofMillis(1100), Duration.ZERO)
));
Style.play(player, Sound.BLOCK_NOTE_BLOCK_PLING, 0.6f + (this.arena.settings().countdownSeconds() - secondsLeft) * 0.2f);
}
}
}
@Override
public void countdownCancelled() {
for(int pad = 0; pad < PadLobby.PADS; pad++) {
Player player = this.occupantPlayer(pad);
if(player == null) continue;
for(Player player : this.playersOn(pad)) {
player.clearTitle();
player.sendActionBar(Component.text("Abgebrochen", NamedTextColor.RED));
Style.play(player, Sound.BLOCK_NOTE_BLOCK_BASS, 0.5f);
}
}
}
@Override
public void start(UUID challenger, UUID opponent, FightMode mode, int challengerPad) {
Player challengerPlayer = Bukkit.getPlayer(challenger);
Player opponentPlayer = Bukkit.getPlayer(opponent);
if(challengerPlayer == null || opponentPlayer == null) {
if(challengerPlayer != null) this.arena.toTribune(challengerPlayer);
if(opponentPlayer != null) this.arena.toTribune(opponentPlayer);
public void start(List<List<UUID>> teams, UUID challenger, FightMode mode, Map<UUID, UUID> kits) {
List<List<Player>> players = teams.stream()
.map(team -> team.stream().map(Bukkit::getPlayer).filter(Objects::nonNull).toList())
.toList();
if(players.get(0).isEmpty() || players.get(1).isEmpty()) {
players.forEach(team -> team.forEach(this.arena::toTribune));
return;
}
Player red = challengerPad == 0 ? challengerPlayer : opponentPlayer;
Player blue = challengerPad == 0 ? opponentPlayer : challengerPlayer;
this.arena.startFight(red, blue, challenger, mode);
this.arena.startFight(players, mode, kits);
}
private @Nullable Player occupantPlayer(int pad) {
UUID occupant = this.pads.occupant(pad);
return occupant == null ? null : Bukkit.getPlayer(occupant);
private List<Player> playersOn(int pad) {
return this.pads.players(pad).stream().map(Bukkit::getPlayer).filter(Objects::nonNull).toList();
}
private Component namesOn(int pad) {
return Component.join(
JoinConfiguration.separator(Component.text(", ", NamedTextColor.GRAY)),
this.playersOn(pad).stream().map(player -> Component.text(player.getName(), Style.PAD_TEXT_COLORS[pad])).toList()
);
}
private void updateDisplays() {
boolean waiting = this.pads.isFull() && !this.arena.hasFreeSlot();
Player challenger = this.pads.challenger() == null ? null : Bukkit.getPlayer(this.pads.challenger());
String modeText = "Modus: " + this.pads.mode().displayName();
int challengerPad = this.pads.challengerPad();
String modeText = "Modus: " + this.modeName();
for(int pad = 0; pad < PadLobby.PADS; pad++) {
List<Player> players = this.playersOn(pad);
Component title = Component.text(Style.PAD_NAMES[pad], Style.PAD_TEXT_COLORS[pad]);
Player occupant = this.occupantPlayer(pad);
if(challenger != null) title = title.append(Component.text(" " + players.size() + "/" + this.pads.size(pad), NamedTextColor.WHITE));
Component body;
if(waiting) {
body = Component.text("Alle Arenen belegt", NamedTextColor.GRAY).appendNewline()
.append(Component.text("Gleich geht es los", NamedTextColor.WHITE));
body = this.namesOn(pad).appendNewline()
.append(Component.text("Alle Arenen belegt, gleich geht es los", NamedTextColor.GRAY));
} else if(this.pads.isCountingDown()) {
body = Component.text(occupant == null ? "" : occupant.getName(), Style.PAD_TEXT_COLORS[pad]).appendNewline()
body = this.namesOn(pad).appendNewline()
.append(Component.text("Kampf beginnt in " + this.pads.secondsLeft(), NamedTextColor.GOLD));
} else if(occupant != null) {
body = Component.text(occupant.getName(), Style.PAD_TEXT_COLORS[pad]).appendNewline()
.append(Component.text("fordert heraus", NamedTextColor.GRAY)).appendNewline()
.append(Component.text(modeText, NamedTextColor.WHITE));
} else if(challenger != null) {
body = Component.text("Stell dich hierhin, um gegen", NamedTextColor.WHITE).appendNewline()
.append(Component.text(challenger.getName(), Style.PAD_TEXT_COLORS[1 - pad]))
.append(Component.text(" zu kämpfen", NamedTextColor.WHITE)).appendNewline()
.append(Component.text(modeText, NamedTextColor.GRAY));
} else if(!this.arena.hasFreeSlot()) {
body = Component.text("Alle Arenen belegt", NamedTextColor.GRAY).appendNewline()
.append(Component.text("Du kannst dich trotzdem anstellen", NamedTextColor.WHITE));
} else {
body = Component.text("Stell dich hierhin,", NamedTextColor.WHITE).appendNewline()
} else if(challenger == null) {
body = !this.arena.hasFreeSlot()
? Component.text("Alle Arenen belegt", NamedTextColor.GRAY).appendNewline()
.append(Component.text("Du kannst dich trotzdem anstellen", NamedTextColor.WHITE))
: Component.text("Stell dich hierhin,", NamedTextColor.WHITE).appendNewline()
.append(Component.text("um herauszufordern", NamedTextColor.WHITE));
} else if(pad == challengerPad) {
body = this.namesOn(pad).appendNewline()
.append(Component.text(players.size() == 1 ? "fordert heraus" : "fordern heraus", NamedTextColor.GRAY)).appendNewline()
.append(Component.text(modeText, NamedTextColor.WHITE));
} else {
Component who = players.isEmpty()
? Component.text("Stell dich hierhin, um gegen", NamedTextColor.WHITE).appendNewline()
.append(Component.text(challenger.getName(), Style.PAD_TEXT_COLORS[challengerPad]))
.append(Component.text(" zu kämpfen", NamedTextColor.WHITE))
: this.namesOn(pad).appendNewline().append(Component.text("nehmen an", NamedTextColor.GRAY));
body = who.appendNewline().append(Component.text(modeText, NamedTextColor.GRAY));
}
this.displays[pad].text(title.appendNewline().append(body));
}
@@ -260,13 +288,12 @@ final class Lobby implements PadLobby.Listener {
for(int pad = 0; pad < PadLobby.PADS; pad++) {
Region region = this.regions[pad];
Color color = waiting ? Style.WAITING_PARTICLE_COLOR : Style.PAD_PARTICLE_COLORS[pad];
boolean taken = this.pads.occupant(pad) != null;
double spacing = countdown ? 0.25 : taken ? 0.4 : 0.6;
this.drawOutline(region, new Particle.DustOptions(color, taken ? 1.2f : 0.9f), spacing);
boolean full = this.pads.isPadFull(pad);
double spacing = countdown ? 0.25 : full ? 0.4 : 0.6;
this.drawOutline(region, new Particle.DustOptions(color, full ? 1.2f : 0.9f), spacing);
Player occupant = this.occupantPlayer(pad);
if(occupant == null) continue;
double speed = countdown ? 0.6 : 0.3;
for(Player occupant : this.playersOn(pad)) {
for(int strand = 0; strand < 2; strand++) {
double angle = this.ticks * speed + strand * Math.PI;
double height = (this.ticks % 40) / 20.0;
@@ -275,6 +302,7 @@ final class Lobby implements PadLobby.Listener {
}
}
}
}
private void drawOutline(Region region, Particle.DustOptions dust, double spacing) {
double y = region.minY() + 0.1;
@@ -1,6 +1,7 @@
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.FightMode;
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.PadLobby;
import eu.mhsl.craftattack.spawn.core.util.inventory.ItemBuilder;
import eu.mhsl.craftattack.spawn.core.util.inventory.PlaceholderItems;
import net.kyori.adventure.text.Component;
@@ -14,55 +15,126 @@ import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SkullMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Chest menu in which the challenger picks the mode. Works for Bedrock players as well.
* Chest menu in which the challenger picks team sizes and mode. Every click applies at once.
* Works for Bedrock players as well.
*/
final class ModeMenu implements InventoryHolder {
private static final int[] SLOTS = {11, 13, 15};
private static final int OWN_ROW = 10;
private static final int OPPONENT_ROW = 19;
private static final int[] MODE_SLOTS = {29, 31, 33};
private static final FightMode[] MODES = {FightMode.OWN, FightMode.CHALLENGER, FightMode.OPPONENT};
private static final int DONE_SLOT = 40;
private final Inventory inventory;
private ModeMenu(Player challenger, FightMode current) {
this.inventory = Bukkit.createInventory(this, 27, Component.text("Wähle den Kampfmodus"));
for(int i = 0; i < this.inventory.getSize(); i++) this.inventory.setItem(i, PlaceholderItems.grayStainedGlassPane);
for(int i = 0; i < MODES.length; i++) this.inventory.setItem(SLOTS[i], this.item(MODES[i], challenger, MODES[i] == current));
sealed interface Click {
}
static void open(Player challenger, FightMode current) {
challenger.openInventory(new ModeMenu(challenger, current).getInventory());
record OwnSize(int size) implements Click {
}
record OpponentSize(int size) implements Click {
}
record Mode(FightMode mode) implements Click {
}
record Done() implements Click {
}
private final Inventory inventory;
private final Player challenger;
private final int ownPad;
int ownSize;
int opponentSize;
FightMode mode;
private ModeMenu(Player challenger, int ownPad, int ownSize, int opponentSize, FightMode mode) {
this.challenger = challenger;
this.ownPad = ownPad;
this.ownSize = ownSize;
this.opponentSize = opponentSize;
this.mode = mode;
this.inventory = Bukkit.createInventory(this, 45, Component.text("Teams und Kampfmodus"));
this.draw();
}
static void open(Player challenger, int ownPad, int ownSize, int opponentSize, FightMode mode) {
challenger.openInventory(new ModeMenu(challenger, ownPad, ownSize, opponentSize, mode).getInventory());
}
static boolean isOpen(Player player) {
return player.getOpenInventory().getTopInventory().getHolder(false) instanceof ModeMenu;
}
@Nullable FightMode modeAt(int slot) {
for(int i = 0; i < SLOTS.length; i++) {
if(SLOTS[i] == slot) return MODES[i];
Click clickAt(int slot) {
if(slot > OWN_ROW && slot <= OWN_ROW + PadLobby.MAX_TEAM_SIZE) return new OwnSize(slot - OWN_ROW);
if(slot > OPPONENT_ROW && slot <= OPPONENT_ROW + PadLobby.MAX_TEAM_SIZE) return new OpponentSize(slot - OPPONENT_ROW);
for(int i = 0; i < MODE_SLOTS.length; i++) {
if(MODE_SLOTS[i] == slot) return new Mode(MODES[i]);
}
if(slot == DONE_SLOT) return new Done();
return null;
}
private ItemStack item(FightMode mode, Player challenger, boolean selected) {
void draw() {
for(int i = 0; i < this.inventory.getSize(); i++) this.inventory.setItem(i, PlaceholderItems.grayStainedGlassPane);
int opponentPad = 1 - this.ownPad;
boolean teams = this.ownSize > 1 || this.opponentSize > 1;
this.inventory.setItem(OWN_ROW, this.label(Material.PLAYER_HEAD, "Dein Team", this.ownPad, "So viele stehen mit dir auf dem " + Style.PAD_NAMES[this.ownPad] + ".")
.withMeta(meta -> ((SkullMeta) meta).setOwningPlayer(this.challenger)).build());
this.inventory.setItem(OPPONENT_ROW, this.label(Material.SKELETON_SKULL, "Gegner", opponentPad, "So viele stellen sich auf das " + Style.PAD_NAMES[opponentPad] + ".").build());
for(int size = 1; size <= PadLobby.MAX_TEAM_SIZE; size++) {
this.inventory.setItem(OWN_ROW + size, this.sizeItem(size, this.ownPad, size == this.ownSize));
this.inventory.setItem(OPPONENT_ROW + size, this.sizeItem(size, opponentPad, size == this.opponentSize));
}
for(int i = 0; i < MODES.length; i++) {
FightMode mode = MODES[i];
this.inventory.setItem(MODE_SLOTS[i], this.modeItem(mode, teams, mode == this.mode, mode.isAllowed(this.ownSize, this.opponentSize)));
}
this.inventory.setItem(DONE_SLOT, ItemBuilder.of(Material.LIME_CONCRETE)
.displayName(Component.text("Fertig", NamedTextColor.GREEN).decoration(TextDecoration.ITALIC, false))
.lore(this.ownSize + " gegen " + this.opponentSize + ", " + this.mode.displayName(teams) + ". Jetzt fehlen nur noch die anderen auf den Feldern.")
.build());
}
private ItemBuilder label(Material material, String name, int pad, String text) {
return ItemBuilder.of(material)
.displayName(Component.text(name, Style.PAD_TEXT_COLORS[pad]).decoration(TextDecoration.ITALIC, false))
.lore(text);
}
private ItemStack sizeItem(int size, int pad, boolean selected) {
ItemBuilder builder = ItemBuilder.of(pad == 0 ? Material.RED_CONCRETE : Material.BLUE_CONCRETE)
.amount(size)
.displayName(Component.text(size + " Spieler", Style.PAD_TEXT_COLORS[pad]).decoration(TextDecoration.ITALIC, false));
builder.lore(selected ? "Ausgewählt" : "Klicken zum Auswählen", selected ? NamedTextColor.GREEN : NamedTextColor.YELLOW);
if(selected) builder.glint();
return builder.build();
}
private ItemStack modeItem(FightMode mode, boolean teams, boolean selected, boolean allowed) {
ItemBuilder builder = switch(mode) {
case OWN -> ItemBuilder.of(Material.IRON_SWORD)
.lore("Ihr kämpft beide mit eurem eigenen Inventar.");
.lore("Jeder kämpft mit seinem eigenen Inventar.");
case CHALLENGER -> ItemBuilder.of(Material.PLAYER_HEAD)
.withMeta(meta -> ((SkullMeta) meta).setOwningPlayer(challenger))
.lore("Ihr kämpft beide mit deinem Inventar.");
case OPPONENT -> ItemBuilder.of(Material.CHEST)
.lore("Ihr kämpft beide mit dem Inventar von dem, der sich auf das andere Feld stellt.");
.withMeta(meta -> ((SkullMeta) meta).setOwningPlayer(this.challenger))
.lore(teams ? "Alle kämpfen mit deinem Inventar." : "Ihr kämpft beide mit deinem Inventar.");
case OPPONENT -> ItemBuilder.of(allowed ? Material.CHEST : Material.BARRIER)
.lore(allowed
? "Ihr kämpft beide mit dem Inventar von dem, der sich auf das andere Feld stellt."
: "Geht nur bei 1 gegen 1.");
};
builder.displayName(Component.text(mode.displayName(), NamedTextColor.GOLD).decoration(TextDecoration.ITALIC, false));
builder.displayName(Component.text(mode.displayName(teams), allowed ? NamedTextColor.GOLD : NamedTextColor.DARK_GRAY).decoration(TextDecoration.ITALIC, false));
if(!allowed) return builder.build();
builder.appendLore(Component.empty());
if(selected) {
builder.glint().appendLore(Component.empty())
.appendLore(Component.text("Ausgewählt", NamedTextColor.GREEN).decoration(TextDecoration.ITALIC, false));
builder.glint().appendLore(Component.text("Ausgewählt", NamedTextColor.GREEN).decoration(TextDecoration.ITALIC, false));
} else {
builder.appendLore(Component.empty())
.appendLore(Component.text("Klicken zum Auswählen", NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false));
builder.appendLore(Component.text("Klicken zum Auswählen", NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false));
}
return builder.build();
}
@@ -1,6 +1,5 @@
package eu.mhsl.craftattack.spawn.arena.appliances.arena;
import eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.FightMode;
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -14,8 +13,8 @@ class ModeMenuListener extends ApplianceListener<Arena> {
if(!(event.getWhoClicked() instanceof Player player)) return;
if(event.getRawSlot() >= event.getView().getTopInventory().getSize()) return;
FightMode mode = menu.modeAt(event.getRawSlot());
ModeMenu.Click click = menu.clickAt(event.getRawSlot());
Lobby lobby = this.getAppliance().lobby();
if(mode != null && lobby != null) lobby.chooseMode(player, mode);
if(click != null && lobby != null) lobby.handleMenuClick(player, menu, click);
}
}
@@ -1,36 +1,25 @@
package eu.mhsl.craftattack.spawn.arena.appliances.arena.logic;
/**
* Which inventory the two fighters use. The challenger chooses it.
* Which inventory the fighters use. The challenger chooses it.
*/
public enum FightMode {
OWN("Jeder sein eigenes"),
CHALLENGER("Beide mit meinem"),
OPPONENT("Beide mit dem des Gegners");
public enum Role {
OWN,
CHALLENGER,
OPPONENT
}
private final String displayName;
FightMode(String displayName) {
this.displayName = displayName;
}
public String displayName() {
return this.displayName;
}
OPPONENT;
/**
* @return whose inventory the fighter with the given role gets
* With more than one player on a side, "the opponent's inventory" would be ambiguous.
*/
public Role kitSource(Role fighter) {
public boolean isAllowed(int ownSize, int opponentSize) {
return this != OPPONENT || (ownSize == 1 && opponentSize == 1);
}
public String displayName(boolean teams) {
return switch(this) {
case OWN -> fighter;
case CHALLENGER -> Role.CHALLENGER;
case OPPONENT -> Role.OPPONENT;
case OWN -> "Jeder sein eigenes";
case CHALLENGER -> teams ? "Alle mit meinem" : "Beide mit meinem";
case OPPONENT -> "Beide mit dem des Gegners";
};
}
}
@@ -2,36 +2,46 @@ package eu.mhsl.craftattack.spawn.arena.appliances.arena.logic;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* State of the two start pads. The first player on a pad becomes the challenger and picks the mode,
* stepping on the other pad means accepting. Once both stand there long enough, the fight starts.
* State of the two start pads. The first player on a pad becomes the challenger and picks team sizes and mode
* while standing there alone. Everyone else joins a team by stepping on a pad. Once both pads hold exactly
* the chosen number of players long enough, the fight starts.
*/
public class PadLobby {
public static final int PADS = 2;
public static final int MAX_TEAM_SIZE = 4;
public interface Listener {
/** The player is now the challenger and should pick a mode. */
void challengerSelected(UUID challenger);
/** The player is now the challenger. Only when alone on the pads the options can be changed. */
void challengerSelected(UUID challenger, boolean canConfigure);
/** Both pads are taken: give both the inventories of the chosen mode. */
void kitsApplied(UUID challenger, UUID opponent, FightMode mode);
/** The player gets the own inventory back. */
void kitReverted(UUID player);
/** The player should now hold the inventory of {@code kitOwner} (the own one to revert). */
void kitAssigned(UUID player, UUID kitOwner);
void countdown(int secondsLeft);
void countdownCancelled();
void start(UUID challenger, UUID opponent, FightMode mode, int challengerPad);
/**
* @param teams players per pad
* @param kits whose inventory each player fights with
*/
void start(List<List<UUID>> teams, UUID challenger, FightMode mode, Map<UUID, UUID> kits);
}
private final Listener listener;
private final int countdownTicks;
private final UUID[] occupants = new UUID[PADS];
private int challengerPad = -1;
private final List<List<UUID>> pads = List.of(new ArrayList<>(), new ArrayList<>());
/** Everyone on a pad, in the order they stepped on. The first one is the challenger. */
private final List<UUID> order = new ArrayList<>();
private final Map<UUID, UUID> kits = new HashMap<>();
private final int[] sizes = {1, 1};
private FightMode mode = FightMode.OWN;
private int ticksLeft = -1;
@@ -41,22 +51,26 @@ public class PadLobby {
}
/**
* @return false if the pad is already taken
* @return false if the pad is already full or the player stands on a pad
*/
public boolean enter(int pad, UUID player) {
if(this.occupants[pad] != null) return false;
if(this.padOf(player) != -1) return false;
boolean first = this.order.isEmpty();
if(!first && this.pads.get(pad).size() >= this.sizes[pad]) return false;
this.occupants[pad] = player;
if(this.challengerPad == -1) {
this.challengerPad = pad;
this.pads.get(pad).add(player);
this.order.add(player);
this.kits.put(player, player);
if(first) {
this.sizes[0] = 1;
this.sizes[1] = 1;
this.mode = FightMode.OWN;
this.listener.challengerSelected(player);
this.listener.challengerSelected(player, true);
return true;
}
this.listener.kitsApplied(this.challenger(), this.opponent(), this.mode);
this.ticksLeft = this.countdownTicks;
this.refreshKits();
if(this.isFull()) this.ticksLeft = this.countdownTicks;
return true;
}
@@ -64,35 +78,47 @@ public class PadLobby {
int pad = this.padOf(player);
if(pad == -1) return;
int other = 1 - pad;
boolean bothThere = this.isFull();
this.occupants[pad] = null;
this.listener.kitReverted(player);
if(bothThere) {
boolean wasFull = this.isFull();
boolean wasChallenger = player.equals(this.challenger());
this.pads.get(pad).remove(player);
this.order.remove(player);
this.kits.remove(player);
this.listener.kitAssigned(player, player);
if(wasFull) {
this.ticksLeft = -1;
this.listener.kitReverted(this.occupants[other]);
this.listener.countdownCancelled();
}
if(pad != this.challengerPad) return;
if(this.occupants[other] == null) {
this.challengerPad = -1;
return;
this.refreshKits();
// the longest waiting player takes over; team sizes and mode stay as chosen
if(wasChallenger && !this.order.isEmpty()) {
this.listener.challengerSelected(this.order.getFirst(), this.order.size() == 1);
}
this.challengerPad = other;
this.mode = FightMode.OWN;
this.listener.challengerSelected(this.occupants[other]);
}
/**
* Only the challenger can change the mode, and only while nobody stands on the other pad.
* Only the challenger can change the options, and only while standing alone on the pads.
*
* @param ownSize players on the challenger's pad
* @param opponentSize players on the other pad
*/
public boolean setMode(UUID player, FightMode mode) {
if(!player.equals(this.challenger()) || this.isFull()) return false;
public boolean configure(UUID player, int ownSize, int opponentSize, FightMode mode) {
if(!this.canConfigure(player)) return false;
if(ownSize < 1 || ownSize > MAX_TEAM_SIZE || opponentSize < 1 || opponentSize > MAX_TEAM_SIZE) return false;
if(!mode.isAllowed(ownSize, opponentSize)) return false;
int own = this.challengerPad();
this.sizes[own] = ownSize;
this.sizes[1 - own] = opponentSize;
this.mode = mode;
this.refreshKits();
return true;
}
public boolean canConfigure(UUID player) {
return player.equals(this.challenger()) && this.order.size() == 1;
}
/**
* Called every server tick. The countdown only runs while an arena is free.
*/
@@ -106,39 +132,67 @@ public class PadLobby {
if(this.ticksLeft % 20 == 0 && this.ticksLeft > 0) this.listener.countdown(this.ticksLeft / 20);
if(this.ticksLeft-- > 0) return;
List<List<UUID>> teams = List.of(List.copyOf(this.pads.get(0)), List.copyOf(this.pads.get(1)));
UUID challenger = this.challenger();
UUID opponent = this.opponent();
FightMode mode = this.mode;
int challengerPad = this.challengerPad;
this.occupants[0] = null;
this.occupants[1] = null;
this.challengerPad = -1;
Map<UUID, UUID> kits = Map.copyOf(this.kits);
this.pads.forEach(List::clear);
this.order.clear();
this.kits.clear();
this.sizes[0] = 1;
this.sizes[1] = 1;
this.mode = FightMode.OWN;
this.ticksLeft = -1;
this.listener.start(challenger, opponent, mode, challengerPad);
this.listener.start(teams, challenger, mode, kits);
}
/**
* Works out whose inventory everyone should hold and reports only the changes.
*/
private void refreshKits() {
UUID challenger = this.challenger();
if(challenger == null) return;
int challengerPad = this.challengerPad();
UUID firstOpponent = this.pads.get(1 - challengerPad).isEmpty() ? null : this.pads.get(1 - challengerPad).getFirst();
for(UUID player : this.order) {
UUID wanted = switch(this.mode) {
case OWN -> player;
case CHALLENGER -> challenger;
case OPPONENT -> player.equals(challenger) && firstOpponent != null ? firstOpponent : player;
};
if(wanted.equals(this.kits.get(player))) continue;
this.kits.put(player, wanted);
this.listener.kitAssigned(player, wanted);
}
}
public int padOf(UUID player) {
for(int i = 0; i < PADS; i++) {
if(player.equals(this.occupants[i])) return i;
if(this.pads.get(i).contains(player)) return i;
}
return -1;
}
public @Nullable UUID occupant(int pad) {
return this.occupants[pad];
public List<UUID> players(int pad) {
return List.copyOf(this.pads.get(pad));
}
public int size(int pad) {
return this.sizes[pad];
}
public boolean isPadFull(int pad) {
return !this.order.isEmpty() && this.pads.get(pad).size() >= this.sizes[pad];
}
public @Nullable UUID challenger() {
return this.challengerPad == -1 ? null : this.occupants[this.challengerPad];
}
public @Nullable UUID opponent() {
return this.challengerPad == -1 ? null : this.occupants[1 - this.challengerPad];
return this.order.isEmpty() ? null : this.order.getFirst();
}
public int challengerPad() {
return this.challengerPad;
UUID challenger = this.challenger();
return challenger == null ? -1 : this.padOf(challenger);
}
public FightMode mode() {
@@ -146,7 +200,7 @@ public class PadLobby {
}
public boolean isFull() {
return this.occupants[0] != null && this.occupants[1] != null;
return this.pads.get(0).size() == this.sizes[0] && this.pads.get(1).size() == this.sizes[1];
}
public boolean isCountingDown() {
@@ -154,7 +208,7 @@ public class PadLobby {
}
public boolean isIdle() {
return this.occupants[0] == null && this.occupants[1] == null;
return this.order.isEmpty();
}
/** Seconds until the start, rounded up; -1 without countdown. */
@@ -2,26 +2,21 @@ package eu.mhsl.craftattack.spawn.arena.appliances.arena.logic;
import org.junit.jupiter.api.Test;
import static eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.FightMode.Role.CHALLENGER;
import static eu.mhsl.craftattack.spawn.arena.appliances.arena.logic.FightMode.Role.OPPONENT;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.*;
class FightModeTest {
@Test
void ownKeepsEveryonesInventory() {
assertEquals(CHALLENGER, FightMode.OWN.kitSource(CHALLENGER));
assertEquals(OPPONENT, FightMode.OWN.kitSource(OPPONENT));
void opponentInventoryOnlyInOneVersusOne() {
assertTrue(FightMode.OPPONENT.isAllowed(1, 1));
assertFalse(FightMode.OPPONENT.isAllowed(1, 2));
assertFalse(FightMode.OPPONENT.isAllowed(3, 1));
assertTrue(FightMode.CHALLENGER.isAllowed(4, 3));
assertTrue(FightMode.OWN.isAllowed(2, 4));
}
@Test
void challengerKitForBoth() {
assertEquals(CHALLENGER, FightMode.CHALLENGER.kitSource(CHALLENGER));
assertEquals(CHALLENGER, FightMode.CHALLENGER.kitSource(OPPONENT));
}
@Test
void opponentKitForBoth() {
assertEquals(OPPONENT, FightMode.OPPONENT.kitSource(CHALLENGER));
assertEquals(OPPONENT, FightMode.OPPONENT.kitSource(OPPONENT));
void namesForTeams() {
assertEquals("Beide mit meinem", FightMode.CHALLENGER.displayName(false));
assertEquals("Alle mit meinem", FightMode.CHALLENGER.displayName(true));
}
}
@@ -5,7 +5,9 @@ import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
@@ -13,22 +15,21 @@ class PadLobbyTest {
private final UUID a = UUID.randomUUID();
private final UUID b = UUID.randomUUID();
private final UUID c = UUID.randomUUID();
private final UUID d = UUID.randomUUID();
private final UUID e = UUID.randomUUID();
private final List<String> events = new ArrayList<>();
private Map<UUID, UUID> startedKits;
private PadLobby lobby;
@BeforeEach
void setUp() {
this.lobby = new PadLobby(new PadLobby.Listener() {
public void challengerSelected(UUID challenger) {
PadLobbyTest.this.events.add("challenger " + PadLobbyTest.this.name(challenger));
public void challengerSelected(UUID challenger, boolean canConfigure) {
PadLobbyTest.this.events.add("challenger " + PadLobbyTest.this.name(challenger) + (canConfigure ? " configurable" : ""));
}
public void kitsApplied(UUID challenger, UUID opponent, FightMode mode) {
PadLobbyTest.this.events.add("kits " + PadLobbyTest.this.name(challenger) + PadLobbyTest.this.name(opponent) + " " + mode);
}
public void kitReverted(UUID player) {
PadLobbyTest.this.events.add("revert " + PadLobbyTest.this.name(player));
public void kitAssigned(UUID player, UUID kitOwner) {
PadLobbyTest.this.events.add("kit " + PadLobbyTest.this.name(player) + "<-" + PadLobbyTest.this.name(kitOwner));
}
public void countdown(int secondsLeft) {
@@ -39,8 +40,10 @@ class PadLobbyTest {
PadLobbyTest.this.events.add("cancelled");
}
public void start(UUID challenger, UUID opponent, FightMode mode, int challengerPad) {
PadLobbyTest.this.events.add("start " + PadLobbyTest.this.name(challenger) + PadLobbyTest.this.name(opponent) + " " + mode + " " + challengerPad);
public void start(List<List<UUID>> teams, UUID challenger, FightMode mode, Map<UUID, UUID> kits) {
PadLobbyTest.this.startedKits = kits;
PadLobbyTest.this.events.add("start " + PadLobbyTest.this.names(teams.get(0)) + " vs " + PadLobbyTest.this.names(teams.get(1))
+ " by " + PadLobbyTest.this.name(challenger) + " " + mode);
}
}, 2);
}
@@ -48,50 +51,99 @@ class PadLobbyTest {
private String name(UUID uuid) {
if(uuid.equals(this.a)) return "a";
if(uuid.equals(this.b)) return "b";
return "c";
if(uuid.equals(this.c)) return "c";
if(uuid.equals(this.d)) return "d";
return "e";
}
private String names(List<UUID> uuids) {
return uuids.stream().map(this::name).collect(Collectors.joining());
}
private void run(int ticks) {
for(int i = 0; i < ticks; i++) this.lobby.tick(true);
}
@Test
void firstOnPadBecomesChallenger() {
void firstOnPadBecomesChallengerAndMayConfigure() {
assertTrue(this.lobby.enter(1, this.a));
assertEquals(List.of("challenger a"), this.events);
assertEquals(List.of("challenger a configurable"), this.events);
assertEquals(this.a, this.lobby.challenger());
assertNull(this.lobby.opponent());
assertTrue(this.lobby.canConfigure(this.a));
assertFalse(this.lobby.isCountingDown());
}
@Test
void takenPadIsRefused() {
void oneVersusOneStartsAfterCountdown() {
this.lobby.enter(0, this.a);
assertFalse(this.lobby.enter(0, this.b));
assertFalse(this.lobby.enter(1, this.a), "a player can only take one pad");
}
@Test
void secondPlayerAppliesKitsAndStartsAfterCountdown() {
this.lobby.enter(0, this.a);
assertTrue(this.lobby.setMode(this.a, FightMode.CHALLENGER));
assertTrue(this.lobby.configure(this.a, 1, 1, FightMode.CHALLENGER));
this.lobby.enter(1, this.b);
assertFalse(this.lobby.setMode(this.a, FightMode.OWN), "mode is fixed once the opponent stands there");
for(int i = 0; i < 41; i++) this.lobby.tick(true);
assertFalse(this.lobby.configure(this.a, 1, 1, FightMode.OWN), "options are fixed once someone else joined");
this.run(41);
assertEquals(List.of(
"challenger a",
"kits ab CHALLENGER",
"challenger a configurable",
"kit b<-a",
"countdown 2",
"countdown 1",
"start ab CHALLENGER 0"
"start a vs b by a CHALLENGER"
), this.events);
assertEquals(Map.of(this.a, this.a, this.b, this.a), this.startedKits);
assertTrue(this.lobby.isIdle());
assertNull(this.lobby.challenger());
}
@Test
void onlyChallengerMayChangeMode() {
void fullPadIsRefusedAndPlayerOnlyTakesOnePad() {
this.lobby.enter(0, this.a);
assertFalse(this.lobby.setMode(this.b, FightMode.OPPONENT));
assertEquals(FightMode.OWN, this.lobby.mode());
this.lobby.enter(1, this.b);
assertFalse(this.lobby.enter(1, this.c), "1v1: the blue pad is full");
assertFalse(this.lobby.enter(0, this.b), "a player can only stand on one pad");
}
@Test
void unevenTeamsStartWhenBothPadsAreExactlyFull() {
this.lobby.enter(0, this.a);
assertTrue(this.lobby.configure(this.a, 2, 3, FightMode.OWN));
assertTrue(this.lobby.enter(0, this.b));
assertFalse(this.lobby.enter(0, this.c), "red is full with 2");
this.lobby.enter(1, this.c);
this.lobby.enter(1, this.d);
assertFalse(this.lobby.isCountingDown());
this.lobby.enter(1, this.e);
assertTrue(this.lobby.isCountingDown());
this.run(41);
assertEquals("start ab vs cde by a OWN", this.events.getLast());
}
@Test
void challengerKitGoesToEveryone() {
this.lobby.enter(0, this.a);
this.lobby.configure(this.a, 2, 2, FightMode.CHALLENGER);
this.lobby.enter(0, this.b);
this.lobby.enter(1, this.c);
this.lobby.enter(1, this.d);
assertEquals(List.of("challenger a configurable", "kit b<-a", "kit c<-a", "kit d<-a"), this.events);
}
@Test
void opponentModeOnlyForOneVersusOne() {
this.lobby.enter(0, this.a);
assertFalse(this.lobby.configure(this.a, 2, 1, FightMode.OPPONENT));
assertTrue(this.lobby.configure(this.a, 1, 1, FightMode.OPPONENT));
this.lobby.enter(1, this.b);
assertEquals("kit a<-b", this.events.getLast());
this.lobby.leave(this.b);
assertTrue(this.events.contains("kit a<-a"), "challenger gets the own inventory back");
assertTrue(this.events.contains("cancelled"));
}
@Test
void onlyChallengerMayConfigure() {
this.lobby.enter(0, this.a);
assertFalse(this.lobby.configure(this.b, 2, 2, FightMode.OWN));
assertFalse(this.lobby.configure(this.a, 5, 1, FightMode.OWN));
assertFalse(this.lobby.configure(this.a, 0, 1, FightMode.OWN));
}
@Test
@@ -101,42 +153,49 @@ class PadLobbyTest {
for(int i = 0; i < 100; i++) this.lobby.tick(false);
assertFalse(this.events.contains("countdown 2"));
assertEquals(2, this.lobby.secondsLeft());
for(int i = 0; i < 41; i++) this.lobby.tick(true);
assertTrue(this.events.contains("start ab OWN 0"));
this.run(41);
assertEquals("start a vs b by a OWN", this.events.getLast());
}
@Test
void opponentLeavingCancelsAndRevertsBoth() {
void leavingCancelsCountdownAndRevertsKit() {
this.lobby.enter(0, this.a);
this.lobby.setMode(this.a, FightMode.OPPONENT);
this.lobby.configure(this.a, 1, 1, FightMode.CHALLENGER);
this.lobby.enter(1, this.b);
this.events.clear();
this.lobby.leave(this.b);
assertEquals(List.of("revert b", "revert a", "cancelled"), this.events);
assertEquals(List.of("kit b<-b", "cancelled"), this.events);
assertEquals(this.a, this.lobby.challenger());
assertEquals(FightMode.OPPONENT, this.lobby.mode(), "challenger keeps the chosen mode");
assertEquals(FightMode.CHALLENGER, this.lobby.mode());
assertFalse(this.lobby.isCountingDown());
}
@Test
void challengerLeavingHandsOverRole() {
void longestWaitingTakesOverAndKeepsTeamSizes() {
this.lobby.enter(0, this.a);
this.lobby.setMode(this.a, FightMode.CHALLENGER);
this.lobby.configure(this.a, 2, 2, FightMode.CHALLENGER);
this.lobby.enter(1, this.b);
this.lobby.enter(0, this.c);
this.events.clear();
this.lobby.leave(this.a);
assertEquals(List.of("revert a", "revert b", "cancelled", "challenger b"), this.events);
assertEquals(List.of("kit a<-a", "kit b<-b", "kit c<-b", "challenger b"), this.events);
assertEquals(this.b, this.lobby.challenger());
assertEquals(1, this.lobby.challengerPad());
assertEquals(FightMode.OWN, this.lobby.mode());
assertFalse(this.lobby.canConfigure(this.b), "others are still there");
assertEquals(2, this.lobby.size(0));
assertEquals(2, this.lobby.size(1));
}
this.lobby.enter(0, this.c);
assertEquals("kits bc OWN", this.events.getLast());
@Test
void aloneAgainMeansConfigurableAgain() {
this.lobby.enter(0, this.a);
this.lobby.enter(1, this.b);
this.lobby.leave(this.a);
assertEquals("challenger b configurable", this.events.getLast());
assertTrue(this.lobby.canConfigure(this.b));
}
@Test
@@ -146,6 +205,6 @@ class PadLobbyTest {
assertTrue(this.lobby.isIdle());
assertNull(this.lobby.challenger());
this.lobby.leave(this.b);
assertEquals(List.of("challenger a", "revert a"), this.events);
assertEquals(List.of("challenger a configurable", "kit a<-a"), this.events);
}
}