From 41b7ac2a37bd4d6c4e6e49b05858033e81f88fff Mon Sep 17 00:00:00 2001 From: jzitnik-dev Date: Fri, 2 Jan 2026 00:35:09 +0100 Subject: [PATCH] feat: New Grid API and Inventory rendered --- .../cz/jzitnik/events/InventoryRerender.java | 6 + .../events/handlers/FullRoomDrawHandler.java | 3 + .../handlers/InventoryRerenderHandler.java | 100 ++++++++++ .../handlers/KeyboardPressEventHandler.java | 7 + src/main/java/cz/jzitnik/game/GameRoom.java | 1 - .../java/cz/jzitnik/game/objects/Chest.java | 173 ++++++++---------- .../java/cz/jzitnik/game/setup/GameSetup.java | 2 +- .../cz/jzitnik/game/setup/rooms/MainRoom.java | 2 +- src/main/java/cz/jzitnik/ui/Grid.java | 157 ++++++++++++++++ 9 files changed, 352 insertions(+), 99 deletions(-) create mode 100644 src/main/java/cz/jzitnik/events/InventoryRerender.java create mode 100644 src/main/java/cz/jzitnik/events/handlers/InventoryRerenderHandler.java create mode 100644 src/main/java/cz/jzitnik/ui/Grid.java diff --git a/src/main/java/cz/jzitnik/events/InventoryRerender.java b/src/main/java/cz/jzitnik/events/InventoryRerender.java new file mode 100644 index 0000000..f51cb6e --- /dev/null +++ b/src/main/java/cz/jzitnik/events/InventoryRerender.java @@ -0,0 +1,6 @@ +package cz.jzitnik.events; + +import cz.jzitnik.utils.events.Event; + +public class InventoryRerender implements Event { +} diff --git a/src/main/java/cz/jzitnik/events/handlers/FullRoomDrawHandler.java b/src/main/java/cz/jzitnik/events/handlers/FullRoomDrawHandler.java index 3a792dc..d1b3e4d 100644 --- a/src/main/java/cz/jzitnik/events/handlers/FullRoomDrawHandler.java +++ b/src/main/java/cz/jzitnik/events/handlers/FullRoomDrawHandler.java @@ -78,6 +78,9 @@ public class FullRoomDrawHandler extends AbstractEventHandler { 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), diff --git a/src/main/java/cz/jzitnik/events/handlers/InventoryRerenderHandler.java b/src/main/java/cz/jzitnik/events/handlers/InventoryRerenderHandler.java new file mode 100644 index 0000000..f934fe0 --- /dev/null +++ b/src/main/java/cz/jzitnik/events/handlers/InventoryRerenderHandler.java @@ -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 { + + 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); + } +} diff --git a/src/main/java/cz/jzitnik/events/handlers/KeyboardPressEventHandler.java b/src/main/java/cz/jzitnik/events/handlers/KeyboardPressEventHandler.java index 3c62cde..56dcb5c 100644 --- a/src/main/java/cz/jzitnik/events/handlers/KeyboardPressEventHandler.java +++ b/src/main/java/cz/jzitnik/events/handlers/KeyboardPressEventHandler.java @@ -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 { public KeyboardPressEventHandler(DependencyManager dm) { @@ -37,6 +40,10 @@ public class KeyboardPressEventHandler extends AbstractEventHandler eventManager.emitEvent(new PlayerMoveEvent(keyStroke)); diff --git a/src/main/java/cz/jzitnik/game/GameRoom.java b/src/main/java/cz/jzitnik/game/GameRoom.java index 0d32395..50326eb 100644 --- a/src/main/java/cz/jzitnik/game/GameRoom.java +++ b/src/main/java/cz/jzitnik/game/GameRoom.java @@ -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(); } } diff --git a/src/main/java/cz/jzitnik/game/objects/Chest.java b/src/main/java/cz/jzitnik/game/objects/Chest.java index 663344d..fccdc8d 100644 --- a/src/main/java/cz/jzitnik/game/objects/Chest.java +++ b/src/main/java/cz/jzitnik/game/objects/Chest.java @@ -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 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); diff --git a/src/main/java/cz/jzitnik/game/setup/GameSetup.java b/src/main/java/cz/jzitnik/game/setup/GameSetup.java index 8f2fbb9..7a9c427 100644 --- a/src/main/java/cz/jzitnik/game/setup/GameSetup.java +++ b/src/main/java/cz/jzitnik/game/setup/GameSetup.java @@ -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); diff --git a/src/main/java/cz/jzitnik/game/setup/rooms/MainRoom.java b/src/main/java/cz/jzitnik/game/setup/rooms/MainRoom.java index c45d006..3b336d9 100644 --- a/src/main/java/cz/jzitnik/game/setup/rooms/MainRoom.java +++ b/src/main/java/cz/jzitnik/game/setup/rooms/MainRoom.java @@ -28,6 +28,6 @@ public class MainRoom extends GameRoom { addObject(chest); Zombie zombie = new Zombie(resourceManager, new RoomCords(100, 100)); - addMob(zombie); + //addMob(zombie); } } diff --git a/src/main/java/cz/jzitnik/ui/Grid.java b/src/main/java/cz/jzitnik/ui/Grid.java new file mode 100644 index 0000000..b916cff --- /dev/null +++ b/src/main/java/cz/jzitnik/ui/Grid.java @@ -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; + } +}