From 92eb93815ddb7213ddd2ceda713fa50188bd3d80 Mon Sep 17 00:00:00 2001 From: jzitnik-dev Date: Mon, 2 Feb 2026 18:51:51 +0100 Subject: [PATCH] feat: Some more multiplayer stuff Hello miss Meitnerova... If you are not Meitnerova but some random person stalking my projects go get a life. Anyways have a great time reading this awful code :) --- .idea/compiler.xml | 3 - .idea/misc.xml | 2 +- .../main/java/cz/jzitnik/common/Config.java | 5 + .../common/models/coordinates/RoomCords.java | 4 + .../common/models/player/PlayerCreation.java | 3 + .../socket/messages/player/PlayerMove.java | 7 + .../messages/player/PlayerMovedInUrRoom.java | 13 + .../handlers/PlayerMoveEventHandler.java | 7 +- .../cz/jzitnik/client/game/GameState.java | 6 + .../cz/jzitnik/client/game/OtherPlayer.java | 20 ++ .../java/cz/jzitnik/client/game/Player.java | 13 +- .../setup/scenes/connect/ServerChoose.java | 321 ++++++++++++++++-- .../java/cz/jzitnik/client/ui/Inventory.java | 13 +- .../jzitnik/client/ui/pixels/AlphaPixel.java | 4 + .../cz/jzitnik/client/ui/utils/Button.java | 50 +++ .../jzitnik/server/context/GlobalContext.java | 37 +- .../events/handlers/CreateGameHandler.java | 22 +- .../events/handlers/PlayerMoveHandler.java | 26 ++ .../java/cz/jzitnik/server/game/Client.java | 22 +- .../java/cz/jzitnik/server/game/Game.java | 9 + .../java/cz/jzitnik/server/game/Player.java | 15 + .../cz/jzitnik/server/socket/WebSocket.java | 3 +- .../server/utils/PasswordGenerator.java | 23 ++ server/src/main/resources/config.properties | 1 + 24 files changed, 572 insertions(+), 57 deletions(-) create mode 100644 common/src/main/java/cz/jzitnik/common/Config.java create mode 100644 common/src/main/java/cz/jzitnik/common/socket/messages/player/PlayerMove.java create mode 100644 common/src/main/java/cz/jzitnik/common/socket/messages/player/PlayerMovedInUrRoom.java create mode 100644 game/src/main/java/cz/jzitnik/client/game/OtherPlayer.java create mode 100644 game/src/main/java/cz/jzitnik/client/ui/utils/Button.java create mode 100644 server/src/main/java/cz/jzitnik/server/events/handlers/PlayerMoveHandler.java create mode 100644 server/src/main/java/cz/jzitnik/server/game/Player.java create mode 100644 server/src/main/java/cz/jzitnik/server/utils/PasswordGenerator.java create mode 100644 server/src/main/resources/config.properties diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b08d639..9abc593 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -14,7 +14,6 @@ - @@ -24,7 +23,6 @@ - @@ -34,7 +32,6 @@ - diff --git a/.idea/misc.xml b/.idea/misc.xml index 1044c37..86993b2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,5 +8,5 @@ - + \ No newline at end of file diff --git a/common/src/main/java/cz/jzitnik/common/Config.java b/common/src/main/java/cz/jzitnik/common/Config.java new file mode 100644 index 0000000..de224e3 --- /dev/null +++ b/common/src/main/java/cz/jzitnik/common/Config.java @@ -0,0 +1,5 @@ +package cz.jzitnik.common; + +public class Config { + public static final int WORLD_PASSWORD_LENGTH = 5; +} diff --git a/common/src/main/java/cz/jzitnik/common/models/coordinates/RoomCords.java b/common/src/main/java/cz/jzitnik/common/models/coordinates/RoomCords.java index 724f2ce..4534fb4 100644 --- a/common/src/main/java/cz/jzitnik/common/models/coordinates/RoomCords.java +++ b/common/src/main/java/cz/jzitnik/common/models/coordinates/RoomCords.java @@ -29,6 +29,10 @@ public class RoomCords implements Cloneable, Serializable { this.y = y; } + public void updateCords(RoomCords roomCords) { + updateCords(roomCords.getX(), roomCords.getY()); + } + public void updateCordsWithColliders(List colliders, int x, int y, RoomPart playerCollider) { var normalizedPlayerCollider = new RoomPart( new RoomCords(playerCollider.getStart().getX() + x, playerCollider.getStart().getY() + y), diff --git a/common/src/main/java/cz/jzitnik/common/models/player/PlayerCreation.java b/common/src/main/java/cz/jzitnik/common/models/player/PlayerCreation.java index 04a6024..a8587ac 100644 --- a/common/src/main/java/cz/jzitnik/common/models/player/PlayerCreation.java +++ b/common/src/main/java/cz/jzitnik/common/models/player/PlayerCreation.java @@ -5,11 +5,14 @@ import com.fasterxml.jackson.annotation.JsonProperty; import cz.jzitnik.common.models.coordinates.RoomCords; import cz.jzitnik.common.models.coordinates.RoomPart; import lombok.Getter; +import lombok.Setter; import java.io.Serializable; @Getter public final class PlayerCreation implements Serializable { + @Setter + private int id; private final RoomCords playerCords; private final RoomPart collider; diff --git a/common/src/main/java/cz/jzitnik/common/socket/messages/player/PlayerMove.java b/common/src/main/java/cz/jzitnik/common/socket/messages/player/PlayerMove.java new file mode 100644 index 0000000..8df9a64 --- /dev/null +++ b/common/src/main/java/cz/jzitnik/common/socket/messages/player/PlayerMove.java @@ -0,0 +1,7 @@ +package cz.jzitnik.common.socket.messages.player; + +import cz.jzitnik.common.models.coordinates.RoomCords; +import cz.jzitnik.common.socket.SocketMessage; + +public record PlayerMove(RoomCords newCords) implements SocketMessage { +} diff --git a/common/src/main/java/cz/jzitnik/common/socket/messages/player/PlayerMovedInUrRoom.java b/common/src/main/java/cz/jzitnik/common/socket/messages/player/PlayerMovedInUrRoom.java new file mode 100644 index 0000000..3559743 --- /dev/null +++ b/common/src/main/java/cz/jzitnik/common/socket/messages/player/PlayerMovedInUrRoom.java @@ -0,0 +1,13 @@ +package cz.jzitnik.common.socket.messages.player; + +import cz.jzitnik.common.models.coordinates.RoomCords; +import cz.jzitnik.common.socket.SocketMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PlayerMovedInUrRoom implements SocketMessage { + private int playerId; + private RoomCords cords; +} diff --git a/game/src/main/java/cz/jzitnik/client/events/handlers/PlayerMoveEventHandler.java b/game/src/main/java/cz/jzitnik/client/events/handlers/PlayerMoveEventHandler.java index 9ab9718..ca0e2c5 100644 --- a/game/src/main/java/cz/jzitnik/client/events/handlers/PlayerMoveEventHandler.java +++ b/game/src/main/java/cz/jzitnik/client/events/handlers/PlayerMoveEventHandler.java @@ -8,10 +8,7 @@ import cz.jzitnik.client.annotations.injectors.InjectDependency; import cz.jzitnik.client.annotations.injectors.InjectState; import cz.jzitnik.client.config.Debugging; import cz.jzitnik.client.config.PlayerConfig; -import cz.jzitnik.client.events.MouseMoveEvent; -import cz.jzitnik.client.events.PlayerMoveEvent; -import cz.jzitnik.client.events.RerenderScreen; -import cz.jzitnik.client.events.RoomChangeEvent; +import cz.jzitnik.client.events.*; import cz.jzitnik.client.game.GameRoom; import cz.jzitnik.client.game.GameState; import cz.jzitnik.client.game.Player; @@ -26,6 +23,7 @@ import cz.jzitnik.client.utils.RerenderUtils; import cz.jzitnik.client.utils.events.AbstractEventHandler; import cz.jzitnik.client.utils.events.Event; import cz.jzitnik.client.utils.events.EventManager; +import cz.jzitnik.common.socket.messages.player.PlayerMove; import lombok.extern.slf4j.Slf4j; import java.awt.image.BufferedImage; @@ -146,6 +144,7 @@ public class PlayerMoveEventHandler extends AbstractEventHandler otherPlayers = new ArrayList<>(); + @Getter @Setter private Interactable interacting; diff --git a/game/src/main/java/cz/jzitnik/client/game/OtherPlayer.java b/game/src/main/java/cz/jzitnik/client/game/OtherPlayer.java new file mode 100644 index 0000000..e131693 --- /dev/null +++ b/game/src/main/java/cz/jzitnik/client/game/OtherPlayer.java @@ -0,0 +1,20 @@ +package cz.jzitnik.client.game; + +import cz.jzitnik.common.models.coordinates.RoomCords; +import cz.jzitnik.common.models.player.PlayerCreation; +import lombok.Getter; +import lombok.Setter; + +@Getter +public class OtherPlayer { + private final int id; + private boolean hitAnimationOn = false; + private final RoomCords playerCords; + @Setter + private Player.PlayerRotation playerRotation = Player.PlayerRotation.FRONT; + + public OtherPlayer(PlayerCreation playerCreation) { + this.id = playerCreation.getId(); + this.playerCords = playerCreation.getPlayerCords(); + } +} diff --git a/game/src/main/java/cz/jzitnik/client/game/Player.java b/game/src/main/java/cz/jzitnik/client/game/Player.java index 0a437fa..b2c15b8 100644 --- a/game/src/main/java/cz/jzitnik/client/game/Player.java +++ b/game/src/main/java/cz/jzitnik/client/game/Player.java @@ -1,7 +1,5 @@ package cz.jzitnik.client.game; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; import cz.jzitnik.client.events.RerenderPart; import cz.jzitnik.client.game.items.GameItem; import cz.jzitnik.client.game.items.types.interfaces.WeaponInterface; @@ -26,6 +24,7 @@ import java.util.concurrent.TimeUnit; @Getter @Slf4j public class Player { + private final int id; public static final int MAX_STAMINA = 20; public static final int MAX_HEALTH = 30; private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); @@ -43,18 +42,10 @@ public class Player { private boolean hitAnimationOn = false; private ScheduledFuture currentTimeoutHitAnimation = null; - @JsonCreator - public Player( - @JsonProperty("playerCords") RoomCords playerCords, - @JsonProperty("collider") RoomPart collider - ) { - this.playerCords = playerCords; - this.collider = collider; - } - public Player(PlayerCreation playerCreation) { this.playerCords = playerCreation.getPlayerCords(); this.collider = playerCreation.getCollider(); + this.id = playerCreation.getId(); } public void increaseStamina() { diff --git a/game/src/main/java/cz/jzitnik/client/game/setup/scenes/connect/ServerChoose.java b/game/src/main/java/cz/jzitnik/client/game/setup/scenes/connect/ServerChoose.java index 2f483a8..888a378 100644 --- a/game/src/main/java/cz/jzitnik/client/game/setup/scenes/connect/ServerChoose.java +++ b/game/src/main/java/cz/jzitnik/client/game/setup/scenes/connect/ServerChoose.java @@ -2,6 +2,7 @@ package cz.jzitnik.client.game.setup.scenes.connect; import com.googlecode.lanterna.TerminalSize; import com.googlecode.lanterna.TextColor; +import com.googlecode.lanterna.graphics.TextGraphics; import com.googlecode.lanterna.input.KeyType; import com.googlecode.lanterna.screen.TerminalScreen; import cz.jzitnik.client.annotations.injectors.InjectDependency; @@ -9,11 +10,13 @@ import cz.jzitnik.client.annotations.injectors.InjectState; import cz.jzitnik.client.events.KeyboardPressEvent; import cz.jzitnik.client.events.MouseAction; import cz.jzitnik.client.events.SendSocketMessageEvent; +import cz.jzitnik.client.game.GameState; import cz.jzitnik.client.screens.Screen; import cz.jzitnik.client.screens.scenes.Scene; import cz.jzitnik.client.socket.Client; import cz.jzitnik.client.sound.SoundPlayer; import cz.jzitnik.client.states.TerminalState; +import cz.jzitnik.client.ui.Inventory; import cz.jzitnik.client.ui.pixels.AlphaPixel; import cz.jzitnik.client.ui.pixels.Empty; import cz.jzitnik.client.ui.utils.Input; @@ -22,10 +25,13 @@ import cz.jzitnik.client.utils.TextRenderer; import cz.jzitnik.client.utils.events.EventManager; import cz.jzitnik.common.socket.messages.game.creation.CreateGame; import jakarta.websocket.DeploymentException; +import cz.jzitnik.client.ui.utils.Button; +import lombok.extern.slf4j.Slf4j; import java.awt.*; import java.io.IOException; +@Slf4j public class ServerChoose extends Scene { public ServerChoose(DependencyManager dependencyManager) { GameMenuAudioScreen gameMenuScreen = new GameMenuAudioScreen(); @@ -78,6 +84,12 @@ public class ServerChoose extends Scene { @InjectDependency private TextRenderer textRenderer; + @InjectState + private GameState gameState; + + @InjectDependency + private DependencyManager dependencyManager; + private void renderInput(boolean refresh) { var tg = terminalState.getTextGraphics(); TerminalScreen screen = terminalState.getTerminalScreen(); @@ -136,32 +148,12 @@ public class ServerChoose extends Scene { AlphaPixel[][] selectServer = textRenderer.renderText("Enter server IP", terminalSize.getColumns(), 20, Color.WHITE, 15f, true); - int padY = 10; - - for (int y = 0; y < selectServer.length; y += 2) { - for (int x = 0; x < selectServer[y].length; x++) { - AlphaPixel topPixel = selectServer[y][x]; - - AlphaPixel bottomPixel; - if (y + 1 < selectServer.length) { - bottomPixel = selectServer[y + 1][x]; - } else { - bottomPixel = new Empty(); - } - - int termX = x; - int termY = padY / 2 + y / 2; - - tg.setBackgroundColor(topPixel instanceof Empty ? TextColor.ANSI.BLACK : topPixel.getColor()); - tg.setForegroundColor(bottomPixel instanceof Empty ? TextColor.ANSI.BLACK : bottomPixel.getColor()); - tg.setCharacter(termX, termY, '▄'); - } - } + render(selectServer, 0, 10, tg); renderInput(false); try { - screen.refresh(com.googlecode.lanterna.screen.Screen.RefreshType.DELTA); + screen.refresh(com.googlecode.lanterna.screen.Screen.RefreshType.COMPLETE); } catch (IOException e) { throw new RuntimeException(e); } @@ -178,7 +170,10 @@ public class ServerChoose extends Scene { connecting = true; client.connect(ipBuffer.toString()); - eventManager.emitEvent(new SendSocketMessageEvent(new CreateGame())); + Screen screen = new ActionSelector(); + dependencyManager.inject(screen); + gameState.setScreen(screen); + screen.fullRender(); } catch (DeploymentException | IOException e) { connecting = false; } @@ -195,7 +190,7 @@ public class ServerChoose extends Scene { return; } - if (event.getKeyStroke().getKeyType() == KeyType.Character) { + if (event.getKeyStroke().getKeyType() == KeyType.Character && !event.getKeyStroke().isCtrlDown()) { ipBuffer.append(event.getKeyStroke().getCharacter()); renderInput(true); } @@ -205,4 +200,282 @@ public class ServerChoose extends Scene { public void handleMouseAction(MouseAction event) { } } + + private static final class ActionSelector extends Screen { + @InjectDependency + private TextRenderer textRenderer; + + @InjectState + private TerminalState terminalState; + + @InjectDependency + private EventManager eventManager; + + @InjectState + private GameState gameState; + + private int selectedIndex = -1; + + @Override + public void fullRender() { + TerminalScreen screen = terminalState.getTerminalScreen(); + var tg = terminalState.getTextGraphics(); + screen.clear(); + TerminalSize terminalSize = screen.getTerminalSize(); + + for (int y = 0; y < terminalSize.getRows(); y += 1) { + for (int x = 0; x < terminalSize.getColumns(); x++) { + tg.setBackgroundColor(TextColor.ANSI.BLACK); + tg.setForegroundColor(TextColor.ANSI.BLACK); + tg.setCharacter(x, y, '▄'); + } + } + + AlphaPixel[][] selectAction = textRenderer.renderText("Select action", terminalSize.getColumns(), 20, Color.WHITE, 15f, true); + + render(selectAction, 0, 10, tg); + renderButtons(); + + try { + screen.refresh(com.googlecode.lanterna.screen.Screen.RefreshType.COMPLETE); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static final int BUTTON_HEIGHT = 20; + private static final int BUTTON_WIDTH = 200; + private static final int BUTTON_GAP = 10; + private static final int BUTTON_COUNT = 2; + private static final int BUTTONS_HEIGHT = BUTTON_HEIGHT * BUTTON_COUNT + (BUTTON_COUNT - 1) * BUTTON_GAP; + + private void renderButtons() { + var tg = terminalState.getTextGraphics(); + TerminalSize terminalSize = terminalState.getTerminalScreen().getTerminalSize(); + + final int BUTTON_PAD_X = terminalSize.getColumns() / 2 - BUTTON_WIDTH / 2; + final int BUTTON_PAD_Y = terminalSize.getRows() - BUTTONS_HEIGHT / 2; + + Button button = new Button( + Inventory.BORDER_COLOR, + Inventory.BACKGROUND_COLOR, + Inventory.BACKGROUND_COLOR_HOVERED, + Color.WHITE, + BUTTON_HEIGHT, + BUTTON_WIDTH, + 1, + 15f, + textRenderer + ); + + render(button.render("Create a world", selectedIndex == 0), BUTTON_PAD_X, BUTTON_PAD_Y, tg); + render(button.render("Connect to an existing world", selectedIndex == 1), BUTTON_PAD_X, BUTTON_PAD_Y + (BUTTON_HEIGHT + BUTTON_GAP), tg); + } + + @Override + public void handleMouseAction(MouseAction event) { + TerminalSize terminalSize = terminalState.getTerminalScreen().getTerminalSize(); + + final int BUTTON_START_X = terminalSize.getColumns() / 2 - BUTTON_WIDTH / 2; + final int BUTTON_START_Y = terminalSize.getRows() - BUTTONS_HEIGHT / 2; + final int BUTTON_END_X = BUTTON_START_X + BUTTON_WIDTH; + final int BUTTON_END_Y = BUTTON_START_Y + BUTTONS_HEIGHT; + + final int TERMINAL_X = event.getPosition().getColumn(); + final int TERMINAL_Y = event.getPosition().getRow() * 2; + + final int SINGLE_BUTTON_HEIGHT = BUTTON_HEIGHT + BUTTON_GAP; + + int index = (TERMINAL_Y - BUTTON_START_Y) / SINGLE_BUTTON_HEIGHT; + int rest = (TERMINAL_Y - BUTTON_START_Y) % SINGLE_BUTTON_HEIGHT; + + if (!(TERMINAL_X >= BUTTON_START_X && TERMINAL_Y >= BUTTON_START_Y && TERMINAL_X < BUTTON_END_X && TERMINAL_Y < BUTTON_END_Y) || rest > BUTTON_HEIGHT) { + if (selectedIndex != -1) { + selectedIndex = -1; + renderButtons(); + refresh(); + } + return; + } + + switch (event.getActionType()) { + case MOVE -> { + selectedIndex = index; + renderButtons(); + refresh(); + } + case CLICK_RELEASE -> { + switch (index) { + case 0 -> eventManager.emitEvent(new SendSocketMessageEvent(new CreateGame())); + case 1 -> { + Screen screen = new ConnectWorld(); + gameState.setScreen(screen); + screen.fullRender(); + } + } + } + } + } + + private void refresh() { + try { + terminalState.getTerminalScreen().refresh(com.googlecode.lanterna.screen.Screen.RefreshType.DELTA); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void handleKeyboardAction(KeyboardPressEvent event) { + + } + } + + + private static final class ConnectWorld extends Screen { + private final StringBuilder passBuffer = new StringBuilder(); + private boolean connecting = false; + + @InjectDependency + private EventManager eventManager; + + @InjectDependency + private Client client; + + @InjectState + private TerminalState terminalState; + + @InjectDependency + private TextRenderer textRenderer; + + @InjectState + private GameState gameState; + + @InjectDependency + private DependencyManager dependencyManager; + + private void renderInput(boolean refresh) { + var tg = terminalState.getTextGraphics(); + TerminalScreen screen = terminalState.getTerminalScreen(); + Input input = new Input(passBuffer.toString(), 18, 100); + var inputBuffer = input.render(textRenderer); + TerminalSize termSize = screen.getTerminalSize(); + int renderPixelWidth = inputBuffer[0].length; + int renderPixelHeight = inputBuffer.length; + int renderCharWidth = renderPixelWidth; + int renderCharHeight = (renderPixelHeight + 1) / 2; + int startX = (termSize.getColumns() - renderCharWidth) / 2; + int startY = (termSize.getRows() - renderCharHeight) / 2; + + for (int y = 0; y < inputBuffer.length; y += 2) { + for (int x = 0; x < inputBuffer[y].length; x++) { + AlphaPixel bottomPixel; + AlphaPixel topPixel = inputBuffer[y][x]; + if (y + 1 < inputBuffer.length) { + bottomPixel = inputBuffer[y + 1][x]; + } else { + bottomPixel = new Empty(); + } + + int termX = startX + x; + int termY = startY + (y / 2); + + tg.setBackgroundColor(topPixel instanceof Empty ? TextColor.ANSI.BLACK : topPixel.getColor()); + tg.setForegroundColor(bottomPixel instanceof Empty ? TextColor.ANSI.BLACK : bottomPixel.getColor()); + tg.setCharacter(termX, termY, '▄'); + } + } + + if (refresh) { + try { + screen.refresh(com.googlecode.lanterna.screen.Screen.RefreshType.DELTA); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public void fullRender() { + TerminalScreen screen = terminalState.getTerminalScreen(); + var tg = terminalState.getTextGraphics(); + screen.clear(); + TerminalSize terminalSize = screen.getTerminalSize(); + + for (int y = 0; y < terminalSize.getRows(); y += 1) { + for (int x = 0; x < terminalSize.getColumns(); x++) { + tg.setBackgroundColor(TextColor.ANSI.BLACK); + tg.setForegroundColor(TextColor.ANSI.BLACK); + tg.setCharacter(x, y, '▄'); + } + } + + AlphaPixel[][] selectServer = textRenderer.renderText("Enter world password", terminalSize.getColumns(), 20, Color.WHITE, 15f, true); + + render(selectServer, 0, 10, tg); + + renderInput(false); + + try { + screen.refresh(com.googlecode.lanterna.screen.Screen.RefreshType.COMPLETE); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void handleKeyboardAction(KeyboardPressEvent event) { + if (connecting) { + return; + } + + if (event.getKeyStroke().getKeyType() == KeyType.Enter) { + connecting = true; + String pass = passBuffer.toString(); + + return; + } + + if (event.getKeyStroke().getKeyType() == KeyType.Backspace) { + if (passBuffer.isEmpty()) { + return; + } + + passBuffer.deleteCharAt(passBuffer.length() - 1); + renderInput(true); + return; + } + + if (event.getKeyStroke().getKeyType() == KeyType.Character && !event.getKeyStroke().isCtrlDown()) { + passBuffer.append(event.getKeyStroke().getCharacter()); + renderInput(true); + } + } + + @Override + public void handleMouseAction(MouseAction event) { + } + } + + private static void render(AlphaPixel[][] buffer, int padX, int padY, TextGraphics tg) { + for (int y = 0; y < buffer.length; y += 2) { + for (int x = 0; x < buffer[y].length; x++) { + AlphaPixel topPixel = buffer[y][x]; + + AlphaPixel bottomPixel; + if (y + 1 < buffer.length) { + bottomPixel = buffer[y + 1][x]; + } else { + bottomPixel = new Empty(); + } + + int termX = padX + x; + int termY = padY / 2 + y / 2; + + tg.setBackgroundColor(topPixel instanceof Empty ? TextColor.ANSI.BLACK : topPixel.getColor()); + tg.setForegroundColor(bottomPixel instanceof Empty ? TextColor.ANSI.BLACK : bottomPixel.getColor()); + tg.setCharacter(termX, termY, '▄'); + } + } + } } diff --git a/game/src/main/java/cz/jzitnik/client/ui/Inventory.java b/game/src/main/java/cz/jzitnik/client/ui/Inventory.java index ed2306c..8390187 100644 --- a/game/src/main/java/cz/jzitnik/client/ui/Inventory.java +++ b/game/src/main/java/cz/jzitnik/client/ui/Inventory.java @@ -18,6 +18,7 @@ import cz.jzitnik.client.game.ResourceManager; import cz.jzitnik.client.game.items.GameItem; import cz.jzitnik.client.game.items.types.InteractableItem; import cz.jzitnik.client.game.objects.DroppedItem; +import cz.jzitnik.client.ui.pixels.AlphaPixel; import cz.jzitnik.common.models.coordinates.RoomCords; import cz.jzitnik.client.states.ScreenBuffer; import cz.jzitnik.client.states.TerminalState; @@ -50,17 +51,17 @@ public class Inventory { private static final int INNER_BORDER_WIDTH = 1; private static final int ITEM_SIZE = 16; // Characters private static final int ITEM_PADDING = 2; // padding on each side - public static final Pixel BORDER_COLOR = + public static final AlphaPixel BORDER_COLOR = new ColoredPixel(new TextColor.RGB(41, 29, 19)); - public static final Pixel BACKGROUND_COLOR = + public static final AlphaPixel BACKGROUND_COLOR = new ColoredPixel(new TextColor.RGB(61, 45, 29)); - private static final Pixel BACKGROUND_COLOR_HOVERED = + public static final AlphaPixel BACKGROUND_COLOR_HOVERED = new ColoredPixel(new TextColor.RGB(77, 56, 36)); - private static final Pixel BACKGROUND_COLOR_SELECTED = + private static final AlphaPixel BACKGROUND_COLOR_SELECTED = new ColoredPixel(new TextColor.RGB(95, 62, 32)); - private static final Pixel BACKGROUND_COLOR_EQUIPPED = + private static final AlphaPixel BACKGROUND_COLOR_EQUIPPED = new ColoredPixel(new TextColor.RGB(50, 145, 150)); - private static final Pixel BACKGROUND_COLOR_EQUIPPED_SELECTED = + private static final AlphaPixel BACKGROUND_COLOR_EQUIPPED_SELECTED = new ColoredPixel(new TextColor.RGB(58, 170, 176)); private static final int ITEM_SLOT_SIZE = ITEM_SIZE + ITEM_PADDING * 2; public static final int INVENTORY_WIDTH = diff --git a/game/src/main/java/cz/jzitnik/client/ui/pixels/AlphaPixel.java b/game/src/main/java/cz/jzitnik/client/ui/pixels/AlphaPixel.java index b6819fb..a7e13f8 100644 --- a/game/src/main/java/cz/jzitnik/client/ui/pixels/AlphaPixel.java +++ b/game/src/main/java/cz/jzitnik/client/ui/pixels/AlphaPixel.java @@ -10,4 +10,8 @@ public sealed abstract class AlphaPixel extends Pixel permits Empty, ColoredPixe super(color); this.alpha = alpha; } + + public boolean isTransparent() { + return alpha == 0f; + } } diff --git a/game/src/main/java/cz/jzitnik/client/ui/utils/Button.java b/game/src/main/java/cz/jzitnik/client/ui/utils/Button.java new file mode 100644 index 0000000..c2876fb --- /dev/null +++ b/game/src/main/java/cz/jzitnik/client/ui/utils/Button.java @@ -0,0 +1,50 @@ +package cz.jzitnik.client.ui.utils; + +import cz.jzitnik.client.ui.pixels.AlphaPixel; +import cz.jzitnik.client.utils.TextRenderer; + +import java.awt.*; + +public record Button( + AlphaPixel borderColor, + AlphaPixel backgroundColor, + AlphaPixel backgroundSelectedColor, + Color textColor, + int height, + int width, + int borderWidth, + float fontSize, + TextRenderer textRenderer +) { + public AlphaPixel[][] render(String text, boolean selected) { + AlphaPixel[][] buf = new AlphaPixel[height][width]; + AlphaPixel[][] textBuf = textRenderer.renderTextSingleLine(text, width - borderWidth * 2, height - borderWidth * 2, textColor, fontSize, true, true); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (x < borderWidth || y < borderWidth || x >= width - borderWidth || y >= height - borderWidth) { + buf[y][x] = borderColor; + continue; + } + + if (x == borderWidth || y == borderWidth || x - 1 == width - borderWidth || y == height - borderWidth) { + buf[y][x] = selected ? backgroundSelectedColor : backgroundColor; + continue; + } + + int textActualX = x - borderWidth - 1; + int textActualY = y - borderWidth - 1; + AlphaPixel textPixel = textBuf[textActualY][textActualX]; + + if (!textPixel.isTransparent()) { + buf[y][x] = textPixel; + continue; + } + + buf[y][x] = selected ? backgroundSelectedColor : backgroundColor; + } + } + + return buf; + } +} diff --git a/server/src/main/java/cz/jzitnik/server/context/GlobalContext.java b/server/src/main/java/cz/jzitnik/server/context/GlobalContext.java index 52519d2..d4cd1fb 100644 --- a/server/src/main/java/cz/jzitnik/server/context/GlobalContext.java +++ b/server/src/main/java/cz/jzitnik/server/context/GlobalContext.java @@ -2,23 +2,52 @@ package cz.jzitnik.server.context; import cz.jzitnik.server.game.Client; import cz.jzitnik.server.events.EventManager; -import cz.jzitnik.server.socket.SocketSession; +import cz.jzitnik.server.game.Game; import jakarta.websocket.Session; import lombok.Getter; import lombok.Setter; +import java.io.IOException; +import java.io.InputStream; import java.util.HashMap; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; public class GlobalContext { @Getter private final HashMap sessions = new HashMap<>(); + @Getter + private final Set games = new HashSet<>(); + @Getter @Setter private EventManager eventManager; - public void registerClient(Session session) { - Client client = new Client(new SocketSession(session)); - sessions.put(session, client); + @Getter + private final Properties properties; + + public void registerClient(Client client) { + sessions.put(client.getSession().getSession(), client); + } + + public GlobalContext() { + Properties props = new Properties(); + + try (InputStream input = GlobalContext.class + .getClassLoader() + .getResourceAsStream("config.properties")) { + + if (input == null) { + throw new RuntimeException("config.properties not found"); + } + + props.load(input); + + this.properties = props; + } catch (IOException e) { + throw new RuntimeException(e); + } } } diff --git a/server/src/main/java/cz/jzitnik/server/events/handlers/CreateGameHandler.java b/server/src/main/java/cz/jzitnik/server/events/handlers/CreateGameHandler.java index c23b3b7..410344f 100644 --- a/server/src/main/java/cz/jzitnik/server/events/handlers/CreateGameHandler.java +++ b/server/src/main/java/cz/jzitnik/server/events/handlers/CreateGameHandler.java @@ -1,5 +1,6 @@ package cz.jzitnik.server.events.handlers; +import cz.jzitnik.common.Config; import cz.jzitnik.common.models.player.PlayerCreation; import cz.jzitnik.common.socket.messages.game.creation.CreateGame; import cz.jzitnik.common.socket.messages.game.creation.CreateGameResponse; @@ -7,11 +8,17 @@ import cz.jzitnik.server.annotations.EventHandler; import cz.jzitnik.server.context.GlobalContext; import cz.jzitnik.server.events.AbstractEventHandler; import cz.jzitnik.server.game.Client; +import cz.jzitnik.server.game.Game; +import cz.jzitnik.server.game.Player; +import cz.jzitnik.server.utils.PasswordGenerator; import lombok.RequiredArgsConstructor; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.ObjectReader; import tools.jackson.dataformat.yaml.YAMLFactory; +import java.util.ArrayList; +import java.util.List; + @RequiredArgsConstructor @EventHandler(CreateGame.class) public class CreateGameHandler extends AbstractEventHandler { @@ -20,13 +27,24 @@ public class CreateGameHandler extends AbstractEventHandler { @Override public void handle(CreateGame event, Client client) { ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); - String pass = "nevim"; // TODO: Generate + String pass = PasswordGenerator.generatePassword(Config.WORLD_PASSWORD_LENGTH); // TODO: Generate + int id = 0; // Owners id is always 0 ObjectReader playerReader = objectMapper.readerFor(PlayerCreation.class); PlayerCreation player = playerReader.readValue(getClass().getClassLoader().getResourceAsStream("setup/player.yaml")); + player.setId(id); + + client.setPlayer(new Player(id, player.getPlayerCords())); CreateGameResponse gameResponse = new CreateGameResponse(pass, player); + Game game = new Game( + pass, + new ArrayList<>(List.of(client)) + ); + client.setGame(game); + client.getPlayer().setCurrentRoom(globalContext.getProperties().getProperty("rooms.default")); + globalContext.getGames().add(game); - client.session().sendMessage(gameResponse); + client.getSession().sendMessage(gameResponse); } } diff --git a/server/src/main/java/cz/jzitnik/server/events/handlers/PlayerMoveHandler.java b/server/src/main/java/cz/jzitnik/server/events/handlers/PlayerMoveHandler.java new file mode 100644 index 0000000..67a74d1 --- /dev/null +++ b/server/src/main/java/cz/jzitnik/server/events/handlers/PlayerMoveHandler.java @@ -0,0 +1,26 @@ +package cz.jzitnik.server.events.handlers; + +import cz.jzitnik.common.socket.messages.player.PlayerMove; +import cz.jzitnik.common.socket.messages.player.PlayerMovedInUrRoom; +import cz.jzitnik.server.annotations.EventHandler; +import cz.jzitnik.server.context.GlobalContext; +import cz.jzitnik.server.events.AbstractEventHandler; +import cz.jzitnik.server.game.Client; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@EventHandler(PlayerMove.class) +public class PlayerMoveHandler extends AbstractEventHandler { + private final GlobalContext globalContext; + + @Override + public void handle(PlayerMove event, Client client) { + client.getPlayer().getCords().updateCords(event.newCords()); + + for (Client player : client.getGame().getPlayers()) { + if (player.getPlayer().getCurrentRoom().equals(client.getPlayer().getCurrentRoom()) && player.getPlayer().getId() != client.getPlayer().getId()) { + player.getSession().sendMessage(new PlayerMovedInUrRoom(client.getPlayer().getId(), event.newCords())); + } + } + } +} diff --git a/server/src/main/java/cz/jzitnik/server/game/Client.java b/server/src/main/java/cz/jzitnik/server/game/Client.java index 0ddae7f..32394d4 100644 --- a/server/src/main/java/cz/jzitnik/server/game/Client.java +++ b/server/src/main/java/cz/jzitnik/server/game/Client.java @@ -1,5 +1,25 @@ package cz.jzitnik.server.game; import cz.jzitnik.server.socket.SocketSession; +import lombok.Getter; +import lombok.Setter; -public record Client(SocketSession session) {} \ No newline at end of file +@Getter +public final class Client { + private final SocketSession session; + @Setter + private Player player; + @Setter + private Game game; + + public Client(SocketSession session, Player player, Game game) { + this.session = session; + this.player = player; + this.game = game; + } + + public Client(SocketSession session, Player player) { + this(session, player, null); + } + +} \ No newline at end of file diff --git a/server/src/main/java/cz/jzitnik/server/game/Game.java b/server/src/main/java/cz/jzitnik/server/game/Game.java index 42011b3..27a421f 100644 --- a/server/src/main/java/cz/jzitnik/server/game/Game.java +++ b/server/src/main/java/cz/jzitnik/server/game/Game.java @@ -1,4 +1,13 @@ package cz.jzitnik.server.game; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor public class Game { + private String password; + private List players; } diff --git a/server/src/main/java/cz/jzitnik/server/game/Player.java b/server/src/main/java/cz/jzitnik/server/game/Player.java new file mode 100644 index 0000000..8fd519d --- /dev/null +++ b/server/src/main/java/cz/jzitnik/server/game/Player.java @@ -0,0 +1,15 @@ +package cz.jzitnik.server.game; + +import cz.jzitnik.common.models.coordinates.RoomCords; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +@Getter +@RequiredArgsConstructor +public class Player { + private final int id; + private final RoomCords cords; + @Setter + private String currentRoom; +} diff --git a/server/src/main/java/cz/jzitnik/server/socket/WebSocket.java b/server/src/main/java/cz/jzitnik/server/socket/WebSocket.java index 623bc86..e43368d 100644 --- a/server/src/main/java/cz/jzitnik/server/socket/WebSocket.java +++ b/server/src/main/java/cz/jzitnik/server/socket/WebSocket.java @@ -18,7 +18,8 @@ public class WebSocket { @OnOpen public void onOpen(Session session, EndpointConfig config) { this.globalContext = (GlobalContext) config.getUserProperties().get("globalContext"); - globalContext.registerClient(session); + + globalContext.registerClient(new Client(new SocketSession(session), null)); log.debug("Client connected: {}", session.getId()); } diff --git a/server/src/main/java/cz/jzitnik/server/utils/PasswordGenerator.java b/server/src/main/java/cz/jzitnik/server/utils/PasswordGenerator.java new file mode 100644 index 0000000..2f4423f --- /dev/null +++ b/server/src/main/java/cz/jzitnik/server/utils/PasswordGenerator.java @@ -0,0 +1,23 @@ +package cz.jzitnik.server.utils; + +import java.security.SecureRandom; + +public class PasswordGenerator { + private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + private static final SecureRandom RANDOM = new SecureRandom(); + + public static String generatePassword(int length) { + if (length <= 0) { + throw new IllegalArgumentException("Password length must be greater than 0"); + } + + StringBuilder password = new StringBuilder(length); + + for (int i = 0; i < length; i++) { + int index = RANDOM.nextInt(CHARACTERS.length()); + password.append(CHARACTERS.charAt(index)); + } + + return password.toString(); + } +} diff --git a/server/src/main/resources/config.properties b/server/src/main/resources/config.properties new file mode 100644 index 0000000..503483b --- /dev/null +++ b/server/src/main/resources/config.properties @@ -0,0 +1 @@ +rooms.default=spawn \ No newline at end of file