Add shift click max buy/sell to the shop trait

This commit is contained in:
fullwall 2023-04-06 02:35:08 +08:00
parent d6ffa085a0
commit 163499f219
8 changed files with 177 additions and 98 deletions

View File

@ -97,25 +97,25 @@ public class CommandTrait extends Trait {
NPCShopAction action = null;
if (cost > 0) {
action = new MoneyAction(cost);
if (!action.take(player).isPossible()) {
if (!action.take(player, 1).isPossible()) {
sendErrorMessage(player, CommandTraitError.MISSING_MONEY, null, cost);
}
}
if (experienceCost > 0) {
action = new ExperienceAction(experienceCost);
if (!action.take(player).isPossible()) {
if (!action.take(player, 1).isPossible()) {
sendErrorMessage(player, CommandTraitError.MISSING_EXPERIENCE, null, experienceCost);
}
}
if (itemRequirements.size() > 0) {
action = new ItemAction(itemRequirements);
if (!action.take(player).isPossible()) {
if (!action.take(player, 1).isPossible()) {
ItemStack stack = itemRequirements.get(0);
sendErrorMessage(player, CommandTraitError.MISSING_ITEM, null, Util.prettyEnum(stack.getType()),
stack.getAmount());
}
}
return action == null ? Transaction.success() : action.take(player);
return action == null ? Transaction.success() : action.take(player, 1);
}
public void clearHistory(CommandTraitError which, String raw) {

View File

@ -262,8 +262,25 @@ public class ShopTrait extends Trait {
@Persist
private ItemStack display;
@Persist
private boolean maxRepeatsOnShiftClick;
@Persist
private final List<NPCShopAction> result = Lists.newArrayList();
public List<Transaction> apply(List<NPCShopAction> actions, Function<NPCShopAction, Transaction> func) {
List<Transaction> pending = Lists.newArrayList();
for (NPCShopAction action : actions) {
Transaction take = func.apply(action);
if (!take.isPossible()) {
pending.forEach(a -> a.rollback());
return null;
} else {
take.run();
pending.add(take);
}
}
return pending;
}
private void changeAction(List<NPCShopAction> source, Function<NPCShopAction, Boolean> filter,
NPCShopAction delta) {
for (int i = 0; i < source.size(); i++) {
@ -298,21 +315,6 @@ public class ShopTrait extends Trait {
}
}
public List<Transaction> execute(List<NPCShopAction> actions, Function<NPCShopAction, Transaction> func) {
List<Transaction> pending = Lists.newArrayList();
for (NPCShopAction action : actions) {
Transaction take = func.apply(action);
if (!take.isPossible()) {
pending.forEach(a -> a.rollback());
return null;
} else {
take.run();
pending.add(take);
}
}
return pending;
}
public ItemStack getDisplayItem(Player player) {
if (display == null)
return null;
@ -323,12 +325,8 @@ public class ShopTrait extends Trait {
}
if (!meta.hasLore()) {
List<String> lore = Lists.newArrayList();
if (cost.size() > 0) {
result.forEach(a -> lore.add(a.describe()));
}
if (result.size() > 0) {
result.forEach(a -> lore.add(a.describe()));
}
cost.forEach(a -> lore.add(a.describe()));
result.forEach(a -> lore.add(a.describe()));
}
if (meta.hasLore()) {
meta.setLore(Lists.transform(meta.getLore(), line -> placeholders(line, player)));
@ -351,11 +349,22 @@ public class ShopTrait extends Trait {
placeholders(clickToConfirmMessage, (Player) event.getWhoClicked()));
return;
}
List<Transaction> take = execute(cost, action -> action.take(event.getWhoClicked()));
int max = Integer.MAX_VALUE;
if (maxRepeatsOnShiftClick && event.isShiftClick()) {
for (NPCShopAction action : cost) {
int r = action.getMaxRepeats(event.getWhoClicked());
if (r != -1) {
max = Math.min(max, r);
}
}
}
final int repeats = max == Integer.MAX_VALUE ? 1 : max;
List<Transaction> take = apply(cost, action -> action.take(event.getWhoClicked(), repeats));
if (take == null)
return;
if (execute(result, action -> action.grant(event.getWhoClicked())) == null) {
if (apply(result, action -> action.grant(event.getWhoClicked(), repeats)) == null) {
take.forEach(a -> a.rollback());
return;
}
if (clickMessage != null) {
Messaging.sendColorless(event.getWhoClicked(),
@ -387,14 +396,14 @@ public class ShopTrait extends Trait {
@MenuPattern(
offset = { 0, 6 },
slots = { @MenuSlot(pat = 'x', material = Material.AIR) },
value = "x x\n x \nx x")
value = "xxx\nxxx\nxxx")
private InventoryMenuPattern actionItems;
private NPCShopItem base;
private final Consumer<NPCShopItem> callback;
@MenuPattern(
offset = { 0, 0 },
slots = { @MenuSlot(pat = 'x', material = Material.AIR) },
value = "x x\n x \nx x")
value = "xxx\nxxx\nxxx")
private InventoryMenuPattern costItems;
private MenuContext ctx;
private final NPCShopItem modified;
@ -412,7 +421,7 @@ public class ShopTrait extends Trait {
ctx.getSlot(9 * 4 + 4).setItemStack(modified.getDisplayItem(null));
}
ctx.getSlot(9 * 3 + 3).setItemStack(new ItemStack(Util.getFallbackMaterial("OAK_SIGN", "SIGN")),
"Set message to send on click, currently:",
"Set message to send on click, currently:\n",
modified.clickMessage == null ? "Unset" : modified.clickMessage);
ctx.getSlot(9 * 3 + 3).setClickHandler(
e -> ctx.getMenu().transition(InputMenus.stringSetter(() -> modified.clickMessage, s -> {
@ -428,6 +437,11 @@ public class ShopTrait extends Trait {
modified.clickToConfirmMessage = s;
ctx.getSlot(9 * 3 + 5).setDescription(modified.clickToConfirmMessage);
})));
ctx.getSlot(9 * 3 + 4).setItemStack(new ItemStack(Material.REDSTONE),
"Sell as many times as possible on shift click\n", "Currently: " + modified.maxRepeatsOnShiftClick);
ctx.getSlot(9 * 3 + 4).setClickHandler(
InputMenus.toggler(res -> modified.maxRepeatsOnShiftClick = res, modified.maxRepeatsOnShiftClick));
int pos = 0;
for (GUI template : NPCShopAction.getGUIs()) {
if (template.createMenuItem(null) == null)
@ -554,9 +568,8 @@ public class ShopTrait extends Trait {
@MenuSlot(slot = { 0, 4 }, material = Material.FEATHER, amount = 1)
public void editPageTitle(InventoryMenuSlot slot, CitizensInventoryClickEvent event) {
ctx.getMenu().transition(InputMenus.stringSetter(() -> page.title, newTitle -> {
page.title = newTitle.isEmpty() ? null : newTitle;
}));
ctx.getMenu().transition(InputMenus.stringSetter(() -> page.title,
newTitle -> page.title = newTitle.isEmpty() ? null : newTitle));
}
@Override
@ -606,16 +619,15 @@ public class ShopTrait extends Trait {
@MenuSlot(slot = { 0, 6 }, material = Material.NAME_TAG, amount = 1)
public void onSetTitle(InventoryMenuSlot slot, CitizensInventoryClickEvent event) {
ctx.getMenu().transition(InputMenus.stringSetter(() -> shop.title, newTitle -> {
shop.title = newTitle;
}));
ctx.getMenu().transition(InputMenus.stringSetter(() -> shop.title, newTitle -> shop.title = newTitle));
}
@MenuSlot(slot = { 0, 0 }, material = Material.BOOK, amount = 1, title = "<f>Edit shop type")
public void onShopTypeChange(InventoryMenuSlot slot, CitizensInventoryClickEvent event) {
ctx.getMenu().transition(InputMenus.<ShopType> picker("Edit shop type", chosen -> {
shop.type = chosen.getValue();
}, Choice.<ShopType> of(ShopType.BUY, Material.DIAMOND, "Players buy items", shop.type == ShopType.BUY),
ctx.getMenu().transition(InputMenus.<ShopType> picker("Edit shop type",
chosen -> shop.type = chosen.getValue(),
Choice.<ShopType> of(ShopType.BUY, Material.DIAMOND, "Players buy items",
shop.type == ShopType.BUY),
Choice.of(ShopType.SELL, Material.EMERALD, "Players sell items", shop.type == ShopType.SELL),
Choice.of(ShopType.COMMAND, Util.getFallbackMaterial("ENDER_EYE", "ENDER_PEARL"),
"Clicks trigger commands only", shop.type == ShopType.COMMAND)));

View File

@ -48,26 +48,35 @@ public class CommandAction extends NPCShopAction {
}
@Override
public Transaction grant(Entity entity) {
public int getMaxRepeats(Entity entity) {
return -1;
}
@Override
public Transaction grant(Entity entity, int repeats) {
if (!(entity instanceof Player))
return Transaction.fail();
Player player = (Player) entity;
return Transaction.create(() -> true, () -> {
for (String command : commands) {
Util.runCommand(null, player, command, op, !server);
for (int i = 0; i < repeats; i++) {
for (String command : commands) {
Util.runCommand(null, player, command, op, !server);
}
}
}, () -> {
});
}
@Override
public Transaction take(Entity entity) {
public Transaction take(Entity entity, int repeats) {
if (!(entity instanceof Player))
return Transaction.fail();
Player player = (Player) entity;
return Transaction.create(() -> true, () -> {
for (String command : commands) {
Util.runCommand(null, player, command, op, !server);
for (int i = 0; i < repeats; i++) {
for (String command : commands) {
Util.runCommand(null, player, command, op, !server);
}
}
}, () -> {
});
@ -122,10 +131,7 @@ public class CommandAction extends NPCShopAction {
}
ctx.getSlot(3 * 9 + 3).setItemStack(new ItemStack(Util.getFallbackMaterial("COMMAND_BLOCK", "COMMAND")),
"Run commands as server", base.server ? ChatColor.GREEN + "On" : ChatColor.RED + "OFF");
ctx.getSlot(3 * 9 + 3).addClickHandler(InputMenus.clickToggle((res) -> {
base.server = res;
return res ? ChatColor.GREEN + "On" : ChatColor.RED + "Off";
}, base.server));
ctx.getSlot(3 * 9 + 3).addClickHandler(InputMenus.toggler((res) -> base.server = res, base.server));
ctx.getSlot(3 * 9 + 4).setItemStack(
new ItemStack(Util.getFallbackMaterial("COMPARATOR", "REDSTONE_COMPARATOR")), "Run commands as op",
base.op ? ChatColor.GREEN + "On" : ChatColor.RED + "OFF");

View File

@ -29,30 +29,39 @@ public class ExperienceAction extends NPCShopAction {
}
@Override
public Transaction grant(Entity entity) {
public int getMaxRepeats(Entity entity) {
if (!(entity instanceof Player))
return 0;
return ((Player) entity).getLevel() / exp;
}
@Override
public Transaction grant(Entity entity, int repeats) {
if (!(entity instanceof Player))
return Transaction.fail();
Player player = (Player) entity;
int amount = exp * repeats;
return Transaction.create(() -> {
return true;
}, () -> {
player.setLevel(player.getLevel() + exp);
player.setLevel(player.getLevel() + amount);
}, () -> {
player.setLevel(player.getLevel() - exp);
player.setLevel(player.getLevel() - amount);
});
}
@Override
public Transaction take(Entity entity) {
public Transaction take(Entity entity, int repeats) {
if (!(entity instanceof Player))
return Transaction.fail();
Player player = (Player) entity;
int amount = exp * repeats;
return Transaction.create(() -> {
return player.getLevel() >= exp;
return player.getLevel() >= amount;
}, () -> {
player.setLevel(player.getLevel() - exp);
player.setLevel(player.getLevel() - amount);
}, () -> {
player.setLevel(player.getLevel() + exp);
player.setLevel(player.getLevel() + amount);
});
}

View File

@ -5,6 +5,7 @@ import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.bukkit.ChatColor;
import org.bukkit.Material;
@ -50,30 +51,24 @@ public class ItemAction extends NPCShopAction {
this.items = items;
}
private boolean containsItems(Inventory source, BiFunction<ItemStack, Integer, ItemStack> filter) {
List<Integer> req = items.stream().map(i -> i.getAmount()).collect(Collectors.toList());
private boolean containsItems(Inventory source, int repeats, BiFunction<ItemStack, Integer, ItemStack> filter) {
List<Integer> req = items.stream().map(i -> i.getAmount() * repeats).collect(Collectors.toList());
ItemStack[] contents = source.getContents();
for (int i = 0; i < contents.length; i++) {
ItemStack stack = contents[i];
if (stack == null || stack.getType() == Material.AIR)
ItemStack toMatch = contents[i];
if (toMatch == null || toMatch.getType() == Material.AIR)
continue;
if (requireUndamaged && stack.getItemMeta() instanceof Damageable
&& ((Damageable) stack.getItemMeta()).getDamage() != 0)
if (requireUndamaged && toMatch.getItemMeta() instanceof Damageable
&& ((Damageable) toMatch.getItemMeta()).getDamage() != 0)
continue;
for (int j = 0; j < items.size(); j++) {
ItemStack match = items.get(j);
if (req.get(j) <= 0)
continue;
if (match.getType() != stack.getType())
continue;
if (metaFilter.size() > 0 && !metaMatches(match, stack, metaFilter))
continue;
if (compareSimilarity && !match.isSimilar(stack))
ItemStack item = items.get(j);
if (req.get(j) <= 0 || !matches(item, toMatch))
continue;
int remaining = req.get(j);
int taken = stack.getAmount() > remaining ? remaining : stack.getAmount();
ItemStack res = filter.apply(stack, taken);
int taken = toMatch.getAmount() > remaining ? remaining : toMatch.getAmount();
ItemStack res = filter.apply(toMatch, taken);
if (res == null) {
source.clear(i);
} else {
@ -103,7 +98,33 @@ public class ItemAction extends NPCShopAction {
}
@Override
public Transaction grant(Entity entity) {
public int getMaxRepeats(Entity entity) {
if (!(entity instanceof InventoryHolder))
return 0;
Inventory source = ((InventoryHolder) entity).getInventory();
List<Integer> req = items.stream().map(i -> i.getAmount()).collect(Collectors.toList());
List<Integer> has = items.stream().map(i -> 0).collect(Collectors.toList());
ItemStack[] contents = source.getContents();
for (int i = 0; i < contents.length; i++) {
ItemStack toMatch = contents[i];
if (toMatch == null || toMatch.getType() == Material.AIR)
continue;
if (requireUndamaged && toMatch.getItemMeta() instanceof Damageable
&& ((Damageable) toMatch.getItemMeta()).getDamage() != 0)
continue;
for (int j = 0; j < items.size(); j++) {
if (!matches(items.get(j), toMatch))
continue;
has.set(j, has.get(j) + toMatch.getAmount());
}
}
return IntStream.range(0, req.size()).map(i -> req.get(i) == 0 ? 0 : has.get(i) / req.get(i)).reduce(Math::min)
.orElse(0);
}
@Override
public Transaction grant(Entity entity, int repeats) {
if (!(entity instanceof InventoryHolder))
return Transaction.fail();
Inventory source = ((InventoryHolder) entity).getInventory();
@ -115,14 +136,28 @@ public class ItemAction extends NPCShopAction {
continue;
}
}
return free >= items.size();
return free >= items.size() * repeats;
}, () -> {
source.addItem(items.stream().map(item -> item.clone()).toArray(ItemStack[]::new));
for (int i = 0; i < repeats; i++) {
source.addItem(items.stream().map(ItemStack::clone).toArray(ItemStack[]::new));
}
}, () -> {
source.removeItem(items.stream().map(item -> item.clone()).toArray(ItemStack[]::new));
for (int i = 0; i < repeats; i++) {
source.removeItem(items.stream().map(ItemStack::clone).toArray(ItemStack[]::new));
}
});
}
private boolean matches(ItemStack a, ItemStack b) {
if (a.getType() != b.getType())
return false;
if (metaFilter.size() > 0 && !metaMatches(a, b, metaFilter))
return false;
if (compareSimilarity && !a.isSimilar(b))
return false;
return true;
}
private boolean metaMatches(ItemStack needle, ItemStack haystack, List<String> meta) {
CompoundTag source = NMS.getNBT(needle);
CompoundTag compare = NMS.getNBT(haystack);
@ -154,14 +189,14 @@ public class ItemAction extends NPCShopAction {
}
@Override
public Transaction take(Entity entity) {
public Transaction take(Entity entity, int repeats) {
if (!(entity instanceof InventoryHolder))
return Transaction.fail();
Inventory source = ((InventoryHolder) entity).getInventory();
return Transaction.create(() -> {
return containsItems(source, (stack, taken) -> stack);
return containsItems(source, repeats, (stack, taken) -> stack);
}, () -> {
containsItems(source, (stack, taken) -> {
containsItems(source, repeats, (stack, taken) -> {
if (stack.getAmount() == taken) {
return null;
} else {
@ -170,7 +205,7 @@ public class ItemAction extends NPCShopAction {
}
});
}, () -> {
source.addItem(items.stream().map(item -> item.clone()).toArray(ItemStack[]::new));
source.addItem(items.stream().map(ItemStack::clone).toArray(ItemStack[]::new));
});
}
@ -205,17 +240,13 @@ public class ItemAction extends NPCShopAction {
ctx.getSlot(3 * 9 + 1).setItemStack(new ItemStack(Material.ANVIL), "Must have no damage",
base.requireUndamaged ? ChatColor.GREEN + "On" : ChatColor.RED + "Off");
ctx.getSlot(3 * 9 + 1).addClickHandler(InputMenus.clickToggle((res) -> {
base.requireUndamaged = res;
return res ? ChatColor.GREEN + "On" : ChatColor.RED + "Off";
}, base.requireUndamaged));
ctx.getSlot(3 * 9 + 1)
.addClickHandler(InputMenus.toggler((res) -> base.requireUndamaged = res, base.requireUndamaged));
ctx.getSlot(3 * 9 + 2).setItemStack(
new ItemStack(Util.getFallbackMaterial("COMPARATOR", "REDSTONE_COMPARATOR")),
"Compare item similarity", base.compareSimilarity ? ChatColor.GREEN + "On" : ChatColor.RED + "Off");
ctx.getSlot(3 * 9 + 2).addClickHandler(InputMenus.clickToggle((res) -> {
base.compareSimilarity = res;
return res ? ChatColor.GREEN + "On" : ChatColor.RED + "Off";
}, base.compareSimilarity));
ctx.getSlot(3 * 9 + 2)
.addClickHandler(InputMenus.toggler((res) -> base.compareSimilarity = res, base.compareSimilarity));
ctx.getSlot(3 * 9 + 3).setItemStack(new ItemStack(Material.BOOK), "NBT comparison filter",
Joiner.on("\n").join(base.metaFilter));
ctx.getSlot(3 * 9 + 3)

View File

@ -32,29 +32,43 @@ public class MoneyAction extends NPCShopAction {
}
@Override
public Transaction grant(Entity entity) {
public int getMaxRepeats(Entity entity) {
if (!(entity instanceof Player))
return 0;
Economy economy = Bukkit.getServicesManager().getRegistration(Economy.class).getProvider();
return (int) Math.floor(economy.getBalance((Player) entity) / money);
}
@Override
public Transaction grant(Entity entity, int repeats) {
if (!(entity instanceof Player))
return Transaction.fail();
Economy economy = Bukkit.getServicesManager().getRegistration(Economy.class).getProvider();
Player player = (Player) entity;
double amount = money * repeats;
return Transaction.create(() -> {
return true;
}, () -> {
economy.depositPlayer(player, money);
economy.depositPlayer(player, amount);
}, () -> {
economy.withdrawPlayer(player, money);
economy.withdrawPlayer(player, amount);
});
}
@Override
public Transaction take(Entity entity) {
public Transaction take(Entity entity, int repeats) {
if (!(entity instanceof Player))
return Transaction.fail();
Economy economy = Bukkit.getServicesManager().getRegistration(Economy.class).getProvider();
Player player = (Player) entity;
double amount = money * repeats;
return Transaction.create(() -> {
return economy.has(player, money);
}, () -> economy.withdrawPlayer(player, money), () -> economy.depositPlayer(player, money));
return economy.has(player, amount);
}, () -> {
economy.withdrawPlayer(player, amount);
}, () -> {
economy.depositPlayer(player, amount);
});
}
public static class MoneyActionGUI implements GUI {

View File

@ -24,9 +24,11 @@ public abstract class NPCShopAction implements Cloneable {
public abstract String describe();
public abstract Transaction grant(Entity entity);
public abstract int getMaxRepeats(Entity entity);
public abstract Transaction take(Entity entity);
public abstract Transaction grant(Entity entity, int repeats);
public abstract Transaction take(Entity entity, int repeats);
public static interface GUI {
public InventoryMenuPage createEditor(NPCShopAction previous, Consumer<NPCShopAction> callback);

View File

@ -46,7 +46,12 @@ public class PermissionAction extends NPCShopAction {
}
@Override
public Transaction grant(Entity entity) {
public int getMaxRepeats(Entity entity) {
return -1;
}
@Override
public Transaction grant(Entity entity, int repeats) {
if (!(entity instanceof Player))
return Transaction.fail();
Player player = (Player) entity;
@ -65,7 +70,7 @@ public class PermissionAction extends NPCShopAction {
}
@Override
public Transaction take(Entity entity) {
public Transaction take(Entity entity, int repeats) {
if (!(entity instanceof Player))
return Transaction.fail();
Player player = (Player) entity;