Compare commits

..

8 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
f8f150cdf0 feat: Give item on dialog 2026-02-18 10:59:51 +01:00
8935349f92 feat: Answer requirements 2026-02-18 09:13:00 +01:00
25 changed files with 650 additions and 119 deletions

1
.idea/compiler.xml generated
View File

@@ -34,6 +34,7 @@
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.38/lombok-1.18.38.jar" />
</processorPath>
<module name="game (1)" />
<module name="game" />
</profile>
</annotationProcessing>
</component>

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

@@ -6,6 +6,7 @@ import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage;
import java.io.Serializable;
import java.util.List;
@@ -16,6 +17,13 @@ public class RoomCords implements Cloneable, Serializable {
private int x;
private int y;
public RoomCords calculateCenter(BufferedImage texture) {
return new RoomCords(
x + texture.getWidth() / 2,
y + texture.getHeight() / 2
);
}
@JsonCreator
public RoomCords(
@JsonProperty("x") int x,

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

@@ -7,6 +7,7 @@ import cz.jzitnik.client.annotations.EventHandler;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.RerenderScreen;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.dialog.Dialog;
import cz.jzitnik.client.game.dialog.OnEnd;
import cz.jzitnik.client.states.DialogState;
@@ -22,8 +23,8 @@ import cz.jzitnik.client.utils.events.EventManager;
import lombok.extern.slf4j.Slf4j;
import java.awt.*;
import java.util.List;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@EventHandler(Dialog.class)
@@ -38,6 +39,9 @@ public class DialogEventHandler extends AbstractEventHandler<Dialog> {
@InjectState
private ScreenBuffer screenBuffer;
@InjectState
private GameState gameState;
@InjectDependency
private EventManager eventManager;
@@ -56,32 +60,45 @@ public class DialogEventHandler extends AbstractEventHandler<Dialog> {
public static final int BUTTON_PADDING = 5;
private static final float FONT_SIZE = 15f;
public static int calculateButtonHeight(Dialog dialog) {
if (dialog.getOnEnd() instanceof OnEnd.AskQuestion(OnEnd.AskQuestion.Answer[] answers)) {
return answers.length * BUTTON_HEIGHT + (answers.length - 1) * BUTTON_PADDING;
} else {
return 0;
public static int calculateButtonHeight(Dialog dialog, GameState gameState) {
if (dialog.getOnEnd() instanceof OnEnd.AskQuestion askQuestion) {
int count = askQuestion.answers(gameState).length;
return count * BUTTON_HEIGHT + Math.max(0, count - 1) * BUTTON_PADDING;
}
return 0;
}
public static int getYStartButtons(TextRenderer textRenderer, Dialog dialog) {
var textSize = textRenderer.measureText(dialog.getText(), WIDTH - PADDING * 2, FONT_SIZE);
var textSize = textRenderer.measureText(
dialog.getText(),
WIDTH - PADDING * 2,
FONT_SIZE
);
return PADDING + textSize.height + BUTTON_PADDING;
}
public static TerminalSize getSize(TextRenderer textRenderer, Dialog dialog) {
var textSize = textRenderer.measureText(dialog.getText(), WIDTH - PADDING * 2, FONT_SIZE);
public static TerminalSize getSize(TextRenderer textRenderer, Dialog dialog, GameState gameState) {
var textSize = textRenderer.measureText(
dialog.getText(),
WIDTH - PADDING * 2,
FONT_SIZE
);
return new TerminalSize(300, PADDING + textSize.height + (
dialog.getOnEnd() instanceof OnEnd.AskQuestion ? BUTTON_PADDING + calculateButtonHeight(dialog) : 0
) + PADDING);
int buttonsHeight = 0;
if (dialog.getOnEnd() instanceof OnEnd.AskQuestion) {
buttonsHeight = BUTTON_PADDING + calculateButtonHeight(dialog, gameState);
}
return new TerminalSize(
300,
PADDING + textSize.height + buttonsHeight + PADDING
);
}
public static TerminalPosition getStart(TerminalSize terminalSize, TerminalSize size) {
int startY = terminalSize.getRows() * 2 - MARGIN_BOTTOM - size.getRows();
int startX = (terminalSize.getColumns() / 2) - (size.getColumns() / 2);
return new TerminalPosition(startX, startY);
}
@@ -89,58 +106,114 @@ public class DialogEventHandler extends AbstractEventHandler<Dialog> {
public void handle(Dialog event) {
boolean onlyLast = dialogState.getCurrentDialog() == event;
dialogState.setCurrentDialog(event);
TerminalSize terminalSize = terminalState.getTerminalScreen().getTerminalSize();
var overrideBuffer = screenBuffer.getGlobalOverrideBuffer();
var size = getSize(textRenderer, event);
var size = getSize(textRenderer, event, gameState);
var start = getStart(terminalSize, size);
var animation = textRenderer.renderTypingAnimation(event.getText(), size.getColumns() - PADDING * 2, size.getRows() - PADDING * 2, Color.WHITE, FONT_SIZE);
var textSize = textRenderer.measureText(event.getText(), size.getColumns() - PADDING * 2, FONT_SIZE);
var animation = textRenderer.renderTypingAnimation(
event.getText(),
size.getColumns() - PADDING * 2,
size.getRows() - PADDING * 2,
Color.WHITE,
FONT_SIZE
);
var textSize = textRenderer.measureText(
event.getText(),
size.getColumns() - PADDING * 2,
FONT_SIZE
);
OnEnd onEnd = event.getOnEnd();
List<AlphaPixel[][]> answersBuf = new ArrayList<>();
OnEnd.AskQuestion askQuestion = null;
if (onEnd instanceof OnEnd.AskQuestion(
OnEnd.AskQuestion.Answer[] answers
)) {
for (OnEnd.AskQuestion.Answer answer : answers) {
answersBuf.add(textRenderer.renderText(answer.answer(), size.getColumns() - PADDING * 2, BUTTON_HEIGHT, Color.BLACK, FONT_SIZE, false));
if (onEnd instanceof OnEnd.AskQuestion aq) {
askQuestion = aq;
for (OnEnd.AskQuestion.Answer answer : aq.answers(gameState)) {
answersBuf.add(
textRenderer.renderText(
answer.answer(),
size.getColumns() - PADDING * 2,
BUTTON_HEIGHT,
Color.BLACK,
FONT_SIZE,
false
)
);
}
}
dialogState.setRenderInProgress(true);
try {
for (int i = onlyLast ? animation.length : 0; i <= animation.length; i++) {
var buf = animation[Math.min(i, animation.length - 1)];
for (int y = 0; y < size.getRows(); y++) {
for (int x = 0; x < size.getColumns(); x++) {
var textPixel = buf[Math.min(Math.max(0, y - PADDING), buf.length - 1)][Math.min(Math.max(0, x - PADDING), buf[0].length - 1)];
if (textPixel instanceof Empty || y < PADDING || x < PADDING || x >= size.getColumns() - PADDING || y >= size.getRows() - PADDING) {
if (i == animation.length && y - 2 > textSize.height + QUESTION_ACTIONS_GAP && onEnd instanceof OnEnd.AskQuestion(
OnEnd.AskQuestion.Answer[] answers
)) {
var textPixel = buf[
Math.min(Math.max(0, y - PADDING), buf.length - 1)
][
Math.min(Math.max(0, x - PADDING), buf[0].length - 1)
];
if (textPixel instanceof Empty
|| y < PADDING
|| x < PADDING
|| x >= size.getColumns() - PADDING
|| y >= size.getRows() - PADDING) {
if (i == animation.length
&& askQuestion != null
&& y - 2 > textSize.height + QUESTION_ACTIONS_GAP) {
var answers = askQuestion.answers(gameState);
int buttonsY = y - textSize.height - QUESTION_ACTIONS_GAP - 2;
int buttonIndex = buttonsY / (BUTTON_HEIGHT + BUTTON_PADDING);
int rest = buttonsY % (BUTTON_HEIGHT + BUTTON_PADDING);
if (buttonIndex < answers.length && rest < BUTTON_HEIGHT && x >= PADDING && x < size.getColumns() - PADDING) {
if (buttonIndex < answers.length
&& rest < BUTTON_HEIGHT
&& x >= PADDING
&& x < size.getColumns() - PADDING) {
int localY = rest - BUTTON_TEXT_PADDING;
int localX = x - PADDING - BUTTON_TEXT_PADDING;
var buttonBuf = answersBuf.get(buttonIndex);
var buttonTextPixel = buttonBuf[Math.min(Math.max(0, localY), buttonBuf.length - 1)][Math.min(Math.max(0, localX), buttonBuf[0].length - 1)];
var buttonTextPixel = buttonBuf[
Math.min(Math.max(0, localY), buttonBuf.length - 1)
][
Math.min(Math.max(0, localX), buttonBuf[0].length - 1)
];
if (buttonTextPixel instanceof Empty || localY < 0 || localX < 0 || localY >= buttonBuf.length || localX >= buttonBuf[0].length) {
overrideBuffer[start.getRow() + y][start.getColumn() + x] = new ColoredPixel(new TextColor.RGB(255, 255, 255), dialogState.getHoveredButtonIndex() == buttonIndex ? 0.8f : 0.6f);
if (buttonTextPixel instanceof Empty
|| localY < 0
|| localX < 0
|| localY >= buttonBuf.length
|| localX >= buttonBuf[0].length) {
overrideBuffer[start.getRow() + y][start.getColumn() + x] =
new ColoredPixel(
new TextColor.RGB(255, 255, 255),
dialogState.getHoveredButtonIndex() == buttonIndex ? 0.8f : 0.6f
);
} else {
overrideBuffer[start.getRow() + y][start.getColumn() + x] = buttonTextPixel;
overrideBuffer[start.getRow() + y][start.getColumn() + x] =
buttonTextPixel;
}
continue;
}
}
overrideBuffer[start.getRow() + y][start.getColumn() + x] = new ColoredPixel(new TextColor.RGB(0, 0, 0), 0.6f);
overrideBuffer[start.getRow() + y][start.getColumn() + x] =
new ColoredPixel(new TextColor.RGB(0, 0, 0), 0.6f);
continue;
}
@@ -152,7 +225,10 @@ public class DialogEventHandler extends AbstractEventHandler<Dialog> {
new RerenderScreen(
new RerenderScreen.ScreenPart(
start,
new TerminalPosition(start.getColumn() + size.getColumns(), start.getRow() + size.getRows())
new TerminalPosition(
start.getColumn() + size.getColumns(),
start.getRow() + size.getRows()
)
)
)
);
@@ -161,7 +237,6 @@ public class DialogEventHandler extends AbstractEventHandler<Dialog> {
}
dialogState.setRenderInProgress(false);
next(onEnd, start, size);
} catch (InterruptedException e) {
@@ -171,34 +246,38 @@ public class DialogEventHandler extends AbstractEventHandler<Dialog> {
private void next(OnEnd onEnd, TerminalPosition start, TerminalSize size) throws InterruptedException {
Thread.sleep(1000);
if (onEnd instanceof OnEnd.Continue(Dialog nextDialog)) {
for (int y = start.getRow(); y < start.getRow() + size.getRows(); y++) {
for (int x = start.getColumn(); x < start.getColumn() + size.getColumns(); x++) {
screenBuffer.getGlobalOverrideBuffer()[y][x] = new Empty();
}
}
if (onEnd instanceof OnEnd.Continue(Dialog nextDialog)) {
clear(start, size);
eventManager.emitEvent(nextDialog);
} else if (onEnd instanceof OnEnd.RunCode(Runnable runnable, OnEnd end)) {
} else if (onEnd instanceof OnEnd.RunCode runCode) {
Runnable runnable = runCode.getRunnable();
dependencyManager.inject(runnable);
runnable.run();
next(end, start, size);
next(runCode.getOnEnd(), start, size);
} else if (onEnd instanceof OnEnd.End) {
for (int y = start.getRow(); y < start.getRow() + size.getRows(); y++) {
for (int x = start.getColumn(); x < start.getColumn() + size.getColumns(); x++) {
screenBuffer.getGlobalOverrideBuffer()[y][x] = new Empty();
}
}
clear(start, size);
dialogState.setCurrentDialog(null);
eventManager.emitEvent(
new RerenderScreen(
new RerenderScreen.ScreenPart(
start,
new TerminalPosition(start.getColumn() + size.getColumns(), start.getRow() + size.getRows())
new TerminalPosition(
start.getColumn() + size.getColumns(),
start.getRow() + size.getRows()
)
)
)
);
}
}
private void clear(TerminalPosition start, TerminalSize size) {
for (int y = start.getRow(); y < start.getRow() + size.getRows(); y++) {
for (int x = start.getColumn(); x < start.getColumn() + size.getColumns(); x++) {
screenBuffer.getGlobalOverrideBuffer()[y][x] = new Empty();
}
}
}
}

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.EventManager;
import cz.jzitnik.client.utils.roomtasks.RoomTaskScheduler;
import cz.jzitnik.common.socket.messages.game.GameWin;
import cz.jzitnik.common.socket.messages.room.MovePlayerRoom;
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)));
gameState.setCurrentRoom(newRoom);
if (newRoom.isEnd()) {
eventManager.emitEvent(new SendSocketMessageEvent(new GameWin()));
} else {
scheduler.schedule(() -> roomTaskScheduler.setupNewSchedulers(newRoom), 200, TimeUnit.MILLISECONDS);
}
}
}

View File

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

View File

@@ -28,7 +28,7 @@ public class Player implements GamePlayer {
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);
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final RoomCords playerCords;
private final RoomPart collider;
@@ -64,6 +64,8 @@ public class Player implements GamePlayer {
public boolean dealDamage(int amount, DependencyManager dependencyManager) {
if (health - amount <= 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;
}

View File

@@ -4,6 +4,21 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import cz.jzitnik.client.annotations.injectors.InjectDependency;
import cz.jzitnik.client.annotations.injectors.InjectState;
import cz.jzitnik.client.events.InventoryRerender;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.Player;
import cz.jzitnik.client.game.Requirement;
import cz.jzitnik.client.game.ResourceManager;
import cz.jzitnik.client.game.items.GameItem;
import cz.jzitnik.client.game.mobs.HittableMobDrops;
import cz.jzitnik.client.utils.events.EventManager;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Optional;
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
@@ -13,11 +28,56 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonSubTypes.Type(value = OnEnd.Continue.class, name = "continue"),
@JsonSubTypes.Type(value = OnEnd.AskQuestion.class, name = "ask_question"),
@JsonSubTypes.Type(value = OnEnd.End.class, name = "end"),
@JsonSubTypes.Type(value = OnEnd.GiveItem.class, name = "give_item")
})
public interface OnEnd {
record End() implements OnEnd {}
record End() implements OnEnd {
}
record RunCode(Runnable runnable, OnEnd onEnd) implements OnEnd {} // TODO: Serialize
class GiveItem extends RunCode {
@JsonCreator
public GiveItem(
@JsonProperty("item") GameItem item,
@JsonProperty("then") OnEnd onEnd
) {
super(new Run(item), onEnd);
}
@RequiredArgsConstructor
private static class Run implements Runnable {
private final GameItem item;
@InjectState
private GameState gameState;
@InjectDependency
private EventManager eventManager;
@InjectDependency
private ResourceManager resourceManager;
@Override
public void run() {
Player player = gameState.getPlayer();
var playerCords = player.getPlayerCords().calculateCenter(player.getTexture(resourceManager));
boolean addedIntoInventory = player.addItem(item);
if (!addedIntoInventory) {
eventManager.emitEvent(HittableMobDrops.dropItem(playerCords.getX(), playerCords.getY(), gameState.getCurrentRoom(), item));
} else {
eventManager.emitEvent(new InventoryRerender());
}
}
}
}
@Getter
@RequiredArgsConstructor
class RunCode implements OnEnd {
private final Runnable runnable;
private final OnEnd onEnd;
}
record Continue(Dialog nextDialog) implements OnEnd {
@JsonCreator
@@ -32,15 +92,44 @@ public interface OnEnd {
this.answers = answers;
}
public record Answer(String answer, Dialog dialog) {
public record Answer(
String answer,
Dialog dialog,
Optional<Requirement> requirement
) {
@JsonCreator
public Answer(
@JsonProperty("answer") String answer,
@JsonProperty("dialog") Dialog dialog
@JsonProperty("dialog") Dialog dialog,
@JsonProperty("requirement") Requirement requirement
) {
this.answer = answer;
this.dialog = dialog;
}
this(answer, dialog, Optional.ofNullable(requirement));
}
private boolean isValid(GameState gameState) {
if (requirement.isPresent()) {
Requirement requirement = requirement().get();
if (requirement.itemType() != null) {
return Arrays.stream(gameState.getPlayer().getInventory()).anyMatch(item -> {
if (item == null) {
return false;
}
return item.getType().getItemType().getSimpleName().equals(requirement.itemType());
});
}
return true;
}
return true;
}
}
public Answer[] answers(GameState gameState) {
return Arrays.stream(answers)
.filter(answer -> answer.isValid(gameState))
.toArray(Answer[]::new);
}
}
}

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.Type(value = Food.class, name = "food"),
@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> {
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

@@ -55,10 +55,9 @@ public class HittableMobDrops extends HittableMob {
Player player = gameState.getPlayer();
RoomCords enemyCords = getCords();
BufferedImage enemyTexture = getTexture();
GameRoom currentRoom = gameState.getCurrentRoom();
int roomX = enemyCords.getX() + enemyTexture.getWidth() / 2;
int roomY = enemyCords.getY() + enemyTexture.getHeight() / 2;
GameRoom currentRoom = gameState.getCurrentRoom();
List<Event> events = new ArrayList<>();
@@ -71,17 +70,21 @@ public class HittableMobDrops extends HittableMob {
events.add(new InventoryRerender());
}
} else {
double angle = ThreadLocalRandom.current().nextDouble(0, Math.PI * 2);
double radius = ThreadLocalRandom.current().nextDouble(0, DROP_ITEM_ON_GROUND_RADIUS);
int randomX = roomX + (int) (Math.cos(angle) * radius);
int randomY = roomY + (int) (Math.sin(angle) * radius);
RoomCords itemCords = new RoomCords(randomX, randomY);
DroppedItem droppedItem = new DroppedItem(currentRoom, itemCords, item);
currentRoom.getDroppedItems().add(droppedItem);
events.add(new DroppedItemRerender(droppedItem));
events.add(dropItem(roomX, roomY, currentRoom, item));
}
}
eventManager.emitEvent(events, this::afterKill);
}
public static Event dropItem(int x, int y, GameRoom currentRoom, GameItem item) {
double angle = ThreadLocalRandom.current().nextDouble(0, Math.PI * 2);
double radius = ThreadLocalRandom.current().nextDouble(0, DROP_ITEM_ON_GROUND_RADIUS);
int randomX = x + (int) (Math.cos(angle) * radius);
int randomY = y + (int) (Math.sin(angle) * radius);
RoomCords itemCords = new RoomCords(randomX, randomY);
DroppedItem droppedItem = new DroppedItem(currentRoom, itemCords, item);
currentRoom.getDroppedItems().add(droppedItem);
return new DroppedItemRerender(droppedItem);
}
}

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

@@ -11,6 +11,7 @@ import cz.jzitnik.client.annotations.ui.UI;
import cz.jzitnik.client.events.MouseAction;
import cz.jzitnik.client.events.RerenderScreen;
import cz.jzitnik.client.events.handlers.DialogEventHandler;
import cz.jzitnik.client.game.GameState;
import cz.jzitnik.client.game.dialog.OnEnd;
import cz.jzitnik.client.states.DialogState;
import cz.jzitnik.client.states.ScreenBuffer;
@@ -27,42 +28,57 @@ import static cz.jzitnik.client.events.handlers.DialogEventHandler.*;
@UI
@Dependency
public class DialogUI {
@InjectState
private DialogState dialogState;
@InjectState
private TerminalState terminalState;
@InjectState
private ScreenBuffer screenBuffer;
@InjectState
private GameState gameState;
@InjectDependency
private TextRenderer textRenderer;
@InjectDependency
private EventManager eventManager;
@InjectState
private ScreenBuffer screenBuffer;
@MouseHandler(MouseHandlerType.CLICK)
public boolean handleClick(MouseAction mouseAction) {
if (dialogState.getCurrentDialog() == null || dialogState.isRenderInProgress()) {
return false;
}
TerminalSize size = DialogEventHandler.getSize(textRenderer, dialogState.getCurrentDialog());
TerminalPosition start = DialogEventHandler.getStart(terminalState.getTerminalScreen().getTerminalSize(), size);
var dialog = dialogState.getCurrentDialog();
TerminalSize size = DialogEventHandler.getSize(textRenderer, dialog, gameState);
TerminalPosition start = DialogEventHandler.getStart(
terminalState.getTerminalScreen().getTerminalSize(),
size
);
if (!(dialogState.getCurrentDialog().getOnEnd() instanceof OnEnd.AskQuestion(
OnEnd.AskQuestion.Answer[] answers
))) {
if (!(dialog.getOnEnd() instanceof OnEnd.AskQuestion askQuestion)) {
setHoveredButtonIndex(-1);
return false;
}
var answers = askQuestion.answers(gameState);
TerminalPosition mouse = mouseAction.getPosition();
TerminalPosition mouseNormalized = new TerminalPosition(mouse.getColumn(), mouse.getRow() * 2);
TerminalPosition mouseNormalized = new TerminalPosition(
mouse.getColumn(),
mouse.getRow() * 2
);
RerenderScreen.ScreenPart part = new RerenderScreen.ScreenPart(
start,
new TerminalPosition(start.getColumn() + size.getColumns(), start.getRow() + size.getRows())
new TerminalPosition(
start.getColumn() + size.getColumns(),
start.getRow() + size.getRows()
)
);
if (!part.isWithin(mouseNormalized)) {
@@ -70,9 +86,13 @@ public class DialogUI {
return false;
}
int buttonsStartY = DialogEventHandler.getYStartButtons(textRenderer, dialogState.getCurrentDialog());
TerminalPosition localPosition = new TerminalPosition(mouseNormalized.getColumn() - start.getColumn(), mouseNormalized.getRow() - start.getRow() - buttonsStartY);
int buttonsHeight = DialogEventHandler.calculateButtonHeight(dialogState.getCurrentDialog());
int buttonsStartY = DialogEventHandler.getYStartButtons(textRenderer, dialog);
TerminalPosition localPosition = new TerminalPosition(
mouseNormalized.getColumn() - start.getColumn(),
mouseNormalized.getRow() - start.getRow() - buttonsStartY
);
int buttonsHeight = DialogEventHandler.calculateButtonHeight(dialog, gameState);
if (localPosition.getRow() < 0 || localPosition.getRow() >= buttonsHeight) {
setHoveredButtonIndex(-1);
@@ -82,23 +102,27 @@ public class DialogUI {
int buttonIndex = localPosition.getRow() / (BUTTON_HEIGHT + BUTTON_PADDING);
int rest = localPosition.getRow() % (BUTTON_HEIGHT + BUTTON_PADDING);
if (buttonIndex < answers.length && rest < BUTTON_HEIGHT && localPosition.getColumn() >= PADDING && localPosition.getColumn() < size.getColumns() - PADDING) {
for (int y = start.getRow(); y < start.getRow() + size.getRows(); y++) {
for (int x = start.getColumn(); x < start.getColumn() + size.getColumns(); x++) {
screenBuffer.getGlobalOverrideBuffer()[y][x] = new Empty();
}
}
if (buttonIndex < answers.length
&& rest < BUTTON_HEIGHT
&& localPosition.getColumn() >= PADDING
&& localPosition.getColumn() < size.getColumns() - PADDING) {
clearDialog(start, size);
eventManager.emitEvent(
new Event[]{
new RerenderScreen(
new RerenderScreen.ScreenPart(
start,
new TerminalPosition(start.getColumn() + size.getColumns(), start.getRow() + size.getRows())
new TerminalPosition(
start.getColumn() + size.getColumns(),
start.getRow() + size.getRows()
)
)
),
answers[buttonIndex].dialog(),
});
answers[buttonIndex].dialog()
}
);
return true;
}
@@ -112,21 +136,32 @@ public class DialogUI {
return false;
}
TerminalSize size = DialogEventHandler.getSize(textRenderer, dialogState.getCurrentDialog());
TerminalPosition start = DialogEventHandler.getStart(terminalState.getTerminalScreen().getTerminalSize(), size);
var dialog = dialogState.getCurrentDialog();
TerminalSize size = DialogEventHandler.getSize(textRenderer, dialog, gameState);
TerminalPosition start = DialogEventHandler.getStart(
terminalState.getTerminalScreen().getTerminalSize(),
size
);
if (!(dialogState.getCurrentDialog().getOnEnd() instanceof OnEnd.AskQuestion(
OnEnd.AskQuestion.Answer[] answers
))) {
if (!(dialog.getOnEnd() instanceof OnEnd.AskQuestion askQuestion)) {
setHoveredButtonIndex(-1);
return false;
}
var answers = askQuestion.answers(gameState);
TerminalPosition mouse = mouseAction.getPosition();
TerminalPosition mouseNormalized = new TerminalPosition(mouse.getColumn(), mouse.getRow() * 2);
TerminalPosition mouseNormalized = new TerminalPosition(
mouse.getColumn(),
mouse.getRow() * 2
);
RerenderScreen.ScreenPart part = new RerenderScreen.ScreenPart(
start,
new TerminalPosition(start.getColumn() + size.getColumns(), start.getRow() + size.getRows())
new TerminalPosition(
start.getColumn() + size.getColumns(),
start.getRow() + size.getRows()
)
);
if (!part.isWithin(mouseNormalized)) {
@@ -134,9 +169,13 @@ public class DialogUI {
return false;
}
int buttonsStartY = DialogEventHandler.getYStartButtons(textRenderer, dialogState.getCurrentDialog());
TerminalPosition localPosition = new TerminalPosition(mouseNormalized.getColumn() - start.getColumn(), mouseNormalized.getRow() - start.getRow() - buttonsStartY);
int buttonsHeight = DialogEventHandler.calculateButtonHeight(dialogState.getCurrentDialog());
int buttonsStartY = DialogEventHandler.getYStartButtons(textRenderer, dialog);
TerminalPosition localPosition = new TerminalPosition(
mouseNormalized.getColumn() - start.getColumn(),
mouseNormalized.getRow() - start.getRow() - buttonsStartY
);
int buttonsHeight = DialogEventHandler.calculateButtonHeight(dialog, gameState);
if (localPosition.getRow() < 0 || localPosition.getRow() >= buttonsHeight) {
setHoveredButtonIndex(-1);
@@ -146,7 +185,11 @@ public class DialogUI {
int buttonIndex = localPosition.getRow() / (BUTTON_HEIGHT + BUTTON_PADDING);
int rest = localPosition.getRow() % (BUTTON_HEIGHT + BUTTON_PADDING);
if (buttonIndex < answers.length && rest < BUTTON_HEIGHT && localPosition.getColumn() >= PADDING && localPosition.getColumn() < size.getColumns() - PADDING) {
if (buttonIndex < answers.length
&& rest < BUTTON_HEIGHT
&& localPosition.getColumn() >= PADDING
&& localPosition.getColumn() < size.getColumns() - PADDING) {
setHoveredButtonIndex(buttonIndex);
return true;
}
@@ -161,4 +204,12 @@ public class DialogUI {
eventManager.emitEvent(dialogState.getCurrentDialog());
}
}
private void clearDialog(TerminalPosition start, TerminalSize size) {
for (int y = start.getRow(); y < start.getRow() + size.getRows(); y++) {
for (int x = start.getColumn(); x < start.getColumn() + size.getColumns(); x++) {
screenBuffer.getGlobalOverrideBuffer()[y][x] = new Empty();
}
}
}
}

View File

@@ -64,7 +64,6 @@ public class EventManager extends Thread {
}
}
@SuppressWarnings("unchecked")
public EventManager(Reflections reflections, DependencyManager dependencyManager) {
this.dependencyManager = dependencyManager;
setDaemon(true);

View File

@@ -75,15 +75,17 @@
answers:
- answer: "I have it"
requirement:
item: "quest_item_boss_skin"
item: "BeastSkin"
dialog:
text: "Well done. Here is the key."
giveItem:
id: 300
name: "Cave Exit Key"
type: { name: "quest_item_final_key" }
texture: "KEY"
onEnd: { type: end }
onEnd:
type: "give_item"
item:
id: 800
name: "Key"
type: { name: "key" }
texture: "APPLE"
then: { type: end }
- answer: "Not yet"
dialog:
text: "Then go back before it finds you."
@@ -112,7 +114,7 @@
itemsDrops:
- id: 200
name: "Beast Skin"
type: { name: "junk" }
type: { name: "beast_skin" }
texture: "BOSS_SKIN"
tasks:
- type: "following_player"
@@ -135,10 +137,8 @@
- id: "final_room"
texture: "ROOM6"
requirement:
item: "quest_item_final_key"
#objects:
# - objectType: "exit"
# cords: { x: 140, y: 40 }
item: "Key"
end: true
west: null
east: null
north: null
@@ -169,7 +169,7 @@
updateRateMs: 700
west: "filler_2"
east: "spawn"
north: null
north: "empty_c"
south: "filler_deadend_1"
@@ -198,7 +198,7 @@
west: "filler_c_west"
east: null
north: "boss"
south: null
south: "empty_a"
@@ -241,7 +241,7 @@
- id: "filler_1b"
texture: "ROOM1"
west: "filler_7"
west: "filler_1"
east: null
north: 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()));
}
}
}