feat: Walking player

Player now can walk using wasd
This commit is contained in:
2025-12-15 20:11:47 +01:00
parent 2623cd1328
commit 3eab104777
27 changed files with 310 additions and 81 deletions

View File

@@ -1,5 +1,6 @@
package cz.jzitnik;
import cz.jzitnik.game.GameSetup;
import cz.jzitnik.utils.DependencyManager;
import org.reflections.Reflections;
@@ -8,6 +9,10 @@ public class Game {
public void start() {
Cli cli = dependencyManager.getDependencyOrThrow(Cli.class);
GameSetup gameSetup = dependencyManager.getDependencyOrThrow(GameSetup.class);
gameSetup.setup();
cli.run();
}
}

View File

@@ -1,11 +0,0 @@
package cz.jzitnik.config;
import cz.jzitnik.annotations.Config;
import lombok.Getter;
@Config
@Getter
public class RoomSizeConfig {
private final int roomHeight = 450;
private final int roomWidth = 450;
}

View File

@@ -1,6 +1,13 @@
package cz.jzitnik.events;
import cz.jzitnik.utils.events.Event;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class FullRoomDraw implements Event {
private boolean fullRerender = false;
}

View File

@@ -0,0 +1,12 @@
package cz.jzitnik.events;
import com.googlecode.lanterna.input.KeyStroke;
import cz.jzitnik.utils.events.Event;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public class PlayerMoveEvent implements Event {
private KeyStroke keyStroke;
}

View File

