Improved client connection.

This commit is contained in:
Andrew Lalis 2021-06-20 11:58:47 +02:00
parent bd02d89995
commit 4bc994d07e
10 changed files with 238 additions and 38 deletions

View File

@ -1,5 +1,6 @@
package nl.andrewlalis.aos_client; 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.GameFrame;
import nl.andrewlalis.aos_client.view.GamePanel; import nl.andrewlalis.aos_client.view.GamePanel;
import nl.andrewlalis.aos_core.model.PlayerControlState; 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.ChatMessage;
import nl.andrewlalis.aos_core.net.chat.PlayerChatMessage; import nl.andrewlalis.aos_core.net.chat.PlayerChatMessage;
import javax.swing.*;
import java.io.IOException; import java.io.IOException;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -153,22 +153,7 @@ public class Client {
public static void main(String[] args) { public static void main(String[] args) {
String hostAndPort = JOptionPane.showInputDialog("Enter server host and port (host:port):"); ConnectDialog dialog = new ConnectDialog();
if (hostAndPort == null) throw new IllegalArgumentException("A host and port is required."); dialog.setVisible(true);
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);
}
} }
} }

View File

@ -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<String> 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);
}
}
}

View File

@ -48,9 +48,11 @@ public class GamePanel extends JPanel {
g2.clearRect(0, 0, this.getWidth(), this.getHeight()); g2.clearRect(0, 0, this.getWidth(), this.getHeight());
World world = client.getWorld(); World world = client.getWorld();
if (world != null) drawWorld(g2, world); if (world != null) {
drawWorld(g2, world);
drawStatus(g2, world);
}
drawChat(g2, world); drawChat(g2, world);
drawStatus(g2, world);
} }
private void drawWorld(Graphics2D g2, World world) { private void drawWorld(Graphics2D g2, World world) {
@ -234,5 +236,20 @@ public class GamePanel extends JPanel {
Gun gun = myPlayer.getGun(); Gun gun = myPlayer.getGun();
g2.drawString("Clips: " + gun.getClipCount() + " / " + gun.getMaxClipCount(), 5, this.getHeight() - 20); g2.drawString("Clips: " + gun.getClipCount() + " / " + gun.getMaxClipCount(), 5, this.getHeight() - 20);
g2.drawString("Bullets: " + gun.getCurrentClipBulletCount() + " / " + gun.getClipSize(), 5, this.getHeight() - 30); 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;
}
} }
} }

View File

@ -1,6 +1,8 @@
package nl.andrewlalis.aos_core.geom; package nl.andrewlalis.aos_core.geom;
import java.io.Serializable; import java.io.Serializable;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
public record Vec2(double x, double y) implements Serializable { 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() { public String toString() {
return "[ " + x + ", " + y + " ]"; 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);
}
} }

View File

