feat: Multiplayer #2

Merged
jzitnik merged 14 commits from multiplayer into main 2026-02-04 10:33:25 +00:00
24 changed files with 572 additions and 57 deletions
Showing only changes of commit 92eb93815d - Show all commits

3
.idea/compiler.xml generated
View File

@@ -14,7 +14,6 @@
<outputRelativeToContentRoot value="true" /> <outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false"> <processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.42/lombok-1.18.42.jar" /> <entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.42/lombok-1.18.42.jar" />
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.42/lombok-1.18.42.jar" />
</processorPath> </processorPath>
<module name="common" /> <module name="common" />
</profile> </profile>
@@ -24,7 +23,6 @@
<outputRelativeToContentRoot value="true" /> <outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false"> <processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.42/lombok-1.18.42.jar" /> <entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.42/lombok-1.18.42.jar" />
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.42/lombok-1.18.42.jar" />
</processorPath> </processorPath>
<module name="server" /> <module name="server" />
</profile> </profile>
@@ -34,7 +32,6 @@
<outputRelativeToContentRoot value="true" /> <outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false"> <processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.38/lombok-1.18.38.jar" /> <entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.38/lombok-1.18.38.jar" />
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.38/lombok-1.18.38.jar" />
</processorPath> </processorPath>
<module name="game (1)" /> <module name="game (1)" />
</profile> </profile>

2
.idea/misc.xml generated
View File

@@ -8,5 +8,5 @@
</list> </list>
</option> </option>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK" /> <component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="openjdk-25" project-jdk-type="JavaSDK" />
</project> </project>

View File

@@ -0,0 +1,5 @@
package cz.jzitnik.common;
public class Config {
public static final int WORLD_PASSWORD_LENGTH = 5;
}

View File