@@ -13,7 +13,6 @@ import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.events.AbstractEventHandler;
import lombok.extern.slf4j.Slf4j;
import java.awt.*;
import java.io.IOException;
@Slf4j
@@ -32,7 +31,7 @@ public class CliHandler extends AbstractEventHandler<RerenderScreen> {
@Override
public void handle(RerenderScreen event) {
var parts = event.parts();
var buffer = screenBuffer.getBuffer();
var buffer = screenBuffer.getRenderedBuffer();
var terminalScreen = terminalState.getTerminalScreen();
var tg = terminalState.getTextGraphics();

View File

@@ -9,8 +9,11 @@ import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.FullRoomDraw;
import cz.jzitnik.events.RerenderScreen;
import cz.jzitnik.game.GameRoom;
import cz.jzitnik.game.GameState;
import cz.jzitnik.game.Player;
import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.states.RenderState;
import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.states.TerminalState;
@@ -22,8 +25,7 @@ import cz.jzitnik.utils.events.EventManager;
import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
@Slf4j
@EventHandler(FullRoomDraw.class)
@@ -50,37 +52,50 @@ public class FullRoomDrawHandler extends AbstractEventHandler<FullRoomDraw> {
@InjectState
private RenderState renderState;
public enum DoorPosition {
TOP,
LEFT,
RIGHT,
BOTTOM,
}
@Override
public void handle(FullRoomDraw event) {
log.debug("Rendering full room");
TerminalScreen terminalScreen = terminalState.getTerminalScreen();
List<RerenderScreen.ScreenPart> partsToRerender = new ArrayList<>();
GameRoom currentRoom = gameState.getCurrentRoom();
var buffer = screenBuffer.getBuffer();
Set<DoorPosition> doorPositions = getDoorPositions(currentRoom);
BufferedImage room = resourceManager.getResource(ResourceManager.Resource.ROOM1);
var buffer = screenBuffer.getRenderedBuffer();
BufferedImage room = resourceManager.getResource(currentRoom.getTexture());
BufferedImage doors = resourceManager.getResource(ResourceManager.Resource.DOORS);
Player player = gameState.getPlayer();
BufferedImage playerTexture = getPlayer(resourceManager, player);
TerminalSize terminalSize = terminalScreen.getTerminalSize();
int terminalHeight = terminalSize.getRows();
int terminalWidth = terminalSize.getColumns();
int width = room.getWidth();
int height = room.getHeight();
int startX = (terminalWidth - (width * 2)) / 2;
int startY = (terminalHeight - height) / 2;
var start = getStart(room, terminalSize);
int startX = start.getX();
int startY = start.getY();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int pixel = room.getRGB(x, y);
int red = (pixel >> 16) & 0xff;
int pixel = getPixel(room, doors, doorPositions, player, playerTexture, x, y);
int red = (pixel >> 16) & 0xff;
int green = (pixel >> 8) & 0xff;
int blue = pixel & 0xff;
int blue = pixel & 0xff;
Pixel pixel1 = new ColoredPixel(new TextColor.RGB(red, green, blue));
buffer[y + startY][x * 2 + startX] = pixel1;
buffer[y + startY][x * 2 + 1 + startX] = pixel1;
/*unrenderedBuffer[y + startY][x * 2 + startX].addFirst(pixel1);
unrenderedBuffer[y + startY][x * 2 + 1 + startX].addFirst(pixel1);*/
}
}
partsToRerender.add(new RerenderScreen.ScreenPart(
@@ -88,12 +103,75 @@ public class FullRoomDrawHandler extends AbstractEventHandler<FullRoomDraw> {
new TerminalPosition(startY + height - 1, (startX + height - 1) * 2)
));
if (renderState.isFirstRender()) {
if (renderState.isFirstRender() || event.isFullRerender()) {
eventManager.emitEvent(RerenderScreen.full(terminalSize));
renderState.setFirstRender(false);
} else {
eventManager.emitEvent(new RerenderScreen(partsToRerender.toArray(RerenderScreen.ScreenPart[]::new)));
}
}
public static RoomCords getStart(BufferedImage room, TerminalSize terminalSize) {
int width = room.getWidth();
int height = room.getHeight();
int terminalHeight = terminalSize.getRows();
int terminalWidth = terminalSize.getColumns();
int x = (terminalWidth - (width * 2)) / 2;
int y = (terminalHeight - height) / 2;
return new RoomCords(x, y);
}
public static int getPixel(BufferedImage room, BufferedImage doors, Set<DoorPosition> doorPositions, Player player, BufferedImage playerTexture, int x, int y) {
if (x >= player.getPlayerCords().getX() && x < player.getPlayerCords().getX() + playerTexture.getWidth() - 1 && y >= player.getPlayerCords().getY() && y <= player.getPlayerCords().getY() + playerTexture.getHeight() - 1) {
int pixel = playerTexture.getRGB(x - player.getPlayerCords().getX(), y - player.getPlayerCords().getY());
int alpha = (pixel >> 24) & 0xff;
if (alpha != 0) {
return pixel;
}
}
record DoorBounds(int startX, int startY, int endX, int endY) {
boolean contains(int x, int y) {
return x >= startX && x <= endX && y >= startY && y <= endY;
}
}
Map<DoorPosition, DoorBounds> doorBounds = Map.of(
DoorPosition.TOP, new DoorBounds(96, 13, 132, 47),
DoorPosition.LEFT, new DoorBounds(0, 84, 37, 135),
DoorPosition.RIGHT, new DoorBounds(187, 98, 244, 136),
DoorPosition.BOTTOM, new DoorBounds(84, 190, 140, 225)
);
for (DoorPosition pos : doorPositions) {
DoorBounds bounds = doorBounds.get(pos);
if (bounds != null && bounds.contains(x, y)) {
return doors.getRGB(x, y);
}
}
return room.getRGB(x, y);
}
public static BufferedImage getPlayer(ResourceManager resourceManager, Player player) {
return resourceManager.getResource(switch (player.getPlayerRotation()) {
case FRONT -> ResourceManager.Resource.PLAYER_FRONT;
case BACK -> ResourceManager.Resource.PLAYER_BACK;
case LEFT -> ResourceManager.Resource.PLAYER_LEFT;
case RIGHT -> ResourceManager.Resource.PLAYER_RIGHT;
});
}
public static Set<DoorPosition> getDoorPositions(GameRoom currentRoom) {
Set<DoorPosition> doorPositions = new HashSet<>();
if (currentRoom.getLeft() != null) doorPositions.add(DoorPosition.LEFT);
if (currentRoom.getRight() != null) doorPositions.add(DoorPosition.RIGHT);
if (currentRoom.getUp() != null) doorPositions.add(DoorPosition.TOP);
if (currentRoom.getDown() != null) doorPositions.add(DoorPosition.BOTTOM);
return doorPositions;
}
}

View File

@@ -1,16 +1,10 @@
package cz.jzitnik.events.handlers;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.input.KeyStroke;
import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.ExitEvent;
import cz.jzitnik.events.KeyboardPressEvent;
import cz.jzitnik.events.RerenderScreen;
import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.ui.pixels.ColoredPixel;
import cz.jzitnik.ui.pixels.Pixel;
import cz.jzitnik.events.PlayerMoveEvent;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.events.AbstractEventHandler;
import cz.jzitnik.utils.events.EventManager;
@@ -21,33 +15,20 @@ public class KeyboardPressEventHandler extends AbstractEventHandler<KeyboardPres
super(dm);
}
@InjectState
private ScreenBuffer screenBuffer;
@Override
public void handle(KeyboardPressEvent event) {
EventManager eventManager = dm.getDependencyOrThrow(EventManager.class);
EventManager eventManager = dm.getDependencyOrThrow(EventManager.class); // TODO: Migrate to InjectDependency
KeyStroke keyStroke = event.getKeyStroke();
Pixel[][] buffer = screenBuffer.getBuffer();
switch (keyStroke.getKeyType()) {
case ArrowUp:
var start = new TerminalPosition(0, 0);
var end = new TerminalPosition(20, 10);
for (int y = start.getRow(); y < end.getRow(); y++) {
for (int x = start.getColumn(); x < end.getColumn(); x++) {
buffer[y][x] = new ColoredPixel(TextColor.ANSI.CYAN);
}
}
eventManager.emitEvent(new RerenderScreen(new RerenderScreen.ScreenPart(start, end)));
break;
case Escape:
eventManager.emitEvent(new ExitEvent());
break;
case Character:
switch (keyStroke.getCharacter()) {
case 'w','a','s','d' -> eventManager.emitEvent(new PlayerMoveEvent(keyStroke));
}
break;
default:
break;

View File

@@ -0,0 +1,117 @@
package cz.jzitnik.events.handlers;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.input.KeyStroke;
import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.FullRoomDraw;
import cz.jzitnik.events.PlayerMoveEvent;
import cz.jzitnik.events.RerenderScreen;
import cz.jzitnik.game.GameRoom;
import cz.jzitnik.game.GameState;
import cz.jzitnik.game.Player;
import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.states.TerminalState;
import cz.jzitnik.ui.pixels.ColoredPixel;
import cz.jzitnik.ui.pixels.Pixel;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.events.AbstractEventHandler;
import cz.jzitnik.utils.events.EventManager;
import lombok.extern.slf4j.Slf4j;
import java.awt.image.BufferedImage;
import java.util.Set;
@Slf4j
@EventHandler(PlayerMoveEvent.class)
public class PlayerMoveEventHandler extends AbstractEventHandler<PlayerMoveEvent> {
public PlayerMoveEventHandler(DependencyManager dm) {
super(dm);
}
@InjectState
private GameState gameState;
@InjectDependency
private EventManager eventManager;
@InjectState
private TerminalState terminalState;
@InjectState
private ScreenBuffer screenBuffer;
@InjectDependency
private ResourceManager resourceManager;
@Override
public void handle(PlayerMoveEvent event) {
KeyStroke keyStroke = event.getKeyStroke();
Player player = gameState.getPlayer();
RoomCords playerCords = player.getPlayerCords();
int originalPlayerX = playerCords.getX();
int originalPlayerY = playerCords.getY();
switch (keyStroke.getCharacter()) {
case 'w' -> {
if (originalPlayerY <= 10) return;
playerCords.updateCords(player.getPlayerCords().getX(), playerCords.getY() - 5);
}
case 'a' -> {
if (originalPlayerX <= 30) return;
playerCords.updateCords(player.getPlayerCords().getX() - 5, playerCords.getY());
}
case 's' -> {
if (originalPlayerY >= 110) return;
playerCords.updateCords(player.getPlayerCords().getX(), playerCords.getY() + 5);
}
case 'd' -> {
if (originalPlayerX >= 155) return;
playerCords.updateCords(player.getPlayerCords().getX() + 5, playerCords.getY());
}
}
int newPlayerX = playerCords.getX();
int newPlayerY = playerCords.getY();
GameRoom currentRoom = gameState.getCurrentRoom();
BufferedImage room = resourceManager.getResource(currentRoom.getTexture());
BufferedImage doors = resourceManager.getResource(ResourceManager.Resource.DOORS);
BufferedImage playerTexture = FullRoomDrawHandler.getPlayer(resourceManager, player);
Set<FullRoomDrawHandler.DoorPosition> doorPositions = FullRoomDrawHandler.getDoorPositions(currentRoom);
var buffer = screenBuffer.getRenderedBuffer();
var start = FullRoomDrawHandler.getStart(room, terminalState.getTerminalScreen().getTerminalSize());
int startX = start.getX();
int startY = start.getY();
int forStartX = Math.min(originalPlayerX, newPlayerX);
int forStartY = Math.min(originalPlayerY, newPlayerY);
int forEndX = Math.max(originalPlayerX, newPlayerX) + playerTexture.getWidth();
int forEndY = Math.max(originalPlayerY, newPlayerY) + playerTexture.getHeight();
for (int x = forStartX; x <= forEndX; x++) {
for (int y = forStartY; y <= forEndY; y++) {
int pixel = FullRoomDrawHandler.getPixel(room, doors, doorPositions, player, playerTexture, x, y);
int red = (pixel >> 16) & 0xff;
int green = (pixel >> 8) & 0xff;
int blue = pixel & 0xff;
Pixel pixel1 = new ColoredPixel(new TextColor.RGB(red, green, blue));
buffer[y + startY][x * 2 + startX] = pixel1;
buffer[y + startY][x * 2 + 1 + startX] = pixel1;
}
}
eventManager.emitEvent(new RerenderScreen(
new RerenderScreen.ScreenPart(
new TerminalPosition(forStartX + startX, forStartY + startY),
new TerminalPosition(forEndX * 2 + 1 + startX, forEndY + startY)
)
));
}
}

View File

@@ -5,7 +5,6 @@ import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.FullRoomDraw;
import cz.jzitnik.events.RerenderScreen;
import cz.jzitnik.events.TerminalResizeEvent;
import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.ui.pixels.Empty;
@@ -40,7 +39,7 @@ public class TerminalResizeEventHandler extends AbstractEventHandler<TerminalRes
buffer[y][x] = new Empty();
}
}
screenBuffer.setBuffer(buffer);
screenBuffer.setRenderedBuffer(buffer);
eventManager.emitEvent(new FullRoomDraw());
}

