feat: Inventory item mouse hover animation

This commit is contained in:
2026-01-02 01:32:39 +01:00
parent 1eea0a701e
commit a9be5fa675
9 changed files with 257 additions and 118 deletions

View File

@@ -18,8 +18,10 @@ import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.states.RenderState;
import cz.jzitnik.states.ScreenBuffer;
import cz.jzitnik.states.TerminalState;
import cz.jzitnik.ui.Inventory;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.RerenderUtils;
import cz.jzitnik.utils.UIClickHandlerRepository;
import cz.jzitnik.utils.events.AbstractEventHandler;
import cz.jzitnik.utils.events.EventManager;
import cz.jzitnik.utils.roomtasks.RoomTaskScheduler;
@@ -52,6 +54,10 @@ public class FullRoomDrawHandler extends AbstractEventHandler<FullRoomDraw> {
private Debugging debugging;
@InjectDependency
private RoomTaskScheduler roomTaskScheduler;
@InjectDependency
private UIClickHandlerRepository uiClickHandlerRepository;
@InjectDependency
private Inventory inventory;
public FullRoomDrawHandler(DependencyManager dm) {
super(dm);
@@ -79,7 +85,8 @@ public class FullRoomDrawHandler extends AbstractEventHandler<FullRoomDraw> {
RerenderUtils.rerenderPart(0, width - 1, 0, height - 1, startX, startY, currentRoom, room, player, playerTexture, screenBuffer, resourceManager, debugging);
if (event.isFullRerender()) {
InventoryRerenderHandler.renderInventoryRerender(dm);
inventory.renderInventoryRerender();
uiClickHandlerRepository.registerGlobalHandler(inventory);
}
partsToRerender.add(new RerenderScreen.ScreenPart(

View File

@@ -1,114 +1,33 @@
package cz.jzitnik.events.handlers;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.TextColor;
import cz.jzitnik.annotations.EventHandler;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.events.InventoryRerender;
import cz.jzitnik.events.RerenderScreen;
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.ui.Inventory;
import cz.jzitnik.utils.DependencyManager;
import cz.jzitnik.utils.RerenderUtils;
import cz.jzitnik.utils.StateManager;
import cz.jzitnik.utils.events.AbstractEventHandler;
import cz.jzitnik.utils.events.EventManager;
import java.awt.image.BufferedImage;
@EventHandler(InventoryRerender.class)
public class InventoryRerenderHandler extends AbstractEventHandler<InventoryRerender> {
@InjectDependency
private EventManager eventManager;
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
@InjectDependency
private Inventory inventory;
public InventoryRerenderHandler(DependencyManager dm) {
super(dm);
}
public record RenderResponse(int offsetX, int offsetY) {}
public static RenderResponse 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());
}
return new RenderResponse(OFFSET_X, OFFSET_Y);
}
@Override
public void handle(InventoryRerender event) {
RenderResponse renderResponse = renderInventoryRerender(dm);
inventory.renderInventoryRerender();
eventManager.emitEvent(new RerenderScreen(new RerenderScreen.ScreenPart(
new TerminalPosition(renderResponse.offsetX, renderResponse.offsetY),
new TerminalPosition(renderResponse.offsetX + INVENTORY_WIDTH, renderResponse.offsetY + INVENTORY_HEIGHT)
new TerminalPosition(inventory.getOffsetX(), inventory.getOffsetY()),
new TerminalPosition(inventory.getOffsetX() + Inventory.INVENTORY_WIDTH, inventory.getOffsetY() + Inventory.INVENTORY_HEIGHT)
)));
}
}

View File

@@ -53,7 +53,13 @@ public class MouseActionEventHandler extends AbstractEventHandler<MouseAction> {
}
switch (event.getActionType()) {
case MOVE -> eventManager.emitEvent(new MouseMoveEvent(event));
case MOVE -> {
boolean registered = uiClickHandlerRepository.handleMove(event);
if (!registered) {
eventManager.emitEvent(new MouseMoveEvent(event));
}
}
case CLICK_RELEASE -> {
boolean clicked = uiClickHandlerRepository.handleClick(event);

View File

@@ -18,7 +18,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.utils.Grid;
import cz.jzitnik.ui.pixels.ColoredPixel;
import cz.jzitnik.ui.pixels.Empty;
import cz.jzitnik.ui.pixels.Pixel;
@@ -92,6 +92,7 @@ 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)
);
}
@@ -227,7 +228,7 @@ public final class Chest extends GameObject implements UIClickHandler {
.map(GameItem::getTexture)
.toArray(BufferedImage[]::new);
Pixel[][] uiPixels = grid.render(textures);
Pixel[][] uiPixels = grid.render(textures, -1);
for (int y = 0; y < grid.getHeight(); y++) {
for (int x = 0; x < grid.getWidth(); x++) {

View File

@@ -0,0 +1,11 @@
package cz.jzitnik.game.objects;
import cz.jzitnik.events.MouseAction;
public interface GlobalUIClickHandler {
boolean handleClick(MouseAction mouseAction);
default boolean handleMove(MouseAction ignoredMouseAction) {
return false;
}
}

View File

@@ -4,4 +4,6 @@ import cz.jzitnik.events.MouseAction;
public interface UIClickHandler {
void handleClick(MouseAction mouseAction);
default void handleMove(MouseAction ignoredMouseAction) {}
}

View File

@@ -0,0 +1,143 @@
package cz.jzitnik.ui;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.TextColor;
import cz.jzitnik.annotations.Dependency;
import cz.jzitnik.annotations.injectors.InjectDependency;
import cz.jzitnik.annotations.injectors.InjectState;
import cz.jzitnik.events.InventoryRerender;
import cz.jzitnik.events.MouseAction;
import cz.jzitnik.game.GameState;
import cz.jzitnik.game.ResourceManager;
import cz.jzitnik.game.items.GameItem;
import cz.jzitnik.game.objects.GlobalUIClickHandler;
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.ui.utils.Grid;
import cz.jzitnik.utils.RerenderUtils;
import cz.jzitnik.utils.events.EventManager;
import lombok.Getter;
import java.awt.image.BufferedImage;
@Dependency
public class Inventory implements GlobalUIClickHandler {
private static final int ITEMS_X = 3;
private 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
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 Pixel BACKGROUND_COLOR_SELECTED =
new ColoredPixel(new TextColor.RGB(186, 186, 186));
private static final int ITEM_SLOT_SIZE = ITEM_SIZE + ITEM_PADDING * 2;
public static final int INVENTORY_WIDTH =
OUTER_BORDER_WIDTH * 2 +
(ITEMS_X * (ITEM_SLOT_SIZE + INNER_BORDER_WIDTH) - INNER_BORDER_WIDTH);
public 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
private static final Grid grid = new Grid(
ITEMS_X,
ITEMS_Y,
OUTER_BORDER_WIDTH,
INNER_BORDER_WIDTH,
ITEM_SIZE,
ITEM_PADDING,
BORDER_COLOR,
BACKGROUND_COLOR,
BACKGROUND_COLOR_SELECTED
);
int selectedIndex = -1;
@InjectDependency
private ResourceManager resourceManager;
@InjectState
private GameState gameState;
@InjectState
private TerminalState terminalState;
@InjectState
private ScreenBuffer screenBuffer;
@InjectDependency
private EventManager eventManager;
@Getter
private int offsetX;
@Getter
private int offsetY;
private void calculateOffset() {
BufferedImage room = resourceManager.getResource(ResourceManager.Resource.ROOM1);
TerminalSize terminalSize = terminalState.getTerminalScreen().getTerminalSize();
var roomStart = RerenderUtils.getStart(room, terminalSize);
int maxX = roomStart.getX() - 1;
offsetY = (((terminalSize.getRows() - 1) / 2) - (REAL_INVENTORY_HEIGHT / 2)) * 2;
offsetX = (maxX / 2) - (INVENTORY_WIDTH / 2);
}
@Override
public boolean handleClick(MouseAction mouseAction) {
TerminalPosition terminalPosition = calculateActualCords(mouseAction.getPosition());
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());
// TODO: Clicking on items
return true;
}
@Override
public boolean handleMove(MouseAction mouseAction) {
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;
eventManager.emitEvent(new InventoryRerender());
}
return false;
}
int newSelectedIndex = grid.getItemIndexAt(terminalPosition.getColumn(), terminalPosition.getRow());
if (newSelectedIndex != selectedIndex) {
selectedIndex = newSelectedIndex;
eventManager.emitEvent(new InventoryRerender());
}
return true;
}
private TerminalPosition calculateActualCords(TerminalPosition position) {
return new TerminalPosition(position.getColumn() - offsetX, position.getRow() * 2 - offsetY);
}
public void renderInventoryRerender() {
calculateOffset();
var inventory = gameState.getPlayer().getInventory();
var buffer = screenBuffer.getRenderedBuffer();
BufferedImage[] textures = inventory.stream()
.map(GameItem::getTexture)
.toArray(BufferedImage[]::new);
Pixel[][] internalBuffer = grid.render(textures, selectedIndex);
for (int y = 0; y < grid.getHeight(); y++) {
System.arraycopy(internalBuffer[y], 0, buffer[y + offsetY], offsetX, grid.getWidth());
}
}
}

View File

@@ -1,4 +1,4 @@
package cz.jzitnik.ui;
package cz.jzitnik.ui.utils;
import cz.jzitnik.ui.pixels.ColoredPixel;
import cz.jzitnik.ui.pixels.Pixel;
@@ -14,6 +14,7 @@ public class Grid {
private final Pixel borderColor;
private final Pixel backgroundColor;
private final Pixel backgroundColorSelected;
private final int itemSlotSize;
private final int cellSize;
@@ -28,7 +29,8 @@ public class Grid {
int itemSize,
int itemPadding,
Pixel borderColor,
Pixel backgroundColor
Pixel backgroundColor,
Pixel backgroundColorSelected
) {
this.itemsX = itemsX;
this.itemsY = itemsY;
@@ -37,6 +39,7 @@ public class Grid {
this.itemPadding = itemPadding;
this.borderColor = borderColor;
this.backgroundColor = backgroundColor;
this.backgroundColorSelected = backgroundColorSelected;
this.itemSlotSize = itemSize + itemPadding * 2;
this.cellSize = itemSlotSize + innerBorderWidth;
@@ -50,10 +53,9 @@ public class Grid {
(itemsY * (itemSlotSize + innerBorderWidth) - innerBorderWidth);
}
public Pixel[][] render(BufferedImage[] textures) {
public Pixel[][] render(BufferedImage[] textures, int selectedIndex) {
Pixel[][] buffer = new Pixel[gridHeight][gridWidth];
// Background + borders
for (int y = 0; y < gridHeight; y++) {
for (int x = 0; x < gridWidth; x++) {
@@ -77,7 +79,6 @@ public class Grid {
}
}
// Items
int maxItems = Math.min(textures.length, itemsX * itemsY);
for (int index = 0; index < maxItems; index++) {
@@ -87,29 +88,39 @@ public class Grid {
int itemX = index % itemsX;
int itemY = index / itemsX;
int baseX =
outerBorderWidth +
itemX * cellSize +
itemPadding;
int slotX = outerBorderWidth + itemX * cellSize;
int slotY = outerBorderWidth + itemY * cellSize;
int baseY =
outerBorderWidth +
itemY * cellSize +
itemPadding;
if (index == selectedIndex) {
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;
}
}
}
}
int textureBaseX = slotX + itemPadding;
int textureBaseY = slotY + 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;
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));
if (textureBaseY + y < gridHeight && textureBaseX + x < gridWidth) {
buffer[textureBaseY + y][textureBaseX + x] =
new ColoredPixel(new com.googlecode.lanterna.TextColor.RGB(r, g, b));
}
}
}
}

View File

@@ -7,17 +7,28 @@ import cz.jzitnik.events.MouseAction;
import cz.jzitnik.events.RerenderScreen;
import cz.jzitnik.game.GameRoom;
import cz.jzitnik.game.GameState;
import cz.jzitnik.game.objects.GlobalUIClickHandler;
import cz.jzitnik.game.objects.UIClickHandler;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Dependency
public class UIClickHandlerRepository {
private final Map<GameRoom, Map<RerenderScreen.ScreenPart, UIClickHandler>> roomSpecificHandlers = new ConcurrentHashMap<>();
private final Set<GlobalUIClickHandler> globalHandlers = ConcurrentHashMap.newKeySet();
@InjectState
private GameState gameState;
private final Map<GameRoom, Map<RerenderScreen.ScreenPart, UIClickHandler>> roomSpecificHandlers = new ConcurrentHashMap<>();
public int registerGlobalHandler(GlobalUIClickHandler handler) {
globalHandlers.add(handler);
return handler.hashCode();
}
public void removeGlobalHandler(int hashCode) {
globalHandlers.removeIf(handler -> handler.hashCode() == hashCode);
}
public int registerCurrentRoomHandler(RerenderScreen.ScreenPart screenPart, UIClickHandler uiClickHandler) {
GameRoom currentRoom = gameState.getCurrentRoom();
@@ -35,19 +46,47 @@ public class UIClickHandlerRepository {
}
public boolean handleClick(MouseAction mouseAction) {
if (!roomSpecificHandlers.containsKey(gameState.getCurrentRoom())) {
return false;
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.handleClick(mouseAction);
return true;
}
}
}
Map<RerenderScreen.ScreenPart, UIClickHandler> handlers = roomSpecificHandlers.get(gameState.getCurrentRoom());
for (var handler : globalHandlers) {
if (handler.handleClick(mouseAction)) {
return true;
}
}
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);
return false;
}
if (part.isWithin(position)) {
uiClickHandler.handleClick(mouseAction);
public boolean handleMove(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.handleMove(mouseAction);
return true;
}
}
}
for (var handler : globalHandlers) {
if (handler.handleMove(mouseAction)) {
return true;
}
}
@@ -60,7 +99,7 @@ public class UIClickHandlerRepository {
if (!roomSpecificHandlers.containsKey(currentRoom)) return;
Map<RerenderScreen.ScreenPart, UIClickHandler> handlers = roomSpecificHandlers.get(currentRoom);
for (var key: handlers.keySet()) {
for (var key : handlers.keySet()) {
if (key.hashCode() == screenPartHashCode) {
handlers.remove(key, uiClickHandler);
}