diff --git a/src/main/java/cz/jzitnik/events/handlers/FullRoomDrawHandler.java b/src/main/java/cz/jzitnik/events/handlers/FullRoomDrawHandler.java index 20408db..1bcc5ca 100644 --- a/src/main/java/cz/jzitnik/events/handlers/FullRoomDrawHandler.java +++ b/src/main/java/cz/jzitnik/events/handlers/FullRoomDrawHandler.java @@ -109,6 +109,7 @@ public class FullRoomDrawHandler extends AbstractEventHandler { // Screen too small to fit the room eventManager.emitEvent(new TerminalTooSmallEvent()); renderState.setTerminalTooSmall(true); + log.error("Terminal too small", e); } } diff --git a/src/main/java/cz/jzitnik/events/handlers/MouseActionEventHandler.java b/src/main/java/cz/jzitnik/events/handlers/MouseActionEventHandler.java index 142a619..eb15a12 100644 --- a/src/main/java/cz/jzitnik/events/handlers/MouseActionEventHandler.java +++ b/src/main/java/cz/jzitnik/events/handlers/MouseActionEventHandler.java @@ -78,6 +78,7 @@ public class MouseActionEventHandler extends AbstractEventHandler { object.ifPresent(selectable -> selectable.interact(dm)); } + default -> uiClickHandlerRepository.handleElse(event); } } } diff --git a/src/main/java/cz/jzitnik/game/Player.java b/src/main/java/cz/jzitnik/game/Player.java index 9292d26..88f8dee 100644 --- a/src/main/java/cz/jzitnik/game/Player.java +++ b/src/main/java/cz/jzitnik/game/Player.java @@ -2,15 +2,12 @@ package cz.jzitnik.game; import cz.jzitnik.game.items.GameItem; import cz.jzitnik.game.utils.RoomCords; -import cz.jzitnik.utils.DependencyManager; -import cz.jzitnik.utils.events.EventManager; +import cz.jzitnik.ui.Inventory; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -21,9 +18,22 @@ import java.util.concurrent.TimeUnit; public class Player { private final RoomCords playerCords; private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); - private final List inventory = new CopyOnWriteArrayList<>(); + private final GameItem[] inventory = new GameItem[Inventory.ITEMS_X * Inventory.ITEMS_Y]; private boolean swinging = false; + public boolean addItem(GameItem item) { + boolean added = false; + for (int i = 0; i < inventory.length; i++) { + if (inventory[i] == null) { + inventory[i] = item; + added = true; + break; + } + } + + return added; + } + @Setter private PlayerRotation playerRotation = PlayerRotation.FRONT; diff --git a/src/main/java/cz/jzitnik/game/objects/Chest.java b/src/main/java/cz/jzitnik/game/objects/Chest.java index b79ba08..1b632d3 100644 --- a/src/main/java/cz/jzitnik/game/objects/Chest.java +++ b/src/main/java/cz/jzitnik/game/objects/Chest.java @@ -92,7 +92,6 @@ public final class Chest extends GameObject implements UIClickHandler { 16, // Item Size 1, // Item Padding (Changed to 1 as per your request example) new ColoredPixel(BORDER_COLOR), - new ColoredPixel(TRANSPARENT_COLOR), new ColoredPixel(TRANSPARENT_COLOR) ); } @@ -228,7 +227,7 @@ public final class Chest extends GameObject implements UIClickHandler { .map(GameItem::getTexture) .toArray(BufferedImage[]::new); - Pixel[][] uiPixels = grid.render(textures, -1); + Pixel[][] uiPixels = grid.render(textures); for (int y = 0; y < grid.getHeight(); y++) { for (int x = 0; x < grid.getWidth(); x++) { @@ -269,7 +268,10 @@ public final class Chest extends GameObject implements UIClickHandler { } GameItem item = items.get(itemIndex); - gameState.getPlayer().getInventory().add(item); + boolean added = gameState.getPlayer().addItem(item); + if (!added) { + return; + } eventManager.emitEvent(new InventoryRerender()); items.remove(item); diff --git a/src/main/java/cz/jzitnik/game/objects/GlobalUIClickHandler.java b/src/main/java/cz/jzitnik/game/objects/GlobalUIClickHandler.java index ccb2284..a9a5b78 100644 --- a/src/main/java/cz/jzitnik/game/objects/GlobalUIClickHandler.java +++ b/src/main/java/cz/jzitnik/game/objects/GlobalUIClickHandler.java @@ -8,4 +8,8 @@ public interface GlobalUIClickHandler { default boolean handleMove(MouseAction ignoredMouseAction) { return false; } + + default boolean handleElse(MouseAction ignoredMouseAction) { + return false; + } } diff --git a/src/main/java/cz/jzitnik/game/objects/UIClickHandler.java b/src/main/java/cz/jzitnik/game/objects/UIClickHandler.java index f89c30d..ae24249 100644 --- a/src/main/java/cz/jzitnik/game/objects/UIClickHandler.java +++ b/src/main/java/cz/jzitnik/game/objects/UIClickHandler.java @@ -6,4 +6,5 @@ public interface UIClickHandler { void handleClick(MouseAction mouseAction); default void handleMove(MouseAction ignoredMouseAction) {} + default void handleElse(MouseAction ignoredMouseAction) {} } diff --git a/src/main/java/cz/jzitnik/ui/Inventory.java b/src/main/java/cz/jzitnik/ui/Inventory.java index 6bad8af..6bb509c 100644 --- a/src/main/java/cz/jzitnik/ui/Inventory.java +++ b/src/main/java/cz/jzitnik/ui/Inventory.java @@ -22,11 +22,12 @@ import cz.jzitnik.utils.events.EventManager; import lombok.Getter; import java.awt.image.BufferedImage; +import java.util.Arrays; @Dependency public class Inventory implements GlobalUIClickHandler { - private static final int ITEMS_X = 3; - private static final int ITEMS_Y = 5; + public static final int ITEMS_X = 3; + public static final int ITEMS_Y = 5; private static final int OUTER_BORDER_WIDTH = 2; private static final int INNER_BORDER_WIDTH = 1; private static final int ITEM_SIZE = 16; // Characters @@ -35,8 +36,10 @@ public class Inventory implements GlobalUIClickHandler { 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 Pixel BACKGROUND_COLOR_SELECTED = + private static final Pixel BACKGROUND_COLOR_HOVERED = new ColoredPixel(new TextColor.RGB(186, 186, 186)); + private static final Pixel BACKGROUND_COLOR_SELECTED = + new ColoredPixel(new TextColor.RGB(216, 216, 216)); private static final int ITEM_SLOT_SIZE = ITEM_SIZE + ITEM_PADDING * 2; public static final int INVENTORY_WIDTH = OUTER_BORDER_WIDTH * 2 + @@ -46,7 +49,6 @@ public class Inventory implements GlobalUIClickHandler { (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 - private static final Grid grid = new Grid( ITEMS_X, ITEMS_Y, @@ -55,10 +57,9 @@ public class Inventory implements GlobalUIClickHandler { ITEM_SIZE, ITEM_PADDING, BORDER_COLOR, - BACKGROUND_COLOR, - BACKGROUND_COLOR_SELECTED + BACKGROUND_COLOR ); - int selectedIndex = -1; + private final InventoryState inventoryState = new InventoryState(); @InjectDependency private ResourceManager resourceManager; @InjectState @@ -86,15 +87,52 @@ public class Inventory implements GlobalUIClickHandler { @Override public boolean handleClick(MouseAction mouseAction) { + inventoryState.onNextDragTakeItemOnIndex = -1; + TerminalPosition terminalPosition = calculateActualCords(mouseAction.getPosition()); + var inventory = gameState.getPlayer().getInventory(); + + if (inventoryState.draggingItem != null) { + int itemClickedOnIndex = grid.getItemIndexAt(terminalPosition.getColumn(), terminalPosition.getRow(), true); + GameItem gameItem = inventoryState.draggingItem; + inventoryState.draggingItem = null; + inventoryState.draggingItemPosition = null; + inventoryState.selectedItem = -1; + + if (itemClickedOnIndex != -1) { + inventory[itemClickedOnIndex] = gameItem; + inventoryState.hoveredItem = itemClickedOnIndex; + } else { + gameState.getPlayer().addItem(gameItem); + } + + eventManager.emitEvent(new InventoryRerender()); + return true; + } + + int itemClickedOnIndex = grid.getItemIndexAt(terminalPosition.getColumn(), terminalPosition.getRow()); if (terminalPosition.getColumn() < 0 || terminalPosition.getRow() < 0 || terminalPosition.getRow() >= INVENTORY_HEIGHT || terminalPosition.getColumn() >= INVENTORY_WIDTH) { return false; } - int itemClickedOnIndex = grid.getItemIndexAt(terminalPosition.getColumn(), terminalPosition.getRow()); + if (itemClickedOnIndex == -1) { + return true; + } - // TODO: Clicking on items + if (inventoryState.selectedItem == itemClickedOnIndex) { + inventoryState.selectedItem = -1; + } else if (inventoryState.selectedItem == -1) { + inventoryState.selectedItem = itemClickedOnIndex; + } else { + // Already have selected item and now clicked on different item so swap items + GameItem temp = inventory[inventoryState.selectedItem]; + inventory[inventoryState.selectedItem] = inventory[itemClickedOnIndex]; + inventory[itemClickedOnIndex] = temp; + inventoryState.selectedItem = -1; + } + + eventManager.emitEvent(new InventoryRerender()); return true; } @@ -104,8 +142,8 @@ public class Inventory implements GlobalUIClickHandler { TerminalPosition terminalPosition = calculateActualCords(mouseAction.getPosition()); if (terminalPosition.getColumn() < 0 || terminalPosition.getRow() < 0 || terminalPosition.getRow() >= INVENTORY_HEIGHT || terminalPosition.getColumn() >= INVENTORY_WIDTH) { - if (selectedIndex != -1) { - selectedIndex = -1; + if (inventoryState.hoveredItem != -1) { + inventoryState.hoveredItem = -1; eventManager.emitEvent(new InventoryRerender()); } return false; @@ -113,14 +151,47 @@ public class Inventory implements GlobalUIClickHandler { int newSelectedIndex = grid.getItemIndexAt(terminalPosition.getColumn(), terminalPosition.getRow()); - if (newSelectedIndex != selectedIndex) { - selectedIndex = newSelectedIndex; + if (newSelectedIndex != inventoryState.hoveredItem) { + inventoryState.hoveredItem = newSelectedIndex; eventManager.emitEvent(new InventoryRerender()); } return true; } + @Override + public boolean handleElse(MouseAction mouseAction) { + TerminalPosition terminalPosition = calculateActualCords(mouseAction.getPosition()); + + if (terminalPosition.getColumn() < 0 || terminalPosition.getRow() < 0 || terminalPosition.getRow() >= INVENTORY_HEIGHT || terminalPosition.getColumn() >= INVENTORY_WIDTH) { + return false; + } + + switch (mouseAction.getActionType()) { + case CLICK_DOWN -> + inventoryState.onNextDragTakeItemOnIndex = grid.getItemIndexAt(terminalPosition.getColumn(), terminalPosition.getRow()); + case DRAG -> { + boolean render = false; + if (inventoryState.onNextDragTakeItemOnIndex != -1) { + inventoryState.draggingItem = gameState.getPlayer().getInventory()[inventoryState.onNextDragTakeItemOnIndex]; + gameState.getPlayer().getInventory()[inventoryState.onNextDragTakeItemOnIndex] = null; + inventoryState.onNextDragTakeItemOnIndex = -1; + render = true; + } else if (inventoryState.draggingItem != null) { + render = true; + } + + if (render) { + inventoryState.selectedItem = -1; + inventoryState.draggingItemPosition = terminalPosition; + eventManager.emitEvent(new InventoryRerender()); + } + } + } + + return true; + } + private TerminalPosition calculateActualCords(TerminalPosition position) { return new TerminalPosition(position.getColumn() - offsetX, position.getRow() * 2 - offsetY); } @@ -130,14 +201,55 @@ public class Inventory implements GlobalUIClickHandler { var inventory = gameState.getPlayer().getInventory(); var buffer = screenBuffer.getRenderedBuffer(); - BufferedImage[] textures = inventory.stream() - .map(GameItem::getTexture) + BufferedImage[] textures = Arrays.stream(inventory) + .map(item -> item == null ? null : item.getTexture()) .toArray(BufferedImage[]::new); - Pixel[][] internalBuffer = grid.render(textures, selectedIndex); + Pixel[] backgrounds = new Pixel[ITEMS_X * ITEMS_Y]; + if (inventoryState.hoveredItem != -1) { + backgrounds[inventoryState.hoveredItem] = BACKGROUND_COLOR_HOVERED; + } + if (inventoryState.selectedItem != -1) { + backgrounds[inventoryState.selectedItem] = BACKGROUND_COLOR_SELECTED; + } + Pixel[][] internalBuffer = grid.render(textures, backgrounds); + + if (inventoryState.draggingItem != null) { + BufferedImage texture = inventoryState.draggingItem.getTexture(); + int offsetX = Math.min( + internalBuffer[0].length - texture.getWidth(), + Math.max(0, inventoryState.draggingItemPosition.getColumn() - (texture.getWidth() / 2)) + ); + int offsetY = Math.min( + internalBuffer.length - texture.getHeight(), + Math.max(0, inventoryState.draggingItemPosition.getRow() - (texture.getHeight() / 2)) + ); + + 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; + int r = (pixel >> 16) & 0xff; + int g = (pixel >> 8) & 0xff; + int b = pixel & 0xff; + if (alpha == 0) { + continue; + } + internalBuffer[y + offsetY][x + offsetX] = new ColoredPixel(new TextColor.RGB(r, g, b)); + } + } + } for (int y = 0; y < grid.getHeight(); y++) { System.arraycopy(internalBuffer[y], 0, buffer[y + offsetY], offsetX, grid.getWidth()); } } + + private static class InventoryState { + protected int hoveredItem = -1; + protected int selectedItem = -1; + protected GameItem draggingItem; + protected TerminalPosition draggingItemPosition; + protected int onNextDragTakeItemOnIndex = -1; + } } diff --git a/src/main/java/cz/jzitnik/ui/utils/Grid.java b/src/main/java/cz/jzitnik/ui/utils/Grid.java index d9c5a96..7fbbf00 100644 --- a/src/main/java/cz/jzitnik/ui/utils/Grid.java +++ b/src/main/java/cz/jzitnik/ui/utils/Grid.java @@ -14,7 +14,6 @@ public class Grid { private final Pixel borderColor; private final Pixel backgroundColor; - private final Pixel backgroundColorSelected; private final int itemSlotSize; private final int cellSize; @@ -29,8 +28,7 @@ public class Grid { int itemSize, int itemPadding, Pixel borderColor, - Pixel backgroundColor, - Pixel backgroundColorSelected + Pixel backgroundColor ) { this.itemsX = itemsX; this.itemsY = itemsY; @@ -39,7 +37,6 @@ public class Grid { this.itemPadding = itemPadding; this.borderColor = borderColor; this.backgroundColor = backgroundColor; - this.backgroundColorSelected = backgroundColorSelected; this.itemSlotSize = itemSize + itemPadding * 2; this.cellSize = itemSlotSize + innerBorderWidth; @@ -53,7 +50,11 @@ public class Grid { (itemsY * (itemSlotSize + innerBorderWidth) - innerBorderWidth); } - public Pixel[][] render(BufferedImage[] textures, int selectedIndex) { + public Pixel[][] render(BufferedImage[] textures) { + return render(textures, new Pixel[textures.length]); + } + + public Pixel[][] render(BufferedImage[] textures, Pixel[] customBackgrounds) { Pixel[][] buffer = new Pixel[gridHeight][gridWidth]; for (int y = 0; y < gridHeight; y++) { @@ -91,11 +92,13 @@ public class Grid { int slotX = outerBorderWidth + itemX * cellSize; int slotY = outerBorderWidth + itemY * cellSize; - if (index == selectedIndex) { + Pixel customBackground = customBackgrounds[index]; + + if (customBackground != null) { for (int dy = 0; dy < itemSlotSize; dy++) { for (int dx = 0; dx < itemSlotSize; dx++) { if (slotY + dy < gridHeight && slotX + dx < gridWidth) { - buffer[slotY + dy][slotX + dx] = backgroundColorSelected; + buffer[slotY + dy][slotX + dx] = customBackgrounds[index]; } } } @@ -128,13 +131,17 @@ public class Grid { return buffer; } + public int getItemIndexAt(int x, int y) { + return getItemIndexAt(x, y, false); + } + /** * 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) { + public int getItemIndexAt(int x, int y, boolean ignoreInnerBorder) { if (x < 0 || x >= gridWidth || y < 0 || y >= gridHeight) { return -1; } @@ -150,11 +157,24 @@ public class Grid { 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; + int innerX = contentX % cellSize; + int innerY = contentY % cellSize; + + if (!ignoreInnerBorder) { + if (innerX >= itemSlotSize) return -1; + if (innerY >= itemSlotSize) return -1; + } else { + // Clamp to nearest valid position inside item slot + if (innerX >= itemSlotSize) { + innerX = itemSlotSize - 1; + } + if (innerY >= itemSlotSize) { + innerY = itemSlotSize - 1; + } + } + return row * itemsX + col; } diff --git a/src/main/java/cz/jzitnik/utils/UIClickHandlerRepository.java b/src/main/java/cz/jzitnik/utils/UIClickHandlerRepository.java index 7b1bd98..45d3b85 100644 --- a/src/main/java/cz/jzitnik/utils/UIClickHandlerRepository.java +++ b/src/main/java/cz/jzitnik/utils/UIClickHandlerRepository.java @@ -70,6 +70,31 @@ public class UIClickHandlerRepository { return false; } + public boolean handleElse(MouseAction mouseAction) { + if (roomSpecificHandlers.containsKey(gameState.getCurrentRoom())) { + Map handlers = roomSpecificHandlers.get(gameState.getCurrentRoom()); + + for (var entry : handlers.entrySet()) { + RerenderScreen.ScreenPart part = entry.getKey(); + UIClickHandler uiClickHandler = entry.getValue(); + TerminalPosition position = new TerminalPosition(mouseAction.getPosition().getColumn(), mouseAction.getPosition().getRow() * 2); + + if (part.isWithin(position)) { + uiClickHandler.handleElse(mouseAction); + return true; + } + } + } + + for (var handler : globalHandlers) { + if (handler.handleElse(mouseAction)) { + return true; + } + } + + return false; + } + public boolean handleMove(MouseAction mouseAction) { if (roomSpecificHandlers.containsKey(gameState.getCurrentRoom())) { Map handlers = roomSpecificHandlers.get(gameState.getCurrentRoom());