From b3fb5225211c8d2218d0281bc0bacc918686f211 Mon Sep 17 00:00:00 2001 From: Face o' Cat Date: Wed, 4 Nov 2026 18:46:29 +0200 Subject: [PATCH] Show carried trade costs, keep experience on the player, and unlock the market recipe. The window, barrel, and recipe book now match how the market is meant to be used. --- CHANGELOG.md | 10 +- .../gametest/FabricMarketGameTests.java | 5 + .../client/MarketScreenLogic.java | 125 ++++++++++--- .../client/VillagerMarketScreen.java | 174 ++++++++++++++---- .../gametest/MarketGameTests.java | 38 +++- .../menu/VillagerMarketMenu.java | 22 +++ .../network/MarketPayloads.java | 1 + .../villagermarket/trade/TradeExecutor.java | 21 ++- .../assets/villagermarket/lang/en_us.json | 4 +- .../models/block/villager_market.json | 117 +++++++++++- .../villagermarket/textures/block/face.png | Bin 0 -> 237 bytes .../villagermarket/textures/block/lid.png | Bin 0 -> 297 bytes .../textures/blocks/crate-villager.png | Bin 7631 -> 0 bytes .../villagermarket/textures/blocks/crate.png | Bin 9476 -> 0 bytes .../recipes/misc/villager_market.json | 43 +++++ .../client/VillagerMarketScreen.java | 172 +++++++++++++---- .../recipes/misc/villager_market.json | 43 +++++ 17 files changed, 655 insertions(+), 120 deletions(-) create mode 100644 src/main/resources/assets/villagermarket/textures/block/face.png create mode 100644 src/main/resources/assets/villagermarket/textures/block/lid.png delete mode 100644 src/main/resources/assets/villagermarket/textures/blocks/crate-villager.png delete mode 100644 src/main/resources/assets/villagermarket/textures/blocks/crate.png create mode 100644 src/main/resources/data/villagermarket/advancement/recipes/misc/villager_market.json create mode 100644 versions/1.21.1/src/main/resources/data/villagermarket/advancement/recipes/misc/villager_market.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 885f03c..f2e4bf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,15 @@ After publishing, the action prepares the next development version and archives ## Unreleased -- Place a Villager Market (planks around an emerald block) and trade with every villager within 48 blocks, 16 up and 16 down. Offers are grouped by the villager's name, or by profession. Show All lists offers you cannot pay for. x1 buys once, and xN buys as many as you can pay for and the offer still has left. +- Under the trades, [available / max until lock] explains the two numbers on each offer. +- Selecting an offer shows its inputs beside the buy buttons, each with how many of that item you are carrying. Those slots cannot be clicked. +- Buying updates that carried count, and how many times you can still pay. +- Experience from a purchase appears on you. It no longer drops on the villager. +- The trade list shows a scrollbar when there are more offers than fit. Drag it, or use the mouse wheel. +- The Villager Market recipe appears in the recipe book once you have planks or an emerald block. +- The Villager Market is a barrel. Each side has a villager face standing one pixel out, with the nose one pixel farther, and an emerald in the middle of the top and the bottom. +- The market window uses the vanilla inventory frame, buttons, and trade arrows. +- Place a Villager Market (planks around an emerald block) and trade with every villager within 48 blocks, 16 up and 16 down. Offers are grouped by the villager's name, or by profession. Show All lists offers you cannot pay for. Turn it off and the button reads Available, listing only offers you can pay. x1 buys once, and xN buys as many as you can pay for and the offer still has left. ## 1.0.2 diff --git a/fabric/src/main/java/net/modcrafters/minecraft/villagermarket/fabric/gametest/FabricMarketGameTests.java b/fabric/src/main/java/net/modcrafters/minecraft/villagermarket/fabric/gametest/FabricMarketGameTests.java index 14c0582..f184667 100644 --- a/fabric/src/main/java/net/modcrafters/minecraft/villagermarket/fabric/gametest/FabricMarketGameTests.java +++ b/fabric/src/main/java/net/modcrafters/minecraft/villagermarket/fabric/gametest/FabricMarketGameTests.java @@ -24,4 +24,9 @@ public void villagerOutsideTheBoxIsNotTraded(GameTestHelper helper) { public void adjustedPriceIsWhatIsCharged(GameTestHelper helper) { MarketGameTests.adjustedPriceIsWhatIsCharged(helper); } + + @GameTest(structure = "minecraft:empty") + public void experienceLandsOnThePlayer(GameTestHelper helper) { + MarketGameTests.experienceLandsOnThePlayer(helper); + } } diff --git a/src/main/java/net/modcrafters/minecraft/villagermarket/client/MarketScreenLogic.java b/src/main/java/net/modcrafters/minecraft/villagermarket/client/MarketScreenLogic.java index 6787ed9..2659806 100644 --- a/src/main/java/net/modcrafters/minecraft/villagermarket/client/MarketScreenLogic.java +++ b/src/main/java/net/modcrafters/minecraft/villagermarket/client/MarketScreenLogic.java @@ -13,32 +13,39 @@ // Hit testing and the Show All filter. Both screens draw this; neither one decides the rules. public final class MarketScreenLogic { public static final int WIDTH = 256; - public static final int HEIGHT = 209; - public static final int GROUP_X = 6; - public static final int GROUP_Y = 6; + public static final int HEIGHT = 180; + public static final int GROUP_X = 8; + public static final int GROUP_Y = 18; public static final int GROUP_W = 88; - public static final int GROUP_H = 140; + public static final int GROUP_H = 120; + public static final int GROUP_ROW = 20; public static final int OFFER_X = 100; - public static final int OFFER_Y = 6; - public static final int OFFER_W = 149; - public static final int OFFER_H = 165; - public static final int OFFER_ROW = 28; - public static final int SHOW_X = 5; + public static final int OFFER_Y = 18; + public static final int OFFER_W = 148; + public static final int OFFER_H = 120; + public static final int OFFER_ROW = 20; + public static final int COUNT_Y = OFFER_Y + OFFER_H + 2; + public static final int SHOW_X = 8; public static final int SHOW_Y = 152; - public static final int SHOW_W = 90; + public static final int SHOW_W = 88; public static final int SHOW_H = 20; - public static final int SELECTED_Y = 176; - public static final int ONCE_X = 156; - public static final int MAX_X = 196; - public static final int BUTTON_Y = 180; - public static final int BUTTON_W = 35; - public static final int BUTTON_H = 20; + public static final int ONCE_X = 164; + public static final int MAX_X = 208; + public static final int BUTTON_W = 40; + public static final int BUTTON_H = 16; + public static final int BUTTON_Y = SHOW_Y + (SHOW_H - BUTTON_H) / 2; + public static final int COST_SLOT = 18; + public static final int COST_Y = BUTTON_Y + (BUTTON_H - COST_SLOT) / 2; + public static final int BAR_W = 6; + public static final int BAR_X = WIDTH - 4 - BAR_W; public boolean showAll = true; public String selectedGroup = ""; public int groupScroll; public int offerScroll; public ListedOffer selected; + private boolean draggingBar; + private int barGrab; public List groups(List offers) { LinkedHashSet keys = new LinkedHashSet<>(); @@ -62,6 +69,69 @@ public List visible(List offers, Inventory inventory) return shown; } + public static int costSlotX(int index, boolean second) { + int slots = second ? 2 : 1; + return ONCE_X - 4 - slots * COST_SLOT + index * COST_SLOT; + } + + public static int offerRows() { + return Math.max(1, OFFER_H / OFFER_ROW); + } + + public static boolean offerBar(int count) { + return count > offerRows(); + } + + public static int offerWidth(int count) { + return offerBar(count) ? BAR_X - OFFER_X - 2 : OFFER_W; + } + + public void fitOffer(int count) { + offerScroll = clamp(offerScroll, count - offerRows()); + } + + public int offerThumbY(int count) { + int thumb = offerThumbH(count); + int travel = Math.max(1, OFFER_H - thumb); + int max = Math.max(1, count - offerRows()); + return OFFER_Y + offerScroll * travel / max; + } + + public int offerThumbH(int count) { + return Math.max(BAR_W * 2, OFFER_H * offerRows() / Math.max(count, 1)); + } + + public boolean pressBar(int x, int y, int count) { + if (!offerBar(count) || !inside(x, y, BAR_X, OFFER_Y, BAR_W, OFFER_H)) { + return false; + } + draggingBar = true; + int thumbY = offerThumbY(count); + int thumbH = offerThumbH(count); + barGrab = y >= thumbY && y < thumbY + thumbH ? y - thumbY : thumbH / 2; + dragBar(y, count); + return true; + } + + public void dragBar(int y, int count) { + if (!draggingBar || !offerBar(count)) { + return; + } + int thumb = offerThumbH(count); + int travel = Math.max(1, OFFER_H - thumb); + int max = Math.max(1, count - offerRows()); + offerScroll = clamp(y - barGrab - OFFER_Y, travel); + offerScroll = offerScroll * max / travel; + } + + public void releaseBar() { + draggingBar = false; + } + + public int held(Inventory inventory, ItemStack cost) { + return count(inventory, cost); + } + public int affordable(ListedOffer offer, Inventory inventory) { boolean hasSecond = !offer.costB().isEmpty(); return TradeMath.affordableUses( @@ -83,7 +153,7 @@ public void onOffers(List offers) { selected = null; } - public Click click(double mouseX, double mouseY, int left, int top, int rowHeight, List offers, Inventory inventory) { + public Click click(double mouseX, double mouseY, int left, int top, List offers, Inventory inventory) { int x = (int) mouseX - left; int y = (int) mouseY - top; if (inside(x, y, SHOW_X, SHOW_Y, SHOW_W, SHOW_H)) { @@ -99,7 +169,7 @@ public Click click(double mouseX, double mouseY, int left, int top, int rowHeigh } } if (inside(x, y, GROUP_X, GROUP_Y, GROUP_W, GROUP_H)) { - int index = groupScroll + (y - GROUP_Y) / rowHeight; + int index = groupScroll + (y - GROUP_Y) / GROUP_ROW; List groups = groups(offers); if (index == 0) { selectedGroup = ""; @@ -108,25 +178,28 @@ public Click click(double mouseX, double mouseY, int left, int top, int rowHeigh } return Click.GROUP; } - if (inside(x, y, OFFER_X, OFFER_Y, OFFER_W, OFFER_H)) { + List shown = visible(offers, inventory); + fitOffer(shown.size()); + if (pressBar(x, y, shown.size())) { + return Click.BAR; + } + if (inside(x, y, OFFER_X, OFFER_Y, offerWidth(shown.size()), OFFER_H)) { int index = offerScroll + (y - OFFER_Y) / OFFER_ROW; - List shown = visible(offers, inventory); selected = index >= 0 && index < shown.size() ? shown.get(index) : null; return Click.OFFER; } return Click.NONE; } - public void scroll(double mouseX, double mouseY, int left, int top, double delta, int rowHeight, int offerCount, int groupCount) { + public void scroll(double mouseX, double mouseY, int left, int top, double delta, int offerCount, int groupCount) { int x = (int) mouseX - left; int y = (int) mouseY - top; int step = delta > 0 ? -1 : 1; if (inside(x, y, GROUP_X, GROUP_Y, GROUP_W, GROUP_H)) { - int visible = Math.max(1, GROUP_H / rowHeight); + int visible = Math.max(1, GROUP_H / GROUP_ROW); groupScroll = clamp(groupScroll + step, groupCount + 1 - visible); - } else if (inside(x, y, OFFER_X, OFFER_Y, OFFER_W, OFFER_H)) { - int visible = Math.max(1, OFFER_H / OFFER_ROW); - offerScroll = clamp(offerScroll + step, offerCount - visible); + } else if (inside(x, y, OFFER_X, OFFER_Y, BAR_X + BAR_W - OFFER_X, OFFER_H)) { + offerScroll = clamp(offerScroll + step, offerCount - offerRows()); } } @@ -149,6 +222,6 @@ private static int count(Inventory inventory, ItemStack cost) { } public enum Click { - NONE, TOGGLE, GROUP, OFFER, ONCE, MAX + NONE, TOGGLE, GROUP, OFFER, ONCE, MAX, BAR } } diff --git a/src/main/java/net/modcrafters/minecraft/villagermarket/client/VillagerMarketScreen.java b/src/main/java/net/modcrafters/minecraft/villagermarket/client/VillagerMarketScreen.java index cfa340c..b11fe9f 100644 --- a/src/main/java/net/modcrafters/minecraft/villagermarket/client/VillagerMarketScreen.java +++ b/src/main/java/net/modcrafters/minecraft/villagermarket/client/VillagerMarketScreen.java @@ -1,6 +1,5 @@ package net.modcrafters.minecraft.villagermarket.client; -import net.modcrafters.minecraft.villagermarket.Constants; import net.modcrafters.minecraft.villagermarket.menu.VillagerMarketMenu; import net.modcrafters.minecraft.villagermarket.network.MarketPayloads; import net.modcrafters.minecraft.villagermarket.platform.Services; @@ -18,9 +17,20 @@ import java.util.List; public class VillagerMarketScreen extends AbstractContainerScreen { - private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(Constants.MOD_ID, "textures/gui/gui.png"); - private static final int TEXT = 0xFFFFFFFF; - private static final int SELECTED = 0x886B4A2A; + private static final Identifier BUTTON = Identifier.withDefaultNamespace("widget/button"); + private static final Identifier BUTTON_HIGHLIGHTED = Identifier.withDefaultNamespace("widget/button_highlighted"); + private static final Identifier BUTTON_DISABLED = Identifier.withDefaultNamespace("widget/button_disabled"); + private static final Identifier SLOT = Identifier.withDefaultNamespace("container/slot"); + private static final Identifier TRADE_ARROW = Identifier.withDefaultNamespace("container/villager/trade_arrow"); + private static final Identifier TRADE_ARROW_OUT = Identifier.withDefaultNamespace("container/villager/trade_arrow_out_of_stock"); + private static final Identifier SCROLLER = Identifier.withDefaultNamespace("widget/scroller"); + private static final Identifier SCROLLER_BACKGROUND = Identifier.withDefaultNamespace("widget/scroller_background"); + private static final int PANEL = 0xFFC6C6C6; + private static final int HIGHLIGHT = 0xFFFFFFFF; + private static final int SHADOW = 0xFF555555; + private static final int BORDER = 0xFF000000; + private static final int LABEL = 0xFF404040; + private static final int BUTTON_TEXT = 0xFFFFFFFF; private final MarketScreenLogic logic = new MarketScreenLogic(); private ItemStack hovered = ItemStack.EMPTY; @@ -44,55 +54,127 @@ public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mou super.extractBackground(graphics, mouseX, mouseY, partialTick); int left = this.leftPos; int top = this.topPos; - graphics.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, left, top, 0, 0, MarketScreenLogic.WIDTH, MarketScreenLogic.HEIGHT, 256, 256); - int row = this.font.lineHeight + 2; + drawPanel(graphics, left, top); + graphics.text(this.font, this.title, left + 8, top + 6, LABEL, false); List groups = logic.groups(this.menu.offers()); - drawGroup(graphics, left, top, row, 0, logic.selectedGroup.isEmpty(), Component.translatable("gui.villagermarket.all")); + graphics.enableScissor(left + MarketScreenLogic.GROUP_X, top + MarketScreenLogic.GROUP_Y, + left + MarketScreenLogic.GROUP_X + MarketScreenLogic.GROUP_W, top + MarketScreenLogic.GROUP_Y + MarketScreenLogic.GROUP_H); + drawGroup(graphics, left, top, mouseX, mouseY, 0, logic.selectedGroup.isEmpty(), Component.translatable("gui.villagermarket.all")); for (int i = 0; i < groups.size(); i++) { - drawGroup(graphics, left, top, row, i + 1, groups.get(i).equals(logic.selectedGroup), groupLabel(groups.get(i))); + drawGroup(graphics, left, top, mouseX, mouseY, i + 1, groups.get(i).equals(logic.selectedGroup), groupLabel(groups.get(i))); } + graphics.disableScissor(); hovered = ItemStack.EMPTY; List shown = logic.visible(this.menu.offers(), this.minecraft.player.getInventory()); + logic.fitOffer(shown.size()); + int offerW = MarketScreenLogic.offerWidth(shown.size()); + graphics.enableScissor(left + MarketScreenLogic.OFFER_X, top + MarketScreenLogic.OFFER_Y, + left + MarketScreenLogic.OFFER_X + offerW, top + MarketScreenLogic.OFFER_Y + MarketScreenLogic.OFFER_H); for (int i = logic.offerScroll; i < shown.size(); i++) { int y = top + MarketScreenLogic.OFFER_Y + (i - logic.offerScroll) * MarketScreenLogic.OFFER_ROW; if (y + MarketScreenLogic.OFFER_ROW > top + MarketScreenLogic.OFFER_Y + MarketScreenLogic.OFFER_H) { break; } - ListedOffer offer = shown.get(i); - if (logic.selected != null && logic.selected.villagerId() == offer.villagerId() && logic.selected.offerIndex() == offer.offerIndex()) { - graphics.fill(left + MarketScreenLogic.OFFER_X, y, left + MarketScreenLogic.OFFER_X + MarketScreenLogic.OFFER_W, y + MarketScreenLogic.OFFER_ROW, SELECTED); - } - drawOffer(graphics, offer, left + MarketScreenLogic.OFFER_X, y, mouseX, mouseY, this.minecraft.player.getInventory()); + drawOffer(graphics, shown.get(i), left + MarketScreenLogic.OFFER_X, y, offerW, mouseX, mouseY, this.minecraft.player.getInventory()); } - graphics.fill(left + MarketScreenLogic.SHOW_X, top + MarketScreenLogic.SHOW_Y, - left + MarketScreenLogic.SHOW_X + MarketScreenLogic.SHOW_W, top + MarketScreenLogic.SHOW_Y + MarketScreenLogic.SHOW_H, - logic.showAll ? SELECTED : 0x66000000); - graphics.text(this.font, Component.translatable("gui.villagermarket.show_all"), left + MarketScreenLogic.SHOW_X + 4, top + MarketScreenLogic.SHOW_Y + 6, TEXT, false); + graphics.disableScissor(); + drawOfferBar(graphics, left, top, shown.size()); + graphics.text(this.font, Component.translatable("gui.villagermarket.offer_count"), + left + MarketScreenLogic.OFFER_X, top + MarketScreenLogic.COUNT_Y, LABEL, false); + Component show = Component.translatable(logic.showAll ? "gui.villagermarket.show_all" : "gui.villagermarket.available"); + drawButton(graphics, left + MarketScreenLogic.SHOW_X, top + MarketScreenLogic.SHOW_Y, MarketScreenLogic.SHOW_W, MarketScreenLogic.SHOW_H, + show, true, logic.showAll || inside(mouseX, mouseY, left + MarketScreenLogic.SHOW_X, top + MarketScreenLogic.SHOW_Y, MarketScreenLogic.SHOW_W, MarketScreenLogic.SHOW_H)); if (logic.selected != null) { - drawOffer(graphics, logic.selected, left + 5, top + MarketScreenLogic.SELECTED_Y, mouseX, mouseY, this.minecraft.player.getInventory()); - int affordable = logic.affordable(logic.selected, this.minecraft.player.getInventory()); - drawButton(graphics, left + MarketScreenLogic.ONCE_X, top + MarketScreenLogic.BUTTON_Y, Component.translatable("gui.villagermarket.once"), affordable > 0); - drawButton(graphics, left + MarketScreenLogic.MAX_X, top + MarketScreenLogic.BUTTON_Y, Component.literal("x " + affordable), affordable > 0); + Inventory inventory = this.minecraft.player.getInventory(); + boolean second = !logic.selected.costB().isEmpty(); + drawHeld(graphics, logic.selected.costA(), left + MarketScreenLogic.costSlotX(0, second), top + MarketScreenLogic.COST_Y, mouseX, mouseY, inventory); + if (second) { + drawHeld(graphics, logic.selected.costB(), left + MarketScreenLogic.costSlotX(1, true), top + MarketScreenLogic.COST_Y, mouseX, mouseY, inventory); + } + int affordable = logic.affordable(logic.selected, inventory); + drawButton(graphics, left + MarketScreenLogic.ONCE_X, top + MarketScreenLogic.BUTTON_Y, MarketScreenLogic.BUTTON_W, MarketScreenLogic.BUTTON_H, + Component.translatable("gui.villagermarket.once"), affordable > 0, + inside(mouseX, mouseY, left + MarketScreenLogic.ONCE_X, top + MarketScreenLogic.BUTTON_Y, MarketScreenLogic.BUTTON_W, MarketScreenLogic.BUTTON_H)); + drawButton(graphics, left + MarketScreenLogic.MAX_X, top + MarketScreenLogic.BUTTON_Y, MarketScreenLogic.BUTTON_W, MarketScreenLogic.BUTTON_H, + Component.literal("x " + affordable), affordable > 0, + inside(mouseX, mouseY, left + MarketScreenLogic.MAX_X, top + MarketScreenLogic.BUTTON_Y, MarketScreenLogic.BUTTON_W, MarketScreenLogic.BUTTON_H)); } } - private void drawGroup(GuiGraphicsExtractor graphics, int left, int top, int row, int index, boolean selected, Component label) { - int y = top + MarketScreenLogic.GROUP_Y + (index - logic.groupScroll) * row; - if (y < top + MarketScreenLogic.GROUP_Y || y + row > top + MarketScreenLogic.GROUP_Y + MarketScreenLogic.GROUP_H) { + private void drawPanel(GuiGraphicsExtractor graphics, int x, int y) { + int right = x + MarketScreenLogic.WIDTH; + int bottom = y + MarketScreenLogic.HEIGHT; + graphics.fill(x, y, right, bottom, PANEL); + graphics.fill(x + 1, y + 1, right - 1, y + 3, HIGHLIGHT); + graphics.fill(x + 1, y + 1, x + 3, bottom - 1, HIGHLIGHT); + graphics.fill(x + 3, bottom - 3, right - 1, bottom - 1, SHADOW); + graphics.fill(right - 3, y + 3, right - 1, bottom - 1, SHADOW); + graphics.fill(x, y, right, y + 1, BORDER); + graphics.fill(x, bottom - 1, right, bottom, BORDER); + graphics.fill(x, y, x + 1, bottom, BORDER); + graphics.fill(right - 1, y, right, bottom, BORDER); + } + + private void drawGroup(GuiGraphicsExtractor graphics, int left, int top, int mouseX, int mouseY, int index, boolean selected, Component label) { + int x = left + MarketScreenLogic.GROUP_X; + int y = top + MarketScreenLogic.GROUP_Y + (index - logic.groupScroll) * MarketScreenLogic.GROUP_ROW; + if (y + MarketScreenLogic.GROUP_ROW <= top + MarketScreenLogic.GROUP_Y || y >= top + MarketScreenLogic.GROUP_Y + MarketScreenLogic.GROUP_H) { return; } - if (selected) { - graphics.fill(left + MarketScreenLogic.GROUP_X, y, left + MarketScreenLogic.GROUP_X + MarketScreenLogic.GROUP_W, y + row, SELECTED); - } - graphics.text(this.font, label, left + MarketScreenLogic.GROUP_X + 2, y + 1, TEXT, false); + boolean hot = selected || inside(mouseX, mouseY, x, y, MarketScreenLogic.GROUP_W, MarketScreenLogic.GROUP_ROW); + blitButton(graphics, x, y, MarketScreenLogic.GROUP_W, MarketScreenLogic.GROUP_ROW, true, hot); + graphics.text(this.font, label, x + 4, y + 6, BUTTON_TEXT, true); } - private void drawOffer(GuiGraphicsExtractor graphics, ListedOffer offer, int x, int y, int mouseX, int mouseY, Inventory inventory) { - drawStack(graphics, offer.costA(), x + 5, y + 5, mouseX, mouseY); - drawStack(graphics, offer.costB(), x + 30, y + 5, mouseX, mouseY); - drawStack(graphics, offer.result(), x + 73, y + 5, mouseX, mouseY); + private void drawOffer(GuiGraphicsExtractor graphics, ListedOffer offer, int x, int y, int width, int mouseX, int mouseY, Inventory inventory) { + boolean selected = logic.selected != null && logic.selected.villagerId() == offer.villagerId() && logic.selected.offerIndex() == offer.offerIndex(); + boolean hot = selected || inside(mouseX, mouseY, x, y, width, MarketScreenLogic.OFFER_ROW); + blitButton(graphics, x, y, width, MarketScreenLogic.OFFER_ROW, true, hot); + drawStack(graphics, offer.costA(), x + 3, y + 2, mouseX, mouseY); + drawStack(graphics, offer.costB(), x + 22, y + 2, mouseX, mouseY); + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, offer.usesLeft() > 0 ? TRADE_ARROW : TRADE_ARROW_OUT, x + 41, y + 5, 10, 9); + drawStack(graphics, offer.result(), x + 54, y + 2, mouseX, mouseY); int affordable = logic.affordable(offer, inventory); - graphics.text(this.font, "[" + affordable + " / " + offer.usesLeft() + "]", x + 101, y + 10, TEXT, false); + graphics.text(this.font, "[" + affordable + " / " + offer.usesLeft() + "]", x + 74, y + 6, BUTTON_TEXT, true); + } + + private void drawOfferBar(GuiGraphicsExtractor graphics, int left, int top, int count) { + if (!MarketScreenLogic.offerBar(count)) { + return; + } + int x = left + MarketScreenLogic.BAR_X; + int y = top + MarketScreenLogic.OFFER_Y; + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, SCROLLER_BACKGROUND, x, y, MarketScreenLogic.BAR_W, MarketScreenLogic.OFFER_H); + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, SCROLLER, x, top + logic.offerThumbY(count), MarketScreenLogic.BAR_W, logic.offerThumbH(count)); + } + + private int shownCount() { + return logic.visible(this.menu.offers(), this.minecraft.player.getInventory()).size(); + } + + private void drawButton(GuiGraphicsExtractor graphics, int x, int y, int width, int height, Component label, boolean enabled, boolean hovered) { + blitButton(graphics, x, y, width, height, enabled, hovered); + int textX = x + (width - this.font.width(label)) / 2; + graphics.text(this.font, label, textX, y + (height - 8) / 2, BUTTON_TEXT, true); + } + + private void blitButton(GuiGraphicsExtractor graphics, int x, int y, int width, int height, boolean enabled, boolean hovered) { + Identifier sprite = !enabled ? BUTTON_DISABLED : hovered ? BUTTON_HIGHLIGHTED : BUTTON; + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, sprite, x, y, width, height); + } + + private void drawHeld(GuiGraphicsExtractor graphics, ItemStack cost, int x, int y, int mouseX, int mouseY, Inventory inventory) { + if (cost.isEmpty()) { + return; + } + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, SLOT, x, y, MarketScreenLogic.COST_SLOT, MarketScreenLogic.COST_SLOT); + int itemX = x + 1; + int itemY = y + 1; + graphics.item(cost, itemX, itemY); + graphics.itemDecorations(this.font, cost, itemX, itemY, Integer.toString(logic.held(inventory, cost))); + if (inside(mouseX, mouseY, x, y, MarketScreenLogic.COST_SLOT, MarketScreenLogic.COST_SLOT)) { + hovered = cost; + } } private void drawStack(GuiGraphicsExtractor graphics, ItemStack stack, int x, int y, int mouseX, int mouseY) { @@ -100,14 +182,18 @@ private void drawStack(GuiGraphicsExtractor graphics, ItemStack stack, int x, in return; } graphics.item(stack, x, y); + graphics.itemDecorations(this.font, stack, x, y); if (mouseX >= x && mouseX < x + 16 && mouseY >= y && mouseY < y + 16) { hovered = stack; } } - private void drawButton(GuiGraphicsExtractor graphics, int x, int y, Component label, boolean enabled) { - graphics.fill(x, y, x + MarketScreenLogic.BUTTON_W, y + MarketScreenLogic.BUTTON_H, enabled ? SELECTED : 0x66000000); - graphics.text(this.font, label, x + 4, y + 6, TEXT, false); + private static boolean inside(int mouseX, int mouseY, int x, int y, int width, int height) { + return mouseX >= x && mouseY >= y && mouseX < x + width && mouseY < y + height; + } + + @Override + protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) { } @Override @@ -121,7 +207,7 @@ public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mo @Override public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) { Inventory inventory = this.minecraft.player.getInventory(); - MarketScreenLogic.Click click = logic.click(event.x(), event.y(), this.leftPos, this.topPos, this.font.lineHeight + 2, this.menu.offers(), inventory); + MarketScreenLogic.Click click = logic.click(event.x(), event.y(), this.leftPos, this.topPos, this.menu.offers(), inventory); if (click == MarketScreenLogic.Click.ONCE && logic.selected != null) { send(1); } else if (click == MarketScreenLogic.Click.MAX && logic.selected != null) { @@ -130,10 +216,22 @@ public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) { return click != MarketScreenLogic.Click.NONE || super.mouseClicked(event, doubleClick); } + @Override + public boolean mouseDragged(MouseButtonEvent event, double dragX, double dragY) { + logic.dragBar((int) event.y() - this.topPos, shownCount()); + return super.mouseDragged(event, dragX, dragY); + } + + @Override + public boolean mouseReleased(MouseButtonEvent event) { + logic.releaseBar(); + return super.mouseReleased(event); + } + @Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { List shown = logic.visible(this.menu.offers(), this.minecraft.player.getInventory()); - logic.scroll(mouseX, mouseY, this.leftPos, this.topPos, scrollY, this.font.lineHeight + 2, shown.size(), logic.groups(this.menu.offers()).size()); + logic.scroll(mouseX, mouseY, this.leftPos, this.topPos, scrollY, shown.size(), logic.groups(this.menu.offers()).size()); return true; } diff --git a/src/main/java/net/modcrafters/minecraft/villagermarket/gametest/MarketGameTests.java b/src/main/java/net/modcrafters/minecraft/villagermarket/gametest/MarketGameTests.java index b7d370b..e6df5bb 100644 --- a/src/main/java/net/modcrafters/minecraft/villagermarket/gametest/MarketGameTests.java +++ b/src/main/java/net/modcrafters/minecraft/villagermarket/gametest/MarketGameTests.java @@ -9,11 +9,15 @@ import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.EntityTypes; +import net.minecraft.world.entity.ExperienceOrb; import net.minecraft.world.entity.npc.villager.Villager; +import net.minecraft.world.phys.AABB; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.trading.ItemCost; +//? if >=26.1 +import net.minecraft.world.level.GameType; import net.minecraft.world.item.trading.MerchantOffer; import net.minecraft.world.item.trading.MerchantOffers; @@ -29,7 +33,8 @@ public record Case(String name, Consumer body) { new Case("one_emerald_buys_bread", MarketGameTests::oneEmeraldBuysBread), new Case("components_must_match", MarketGameTests::componentsMustMatch), new Case("villager_outside_the_box_is_not_traded", MarketGameTests::villagerOutsideTheBoxIsNotTraded), - new Case("adjusted_price_is_what_is_charged", MarketGameTests::adjustedPriceIsWhatIsCharged) + new Case("adjusted_price_is_what_is_charged", MarketGameTests::adjustedPriceIsWhatIsCharged), + new Case("experience_lands_on_the_player", MarketGameTests::experienceLandsOnThePlayer) ); private MarketGameTests() { @@ -93,6 +98,31 @@ public static void adjustedPriceIsWhatIsCharged(GameTestHelper helper) { helper.succeed(); } + public static void experienceLandsOnThePlayer(GameTestHelper helper) { + Villager villager = villager(helper, offer(new ItemCost(Items.EMERALD), new ItemStack(Items.BREAD), 4, 0.0f, 0)); + ServerPlayer player = player(helper); + player.setPos(villager.getX() + 8, villager.getY(), villager.getZ()); + player.getInventory().add(new ItemStack(Items.EMERALD)); + + TradeExecutor.Result result = TradeExecutor.execute(player, villager.blockPosition(), villager.getId(), 0, 1); + + helper.assertTrue(result.performed() == 1, "performed " + result.performed()); + AABB area = villager.getBoundingBox().minmax(player.getBoundingBox()).inflate(2); + boolean onPlayer = false; + boolean onVillager = false; + for (ExperienceOrb orb : player.level().getEntitiesOfClass(ExperienceOrb.class, area)) { + if (orb.distanceToSqr(player) < 4) { + onPlayer = true; + } + if (orb.distanceToSqr(villager) < 4) { + onVillager = true; + } + } + helper.assertTrue(onPlayer, "the experience orb was not on the player"); + helper.assertTrue(!onVillager, "the experience orb stayed on the villager"); + helper.succeed(); + } + private static MerchantOffer offer(ItemCost cost, ItemStack result, int maxUses, float multiplier, int demand) { return new MerchantOffer(cost, Optional.empty(), result, 0, maxUses, 1, multiplier, demand); } @@ -105,8 +135,12 @@ private static Villager villager(GameTestHelper helper, MerchantOffer offer) { return villager; } + @SuppressWarnings("removal") private static ServerPlayer player(GameTestHelper helper) { - return helper.makeMockServerPlayerInLevel(); + //? if >=26.1 { + return (ServerPlayer) helper.makeMockServerPlayer(GameType.CREATIVE); + //?} else + //return helper.makeMockServerPlayerInLevel(); } private static int count(ServerPlayer player, Item item) { diff --git a/src/main/java/net/modcrafters/minecraft/villagermarket/menu/VillagerMarketMenu.java b/src/main/java/net/modcrafters/minecraft/villagermarket/menu/VillagerMarketMenu.java index fadd07c..299441a 100644 --- a/src/main/java/net/modcrafters/minecraft/villagermarket/menu/VillagerMarketMenu.java +++ b/src/main/java/net/modcrafters/minecraft/villagermarket/menu/VillagerMarketMenu.java @@ -9,6 +9,7 @@ import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; import net.minecraft.world.phys.Vec3; @@ -16,6 +17,7 @@ import java.util.List; public class VillagerMarketMenu extends AbstractContainerMenu { + private static final int MAIN_SLOTS = 36; private final BlockPos pos; private List offers; @@ -23,6 +25,10 @@ public VillagerMarketMenu(int containerId, Inventory inventory, OpeningData data super(ModContent.menu(), containerId); this.pos = data.pos(); this.offers = List.copyOf(data.offers()); + // The screen never draws these. They exist so a purchase is sent to the client inventory. + for (int index = 0; index < MAIN_SLOTS; index++) { + this.addSlot(new HiddenSlot(inventory, index)); + } } public static OpeningData opening(ServerLevel level, BlockPos pos) { @@ -59,4 +65,20 @@ public boolean stillValid(Player player) { public ItemStack quickMoveStack(Player player, int index) { return ItemStack.EMPTY; } + + private static final class HiddenSlot extends Slot { + private HiddenSlot(Inventory inventory, int index) { + super(inventory, index, 0, 0); + } + + @Override + public boolean isActive() { + return false; + } + + @Override + public boolean mayPickup(Player player) { + return false; + } + } } diff --git a/src/main/java/net/modcrafters/minecraft/villagermarket/network/MarketPayloads.java b/src/main/java/net/modcrafters/minecraft/villagermarket/network/MarketPayloads.java index 5a43907..32a0d40 100644 --- a/src/main/java/net/modcrafters/minecraft/villagermarket/network/MarketPayloads.java +++ b/src/main/java/net/modcrafters/minecraft/villagermarket/network/MarketPayloads.java @@ -49,6 +49,7 @@ public static void handleTrade(ServerPlayer player, Trade trade) { return; } TradeExecutor.execute(player, menu.pos(), trade.villagerId(), trade.offerIndex(), trade.uses()); + menu.broadcastChanges(); List fresh = VillagerMarketMenu.scan((ServerLevel) player.level(), menu.pos()); menu.replace(fresh); Services.PLATFORM.sendToPlayer(player, new Offers(fresh)); diff --git a/src/main/java/net/modcrafters/minecraft/villagermarket/trade/TradeExecutor.java b/src/main/java/net/modcrafters/minecraft/villagermarket/trade/TradeExecutor.java index c142ac6..105e66b 100644 --- a/src/main/java/net/modcrafters/minecraft/villagermarket/trade/TradeExecutor.java +++ b/src/main/java/net/modcrafters/minecraft/villagermarket/trade/TradeExecutor.java @@ -11,6 +11,8 @@ import net.minecraft.world.item.trading.MerchantOffer; import net.minecraft.world.item.trading.MerchantOffers; +import java.util.List; + public final class TradeExecutor { private TradeExecutor() { } @@ -51,16 +53,23 @@ public static Result execute(ServerPlayer player, BlockPos marketPos, int villag if (!result.isEmpty()) { player.level().addFreshEntity(new ItemEntity(player.level(), player.getX(), player.getY(), player.getZ(), result)); } - villager.notifyTrade(offer); - if (offer.shouldRewardExp()) { - spawnXp(player, offer.getXp()); - } + rewardAt(villager, player, offer); } return new Result(performed, false); } - private static void spawnXp(ServerPlayer player, int xp) { - player.level().addFreshEntity(new ExperienceOrb(player.level(), player.getX(), player.getY(), player.getZ(), xp)); + private static void rewardAt(Villager villager, ServerPlayer player, MerchantOffer offer) { + List already = orbsNear(villager); + villager.notifyTrade(offer); + for (ExperienceOrb orb : orbsNear(villager)) { + if (!already.contains(orb)) { + orb.setPos(player.getX(), player.getY(), player.getZ()); + } + } + } + + private static List orbsNear(Villager villager) { + return villager.level().getEntitiesOfClass(ExperienceOrb.class, villager.getBoundingBox().inflate(1.0)); } static int count(ServerPlayer player, ItemStack cost) { diff --git a/src/main/resources/assets/villagermarket/lang/en_us.json b/src/main/resources/assets/villagermarket/lang/en_us.json index 7c2ad59..ef9832f 100644 --- a/src/main/resources/assets/villagermarket/lang/en_us.json +++ b/src/main/resources/assets/villagermarket/lang/en_us.json @@ -3,5 +3,7 @@ "itemGroup.villagermarket": "Villager Market", "gui.villagermarket.all": "[ ALL ]", "gui.villagermarket.show_all": "Show All", - "gui.villagermarket.once": "x 1" + "gui.villagermarket.available": "Available", + "gui.villagermarket.once": "x 1", + "gui.villagermarket.offer_count": "[available / max until lock]" } diff --git a/src/main/resources/assets/villagermarket/models/block/villager_market.json b/src/main/resources/assets/villagermarket/models/block/villager_market.json index 53a8f2d..4ed483e 100644 --- a/src/main/resources/assets/villagermarket/models/block/villager_market.json +++ b/src/main/resources/assets/villagermarket/models/block/villager_market.json @@ -1,12 +1,111 @@ { - "parent": "minecraft:block/cube", + "parent": "minecraft:block/block", "textures": { - "particle": "villagermarket:blocks/crate", - "north": "villagermarket:blocks/crate-villager", - "west": "villagermarket:blocks/crate-villager", - "east": "villagermarket:blocks/crate-villager", - "south": "villagermarket:blocks/crate-villager", - "up": "villagermarket:blocks/crate", - "down": "villagermarket:blocks/crate" - } + "particle": "minecraft:block/barrel_side", + "side": "minecraft:block/barrel_side", + "lid": "villagermarket:block/lid", + "face": "villagermarket:block/face" + }, + "elements": [ + { + "from": [0, 0, 0], + "to": [16, 16, 16], + "faces": { + "north": { "texture": "#side", "cullface": "north" }, + "south": { "texture": "#side", "cullface": "south" }, + "east": { "texture": "#side", "cullface": "east" }, + "west": { "texture": "#side", "cullface": "west" }, + "up": { "texture": "#lid", "cullface": "up" }, + "down": { "texture": "#lid", "cullface": "down" } + } + }, + { + "from": [4, 1, -1], + "to": [12, 11, 0], + "faces": { + "north": { "texture": "#face", "uv": [0, 0, 8, 10] }, + "up": { "texture": "#face", "uv": [15, 0, 16, 1] }, + "down": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "east": { "texture": "#face", "uv": [15, 0, 16, 1] }, + "west": { "texture": "#face", "uv": [15, 0, 16, 1] } + } + }, + { + "from": [6, 1, -2], + "to": [10, 5, -1], + "faces": { + "north": { "texture": "#face", "uv": [2, 6, 6, 10] }, + "up": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "down": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "east": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "west": { "texture": "#face", "uv": [15, 15, 16, 16] } + } + }, + { + "from": [4, 1, 16], + "to": [12, 11, 17], + "faces": { + "south": { "texture": "#face", "uv": [0, 0, 8, 10] }, + "up": { "texture": "#face", "uv": [15, 0, 16, 1] }, + "down": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "east": { "texture": "#face", "uv": [15, 0, 16, 1] }, + "west": { "texture": "#face", "uv": [15, 0, 16, 1] } + } + }, + { + "from": [6, 1, 17], + "to": [10, 5, 18], + "faces": { + "south": { "texture": "#face", "uv": [2, 6, 6, 10] }, + "up": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "down": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "east": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "west": { "texture": "#face", "uv": [15, 15, 16, 16] } + } + }, + { + "from": [-1, 1, 4], + "to": [0, 11, 12], + "faces": { + "west": { "texture": "#face", "uv": [0, 0, 8, 10] }, + "up": { "texture": "#face", "uv": [15, 0, 16, 1] }, + "down": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "north": { "texture": "#face", "uv": [15, 0, 16, 1] }, + "south": { "texture": "#face", "uv": [15, 0, 16, 1] } + } + }, + { + "from": [-2, 1, 6], + "to": [-1, 5, 10], + "faces": { + "west": { "texture": "#face", "uv": [2, 6, 6, 10] }, + "up": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "down": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "north": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "south": { "texture": "#face", "uv": [15, 15, 16, 16] } + } + }, + { + "from": [16, 1, 4], + "to": [17, 11, 12], + "faces": { + "east": { "texture": "#face", "uv": [0, 0, 8, 10] }, + "up": { "texture": "#face", "uv": [15, 0, 16, 1] }, + "down": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "north": { "texture": "#face", "uv": [15, 0, 16, 1] }, + "south": { "texture": "#face", "uv": [15, 0, 16, 1] } + } + }, + { + "from": [17, 1, 6], + "to": [18, 5, 10], + "faces": { + "east": { "texture": "#face", "uv": [2, 6, 6, 10] }, + "up": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "down": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "north": { "texture": "#face", "uv": [15, 15, 16, 16] }, + "south": { "texture": "#face", "uv": [15, 15, 16, 16] } + } + } + ] } diff --git a/src/main/resources/assets/villagermarket/textures/block/face.png b/src/main/resources/assets/villagermarket/textures/block/face.png new file mode 100644 index 0000000000000000000000000000000000000000..a02c1a956c71e98e21e10bc14ffa4809c074a67b GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1quc!6r`^#}EtuGIUj?|L2-zFazis22WQ%mvv4FO#t%PPPG64 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/villagermarket/textures/block/lid.png b/src/main/resources/assets/villagermarket/textures/block/lid.png new file mode 100644 index 0000000000000000000000000000000000000000..65a0052cb8545eb886d9ee3a77c3c6a67fc3e5d9 GIT binary patch literal 297 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 diff --git a/src/main/resources/assets/villagermarket/textures/blocks/crate.png b/src/main/resources/assets/villagermarket/textures/blocks/crate.png deleted file mode 100644 index b7e9d142f89c2dd5095ea2b422ca4e850415c69b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9476 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 NHP$!RE5kYC{|{&A^cDaB diff --git a/src/main/resources/data/villagermarket/advancement/recipes/misc/villager_market.json b/src/main/resources/data/villagermarket/advancement/recipes/misc/villager_market.json new file mode 100644 index 0000000..c3745a0 --- /dev/null +++ b/src/main/resources/data/villagermarket/advancement/recipes/misc/villager_market.json @@ -0,0 +1,43 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_planks": { + "conditions": { + "items": [ + { + "items": "#minecraft:planks" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_emerald_block": { + "conditions": { + "items": [ + { + "items": "minecraft:emerald_block" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipes": "villagermarket:villager_market" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_planks", + "has_emerald_block" + ] + ], + "rewards": { + "recipes": [ + "villagermarket:villager_market" + ] + } +} diff --git a/versions/1.21.1/src/main/java/net/modcrafters/minecraft/villagermarket/client/VillagerMarketScreen.java b/versions/1.21.1/src/main/java/net/modcrafters/minecraft/villagermarket/client/VillagerMarketScreen.java index fbbbc95..d64b2f7 100644 --- a/versions/1.21.1/src/main/java/net/modcrafters/minecraft/villagermarket/client/VillagerMarketScreen.java +++ b/versions/1.21.1/src/main/java/net/modcrafters/minecraft/villagermarket/client/VillagerMarketScreen.java @@ -16,9 +16,20 @@ // 1.21.1 still draws with GuiGraphics. The 26.3 screen is the shared file. public class VillagerMarketScreen extends AbstractContainerScreen { - private static final ResourceLocation TEXTURE = ResourceLocation.fromNamespaceAndPath("villagermarket", "textures/gui/gui.png"); - private static final int TEXT = 0xFFFFFFFF; - private static final int SELECTED = 0x886B4A2A; + private static final ResourceLocation BUTTON = ResourceLocation.withDefaultNamespace("widget/button"); + private static final ResourceLocation BUTTON_HIGHLIGHTED = ResourceLocation.withDefaultNamespace("widget/button_highlighted"); + private static final ResourceLocation BUTTON_DISABLED = ResourceLocation.withDefaultNamespace("widget/button_disabled"); + private static final ResourceLocation SLOT = ResourceLocation.withDefaultNamespace("container/slot"); + private static final ResourceLocation TRADE_ARROW = ResourceLocation.withDefaultNamespace("container/villager/trade_arrow"); + private static final ResourceLocation TRADE_ARROW_OUT = ResourceLocation.withDefaultNamespace("container/villager/trade_arrow_out_of_stock"); + private static final ResourceLocation SCROLLER = ResourceLocation.withDefaultNamespace("widget/scroller"); + private static final ResourceLocation SCROLLER_BACKGROUND = ResourceLocation.withDefaultNamespace("widget/scroller_background"); + private static final int PANEL = 0xFFC6C6C6; + private static final int HIGHLIGHT = 0xFFFFFFFF; + private static final int SHADOW = 0xFF555555; + private static final int BORDER = 0xFF000000; + private static final int LABEL = 0xFF404040; + private static final int BUTTON_TEXT = 0xFFFFFFFF; private final MarketScreenLogic logic = new MarketScreenLogic(); private ItemStack hovered = ItemStack.EMPTY; @@ -43,58 +54,130 @@ public static void apply(MarketPayloads.Offers payload) { protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) { int left = this.leftPos; int top = this.topPos; - graphics.blit(TEXTURE, left, top, 0, 0, this.imageWidth, this.imageHeight); - int row = this.font.lineHeight + 2; + drawPanel(graphics, left, top); + graphics.drawString(this.font, this.title, left + 8, top + 6, LABEL, false); List groups = logic.groups(this.menu.offers()); - drawGroup(graphics, left, top, row, 0, logic.selectedGroup.isEmpty(), Component.translatable("gui.villagermarket.all")); + graphics.enableScissor(left + MarketScreenLogic.GROUP_X, top + MarketScreenLogic.GROUP_Y, + left + MarketScreenLogic.GROUP_X + MarketScreenLogic.GROUP_W, top + MarketScreenLogic.GROUP_Y + MarketScreenLogic.GROUP_H); + drawGroup(graphics, left, top, mouseX, mouseY, 0, logic.selectedGroup.isEmpty(), Component.translatable("gui.villagermarket.all")); for (int i = 0; i < groups.size(); i++) { - drawGroup(graphics, left, top, row, i + 1, groups.get(i).equals(logic.selectedGroup), groupLabel(groups.get(i))); + drawGroup(graphics, left, top, mouseX, mouseY, i + 1, groups.get(i).equals(logic.selectedGroup), groupLabel(groups.get(i))); } + graphics.disableScissor(); hovered = ItemStack.EMPTY; List shown = logic.visible(this.menu.offers(), this.minecraft.player.getInventory()); + logic.fitOffer(shown.size()); + int offerW = MarketScreenLogic.offerWidth(shown.size()); + graphics.enableScissor(left + MarketScreenLogic.OFFER_X, top + MarketScreenLogic.OFFER_Y, + left + MarketScreenLogic.OFFER_X + offerW, top + MarketScreenLogic.OFFER_Y + MarketScreenLogic.OFFER_H); for (int i = logic.offerScroll; i < shown.size(); i++) { int y = top + MarketScreenLogic.OFFER_Y + (i - logic.offerScroll) * MarketScreenLogic.OFFER_ROW; if (y + MarketScreenLogic.OFFER_ROW > top + MarketScreenLogic.OFFER_Y + MarketScreenLogic.OFFER_H) { break; } - ListedOffer offer = shown.get(i); - if (logic.selected != null && logic.selected.villagerId() == offer.villagerId() && logic.selected.offerIndex() == offer.offerIndex()) { - graphics.fill(left + MarketScreenLogic.OFFER_X, y, left + MarketScreenLogic.OFFER_X + MarketScreenLogic.OFFER_W, y + MarketScreenLogic.OFFER_ROW, SELECTED); - } - drawOffer(graphics, offer, left + MarketScreenLogic.OFFER_X, y, mouseX, mouseY); + drawOffer(graphics, shown.get(i), left + MarketScreenLogic.OFFER_X, y, offerW, mouseX, mouseY); } - graphics.fill(left + MarketScreenLogic.SHOW_X, top + MarketScreenLogic.SHOW_Y, - left + MarketScreenLogic.SHOW_X + MarketScreenLogic.SHOW_W, top + MarketScreenLogic.SHOW_Y + MarketScreenLogic.SHOW_H, - logic.showAll ? SELECTED : 0x66000000); - graphics.drawString(this.font, Component.translatable("gui.villagermarket.show_all"), left + MarketScreenLogic.SHOW_X + 4, top + MarketScreenLogic.SHOW_Y + 6, TEXT, false); + graphics.disableScissor(); + drawOfferBar(graphics, left, top, shown.size()); + graphics.drawString(this.font, Component.translatable("gui.villagermarket.offer_count"), + left + MarketScreenLogic.OFFER_X, top + MarketScreenLogic.COUNT_Y, LABEL, false); + Component show = Component.translatable(logic.showAll ? "gui.villagermarket.show_all" : "gui.villagermarket.available"); + drawButton(graphics, left + MarketScreenLogic.SHOW_X, top + MarketScreenLogic.SHOW_Y, MarketScreenLogic.SHOW_W, MarketScreenLogic.SHOW_H, + show, true, logic.showAll || inside(mouseX, mouseY, left + MarketScreenLogic.SHOW_X, top + MarketScreenLogic.SHOW_Y, MarketScreenLogic.SHOW_W, MarketScreenLogic.SHOW_H)); if (logic.selected != null) { - drawOffer(graphics, logic.selected, left + 5, top + MarketScreenLogic.SELECTED_Y, mouseX, mouseY); - int affordable = logic.affordable(logic.selected, this.minecraft.player.getInventory()); - drawButton(graphics, left + MarketScreenLogic.ONCE_X, top + MarketScreenLogic.BUTTON_Y, Component.translatable("gui.villagermarket.once"), affordable > 0); - drawButton(graphics, left + MarketScreenLogic.MAX_X, top + MarketScreenLogic.BUTTON_Y, Component.literal("x " + affordable), affordable > 0); + Inventory inventory = this.minecraft.player.getInventory(); + boolean second = !logic.selected.costB().isEmpty(); + drawHeld(graphics, logic.selected.costA(), left + MarketScreenLogic.costSlotX(0, second), top + MarketScreenLogic.COST_Y, mouseX, mouseY, inventory); + if (second) { + drawHeld(graphics, logic.selected.costB(), left + MarketScreenLogic.costSlotX(1, true), top + MarketScreenLogic.COST_Y, mouseX, mouseY, inventory); + } + int affordable = logic.affordable(logic.selected, inventory); + drawButton(graphics, left + MarketScreenLogic.ONCE_X, top + MarketScreenLogic.BUTTON_Y, MarketScreenLogic.BUTTON_W, MarketScreenLogic.BUTTON_H, + Component.translatable("gui.villagermarket.once"), affordable > 0, + inside(mouseX, mouseY, left + MarketScreenLogic.ONCE_X, top + MarketScreenLogic.BUTTON_Y, MarketScreenLogic.BUTTON_W, MarketScreenLogic.BUTTON_H)); + drawButton(graphics, left + MarketScreenLogic.MAX_X, top + MarketScreenLogic.BUTTON_Y, MarketScreenLogic.BUTTON_W, MarketScreenLogic.BUTTON_H, + Component.literal("x " + affordable), affordable > 0, + inside(mouseX, mouseY, left + MarketScreenLogic.MAX_X, top + MarketScreenLogic.BUTTON_Y, MarketScreenLogic.BUTTON_W, MarketScreenLogic.BUTTON_H)); } if (!hovered.isEmpty()) { graphics.renderTooltip(this.font, hovered, mouseX, mouseY); } } - private void drawGroup(GuiGraphics graphics, int left, int top, int row, int index, boolean selected, Component label) { - int y = top + MarketScreenLogic.GROUP_Y + (index - logic.groupScroll) * row; - if (y < top + MarketScreenLogic.GROUP_Y || y + row > top + MarketScreenLogic.GROUP_Y + MarketScreenLogic.GROUP_H) { + private void drawPanel(GuiGraphics graphics, int x, int y) { + int right = x + MarketScreenLogic.WIDTH; + int bottom = y + MarketScreenLogic.HEIGHT; + graphics.fill(x, y, right, bottom, PANEL); + graphics.fill(x + 1, y + 1, right - 1, y + 3, HIGHLIGHT); + graphics.fill(x + 1, y + 1, x + 3, bottom - 1, HIGHLIGHT); + graphics.fill(x + 3, bottom - 3, right - 1, bottom - 1, SHADOW); + graphics.fill(right - 3, y + 3, right - 1, bottom - 1, SHADOW); + graphics.fill(x, y, right, y + 1, BORDER); + graphics.fill(x, bottom - 1, right, bottom, BORDER); + graphics.fill(x, y, x + 1, bottom, BORDER); + graphics.fill(right - 1, y, right, bottom, BORDER); + } + + private void drawGroup(GuiGraphics graphics, int left, int top, int mouseX, int mouseY, int index, boolean selected, Component label) { + int x = left + MarketScreenLogic.GROUP_X; + int y = top + MarketScreenLogic.GROUP_Y + (index - logic.groupScroll) * MarketScreenLogic.GROUP_ROW; + if (y + MarketScreenLogic.GROUP_ROW <= top + MarketScreenLogic.GROUP_Y || y >= top + MarketScreenLogic.GROUP_Y + MarketScreenLogic.GROUP_H) { return; } - if (selected) { - graphics.fill(left + MarketScreenLogic.GROUP_X, y, left + MarketScreenLogic.GROUP_X + MarketScreenLogic.GROUP_W, y + row, SELECTED); - } - graphics.drawString(this.font, label, left + MarketScreenLogic.GROUP_X + 2, y + 1, TEXT, false); + boolean hot = selected || inside(mouseX, mouseY, x, y, MarketScreenLogic.GROUP_W, MarketScreenLogic.GROUP_ROW); + blitButton(graphics, x, y, MarketScreenLogic.GROUP_W, MarketScreenLogic.GROUP_ROW, true, hot); + graphics.drawString(this.font, label, x + 4, y + 6, BUTTON_TEXT, true); } - private void drawOffer(GuiGraphics graphics, ListedOffer offer, int x, int y, int mouseX, int mouseY) { - drawStack(graphics, offer.costA(), x + 5, y + 5, mouseX, mouseY); - drawStack(graphics, offer.costB(), x + 30, y + 5, mouseX, mouseY); - drawStack(graphics, offer.result(), x + 73, y + 5, mouseX, mouseY); + private void drawOffer(GuiGraphics graphics, ListedOffer offer, int x, int y, int width, int mouseX, int mouseY) { + boolean selected = logic.selected != null && logic.selected.villagerId() == offer.villagerId() && logic.selected.offerIndex() == offer.offerIndex(); + boolean hot = selected || inside(mouseX, mouseY, x, y, width, MarketScreenLogic.OFFER_ROW); + blitButton(graphics, x, y, width, MarketScreenLogic.OFFER_ROW, true, hot); + drawStack(graphics, offer.costA(), x + 3, y + 2, mouseX, mouseY); + drawStack(graphics, offer.costB(), x + 22, y + 2, mouseX, mouseY); + graphics.blitSprite(offer.usesLeft() > 0 ? TRADE_ARROW : TRADE_ARROW_OUT, x + 41, y + 5, 10, 9); + drawStack(graphics, offer.result(), x + 54, y + 2, mouseX, mouseY); int affordable = logic.affordable(offer, this.minecraft.player.getInventory()); - graphics.drawString(this.font, "[" + affordable + " / " + offer.usesLeft() + "]", x + 101, y + 10, TEXT, false); + graphics.drawString(this.font, "[" + affordable + " / " + offer.usesLeft() + "]", x + 74, y + 6, BUTTON_TEXT, true); + } + + private void drawOfferBar(GuiGraphics graphics, int left, int top, int count) { + if (!MarketScreenLogic.offerBar(count)) { + return; + } + int x = left + MarketScreenLogic.BAR_X; + int y = top + MarketScreenLogic.OFFER_Y; + graphics.blitSprite(SCROLLER_BACKGROUND, x, y, MarketScreenLogic.BAR_W, MarketScreenLogic.OFFER_H); + graphics.blitSprite(SCROLLER, x, top + logic.offerThumbY(count), MarketScreenLogic.BAR_W, logic.offerThumbH(count)); + } + + private int shownCount() { + return logic.visible(this.menu.offers(), this.minecraft.player.getInventory()).size(); + } + + private void drawButton(GuiGraphics graphics, int x, int y, int width, int height, Component label, boolean enabled, boolean hovered) { + blitButton(graphics, x, y, width, height, enabled, hovered); + int textX = x + (width - this.font.width(label)) / 2; + graphics.drawString(this.font, label, textX, y + (height - 8) / 2, BUTTON_TEXT, true); + } + + private void blitButton(GuiGraphics graphics, int x, int y, int width, int height, boolean enabled, boolean hovered) { + ResourceLocation sprite = !enabled ? BUTTON_DISABLED : hovered ? BUTTON_HIGHLIGHTED : BUTTON; + graphics.blitSprite(sprite, x, y, width, height); + } + + private void drawHeld(GuiGraphics graphics, ItemStack cost, int x, int y, int mouseX, int mouseY, Inventory inventory) { + if (cost.isEmpty()) { + return; + } + graphics.blitSprite(SLOT, x, y, MarketScreenLogic.COST_SLOT, MarketScreenLogic.COST_SLOT); + int itemX = x + 1; + int itemY = y + 1; + graphics.renderItem(cost, itemX, itemY); + graphics.renderItemDecorations(this.font, cost, itemX, itemY, Integer.toString(logic.held(inventory, cost))); + if (inside(mouseX, mouseY, x, y, MarketScreenLogic.COST_SLOT, MarketScreenLogic.COST_SLOT)) { + hovered = cost; + } } private void drawStack(GuiGraphics graphics, ItemStack stack, int x, int y, int mouseX, int mouseY) { @@ -108,15 +191,18 @@ private void drawStack(GuiGraphics graphics, ItemStack stack, int x, int y, int } } - private void drawButton(GuiGraphics graphics, int x, int y, Component label, boolean enabled) { - graphics.fill(x, y, x + MarketScreenLogic.BUTTON_W, y + MarketScreenLogic.BUTTON_H, enabled ? SELECTED : 0x66000000); - graphics.drawString(this.font, label, x + 4, y + 6, TEXT, false); + private static boolean inside(int mouseX, int mouseY, int x, int y, int width, int height) { + return mouseX >= x && mouseY >= y && mouseX < x + width && mouseY < y + height; + } + + @Override + protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) { } @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { Inventory inventory = this.minecraft.player.getInventory(); - MarketScreenLogic.Click click = logic.click(mouseX, mouseY, this.leftPos, this.topPos, this.font.lineHeight + 2, this.menu.offers(), inventory); + MarketScreenLogic.Click click = logic.click(mouseX, mouseY, this.leftPos, this.topPos, this.menu.offers(), inventory); if (click == MarketScreenLogic.Click.ONCE && logic.selected != null) { send(1); } else if (click == MarketScreenLogic.Click.MAX && logic.selected != null) { @@ -125,10 +211,22 @@ public boolean mouseClicked(double mouseX, double mouseY, int button) { return click != MarketScreenLogic.Click.NONE || super.mouseClicked(mouseX, mouseY, button); } + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + logic.dragBar((int) mouseY - this.topPos, shownCount()); + return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + logic.releaseBar(); + return super.mouseReleased(mouseX, mouseY, button); + } + @Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { List shown = logic.visible(this.menu.offers(), this.minecraft.player.getInventory()); - logic.scroll(mouseX, mouseY, this.leftPos, this.topPos, scrollY, this.font.lineHeight + 2, shown.size(), logic.groups(this.menu.offers()).size()); + logic.scroll(mouseX, mouseY, this.leftPos, this.topPos, scrollY, shown.size(), logic.groups(this.menu.offers()).size()); return true; } diff --git a/versions/1.21.1/src/main/resources/data/villagermarket/advancement/recipes/misc/villager_market.json b/versions/1.21.1/src/main/resources/data/villagermarket/advancement/recipes/misc/villager_market.json new file mode 100644 index 0000000..a465308 --- /dev/null +++ b/versions/1.21.1/src/main/resources/data/villagermarket/advancement/recipes/misc/villager_market.json @@ -0,0 +1,43 @@ +{ + "parent": "minecraft:recipes/root", + "criteria": { + "has_planks": { + "conditions": { + "items": [ + { + "items": "#minecraft:planks" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_emerald_block": { + "conditions": { + "items": [ + { + "items": "minecraft:emerald_block" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "has_the_recipe": { + "conditions": { + "recipe": "villagermarket:villager_market" + }, + "trigger": "minecraft:recipe_unlocked" + } + }, + "requirements": [ + [ + "has_the_recipe", + "has_planks", + "has_emerald_block" + ] + ], + "rewards": { + "recipes": [ + "villagermarket:villager_market" + ] + } +}