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 ee7fc84..2b132fd 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/Client.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/Client.java @@ -131,7 +131,7 @@ public class Client { public void sendChat() { String message = this.chatBuffer.toString().trim(); - if (!message.isBlank()) { + if (!message.isBlank() && !message.equals("/")) { try { this.messageTransceiver.send(new PlayerChatMessage(this.playerId, message)); } catch (IOException e) { diff --git a/client/src/main/java/nl/andrewlalis/aos_client/Tester.java b/client/src/main/java/nl/andrewlalis/aos_client/Tester.java new file mode 100644 index 0000000..bf48292 --- /dev/null +++ b/client/src/main/java/nl/andrewlalis/aos_client/Tester.java @@ -0,0 +1,22 @@ +package nl.andrewlalis.aos_client; + +import java.io.IOException; +import java.util.concurrent.ThreadLocalRandom; + +public class Tester { + private static final String[] names = { + "andrew", "john", "william", "farnsworth", "xXx_noSc0p3r_xXx" + }; + + public static void main(String[] args) { + for (int i = 0; i < 2; i++) { + Client client = new Client(); + try { + client.connect("localhost", 8035, names[ThreadLocalRandom.current().nextInt(names.length)]); + } catch (IOException | ClassNotFoundException e) { + client.shutdown(); + e.printStackTrace(); + } + } + } +} 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 73ee2aa..5a1f38d 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 @@ -3,6 +3,7 @@ package nl.andrewlalis.aos_client.view; import nl.andrewlalis.aos_client.Client; import nl.andrewlalis.aos_core.model.*; import nl.andrewlalis.aos_core.model.tools.Gun; +import nl.andrewlalis.aos_core.model.tools.GunType; import nl.andrewlalis.aos_core.net.chat.ChatMessage; import nl.andrewlalis.aos_core.net.chat.PlayerChatMessage; import nl.andrewlalis.aos_core.net.chat.SystemChatMessage; @@ -63,7 +64,21 @@ public class GamePanel extends JPanel { this.drawField(g2, world); this.drawPlayers(g2, world); this.drawBullets(g2, world); + this.drawMarkers(g2, world, myPlayer); + g2.setTransform(pre); + + // Put shadow gradient. + RadialGradientPaint p = new RadialGradientPaint( + this.getWidth() / 2.0f, + this.getHeight() / 2.0f, + (float) (25 * scale), + new float[]{0.0f, 1.0f}, + new Color[]{new Color(0, 0, 0, 0), new Color(0, 0, 0, 255)}, + MultipleGradientPaint.CycleMethod.NO_CYCLE + ); + g2.setPaint(p); + g2.fillRect(0, 0, this.getWidth(), this.getHeight()); } private AffineTransform getWorldTransform(Player player, double scale) { @@ -97,12 +112,19 @@ public class GamePanel extends JPanel { for (Team t : world.getTeams()) { g2.setColor(t.getColor()); Ellipse2D.Double spawnCircle = new Ellipse2D.Double( - t.getSpawnPoint().x() - Player.RADIUS, - t.getSpawnPoint().y() - Player.RADIUS, - Player.RADIUS * 2, - Player.RADIUS * 2 + t.getSpawnPoint().x() - Team.SPAWN_RADIUS, + t.getSpawnPoint().y() - Team.SPAWN_RADIUS, + Team.SPAWN_RADIUS * 2, + Team.SPAWN_RADIUS * 2 ); g2.draw(spawnCircle); + Rectangle2D.Double supplyMarker = new Rectangle2D.Double( + t.getSupplyPoint().x() - Team.SUPPLY_POINT_RADIUS, + t.getSupplyPoint().y() - Team.SUPPLY_POINT_RADIUS, + Team.SUPPLY_POINT_RADIUS * 2, + Team.SUPPLY_POINT_RADIUS * 2 + ); + g2.draw(supplyMarker); } } @@ -110,7 +132,6 @@ public class GamePanel extends JPanel { for (Player p : world.getPlayers().values()) { AffineTransform pre = g2.getTransform(); AffineTransform tx = g2.getTransform(); - tx.translate(p.getPosition().x(), p.getPosition().y()); tx.rotate(p.getOrientation().x(), p.getOrientation().y()); g2.setTransform(tx); @@ -119,23 +140,30 @@ public class GamePanel extends JPanel { Color playerColor = p.getTeam() != null ? p.getTeam().getColor() : Color.BLACK; g2.setColor(playerColor); g2.fill(dot); - - g2.setColor(Color.GRAY); - Rectangle2D.Double gun = new Rectangle2D.Double( - 0, - 0.5, - 2, - 0.25 - ); - g2.fill(gun); - + this.drawGun(g2, p.getGun()); g2.setTransform(pre); } } + private void drawGun(Graphics2D g2, Gun gun) { + g2.setColor(Color.GRAY); + if (gun.getType() == GunType.RIFLE) { + g2.setColor(new Color(59, 43, 0)); + } else if (gun.getType() == GunType.SHOTGUN) { + g2.setColor(new Color(18, 18, 17)); + } + Rectangle2D.Double gunBarrel = new Rectangle2D.Double( + 0, + 0.5, + 2, + 0.25 + ); + g2.fill(gunBarrel); + } + private void drawBullets(Graphics2D g2, World world) { - g2.setColor(Color.YELLOW); - double bulletSize = 0.5; + g2.setColor(Color.BLACK); + double bulletSize = 0.25; for (Bullet b : world.getBullets()) { Ellipse2D.Double bulletShape = new Ellipse2D.Double( b.getPosition().x() - bulletSize / 2, @@ -147,6 +175,21 @@ public class GamePanel extends JPanel { } } + private void drawMarkers(Graphics2D g2, World world, Player myPlayer) { + g2.setColor(Color.WHITE); + for (Player p : world.getPlayers().values()) { + if (p.getId() == myPlayer.getId()) continue; + AffineTransform pre = g2.getTransform(); + AffineTransform tx = g2.getTransform(); + tx.translate(p.getPosition().x(), p.getPosition().y()); + tx.rotate(myPlayer.getTeam().getOrientation().perp().angle()); + tx.scale(0.1, 0.1); + g2.setTransform(tx); + g2.drawString(p.getName(), 0, 0); + g2.setTransform(pre); + } + } + private void drawChat(Graphics2D g2, World world) { int height = g2.getFontMetrics().getHeight(); int y = height; diff --git a/client/src/main/resources/sound/m1garand-shot1.wav b/client/src/main/resources/sound/m1garand-shot1.wav new file mode 100644 index 0000000..6b689bf Binary files /dev/null and b/client/src/main/resources/sound/m1garand-shot1.wav differ diff --git a/client/src/main/resources/sound/reload.wav b/client/src/main/resources/sound/reload.wav new file mode 100644 index 0000000..54608dc Binary files /dev/null and b/client/src/main/resources/sound/reload.wav differ diff --git a/client/src/main/resources/sound/shotgun-shot1.wav b/client/src/main/resources/sound/shotgun-shot1.wav new file mode 100644 index 0000000..e60c709 Binary files /dev/null and b/client/src/main/resources/sound/shotgun-shot1.wav differ 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 15099f6..2c843e7 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 @@ -7,6 +7,7 @@ import java.util.Objects; 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. private final int id; private final String name; @@ -17,6 +18,7 @@ public class Player extends PhysicsObject { private transient long lastShot; private transient long reloadingStartedAt; private boolean reloading; + private transient long lastResupply; public Player(int id, String name, Team team) { this.id = id; @@ -24,7 +26,7 @@ public class Player extends PhysicsObject { this.team = team; this.state = new PlayerControlState(); this.state.setPlayerId(this.id); - this.gun = Gun.m1Garand(); + this.gun = Gun.winchester(); this.useWeapon(); } @@ -69,7 +71,8 @@ public class Player extends PhysicsObject { !this.state.isReloading() && !this.reloading && this.gun.getCurrentClipBulletCount() > 0 && - this.lastShot + this.gun.getShotCooldownTime() * 1000 < System.currentTimeMillis(); + this.lastShot + this.gun.getShotCooldownTime() * 1000 < System.currentTimeMillis() && + (this.getTeam() == null || this.getTeam().getSpawnPoint().dist(this.getPosition()) > Team.SPAWN_RADIUS); } public void useWeapon() { @@ -99,6 +102,17 @@ public class Player extends PhysicsObject { return reloading; } + public boolean canResupply() { + return this.team != null && + this.team.getSupplyPoint().dist(this.getPosition()) < Team.SUPPLY_POINT_RADIUS && + System.currentTimeMillis() - this.lastResupply > RESUPPLY_COOLDOWN * 1000; + } + + public void resupply() { + this.lastResupply = System.currentTimeMillis(); + this.gun.refillClips(); + } + @Override public boolean equals(Object o) { if (this == o) return true; 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 ff9612e..911fe32 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 @@ -8,17 +8,22 @@ import java.util.ArrayList; import java.util.List; public class Team implements Serializable { + public static final double SPAWN_RADIUS = 3; + public static final double SUPPLY_POINT_RADIUS = 2; + private final String name; private final java.awt.Color color; private final Vec2 spawnPoint; + private final Vec2 supplyPoint; private final Vec2 orientation; private final List players; - public Team(String name, Color color, Vec2 spawnPoint, Vec2 orientation) { + public Team(String name, Color color, Vec2 spawnPoint, Vec2 supplyPoint, Vec2 orientation) { this.name = name; this.color = color; this.spawnPoint = spawnPoint; + this.supplyPoint = supplyPoint; this.orientation = orientation; this.players = new ArrayList<>(); } @@ -35,6 +40,10 @@ public class Team implements Serializable { return spawnPoint; } + public Vec2 getSupplyPoint() { + return supplyPoint; + } + public Vec2 getOrientation() { return orientation; } 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 2e96c78..df98666 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 @@ -15,6 +15,12 @@ public class Gun implements Serializable { */ private final int clipSize; + /** + * Number of bullets that are fired simultaneously per round. Usually only + * shotguns fire multiple. + */ + private final int bulletsPerRound; + /** * How accurate shots from this gun are. */ @@ -44,10 +50,11 @@ public class Gun implements Serializable { */ private int clipCount; - private Gun(GunType type, int maxClipCount, int clipSize, 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) { this.type = type; this.maxClipCount = maxClipCount; this.clipSize = clipSize; + this.bulletsPerRound = bulletsPerRound; this.accuracy = accuracy; this.shotCooldownTime = shotCooldownTime; this.reloadTime = reloadTime; @@ -69,6 +76,10 @@ public class Gun implements Serializable { return clipSize; } + public int getBulletsPerRound() { + return bulletsPerRound; + } + public double getAccuracy() { return accuracy; } @@ -93,6 +104,10 @@ public class Gun implements Serializable { return clipCount; } + public void refillClips() { + this.clipCount = this.maxClipCount; + } + public void decrementBulletCount() { this.currentClipBulletCount = Math.max(this.currentClipBulletCount - 1, 0); } @@ -109,14 +124,14 @@ public class Gun implements Serializable { } public static Gun ak47() { - return new Gun(GunType.SMG, 4, 30, 0.10, 0.05, 1.2, 90); + return new Gun(GunType.SMG, 4, 30, 1, 0.10, 0.05, 1.2, 90); } public static Gun m1Garand() { - return new Gun(GunType.RIFLE, 6, 8, 0.02, 0.75, 1.5, 150); + return new Gun(GunType.RIFLE, 6, 8, 1, 0.02, 0.75, 1.5, 150); } public static Gun winchester() { - return new Gun(GunType.SHOTGUN, 8, 4, 0.15, 0.5, 2.0, 75); + return new Gun(GunType.SHOTGUN, 8, 4, 3, 0.15, 0.5, 2.0, 75); } } diff --git a/server/src/main/java/nl/andrewlalis/aos_server/ClientHandler.java b/server/src/main/java/nl/andrewlalis/aos_server/ClientHandler.java index 3b6228a..0806f53 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/ClientHandler.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/ClientHandler.java @@ -7,8 +7,11 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class ClientHandler extends Thread { + private final ExecutorService sendingQueue = Executors.newSingleThreadExecutor(); private final Server server; private final Socket socket; private final ObjectOutputStream out; @@ -33,9 +36,15 @@ public class ClientHandler extends Thread { this.running = false; } - public synchronized void send(Message message) throws IOException { - this.out.reset(); - this.out.writeObject(message); + public void send(Message message) { + this.sendingQueue.submit(() -> { + try { + this.out.reset(); + this.out.writeObject(message); + } catch (IOException e) { + e.printStackTrace(); + } + }); } @Override @@ -48,7 +57,7 @@ public class ClientHandler extends Thread { IdentMessage ident = (IdentMessage) msg; this.playerId = this.server.registerNewPlayer(ident.getName(), this); } else if (msg.getType() == Type.CHAT) { - this.server.broadcastPlayerChat(this.playerId, (ChatMessage) msg); + this.server.handlePlayerChat(this, this.playerId, (ChatMessage) msg); } else if (msg.getType() == Type.PLAYER_CONTROL_STATE) { this.server.updatePlayerState(((PlayerControlStateMessage) msg).getPlayerControlState()); } 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 ed58659..c6b4d10 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/Server.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/Server.java @@ -2,6 +2,7 @@ package nl.andrewlalis.aos_server; import nl.andrewlalis.aos_core.geom.Vec2; import nl.andrewlalis.aos_core.model.*; +import nl.andrewlalis.aos_core.model.tools.Gun; import nl.andrewlalis.aos_core.net.Message; import nl.andrewlalis.aos_core.net.PlayerRegisteredMessage; import nl.andrewlalis.aos_core.net.WorldUpdateMessage; @@ -13,6 +14,7 @@ import java.awt.*; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; +import java.util.Arrays; import java.util.List; import java.util.Scanner; import java.util.concurrent.CopyOnWriteArrayList; @@ -37,8 +39,20 @@ public class Server { world.getBarricades().add(new Barricade(0, 30, 10, 10)); world.getBarricades().add(new Barricade(40, 30, 10, 10)); - world.getTeams().add(new Team("Red", Color.RED, new Vec2(3, 3), new Vec2(0, 1))); - world.getTeams().add(new Team("Blue", Color.BLUE, new Vec2(world.getSize().x() - 3, world.getSize().y() - 3), new Vec2(0, -1))); + world.getTeams().add(new Team( + "Red", + Color.RED, + new Vec2(3, 3), + new Vec2(15, 3), + new Vec2(0, 1) + )); + world.getTeams().add(new Team( + "Blue", + Color.BLUE, + new Vec2(world.getSize().x() - 3, world.getSize().y() - 3), + new Vec2(world.getSize().x() - 15, world.getSize().y() - 3), + new Vec2(0, -1) + )); this.worldUpdater = new WorldUpdater(this, this.world); System.out.println("Started AOS-Server TCP on port " + port); @@ -63,11 +77,7 @@ public class Server { } Player p = new Player(id, name, team); this.world.getPlayers().put(p.getId(), p); - try { - handler.send(new PlayerRegisteredMessage(id)); - } catch (IOException e) { - e.printStackTrace(); - } + handler.send(new PlayerRegisteredMessage(id)); String message = p.getName() + " connected."; this.broadcastMessage(new SystemChatMessage(SystemChatMessage.Level.INFO, message)); System.out.println(message); @@ -97,11 +107,7 @@ public class Server { public void sendWorldToClients() { for (ClientHandler handler : this.clientHandlers) { - try { - handler.send(new WorldUpdateMessage(this.world)); - } catch (IOException e) { - e.printStackTrace(); - } + handler.send(new WorldUpdateMessage(this.world)); } } @@ -114,18 +120,39 @@ public class Server { public void broadcastMessage(Message message) { for (ClientHandler handler : this.clientHandlers) { - try { - handler.send(message); - } catch (IOException e) { - e.printStackTrace(); - } + handler.send(message); } } - public void broadcastPlayerChat(int playerId, ChatMessage msg) { + public void handlePlayerChat(ClientHandler handler, int playerId, ChatMessage msg) { Player p = this.world.getPlayers().get(playerId); if (p == null) return; - this.broadcastMessage(new PlayerChatMessage(p.getId(), msg.getText())); + if (msg.getText().startsWith("/")) { + String[] words = msg.getText().substring(1).split("\\s+"); + if (words.length == 0) return; + String command = words[0]; + String[] args = Arrays.copyOfRange(words, 1, words.length); + this.handleCommand(handler, p, command, args); + } else { + this.broadcastMessage(new PlayerChatMessage(p.getId(), msg.getText())); + } + } + + public void handleCommand(ClientHandler handler, Player player, String command, String[] args) { + if (command.equalsIgnoreCase("gun")) { + if (args.length < 1) { + return; + } + String gunName = args[0]; + if (gunName.equalsIgnoreCase("smg")) { + player.setGun(Gun.ak47()); + } else if (gunName.equalsIgnoreCase("rifle")) { + player.setGun(Gun.m1Garand()); + } else if (gunName.equalsIgnoreCase("shotgun")) { + player.setGun(Gun.winchester()); + } + handler.send(new SystemChatMessage(SystemChatMessage.Level.INFO, "Changed gun to " + player.getGun().getType().name() + ".")); + } } 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 6236897..0f42e04 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/WorldUpdater.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/WorldUpdater.java @@ -1,10 +1,8 @@ package nl.andrewlalis.aos_server; import nl.andrewlalis.aos_core.geom.Vec2; -import nl.andrewlalis.aos_core.model.Barricade; -import nl.andrewlalis.aos_core.model.Bullet; -import nl.andrewlalis.aos_core.model.Player; -import nl.andrewlalis.aos_core.model.World; +import nl.andrewlalis.aos_core.model.*; +import nl.andrewlalis.aos_core.model.tools.GunType; import nl.andrewlalis.aos_core.net.chat.SystemChatMessage; import java.util.ArrayList; @@ -113,12 +111,24 @@ public class WorldUpdater extends Thread { if (ny - Player.RADIUS < 0) ny = Player.RADIUS; if (ny + Player.RADIUS > this.world.getSize().y()) ny = this.world.getSize().y() - Player.RADIUS; p.setPosition(new Vec2(nx, ny)); + + if (p.canResupply()) { + p.resupply(); + } } private void updatePlayerShooting(Player p) { if (p.canUseWeapon()) { - this.world.getBullets().add(new Bullet(p)); - this.world.getSoundsToPlay().add("ak47shot1.wav"); + for (int i = 0; i < p.getGun().getBulletsPerRound(); i++) { + this.world.getBullets().add(new Bullet(p)); + } + String sound = "ak47shot1.wav"; + if (p.getGun().getType() == GunType.RIFLE) { + sound = "m1garand-shot1.wav"; + } else if (p.getGun().getType() == GunType.SHOTGUN) { + sound = "shotgun-shot1.wav"; + } + this.world.getSoundsToPlay().add(sound); p.useWeapon(); } if (p.getState().isReloading() && !p.isReloading() && p.getGun().canReload()) { @@ -126,6 +136,7 @@ public class WorldUpdater extends Thread { } if (p.isReloading() && p.isReloadingComplete()) { p.finishReloading(); + this.world.getSoundsToPlay().add("reload.wav"); } } @@ -159,7 +170,7 @@ public class WorldUpdater extends Thread { double n = ((p.getPosition().x() - x1) * (x2 - x1) + (p.getPosition().y() - y1) * (y2 - y1)) / lineDist; 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) { + 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");