View File

@@ -1,19 +1,19 @@
package cz.jzitnik.game;
import cz.jzitnik.game.objects.GameObject;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
@AllArgsConstructor
@Getter
public class GameRoom {
private GameRoom left;
private GameRoom right;
private GameRoom up;
private GameRoom down;
private ResourceManager.Resource texture;
private List<GameObject> objects;
public GameRoom() {
objects = new ArrayList<>();
}
}

View File

@@ -0,0 +1,25 @@
package cz.jzitnik.game;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.game.utils.RoomCords;
import java.util.ArrayList;
@Dependency
public class GameSetup {
@InjectState
private GameState gameState;
public void setup() {
GameRoom mainRoom = new GameRoom(
null, null, null, null,
ResourceManager.Resource.ROOM1,
new ArrayList<>()
);
gameState.setCurrentRoom(mainRoom);
gameState.setPlayer(new Player(new RoomCords(155, 110)));
}
}

View File

@@ -1,11 +1,13 @@
package cz.jzitnik.game;
import cz.jzitnik.annotations.State;
import cz.jzitnik.game.utils.RoomCords;
import lombok.Getter;
import lombok.Setter;
@State
@Getter
@Setter
public class GameState {
private GameRoom currentRoom;
private RoomCords playerCords;
private Player player;
}

