Compare commits

...

6 Commits

Author SHA1 Message Date
f2b6200355 docs: Add controls 2026-02-22 22:00:26 +01:00
e15d4ec874 chore: Late something 2026-02-22 21:46:12 +01:00
dac6d666b2 chore: Minor changes 2026-02-22 20:59:20 +01:00
3dd2c389b8 feat: Multiplayer 2026-02-22 20:37:05 +01:00
f7d878f430 feat: End 2026-02-22 20:24:31 +01:00
32f8521951 feat: Death 2026-02-21 13:46:27 +01:00
18 changed files with 312 additions and 13 deletions

37
README.md Normal file
View File

@@ -0,0 +1,37 @@
# Terminal Game
A multiplayer terminal-based game built with Java, utilizing WebSockets for communication and Lanterna for the text-based user interface.
## Project Structure
* **game**: Client application (TUI).
* **server**: WebSocket server.
* **common**: Shared libraries and logic.
## Requirements
* Java 25
* Maven
## How to Run
1. Build the project:
```bash
mvn clean install
```
2. Start the server:
```bash
mvn compile exec:java -pl server -am
```
3. Start the client (in a new terminal):
```bash
mvn compile exec:java -pl game -am
```
## Controls
* **Left Click**: Interact with objects and fight.
* **WASD**: Move the character.
* **CTRL**: Hold to run (sprint).

View File

@@ -0,0 +1,6 @@
package cz.jzitnik.common.socket.messages.game;
import cz.jzitnik.common.socket.SocketMessage;
public record GameWin() implements SocketMessage {
}

View File

@@ -0,0 +1,6 @@
package cz.jzitnik.common.socket.messages.game;
import cz.jzitnik.common.socket.SocketMessage;
public record PlayerDeath(int playerId) implements SocketMessage {
}

View File

@@ -12,6 +12,7 @@ import cz.jzitnik.common.models.coordinates.RoomCords;
import cz.jzitnik.client.utils.events.AbstractEventHandler; import cz.jzitnik.client.utils.events.AbstractEventHandler;
import cz.jzitnik.client.utils.events.EventManager; import cz.jzitnik.client.utils.events.EventManager;
import cz.jzitnik.client.utils.roomtasks.RoomTaskScheduler; import cz.jzitnik.client.utils.roomtasks.RoomTaskScheduler;
import cz.jzitnik.common.socket.messages.game.GameWin;
import cz.jzitnik.common.socket.messages.room.MovePlayerRoom; import cz.jzitnik.common.socket.messages.room.MovePlayerRoom;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -71,6 +72,10 @@ public class RoomChangeEventHandler extends AbstractEventHandler<RoomChangeEvent
eventManager.emitEvent(new SendSocketMessageEvent(new MovePlayerRoom(newRoom.getId(), oldCords, playerCords))); eventManager.emitEvent(new SendSocketMessageEvent(new MovePlayerRoom(newRoom.getId(), oldCords, playerCords)));
gameState.setCurrentRoom(newRoom); gameState.setCurrentRoom(newRoom);
if (newRoom.isEnd()) {
eventManager.emitEvent(new SendSocketMessageEvent(new GameWin()));
} else {
scheduler.schedule(() -> roomTaskScheduler.setupNewSchedulers(newRoom), 200, TimeUnit.MILLISECONDS); scheduler.schedule(() -> roomTaskScheduler.setupNewSchedulers(newRoom), 200, TimeUnit.MILLISECONDS);
} }
}
} }

View File

