diff --git a/src/main/java/com/trunksbomb/trade/client/TradeScreen.java b/src/main/java/com/trunksbomb/trade/client/TradeScreen.java index b29704e..fd0b69f 100644 --- a/src/main/java/com/trunksbomb/trade/client/TradeScreen.java +++ b/src/main/java/com/trunksbomb/trade/client/TradeScreen.java @@ -6,6 +6,8 @@ import com.trunksbomb.trade.mod.trade.DebugControlAction; import com.trunksbomb.trade.mod.trade.TradeAction; import com.trunksbomb.trade.mod.trade.TradeStage; import com.trunksbomb.trade.mod.trade.TradeView; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; @@ -30,6 +32,8 @@ public class TradeScreen extends AbstractContainerScreen private static final int OFFER_START_Y = 18; private static final int INVENTORY_START_X = 62; private static final int INVENTORY_START_Y = 139; + private static final int BANNER_X = 58; + private static final int BANNER_Y = 6; private static final int CENTER_COLUMN_X = 115; private static final int STATUS_LABEL_Y = 54; private static final int ACCEPT_BUTTON_Y = 74; @@ -40,10 +44,7 @@ public class TradeScreen extends AbstractContainerScreen private static final int DEBUG_BUTTON_WIDTH = 86; private static final int DEBUG_BUTTON_HEIGHT = 20; private static final int DEBUG_BUTTON_GAP = 4; - private static final int CONFIRM_PANEL_X = 89; - private static final int CONFIRM_PANEL_Y = 77; - private static final int CONFIRM_PANEL_WIDTH = 106; - private static final int CONFIRM_PANEL_HEIGHT = 64; + private ContextMenu contextMenu; public TradeScreen(TradeMenu menu, Inventory inventory, Component title) { super(menu, inventory, title); @@ -64,13 +65,13 @@ public class TradeScreen extends AbstractContainerScreen @Override protected void init() { super.init(); - titleLabelX = LEFT_OFFER_START_X; - titleLabelY = 6; + titleLabelX = BANNER_X; + titleLabelY = BANNER_Y; - addRenderableWidget(Button.builder(Component.literal("Accept"), button -> sendAction(TradeAction.ACCEPT, -1, false)) + addRenderableWidget(Button.builder(Component.literal("Accept"), button -> sendAction(TradeAction.ACCEPT, -1, 1)) .bounds(leftPos + CENTER_COLUMN_X + 3, topPos + ACCEPT_BUTTON_Y, ACTION_BUTTON_WIDTH, ACTION_BUTTON_HEIGHT) .build()); - addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> sendAction(TradeAction.DECLINE, -1, false)) + addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> sendAction(TradeAction.DECLINE, -1, 1)) .bounds(leftPos + CENTER_COLUMN_X + 3, topPos + CANCEL_BUTTON_Y, ACTION_BUTTON_WIDTH, ACTION_BUTTON_HEIGHT) .build()); @@ -103,7 +104,7 @@ public class TradeScreen extends AbstractContainerScreen @Override public void onClose() { - sendAction(TradeAction.CLOSE, -1, false); + sendAction(TradeAction.CLOSE, -1, 1); if (minecraft != null) { minecraft.setScreen(null); } @@ -111,12 +112,23 @@ public class TradeScreen extends AbstractContainerScreen @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (contextMenu != null) { + if (handleContextMenuClick(mouseX, mouseY)) { + return true; + } + contextMenu = null; + } + if (menu.view().stage() == TradeStage.CONFIRMING) { return super.mouseClicked(mouseX, mouseY, button); } if (hoveredSlot instanceof SelfOfferSlot selfOfferSlot) { - sendAction(TradeAction.REMOVE_ITEM, selfOfferSlot.offerIndex(), button == 1); + if (button == 1) { + openContextMenu(mouseX, mouseY, TradeAction.REMOVE_ITEM, selfOfferSlot.offerIndex(), hoveredSlot.getItem(), "Remove"); + } else { + sendAction(TradeAction.REMOVE_ITEM, selfOfferSlot.offerIndex(), 1); + } return true; } @@ -125,7 +137,11 @@ public class TradeScreen extends AbstractContainerScreen } if (hoveredSlot != null && hoveredSlot.container == menu.playerInventory()) { - sendAction(TradeAction.ADD_ITEM, hoveredSlot.getSlotIndex(), button == 1); + if (button == 1) { + openContextMenu(mouseX, mouseY, TradeAction.ADD_ITEM, hoveredSlot.getSlotIndex(), hoveredSlot.getItem(), "Trade"); + } else { + sendAction(TradeAction.ADD_ITEM, hoveredSlot.getSlotIndex(), 1); + } return true; } @@ -135,37 +151,34 @@ public class TradeScreen extends AbstractContainerScreen @Override protected void renderBg(GuiGraphics guiGraphics, float partialTick, int mouseX, int mouseY) { guiGraphics.blit(TEXTURE, leftPos, topPos, 0, 0, imageWidth, imageHeight, imageWidth, imageHeight); - if (menu.view().stage() == TradeStage.CONFIRMING) { - renderConfirmationOverlay(guiGraphics); - } } @Override protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { - guiGraphics.drawString(font, menu.view().selfName(), titleLabelX, titleLabelY, 0x404040, false); - guiGraphics.drawString(font, menu.view().otherName(), RIGHT_OFFER_START_X, 6, 0x404040, false); + guiGraphics.drawString(font, "Trading with " + menu.view().otherName(), titleLabelX, titleLabelY, 0x404040, false); guiGraphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0x404040, false); - if (menu.view().otherAccepted()) { + if (menu.view().stage() == TradeStage.CONFIRMING) { + drawScaledCenteredColumnText(guiGraphics, "Are you", STATUS_LABEL_Y, 0xB02020, 0.7F); + drawScaledCenteredColumnText(guiGraphics, "sure you want", STATUS_LABEL_Y + 7, 0xB02020, 0.7F); + drawScaledCenteredColumnText(guiGraphics, "to trade?", STATUS_LABEL_Y + 14, 0xB02020, 0.7F); + } else if (menu.view().otherAccepted()) { drawScaledCenteredColumnText(guiGraphics, "Other player", STATUS_LABEL_Y, 0x2E7D32, 0.7F); drawScaledCenteredColumnText(guiGraphics, "has accepted", STATUS_LABEL_Y + 7, 0x2E7D32, 0.7F); } - if (menu.view().stage() == TradeStage.CONFIRMING) { - drawCenteredColumnText(guiGraphics, "FINAL", 16, 0x7A4F00); - drawCenteredColumnText(guiGraphics, "CONFIRM", 28, 0x7A4F00); - } } @Override public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { super.render(guiGraphics, mouseX, mouseY, partialTick); + renderContextMenu(guiGraphics, mouseX, mouseY); renderTooltip(guiGraphics, mouseX, mouseY); } - private void sendAction(TradeAction action, int slot, boolean secondary) { + private void sendAction(TradeAction action, int slot, int amount) { if (minecraft == null || minecraft.getConnection() == null) { return; } - PacketDistributor.sendToServer(new TradeActionPayload(menu.view().sessionId(), action, slot, secondary)); + PacketDistributor.sendToServer(new TradeActionPayload(menu.view().sessionId(), action, slot, amount)); } private void sendDebug(DebugControlAction action) { @@ -175,17 +188,6 @@ public class TradeScreen extends AbstractContainerScreen PacketDistributor.sendToServer(new DebugTradeControlPayload(menu.view().sessionId(), action, "")); } - private void renderConfirmationOverlay(GuiGraphics guiGraphics) { - int panelLeft = leftPos + CONFIRM_PANEL_X; - int panelTop = topPos + CONFIRM_PANEL_Y; - guiGraphics.fill(leftPos, topPos, leftPos + imageWidth, topPos + imageHeight, 0x22000000); - guiGraphics.fill(panelLeft, panelTop, panelLeft + CONFIRM_PANEL_WIDTH, panelTop + CONFIRM_PANEL_HEIGHT, 0xDDF7D48C); - guiGraphics.fill(panelLeft + 1, panelTop + 1, panelLeft + CONFIRM_PANEL_WIDTH - 1, panelTop + CONFIRM_PANEL_HEIGHT - 1, 0xAAFFF4D6); - guiGraphics.drawString(font, Component.literal("Confirm Trade"), panelLeft + 11, panelTop + 9, 0x5B3A00, false); - guiGraphics.drawString(font, Component.literal("Review items"), panelLeft + 18, panelTop + 24, 0x5B3A00, false); - guiGraphics.drawString(font, Component.literal("No more changes"), panelLeft + 9, panelTop + 36, 0x5B3A00, false); - } - private void drawCenteredColumnText(GuiGraphics guiGraphics, String text, int y, int color) { int textX = CENTER_COLUMN_X + (ACTION_BUTTON_WIDTH / 2) - (font.width(text) / 2); guiGraphics.drawString(font, text, textX, y, color, false); @@ -202,6 +204,115 @@ public class TradeScreen extends AbstractContainerScreen pose.popPose(); } + private void openContextMenu(double mouseX, double mouseY, TradeAction action, int slot, ItemStack stack, String verb) { + if (stack.isEmpty()) { + return; + } + + List options = new ArrayList<>(); + for (int amount : contextAmounts(stack.getMaxStackSize())) { + options.add(new ContextOption(verb + " " + amount, amount)); + } + options.add(new ContextOption(verb + " X", -1)); + contextMenu = new ContextMenu(action, slot, options, (int) mouseX, (int) mouseY); + } + + private boolean handleContextMenuClick(double mouseX, double mouseY) { + if (contextMenu == null) { + return false; + } + + int menuX = contextMenuX(); + int menuY = contextMenuY(); + int menuWidth = contextMenuWidth(); + int optionHeight = 12; + int menuHeight = contextMenu.options().size() * optionHeight + 4; + if (mouseX < menuX || mouseX >= menuX + menuWidth || mouseY < menuY || mouseY >= menuY + menuHeight) { + return false; + } + + int index = ((int) mouseY - menuY - 2) / optionHeight; + if (index < 0 || index >= contextMenu.options().size()) { + return true; + } + + ContextOption option = contextMenu.options().get(index); + if (option.amount() < 0) { + if (minecraft != null && minecraft.player != null) { + minecraft.player.displayClientMessage(Component.literal(option.label() + " is not implemented yet."), true); + } + } else { + sendAction(contextMenu.action(), contextMenu.slot(), option.amount()); + } + + contextMenu = null; + return true; + } + + private void renderContextMenu(GuiGraphics guiGraphics, int mouseX, int mouseY) { + if (contextMenu == null) { + return; + } + + var pose = guiGraphics.pose(); + pose.pushPose(); + pose.translate(0.0F, 0.0F, 400.0F); + + int menuX = contextMenuX(); + int menuY = contextMenuY(); + int menuWidth = contextMenuWidth(); + int optionHeight = 12; + int menuHeight = contextMenu.options().size() * optionHeight + 4; + guiGraphics.fill(menuX, menuY, menuX + menuWidth, menuY + menuHeight, 0xF0101010); + guiGraphics.fill(menuX + 1, menuY + 1, menuX + menuWidth - 1, menuY + menuHeight - 1, 0xFF2B2B2B); + + for (int i = 0; i < contextMenu.options().size(); i++) { + int optionY = menuY + 2 + i * optionHeight; + boolean hovered = mouseX >= menuX + 2 && mouseX < menuX + menuWidth - 2 && mouseY >= optionY && mouseY < optionY + optionHeight; + if (hovered) { + guiGraphics.fill(menuX + 2, optionY, menuX + menuWidth - 2, optionY + optionHeight, 0xFF5B6EE1); + } + guiGraphics.drawString(font, contextMenu.options().get(i).label(), menuX + 6, optionY + 2, 0xFFFFFF, false); + } + + pose.popPose(); + } + + private int contextMenuX() { + int width = contextMenuWidth(); + return Math.min(contextMenu.mouseX(), this.width - width - 4); + } + + private int contextMenuY() { + int height = contextMenu.options().size() * 12 + 4; + return Math.min(contextMenu.mouseY(), this.height - height - 4); + } + + private int contextMenuWidth() { + int width = 0; + for (ContextOption option : contextMenu.options()) { + width = Math.max(width, font.width(option.label())); + } + return width + 12; + } + + private static int[] contextAmounts(int maxStackSize) { + if (maxStackSize == 1) { + return new int[] {1}; + } + if (maxStackSize == 16) { + return new int[] {1, 2, 4, 8, 16}; + } + if (maxStackSize == 32) { + return new int[] {1, 4, 8, 16, 32}; + } + return new int[] {1, 8, 16, 32, 64}; + } + + private record ContextMenu(TradeAction action, int slot, List options, int mouseX, int mouseY) {} + + private record ContextOption(String label, int amount) {} + public static class TradeMenu extends AbstractContainerMenu { private static final int OFFER_COLUMNS = 6; private static final int OFFER_ROWS = 6; diff --git a/src/main/java/com/trunksbomb/trade/network/TradeActionPayload.java b/src/main/java/com/trunksbomb/trade/network/TradeActionPayload.java index 0b3c974..3be605d 100644 --- a/src/main/java/com/trunksbomb/trade/network/TradeActionPayload.java +++ b/src/main/java/com/trunksbomb/trade/network/TradeActionPayload.java @@ -8,7 +8,7 @@ import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.resources.ResourceLocation; -public record TradeActionPayload(UUID sessionId, TradeAction action, int slot, boolean secondary) implements CustomPacketPayload { +public record TradeActionPayload(UUID sessionId, TradeAction action, int slot, int amount) implements CustomPacketPayload { public static final Type TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(TradeMod.MODID, "trade_action")); public static final StreamCodec STREAM_CODEC = new StreamCodec<>() { @Override @@ -16,12 +16,12 @@ public record TradeActionPayload(UUID sessionId, TradeAction action, int slot, b buf.writeUUID(value.sessionId); buf.writeEnum(value.action); buf.writeVarInt(value.slot); - buf.writeBoolean(value.secondary); + buf.writeVarInt(value.amount); } @Override public TradeActionPayload decode(RegistryFriendlyByteBuf buf) { - return new TradeActionPayload(buf.readUUID(), buf.readEnum(TradeAction.class), buf.readVarInt(), buf.readBoolean()); + return new TradeActionPayload(buf.readUUID(), buf.readEnum(TradeAction.class), buf.readVarInt(), buf.readVarInt()); } }; diff --git a/src/main/java/com/trunksbomb/trade/network/TradeNetworking.java b/src/main/java/com/trunksbomb/trade/network/TradeNetworking.java index 61e21be..1c093e6 100644 --- a/src/main/java/com/trunksbomb/trade/network/TradeNetworking.java +++ b/src/main/java/com/trunksbomb/trade/network/TradeNetworking.java @@ -61,7 +61,7 @@ public final class TradeNetworking { private static void handleTradeActionServer(TradeActionPayload payload, IPayloadContext context) { if (context.player() instanceof ServerPlayer player) { - TradeManager.get(player.server).handleAction(player, payload.sessionId(), payload.action(), payload.slot(), payload.secondary()); + TradeManager.get(player.server).handleAction(player, payload.sessionId(), payload.action(), payload.slot(), payload.amount()); } } diff --git a/src/main/java/com/trunksbomb/trade/trade/DebugTradeSession.java b/src/main/java/com/trunksbomb/trade/trade/DebugTradeSession.java index e7ffe39..f16ec93 100644 --- a/src/main/java/com/trunksbomb/trade/trade/DebugTradeSession.java +++ b/src/main/java/com/trunksbomb/trade/trade/DebugTradeSession.java @@ -48,7 +48,7 @@ public class DebugTradeSession { PacketDistributor.sendToPlayer(player, new TradeClosePayload(id, reason)); } - public boolean addFromInventory(int inventorySlot, boolean singleItem) { + public boolean addFromInventory(int inventorySlot, int amount) { if (stage != TradeStage.OFFERING || inventorySlot < 0 || inventorySlot >= INVENTORY_SLOT_COUNT) { return false; } @@ -59,12 +59,13 @@ public class DebugTradeSession { return false; } - ItemStack moving = sourceStack.copyWithCount(singleItem ? 1 : sourceStack.getCount()); + int moveAmount = Math.max(1, Math.min(amount, sourceStack.getCount())); + ItemStack moving = sourceStack.copyWithCount(moveAmount); if (!insertStack(selfOffer, moving)) { return false; } - sourceStack.shrink(singleItem ? 1 : sourceStack.getCount()); + sourceStack.shrink(moveAmount); if (sourceStack.isEmpty()) { inventory.setItem(inventorySlot, ItemStack.EMPTY); } @@ -73,7 +74,7 @@ public class DebugTradeSession { return true; } - public boolean removeFromOffer(int offerSlot, boolean singleItem) { + public boolean removeFromOffer(int offerSlot, int amount) { if (stage != TradeStage.OFFERING || offerSlot < 0 || offerSlot >= OFFER_SLOT_COUNT) { return false; } @@ -84,7 +85,8 @@ public class DebugTradeSession { } List workingInventory = inventorySnapshot(); - ItemStack returned = offered.copyWithCount(singleItem ? 1 : offered.getCount()); + int moveAmount = Math.max(1, Math.min(amount, offered.getCount())); + ItemStack returned = offered.copyWithCount(moveAmount); if (!insertStack(workingInventory, returned)) { return false; } @@ -95,9 +97,9 @@ public class DebugTradeSession { } inventory.setChanged(); - if (singleItem && offered.getCount() > 1) { + if (moveAmount < offered.getCount()) { ItemStack updated = offered.copy(); - updated.shrink(1); + updated.shrink(moveAmount); selfOffer.set(offerSlot, updated); } else { selfOffer.set(offerSlot, ItemStack.EMPTY); diff --git a/src/main/java/com/trunksbomb/trade/trade/TradeManager.java b/src/main/java/com/trunksbomb/trade/trade/TradeManager.java index 309684b..b0a6e9a 100644 --- a/src/main/java/com/trunksbomb/trade/trade/TradeManager.java +++ b/src/main/java/com/trunksbomb/trade/trade/TradeManager.java @@ -37,24 +37,24 @@ public class TradeManager { return true; } - public void handleAction(ServerPlayer player, UUID sessionId, TradeAction action, int slot, boolean secondary) { + public void handleAction(ServerPlayer player, UUID sessionId, TradeAction action, int slot, int amount) { TradeSession session = sessionsById.get(sessionId); if (session != null && session.involves(player)) { - handleRealAction(player, session, action, slot, secondary); + handleRealAction(player, session, action, slot, amount); return; } DebugTradeSession debugSession = debugSessionsById.get(sessionId); if (debugSession != null && debugSession.player() == player) { - handleDebugAction(debugSession, action, slot, secondary); + handleDebugAction(debugSession, action, slot, amount); return; } } - private void handleRealAction(ServerPlayer player, TradeSession session, TradeAction action, int slot, boolean secondary) { + private void handleRealAction(ServerPlayer player, TradeSession session, TradeAction action, int slot, int amount) { boolean changed = switch (action) { - case ADD_ITEM -> session.addFromInventory(player, slot, secondary); - case REMOVE_ITEM -> session.removeFromOffer(player, slot, secondary); + case ADD_ITEM -> session.addFromInventory(player, slot, amount); + case REMOVE_ITEM -> session.removeFromOffer(player, slot, amount); case ACCEPT -> { session.accept(player); yield true; @@ -87,10 +87,10 @@ public class TradeManager { } } - private void handleDebugAction(DebugTradeSession session, TradeAction action, int slot, boolean secondary) { + private void handleDebugAction(DebugTradeSession session, TradeAction action, int slot, int amount) { boolean changed = switch (action) { - case ADD_ITEM -> session.addFromInventory(slot, secondary); - case REMOVE_ITEM -> session.removeFromOffer(slot, secondary); + case ADD_ITEM -> session.addFromInventory(slot, amount); + case REMOVE_ITEM -> session.removeFromOffer(slot, amount); case ACCEPT -> { session.acceptSelf(); yield true; diff --git a/src/main/java/com/trunksbomb/trade/trade/TradeSession.java b/src/main/java/com/trunksbomb/trade/trade/TradeSession.java index 7976a53..af77435 100644 --- a/src/main/java/com/trunksbomb/trade/trade/TradeSession.java +++ b/src/main/java/com/trunksbomb/trade/trade/TradeSession.java @@ -59,7 +59,7 @@ public class TradeSession { PacketDistributor.sendToPlayer(second, new TradeClosePayload(id, reason)); } - public boolean addFromInventory(ServerPlayer player, int inventorySlot, boolean singleItem) { + public boolean addFromInventory(ServerPlayer player, int inventorySlot, int amount) { if (stage != TradeStage.OFFERING || inventorySlot < 0 || inventorySlot >= INVENTORY_SLOT_COUNT) { return false; } @@ -71,12 +71,13 @@ public class TradeSession { return false; } - ItemStack moving = sourceStack.copyWithCount(singleItem ? 1 : sourceStack.getCount()); + int moveAmount = Math.max(1, Math.min(amount, sourceStack.getCount())); + ItemStack moving = sourceStack.copyWithCount(moveAmount); if (!insertStack(offer, moving)) { return false; } - sourceStack.shrink(singleItem ? 1 : sourceStack.getCount()); + sourceStack.shrink(moveAmount); if (sourceStack.isEmpty()) { inventory.setItem(inventorySlot, ItemStack.EMPTY); } @@ -85,7 +86,7 @@ public class TradeSession { return true; } - public boolean removeFromOffer(ServerPlayer player, int offerSlot, boolean singleItem) { + public boolean removeFromOffer(ServerPlayer player, int offerSlot, int amount) { if (stage != TradeStage.OFFERING || offerSlot < 0 || offerSlot >= OFFER_SLOT_COUNT) { return false; } @@ -98,15 +99,16 @@ public class TradeSession { Inventory inventory = player.getInventory(); List workingInventory = inventorySnapshot(player); - ItemStack returned = offered.copyWithCount(singleItem ? 1 : offered.getCount()); + int moveAmount = Math.max(1, Math.min(amount, offered.getCount())); + ItemStack returned = offered.copyWithCount(moveAmount); if (!insertStack(workingInventory, returned)) { return false; } applyInventory(player, workingInventory); - if (singleItem && offered.getCount() > 1) { + if (moveAmount < offered.getCount()) { ItemStack updated = offered.copy(); - updated.shrink(1); + updated.shrink(moveAmount); offer.set(offerSlot, updated); } else { offer.set(offerSlot, ItemStack.EMPTY);