From ae3bde626fcef480dcc2dc9599cd4774b8d3f166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Thu, 15 Oct 2026 23:21:20 +0200 Subject: [PATCH] added team fights up to 4 vs 4 to the arena --- .../spawn/arena/appliances/arena/Arena.java | 11 +- .../spawn/arena/appliances/arena/Fight.java | 286 ++++++++++++------ .../arena/appliances/arena/FightListener.java | 12 +- .../spawn/arena/appliances/arena/Lobby.java | 214 +++++++------ .../arena/appliances/arena/ModeMenu.java | 122 ++++++-- .../appliances/arena/ModeMenuListener.java | 5 +- .../appliances/arena/logic/FightMode.java | 37 +-- .../appliances/arena/logic/PadLobby.java | 160 ++++++---- .../appliances/arena/logic/FightModeTest.java | 25 +- .../appliances/arena/logic/PadLobbyTest.java | 155 +++++++--- 10 files changed, 664 insertions(+), 363 deletions(-) diff --git a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/Arena.java b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/Arena.java index 6d3baa1..a9ceaa0 100644 --- a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/Arena.java +++ b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/Arena.java @@ -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> teams, FightMode mode, Map 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() { diff --git a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/Fight.java b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/Fight.java index d8733b2..d160791 100644 --- a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/Fight.java +++ b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/Fight.java @@ -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> teams; + private final Map names = new HashMap<>(); + private final List> alive; private final FightMode mode; + private final Map kits; private final BossBar bossBar; private final Set 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> teams, FightMode mode, Map 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) 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]); - 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)); - player.closeInventory(); - player.teleport(this.slot.settings.spawns()[i]); - Arena.restore(player, GameMode.SURVIVAL); - InventoryStore.apply(player, kitOwner); - - 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])), - Title.Times.times(Duration.ZERO, Duration.ofMillis(1500), Duration.ofMillis(500)) - )); - Style.play(player, Sound.EVENT_RAID_HORN, 1.2f); + for(int team = 0; team < 2; team++) { + List members = this.teams.get(team); + for(int i = 0; i < members.size(); i++) { + Player player = Bukkit.getPlayer(members.get(i)); + if(player == null) continue; + this.arena.setState(player, Arena.PlayerState.FIGHT); + this.arena.teams().join(player, Teams.fight(team)); + player.closeInventory(); + player.teleport(this.spawnFor(team, i, members.size())); + Arena.restore(player, GameMode.SURVIVAL); + 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(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 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,17 +375,19 @@ 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 fighterPlayers() { + private List alivePlayers() { List players = new ArrayList<>(); - for(UUID uuid : this.fighters) { - Player player = Bukkit.getPlayer(uuid); - if(player != null) players.add(player); + for(Set team : this.alive) { + for(UUID uuid : team) { + Player player = Bukkit.getPlayer(uuid); + if(player != null) players.add(player); + } } return players; } diff --git a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/FightListener.java b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/FightListener.java index b4f02b1..0562df7 100644 --- a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/FightListener.java +++ b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/FightListener.java @@ -22,19 +22,23 @@ import org.bukkit.inventory.ItemStack; class FightListener extends ApplianceListener { 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) diff --git a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/Lobby.java b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/Lobby.java index 6f1c15a..05f6f93 100644 --- a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/Lobby.java +++ b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/Lobby.java @@ -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; } + 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(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(); - player.closeInventory(); - player.sendActionBar(Component.text("Modus: " + mode.displayName(), NamedTextColor.GOLD)); + } + + 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.")); - InteractSounds.of(player).open(); - ModeMenu.open(player, 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); + 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(); + int pad = this.pads.challengerPad(); + ModeMenu.open(player, pad, this.pads.size(pad), this.pads.size(1 - pad), this.pads.mode()); } @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; - 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])), - 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); + 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(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; - player.clearTitle(); - player.sendActionBar(Component.text("Abgebrochen", NamedTextColor.RED)); - Style.play(player, Sound.BLOCK_NOTE_BLOCK_BASS, 0.5f); + 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> teams, UUID challenger, FightMode mode, Map kits) { + List> 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 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 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() + } 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 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() - .append(Component.text("um herauszufordern", NamedTextColor.WHITE)); + 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,18 +288,18 @@ 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(int strand = 0; strand < 2; strand++) { - double angle = this.ticks * speed + strand * Math.PI; - double height = (this.ticks % 40) / 20.0; - Location point = occupant.getLocation().add(Math.cos(angle) * 0.7, height, Math.sin(angle) * 0.7); - Particle.DUST.builder().location(point).data(new Particle.DustOptions(color, 1f)).receivers(48).spawn(); + 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; + Location point = occupant.getLocation().add(Math.cos(angle) * 0.7, height, Math.sin(angle) * 0.7); + Particle.DUST.builder().location(point).data(new Particle.DustOptions(color, 1f)).receivers(48).spawn(); + } } } } diff --git a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/ModeMenu.java b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/ModeMenu.java index db49275..6c07806 100644 --- a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/ModeMenu.java +++ b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/ModeMenu.java @@ -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(); } diff --git a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/ModeMenuListener.java b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/ModeMenuListener.java index 8ca40a8..795253e 100644 --- a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/ModeMenuListener.java +++ b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/ModeMenuListener.java @@ -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 { 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); } } diff --git a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/FightMode.java b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/FightMode.java index 560cdb9..6dc00af 100644 --- a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/FightMode.java +++ b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/FightMode.java @@ -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 { - CHALLENGER, - OPPONENT - } - - private final String displayName; - - FightMode(String displayName) { - this.displayName = displayName; - } - - public String displayName() { - return this.displayName; - } + OWN, + CHALLENGER, + 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"; }; } } diff --git a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/PadLobby.java b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/PadLobby.java index 9eecd13..782a1c8 100644 --- a/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/PadLobby.java +++ b/arena/src/main/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/PadLobby.java @@ -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> teams, UUID challenger, FightMode mode, Map kits); } private final Listener listener; private final int countdownTicks; - private final UUID[] occupants = new UUID[PADS]; - private int challengerPad = -1; + private final List> 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 order = new ArrayList<>(); + private final Map 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> 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 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 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. */ diff --git a/arena/src/test/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/FightModeTest.java b/arena/src/test/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/FightModeTest.java index 336a9b6..10037f8 100644 --- a/arena/src/test/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/FightModeTest.java +++ b/arena/src/test/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/FightModeTest.java @@ -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)); } } diff --git a/arena/src/test/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/PadLobbyTest.java b/arena/src/test/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/PadLobbyTest.java index 5ed9738..326d3cf 100644 --- a/arena/src/test/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/PadLobbyTest.java +++ b/arena/src/test/java/eu/mhsl/craftattack/spawn/arena/appliances/arena/logic/PadLobbyTest.java @@ -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 events = new ArrayList<>(); + private Map 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> teams, UUID challenger, FightMode mode, Map 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 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); } }