@ -1,26 +1,34 @@
package nl.andrewlalis.aos_core.model; package nl.andrewlalis.aos_core.model;
import nl.andrewlalis.aos_core.geom.Vec2; import nl.andrewlalis.aos_core.geom.Vec2;
import nl.andrewlalis.aos_core.model.tools.Gun;
import java.util.Random; import java.util.Random;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
public class Bullet extends PhysicsObject { public class Bullet extends PhysicsObject {
private final int playerId; private final int playerId;
private final Gun gun;
public Bullet(Player player) { public Bullet(Player player) {
super(
player.getPosition().add(player.getOrientation().mul(1.5)),
player.getOrientation(),
null
);
this.playerId = player.getId(); 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(); Random r = ThreadLocalRandom.current();
Vec2 perturbation = new Vec2((r.nextDouble() - 0.5) * 2, (r.nextDouble() - 0.5) * 2).mul(player.getGun().getAccuracy()); 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() { public int getPlayerId() {
return playerId; return playerId;
} }
public Gun getGun() {
return gun;
}
} }

View File

@ -1,5 +1,6 @@
package nl.andrewlalis.aos_core.model; package nl.andrewlalis.aos_core.model;
import nl.andrewlalis.aos_core.geom.Vec2;
import nl.andrewlalis.aos_core.model.tools.Gun; import nl.andrewlalis.aos_core.model.tools.Gun;
import java.util.Objects; 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 MOVEMENT_SPEED = 10; // Movement speed, in m/s
public static final double RADIUS = 0.5; // Collision radius, in meters. 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 double RESUPPLY_COOLDOWN = 30; // Seconds between allowing resupply.
public static final float MAX_HEALTH = 100.0f;
private final int id; private final int id;
private final String name; private final String name;
private Team team; private Team team;
private PlayerControlState state; private PlayerControlState state;
private Gun gun; private Gun gun;
private float health;
private transient long lastShot; private transient long lastShot;
private transient long reloadingStartedAt; private transient long reloadingStartedAt;
@ -27,6 +30,7 @@ public class Player extends PhysicsObject {
this.state = new PlayerControlState(); this.state = new PlayerControlState();
this.state.setPlayerId(this.id); this.state.setPlayerId(this.id);
this.gun = Gun.winchester(); this.gun = Gun.winchester();
this.health = MAX_HEALTH;
this.useWeapon(); this.useWeapon();
} }
@ -111,6 +115,23 @@ public class Player extends PhysicsObject {
public void resupply() { public void resupply() {
this.lastResupply = System.currentTimeMillis(); this.lastResupply = System.currentTimeMillis();
this.gun.refillClips(); 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 @Override

View File

@ -19,6 +19,8 @@ public class Team implements Serializable {
private final List<Player> players; private final List<Player> players;
private int score;
public Team(String name, Color color, Vec2 spawnPoint, Vec2 supplyPoint, Vec2 orientation) { public Team(String name, Color color, Vec2 spawnPoint, Vec2 supplyPoint, Vec2 orientation) {
this.name = name; this.name = name;
this.color = color; this.color = color;
@ -26,6 +28,7 @@ public class Team implements Serializable {
this.supplyPoint = supplyPoint; this.supplyPoint = supplyPoint;
this.orientation = orientation; this.orientation = orientation;
this.players = new ArrayList<>(); this.players = new ArrayList<>();
this.score = 0;
} }
public String getName() { public String getName() {
@ -51,4 +54,16 @@ public class Team implements Serializable {
public List<Player> getPlayers() { public List<Player> getPlayers() {
return players; return players;
} }
public int getScore() {
return score;
}
public void incrementScore() {
this.score++;
}
public void resetScore() {
this.score = 0;
}
} }

View File

@ -41,6 +41,11 @@ public class Gun implements Serializable {
*/ */
private final double bulletSpeed; 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. * Number of bullets left in the current clip.
*/ */
@ -50,7 +55,7 @@ public class Gun implements Serializable {
*/ */
private int clipCount; 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.type = type;
this.maxClipCount = maxClipCount; this.maxClipCount = maxClipCount;
this.clipSize = clipSize; this.clipSize = clipSize;
@ -59,6 +64,7 @@ public class Gun implements Serializable {
this.shotCooldownTime = shotCooldownTime; this.shotCooldownTime = shotCooldownTime;
this.reloadTime = reloadTime; this.reloadTime = reloadTime;
this.bulletSpeed = bulletSpeed; this.bulletSpeed = bulletSpeed;
this.baseDamage = baseDamage;
this.currentClipBulletCount = 0; this.currentClipBulletCount = 0;
this.clipCount = maxClipCount; this.clipCount = maxClipCount;
@ -96,6 +102,10 @@ public class Gun implements Serializable {
return bulletSpeed; return bulletSpeed;
} }
public double getBaseDamage() {
return baseDamage;
}
public int getCurrentClipBulletCount() { public int getCurrentClipBulletCount() {
return currentClipBulletCount; return currentClipBulletCount;
} }
@ -112,6 +122,10 @@ public class Gun implements Serializable {
this.currentClipBulletCount = Math.max(this.currentClipBulletCount - 1, 0); this.currentClipBulletCount = Math.max(this.currentClipBulletCount - 1, 0);
} }
public void emptyCurrentClip() {
this.currentClipBulletCount = 0;
}
public boolean canReload() { public boolean canReload() {
return this.clipCount > 0; return this.clipCount > 0;
} }
@ -124,14 +138,14 @@ public class Gun implements Serializable {
} }
public static Gun ak47() { 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() { 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() { 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);
} }
} }

View File

@ -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) { public void broadcastMessage(Message message) {
for (ClientHandler handler : this.clientHandlers) { for (ClientHandler handler : this.clientHandlers) {
handler.send(message); handler.send(message);
@ -152,6 +161,9 @@ public class Server {
player.setGun(Gun.winchester()); player.setGun(Gun.winchester());
} }
handler.send(new SystemChatMessage(SystemChatMessage.Level.INFO, "Changed gun to " + player.getGun().getType().name() + ".")); 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."));
} }
} }

View File

@ -77,9 +77,12 @@ public class WorldUpdater extends Thread {
if (p.getState().isMovingBackward()) vy -= Player.MOVEMENT_SPEED; if (p.getState().isMovingBackward()) vy -= Player.MOVEMENT_SPEED;
if (p.getState().isMovingLeft()) vx -= Player.MOVEMENT_SPEED; if (p.getState().isMovingLeft()) vx -= Player.MOVEMENT_SPEED;
if (p.getState().isMovingRight()) vx += Player.MOVEMENT_SPEED; if (p.getState().isMovingRight()) vx += Player.MOVEMENT_SPEED;
Vec2 forwardVector = p.getOrientation().mul(vy); Vec2 forwardVector = new Vec2(0, -1);
Vec2 leftVector = p.getOrientation().perp().mul(vx); if (p.getTeam() != null) {
Vec2 newPos = p.getPosition().add(forwardVector.mul(t)).add(leftVector.mul(t)); 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 nx = newPos.x();
double ny = newPos.y(); double ny = newPos.y();
@ -171,11 +174,18 @@ public class WorldUpdater extends Thread {
n = Math.max(Math.min(n, 1), 0); n = Math.max(Math.min(n, 1), 0);
double dist = p.getPosition().dist(new Vec2(x1 + n * (x2 - x1), y1 + n * (y2 - y1))); 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)) { 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() + ".")); // Player was shot!
world.getSoundsToPlay().add("death.wav"); float damage = (float) (((Player.RADIUS - dist) / Player.RADIUS) * b.getGun().getBaseDamage());
if (p.getTeam() != null) { p.takeDamage(damage);
p.setPosition(p.getTeam().getSpawnPoint()); 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();
} }
} }
} }