@@ -29,6 +29,10 @@ public class RoomCords implements Cloneable, Serializable {
this.y = y; this.y = y;
} }
public void updateCords(RoomCords roomCords) {
updateCords(roomCords.getX(), roomCords.getY());
}
public void updateCordsWithColliders(List<RoomPart> colliders, int x, int y, RoomPart playerCollider) { public void updateCordsWithColliders(List<RoomPart> colliders, int x, int y, RoomPart playerCollider) {
var normalizedPlayerCollider = new RoomPart( var normalizedPlayerCollider = new RoomPart(
new RoomCords(playerCollider.getStart().getX() + x, playerCollider.getStart().getY() + y), new RoomCords(playerCollider.getStart().getX() + x, playerCollider.getStart().getY() + y),

View File

@@ -5,11 +5,14 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import cz.jzitnik.common.models.coordinates.RoomCords; import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.common.models.coordinates.RoomPart; import cz.jzitnik.common.models.coordinates.RoomPart;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
import java.io.Serializable; import java.io.Serializable;
@Getter @Getter
public final class PlayerCreation implements Serializable { public final class PlayerCreation implements Serializable {
@Setter
private int id;
private final RoomCords playerCords; private final RoomCords playerCords;
private final RoomPart collider; private final RoomPart collider;

View File

@@ -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 {
}

View File

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

View File

@@ -8,10 +8,7 @@ import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState; import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.config.Debugging; import cz.jzitnik.client.config.Debugging;
import cz.jzitnik.client.config.PlayerConfig; import cz.jzitnik.client.config.PlayerConfig;
import cz.jzitnik.client.events.MouseMoveEvent; import cz.jzitnik.client.events.*;
import cz.jzitnik.client.events.PlayerMoveEvent;
import cz.jzitnik.client.events.RerenderScreen;
import cz.jzitnik.client.events.RoomChangeEvent;
import cz.jzitnik.client.game.GameRoom; import cz.jzitnik.client.game.GameRoom;
import cz.jzitnik.client.game.GameState; import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.Player; 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.AbstractEventHandler;
import cz.jzitnik.client.utils.events.Event; import cz.jzitnik.client.utils.events.Event;
import cz.jzitnik.client.utils.events.EventManager; import cz.jzitnik.client.utils.events.EventManager;
import cz.jzitnik.common.socket.messages.player.PlayerMove;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
@@ -146,6 +144,7 @@ public class PlayerMoveEventHandler extends AbstractEventHandler<PlayerMoveEvent
RerenderUtils.rerenderPart(forStartX, forEndX, forStartY, forEndY, startX, startY, currentRoom, room, player, playerTexture, screenBuffer, resourceManager, debugging); RerenderUtils.rerenderPart(forStartX, forEndX, forStartY, forEndY, startX, startY, currentRoom, room, player, playerTexture, screenBuffer, resourceManager, debugging);
eventManager.emitEvent(new Event[]{ eventManager.emitEvent(new Event[]{
new SendSocketMessageEvent(new PlayerMove(playerCords)),
new MouseMoveEvent(null), new MouseMoveEvent(null),
new RerenderScreen( new RerenderScreen(
new RerenderScreen.ScreenPart[]{ new RerenderScreen.ScreenPart[]{

View File

@@ -8,6 +8,9 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.Setter; import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@RequiredArgsConstructor @RequiredArgsConstructor
@State @State
public class GameState { public class GameState {
@@ -21,6 +24,9 @@ public class GameState {
@Setter @Setter
private Player player; private Player player;
@Getter
private final List<OtherPlayer> otherPlayers = new ArrayList<>();
@Getter @Getter
@Setter @Setter
private Interactable interacting; private Interactable interacting;

View File

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

View File

@@ -1,7 +1,5 @@
package cz.jzitnik.client.game; 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.events.RerenderPart;
import cz.jzitnik.client.game.items.GameItem; import cz.jzitnik.client.game.items.GameItem;
import cz.jzitnik.client.game.items.types.interfaces.WeaponInterface; import cz.jzitnik.client.game.items.types.interfaces.WeaponInterface;
@@ -26,6 +24,7 @@ import java.util.concurrent.TimeUnit;
@Getter @Getter
@Slf4j @Slf4j
public class Player { public class Player {
private final int id;
public static final int MAX_STAMINA = 20; public static final int MAX_STAMINA = 20;
public static final int MAX_HEALTH = 30; public static final int MAX_HEALTH = 30;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
@@ -43,18 +42,10 @@ public class Player {
private boolean hitAnimationOn = false; private boolean hitAnimationOn = false;
private ScheduledFuture<?> currentTimeoutHitAnimation = null; 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) { public Player(PlayerCreation playerCreation) {
this.playerCords = playerCreation.getPlayerCords(); this.playerCords = playerCreation.getPlayerCords();
this.collider = playerCreation.getCollider(); this.collider = playerCreation.getCollider();
this.id = playerCreation.getId();
} }
public void increaseStamina() { public void increaseStamina() {

View File

@@ -2,6 +2,7 @@ package cz.jzitnik.client.game.setup.scenes.connect;
import com.googlecode.lanterna.TerminalSize; import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.TextColor; import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.graphics.TextGraphics;
import com.googlecode.lanterna.input.KeyType; import com.googlecode.lanterna.input.KeyType;
import com.googlecode.lanterna.screen.TerminalScreen; import com.googlecode.lanterna.screen.TerminalScreen;
import cz.jzitnik.client.annotations.injectors.InjectDependency; 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.KeyboardPressEvent;
import cz.jzitnik.client.events.MouseAction; import cz.jzitnik.client.events.MouseAction;
import cz.jzitnik.client.events.SendSocketMessageEvent; import cz.jzitnik.client.events.SendSocketMessageEvent;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.screens.Screen; import cz.jzitnik.client.screens.Screen;
import cz.jzitnik.client.screens.scenes.Scene; import cz.jzitnik.client.screens.scenes.Scene;
import cz.jzitnik.client.socket.Client; import cz.jzitnik.client.socket.Client;
import cz.jzitnik.client.sound.SoundPlayer; import cz.jzitnik.client.sound.SoundPlayer;
import cz.jzitnik.client.states.TerminalState; 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.AlphaPixel;
import cz.jzitnik.client.ui.pixels.Empty; import cz.jzitnik.client.ui.pixels.Empty;
import cz.jzitnik.client.ui.utils.Input; 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.client.utils.events.EventManager;
import cz.jzitnik.common.socket.messages.game.creation.CreateGame; import cz.jzitnik.common.socket.messages.game.creation.CreateGame;
import jakarta.websocket.DeploymentException; import jakarta.websocket.DeploymentException;
import cz.jzitnik.client.ui.utils.Button;
import lombok.extern.slf4j.Slf4j;
import java.awt.*; import java.awt.*;
import java.io.IOException; import java.io.IOException;
@Slf4j
public class ServerChoose extends Scene { public class ServerChoose extends Scene {
public ServerChoose(DependencyManager dependencyManager) { public ServerChoose(DependencyManager dependencyManager) {
GameMenuAudioScreen gameMenuScreen = new GameMenuAudioScreen(); GameMenuAudioScreen gameMenuScreen = new GameMenuAudioScreen();
@@ -78,6 +84,12 @@ public class ServerChoose extends Scene {
@InjectDependency @InjectDependency
private TextRenderer textRenderer; private TextRenderer textRenderer;
@InjectState
private GameState gameState;
@InjectDependency
private DependencyManager dependencyManager;
private void renderInput(boolean refresh) { private void renderInput(boolean refresh) {
var tg = terminalState.getTextGraphics(); var tg = terminalState.getTextGraphics();
TerminalScreen screen = terminalState.getTerminalScreen(); 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); AlphaPixel[][] selectServer = textRenderer.renderText("Enter server IP", terminalSize.getColumns(), 20, Color.WHITE, 15f, true);
int padY = 10; render(selectServer, 0, 10, tg);
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, '▄');
}
}
renderInput(false); renderInput(false);
try { try {
screen.refresh(com.googlecode.lanterna.screen.Screen.RefreshType.DELTA); screen.refresh(com.googlecode.lanterna.screen.Screen.RefreshType.COMPLETE);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@@ -178,7 +170,10 @@ public class ServerChoose extends Scene {
connecting = true; connecting = true;
client.connect(ipBuffer.toString()); 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) { } catch (DeploymentException | IOException e) {
connecting = false; connecting = false;
} }
@@ -195,7 +190,7 @@ public class ServerChoose extends Scene {
return; return;
} }
if (event.getKeyStroke().getKeyType() == KeyType.Character) { if (event.getKeyStroke().getKeyType() == KeyType.Character && !event.getKeyStroke().isCtrlDown()) {
ipBuffer.append(event.getKeyStroke().getCharacter()); ipBuffer.append(event.getKeyStroke().getCharacter());
renderInput(true); renderInput(true);
} }
@@ -205,4 +200,282 @@ public class ServerChoose extends Scene {
public void handleMouseAction(MouseAction event) { 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, '▄');
}
}
}
} }

View File

@@ -18,6 +18,7 @@ import cz.jzitnik.client.game.ResourceManager;
import cz.jzitnik.client.game.items.GameItem; import cz.jzitnik.client.game.items.GameItem;
import cz.jzitnik.client.game.items.types.InteractableItem; import cz.jzitnik.client.game.items.types.InteractableItem;
import cz.jzitnik.client.game.objects.DroppedItem; import cz.jzitnik.client.game.objects.DroppedItem;
import cz.jzitnik.client.ui.pixels.AlphaPixel;
import cz.jzitnik.common.models.coordinates.RoomCords; import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.states.ScreenBuffer; import cz.jzitnik.client.states.ScreenBuffer;
import cz.jzitnik.client.states.TerminalState; 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 INNER_BORDER_WIDTH = 1;
private static final int ITEM_SIZE = 16; // Characters private static final int ITEM_SIZE = 16; // Characters
private static final int ITEM_PADDING = 2; // padding on each side 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)); 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)); 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)); 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)); 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)); 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)); new ColoredPixel(new TextColor.RGB(58, 170, 176));
private static final int ITEM_SLOT_SIZE = ITEM_SIZE + ITEM_PADDING * 2; private static final int ITEM_SLOT_SIZE = ITEM_SIZE + ITEM_PADDING * 2;
public static final int INVENTORY_WIDTH = public static final int INVENTORY_WIDTH =

View File

@@ -10,4 +10,8 @@ public sealed abstract class AlphaPixel extends Pixel permits Empty, ColoredPixe
super(color); super(color);
this.alpha = alpha; this.alpha = alpha;
} }
public boolean isTransparent() {
return alpha == 0f;
}
} }

View File

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

View File

@@ -2,23 +2,52 @@ package cz.jzitnik.server.context;
import cz.jzitnik.server.game.Client; import cz.jzitnik.server.game.Client;
import cz.jzitnik.server.events.EventManager; import cz.jzitnik.server.events.EventManager;
import cz.jzitnik.server.socket.SocketSession; import cz.jzitnik.server.game.Game;
import jakarta.websocket.Session; import jakarta.websocket.Session;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
public class GlobalContext { public class GlobalContext {
@Getter @Getter
private final HashMap<Session, Client> sessions = new HashMap<>(); private final HashMap<Session, Client> sessions = new HashMap<>();
@Getter
private final Set<Game> games = new HashSet<>();
@Getter @Getter
@Setter @Setter
private EventManager eventManager; private EventManager eventManager;
public void registerClient(Session session) { @Getter
Client client = new Client(new SocketSession(session)); private final Properties properties;
sessions.put(session, client);
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);
}
} }
} }

View File

@@ -1,5 +1,6 @@
package cz.jzitnik.server.events.handlers; package cz.jzitnik.server.events.handlers;
import cz.jzitnik.common.Config;
import cz.jzitnik.common.models.player.PlayerCreation; import cz.jzitnik.common.models.player.PlayerCreation;
import cz.jzitnik.common.socket.messages.game.creation.CreateGame; import cz.jzitnik.common.socket.messages.game.creation.CreateGame;
import cz.jzitnik.common.socket.messages.game.creation.CreateGameResponse; 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.context.GlobalContext;
import cz.jzitnik.server.events.AbstractEventHandler; import cz.jzitnik.server.events.AbstractEventHandler;
import cz.jzitnik.server.game.Client; 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 lombok.RequiredArgsConstructor;
import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectReader; import tools.jackson.databind.ObjectReader;
import tools.jackson.dataformat.yaml.YAMLFactory; import tools.jackson.dataformat.yaml.YAMLFactory;
import java.util.ArrayList;
import java.util.List;
@RequiredArgsConstructor @RequiredArgsConstructor
@EventHandler(CreateGame.class) @EventHandler(CreateGame.class)
public class CreateGameHandler extends AbstractEventHandler<CreateGame> { public class CreateGameHandler extends AbstractEventHandler<CreateGame> {
@@ -20,13 +27,24 @@ public class CreateGameHandler extends AbstractEventHandler<CreateGame> {
@Override @Override
public void handle(CreateGame event, Client client) { public void handle(CreateGame event, Client client) {
ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); 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); ObjectReader playerReader = objectMapper.readerFor(PlayerCreation.class);
PlayerCreation player = playerReader.readValue(getClass().getClassLoader().getResourceAsStream("setup/player.yaml")); 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); 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);
} }
} }

View File

@@ -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<PlayerMove> {
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()));
}
}
}
}

View File

@@ -1,5 +1,25 @@
package cz.jzitnik.server.game; package cz.jzitnik.server.game;
import cz.jzitnik.server.socket.SocketSession; import cz.jzitnik.server.socket.SocketSession;
import lombok.Getter;
import lombok.Setter;
public record Client(SocketSession session) {} @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);
}
}

View File

@@ -1,4 +1,13 @@
package cz.jzitnik.server.game; package cz.jzitnik.server.game;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
@Getter
@AllArgsConstructor
public class Game { public class Game {
private String password;
private List<Client> players;
} }

View File

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

View File

@@ -18,7 +18,8 @@ public class WebSocket {
@OnOpen @OnOpen
public void onOpen(Session session, EndpointConfig config) { public void onOpen(Session session, EndpointConfig config) {
this.globalContext = (GlobalContext) config.getUserProperties().get("globalContext"); this.globalContext = (GlobalContext) config.getUserProperties().get("globalContext");
globalContext.registerClient(session);
globalContext.registerClient(new Client(new SocketSession(session), null));
log.debug("Client connected: {}", session.getId()); log.debug("Client connected: {}", session.getId());
} }

View File

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

View File

@@ -0,0 +1 @@
rooms.default=spawn