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 2b132fd..15b1d5d 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/Client.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/Client.java @@ -1,5 +1,6 @@ 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.PlayerControlState; @@ -8,7 +9,6 @@ import nl.andrewlalis.aos_core.net.PlayerControlStateMessage; import nl.andrewlalis.aos_core.net.chat.ChatMessage; import nl.andrewlalis.aos_core.net.chat.PlayerChatMessage; -import javax.swing.*; import java.io.IOException; import java.util.LinkedList; import java.util.List; @@ -153,22 +153,7 @@ public class Client { public static void main(String[] args) { - String hostAndPort = JOptionPane.showInputDialog("Enter server host and port (host:port):"); - if (hostAndPort == null) throw new IllegalArgumentException("A host and port is required."); - String[] parts = hostAndPort.split(":"); - if (parts.length != 2) throw new IllegalArgumentException("Invalid host:port."); - String host = parts[0].trim(); - int port = Integer.parseInt(parts[1]); - String username = JOptionPane.showInputDialog("Enter a username:"); - if (username == null || username.isBlank()) throw new IllegalArgumentException("Username is required."); - - Client client = new Client(); - try { - client.connect(host, port, username); - } catch (IOException | ClassNotFoundException e) { - client.shutdown(); - e.printStackTrace(); - JOptionPane.showMessageDialog(null, "Could not connect:\n" + e.getMessage(), "Connection Error", JOptionPane.WARNING_MESSAGE); - } + ConnectDialog dialog = new ConnectDialog(); + dialog.setVisible(true); } } 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 new file mode 100644 index 0000000..b00515c --- /dev/null +++ b/client/src/main/java/nl/andrewlalis/aos_client/view/ConnectDialog.java @@ -0,0 +1,109 @@ +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 (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(); + Client client = new Client(); + try { + client.connect(host, port, username); + } catch (IOException | ClassNotFoundException ex) { + client.shutdown(); + ex.printStackTrace(); + JOptionPane.showMessageDialog(null, "Could not connect:\n" + ex.getMessage(), "Connection Error", JOptionPane.WARNING_MESSAGE); + } + } +} diff --git a/client/src/main/java/nl/andrewlalis/aos_client/view/GamePanel.java b/client/src/main/java/nl/andrewlalis/aos_client/view/GamePanel.java index 5a1f38d..fcedac6 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/view/GamePanel.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/view/GamePanel.java @@ -48,9 +48,11 @@ public class GamePanel extends JPanel { g2.clearRect(0, 0, this.getWidth(), this.getHeight()); World world = client.getWorld(); - if (world != null) drawWorld(g2, world); + if (world != null) { + drawWorld(g2, world); + drawStatus(g2, world); + } drawChat(g2, world); - drawStatus(g2, world); } private void drawWorld(Graphics2D g2, World world) { @@ -234,5 +236,20 @@ public class GamePanel extends JPanel { Gun gun = myPlayer.getGun(); g2.drawString("Clips: " + gun.getClipCount() + " / " + gun.getMaxClipCount(), 5, this.getHeight() - 20); g2.drawString("Bullets: " + gun.getCurrentClipBulletCount() + " / " + gun.getClipSize(), 5, this.getHeight() - 30); + if (myPlayer.getHealth() >= 66.0f) { + g2.setColor(Color.GREEN); + } else if (myPlayer.getHealth() >= 33.0f) { + g2.setColor(Color.YELLOW); + } else { + g2.setColor(Color.RED); + } + g2.drawString(String.format("Health: %.1f / %.1f", myPlayer.getHealth(), Player.MAX_HEALTH), 5, this.getHeight() - 40); + + int y = this.getHeight() - 60; + for (Team t : world.getTeams()) { + g2.setColor(t.getColor()); + g2.drawString("Team " + t.getName() + ": " + t.getScore(), 5, y); + y -= 15; + } } } diff --git a/core/src/main/java/nl/andrewlalis/aos_core/geom/Vec2.java b/core/src/main/java/nl/andrewlalis/aos_core/geom/Vec2.java index 8921a29..9b53958 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/geom/Vec2.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/geom/Vec2.java @@ -1,6 +1,8 @@ package nl.andrewlalis.aos_core.geom; import java.io.Serializable; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; public record Vec2(double x, double y) implements Serializable { @@ -56,4 +58,11 @@ public record Vec2(double x, double y) implements Serializable { public String toString() { return "[ " + x + ", " + y + " ]"; } + + public static Vec2 random(double min, double max) { + Random r = ThreadLocalRandom.current(); + double x = r.nextDouble() * (max - min) + min; + double y = r.nextDouble() * (max - min) + min; + return new Vec2(x, y); + } } diff --git a/core/src/main/java/nl/andrewlalis/aos_core/model/Bullet.java b/core/src/main/java/nl/andrewlalis/aos_core/model/Bullet.java index 42ef7fd..e723a6a 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/model/Bullet.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/model/Bullet.java @@ -1,26 +1,34 @@ package nl.andrewlalis.aos_core.model; import nl.andrewlalis.aos_core.geom.Vec2; +import nl.andrewlalis.aos_core.model.tools.Gun; import java.util.Random; import java.util.concurrent.ThreadLocalRandom; public class Bullet extends PhysicsObject { private final int playerId; + private final Gun gun; public Bullet(Player player) { - super( - player.getPosition().add(player.getOrientation().mul(1.5)), - player.getOrientation(), - null - ); this.playerId = player.getId(); + this.setPosition(player.getPosition() + .add(player.getOrientation().mul(1.5)) + .add(player.getOrientation().perp().mul(Player.RADIUS)) + ); + this.setOrientation(player.getOrientation()); + Random r = ThreadLocalRandom.current(); Vec2 perturbation = new Vec2((r.nextDouble() - 0.5) * 2, (r.nextDouble() - 0.5) * 2).mul(player.getGun().getAccuracy()); - this.setVelocity(player.getOrientation().add(perturbation).mul(player.getGun().getBulletSpeed())); + this.setVelocity(this.getOrientation().add(perturbation).mul(player.getGun().getBulletSpeed())); + this.gun = player.getGun(); } public int getPlayerId() { return playerId; } + + public Gun getGun() { + return gun; + } } diff --git a/core/src/main/java/nl/andrewlalis/aos_core/model/Player.java b/core/src/main/java/nl/andrewlalis/aos_core/model/Player.java index 2c843e7..ce577b6 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/model/Player.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/model/Player.java @@ -1,5 +1,6 @@ package nl.andrewlalis.aos_core.model; +import nl.andrewlalis.aos_core.geom.Vec2; import nl.andrewlalis.aos_core.model.tools.Gun; import java.util.Objects; @@ -8,12 +9,14 @@ public class Player extends PhysicsObject { public static final double MOVEMENT_SPEED = 10; // Movement speed, in m/s public static final double RADIUS = 0.5; // Collision radius, in meters. public static final double RESUPPLY_COOLDOWN = 30; // Seconds between allowing resupply. + public static final float MAX_HEALTH = 100.0f; private final int id; private final String name; private Team team; private PlayerControlState state; private Gun gun; + private float health; private transient long lastShot; private transient long reloadingStartedAt; @@ -27,6 +30,7 @@ public class Player extends PhysicsObject { this.state = new PlayerControlState(); this.state.setPlayerId(this.id); this.gun = Gun.winchester(); + this.health = MAX_HEALTH; this.useWeapon(); } @@ -111,6 +115,23 @@ public class Player extends PhysicsObject { public void resupply() { this.lastResupply = System.currentTimeMillis(); this.gun.refillClips(); + this.health = MAX_HEALTH; + } + + public float getHealth() { + return health; + } + + public void takeDamage(float damage) { + this.health = Math.max(this.health - damage, 0.0f); + } + + public void respawn() { + this.resupply(); + this.gun.emptyCurrentClip(); + if (this.team != null) { + this.setPosition(this.team.getSpawnPoint().add(Vec2.random(-Team.SPAWN_RADIUS / 2, Team.SPAWN_RADIUS / 2))); + } } @Override diff --git a/core/src/main/java/nl/andrewlalis/aos_core/model/Team.java b/core/src/main/java/nl/andrewlalis/aos_core/model/Team.java index 911fe32..3222d0d 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/model/Team.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/model/Team.java @@ -19,6 +19,8 @@ public class Team implements Serializable { private final List players; + private int score; + public Team(String name, Color color, Vec2 spawnPoint, Vec2 supplyPoint, Vec2 orientation) { this.name = name; this.color = color; @@ -26,6 +28,7 @@ public class Team implements Serializable { this.supplyPoint = supplyPoint; this.orientation = orientation; this.players = new ArrayList<>(); + this.score = 0; } public String getName() { @@ -51,4 +54,16 @@ public class Team implements Serializable { public List getPlayers() { return players; } + + public int getScore() { + return score; + } + + public void incrementScore() { + this.score++; + } + + public void resetScore() { + this.score = 0; + } } diff --git a/core/src/main/java/nl/andrewlalis/aos_core/model/tools/Gun.java b/core/src/main/java/nl/andrewlalis/aos_core/model/tools/Gun.java index df98666..0808a22 100644 --- a/core/src/main/java/nl/andrewlalis/aos_core/model/tools/Gun.java +++ b/core/src/main/java/nl/andrewlalis/aos_core/model/tools/Gun.java @@ -41,6 +41,11 @@ public class Gun implements Serializable { */ private final double bulletSpeed; + /** + * How much damage the bullet does for a direct hit. + */ + private final double baseDamage; + /** * Number of bullets left in the current clip. */ @@ -50,7 +55,7 @@ public class Gun implements Serializable { */ private int clipCount; - private Gun(GunType type, int maxClipCount, int clipSize, int bulletsPerRound, double accuracy, double shotCooldownTime, double reloadTime, double bulletSpeed) { + private Gun(GunType type, int maxClipCount, int clipSize, int bulletsPerRound, double accuracy, double shotCooldownTime, double reloadTime, double bulletSpeed, double baseDamage) { this.type = type; this.maxClipCount = maxClipCount; this.clipSize = clipSize; @@ -59,6 +64,7 @@ public class Gun implements Serializable { this.shotCooldownTime = shotCooldownTime; this.reloadTime = reloadTime; this.bulletSpeed = bulletSpeed; + this.baseDamage = baseDamage; this.currentClipBulletCount = 0; this.clipCount = maxClipCount; @@ -96,6 +102,10 @@ public class Gun implements Serializable { return bulletSpeed; } + public double getBaseDamage() { + return baseDamage; + } + public int getCurrentClipBulletCount() { return currentClipBulletCount; } @@ -112,6 +122,10 @@ public class Gun implements Serializable { this.currentClipBulletCount = Math.max(this.currentClipBulletCount - 1, 0); } + public void emptyCurrentClip() { + this.currentClipBulletCount = 0; + } + public boolean canReload() { return this.clipCount > 0; } @@ -124,14 +138,14 @@ public class Gun implements Serializable { } public static Gun ak47() { - return new Gun(GunType.SMG, 4, 30, 1, 0.10, 0.05, 1.2, 90); + return new Gun(GunType.SMG, 4, 30, 1, 0.10, 0.05, 1.2, 90, 40); } public static Gun m1Garand() { - return new Gun(GunType.RIFLE, 6, 8, 1, 0.02, 0.75, 1.5, 150); + return new Gun(GunType.RIFLE, 6, 8, 1, 0.02, 0.75, 1.5, 150, 100); } public static Gun winchester() { - return new Gun(GunType.SHOTGUN, 8, 4, 3, 0.15, 0.5, 2.0, 75); + return new Gun(GunType.SHOTGUN, 8, 4, 3, 0.15, 0.5, 2.0, 75, 60); } } diff --git a/server/src/main/java/nl/andrewlalis/aos_server/Server.java b/server/src/main/java/nl/andrewlalis/aos_server/Server.java index c6b4d10..fc6635c 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/Server.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/Server.java @@ -118,6 +118,15 @@ public class Server { } } + public void resetGame() { + for (Team t : this.world.getTeams()) { + t.resetScore(); + for (Player p : t.getPlayers()) { + p.respawn(); + } + } + } + public void broadcastMessage(Message message) { for (ClientHandler handler : this.clientHandlers) { handler.send(message); @@ -152,6 +161,9 @@ public class Server { player.setGun(Gun.winchester()); } handler.send(new SystemChatMessage(SystemChatMessage.Level.INFO, "Changed gun to " + player.getGun().getType().name() + ".")); + } else if (command.equalsIgnoreCase("reset")) { + this.resetGame(); + this.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.INFO, "Game has been reset.")); } } diff --git a/server/src/main/java/nl/andrewlalis/aos_server/WorldUpdater.java b/server/src/main/java/nl/andrewlalis/aos_server/WorldUpdater.java index 0f42e04..02c92f4 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/WorldUpdater.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/WorldUpdater.java @@ -77,9 +77,12 @@ public class WorldUpdater extends Thread { if (p.getState().isMovingBackward()) vy -= Player.MOVEMENT_SPEED; if (p.getState().isMovingLeft()) vx -= Player.MOVEMENT_SPEED; if (p.getState().isMovingRight()) vx += Player.MOVEMENT_SPEED; - Vec2 forwardVector = p.getOrientation().mul(vy); - Vec2 leftVector = p.getOrientation().perp().mul(vx); - Vec2 newPos = p.getPosition().add(forwardVector.mul(t)).add(leftVector.mul(t)); + Vec2 forwardVector = new Vec2(0, -1); + if (p.getTeam() != null) { + forwardVector = p.getTeam().getOrientation(); + } + Vec2 leftVector = forwardVector.perp(); + Vec2 newPos = p.getPosition().add(forwardVector.mul(vy * t)).add(leftVector.mul(vx * t)); double nx = newPos.x(); double ny = newPos.y(); @@ -171,11 +174,18 @@ public class WorldUpdater extends Thread { n = Math.max(Math.min(n, 1), 0); double dist = p.getPosition().dist(new Vec2(x1 + n * (x2 - x1), y1 + n * (y2 - y1))); if (dist < Player.RADIUS && (p.getTeam() == null || p.getTeam().getSpawnPoint().dist(p.getPosition()) > Team.SPAWN_RADIUS)) { - Player killer = this.world.getPlayers().get(b.getPlayerId()); - this.server.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.SEVERE, p.getName() + " was shot by " + killer.getName() + ".")); - world.getSoundsToPlay().add("death.wav"); - if (p.getTeam() != null) { - p.setPosition(p.getTeam().getSpawnPoint()); + + // Player was shot! + float damage = (float) (((Player.RADIUS - dist) / Player.RADIUS) * b.getGun().getBaseDamage()); + p.takeDamage(damage); + if (p.getHealth() == 0.0f) { + Player shooter = this.world.getPlayers().get(b.getPlayerId()); + this.server.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.SEVERE, p.getName() + " was shot by " + shooter.getName() + ".")); + world.getSoundsToPlay().add("death.wav"); + if (shooter.getTeam() != null) { + shooter.getTeam().incrementScore(); + } + p.respawn(); } } }