feat: New Grid API and Inventory rendered

This commit is contained in:
2026-01-02 00:35:09 +01:00
parent bf8ca30d6a
commit 41b7ac2a37
9 changed files with 352 additions and 99 deletions

View File

@@ -0,0 +1,6 @@
package cz.jzitnik.events;
import cz.jzitnik.utils.events.Event;
public class InventoryRerender implements Event {
}

View File

@@ -78,6 +78,9 @@ public class FullRoomDrawHandler extends AbstractEventHandler<FullRoomDraw> {
int startY = start.getY();
RerenderUtils.rerenderPart(0, width - 1, 0, height - 1, startX, startY, currentRoom, room, player, playerTexture, screenBuffer, resourceManager, debugging);
if (event.isFullRerender()) {
InventoryRerenderHandler.renderInventoryRerender(dm);
}
partsToRerender.add(new RerenderScreen.ScreenPart(
new TerminalPosition(startX, startY),

View File

@@ -0,0 +1,100 @@
package cz.jzitnik.events.handlers;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.TextColor;
import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.events.InventoryRerender;
import cz.jzitnik.game.GameRoom;
import cz.jzitnik.game.GameState;
import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.items.GameItem;
import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.states.TerminalState;
import cz.jzitnik.ui.Grid;
import cz.jzitnik.ui.pixels.ColoredPixel;
import cz.jzitnik.ui.pixels.Pixel;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.RerenderUtils;
import cz.jzitnik.utils.StateManager;
import cz.jzitnik.utils.events.AbstractEventHandler;
import java.awt.image.BufferedImage;
@EventHandler(InventoryRerender.class)
public class InventoryRerenderHandler extends AbstractEventHandler<InventoryRerender> {
private static final int ITEMS_X = 3;
private static final int ITEMS_Y = 5;
private static final int OUTER_BORDER_WIDTH = 3;
private static final int INNER_BORDER_WIDTH = 2;
private static final int ITEM_SIZE = 16; // Characters
private static final int ITEM_PADDING = 2; // padding on each side
private static final Pixel BORDER_COLOR =
new ColoredPixel(new TextColor.RGB(255, 255, 255));
private static final Pixel BACKGROUND_COLOR =
new ColoredPixel(new TextColor.RGB(166, 166, 166));
private static final int ITEM_SLOT_SIZE = ITEM_SIZE + ITEM_PADDING * 2;
private static final int INVENTORY_WIDTH =
OUTER_BORDER_WIDTH * 2 +
(ITEMS_X * (ITEM_SLOT_SIZE + INNER_BORDER_WIDTH) - INNER_BORDER_WIDTH);
private static final int INVENTORY_HEIGHT =
OUTER_BORDER_WIDTH * 2 +
(ITEMS_Y * (ITEM_SLOT_SIZE + INNER_BORDER_WIDTH) - INNER_BORDER_WIDTH);
private static final int REAL_INVENTORY_HEIGHT =
Math.ceilDiv(INVENTORY_HEIGHT, 2); // 2 pixels per terminal row
public InventoryRerenderHandler(DependencyManager dm) {
super(dm);
}
public static void renderInventoryRerender(DependencyManager dm) {
StateManager stateManager = dm.getDependencyOrThrow(StateManager.class);
ResourceManager resourceManager = dm.getDependencyOrThrow(ResourceManager.class);
GameState gameState = stateManager.getOrThrow(GameState.class);
GameRoom currentRoom = gameState.getCurrentRoom();
BufferedImage room = resourceManager.getResource(currentRoom.getTexture());
TerminalState terminalState = stateManager.getOrThrow(TerminalState.class);
TerminalSize terminalSize = terminalState.getTerminalScreen().getTerminalSize();
var inventory = gameState.getPlayer().getInventory();
var buffer = stateManager.getOrThrow(ScreenBuffer.class).getRenderedBuffer();
var roomStart = RerenderUtils.getStart(room, terminalSize);
int maxX = roomStart.getX() - 1;
final int OFFSET_Y =
(((terminalSize.getRows() - 1) / 2) - (REAL_INVENTORY_HEIGHT / 2)) * 2;
final int OFFSET_X =
(maxX / 2) - (INVENTORY_WIDTH / 2);
BufferedImage[] textures = inventory.stream()
.map(GameItem::getTexture)
.toArray(BufferedImage[]::new);
Grid grid = new Grid(
ITEMS_X,
ITEMS_Y,
OUTER_BORDER_WIDTH,
INNER_BORDER_WIDTH,
ITEM_SIZE,
ITEM_PADDING,
BORDER_COLOR,
BACKGROUND_COLOR
);
Pixel[][] internalBuffer = grid.render(textures);
for (int y = 0; y < grid.getHeight(); y++) {
System.arraycopy(internalBuffer[y], 0, buffer[y + OFFSET_Y], OFFSET_X, grid.getWidth());
}
}
@Override
public void handle(InventoryRerender event) {
renderInventoryRerender(dm);
}
}

View File

@@ -5,13 +5,16 @@ import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.ExitEvent;
import cz.jzitnik.events.FullRoomDraw;
import cz.jzitnik.events.KeyboardPressEvent;
import cz.jzitnik.events.PlayerMoveEvent;
import cz.jzitnik.game.GameState;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.events.AbstractEventHandler;
import cz.jzitnik.utils.events.EventManager;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@EventHandler(KeyboardPressEvent.class)
public class KeyboardPressEventHandler extends AbstractEventHandler<KeyboardPressEvent> {
public KeyboardPressEventHandler(DependencyManager dm) {
@@ -37,6 +40,10 @@ public class KeyboardPressEventHandler extends AbstractEventHandler<KeyboardPres
case Escape:
eventManager.emitEvent(new ExitEvent());
break;
case F5:
log.debug("Fully rerendering screen");
eventManager.emitEvent(new FullRoomDraw(true));
break;
case Character:
switch (keyStroke.getCharacter()) {
case 'w','a','s','d' -> eventManager.emitEvent(new PlayerMoveEvent(keyStroke));

View File

@@ -32,7 +32,6 @@ public class GameRoom {
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
overrideBuffer[y][x] = new Empty();
overrideBuffer[y][x] = new Empty();
}
}

View File

@@ -17,6 +17,7 @@ import cz.jzitnik.game.items.GameItem;
import cz.jzitnik.game.utils.RoomCords;
import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.states.TerminalState;
import cz.jzitnik.ui.Grid;
import cz.jzitnik.ui.pixels.ColoredPixel;
import cz.jzitnik.ui.pixels.Empty;
import cz.jzitnik.ui.pixels.Pixel;
@@ -33,8 +34,10 @@ import java.util.List;
@Slf4j
public final class Chest extends GameObject implements UIClickHandler {
private static final TextColor BORDER_COLOR = new TextColor.RGB(0, 0, 0);
private static final TextColor TRANSPARENT_COLOR = new TextColor.RGB(255, 255, 255);
private static final TextColor BORDER_COLOR = new TextColor.RGB(255, 255, 255);
private static final TextColor TRANSPARENT_COLOR = new TextColor.RGB(166, 166, 166);
private static final int RENDER_PADDING = 1;
private final List<GameItem> items;
private final DependencyManager dependencyManager;
@@ -43,6 +46,7 @@ public final class Chest extends GameObject implements UIClickHandler {
private int actualDisplayStartX;
private int actualDisplayStartY;
private int chestUISizeX;
private int chestUISizeY;
@@ -78,6 +82,19 @@ public final class Chest extends GameObject implements UIClickHandler {
render(false);
}
private Grid createGrid(int itemCount) {
return new Grid(
Math.max(1, itemCount), // Items X
1, // Items Y
2, // Outer Border
2, // Inner Border
16, // Item Size
1, // Item Padding (Changed to 1 as per your request example)
new ColoredPixel(BORDER_COLOR),
new ColoredPixel(TRANSPARENT_COLOR)
);
}
private void render(boolean clear) {
GameRoom currentRoom = gameState.getCurrentRoom();
Player player = gameState.getPlayer();
@@ -94,8 +111,9 @@ public final class Chest extends GameObject implements UIClickHandler {
terminalState.getTerminalScreen().getTerminalSize()
);
int itemCount = items.size();
calculateUISize(itemCount);
Grid currentGrid = createGrid(items.size());
this.chestUISizeX = currentGrid.getWidth();
this.chestUISizeY = currentGrid.getHeight();
int chestUIStartX = getCords().getX();
int chestUIStartY = getCords().getY();
@@ -103,39 +121,39 @@ public final class Chest extends GameObject implements UIClickHandler {
int guiStartX = chestUIStartX + (chestTexture.getWidth() / 2) - (chestUISizeX / 2);
int guiStartY = chestUIStartY - chestUISizeY - 1;
TerminalPosition guiStart = new TerminalPosition(guiStartX + start.getX(), guiStartY + start.getY());
TerminalPosition guiEnd = new TerminalPosition(
(guiStartX + chestUISizeX - 1) + start.getX(),
(guiStartY + chestUISizeY - 1) + start.getY()
);
actualDisplayStartX = guiStartX + start.getX();
actualDisplayStartY = guiStartY + start.getY();
actualDisplayStartX = guiStartX + start.getX() + 2;
actualDisplayStartY = guiStartY + start.getY() + 2;
int renderMinX = actualDisplayStartX;
int renderMinY = actualDisplayStartY;
int renderMaxX = actualDisplayStartX + chestUISizeX;
int renderMaxY = actualDisplayStartY + chestUISizeY;
if (clear) {
Grid previousGrid = createGrid(items.size() + 1);
int prevWidth = previousGrid.getWidth();
int prevGuiStartX = chestUIStartX + (chestTexture.getWidth() / 2) - (prevWidth / 2);
int prevDisplayStartX = prevGuiStartX + start.getX();
renderMinX = Math.min(renderMinX, prevDisplayStartX);
renderMaxX = Math.max(renderMaxX, prevDisplayStartX + prevWidth);
clearPreviousUI(
currentRoom,
roomTexture,
player,
playerTexture,
buffer,
overrideBuffer,
start,
currentRoom, roomTexture, player, playerTexture,
buffer, overrideBuffer, start,
guiStartY,
chestUIStartX,
chestTexture.getWidth(),
itemCount
prevGuiStartX,
prevWidth,
chestUISizeY
);
int clearWidth = 4 + (itemCount + 1) * 16 + itemCount * 2;
int clearStartX = chestUIStartX + (chestTexture.getWidth() / 2) - (clearWidth / 2);
guiStart = new TerminalPosition(clearStartX + start.getX(), guiStartY + start.getY());
guiEnd = new TerminalPosition(clearStartX + start.getX() + clearWidth, (guiStartY + chestUISizeY - 1) + start.getY());
}
TerminalPosition guiStart = new TerminalPosition(renderMinX - RENDER_PADDING, renderMinY - RENDER_PADDING);
TerminalPosition guiEnd = new TerminalPosition(renderMaxX + RENDER_PADDING, renderMaxY + RENDER_PADDING);
if (!items.isEmpty()) {
drawUI(buffer, overrideBuffer, start, guiStartX, guiStartY);
drawUI(currentGrid, buffer, overrideBuffer, start, guiStartX, guiStartY);
}
RerenderUtils.rerenderPart(
@@ -156,18 +174,13 @@ public final class Chest extends GameObject implements UIClickHandler {
RerenderScreen.ScreenPart sp = new RerenderScreen.ScreenPart(guiStart, guiEnd);
listenerHashCode = uiClickHandlerRepository.registerCurrentRoomHandler(sp, this);
//eventManager.emitEvent(RerenderScreen.full(terminalState.getTerminalScreen().getTerminalSize()));
if (!items.isEmpty()) {
listenerHashCode = uiClickHandlerRepository.registerCurrentRoomHandler(sp, this);
}
eventManager.emitEvent(new RerenderScreen(sp));
}
private void calculateUISize(int itemCount) {
chestUISizeY = 20;
chestUISizeX = 4 + itemCount * 16 + (itemCount - 1) * 2;
}
private void clearPreviousUI(
GameRoom room,
BufferedImage roomTexture,
@@ -177,14 +190,11 @@ public final class Chest extends GameObject implements UIClickHandler {
Pixel[][] overrideBuffer,
RoomCords start,
int guiStartY,
int chestUIStartX,
int chestTextureWidth,
int itemCount
int clearStartX,
int clearWidth,
int clearHeight
) {
int clearWidth = 4 + (itemCount + 1) * 16 + itemCount * 2;
int clearStartX = chestUIStartX + (chestTextureWidth / 2) - (clearWidth / 2);
for (int y = guiStartY; y < guiStartY + chestUISizeY; y++) {
for (int y = guiStartY; y < guiStartY + clearHeight; y++) {
for (int x = clearStartX; x < clearStartX + clearWidth; x++) {
int pixel = RerenderUtils.getPixel(
room,
@@ -205,17 +215,30 @@ public final class Chest extends GameObject implements UIClickHandler {
}
private void drawUI(
Grid grid,
Pixel[][] buffer,
Pixel[][] overrideBuffer,
RoomCords start,
int guiStartX,
int guiStartY
) {
for (int y = 0; y < chestUISizeY; y++) {
for (int x = 0; x < chestUISizeX; x++) {
Pixel pixel = getPixel(x, y, chestUISizeX, chestUISizeY);
buffer[guiStartY + y + start.getY()][guiStartX + x + start.getX()] = pixel;
overrideBuffer[guiStartY + y][guiStartX + x] = pixel;
BufferedImage[] textures = items.stream()
.map(GameItem::getTexture)
.toArray(BufferedImage[]::new);
Pixel[][] uiPixels = grid.render(textures);
for (int y = 0; y < grid.getHeight(); y++) {
for (int x = 0; x < grid.getWidth(); x++) {
Pixel pixel = uiPixels[y][x];
int targetY = guiStartY + y + start.getY();
int targetX = guiStartX + x + start.getX();
if (targetY >= 0 && targetY < buffer.length && targetX >= 0 && targetX < buffer[0].length) {
buffer[targetY][targetX] = pixel;
overrideBuffer[guiStartY + y][guiStartX + x] = pixel;
}
}
}
}
@@ -227,65 +250,23 @@ public final class Chest extends GameObject implements UIClickHandler {
return new ColoredPixel(new TextColor.RGB(r, g, b));
}
private Pixel getPixel(int objectX, int objectY, int width, int height) {
if (isBorder(objectX, objectY, width, height)) {
return new ColoredPixel(BORDER_COLOR);
}
int screenX = objectX - 2;
int screenY = objectY - 2;
int part = screenX / 18;
int rest = screenX % 18;
if (rest >= 16) {
return new ColoredPixel(BORDER_COLOR);
}
GameItem item = items.get(part);
BufferedImage texture = item.getTexture();
int pixel = texture.getRGB(rest, screenY);
int alpha = (pixel >> 24) & 0xff;
if (alpha == 0) {
return new ColoredPixel(TRANSPARENT_COLOR);
}
return pixelToColored(pixel);
}
private boolean isBorder(int x, int y, int width, int height) {
return x <= 1 || x >= width - 2 || y <= 1 || y >= height - 2;
}
@Override
public void handleClick(MouseAction mouseAction) {
int mouseX = mouseAction.getPosition().getColumn();
int mouseY = mouseAction.getPosition().getRow();
log.debug("Mouse: x: {}, y: {}", mouseX, mouseY);
int localX = mouseX - actualDisplayStartX;
int localY = (mouseY * 2) - actualDisplayStartY;
int screenX = mouseX - actualDisplayStartX;
int screenY = (mouseY * 2) - actualDisplayStartY;
Grid grid = createGrid(items.size());
log.debug("Screen: x: {}, y: {}", screenX, screenY);
int itemIndex = grid.getItemIndexAt(localX, localY);
if (screenX < 0 || screenY < 0 || screenX > chestUISizeX || screenY > chestUISizeY) {
if (itemIndex == -1 || itemIndex >= items.size()) {
return;
}
int part = screenX / 18;
if (part >= items.size()) {
return;
}
int rest = screenX % 18;
if (rest >= 16) {
return;
}
var item = items.get(part);
GameItem item = items.get(itemIndex);
gameState.getPlayer().getInventory().add(item);
items.remove(item);

View File

@@ -20,7 +20,7 @@ public class GameSetup {
private DependencyManager dependencyManager;
public void setup() {
gameState.setScreen(new IntroScene(dependencyManager));
//gameState.setScreen(new IntroScene(dependencyManager));
GameRoom mainRoom = new MainRoom(dependencyManager, resourceManager);
GameRoom rightRoom = new GameRoom(ResourceManager.Resource.ROOM2);

View File

@@ -28,6 +28,6 @@ public class MainRoom extends GameRoom {
addObject(chest);
Zombie zombie = new Zombie(resourceManager, new RoomCords(100, 100));
addMob(zombie);
//addMob(zombie);
}
}

View File

@@ -0,0 +1,157 @@
package cz.jzitnik.ui;
import cz.jzitnik.ui.pixels.ColoredPixel;
import cz.jzitnik.ui.pixels.Pixel;
import java.awt.image.BufferedImage;
public class Grid {
private final int itemsX;
private final int itemsY;
private final int outerBorderWidth;
private final int innerBorderWidth;
private final int itemPadding;
private final Pixel borderColor;
private final Pixel backgroundColor;
private final int itemSlotSize;
private final int cellSize;
private final int gridWidth;
private final int gridHeight;
public Grid(
int itemsX,
int itemsY,
int outerBorderWidth,
int innerBorderWidth,
int itemSize,
int itemPadding,
Pixel borderColor,
Pixel backgroundColor
) {
this.itemsX = itemsX;
this.itemsY = itemsY;
this.outerBorderWidth = outerBorderWidth;
this.innerBorderWidth = innerBorderWidth;
this.itemPadding = itemPadding;
this.borderColor = borderColor;
this.backgroundColor = backgroundColor;
this.itemSlotSize = itemSize + itemPadding * 2;
this.cellSize = itemSlotSize + innerBorderWidth;
this.gridWidth =
outerBorderWidth * 2 +
(itemsX * (itemSlotSize + innerBorderWidth) - innerBorderWidth);
this.gridHeight =
outerBorderWidth * 2 +
(itemsY * (itemSlotSize + innerBorderWidth) - innerBorderWidth);
}
public Pixel[][] render(BufferedImage[] textures) {
Pixel[][] buffer = new Pixel[gridHeight][gridWidth];
// Background + borders
for (int y = 0; y < gridHeight; y++) {
for (int x = 0; x < gridWidth; x++) {
boolean isOuterBorder =
x < outerBorderWidth ||
y < outerBorderWidth ||
x >= gridWidth - outerBorderWidth ||
y >= gridHeight - outerBorderWidth;
boolean isInnerBorderX =
x >= outerBorderWidth + itemSlotSize &&
((x - outerBorderWidth - itemSlotSize) % cellSize) < innerBorderWidth;
boolean isInnerBorderY =
y >= outerBorderWidth + itemSlotSize &&
((y - outerBorderWidth - itemSlotSize) % cellSize) < innerBorderWidth;
buffer[y][x] = (isOuterBorder || isInnerBorderX || isInnerBorderY)
? borderColor
: backgroundColor;
}
}
// Items
int maxItems = Math.min(textures.length, itemsX * itemsY);
for (int index = 0; index < maxItems; index++) {
BufferedImage texture = textures[index];
if (texture == null) continue;
int itemX = index % itemsX;
int itemY = index / itemsX;
int baseX =
outerBorderWidth +
itemX * cellSize +
itemPadding;
int baseY =
outerBorderWidth +
itemY * cellSize +
itemPadding;
for (int y = 0; y < texture.getHeight(); y++) {
for (int x = 0; x < texture.getWidth(); x++) {
int pixel = texture.getRGB(x, y);
int alpha = (pixel >> 24) & 0xff;
if (alpha == 0) continue;
int r = (pixel >> 16) & 0xff;
int g = (pixel >> 8) & 0xff;
int b = pixel & 0xff;
buffer[baseY + y][baseX + x] =
new ColoredPixel(new com.googlecode.lanterna.TextColor.RGB(r, g, b));
}
}
}
return buffer;
}
/**
* Calculates which item index corresponds to the given x/y coordinates relative to the Grid's top-left.
* @param x X coordinate relative to the Grid (0 is left edge of the outer border)
* @param y Y coordinate relative to the Grid (0 is top edge of the outer border)
* @return The index of the item, or -1 if the click was on a border/background/out of bounds.
*/
public int getItemIndexAt(int x, int y) {
if (x < 0 || x >= gridWidth || y < 0 || y >= gridHeight) {
return -1;
}
if (x < outerBorderWidth || x >= gridWidth - outerBorderWidth ||
y < outerBorderWidth || y >= gridHeight - outerBorderWidth) {
return -1;
}
int contentX = x - outerBorderWidth;
int contentY = y - outerBorderWidth;
int col = contentX / cellSize;
int row = contentY / cellSize;
if (contentX % cellSize >= itemSlotSize) return -1;
if (contentY % cellSize >= itemSlotSize) return -1;
if (col >= itemsX || row >= itemsY) return -1;
return row * itemsX + col;
}
public int getWidth() {
return gridWidth;
}
public int getHeight() {
return gridHeight;
}
}