From c89026c6e148e16bc0d378b13375a780f000acf6 Mon Sep 17 00:00:00 2001 From: Jonah Bauer Date: Mon, 10 Jan 2022 03:10:32 +0100 Subject: [PATCH] improved menus in libGDX client --- .../wizard/client/libgdx/Client.java | 13 ++ .../libgdx/actors/AutoFocusScrollPane.java | 6 +- .../client/libgdx/actors/game/CardStack.java | 37 ++++++ .../client/libgdx/screens/ConnectScreen.java | 23 ++-- .../libgdx/screens/CreateGameScreen.java | 27 +++-- .../client/libgdx/screens/LoadingScreen.java | 1 - .../client/libgdx/screens/LobbyScreen.java | 51 +++----- .../client/libgdx/screens/MainMenuScreen.java | 5 +- .../client/libgdx/screens/MenuScreen.java | 21 +--- .../client/libgdx/screens/WaitingScreen.java | 38 ++++-- .../libgdx/state/AwaitingConnection.java | 3 +- .../libgdx/state/AwaitingJoinLobby.java | 2 +- .../libgdx/state/AwaitingJoinSession.java | 9 +- .../wizard/client/libgdx/state/Lobby.java | 71 +++++++++-- .../wizard/client/libgdx/state/Menu.java | 27 ++++- .../wizard/client/libgdx/state/Session.java | 113 +++++++++++++----- .../main/resources/i18n/messages.properties | 3 + .../resources/i18n/messages_de.properties | 9 +- .../core/src/main/textures/menu/not_ready.png | Bin 0 -> 1398 bytes .../core/src/main/textures/menu/ready.png | Bin 0 -> 1705 bytes .../src/main/textures/uiskin/selection.9.png | Bin 185 -> 102 bytes 21 files changed, 314 insertions(+), 145 deletions(-) create mode 100644 wizard-client/wizard-client-libgdx/core/src/main/textures/menu/not_ready.png create mode 100644 wizard-client/wizard-client-libgdx/core/src/main/textures/menu/ready.png diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/Client.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/Client.java index 79d2882..fd9f364 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/Client.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/Client.java @@ -8,6 +8,9 @@ import eu.jonahbauer.wizard.common.messages.server.ServerMessage; import lombok.Getter; import lombok.extern.log4j.Log4j2; +import java.util.Optional; +import java.util.function.BiFunction; + @Log4j2 public class Client extends TimeoutContext { @@ -55,4 +58,14 @@ public class Client extends TimeoutContext { public void send(ClientMessage message) { getSocket().send(message.toString()); } + + public void execute(Class stateClass, BiFunction> transition) { + execute(s -> { + if (stateClass.isInstance(s)) { + return transition.apply(stateClass.cast(s), this); + } else { + return Optional.empty(); + } + }); + } } \ No newline at end of file diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/AutoFocusScrollPane.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/AutoFocusScrollPane.java index 6027a2d..9a47981 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/AutoFocusScrollPane.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/AutoFocusScrollPane.java @@ -32,11 +32,13 @@ public class AutoFocusScrollPane extends ScrollPane { private void init() { addListener(new InputListener() { public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) { - getStage().setScrollFocus(AutoFocusScrollPane.this); + var stage = getStage(); + if (stage != null) stage.setScrollFocus(AutoFocusScrollPane.this); } public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) { - getStage().setScrollFocus(null); + var stage = getStage(); + if (stage != null) stage.setScrollFocus(null); } }); } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardStack.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardStack.java index 6b47614..18b3f42 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardStack.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/game/CardStack.java @@ -71,10 +71,47 @@ public class CardStack extends Group { addActor(hover); } + public List removeAll() { + var out = cards.stream().map(Entry::getActor).toList(); + clearChildren(true); + return out; + } + + @Override + public Actor removeActorAt(int index, boolean unfocus) { + var actor = super.removeActorAt(index, unfocus); + cards.remove(index); + return actor; + } + public void setHoverBounds(float x, float y, float width, float height) { hover.setBounds(x, y, width, height); } + @Override + @Deprecated + public void addActor(Actor actor) { + super.addActor(actor); + } + + @Override + @Deprecated + public void addActorAfter(Actor actorAfter, Actor actor) { + super.addActorAfter(actorAfter, actor); + } + + @Override + @Deprecated + public void addActorAt(int index, Actor actor) { + super.addActorAt(index, actor); + } + + @Override + @Deprecated + public void addActorBefore(Actor actorBefore, Actor actor) { + super.addActorBefore(actorBefore, actor); + } + @Data private static class Entry { private final CardActor actor; diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/ConnectScreen.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/ConnectScreen.java index 39f50c4..29f1322 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/ConnectScreen.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/ConnectScreen.java @@ -7,13 +7,12 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextButton; import com.badlogic.gdx.scenes.scene2d.ui.TextField; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.utils.Align; -import eu.jonahbauer.wizard.client.libgdx.ClientSocket; import eu.jonahbauer.wizard.client.libgdx.WizardGame; -import eu.jonahbauer.wizard.client.libgdx.state.AwaitingConnection; -import eu.jonahbauer.wizard.common.messages.server.ServerMessage; -import lombok.SneakyThrows; +import eu.jonahbauer.wizard.client.libgdx.listeners.ResetErrorListener; +import eu.jonahbauer.wizard.client.libgdx.state.Menu; import java.net.URI; +import java.net.URISyntaxException; public class ConnectScreen extends MenuScreen { @@ -27,18 +26,19 @@ public class ConnectScreen extends MenuScreen { } private final ChangeListener listener = new ChangeListener() { - @SneakyThrows @Override public void changed(ChangeEvent event, Actor actor) { if (actor == buttonBack) { - game.setScreen(new MainMenuScreen(game)); + game.getClient().execute(Menu.class, Menu::showMenuScreen); sfxClick(); } else if (actor == buttonConnect) { - // TODO error handling, uri syntax, etc. - ClientSocket socket = new ClientSocket(new URI(uri.getText())); - game.getClient().setSocket(socket); - game.getClient().transition(new AwaitingConnection()); - socket.connect(); + try { + var uri = new URI(ConnectScreen.this.uri.getText()); + game.getClient().execute(Menu.class, (s, c) -> s.connect(c, uri)); + } catch (URISyntaxException e) { + uri.setStyle(game.data.skin.get("error", TextField.TextFieldStyle.class)); + } + sfxClick(); } } @@ -63,6 +63,7 @@ public class ConnectScreen extends MenuScreen { uri.setMessageText(game.messages.get("menu.connect.uri.hint")); uri.setSize(0.4f * WizardGame.WIDTH, 64); uri.setPosition(0.5f * (WizardGame.WIDTH - uri.getWidth()), 0.45f * (WizardGame.HEIGHT - uri.getHeight())); + uri.addListener(new ResetErrorListener(game.data.skin)); Gdx.input.setInputProcessor(game.data.stage); game.data.stage.addActor(buttonBack); diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/CreateGameScreen.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/CreateGameScreen.java index ac82020..d07999e 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/CreateGameScreen.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/CreateGameScreen.java @@ -7,9 +7,7 @@ import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.utils.Array; import eu.jonahbauer.wizard.client.libgdx.WizardGame; import eu.jonahbauer.wizard.client.libgdx.listeners.ResetErrorListener; -import eu.jonahbauer.wizard.client.libgdx.state.AwaitingJoinSession; import eu.jonahbauer.wizard.client.libgdx.state.Lobby; -import eu.jonahbauer.wizard.common.messages.client.CreateSessionMessage; import eu.jonahbauer.wizard.common.model.Configuration; import lombok.extern.log4j.Log4j2; @@ -24,13 +22,16 @@ public class CreateGameScreen extends MenuScreen { private TextField timeOut; private SelectBox configurations; - private final Lobby state; + private String oldPlayerName; private final ChangeListener listener = new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { if (actor == buttonBack) { - game.setScreen(new LobbyScreen(game)); + game.getClient().execute(Lobby.class, (s, c) -> { + s.setPlayerName(playerName.getText()); + return s.showListScreen(c); + }); sfxClick(); } else if (actor == buttonContinue) { create(); @@ -39,9 +40,9 @@ public class CreateGameScreen extends MenuScreen { } }; - public CreateGameScreen(WizardGame game) { + public CreateGameScreen(WizardGame game, String playerName) { super(game); - this.state = (Lobby) game.getClient().getState(); + this.oldPlayerName = playerName; } @Override @@ -61,7 +62,7 @@ public class CreateGameScreen extends MenuScreen { sessionName.addListener(errorListener); sessionName.setProgrammaticChangeEvents(true); - playerName = new TextField(state.getPlayerName(), game.data.skin); + playerName = new TextField(oldPlayerName, game.data.skin); playerName.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.45f); playerName.setSize(0.4f * WizardGame.WIDTH, 64); playerName.addListener(errorListener); @@ -70,18 +71,17 @@ public class CreateGameScreen extends MenuScreen { @Override public void changed(ChangeEvent event, Actor actor) { - var old = state.getPlayerName(); - state.setPlayerName(playerName.getText()); - var player = playerName.getText(); var session = sessionName.getText(); - if (session.isEmpty() || session.equals(format.formatted(old))) { + if (session.isEmpty() || session.equals(format.formatted(oldPlayerName))) { if (player.isEmpty()) { sessionName.setText(""); } else { sessionName.setText(format.formatted(player)); } } + + oldPlayerName = playerName.getText(); } }; playerName.addListener(playerNameListener); @@ -163,10 +163,11 @@ public class CreateGameScreen extends MenuScreen { error = true; } + var fConfig = config; + var fTimeout = timeout; if (!error) { var client = game.getClient(); - client.transition(new AwaitingJoinSession(null, sessionName, config, playerName)); - client.send(new CreateSessionMessage(sessionName, playerName, 1000 * timeout, config)); + client.execute(Lobby.class, (s, c) -> s.createSession(c, sessionName, fConfig, 1000 * fTimeout, playerName)); } } } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/LoadingScreen.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/LoadingScreen.java index 4b6eb04..710e12b 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/LoadingScreen.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/LoadingScreen.java @@ -2,7 +2,6 @@ package eu.jonahbauer.wizard.client.libgdx.screens; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.scenes.scene2d.ui.Label; -import com.badlogic.gdx.scenes.scene2d.ui.TextButton; import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup; import eu.jonahbauer.wizard.client.libgdx.WizardGame; diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/LobbyScreen.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/LobbyScreen.java index 00e5e7d..c5d302f 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/LobbyScreen.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/LobbyScreen.java @@ -7,16 +7,10 @@ import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import eu.jonahbauer.wizard.client.libgdx.WizardGame; import eu.jonahbauer.wizard.client.libgdx.actors.AutoFocusScrollPane; import eu.jonahbauer.wizard.client.libgdx.listeners.ResetErrorListener; -import eu.jonahbauer.wizard.client.libgdx.state.AwaitingJoinSession; import eu.jonahbauer.wizard.client.libgdx.state.Lobby; -import eu.jonahbauer.wizard.client.libgdx.state.Menu; -import eu.jonahbauer.wizard.common.messages.client.JoinSessionMessage; import eu.jonahbauer.wizard.common.messages.data.SessionData; import lombok.extern.log4j.Log4j2; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; import java.util.stream.StreamSupport; @@ -31,33 +25,34 @@ public class LobbyScreen extends MenuScreen { private Label labelSessionName; private Label labelSessionPlayerCount; private Label labelSessionConfiguration; - - private UUID selectedSession; + + private final String oldPlayerName; + private SessionData selectedSession; private List sessions; private ScrollPane sessionListContainer; - private final Map sessionData = new HashMap<>(); - - private final Lobby state; private final ChangeListener listener = new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { if (actor == buttonBack) { - game.getClient().transition(new Menu()); + game.getClient().execute(Lobby.class, Lobby::disconnect); sfxClick(); } else if (actor == buttonJoin) { join(); sfxClick(); } else if (actor == buttonCreate) { - game.setScreen(new CreateGameScreen(game)); + game.getClient().execute(Lobby.class, (s, c) -> { + s.setPlayerName(playerName.getText()); + return s.showCreateScreen(c); + }); sfxClick(); } } }; - public LobbyScreen(WizardGame game) { + public LobbyScreen(WizardGame game, String playerName) { super(game); - this.state = (Lobby) game.getClient().getState(); + this.oldPlayerName = playerName; } @Override @@ -112,13 +107,11 @@ public class LobbyScreen extends MenuScreen { } public void addSession(SessionData session) { - this.sessionData.put(session.getUuid(), session); this.sessions.getItems().add(session); this.sessions.invalidateHierarchy(); } public void removeSession(UUID session) { - this.sessionData.remove(session); var items = this.sessions.getItems(); for (int i = 0; i < items.size; i++) { if (items.get(i).getUuid().equals(session)) { @@ -128,13 +121,12 @@ public class LobbyScreen extends MenuScreen { } this.sessions.invalidateHierarchy(); - if (session.equals(selectedSession)) { + if (selectedSession != null && selectedSession.getUuid().equals(session)) { updateData(null); } } public void modifySession(SessionData session) { - this.sessionData.put(session.getUuid(), session); var items = this.sessions.getItems(); for (int i = 0; i < items.size; i++) { if (items.get(i).getUuid().equals(session.getUuid())) { @@ -144,17 +136,16 @@ public class LobbyScreen extends MenuScreen { } this.sessions.invalidateHierarchy(); - if (session.getUuid().equals(selectedSession)) { + if (selectedSession != null && selectedSession.getUuid().equals(session.getUuid())) { updateData(session); } } public void setSessions(SessionData... sessions) { - this.sessionData.clear(); - Arrays.stream(sessions).forEach(s -> this.sessionData.put(s.getUuid(), s)); var items = this.sessions.getItems(); items.clear(); items.addAll(sessions); + this.selectedSession = null; this.sessions.invalidateHierarchy(); } @@ -163,7 +154,7 @@ public class LobbyScreen extends MenuScreen { labelSessionName.setText(data.getName()); labelSessionPlayerCount.setText(Integer.toString(data.getPlayerCount())); labelSessionConfiguration.setText(data.getConfiguration().toString()); - selectedSession = data.getUuid(); + selectedSession = data; } else { labelSessionName.setText(""); labelSessionPlayerCount.setText(""); @@ -175,14 +166,8 @@ public class LobbyScreen extends MenuScreen { private Table createInfoTable() { float infoTableWidth = 0.3f * WizardGame.WIDTH - 20; - playerName = new TextField(state.getPlayerName(), game.data.skin); + playerName = new TextField(oldPlayerName, game.data.skin); playerName.addListener(new ResetErrorListener(game.data.skin)); - playerName.addListener(new ChangeListener() { - @Override - public void changed(ChangeEvent event, Actor actor) { - state.setPlayerName(playerName.getText()); - } - }); labelSessionName = new Label("", game.data.skin, "textfield"); labelSessionConfiguration = new Label("", game.data.skin, "textfield"); @@ -218,8 +203,7 @@ public class LobbyScreen extends MenuScreen { error = true; } - SessionData session = sessionData.get(selectedSession); - if (session == null) { + if (selectedSession == null) { log.warn("Please select a session."); this.sessionListContainer.setStyle(game.data.skin.get("error", ScrollPane.ScrollPaneStyle.class)); error = true; @@ -227,8 +211,7 @@ public class LobbyScreen extends MenuScreen { if (!error) { var client = game.getClient(); - client.transition(new AwaitingJoinSession(session.getUuid(), session.getName(), session.getConfiguration(), playerName)); - client.send(new JoinSessionMessage(selectedSession, playerName)); + client.execute(Lobby.class, (s, c) -> s.joinSession(client, selectedSession, playerName)); } } } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/MainMenuScreen.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/MainMenuScreen.java index c4994dc..fe8ca26 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/MainMenuScreen.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/MainMenuScreen.java @@ -5,7 +5,7 @@ import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.TextButton; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import eu.jonahbauer.wizard.client.libgdx.WizardGame; -import eu.jonahbauer.wizard.common.messages.server.ServerMessage; +import eu.jonahbauer.wizard.client.libgdx.state.Menu; public class MainMenuScreen extends MenuScreen { @@ -16,11 +16,10 @@ public class MainMenuScreen extends MenuScreen { @Override public void changed(ChangeEvent event, Actor actor) { if (actor == buttonPlay) { - game.setScreen(new ConnectScreen(game)); + game.getClient().execute(Menu.class, Menu::showConnectScreen); sfxClick(); } else if (actor == buttonQuit) { sfxClick(); - dispose = true; Gdx.app.exit(); } } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/MenuScreen.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/MenuScreen.java index 3b359a1..64ea37a 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/MenuScreen.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/MenuScreen.java @@ -10,9 +10,6 @@ public abstract class MenuScreen implements Screen { protected WizardGame game; - // shared game.data between all menu screens - protected boolean dispose; - public MenuScreen(WizardGame game) { this.game = game; } @@ -71,26 +68,16 @@ public abstract class MenuScreen implements Screen { } @Override - public void pause() { - - } + public void pause() {} @Override - public void resume() { - - } + public void resume() {} @Override - public void hide() { - - } + public void hide() {} @Override - public void dispose() { - if (dispose) { - game.data.dispose(); - } - } + public void dispose() {} protected void sfxClick() { game.data.buttonClickSound.play(0.6f); diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/WaitingScreen.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/WaitingScreen.java index 91e5408..793139c 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/WaitingScreen.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/WaitingScreen.java @@ -1,15 +1,18 @@ package eu.jonahbauer.wizard.client.libgdx.screens; import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.g2d.Batch; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.graphics.g2d.GlyphLayout; +import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.*; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; +import com.badlogic.gdx.utils.Align; +import eu.jonahbauer.wizard.client.libgdx.MenuAtlas; import eu.jonahbauer.wizard.client.libgdx.WizardGame; import eu.jonahbauer.wizard.client.libgdx.actors.AutoFocusScrollPane; -import eu.jonahbauer.wizard.client.libgdx.state.AwaitingJoinLobby; import eu.jonahbauer.wizard.client.libgdx.state.Session; -import eu.jonahbauer.wizard.common.messages.client.LeaveSessionMessage; -import eu.jonahbauer.wizard.common.messages.client.ReadyMessage; import eu.jonahbauer.wizard.common.messages.data.PlayerData; import eu.jonahbauer.wizard.common.model.Configuration; @@ -33,14 +36,10 @@ public class WaitingScreen extends MenuScreen { @Override public void changed(ChangeEvent event, Actor actor) { if (actor == buttonLeave) { - game.getClient().transition(new AwaitingJoinLobby()); - game.getClient().send(new LeaveSessionMessage()); + game.getClient().execute(Session.class, Session::leave); sfxClick(); } else if (actor == buttonReady) { - var state = (Session) game.getClient().getState(); - state.setNextReady(!state.isReady()); - game.getClient().send(new ReadyMessage(!state.isReady())); - buttonReady.setDisabled(true); + game.getClient().execute(Session.class, Session::toggleReady); sfxClick(); } } @@ -60,10 +59,26 @@ public class WaitingScreen extends MenuScreen { buttonReady.setPosition(WizardGame.WIDTH * 0.725f - buttonReady.getWidth(), BUTTON_BAR_Y); players = new List<>(game.data.skin) { + private final TextureRegion ready = game.data.menuAtlas.findRegion(MenuAtlas.READY); + private final TextureRegion notReady = game.data.menuAtlas.findRegion(MenuAtlas.NOT_READY); + @Override public String toString(PlayerData player) { return player.getName(); } + + @Override + @SuppressWarnings("SuspiciousNameCombination") + protected GlyphLayout drawItem(Batch batch, BitmapFont font, int index, PlayerData item, float x, float y, float width) { + String string = toString(item); + var height = font.getCapHeight(); + if (item.isReady()) { + batch.draw(ready, x, y - height, height, height); + } else { + batch.draw(notReady, x, y - height, height, height); + } + return font.draw(batch, string, x + height + 8, y, 0, string.length(), width - height - 8, Align.left, false, "..."); + } }; listContainer = new AutoFocusScrollPane(players, game.data.skin); @@ -85,8 +100,11 @@ public class WaitingScreen extends MenuScreen { buttonReady.addListener(listener); } + public void setSending(boolean sending) { + buttonReady.setDisabled(sending); + } + public void setReady(boolean ready) { - buttonReady.setDisabled(false); buttonReady.setText(game.messages.get(ready ? "menu.waiting.not_ready" : "menu.waiting.ready")); } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingConnection.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingConnection.java index b3341cf..28d0432 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingConnection.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingConnection.java @@ -12,7 +12,7 @@ public class AwaitingConnection extends Awaiting { @Override public Optional onEnter(Client client) { log.info("Awaiting connection..."); - client.getGame().setScreen(new LoadingScreen(client.getGame())); + client.getGame().setScreen(new LoadingScreen(client.getGame(), "menu.loading.connecting")); return super.onEnter(client); } @@ -24,6 +24,7 @@ public class AwaitingConnection extends Awaiting { @Override public Optional onClose(Client client, int code, String reason, boolean remote) { + // TODO user feedback log.error("Connection could not be established. (code={}, reason={}, remote={})", code, reason, remote); return Optional.of(new Menu()); } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingJoinLobby.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingJoinLobby.java index fd0bbfd..8219e0b 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingJoinLobby.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingJoinLobby.java @@ -14,7 +14,7 @@ public final class AwaitingJoinLobby extends Awaiting { @Override public Optional onEnter(Client client) { log.info("Waiting for session list..."); - client.getGame().setScreen(new LoadingScreen(client.getGame())); + client.getGame().setScreen(new LoadingScreen(client.getGame(), "menu.loading.joining_lobby")); return super.onEnter(client); } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingJoinSession.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingJoinSession.java index 2635842..5556269 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingJoinSession.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingJoinSession.java @@ -2,6 +2,7 @@ package eu.jonahbauer.wizard.client.libgdx.state; import eu.jonahbauer.wizard.client.libgdx.Client; import eu.jonahbauer.wizard.client.libgdx.screens.LoadingScreen; +import eu.jonahbauer.wizard.common.messages.data.SessionData; import eu.jonahbauer.wizard.common.messages.server.*; import eu.jonahbauer.wizard.common.model.Configuration; import lombok.Getter; @@ -24,7 +25,7 @@ public final class AwaitingJoinSession extends Awaiting { @Override public Optional onEnter(Client client) { log.info("Waiting for acknowledgment..."); - client.getGame().setScreen(new LoadingScreen(client.getGame())); + client.getGame().setScreen(new LoadingScreen(client.getGame(), "menu.loading.joining_session")); return super.onEnter(client); } @@ -37,7 +38,11 @@ public final class AwaitingJoinSession extends Awaiting { log.info("There are {} players in this session.", joined.getPlayers().size()); log.info("Your uuid is {}.", joined.getPlayer()); log.info("Your secret is {}.", joined.getSecret()); - return Optional.of(new Session(joined, sessionName, configuration, playerName)); + return Optional.of(new Session( + new SessionData(joined.getSession(), sessionName, -1, configuration), + joined.getPlayers(), + joined.getPlayer(), joined.getSecret() + )); } } else if (message instanceof NackMessage nack) { switch (nack.getCode()) { diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Lobby.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Lobby.java index 7c9580a..001200b 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Lobby.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Lobby.java @@ -1,17 +1,22 @@ package eu.jonahbauer.wizard.client.libgdx.state; import eu.jonahbauer.wizard.client.libgdx.Client; +import eu.jonahbauer.wizard.client.libgdx.screens.CreateGameScreen; import eu.jonahbauer.wizard.client.libgdx.screens.LobbyScreen; +import eu.jonahbauer.wizard.common.messages.client.CreateSessionMessage; +import eu.jonahbauer.wizard.common.messages.client.JoinSessionMessage; import eu.jonahbauer.wizard.common.messages.data.SessionData; import eu.jonahbauer.wizard.common.messages.server.*; +import eu.jonahbauer.wizard.common.model.Configuration; import lombok.Getter; import lombok.Setter; -import java.util.Optional; +import java.util.*; public final class Lobby extends BaseState { - - private SessionListMessage list; + + private final Map sessions = new HashMap<>(); + private LobbyScreen lobbyScreen; @Getter @@ -19,35 +24,75 @@ public final class Lobby extends BaseState { private String playerName = ""; public Lobby(SessionListMessage list) { - this.list = list; + list.getSessions().forEach(s -> sessions.put(s.getUuid(), s)); } @Override public Optional onEnter(Client client) { - lobbyScreen = new LobbyScreen(client.getGame()); - client.getGame().setScreen(lobbyScreen); - lobbyScreen.setSessions(list.getSessions().toArray(new SessionData[0])); - - list = null; + showListScreen(client); return super.onEnter(client); } @Override public Optional onMessage(Client client, ServerMessage message) { if (message instanceof SessionCreatedMessage created) { - lobbyScreen.addSession(created.getSession()); + var session = created.getSession(); + sessions.put(session.getUuid(), session); + if (lobbyScreen != null) { + lobbyScreen.addSession(session); + } return Optional.empty(); } else if (message instanceof SessionRemovedMessage removed) { - lobbyScreen.removeSession(removed.getSession()); + var session = removed.getSession(); + sessions.remove(session); + if (lobbyScreen != null) { + lobbyScreen.removeSession(session); + } return Optional.empty(); } else if (message instanceof SessionModifiedMessage modified) { - lobbyScreen.modifySession(modified.getSession()); + var session = modified.getSession(); + sessions.put(session.getUuid(), session); + if (lobbyScreen != null) { + lobbyScreen.modifySession(session); + } return Optional.empty(); } else if (message instanceof SessionListMessage list) { - lobbyScreen.setSessions(list.getSessions().toArray(new SessionData[0])); + list.getSessions().forEach(s -> sessions.put(s.getUuid(), s)); + if (lobbyScreen != null) { + lobbyScreen.setSessions(list.getSessions().toArray(new SessionData[0])); + } return Optional.empty(); } else { return unexpectedMessage(client, message); } } + + public Optional disconnect(@SuppressWarnings("unused") Client client) { + return Optional.of(new Menu()); + } + + public Optional createSession(Client client, String sessionName, Configuration config, long timeout, String playerName) { + client.send(new CreateSessionMessage(sessionName, playerName, timeout, config)); + return Optional.of(new AwaitingJoinSession(null, sessionName, config, playerName)); + } + + public Optional joinSession(Client client, SessionData session, String playerName) { + client.send(new JoinSessionMessage(session.getUuid(), playerName)); + return Optional.of(new AwaitingJoinSession(session.getUuid(), session.getName(), session.getConfiguration(), playerName)); + } + + public Optional showCreateScreen(Client client) { + var game = client.getGame(); + lobbyScreen = null; + game.setScreen(new CreateGameScreen(game, playerName)); + return Optional.empty(); + } + + public Optional showListScreen(Client client) { + var game = client.getGame(); + lobbyScreen = new LobbyScreen(game, playerName); + game.setScreen(lobbyScreen); + lobbyScreen.setSessions(sessions.values().toArray(SessionData[]::new)); + return Optional.empty(); + } } \ No newline at end of file diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Menu.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Menu.java index cc526b3..eb1a1e7 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Menu.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Menu.java @@ -1,13 +1,18 @@ package eu.jonahbauer.wizard.client.libgdx.state; import eu.jonahbauer.wizard.client.libgdx.Client; +import eu.jonahbauer.wizard.client.libgdx.ClientSocket; +import eu.jonahbauer.wizard.client.libgdx.screens.ConnectScreen; import eu.jonahbauer.wizard.client.libgdx.screens.MainMenuScreen; import eu.jonahbauer.wizard.common.messages.server.ServerMessage; import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; import org.java_websocket.framing.CloseFrame; +import java.net.URI; import java.util.Optional; +@Log4j2 public final class Menu extends BaseState { @Override @@ -16,7 +21,7 @@ public final class Menu extends BaseState { if (client.getSocket() != null && client.getSocket().isOpen()) { client.getSocket().close(CloseFrame.GOING_AWAY); } - client.getGame().setScreen(new MainMenuScreen(client.getGame())); + showMenuScreen(client); return super.onEnter(client); } @@ -24,6 +29,7 @@ public final class Menu extends BaseState { public Optional onMessage(Client client, ServerMessage message) { // it is possible that there are messages still queued after // returning to the menu as a result of a previous message + log.debug("Dropped message {}.", message); return Optional.empty(); } @@ -32,4 +38,23 @@ public final class Menu extends BaseState { super.onClose(client, code, reason, remote); return Optional.empty(); } + + public Optional showConnectScreen(Client client) { + var game = client.getGame(); + game.setScreen(new ConnectScreen(game)); + return Optional.empty(); + } + + public Optional showMenuScreen(Client client) { + var game = client.getGame(); + game.setScreen(new MainMenuScreen(game)); + return Optional.empty(); + } + + public Optional connect(Client client, URI uri) { + ClientSocket socket = new ClientSocket(uri); + client.setSocket(socket); + socket.connect(); + return Optional.of(new AwaitingConnection()); + } } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Session.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Session.java index 6b34077..6b3860f 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Session.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Session.java @@ -1,23 +1,22 @@ package eu.jonahbauer.wizard.client.libgdx.state; import eu.jonahbauer.wizard.client.libgdx.Client; -import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; import eu.jonahbauer.wizard.client.libgdx.screens.WaitingScreen; +import eu.jonahbauer.wizard.common.messages.client.LeaveSessionMessage; +import eu.jonahbauer.wizard.common.messages.client.ReadyMessage; import eu.jonahbauer.wizard.common.messages.data.PlayerData; +import eu.jonahbauer.wizard.common.messages.data.SessionData; import eu.jonahbauer.wizard.common.messages.server.*; import eu.jonahbauer.wizard.common.model.Configuration; import lombok.Getter; -import lombok.Setter; import lombok.extern.log4j.Log4j2; -import java.util.Optional; -import java.util.UUID; +import java.util.*; +import java.util.stream.Collectors; @Log4j2 @Getter public final class Session extends BaseState { - - private SessionJoinedMessage joined; private WaitingScreen sessionScreen; private final UUID self; @@ -26,32 +25,29 @@ public final class Session extends BaseState { private final UUID session; private final String sessionName; private final Configuration configuration; - private final String playerName; - - private boolean ready; - @Setter - private Boolean nextReady; - - public Session(SessionJoinedMessage joined, String sessionName, Configuration configuration, String playerName) { - this.self = joined.getPlayer(); - this.secret = joined.getSecret(); - this.session = joined.getSession(); - this.sessionName = sessionName; - this.configuration = configuration; - this.joined = joined; - this.playerName = playerName; + private final List players = new ArrayList<>(); + + private boolean sending; + + public Session(SessionData session, Collection players, UUID self, String secret) { + this.session = session.getUuid(); + this.sessionName = session.getName(); + this.configuration = session.getConfiguration(); + this.players.addAll(players); + + this.self = self; + this.secret = secret; } @Override public Optional onEnter(Client client) { sessionScreen = new WaitingScreen(client.getGame()); client.getGame().setScreen(sessionScreen); - sessionScreen.setPlayers(joined.getPlayers().toArray(new PlayerData[0])); + sessionScreen.setPlayers(players.toArray(new PlayerData[0])); sessionScreen.setReady(false); - sessionScreen.setPlayerName(playerName); + sessionScreen.setPlayerName(getName()); sessionScreen.setSession(session, sessionName, configuration); - - joined = null; + return super.onEnter(client); } @@ -59,29 +55,80 @@ public final class Session extends BaseState { public Optional onMessage(Client client, ServerMessage message) { if (message instanceof PlayerJoinedMessage join) { sessionScreen.addPlayer(join.getPlayer()); + players.add(join.getPlayer()); return Optional.empty(); } else if (message instanceof PlayerLeftMessage leave) { sessionScreen.removePlayer(leave.getPlayer()); + players.remove(new PlayerData(leave.getPlayer(), null, false)); return Optional.empty(); } else if (message instanceof PlayerModifiedMessage modified) { - sessionScreen.modifyPlayer(modified.getPlayer()); + var player = modified.getPlayer(); + + sessionScreen.modifyPlayer(player); + players.remove(player); + players.add(player); + + if (self.equals(player.getUuid())) { + sessionScreen.setReady(player.isReady()); + } + return Optional.empty(); } else if (message instanceof StartingGameMessage) { - client.getGame().setScreen(new GameScreen(client.getGame())); return Optional.empty(); - } else if (nextReady != null && message instanceof NackMessage nack) { +// return Optional.of(new Game( +// self, session, sessionName, secret, +// players.stream().collect(Collectors.toMap(PlayerData::getUuid, PlayerData::getName)) +// )); + } else if (sending && message instanceof NackMessage nack) { // TODO display error log.error(nack.getMessage()); - sessionScreen.setReady(!nextReady); - nextReady = null; + sending = false; + sessionScreen.setSending(false); return Optional.empty(); - } else if (nextReady != null && message instanceof AckMessage) { - sessionScreen.setReady(nextReady); - ready = nextReady; - nextReady = null; + } else if (sending && message instanceof AckMessage) { + sending = false; + sessionScreen.setSending(false); return Optional.empty(); } else { return unexpectedMessage(client, message); } } + + public Optional setReady(Client client, boolean ready) { + if (sending) { + log.warn("Please slow down"); + } else { + sending = true; + sessionScreen.setSending(true); + client.send(new ReadyMessage(ready)); + } + return Optional.empty(); + } + + public Optional toggleReady(Client client) { + return setReady(client, !isReady()); + } + + public Optional leave(Client client) { + client.send(new LeaveSessionMessage()); + return Optional.of(new AwaitingJoinLobby()); + } + + private boolean isReady() { + for (PlayerData player : players) { + if (self.equals(player.getUuid())) { + return player.isReady(); + } + } + throw new NoSuchElementException(); + } + + private String getName() { + for (PlayerData player : players) { + if (self.equals(player.getUuid())) { + return player.getName(); + } + } + throw new NoSuchElementException(); + } } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages.properties b/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages.properties index 6cd0d8c..f6f4c17 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages.properties +++ b/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages.properties @@ -23,6 +23,9 @@ menu.create_game.back=Back menu.create_game.create=Create menu.loading.loading=Loading... +menu.loading.connecting=Connecting... +menu.loading.joining_session=Joining session... +menu.loading.joining_lobby=Joining lobby... menu.loading.back=Return To Main Menu menu.waiting.ready=Ready diff --git a/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages_de.properties b/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages_de.properties index dc0000a..7a4d06b 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages_de.properties +++ b/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages_de.properties @@ -10,7 +10,7 @@ menu.lobby.join=Beitreten menu.lobby.create=Erstellen menu.lobby.back=Verlassen menu.lobby.player_name.label=Spielername -menu.lobby.session_name.label=Session Name +menu.lobby.session_name.label=Sitzungsname menu.lobby.session_player_count.label=Spieleranzahl menu.lobby.session_configuration.label=Spielvariante @@ -23,6 +23,9 @@ menu.create_game.back=Zurück menu.create_game.create=Erstellen menu.loading.loading=Laden... +menu.loading.connecting=Verbinde... +menu.loading.joining_session=Trete Sitzung bei... +menu.loading.joining_lobby=Trete Warteraum bei... menu.loading.back=Zurück zum Hauptmenü menu.waiting.ready=Bereit @@ -30,6 +33,6 @@ menu.waiting.not_ready=Nicht Bereit menu.waiting.leave=Verlassen menu.waiting.player_name.label=Eigener Name -menu.waiting.session_name.label=Session Name -menu.waiting.session_uuid.label=Session UUID +menu.waiting.session_name.label=Sitzungsname +menu.waiting.session_uuid.label=Sitzungs-ID menu.waiting.session_configuration.label=Spielvariante \ No newline at end of file diff --git a/wizard-client/wizard-client-libgdx/core/src/main/textures/menu/not_ready.png b/wizard-client/wizard-client-libgdx/core/src/main/textures/menu/not_ready.png new file mode 100644 index 0000000000000000000000000000000000000000..860c55b5e906f32229c37df9c7d28e970d0a5a3d GIT binary patch literal 1398 zcmV-+1&R8JP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1q(?;K~!i%?U~(A zR7VuR&lVT9g;>}wEShd9rH~M4EN$@Pg(yjdY+|L<>O<9a`{D~~V#I#{Xp^Q%{lrjW zi6%Zs;)4w~mcF+o7;EEF>4Txwl29oRCX%ACoSrit%+CGVyI)$<{Uwvlz31bexp(Hw z%vs2j=YNhzelr3G2D}iDcfiA$UM6cI6 zxKvtUA(F3O@ywam#eHtU!-v1b($ZziU@~cCWy|FI3wexdT+aa-=P$vfOFzTtXr0{B zDJel%zy1z1G>k%h{RpgH{R=EE{-tDIZxHU^Z-U9mFvMbQFg_lF2M?^D-QIo+cI|q{ z6OAs&oy@_(K|gNTFl+hdg26a;b$yM)!!L_^Qdk!c9vr}`sz;VSH*UO#Lqnyao*De+ zErTyALfo_GJG^kAOjJ{bg7jfDdRh50`hVOBpQxq}Zr%Eu$$3RZ9D8~`7L|;_zP{a9 zULH3+hbFhEq=%ZYMyIv4|KuX|sg%sVjn7MCVk;TnUZwoYMp_os5p7S>U^WFxmsP?~)aLm@rIc6R0Oo)@tR z75wJSca+|$Rp`_S6+G*(F55VDqN{6Ai(&2BCF)p#i0z>X^O0s)QnKu{NU67(sBhB= zE%=p2wjwscUmS~z^(1oobccxT!ARs=&8W5YcM(})8JM9HZgi}8@?}?AH2RH*?7^0n zY0c=sfv-eli&KE<2G7tiI;+{WwA>YuJy=t-pc(b|zb_(3Yy(X*c!svvA859;(22+% zwQwXFojdoMh>~L2iNuyd@C=?mU#;2u{c#c5qjGMd5v4Q{xdM+K?Xf)Q6cnVqEuaq2 zSv0{1EpQ@w!VB`7UHEZUcsyA-gFEv4524yxeGICaL1<(0E6k60`=%P3s>%hf}*Wv2bdby>;eWJRZF@-EKRvkI#}zZRPca6Ttg#Jk4^m!X4P@nrr#4x$v)ka>wNK#o?IxI+3D9+s z_BU%q#X|@N|Av~H>oix+LLhL(vvX(i6IPx)c^m-v7poEE@a)YUZ~y=R07*qoM6N<$ Ef{w4Nd;kCd literal 0 HcmV?d00001 diff --git a/wizard-client/wizard-client-libgdx/core/src/main/textures/menu/ready.png b/wizard-client/wizard-client-libgdx/core/src/main/textures/menu/ready.png new file mode 100644 index 0000000000000000000000000000000000000000..6c325ba156801572ab4d33736b3b4627fee9537a GIT binary patch literal 1705 zcmV;a23GlrP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D20lqdK~!i%?U`Fl zR96&+XJCLCX9UV!C^d~Cslmk3D1k%~O{57hCPpD9)uPhI7bWR~)fZb^HA>%9qA`d@ z6Ps2anpPw!#I#TnYoLk=m!XZJT;(=g>$&B?h9o|!ZHA|E+n@4fcywbnlS zx{hvz72b06^j{NqXYWQ%xF@XbuJ*&v7oUGL>K;uTcaO)+dFQzHS4FJyr5I8c(hO;n z>!Q~^-DKQ!BiWFAzdWwo!!@7*O%7bLU;3cg*?j%c{G+@P_edlat0LWy?#?&mKi+2E z_E}Y8m5qvnCL{;0+OK9bI2s-_Ih!`Rd@d>t4Wr(u+iBkUtjJPSd@S+U2oeE!D3e^r=o4OWN|6hIEeaHQcR3yTpxA<~R zxdYk8?8f!c>+549W1lVgmc}59(nqDvdS-KMi?-rUSLerV^KF?9pF>f%)ne@^U0s@c zB;m*s6=~=C)b-U{`?e12x^zD2lWI%#9344&gH7Z(s{Tq)x9UaJwY7h*T~ullx}cM) zYXd#MrEklKR7p&CjPJnEfnV>;+ZQ8 zQaoqI&wN5vG;?nJ-2P-+vPY_i>3%|0LIV?4+Gy54S@Uf9RGbG|xM;tagSLgb^j&)2 z$yFTD=q{lE$7n?N4RHdRqiX@99`=oq_=zHknkK%5;v`>AwJ zaKktlu*1S&VI!EZBn~UAcXuBjJ${d}90!LE)(dsR7T84MSF>kePphy23?(hAO(59h z-51*v>xGWJ1AAMj7|X0Fvo}+;#Bw&N0SB}6-t;BmkFm~^IBW=w_djKWWxtbH$8^>T zWf{F0OR$Z^JhI~ePL=FnW}qy$!xoqezMyPafbrG#>hE8AUj{r05+&>_6BgCrz8i?Hk8y% zpq?*_Eo;7)d9RSNJR4#*G*Oa6Y8#L_@WTBfk&+xZm{C#_!5R;;W3olM*+nUdk~}g8 z>~4DuB{|Fy=HS*~1wt_%NOe!TC!;CJBXhvv6(4g=5vB=BieOv(oh$Y$yC@rwP>m2D2m`5D#`%f!yW@M}l_u>Co~Ql9;xmmL((E8d1MZ(F|4suk^|R3N^-ye7(!VN7)nm;3zzYr zd!bvT8(|qr21dG(ZSULOPa+Z0p@auRM(eNl|PY)NA?B~bfP2&F5tMs zQkEysn+QkG1Ew>9>3)~89Ggv>WoI&(1BjR*=tNl^q6e*Wt=~~rgpw0NLNy+=I9saO zvPn6eF2j)FPKZospzQKUkJE@A-~vu!8A`(`)L{d#e6r)T=tRtH`h&zG6gLKh`w%hn z)4$Xo)iT%eBV}bs--MQ)2M?VO?+&^L`4cg0VK#kES#?OG5!gi73bUWYilEoE^gLkK z)(Yun%4~T`V)fKc)ut+4>T&U=Dq$zkrT()0aynHB4un+@+7LE~(8ud=g2E~;p)Lxm zoT4hB!M#2~qj=COh#pWC&0yFh*^*>|tzG@Q{-P?OgYceGToiKx(F3K!o?7tRilK@J z(lL*xpsyf2p+!(*udxIQ?jm}C3wSV53v5#OjdZE>KQJCTsrs^rcFpCW@cQF=M}3Zu z>5+m9INK?Qa1F#w3YQUMgmDxym)Di%^g2K6TIjNNE_Ck14lCTG{OH-A)na{8vbrQs zZFpU^1AY*ztedI(YQi(2xQB!aK5$gVSAKgT?ZP)yq>7*&09e)<9gPiP{KV8zWGO0n zBcGUr&_}*n`pC!coNB$v8o!p0PN{-HBn{IhmIX3=Aykj=qiz3>*8o|0J>k`J4qFk;M!Qe1|}oQB=dL z0VwF_>Eak-;h+5H|9^XCW#z~9Y+8&9DtmeqeqAgq?%)t@SaX!qO-*C=s)Q>6ub5X! zb9v1RF&CNQ=dRrKt3j=;$y+mtL1Se@lGv%E%o1V}45}Z+gf~d%p931r;OXk;vd$@? F2>>MMGaLW_