Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ After publishing, the action prepares the next development version and archives

## Unreleased

- The villager faces sit in the barrel instead of sticking out. The market opening is an oval. The automaton opening is an oval over a square cut, and its bottom stays solid. The nose is narrower.
- Simplified Chinese covers the Market Automaton and its window.
- The mod description mentions the Market Automaton as well as the market block.
- A Market Automaton trades on its own, at the same speed as a hopper. Choose an offer, put the payment in the two slots or let a hopper fill them, and take the result from the bottom. The choice stays when the trade locks, and you can take the payment back out of the window.
- The furnace arrow sits between the payment and the result. A button on the right side of the window chooses whether the block ignores redstone, runs only while powered, or runs only while unpowered.
- Each automaton offer shows `[max: uses left]`, or `[locked]` once none remain. That label updates while the window stays open.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Villager Market

One block for trading with nearby villagers.
Trade with nearby villagers from one block, or let a Market Automaton trade for you.

The Villager Market looks for villagers within 48 blocks, and 16 blocks up or down. Their offers are grouped by a custom name when they have one, and by profession otherwise. Show All, on by default, lists offers you cannot pay for yet. x1 buys the selected offer once. xN buys as many as you can pay for and the offer still has left. The window closes when you walk more than 8 blocks away.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,47 @@
import net.minecraft.gametest.framework.GameTestHelper;

