feat: Dragging around items in inventory

This commit is contained in:
2026-01-02 12:56:28 +01:00
parent a9be5fa675
commit 86f2159750
9 changed files with 211 additions and 35 deletions

View File

@@ -109,6 +109,7 @@ public class FullRoomDrawHandler extends AbstractEventHandler<FullRoomDraw> {
// Screen too small to fit the room
eventManager.emitEvent(new TerminalTooSmallEvent());
renderState.setTerminalTooSmall(true);
log.error("Terminal too small", e);
}
}

View File

@@ -78,6 +78,7 @@ public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
object.ifPresent(selectable -> selectable.interact(dm));
}
default -> uiClickHandlerRepository.handleElse(event);
}
}
}

View File

@@ -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<GameItem> 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;

View File

@@ -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);

View File

@@ -8,4 +8,8 @@ public interface GlobalUIClickHandler {
default boolean handleMove(MouseAction ignoredMouseAction) {
return false;
}
default boolean handleElse(MouseAction ignoredMouseAction) {
return false;
}
}

View File

@@ -6,4 +6,5 @@ public interface UIClickHandler {
void handleClick(MouseAction mouseAction);
default void handleMove(MouseAction ignoredMouseAction) {}
default void handleElse(MouseAction ignoredMouseAction) {}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -70,6 +70,31 @@ public class UIClickHandlerRepository {
return false;
}
public boolean handleElse(MouseAction mouseAction) {
if (roomSpecificHandlers.containsKey(gameState.getCurrentRoom())) {
Map<RerenderScreen.ScreenPart, UIClickHandler> 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<RerenderScreen.ScreenPart, UIClickHandler> handlers = roomSpecificHandlers.get(gameState.getCurrentRoom());