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 05f6f93..eccb546 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 @@ -101,10 +101,6 @@ final class Lobby implements PadLobby.Listener { } 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; @@ -116,13 +112,27 @@ final class Lobby implements PadLobby.Listener { 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)); + + ", Modus: " + this.modeName() + ". Rechtsklick öffnet die Auswahl wieder.", 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; + + int ownPad = this.pads.challengerPad(); + String problem = switch(this.pads.configure(player.getUniqueId(), ownSize, opponentSize, mode)) { + case OK -> null; + case NOT_ALLOWED -> "Der Countdown läuft schon"; + case OWN_TOO_SMALL -> "Auf deinem Feld stehen schon " + this.pads.players(ownPad).size(); + case OPPONENT_TOO_SMALL -> "Auf dem anderen Feld stehen schon " + this.pads.players(1 - ownPad).size(); + case INVALID -> "Das geht so nicht"; + }; + if(problem != null) { + Style.play(player, Sound.BLOCK_NOTE_BLOCK_BASS, 0.6f); + player.sendActionBar(Component.text(problem, NamedTextColor.RED)); + if(!this.pads.canConfigure(player.getUniqueId())) player.closeInventory(); + return; + } menu.ownSize = ownSize; menu.opponentSize = opponentSize; menu.mode = mode; @@ -130,6 +140,17 @@ final class Lobby implements PadLobby.Listener { InteractSounds.of(player).click(); } + /** + * Right click on the pad: the challenger opens the options again. + */ + boolean openMenu(Player player) { + if(!this.pads.canConfigure(player.getUniqueId()) || ModeMenu.isOpen(player)) return false; + int pad = this.pads.challengerPad(); + InteractSounds.of(player).open(); + ModeMenu.open(player, pad, this.pads.size(pad), this.pads.size(1 - pad), this.pads.mode()); + return true; + } + private boolean isTeamFight() { return this.pads.size(0) > 1 || this.pads.size(1) > 1; } @@ -168,24 +189,29 @@ final class Lobby implements PadLobby.Listener { } @Override - public void challengerSelected(UUID challenger, boolean canConfigure) { + public void challengerSelected(UUID challenger, boolean first) { Player player = Bukkit.getPlayer(challenger); if(player == null) return; - if(!canConfigure) { - player.sendMessage(Style.message("Du führst jetzt die Herausforderung an. Teams und Modus bleiben wie gewählt.")); + if(!first) { + player.sendMessage(Style.message("Du führst jetzt die Herausforderung an. Rechtsklick auf dem Feld öffnet die Auswahl.")); + Style.play(player, Sound.BLOCK_NOTE_BLOCK_PLING, 1.8f); 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()); + player.sendMessage(Style.message("Du forderst heraus. Wähle Teamgröße und Modus, Rechtsklick auf dem Feld öffnet die Auswahl wieder. Wer sich auf die Felder stellt, macht mit.")); + this.openMenu(player); + } + + @Override + public void countdownStarted() { + UUID challenger = this.pads.challenger(); + Player player = challenger == null ? null : Bukkit.getPlayer(challenger); + if(player != null && ModeMenu.isOpen(player)) player.closeInventory(); } @Override public void kitAssigned(UUID uuid, UUID kitOwner) { Player player = Bukkit.getPlayer(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); 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 795253e..09b2b2d 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 @@ -3,6 +3,9 @@ package eu.mhsl.craftattack.spawn.arena.appliances.arena; import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; import org.bukkit.event.inventory.InventoryClickEvent; class ModeMenuListener extends ApplianceListener { @@ -17,4 +20,13 @@ class ModeMenuListener extends ApplianceListener { Lobby lobby = this.getAppliance().lobby(); if(click != null && lobby != null) lobby.handleMenuClick(player, menu, click); } + + @EventHandler + public void onRightClick(PlayerInteractEvent event) { + if(event.getHand() != EquipmentSlot.HAND) return; + if(event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) return; + if(this.getAppliance().state(event.getPlayer()) != Arena.PlayerState.PAD) return; + Lobby lobby = this.getAppliance().lobby(); + if(lobby != null && lobby.openMenu(event.getPlayer())) event.setCancelled(true); + } } 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 782a1c8..f7932a8 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 @@ -18,12 +18,15 @@ public class PadLobby { public static final int MAX_TEAM_SIZE = 4; public interface Listener { - /** The player is now the challenger. Only when alone on the pads the options can be changed. */ - void challengerSelected(UUID challenger, boolean canConfigure); + /** The player is now the challenger and may change the options until the countdown runs. */ + void challengerSelected(UUID challenger, boolean first); /** The player should now hold the inventory of {@code kitOwner} (the own one to revert). */ void kitAssigned(UUID player, UUID kitOwner); + /** Both pads are exactly full, the options are fixed now. */ + void countdownStarted(); + void countdown(int secondsLeft); void countdownCancelled(); @@ -70,7 +73,10 @@ public class PadLobby { } this.refreshKits(); - if(this.isFull()) this.ticksLeft = this.countdownTicks; + if(this.isFull()) { + this.ticksLeft = this.countdownTicks; + this.listener.countdownStarted(); + } return true; } @@ -93,30 +99,46 @@ public class PadLobby { 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.listener.challengerSelected(this.order.getFirst(), false); } } + public enum ConfigureResult { + OK, + NOT_ALLOWED, + OWN_TOO_SMALL, + OPPONENT_TOO_SMALL, + INVALID + } + /** - * Only the challenger can change the options, and only while standing alone on the pads. + * Only the challenger can change the options, and only until the countdown runs. + * Teams cannot be smaller than the players already standing there. * * @param ownSize players on the challenger's pad * @param opponentSize players on the other pad */ - 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; + public ConfigureResult configure(UUID player, int ownSize, int opponentSize, FightMode mode) { + if(!this.canConfigure(player)) return ConfigureResult.NOT_ALLOWED; + if(ownSize < 1 || ownSize > MAX_TEAM_SIZE || opponentSize < 1 || opponentSize > MAX_TEAM_SIZE) return ConfigureResult.INVALID; + if(!mode.isAllowed(ownSize, opponentSize)) return ConfigureResult.INVALID; int own = this.challengerPad(); + if(ownSize < this.pads.get(own).size()) return ConfigureResult.OWN_TOO_SMALL; + if(opponentSize < this.pads.get(1 - own).size()) return ConfigureResult.OPPONENT_TOO_SMALL; + this.sizes[own] = ownSize; this.sizes[1 - own] = opponentSize; this.mode = mode; this.refreshKits(); - return true; + if(this.isFull()) { + this.ticksLeft = this.countdownTicks; + this.listener.countdownStarted(); + } + return ConfigureResult.OK; } public boolean canConfigure(UUID player) { - return player.equals(this.challenger()) && this.order.size() == 1; + return player.equals(this.challenger()) && !this.isCountingDown(); } /** 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 326d3cf..5462985 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 @@ -24,8 +24,12 @@ class PadLobbyTest { @BeforeEach void setUp() { this.lobby = new PadLobby(new PadLobby.Listener() { - public void challengerSelected(UUID challenger, boolean canConfigure) { - PadLobbyTest.this.events.add("challenger " + PadLobbyTest.this.name(challenger) + (canConfigure ? " configurable" : "")); + public void challengerSelected(UUID challenger, boolean first) { + PadLobbyTest.this.events.add("challenger " + PadLobbyTest.this.name(challenger) + (first ? " first" : "")); + } + + public void countdownStarted() { + PadLobbyTest.this.events.add("countdown started"); } public void kitAssigned(UUID player, UUID kitOwner) { @@ -67,7 +71,7 @@ class PadLobbyTest { @Test void firstOnPadBecomesChallengerAndMayConfigure() { assertTrue(this.lobby.enter(1, this.a)); - assertEquals(List.of("challenger a configurable"), this.events); + assertEquals(List.of("challenger a first"), this.events); assertEquals(this.a, this.lobby.challenger()); assertTrue(this.lobby.canConfigure(this.a)); assertFalse(this.lobby.isCountingDown()); @@ -76,14 +80,15 @@ class PadLobbyTest { @Test void oneVersusOneStartsAfterCountdown() { this.lobby.enter(0, this.a); - assertTrue(this.lobby.configure(this.a, 1, 1, FightMode.CHALLENGER)); + assertEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 1, 1, FightMode.CHALLENGER)); this.lobby.enter(1, this.b); - assertFalse(this.lobby.configure(this.a, 1, 1, FightMode.OWN), "options are fixed once someone else joined"); + assertEquals(PadLobby.ConfigureResult.NOT_ALLOWED, this.lobby.configure(this.a, 1, 1, FightMode.OWN), "options are fixed during the countdown"); this.run(41); assertEquals(List.of( - "challenger a configurable", + "challenger a first", "kit b<-a", + "countdown started", "countdown 2", "countdown 1", "start a vs b by a CHALLENGER" @@ -103,7 +108,7 @@ class PadLobbyTest { @Test void unevenTeamsStartWhenBothPadsAreExactlyFull() { this.lobby.enter(0, this.a); - assertTrue(this.lobby.configure(this.a, 2, 3, FightMode.OWN)); + assertEquals(PadLobby.ConfigureResult.OK, 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); @@ -122,16 +127,16 @@ class PadLobbyTest { 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); + assertEquals(List.of("challenger a first", "kit b<-a", "kit c<-a", "kit d<-a", "countdown started"), 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)); + assertNotEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 2, 1, FightMode.OPPONENT)); + assertEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 1, 1, FightMode.OPPONENT)); this.lobby.enter(1, this.b); - assertEquals("kit a<-b", this.events.getLast()); + assertTrue(this.events.contains("kit a<-b")); this.lobby.leave(this.b); assertTrue(this.events.contains("kit a<-a"), "challenger gets the own inventory back"); @@ -141,9 +146,9 @@ class PadLobbyTest { @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)); + assertNotEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.b, 2, 2, FightMode.OWN)); + assertNotEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 5, 1, FightMode.OWN)); + assertNotEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 0, 1, FightMode.OWN)); } @Test @@ -184,20 +189,47 @@ class PadLobbyTest { assertEquals(List.of("kit a<-a", "kit b<-b", "kit c<-b", "challenger b"), this.events); assertEquals(this.b, this.lobby.challenger()); - assertFalse(this.lobby.canConfigure(this.b), "others are still there"); + assertTrue(this.lobby.canConfigure(this.b), "the new challenger may change the options"); assertEquals(2, this.lobby.size(0)); assertEquals(2, this.lobby.size(1)); } @Test - void aloneAgainMeansConfigurableAgain() { + void handOverWhenAlone() { this.lobby.enter(0, this.a); this.lobby.enter(1, this.b); this.lobby.leave(this.a); - assertEquals("challenger b configurable", this.events.getLast()); + assertEquals("challenger b", this.events.getLast()); assertTrue(this.lobby.canConfigure(this.b)); } + @Test + void challengerMayChangeOptionsWhileOthersWait() { + this.lobby.enter(0, this.a); + this.lobby.configure(this.a, 2, 2, FightMode.OWN); + this.lobby.enter(0, this.b); + this.lobby.enter(1, this.c); + + assertEquals(PadLobby.ConfigureResult.OWN_TOO_SMALL, this.lobby.configure(this.a, 1, 2, FightMode.OWN), + "two already stand on the own pad"); + assertEquals(2, this.lobby.size(0)); + + this.events.clear(); + assertEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 2, 1, FightMode.CHALLENGER)); + assertEquals(List.of("kit b<-a", "kit c<-a", "countdown started"), this.events, "shrinking to the players there starts the countdown"); + assertTrue(this.lobby.isCountingDown()); + } + + @Test + void opponentSideCannotShrinkBelowPlayers() { + this.lobby.enter(0, this.a); + this.lobby.configure(this.a, 1, 3, FightMode.OWN); + this.lobby.enter(1, this.b); + this.lobby.enter(1, this.c); + assertEquals(PadLobby.ConfigureResult.OPPONENT_TOO_SMALL, this.lobby.configure(this.a, 1, 1, FightMode.OWN)); + assertEquals(PadLobby.ConfigureResult.OK, this.lobby.configure(this.a, 1, 4, FightMode.OWN)); + } + @Test void lastPlayerLeavingResetsLobby() { this.lobby.enter(0, this.a); @@ -205,6 +237,6 @@ class PadLobbyTest { assertTrue(this.lobby.isIdle()); assertNull(this.lobby.challenger()); this.lobby.leave(this.b); - assertEquals(List.of("challenger a configurable", "kit a<-a"), this.events); + assertEquals(List.of("challenger a first", "kit a<-a"), this.events); } }