public class FabricMarketGameTests {
@GameTest(structure = "minecraft:empty")
@GameTest(structure = "villagermarket:wide")
public void oneEmeraldBuysBread(GameTestHelper helper) {
MarketGameTests.oneEmeraldBuysBread(helper);
}

@GameTest(structure = "minecraft:empty")
@GameTest(structure = "villagermarket:wide")
public void componentsMustMatch(GameTestHelper helper) {
MarketGameTests.componentsMustMatch(helper);
}

@GameTest(structure = "minecraft:empty")
@GameTest(structure = "villagermarket:wide")
public void villagerOutsideTheBoxIsNotTraded(GameTestHelper helper) {
MarketGameTests.villagerOutsideTheBoxIsNotTraded(helper);
}

@GameTest(structure = "minecraft:empty")
@GameTest(structure = "villagermarket:wide")
public void adjustedPriceIsWhatIsCharged(GameTestHelper helper) {
MarketGameTests.adjustedPriceIsWhatIsCharged(helper);
}

@GameTest(structure = "minecraft:empty")
@GameTest(structure = "villagermarket:wide")
public void experienceLandsOnThePlayer(GameTestHelper helper) {
MarketGameTests.experienceLandsOnThePlayer(helper);
}

@GameTest(structure = "minecraft:empty")
@GameTest(structure = "villagermarket:wide")
public void automatonTradesFromItsSlots(GameTestHelper helper) {
MarketGameTests.automatonTradesFromItsSlots(helper);
}

@GameTest(structure = "minecraft:empty")
@GameTest(structure = "villagermarket:wide")
public void automatonRejectsADifferentBook(GameTestHelper helper) {
MarketGameTests.automatonRejectsADifferentBook(helper);
}

@GameTest(structure = "minecraft:empty")
@GameTest(structure = "villagermarket:wide")
public void automatonStopsWithoutRedstone(GameTestHelper helper) {
MarketGameTests.automatonStopsWithoutRedstone(helper);
}

@GameTest(structure = "minecraft:empty")
@GameTest(structure = "villagermarket:wide")
public void automatonBuysTheCheapestSimilar(GameTestHelper helper) {
MarketGameTests.automatonBuysTheCheapestSimilar(helper);
}
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ mod_author=Face_of_Cat
mod_id=villagermarket
license=MIT
credits=
description=One block for trading with nearby villagers.
description=Trade with nearby villagers from one block, or let a Market Automaton trade for you.

# Stonecutter refuses to run when the generated sources are not the VCS version.
dev.kikugie.stonecutter.hard_mode=true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ private static void registerTests(RegisterGameTestsEvent event) {
for (MarketGameTests.Case test : MarketGameTests.CASES) {
event.registerTest(id(test.name()), new FunctionGameTestInstance(
ResourceKey.create(Registries.TEST_FUNCTION, id(test.name())),
new TestData<>(environment, Identifier.withDefaultNamespace("empty"), 100, 0, true)
new TestData<>(environment, id("wide"), 100, 0, true)
));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.trading.ItemCost;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
//? if >=26.1
import net.minecraft.world.level.GameType;
import net.minecraft.world.item.trading.MerchantOffer;
import net.minecraft.world.item.trading.MerchantOffers;

import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

public final class MarketGameTests {
Expand Down Expand Up @@ -58,7 +61,7 @@ public static void oneEmeraldBuysBread(GameTestHelper helper) {

TradeExecutor.Result result = TradeExecutor.execute(player, villager.blockPosition(), villager.getId(), 0, 1);

helper.assertTrue(result.performed() == 1, "performed " + result.performed());
helper.assertTrue(result.performed() == 1, "performed " + result.performed() + " outOfRange " + result.outOfRange());
helper.assertTrue(count(player, Items.BREAD) == 1, "bread " + count(player, Items.BREAD));
helper.assertTrue(count(player, Items.EMERALD) == 0, "emeralds left " + count(player, Items.EMERALD));
helper.succeed();
Expand Down Expand Up @@ -114,23 +117,13 @@ public static void experienceLandsOnThePlayer(GameTestHelper helper) {
ServerPlayer player = player(helper);
player.setPos(villager.getX() + 8, villager.getY(), villager.getZ());
player.getInventory().add(new ItemStack(Items.EMERALD));
AABB area = villager.getBoundingBox().minmax(player.getBoundingBox()).inflate(2);
Set<Integer> before = orbIds(helper.getLevel(), area);

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");
assertRewardMoved(helper, area, before, player.position(), villager.position(), "player");
helper.succeed();
}

Expand All @@ -144,24 +137,14 @@ public static void automatonTradesFromItsSlots(GameTestHelper helper) {
machine.arm(deal.getCostA(), ItemStack.EMPTY, deal.getResult());
machine.setItem(MachineSlots.INPUT_A, new ItemStack(Items.DIAMOND, 3));
BlockPos absolute = helper.absolutePos(at);
AABB area = new AABB(absolute).inflate(8);
Set<Integer> before = orbIds(helper.getLevel(), area);
tick(helper, machine, absolute);
helper.assertTrue(machine.getItem(MachineSlots.OUTPUT).is(Items.BREAD) && machine.getItem(MachineSlots.OUTPUT).getCount() == 1, "first bread");
helper.assertTrue(machine.getItem(MachineSlots.INPUT_A).getCount() == 2, "diamonds after one trade");
helper.assertTrue(!machine.canTakeItemThroughFace(MachineSlots.INPUT_A, machine.getItem(MachineSlots.INPUT_A), Direction.NORTH), "a side hopper pulled the payment");
helper.assertTrue(machine.canTakeItemThroughFace(MachineSlots.OUTPUT, machine.getItem(MachineSlots.OUTPUT), Direction.DOWN), "the bottom could not take the result");
AABB area = new AABB(absolute).inflate(8);
boolean onBlock = false;
boolean onVillager = false;
for (ExperienceOrb orb : helper.getLevel().getEntitiesOfClass(ExperienceOrb.class, area)) {
if (orb.distanceToSqr(Vec3.atCenterOf(absolute)) < 4) {
onBlock = true;
}
if (orb.distanceToSqr(villager) < 4) {
onVillager = true;
}
}
helper.assertTrue(onBlock, "the experience orb was not on the automaton");
helper.assertTrue(!onVillager, "the experience orb stayed on the villager");
assertRewardMoved(helper, area, before, Vec3.atCenterOf(absolute), villager.position(), "automaton");
tick(helper, machine, absolute);
helper.assertTrue(machine.getItem(MachineSlots.OUTPUT).getCount() == 2, "second bread");
helper.assertTrue(machine.getItem(MachineSlots.INPUT_A).getCount() == 1, "the locked trade kept spending");
Expand Down Expand Up @@ -232,6 +215,32 @@ public static void automatonBuysTheCheapestSimilar(GameTestHelper helper) {
helper.succeed();
}

private static Set<Integer> orbIds(Level level, AABB area) {
Set<Integer> ids = new HashSet<>();
for (ExperienceOrb orb : level.getEntitiesOfClass(ExperienceOrb.class, area)) {
ids.add(orb.getId());
}
return ids;
}

private static void assertRewardMoved(GameTestHelper helper, AABB area, Set<Integer> before, Vec3 destination, Vec3 origin, String where) {
boolean arrived = false;
boolean stayed = false;
for (ExperienceOrb orb : helper.getLevel().getEntitiesOfClass(ExperienceOrb.class, area)) {
if (before.contains(orb.getId())) {
continue;
}
if (orb.distanceToSqr(destination) < 4.0) {
arrived = true;
}
if (orb.distanceToSqr(origin) < 4.0) {
stayed = true;
}
}
helper.assertTrue(arrived, "the experience orb was not on the " + where);
helper.assertTrue(!stayed, "the experience orb stayed on the villager");
}

private static MarketAutomatonBlockEntity machine(GameTestHelper helper, BlockPos at) {
//? if >=26.1 {
return helper.getBlockEntity(at, MarketAutomatonBlockEntity.class);
Expand Down
10 changes: 9 additions & 1 deletion src/main/resources/assets/villagermarket/lang/zh_cn.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
{
"block.villagermarket.villager_market": "村民市场方块",
"block.villagermarket.market_automaton": "市场自动机方块",
"itemGroup.villagermarket": "村民市场",
"gui.villagermarket.all": "[ 全部 ]",
"gui.villagermarket.show_all": "显示全部",
"gui.villagermarket.available": "可购买",
"gui.villagermarket.once": "x 1",
"gui.villagermarket.offer_count": "[可购买 / 锁定前剩余]"
"gui.villagermarket.offer_count": "[可购买 / 锁定前剩余]",
"gui.villagermarket.max_uses": "[上限: %s]",
"gui.villagermarket.locked": "[已锁定]",
"gui.villagermarket.redstone.ignored": "忽略红石",
"gui.villagermarket.redstone.powered": "充能时运行",
"gui.villagermarket.redstone.unpowered": "未充能时运行",
"gui.villagermarket.similar.on": "包含相似交易,并购买当前能支付的最便宜的一项",
"gui.villagermarket.similar.off": "仅限所选交易"
}
Loading
Loading