diff --git a/gradle.properties b/gradle.properties index cf51ce0..cd24e1e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,8 @@ # Plugin properties -repo=https://github.com/JouTak/TemplatePlugin-Java +repo=https://github.com/gafrus06/TemplatePlugin-Java group=ru.joutak -name=Template-Java -version=1.0.0 +name=JoyBank +version=0.1.0 # Gradle properties org.gradle.parallel=true \ No newline at end of file diff --git a/resourcepack/JoyBankPack/assets/minecraft/items/gold_nugget.json b/resourcepack/JoyBankPack/assets/minecraft/items/gold_nugget.json new file mode 100644 index 0000000..fd4770f --- /dev/null +++ b/resourcepack/JoyBankPack/assets/minecraft/items/gold_nugget.json @@ -0,0 +1,20 @@ +{ + "model": { + "type": "minecraft:range_dispatch", + "property": "minecraft:custom_model_data", + "index": 0, + "entries": [ + { + "threshold": 1001.0, + "model": { + "type": "minecraft:model", + "model": "minecraft:item/joy_coin" + } + } + ], + "fallback": { + "type": "minecraft:model", + "model": "minecraft:item/gold_nugget" + } + } +} \ No newline at end of file diff --git a/resourcepack/JoyBankPack/assets/minecraft/models/item/joy_coin.json b/resourcepack/JoyBankPack/assets/minecraft/models/item/joy_coin.json new file mode 100644 index 0000000..3196b5a --- /dev/null +++ b/resourcepack/JoyBankPack/assets/minecraft/models/item/joy_coin.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:item/joy_coin" + } +} \ No newline at end of file diff --git a/resourcepack/JoyBankPack/assets/minecraft/textures/item/joy_coin.png b/resourcepack/JoyBankPack/assets/minecraft/textures/item/joy_coin.png new file mode 100644 index 0000000..6173eed Binary files /dev/null and b/resourcepack/JoyBankPack/assets/minecraft/textures/item/joy_coin.png differ diff --git a/resourcepack/JoyBankPack/pack.mcmeta b/resourcepack/JoyBankPack/pack.mcmeta new file mode 100644 index 0000000..693434b --- /dev/null +++ b/resourcepack/JoyBankPack/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "pack_format": 55, + "description": "JoyBank Resource Pack" + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/EmptyPlugin.java b/src/main/java/ru/joutak/template/EmptyPlugin.java deleted file mode 100644 index d93430e..0000000 --- a/src/main/java/ru/joutak/template/EmptyPlugin.java +++ /dev/null @@ -1,23 +0,0 @@ -package ru.joutak.template; - -import lombok.Getter; -import org.bukkit.plugin.java.JavaPlugin; - -public final class EmptyPlugin extends JavaPlugin { - @Getter - private static EmptyPlugin instance; - - @Override - public void onEnable() { - instance = this; - - getLogger().info( - String.format("Плагин %s версии %s включен!", getPluginMeta().getName(), getPluginMeta().getVersion()) - ); - } - - @Override - public void onDisable() { - - } -} diff --git a/src/main/java/ru/joutak/template/JoyBankPlugin.java b/src/main/java/ru/joutak/template/JoyBankPlugin.java new file mode 100644 index 0000000..d44a5ef --- /dev/null +++ b/src/main/java/ru/joutak/template/JoyBankPlugin.java @@ -0,0 +1,90 @@ +package ru.joutak.template; + +import lombok.Getter; +import org.bukkit.plugin.java.JavaPlugin; +import ru.joutak.template.atm.AtmService; +import ru.joutak.template.atm.listener.AtmMenuListener; +import ru.joutak.template.coin.CoinService; +import ru.joutak.template.command.JoyCommand; +import ru.joutak.template.bank.BankService; +import ru.joutak.template.storage.AccountRepository; +import ru.joutak.template.storage.YamlAccountRepository; +import ru.joutak.template.storage.AtmRepository; +import ru.joutak.template.storage.YamlAtmRepository; +import ru.joutak.template.atm.listener.AtmCreationListener; +import ru.joutak.template.gui.AtmMenu; +import ru.joutak.template.atm.listener.AtmProtectionListener; + +import ru.joutak.template.atm.listener.AtmInteractionListener; + +public final class JoyBankPlugin extends JavaPlugin { + @Getter + private static JoyBankPlugin instance; + private CoinService coinService; + private BankService bankService; + private AtmService atmService; + private AtmMenu atmMenu; + + @Override + public void onEnable() { + instance = this; + coinService = new CoinService(this); + AccountRepository accountRepository = + new YamlAccountRepository(this); + bankService = new BankService(accountRepository); + + AtmRepository atmRepository = + new YamlAtmRepository(this); + + atmService = + new AtmService(atmRepository); + + atmMenu = new AtmMenu(bankService); + + getCommand("joy") + .setExecutor( + new JoyCommand(coinService, bankService) + ); + getLogger().info( + String.format("Плагин %s версии %s включен!", getPluginMeta().getName(), getPluginMeta().getVersion()) + ); + + getServer() + .getPluginManager() + .registerEvents( + new AtmCreationListener(atmService), + this + ); + getServer() + .getPluginManager() + .registerEvents( + new AtmInteractionListener( + atmService, + atmMenu + ), + this + ); + getServer() + .getPluginManager() + .registerEvents( + new AtmMenuListener( + coinService, + bankService, + atmService, + atmMenu + ), + this + ); + getServer() + .getPluginManager() + .registerEvents( + new AtmProtectionListener(atmService), + this + ); + } + + @Override + public void onDisable() { + + } +} diff --git a/src/main/java/ru/joutak/template/atm/Atm.java b/src/main/java/ru/joutak/template/atm/Atm.java new file mode 100644 index 0000000..fd3fcb6 --- /dev/null +++ b/src/main/java/ru/joutak/template/atm/Atm.java @@ -0,0 +1,15 @@ +package ru.joutak.template.atm; + +import java.util.UUID; + +public record Atm( + UUID id, + UUID worldId, + int barrelX, + int barrelY, + int barrelZ, + int signX, + int signY, + int signZ +) { +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/atm/AtmService.java b/src/main/java/ru/joutak/template/atm/AtmService.java new file mode 100644 index 0000000..df037ec --- /dev/null +++ b/src/main/java/ru/joutak/template/atm/AtmService.java @@ -0,0 +1,102 @@ +package ru.joutak.template.atm; + +import org.bukkit.Location; +import org.bukkit.World; +import ru.joutak.template.storage.AtmRepository; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public class AtmService { + + private final AtmRepository atmRepository; + private final Map atms = new HashMap<>(); + + public AtmService(AtmRepository atmRepository) { + this.atmRepository = atmRepository; + + for (Atm atm : atmRepository.findAll()) { + atms.put(atm.id(), atm); + } + } + + public Atm register( + Location barrelLocation, + Location signLocation + ) { + Atm atm = new Atm( + UUID.randomUUID(), + barrelLocation.getWorld().getUID(), + + barrelLocation.getBlockX(), + barrelLocation.getBlockY(), + barrelLocation.getBlockZ(), + + signLocation.getBlockX(), + signLocation.getBlockY(), + signLocation.getBlockZ() + ); + atmRepository.save(atm); + atms.put(atm.id(), atm); + + return atm; + } + + public Optional findByBlock(Location location) { + World world = location.getWorld(); + + if (world == null) { + return Optional.empty(); + } + + for (Atm atm : atms.values()) { + + if (!atm.worldId().equals(world.getUID())) { + continue; + } + + int x = location.getBlockX(); + int y = location.getBlockY(); + int z = location.getBlockZ(); + + boolean isBarrel = + x == atm.barrelX() + && y == atm.barrelY() + && z == atm.barrelZ(); + + boolean isSign = + x == atm.signX() + && y == atm.signY() + && z == atm.signZ(); + + boolean isBase = + x == atm.barrelX() + && y == atm.barrelY() - 1 + && z == atm.barrelZ(); + + if (isBarrel || isSign || isBase) { + return Optional.of(atm); + } + } + + return Optional.empty(); + } + + public Collection getAll() { + return atms.values(); + } + + public void unregister(UUID atmId) { + + atmRepository.delete(atmId); + atms.remove(atmId); + } + public Optional findById(UUID atmId) { + return Optional.ofNullable( + atms.get(atmId) + ); + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/atm/listener/AtmCreationListener.java b/src/main/java/ru/joutak/template/atm/listener/AtmCreationListener.java new file mode 100644 index 0000000..38ab367 --- /dev/null +++ b/src/main/java/ru/joutak/template/atm/listener/AtmCreationListener.java @@ -0,0 +1,92 @@ +package ru.joutak.template.atm.listener; + +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.WallSign; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.SignChangeEvent; +import ru.joutak.template.atm.AtmService; + +public class AtmCreationListener implements Listener { + + private final AtmService atmService; + + public AtmCreationListener(AtmService atmService) { + this.atmService = atmService; + } + + @EventHandler + public void onSignChange(SignChangeEvent event) { + + String firstLine = event.getLine(0); + + if (firstLine == null + || !firstLine.equalsIgnoreCase("[joybank]")) { + return; + } + + if (!event.getPlayer() + .hasPermission("joybank.atm.create")) { + + event.getPlayer().sendMessage( + "У вас нет прав на создание банкомата." + ); + + return; + } + + Block signBlock = event.getBlock(); + + BlockData blockData = signBlock.getBlockData(); + + if (!(blockData instanceof WallSign wallSign)) { + event.getPlayer().sendMessage( + "Табличка должна быть прикреплена к боку бочки." + ); + return; + } + + Block barrel = signBlock.getRelative( + wallSign.getFacing().getOppositeFace() + ); + + if (barrel.getType() != Material.BARREL) { + event.getPlayer().sendMessage( + "Табличка должна быть прикреплена к бочке." + ); + return; + } + + Block base = barrel.getRelative(0, -1, 0); + + if (base.getType() != Material.IRON_BLOCK) { + event.getPlayer().sendMessage( + "Под бочкой должен находиться железный блок." + ); + return; + } + + if (atmService.findByBlock(barrel.getLocation()).isPresent()) { + event.getPlayer().sendMessage( + "Этот банкомат уже зарегистрирован." + ); + return; + } + + atmService.register( + barrel.getLocation(), + signBlock.getLocation() + ); + + event.setLine(0, "JOY BANK"); + event.setLine(1, "Банкомат"); + event.setLine(2, ""); + event.setLine(3, ""); + + event.getPlayer().sendMessage( + "Банкомат успешно зарегистрирован." + ); + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/atm/listener/AtmInteractionListener.java b/src/main/java/ru/joutak/template/atm/listener/AtmInteractionListener.java new file mode 100644 index 0000000..5504c5e --- /dev/null +++ b/src/main/java/ru/joutak/template/atm/listener/AtmInteractionListener.java @@ -0,0 +1,56 @@ +package ru.joutak.template.atm.listener; + +import org.bukkit.block.Block; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import ru.joutak.template.atm.Atm; +import ru.joutak.template.atm.AtmService; +import ru.joutak.template.gui.AtmMenu; + +import java.util.Optional; + +public class AtmInteractionListener implements Listener { + + private final AtmService atmService; + private final AtmMenu atmMenu; + + public AtmInteractionListener( + AtmService atmService, + AtmMenu atmMenu + ) { + this.atmService = atmService; + this.atmMenu = atmMenu; + } + + @EventHandler + public void onInteract(PlayerInteractEvent event) { + + if (event.getAction() != Action.RIGHT_CLICK_BLOCK) { + return; + } + + Block block = event.getClickedBlock(); + + if (block == null) { + return; + } + + Optional atm = + atmService.findByBlock( + block.getLocation() + ); + + if (atm.isEmpty()) { + return; + } + + event.setCancelled(true); + + atmMenu.open( + event.getPlayer(), + atm.get() + ); + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java b/src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java new file mode 100644 index 0000000..c367981 --- /dev/null +++ b/src/main/java/ru/joutak/template/atm/listener/AtmMenuListener.java @@ -0,0 +1,288 @@ +package ru.joutak.template.atm.listener; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import ru.joutak.template.atm.Atm; +import ru.joutak.template.atm.AtmService; +import ru.joutak.template.bank.BankService; +import ru.joutak.template.coin.CoinService; +import ru.joutak.template.gui.AtmMenu; +import ru.joutak.template.gui.AtmMenuHolder; + +import java.util.Optional; + +public class AtmMenuListener implements Listener { + + private final CoinService coinService; + private final BankService bankService; + private final AtmService atmService; + private final AtmMenu atmMenu; + + public AtmMenuListener( + CoinService coinService, + BankService bankService, + AtmService atmService, + AtmMenu atmMenu + ) { + this.coinService = coinService; + this.bankService = bankService; + this.atmService = atmService; + this.atmMenu = atmMenu; + } + + @EventHandler + public void onClick(InventoryClickEvent event) { + + if (!(event.getView() + .getTopInventory() + .getHolder(false) + instanceof AtmMenuHolder holder)) { + return; + } + event.setCancelled(true); + + if (!(event.getWhoClicked() instanceof Player player)) { + return; + } + if (event.getRawSlot() + >= event.getView().getTopInventory().getSize()) { + return; + } + + Optional optionalAtm = + atmService.findById(holder.getAtmId()); + + if (optionalAtm.isEmpty()) { + player.closeInventory(); + player.sendMessage("Этот банкомат больше не существует."); + return; + } + + Atm atm = optionalAtm.get(); + + switch (event.getRawSlot()) { + + case 9 -> deposit(player, atm, 1); + case 10 -> deposit(player, atm, 16); + case 11 -> deposit(player, atm, 64); + case 12 -> depositAll(player, atm); + + case 14 -> withdraw(player, atm, 1); + case 15 -> withdraw(player, atm, 16); + case 16 -> withdraw(player, atm, 64); + case 17 -> withdrawAll(player, atm); + } + } + + @EventHandler + public void onDrag(InventoryDragEvent event) { + + if (event.getView() + .getTopInventory() + .getHolder(false) + instanceof AtmMenuHolder) { + + event.setCancelled(true); + } + } + + private void deposit( + Player player, + Atm atm, + int amount + ) { + + int available = + coinService.countCoins( + player.getInventory() + ); + + if (available < amount) { + player.sendMessage( + "Недостаточно наличных. У вас: " + + available + " Джой." + ); + return; + } + + boolean removed = + coinService.removeCoins( + player.getInventory(), + amount + ); + + if (!removed) { + player.sendMessage( + "Не удалось забрать монеты." + ); + return; + } + + try { + + bankService.deposit( + player.getUniqueId(), + amount + ); + + } catch (RuntimeException e) { + + // Возвращаем физические деньги + coinService.addCoins( + player.getInventory(), + amount + ); + + player.sendMessage( + "Ошибка сохранения. Монеты возвращены." + ); + + return; + } + + player.sendMessage( + "Внесено: " + amount + " Джой." + ); + + atmMenu.open(player, atm); + } + private void depositAll( + Player player, + Atm atm + ) { + + int amount = + coinService.countCoins( + player.getInventory() + ); + + if (amount == 0) { + player.sendMessage( + "У вас нет наличных Джой." + ); + return; + } + + deposit( + player, + atm, + amount + ); + } + private void withdraw( + Player player, + Atm atm, + int amount + ) { + + long balance = + bankService.getBalance( + player.getUniqueId() + ); + + if (balance < amount) { + player.sendMessage( + "Недостаточно денег на счёте. Баланс: " + + balance + " Джой." + ); + return; + } + + if (!coinService.canFitCoins( + player.getInventory(), + amount + )) { + player.sendMessage( + "Недостаточно места в инвентаре." + ); + return; + } + + boolean added = + coinService.addCoins( + player.getInventory(), + amount + ); + + if (!added) { + player.sendMessage( + "Не удалось выдать монеты." + ); + return; + } + + try { + + boolean withdrawn = + bankService.withdraw( + player.getUniqueId(), + amount + ); + + if (!withdrawn) { + + coinService.removeCoins( + player.getInventory(), + amount + ); + + player.sendMessage( + "Не удалось снять деньги." + ); + + return; + } + + } catch (RuntimeException e) { + coinService.removeCoins( + player.getInventory(), + amount + ); + + player.sendMessage( + "Ошибка сохранения. Операция отменена." + ); + + return; + } + + player.sendMessage( + "Снято: " + amount + " Джой." + ); + + atmMenu.open(player, atm); + } + private void withdrawAll( + Player player, + Atm atm + ) { + + long balance = + bankService.getBalance( + player.getUniqueId() + ); + + if (balance <= 0) { + player.sendMessage( + "На счёте нет денег." + ); + return; + } + + if (balance > Integer.MAX_VALUE) { + player.sendMessage( + "Баланс слишком большой для одной операции." + ); + return; + } + + withdraw( + player, + atm, + (int) balance + ); + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/atm/listener/AtmProtectionListener.java b/src/main/java/ru/joutak/template/atm/listener/AtmProtectionListener.java new file mode 100644 index 0000000..ac364dd --- /dev/null +++ b/src/main/java/ru/joutak/template/atm/listener/AtmProtectionListener.java @@ -0,0 +1,134 @@ +package ru.joutak.template.atm.listener; + +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import ru.joutak.template.atm.Atm; +import ru.joutak.template.atm.AtmService; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockBurnEvent; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.block.BlockPistonExtendEvent; +import org.bukkit.event.block.BlockPistonRetractEvent; +import org.bukkit.event.entity.EntityChangeBlockEvent; +import org.bukkit.event.entity.EntityExplodeEvent; + +import java.util.Optional; + +public class AtmProtectionListener implements Listener { + + private final AtmService atmService; + + public AtmProtectionListener(AtmService atmService) { + this.atmService = atmService; + } + + + @EventHandler + public void onEntityExplode(EntityExplodeEvent event) { + event.blockList().removeIf(this::isAtmBlock); + } + + @EventHandler + public void onBlockExplode(BlockExplodeEvent event) { + event.blockList().removeIf(this::isAtmBlock); + } + private boolean isAtmBlock(Block block) { + return atmService + .findByBlock(block.getLocation()) + .isPresent(); + } + + @EventHandler + public void onPistonExtend(BlockPistonExtendEvent event) { + + boolean movesAtm = event.getBlocks() + .stream() + .anyMatch(this::isAtmBlock); + + if (movesAtm) { + event.setCancelled(true); + } + } + + @EventHandler + public void onPistonRetract(BlockPistonRetractEvent event) { + + boolean movesAtm = event.getBlocks() + .stream() + .anyMatch(this::isAtmBlock); + + if (movesAtm) { + event.setCancelled(true); + } + } + + @EventHandler + public void onBurn(BlockBurnEvent event) { + if (isAtmBlock(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler + public void onEntityChangeBlock(EntityChangeBlockEvent event) { + if (isAtmBlock(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler( + priority = EventPriority.HIGHEST, + ignoreCancelled = true + ) + public void onBlockBreakProtection(BlockBreakEvent event) { + + if (!isAtmBlock(event.getBlock())) { + return; + } + + if (!event.getPlayer() + .hasPermission("joybank.atm.break")) { + + event.setCancelled(true); + + event.getPlayer().sendMessage( + "Вы не можете разрушить зарегистрированный банкомат." + ); + } + } + + @EventHandler( + priority = EventPriority.MONITOR, + ignoreCancelled = true + ) + public void onBlockBreakCleanup(BlockBreakEvent event) { + + Optional atm = + atmService.findByBlock( + event.getBlock().getLocation() + ); + + if (atm.isEmpty()) { + return; + } + + if (!event.getPlayer() + .hasPermission("joybank.atm.break")) { + return; + } + + atmService.unregister( + atm.get().id() + ); + + event.getPlayer().sendMessage( + "Банкомат удалён." + ); + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/bank/BankService.java b/src/main/java/ru/joutak/template/bank/BankService.java new file mode 100644 index 0000000..5004a34 --- /dev/null +++ b/src/main/java/ru/joutak/template/bank/BankService.java @@ -0,0 +1,83 @@ +package ru.joutak.template.bank; + +import ru.joutak.template.storage.AccountRepository; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +public class BankService { + + private final AccountRepository accountRepository; + + public BankService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + } + + public long getBalance(UUID playerId) { + return accountRepository.getBalance(playerId); + } + + public List getTransactions(UUID playerId) { + return accountRepository.getTransactions(playerId); + } + + public void deposit(UUID playerId, long amount) { + + if (amount <= 0) { + throw new IllegalArgumentException( + "Сумма пополнения должна быть больше нуля" + ); + } + + long currentBalance = getBalance(playerId); + + long newBalance = Math.addExact( + currentBalance, + amount + ); + + Transaction transaction = new Transaction( + TransactionType.DEPOSIT, + amount, + Instant.now() + ); + + accountRepository.applyTransaction( + playerId, + newBalance, + transaction + ); + } + + + public boolean withdraw(UUID playerId, long amount) { + + if (amount <= 0) { + return false; + } + + long currentBalance = getBalance(playerId); + + if (currentBalance < amount) { + return false; + } + + long newBalance = + currentBalance - amount; + + Transaction transaction = new Transaction( + TransactionType.WITHDRAW, + amount, + Instant.now() + ); + + accountRepository.applyTransaction( + playerId, + newBalance, + transaction + ); + + return true; + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/bank/Transaction.java b/src/main/java/ru/joutak/template/bank/Transaction.java new file mode 100644 index 0000000..8dc33e7 --- /dev/null +++ b/src/main/java/ru/joutak/template/bank/Transaction.java @@ -0,0 +1,10 @@ +package ru.joutak.template.bank; + +import java.time.Instant; + +public record Transaction( + TransactionType type, + long amount, + Instant timestamp +) { +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/bank/TransactionType.java b/src/main/java/ru/joutak/template/bank/TransactionType.java new file mode 100644 index 0000000..5803e2b --- /dev/null +++ b/src/main/java/ru/joutak/template/bank/TransactionType.java @@ -0,0 +1,6 @@ +package ru.joutak.template.bank; + +public enum TransactionType { + DEPOSIT, + WITHDRAW +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/coin/CoinService.java b/src/main/java/ru/joutak/template/coin/CoinService.java new file mode 100644 index 0000000..9b084fd --- /dev/null +++ b/src/main/java/ru/joutak/template/coin/CoinService.java @@ -0,0 +1,180 @@ +package ru.joutak.template.coin; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.inventory.meta.components.CustomModelDataComponent; + +import java.util.List; + +import java.util.Map; + +public class CoinService { + + private final NamespacedKey coinKey; + private static final float JOY_MODEL_DATA = 1001.0f; + + public CoinService(JavaPlugin plugin) { + this.coinKey = new NamespacedKey(plugin, "joy_coin"); + } + + public ItemStack createCoin(int amount) { + ItemStack coin = + new ItemStack(Material.GOLD_NUGGET, amount); + + ItemMeta meta = coin.getItemMeta(); + + meta.displayName( + Component.text( + "Джой", + NamedTextColor.GOLD + ) + ); + + meta.getPersistentDataContainer().set( + coinKey, + PersistentDataType.BYTE, + (byte) 1 + ); + + CustomModelDataComponent modelData = + meta.getCustomModelDataComponent(); + + modelData.setFloats( + List.of(JOY_MODEL_DATA) + ); + + meta.setCustomModelDataComponent(modelData); + + coin.setItemMeta(meta); + + return coin; + } + + public boolean isCoin(ItemStack item) { + if (item == null) { + return false; + } + + if (item.getType() != Material.GOLD_NUGGET) { + return false; + } + + ItemMeta meta = item.getItemMeta(); + + return meta.getPersistentDataContainer().has( + coinKey, + PersistentDataType.BYTE + ); + } + + public int countCoins(PlayerInventory inventory) { + int total = 0; + + for (ItemStack item : inventory.getContents()) { + if (isCoin(item)) { + total += item.getAmount(); + } + } + + return total; + } + public boolean removeCoins(PlayerInventory inventory, int amount) { + if (amount <= 0) { + return false; + } + + if (countCoins(inventory) < amount) { + return false; + } + + int remaining = amount; + + for (int slot = 0; slot < inventory.getSize(); slot++) { + ItemStack item = inventory.getItem(slot); + + if (!isCoin(item)) { + continue; + } + + int stackAmount = item.getAmount(); + + if (stackAmount <= remaining) { + remaining -= stackAmount; + inventory.setItem(slot, null); + } else { + item.setAmount(stackAmount - remaining); + remaining = 0; + } + + if (remaining == 0) { + break; + } + } + + return true; + } + public int getFreeCoinCapacity(PlayerInventory inventory) { + ItemStack template = createCoin(1); + int capacity = 0; + + for (ItemStack item : inventory.getStorageContents()) { + if (item == null || item.getType().isAir()) { + capacity += template.getMaxStackSize(); + continue; + } + + if (item.isSimilar(template)) { + capacity += item.getMaxStackSize() - item.getAmount(); + } + } + return capacity; + } + + public boolean canFitCoins( + PlayerInventory inventory, + int amount + ) { + if (amount <= 0) { + return false; + } + + return getFreeCoinCapacity(inventory) >= amount; + } + public boolean addCoins( + PlayerInventory inventory, + int amount + ) { + if (!canFitCoins(inventory, amount)) { + return false; + } + + int remaining = amount; + + while (remaining > 0) { + int stackAmount = Math.min( + remaining, + 64 + ); + + ItemStack stack = createCoin(stackAmount); + + Map leftovers = + inventory.addItem(stack); + + if (!leftovers.isEmpty()) { + return false; + } + + remaining -= stackAmount; + } + + return true; + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/command/JoyCommand.java b/src/main/java/ru/joutak/template/command/JoyCommand.java new file mode 100644 index 0000000..eb0880f --- /dev/null +++ b/src/main/java/ru/joutak/template/command/JoyCommand.java @@ -0,0 +1,118 @@ +package ru.joutak.template.command; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import ru.joutak.template.bank.BankService; +import ru.joutak.template.coin.CoinService; +import org.bukkit.entity.Player; +import ru.joutak.template.bank.Transaction; + +import java.util.List; + +public class JoyCommand implements CommandExecutor { + + private final CoinService coinService; + private final BankService bankService; + + public JoyCommand(CoinService coinService, BankService bankService) { + this.coinService = coinService; + this.bankService = bankService; + } + + @Override + public boolean onCommand( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String[] args + ){ + if(!(sender instanceof Player player)){ + sender.sendMessage("Эту команду может использовать только игрок"); + return true; + } + if(args.length == 0){ + player.sendMessage("Команда: /joy "); + return true; + } + if (args[0].equalsIgnoreCase("history")) { + + List transactions = + bankService.getTransactions(player.getUniqueId()); + + if (transactions.isEmpty()) { + player.sendMessage("История операций пуста."); + return true; + } + + player.sendMessage("История операций:"); + + for (Transaction transaction : transactions) { + player.sendMessage( + transaction.type() + + " | " + + transaction.amount() + + " Джой | " + + transaction.timestamp() + ); + } + + return true; + } + if (args[0].equalsIgnoreCase("balance")) { + + long balance = bankService.getBalance( + player.getUniqueId() + ); + + player.sendMessage( + "Ваш баланс: " + balance + " Джой." + ); + + return true; + } + if (args[0].equalsIgnoreCase("give")) { + + if (!player.hasPermission("joybank.admin.give")) { + player.sendMessage("У вас нет прав на эту команду."); + return true; + } + + if (args.length < 2) { + player.sendMessage("Использование: /joy give <количество>"); + return true; + } + + int amount; + + try { + amount = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + player.sendMessage("Количество должно быть числом."); + return true; + } + + if (amount <= 0) { + player.sendMessage("Количество должно быть больше 0."); + return true; + } + + boolean added = coinService.addCoins( + player.getInventory(), + amount + ); + + if (!added) { + player.sendMessage("В инвентаре недостаточно места."); + return true; + } + + player.sendMessage("Вы получили " + amount + " Джой."); + + return true; + } + player.sendMessage("Неизвестная команда"); + return true; + } +} diff --git a/src/main/java/ru/joutak/template/gui/AtmMenu.java b/src/main/java/ru/joutak/template/gui/AtmMenu.java new file mode 100644 index 0000000..a7398f7 --- /dev/null +++ b/src/main/java/ru/joutak/template/gui/AtmMenu.java @@ -0,0 +1,101 @@ +package ru.joutak.template.gui; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import ru.joutak.template.atm.Atm; +import ru.joutak.template.bank.BankService; + +public class AtmMenu { + + private final BankService bankService; + + public AtmMenu(BankService bankService) { + this.bankService = bankService; + } + + public void open(Player player, Atm atm) { + + AtmMenuHolder holder = + new AtmMenuHolder(atm.id()); + + long balance = bankService.getBalance( + player.getUniqueId() + ); + + holder.getInventory().setItem( + 4, + createButton( + Material.GOLD_INGOT, + "Баланс: " + balance + " Джой", + NamedTextColor.GOLD + ) + ); + + // Внести + holder.getInventory().setItem( + 9, + createButton(Material.LIME_WOOL, "Внести 1", NamedTextColor.GREEN) + ); + + holder.getInventory().setItem( + 10, + createButton(Material.LIME_WOOL, "Внести 16", NamedTextColor.GREEN) + ); + + holder.getInventory().setItem( + 11, + createButton(Material.LIME_WOOL, "Внести 64", NamedTextColor.GREEN) + ); + + holder.getInventory().setItem( + 12, + createButton(Material.EMERALD_BLOCK, "Внести всё", NamedTextColor.GREEN) + ); + + // Снять + holder.getInventory().setItem( + 14, + createButton(Material.RED_WOOL, "Снять 1", NamedTextColor.RED) + ); + + holder.getInventory().setItem( + 15, + createButton(Material.RED_WOOL, "Снять 16", NamedTextColor.RED) + ); + + holder.getInventory().setItem( + 16, + createButton(Material.RED_WOOL, "Снять 64", NamedTextColor.RED) + ); + + holder.getInventory().setItem( + 17, + createButton(Material.REDSTONE_BLOCK, "Снять всё", NamedTextColor.RED) + ); + + player.openInventory(holder.getInventory()); + } + + private ItemStack createButton( + Material material, + String name, + NamedTextColor color + ) { + + ItemStack item = new ItemStack(material); + + ItemMeta meta = item.getItemMeta(); + + meta.displayName( + Component.text(name, color) + ); + + item.setItemMeta(meta); + + return item; + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/gui/AtmMenuHolder.java b/src/main/java/ru/joutak/template/gui/AtmMenuHolder.java new file mode 100644 index 0000000..66fc3f8 --- /dev/null +++ b/src/main/java/ru/joutak/template/gui/AtmMenuHolder.java @@ -0,0 +1,34 @@ +package ru.joutak.template.gui; + +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class AtmMenuHolder implements InventoryHolder { + + private final UUID atmId; + private final Inventory inventory; + + public AtmMenuHolder(UUID atmId) { + this.atmId = atmId; + + this.inventory = Bukkit.createInventory( + this, + 27, + Component.text("JoyBank") + ); + } + + public UUID getAtmId() { + return atmId; + } + + @Override + public @NotNull Inventory getInventory() { + return inventory; + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/storage/AccountRepository.java b/src/main/java/ru/joutak/template/storage/AccountRepository.java new file mode 100644 index 0000000..f1d5197 --- /dev/null +++ b/src/main/java/ru/joutak/template/storage/AccountRepository.java @@ -0,0 +1,19 @@ +package ru.joutak.template.storage; + +import ru.joutak.template.bank.Transaction; + +import java.util.List; +import java.util.UUID; + +public interface AccountRepository { + + long getBalance(UUID playerId); + + void applyTransaction( + UUID playerId, + long newBalance, + Transaction transaction + ); + + List getTransactions(UUID playerId); +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/storage/AtmRepository.java b/src/main/java/ru/joutak/template/storage/AtmRepository.java new file mode 100644 index 0000000..0e7a85e --- /dev/null +++ b/src/main/java/ru/joutak/template/storage/AtmRepository.java @@ -0,0 +1,18 @@ +package ru.joutak.template.storage; + +import ru.joutak.template.atm.Atm; + +import java.util.Collection; +import java.util.Optional; +import java.util.UUID; + +public interface AtmRepository { + + void save(Atm atm); + + void delete(UUID atmId); + + Optional findById(UUID atmId); + + Collection findAll(); +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java b/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java new file mode 100644 index 0000000..e74a7c9 --- /dev/null +++ b/src/main/java/ru/joutak/template/storage/YamlAccountRepository.java @@ -0,0 +1,216 @@ +package ru.joutak.template.storage; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; +import ru.joutak.template.bank.Transaction; +import ru.joutak.template.bank.TransactionType; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +import java.io.File; +import java.io.IOException; +import java.time.Instant; +import java.util.*; + + +public class YamlAccountRepository implements AccountRepository { + + private final JavaPlugin plugin; + private final File file; + private final YamlConfiguration configuration; + + public YamlAccountRepository(JavaPlugin plugin) { + this.plugin = plugin; + + if (!plugin.getDataFolder().exists()) { + plugin.getDataFolder().mkdirs(); + } + + this.file = new File( + plugin.getDataFolder(), + "accounts.yml" + ); + + this.configuration = YamlConfiguration.loadConfiguration(file); + } + + @Override + public long getBalance(UUID playerId) { + String path = "players." + playerId + ".balance"; + + return configuration.getLong(path, 0L); + } + + @Override + public void applyTransaction( + UUID playerId, + long newBalance, + Transaction transaction + ) { + + if (newBalance < 0) { + throw new IllegalArgumentException( + "Баланс не может быть отрицательным" + ); + } + + String playerPath = + "players." + playerId; + String balancePath = + playerPath + ".balance"; + String transactionsPath = + playerPath + ".transactions"; + Object oldBalance = + configuration.get(balancePath); + + List> oldTransactions = + new ArrayList<>( + configuration.getMapList( + transactionsPath + ) + ); + List> newTransactions = + new ArrayList<>(oldTransactions); + + Map data = + new HashMap<>(); + data.put( + "type", + transaction.type().name() + ); + data.put( + "amount", + transaction.amount() + ); + data.put( + "timestamp", + transaction.timestamp().toString() + ); + newTransactions.add(data); + + try { + + configuration.set( + balancePath, + newBalance + ); + + configuration.set( + transactionsPath, + newTransactions + ); + + save(); + + } catch (RuntimeException e) { + + configuration.set( + balancePath, + oldBalance + ); + + configuration.set( + transactionsPath, + oldTransactions + ); + + throw e; + } + } + + @Override + public List getTransactions(UUID playerId) { + String path = "players." + playerId + ".transactions"; + + List> storedTransactions = + configuration.getMapList(path); + + List result = new ArrayList<>(); + + for (Map data : storedTransactions) { + try { + TransactionType type = TransactionType.valueOf( + String.valueOf(data.get("type")) + ); + + long amount = Long.parseLong( + String.valueOf(data.get("amount")) + ); + + Instant timestamp = Instant.parse( + String.valueOf(data.get("timestamp")) + ); + + result.add( + new Transaction( + type, + amount, + timestamp + ) + ); + + } catch (Exception e) { + plugin.getLogger().warning( + "Не удалось прочитать операцию игрока " + + playerId + ); + } + } + + return result; + } + + private void save() { + + Path target = file.toPath(); + + Path temp = target.resolveSibling( + file.getName() + ".tmp" + ); + + try { + + String yaml = + configuration.saveToString(); + + Files.writeString( + temp, + yaml, + StandardCharsets.UTF_8 + ); + + try { + + Files.move( + temp, + target, + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING + ); + + } catch (AtomicMoveNotSupportedException e) { + + Files.move( + temp, + target, + StandardCopyOption.REPLACE_EXISTING + ); + } + + } catch (IOException e) { + + try { + Files.deleteIfExists(temp); + } catch (IOException ignored) { + } + + throw new IllegalStateException( + "Не удалось сохранить accounts.yml", + e + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/joutak/template/storage/YamlAtmRepository.java b/src/main/java/ru/joutak/template/storage/YamlAtmRepository.java new file mode 100644 index 0000000..b59aee6 --- /dev/null +++ b/src/main/java/ru/joutak/template/storage/YamlAtmRepository.java @@ -0,0 +1,175 @@ +package ru.joutak.template.storage; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; +import ru.joutak.template.atm.Atm; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; +import java.util.UUID; + +public class YamlAtmRepository implements AtmRepository { + + private final JavaPlugin plugin; + private final File file; + private final YamlConfiguration configuration; + + public YamlAtmRepository(JavaPlugin plugin) { + this.plugin = plugin; + + if (!plugin.getDataFolder().exists()) { + plugin.getDataFolder().mkdirs(); + } + + this.file = new File( + plugin.getDataFolder(), + "atms.yml" + ); + + this.configuration = + YamlConfiguration.loadConfiguration(file); + } + + @Override + public void save(Atm atm) { + String path = "atms." + atm.id(); + + configuration.set( + path + ".world", + atm.worldId().toString() + ); + + configuration.set( + path + ".barrel.x", + atm.barrelX() + ); + configuration.set( + path + ".barrel.y", + atm.barrelY() + ); + configuration.set( + path + ".barrel.z", + atm.barrelZ() + ); + + configuration.set( + path + ".sign.x", + atm.signX() + ); + configuration.set( + path + ".sign.y", + atm.signY() + ); + configuration.set( + path + ".sign.z", + atm.signZ() + ); + + saveFile(); + } + + @Override + public void delete(UUID atmId) { + configuration.set( + "atms." + atmId, + null + ); + + saveFile(); + } + + @Override + public Optional findById(UUID atmId) { + String path = "atms." + atmId; + + if (!configuration.contains(path)) { + return Optional.empty(); + } + + return Optional.of(readAtm(path, atmId)); + } + + @Override + public Collection findAll() { + Collection result = new ArrayList<>(); + + ConfigurationSection section = + configuration.getConfigurationSection("atms"); + + if (section == null) { + return result; + } + + for (String idString : section.getKeys(false)) { + + try { + UUID id = UUID.fromString(idString); + + result.add( + readAtm( + "atms." + idString, + id + ) + ); + + } catch (IllegalArgumentException e) { + plugin.getLogger().warning( + "Некорректный ATM id: " + idString + ); + } + } + + return result; + } + + private Atm readAtm( + String path, + UUID id + ) { + UUID worldId = UUID.fromString( + configuration.getString( + path + ".world" + ) + ); + + return new Atm( + id, + worldId, + + configuration.getInt( + path + ".barrel.x" + ), + configuration.getInt( + path + ".barrel.y" + ), + configuration.getInt( + path + ".barrel.z" + ), + + configuration.getInt( + path + ".sign.x" + ), + configuration.getInt( + path + ".sign.y" + ), + configuration.getInt( + path + ".sign.z" + ) + ); + } + + private void saveFile() { + try { + configuration.save(file); + } catch (IOException e) { + throw new IllegalStateException( + "Не удалось сохранить atms.yml", + e + ); + } + } +} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 4428874..ec3bbe3 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,12 +1,27 @@ name: ${NAME} version: ${VERSION} api-version: ${MINECRAFT_VERSION} -main: ru.joutak.template.EmptyPlugin +main: ru.joutak.template.JoyBankPlugin description: 'A template plugin' -authors: ['EnderDissa', 'Stunnerer', 's4dnex'] -contributors: ['technosimp'] +authors: ['russlan_1esse'] +contributors: ['EnderDissa', 'Stunnerer', 's4dnex', 'technosimp'] website: ${WEBSITE} depend: [] libraries: - - 'org.projectlombok:lombok:${LOMBOK_VERSION}' \ No newline at end of file + - 'org.projectlombok:lombok:${LOMBOK_VERSION}' +commands: + joy: + description: Основная команда JoyBank + usage: /joy + +permissions: + joybank.admin.give: + description: Позволяет выдавать монеты Джой + default: op + joybank.atm.create: + description: Позволяет создавать банкоматы + default: op + joybank.atm.break: + description: Позволяет удалять зарегистрированные банкоматы + default: op \ No newline at end of file