diff --git a/client/package.bat b/client/package.bat index 83c5cc0..73f44c5 100644 --- a/client/package.bat +++ b/client/package.bat @@ -4,6 +4,6 @@ jpackage^ --name "Ace-of-Shades"^ --description "Top-down 2D shooter game inspired by Ace of Spades."^ --module-path "aos-client-3.1.jar;../../core/target/aos-core-3.1.jar"^ - --module aos_client/nl.andrewlalis.aos_client.Client^ + --module aos_client/nl.andrewlalis.aos_client.launcher.Launcher^ --win-shortcut^ --win-dir-chooser \ No newline at end of file diff --git a/client/src/main/java/nl/andrewlalis/aos_client/Client.java b/client/src/main/java/nl/andrewlalis/aos_client/Client.java index 835cd66..8215151 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/Client.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/Client.java @@ -1,6 +1,5 @@ package nl.andrewlalis.aos_client; -import nl.andrewlalis.aos_client.view.ConnectDialog; import nl.andrewlalis.aos_client.view.GameFrame; import nl.andrewlalis.aos_client.view.GamePanel; import nl.andrewlalis.aos_core.model.Player; @@ -122,11 +121,4 @@ public class Client { System.out.println("Sound manager closed."); this.frame.dispose(); } - - - - public static void main(String[] args) { - ConnectDialog dialog = new ConnectDialog(); - dialog.setVisible(true); - } } diff --git a/client/src/main/java/nl/andrewlalis/aos_client/launcher/Launcher.java b/client/src/main/java/nl/andrewlalis/aos_client/launcher/Launcher.java index 0d31b71..4d45b87 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/launcher/Launcher.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/launcher/Launcher.java @@ -1,16 +1,26 @@ package nl.andrewlalis.aos_client.launcher; import nl.andrewlalis.aos_client.Client; +import nl.andrewlalis.aos_client.launcher.servers.ServerInfo; +import nl.andrewlalis.aos_client.launcher.servers.ServerInfoCellRenderer; +import nl.andrewlalis.aos_client.launcher.servers.ServerInfoListModel; import javax.swing.*; import java.awt.*; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; +/** + * Launcher application for starting the game. Because the client is only + * actually started when connecting to a server, this user interface serves as + * the menu that the user interacts with before joining a game. + */ public class Launcher extends JFrame { private static final Pattern addressPattern = Pattern.compile("(.+):(\\d+)"); @@ -18,6 +28,7 @@ public class Launcher extends JFrame { super("Ace of Shades - Launcher"); this.setDefaultCloseOperation(EXIT_ON_CLOSE); this.setContentPane(this.buildContent()); + this.setPreferredSize(new Dimension(400, 500)); this.pack(); this.setLocationRelativeTo(null); } @@ -25,12 +36,10 @@ public class Launcher extends JFrame { private Container buildContent() { JTabbedPane mainPanel = new JTabbedPane(SwingConstants.TOP, JTabbedPane.SCROLL_TAB_LAYOUT); mainPanel.addTab("Connect", null, this.getConnectPanel(), "Connect to a server and play."); - - JPanel serversPanel = new JPanel(); - mainPanel.addTab("Servers", null, serversPanel, "View a list of available servers."); - - JPanel settingsPanel = new JPanel(); - mainPanel.addTab("Settings", null, settingsPanel, "Change game settings."); + mainPanel.addTab("Servers", null, this.getServersPanel(), "View a list of available servers."); +// +// JPanel settingsPanel = new JPanel(); +// mainPanel.addTab("Settings", null, settingsPanel, "Change game settings."); return mainPanel; } @@ -58,7 +67,7 @@ public class Launcher extends JFrame { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER) { - if (validateInput(addressField, usernameField)) { + if (validateConnectInput(addressField, usernameField)) { connect(addressField, usernameField); } } @@ -72,7 +81,7 @@ public class Launcher extends JFrame { cancelButton.addActionListener(e -> this.dispose()); JButton connectButton = new JButton("Connect"); connectButton.addActionListener(e -> { - if (validateInput(addressField, usernameField)) { + if (validateConnectInput(addressField, usernameField)) { connect(addressField, usernameField); } }); @@ -80,12 +89,72 @@ public class Launcher extends JFrame { buttonPanel.add(connectButton); JPanel mainPanel = new JPanel(new BorderLayout()); + mainPanel.add(new JLabel("Directly connect to a server and play."), BorderLayout.NORTH); mainPanel.add(inputPanel, BorderLayout.CENTER); mainPanel.add(buttonPanel, BorderLayout.SOUTH); return mainPanel; } - private boolean validateInput(JTextField addressField, JTextField usernameField) { + private Container getServersPanel() { + JPanel panel = new JPanel(new BorderLayout()); + + ServerInfoListModel listModel = new ServerInfoListModel(); + listModel.add(new ServerInfo("one", "localhost:8035", "andrew")); + listModel.add(new ServerInfo("two", "localhost:25565", null)); + JList serversList = new JList<>(listModel); + serversList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + serversList.setCellRenderer(new ServerInfoCellRenderer()); + JScrollPane scrollPane = new JScrollPane(serversList, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + panel.add(scrollPane, BorderLayout.CENTER); + + serversList.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + serversList.setSelectedIndex(serversList.locationToIndex(e.getPoint())); + ServerInfo server = serversList.getSelectedValue(); + if (server == null) return; + JPopupMenu menu = new JPopupMenu(); + JMenuItem connectItem = new JMenuItem("Connect"); + connectItem.addActionListener(a -> connect(server)); + menu.add(connectItem); + JMenuItem editItem = new JMenuItem("Edit"); + editItem.addActionListener(a -> { + // TODO: Open edit dialog. + listModel.serverEdited(); + }); + menu.add(editItem); + JMenuItem removeItem = new JMenuItem("Remove"); + removeItem.addActionListener(a -> { + int choice = JOptionPane.showConfirmDialog(panel, "Are you sure you want to remove this server?", "Confirm Server Removal", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); + if (choice == JOptionPane.OK_OPTION) { + listModel.remove(serversList.getSelectedValue()); + } + }); + menu.add(removeItem); + menu.show(serversList, e.getX(), e.getY()); + } else if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) { + serversList.setSelectedIndex(serversList.locationToIndex(e.getPoint())); + ServerInfo server = serversList.getSelectedValue(); + if (server == null) return; + connect(server); + } + } + }); + + JPanel buttonPanel = new JPanel(); + JButton addServerButton = new JButton("Add Server"); + addServerButton.addActionListener(e -> { + // TODO: Add server dialog. + }); + buttonPanel.add(addServerButton); + + panel.add(buttonPanel, BorderLayout.SOUTH); + + return panel; + } + + private boolean validateConnectInput(JTextField addressField, JTextField usernameField) { List warnings = new ArrayList<>(); if (addressField.getText() == null || addressField.getText().isBlank()) { warnings.add("Address must not be empty."); @@ -124,6 +193,20 @@ public class Launcher extends JFrame { } } + private void connect(ServerInfo serverInfo) { + String username = serverInfo.getUsername(); + if (username == null) { + username = JOptionPane.showInputDialog(this, "Enter a username.", "Username", JOptionPane.PLAIN_MESSAGE); + if (username == null || username.isBlank()) return; + } + try { + new Client(serverInfo.getHostAddress(), serverInfo.getHostPort(), username); + } catch (IOException e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(this, "Could not connect:\n" + e.getMessage(), "Connection Error", JOptionPane.WARNING_MESSAGE); + } + } + public static void main(String[] args) { Launcher launcher = new Launcher(); diff --git a/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/ServerInfo.java b/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/ServerInfo.java new file mode 100644 index 0000000..fdfbc8e --- /dev/null +++ b/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/ServerInfo.java @@ -0,0 +1,67 @@ +package nl.andrewlalis.aos_client.launcher.servers; + +import java.util.Objects; + +public class ServerInfo implements Comparable { + private String name; + private String host; + private String username; + + public ServerInfo(String name, String host, String username) { + this.name = name; + this.host = host; + this.username = username; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getHostAddress() { + return this.host.split(":")[0]; + } + + public int getHostPort() { + String[] parts = this.host.split(":"); + if (parts.length < 2) return 8035; + return Integer.parseInt(parts[1]); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ServerInfo that = (ServerInfo) o; + return getName().equals(that.getName()) && getHost().equals(that.getHost()) && Objects.equals(getUsername(), that.getUsername()); + } + + @Override + public int hashCode() { + return Objects.hash(getName(), getHost(), getUsername()); + } + + @Override + public int compareTo(ServerInfo o) { + return this.name.compareTo(o.name); + } +} diff --git a/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/ServerInfoCellRenderer.java b/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/ServerInfoCellRenderer.java new file mode 100644 index 0000000..464ac15 --- /dev/null +++ b/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/ServerInfoCellRenderer.java @@ -0,0 +1,28 @@ +package nl.andrewlalis.aos_client.launcher.servers; + +import javax.swing.*; +import java.awt.*; + +public class ServerInfoCellRenderer implements ListCellRenderer { + @Override + public Component getListCellRendererComponent(JList list, ServerInfo value, int index, boolean isSelected, boolean cellHasFocus) { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBorder(BorderFactory.createTitledBorder(value.getName())); + JTextField hostField = new JTextField(value.getHost()); + hostField.setEditable(false); + panel.add(hostField, BorderLayout.CENTER); + if (value.getUsername() != null) { + panel.add(new JLabel("Username: " + value.getUsername()), BorderLayout.SOUTH); + } + + if (isSelected) { + panel.setBackground(list.getSelectionBackground()); + panel.setForeground(list.getSelectionForeground()); + } else { + panel.setBackground(list.getBackground()); + panel.setForeground(list.getForeground()); + } + + return panel; + } +} diff --git a/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/ServerInfoListModel.java b/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/ServerInfoListModel.java new file mode 100644 index 0000000..4bd4408 --- /dev/null +++ b/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/ServerInfoListModel.java @@ -0,0 +1,42 @@ +package nl.andrewlalis.aos_client.launcher.servers; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class ServerInfoListModel extends AbstractListModel { + private final List servers; + + public ServerInfoListModel() { + this.servers = new ArrayList<>(); + } + + public void add(ServerInfo server) { + if (this.servers.contains(server)) return; + this.servers.add(server); + this.servers.sort(Comparator.naturalOrder()); + this.fireContentsChanged(this, 0, this.getSize()); + } + + public void remove(ServerInfo server) { + int index = this.servers.indexOf(server); + if (index == -1) return; + this.servers.remove(index); + this.fireIntervalRemoved(this, index, index); + } + + public void serverEdited() { + this.fireContentsChanged(this, 0, this.getSize()); + } + + @Override + public int getSize() { + return this.servers.size(); + } + + @Override + public ServerInfo getElementAt(int index) { + return this.servers.get(index); + } +} diff --git a/client/src/main/java/nl/andrewlalis/aos_client/view/ConnectDialog.java b/client/src/main/java/nl/andrewlalis/aos_client/view/ConnectDialog.java deleted file mode 100644 index 4b6ae4e..0000000 --- a/client/src/main/java/nl/andrewlalis/aos_client/view/ConnectDialog.java +++ /dev/null @@ -1,110 +0,0 @@ -package nl.andrewlalis.aos_client.view; - -import nl.andrewlalis.aos_client.Client; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.KeyAdapter; -import java.awt.event.KeyEvent; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -public class ConnectDialog extends JDialog { - private static final Pattern addressPattern = Pattern.compile("(.+):(\\d+)"); - - public ConnectDialog() { - super((Frame) null, "Connect to Server", false); - JPanel inputPanel = new JPanel(new GridBagLayout()); - GridBagConstraints c = new GridBagConstraints(); - c.insets = new Insets(5, 5, 5, 5); - - c.gridx = 0; - c.gridy = 0; - inputPanel.add(new JLabel("Address"), c); - JTextField addressField = new JTextField(20); - c.gridx = 1; - inputPanel.add(addressField, c); - - c.gridy = 1; - c.gridx = 0; - inputPanel.add(new JLabel("Username"), c); - JTextField usernameField = new JTextField(20); - c.gridx = 1; - inputPanel.add(usernameField, c); - - var enterListener = new KeyAdapter() { - @Override - public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_ENTER) { - if (validateInput(addressField, usernameField)) { - connect(addressField, usernameField); - } - } - } - }; - addressField.addKeyListener(enterListener); - usernameField.addKeyListener(enterListener); - - JPanel buttonPanel = new JPanel(new FlowLayout()); - JButton cancelButton = new JButton("Cancel"); - cancelButton.addActionListener(e -> this.dispose()); - JButton connectButton = new JButton("Connect"); - connectButton.addActionListener(e -> { - if (validateInput(addressField, usernameField)) { - connect(addressField, usernameField); - } - }); - buttonPanel.add(cancelButton); - buttonPanel.add(connectButton); - - JPanel mainPanel = new JPanel(new BorderLayout()); - mainPanel.add(inputPanel, BorderLayout.CENTER); - mainPanel.add(buttonPanel, BorderLayout.SOUTH); - - this.setContentPane(mainPanel); - this.pack(); - this.setLocationRelativeTo(null); - this.setDefaultCloseOperation(DISPOSE_ON_CLOSE); - } - - private boolean validateInput(JTextField addressField, JTextField usernameField) { - List warnings = new ArrayList<>(); - if (addressField.getText() == null || addressField.getText().isBlank()) { - warnings.add("Address must not be empty."); - } - if (usernameField.getText() == null || usernameField.getText().isBlank()) { - warnings.add("Username must not be empty."); - } - if (usernameField.getText() != null && usernameField.getText().length() > 16) { - warnings.add("Username is too long."); - } - if (addressField.getText() != null && !addressPattern.matcher(addressField.getText()).matches()) { - warnings.add("Address must be in the form HOST:PORT."); - } - if (!warnings.isEmpty()) { - JOptionPane.showMessageDialog( - this, - String.join("\n", warnings), - "Invalid Input", - JOptionPane.WARNING_MESSAGE - ); - } - return warnings.isEmpty(); - } - - private void connect(JTextField addressField, JTextField usernameField) { - String hostAndPort = addressField.getText(); - String[] parts = hostAndPort.split(":"); - String host = parts[0].trim(); - int port = Integer.parseInt(parts[1]); - String username = usernameField.getText(); - try { - new Client(host, port, username); - } catch (IOException ex) { - ex.printStackTrace(); - JOptionPane.showMessageDialog(null, "Could not connect:\n" + ex.getMessage(), "Connection Error", JOptionPane.WARNING_MESSAGE); - } - } -}