@@ -37,6 +37,9 @@ public class GameRoom {
@JsonProperty("requirement") @JsonProperty("requirement")
private Requirement requirement; private Requirement requirement;
@JsonProperty("end")
private boolean end;
private GameRoom left; private GameRoom left;
private GameRoom right; private GameRoom right;
private GameRoom up; private GameRoom up;

View File

@@ -64,6 +64,8 @@ public class Player implements GamePlayer {
public boolean dealDamage(int amount, DependencyManager dependencyManager) { public boolean dealDamage(int amount, DependencyManager dependencyManager) {
if (health - amount <= 0) { if (health - amount <= 0) {
health = 0; health = 0;
EventManager eventManager = dependencyManager.getDependencyOrThrow(EventManager.class);
eventManager.emitEvent(new cz.jzitnik.client.events.SendSocketMessageEvent(new cz.jzitnik.common.socket.messages.game.PlayerDeath(id)));
return true; return true;
} }

View File

@@ -0,0 +1,8 @@
package cz.jzitnik.client.game.items.types;
public class BeastSkin implements ItemType<BeastSkin> {
@Override
public Class<BeastSkin> getItemType() {
return BeastSkin.class;
}
}

View File

@@ -12,7 +12,9 @@ import cz.jzitnik.client.game.items.types.weapons.Sword;
@JsonSubTypes({ @JsonSubTypes({
@JsonSubTypes.Type(value = Food.class, name = "food"), @JsonSubTypes.Type(value = Food.class, name = "food"),
@JsonSubTypes.Type(value = Sword.class, name = "weapon_sword"), @JsonSubTypes.Type(value = Sword.class, name = "weapon_sword"),
@JsonSubTypes.Type(value = Junk.class, name = "junk") @JsonSubTypes.Type(value = Junk.class, name = "junk"),
@JsonSubTypes.Type(value = Key.class, name = "key"),
@JsonSubTypes.Type(value = BeastSkin.class, name = "beast_skin"),
}) })
public interface ItemType<T> { public interface ItemType<T> {
Class<T> getItemType(); Class<T> getItemType();

View File

@@ -0,0 +1,8 @@
package cz.jzitnik.client.game.items.types;
public class Key implements ItemType<Key> {
@Override
public Class<Key> getItemType() {
return Key.class;
}
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.client.game.setup.scenes;
import cz.jzitnik.client.screens.DeathScreen;
import cz.jzitnik.client.screens.Screen;
import cz.jzitnik.client.screens.scenes.Scene;
public class DeathScene extends Scene {
public DeathScene() {
super(new Screen[]{new DeathScreen()}, new OnEndAction.Repeat());
}
}

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.client.game.setup.scenes;
import cz.jzitnik.client.screens.WinScreen;
import cz.jzitnik.client.screens.Screen;
import cz.jzitnik.client.screens.scenes.Scene;
public class WinScene extends Scene {
public WinScene() {
super(new Screen[]{new WinScreen()}, new OnEndAction.Repeat());
}
}

View File

@@ -0,0 +1,46 @@
package cz.jzitnik.client.screens;
import com.googlecode.lanterna.SGR;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.graphics.TextGraphics;
import com.googlecode.lanterna.screen.TerminalScreen;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.KeyboardPressEvent;
import cz.jzitnik.client.events.MouseAction;
import cz.jzitnik.client.states.TerminalState;
import java.io.IOException;
public class DeathScreen extends Screen {
@InjectState
private TerminalState terminalState;
@Override
public void fullRender() {
TerminalScreen screen = terminalState.getTerminalScreen();
screen.clear();
TextGraphics tg = terminalState.getTextGraphics();
int termWidth = screen.getTerminalSize().getColumns();
int termHeight = screen.getTerminalSize().getRows();
String message = "GAME OVER";
tg.setForegroundColor(TextColor.ANSI.RED);
tg.enableModifiers(SGR.BOLD);
tg.putString((termWidth - message.length()) / 2, termHeight / 2, message);
try {
screen.refresh(com.googlecode.lanterna.screen.Screen.RefreshType.COMPLETE);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void handleMouseAction(MouseAction event) {
}
@Override
public void handleKeyboardAction(KeyboardPressEvent event) {
}
}

View File

@@ -0,0 +1,46 @@
package cz.jzitnik.client.screens;
import com.googlecode.lanterna.SGR;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.graphics.TextGraphics;
import com.googlecode.lanterna.screen.TerminalScreen;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.KeyboardPressEvent;
import cz.jzitnik.client.events.MouseAction;
import cz.jzitnik.client.states.TerminalState;
import java.io.IOException;
public class WinScreen extends Screen {
@InjectState
private TerminalState terminalState;
@Override
public void fullRender() {
TerminalScreen screen = terminalState.getTerminalScreen();
screen.clear();
TextGraphics tg = terminalState.getTextGraphics();
int termWidth = screen.getTerminalSize().getColumns();
int termHeight = screen.getTerminalSize().getRows();
String message = "YOU WON!";
tg.setForegroundColor(TextColor.ANSI.GREEN);
tg.enableModifiers(SGR.BOLD);
tg.putString((termWidth - message.length()) / 2, termHeight / 2, message);
try {
screen.refresh(com.googlecode.lanterna.screen.Screen.RefreshType.COMPLETE);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void handleMouseAction(MouseAction event) {
}
@Override
public void handleKeyboardAction(KeyboardPressEvent event) {
}
}

View File

@@ -0,0 +1,34 @@
package cz.jzitnik.client.socket.events;
import cz.jzitnik.client.annotations.SocketEventHandler;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.setup.scenes.WinScene;
import cz.jzitnik.client.socket.AbstractSocketEventHandler;
import cz.jzitnik.common.socket.messages.game.GameWin;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@SocketEventHandler(GameWin.class)
public class GameWinHandler extends AbstractSocketEventHandler<GameWin> {
@InjectState
private GameState gameState;
@InjectDependency
private cz.jzitnik.client.utils.roomtasks.RoomTaskScheduler roomTaskScheduler;
@Override
public void handle(GameWin event) {
log.debug("Game won!");
roomTaskScheduler.finalShutdown();
WinScene winScene = new WinScene();
gameState.setScreen(winScene);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
winScene.fullRender();
}
}

View File

@@ -0,0 +1,34 @@
package cz.jzitnik.client.socket.events;
import cz.jzitnik.client.annotations.SocketEventHandler;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.setup.scenes.DeathScene;
import cz.jzitnik.client.socket.AbstractSocketEventHandler;
import cz.jzitnik.client.utils.DependencyManager;
import cz.jzitnik.common.socket.messages.game.PlayerDeath;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@SocketEventHandler(PlayerDeath.class)
public class PlayerDeathHandler extends AbstractSocketEventHandler<PlayerDeath> {
@InjectState
private GameState gameState;
@InjectDependency
private DependencyManager dependencyManager;
@InjectDependency
private cz.jzitnik.client.utils.roomtasks.RoomTaskScheduler roomTaskScheduler;
@Override
public void handle(PlayerDeath event) {
log.debug("Player death: {}", event.playerId());
roomTaskScheduler.finalShutdown();
DeathScene deathScene = new DeathScene();
dependencyManager.inject(deathScene);
gameState.setScreen(deathScene);
deathScene.fullRender();
}
}

View File

@@ -75,15 +75,15 @@
answers: answers:
- answer: "I have it" - answer: "I have it"
requirement: requirement:
item: "quest_item_boss_skin" item: "BeastSkin"
dialog: dialog:
text: "Well done. Here is the key." text: "Well done. Here is the key."
onEnd: onEnd:
type: "give_item" type: "give_item"
item: item:
id: 800 id: 800
name: "Something" name: "Key"
type: { name: "junk" } type: { name: "key" }
texture: "APPLE" texture: "APPLE"
then: { type: end } then: { type: end }
- answer: "Not yet" - answer: "Not yet"
@@ -114,7 +114,7 @@
itemsDrops: itemsDrops:
- id: 200 - id: 200
name: "Beast Skin" name: "Beast Skin"
type: { name: "junk" } type: { name: "beast_skin" }
texture: "BOSS_SKIN" texture: "BOSS_SKIN"
tasks: tasks:
- type: "following_player" - type: "following_player"
@@ -137,10 +137,8 @@
- id: "final_room" - id: "final_room"
texture: "ROOM6" texture: "ROOM6"
requirement: requirement:
item: "quest_item_final_key" item: "Key"
#objects: end: true
# - objectType: "exit"
# cords: { x: 140, y: 40 }
west: null west: null
east: null east: null
north: null north: null
@@ -171,7 +169,7 @@
updateRateMs: 700 updateRateMs: 700
west: "filler_2" west: "filler_2"
east: "spawn" east: "spawn"
north: null north: "empty_c"
south: "filler_deadend_1" south: "filler_deadend_1"
@@ -200,7 +198,7 @@
west: "filler_c_west" west: "filler_c_west"
east: null east: null
north: "boss" north: "boss"
south: null south: "empty_a"
@@ -243,7 +241,7 @@
- id: "filler_1b" - id: "filler_1b"
texture: "ROOM1" texture: "ROOM1"
west: "filler_7" west: "filler_1"
east: null east: null
north: null north: null
south: null south: null

View File

@@ -0,0 +1,21 @@
package cz.jzitnik.server.events.handlers;
import cz.jzitnik.common.socket.messages.game.GameWin;
import cz.jzitnik.server.annotations.EventHandler;
import cz.jzitnik.server.context.GlobalContext;
import cz.jzitnik.server.events.AbstractEventHandler;
import cz.jzitnik.server.game.Client;
@EventHandler(GameWin.class)
public class GameWinHandler extends AbstractEventHandler<GameWin> {
public GameWinHandler(GlobalContext globalContext) {
super(globalContext);
}
@Override
public void handle(GameWin event, Client client) {
for (Client player : client.getGame().getPlayers()) {
player.getSession().sendMessage(new GameWin());
}
}
}

View File

@@ -0,0 +1,21 @@
package cz.jzitnik.server.events.handlers;
import cz.jzitnik.common.socket.messages.game.PlayerDeath;
import cz.jzitnik.server.annotations.EventHandler;
import cz.jzitnik.server.context.GlobalContext;
import cz.jzitnik.server.events.AbstractEventHandler;
import cz.jzitnik.server.game.Client;
@EventHandler(PlayerDeath.class)
public class PlayerDeathHandler extends AbstractEventHandler<PlayerDeath> {
public PlayerDeathHandler(GlobalContext globalContext) {
super(globalContext);
}
@Override
public void handle(PlayerDeath event, Client client) {
for (Client player : client.getGame().getPlayers()) {
player.getSession().sendMessage(new PlayerDeath(event.playerId()));
}
}
}