View File

@@ -0,0 +1,19 @@
package cz.jzitnik.game;
import cz.jzitnik.game.utils.RoomCords;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
@RequiredArgsConstructor
@Getter
public class Player {
public enum PlayerRotation {
FRONT, BACK, LEFT, RIGHT
}
private final RoomCords playerCords;
@Setter
private PlayerRotation playerRotation = PlayerRotation.FRONT;
}

View File

@@ -19,9 +19,20 @@ public class ResourceManager {
@AllArgsConstructor
@Getter
public enum Resource {
ROOM1("rooms/1.png");
ROOM1("rooms/1.png"),
ROOM2("rooms/2.png"),
ROOM3("rooms/3.png"),
ROOM4("rooms/4.png"),
ROOM_FROZEN("rooms/frozen.png"),
private final String path; // Field must be final for enum
PLAYER_FRONT("player/front.png"),
PLAYER_BACK("player/back.png"),
PLAYER_LEFT("player/left.png"),
PLAYER_RIGHT("player/right.png"),
DOORS("rooms/doors.png");
private final String path;
}
private HashMap<Resource, BufferedImage> resourceCache = new HashMap<>();

View File

@@ -1,26 +1,17 @@
package cz.jzitnik.game.utils;
import cz.jzitnik.config.RoomSizeConfig;
import cz.jzitnik.game.exceptions.InvalidCoordinatesException;
import lombok.Getter;
@Getter
public class RoomCords {
private final RoomSizeConfig roomSizeConfig;
private int x;
private int y;
public RoomCords(int x, int y, RoomSizeConfig roomSizeConfig) {
this.roomSizeConfig = roomSizeConfig;
public RoomCords(int x, int y) {
updateCords(x, y);
}
public void updateCords(int x, int y) {
if (x >= roomSizeConfig.getRoomWidth() || y >= roomSizeConfig.getRoomHeight()) {
throw new InvalidCoordinatesException();
}
this.x = x;
this.y = y;
}

View File

@@ -7,5 +7,5 @@ import lombok.Data;
@Data
@State
public class ScreenBuffer {
private Pixel[][] buffer = new Pixel[][] {};
private Pixel[][] renderedBuffer = new Pixel[][] {};
}

View File

@@ -86,7 +86,7 @@ public class EventManager extends Thread {
try {
handler.handle(typedEvent);
} catch (Exception e) {
log.debug(e.toString());
log.error("Error", e);
}
});
}

View File

@@ -13,12 +13,6 @@
<totalSizeCap>30MB</totalSizeCap>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB