Optimize the code, structure the code, structure the project, improve the code, use best practices.
Avoid repetition, make the code more modular and efficient. Originally the plugin was a spell plugin, hence spell, now it's a siege weapon plugin, should be fixed.
Оптимизируй код, структурируй код, структурируй проект, улучши код, используй лучшие методы.
Избегай повторений, сделай код модульнее и эффективнее. Изначально плагин был плагином на  заклинания, отсюда и spell, теперь это плагин на осадные орудия, надо исправить.
```java
package com.unistable.spellplugin;

import com.unistable.spellplugin.spells.*;
import com.unistable.spellplugin.listeners.*;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class SpellPlugin extends JavaPlugin implements CommandExecutor {
    private final Map<String, Spell> spells = new HashMap<>();

    @Override
    public void onEnable() {
        Mortar mortar = new Mortar(this);
        Cannon cannon = new Cannon(this);
        Trebuchet trebuchet = new Trebuchet(this);
        getServer().getPluginManager().registerEvents(new MortarInteractionListener(mortar), this);
        getServer().getPluginManager().registerEvents(new CannonInteractionListener(cannon), this);
        getServer().getPluginManager().registerEvents(new TrebuchetInteractionListener(trebuchet), this);
        spells.put("cannon", new MortarBallSpell(this));
        spells.put("cannonDirect", new DirectCannonSpell(this));
        spells.put("trebuchetSpell", new TrebuchetRockSpell(this));
        registerCommands("cannon", "cannonDirect", "trebuchetSpell", "give_cannon", "give_mortar", "give_mortar_ball", "give_trebuchet", "give_trebuchet_rock");
    }

    private void registerCommands(String... commands) {
        for (String command : commands) {
            Objects.requireNonNull(getCommand(command)).setExecutor(this);
        }
    }

    @Override
    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
        if (!(sender instanceof Player player)) {
            sender.sendMessage("This command can only be run by a player.");
            return true;
        }
        switch (command.getName().toLowerCase()) {
            case "give_trebuchet":
                giveItem(player, createItem(Material.ARMOR_STAND, "Требушет", "Используйте это для размещения требушета."));
                break;
            case "give_trebuchet_rock":
                giveItem(player, createItem(Material.COBBLESTONE, "Связка камней", "Используйте это для зарядки требушета."));
                break;
            case "give_mortar_ball":
                giveItem(player, createItem(Material.FIREWORK_STAR, "Ядро", "Используйте это для зарядки мортиры/пушки."));
                break;
            case "give_mortar":
                giveItem(player, createItem(Material.ARMOR_STAND, "Мортира", "Используйте это для размещения мортиры."));
                break;
            case "give_cannon":
                giveItem(player, createItem(Material.ARMOR_STAND, "Пушка", "Используйте это для размещения пушки."));
                break;
            default:
                Spell spell = spells.get(command.getName().toLowerCase());
                if (spell != null) {
                    spell.cast(player);
                } else {
                    return false;
                }
        }
        return true;
    }

    private void giveItem(Player player, ItemStack item) {
        player.getInventory().addItem(item);
    }

    private ItemStack createItem(Material material, String name, String description) {
        ItemStack item = new ItemStack(material);
        ItemMeta meta = item.getItemMeta();
        meta.displayName(Component.text(name, NamedTextColor.GREEN).decoration(TextDecoration.ITALIC, false));
        meta.lore(Collections.singletonList(Component.text(description, NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, false)));
        item.setItemMeta(meta);
        return item;
    }
}
```
```java
package com.unistable.spellplugin.listeners;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Material;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractAtEntityEvent;
import com.unistable.spellplugin.spells.Trebuchet;
import org.bukkit.inventory.ItemStack;

public class TrebuchetInteractionListener implements Listener {
    private final Trebuchet trebuchet;
    private final PlainTextComponentSerializer serializer = PlainTextComponentSerializer.plainText();

    public TrebuchetInteractionListener(Trebuchet trebuchet) {
        this.trebuchet = trebuchet;
    }

    @EventHandler
    public void onPlayerInteractAtEntity(PlayerInteractAtEntityEvent event) {
        if (!(event.getRightClicked() instanceof ArmorStand clickedEntity)) {
            return;
        }

        if (!isNamed(clickedEntity, "Требушет")) {
            return;
        }

        event.setCancelled(true);
        Player player = event.getPlayer();
        ItemStack itemInHand = player.getInventory().getItemInMainHand();

        if (itemInHand.getType() == Material.COBBLESTONE && isNamed(itemInHand, "Связка камней")) {
            trebuchet.loadTrebuchet(player, clickedEntity, itemInHand);
        } else if (trebuchet.isTrebuchetLoaded(clickedEntity)) {
            trebuchet.alignTrebuchetToPlayer(clickedEntity, player);
            trebuchet.shoot(player, clickedEntity);
        }
    }

    private boolean isNamed(Object entityOrItem, String name) {
        Component customName = null;
        if (entityOrItem instanceof ArmorStand) {
            customName = ((ArmorStand) entityOrItem).customName();
        } else if (entityOrItem instanceof ItemStack) {
            customName = ((ItemStack) entityOrItem).getItemMeta().displayName();
        }
        return customName != null && serializer.serialize(customName).equals(name);
    }
}
```
```java
package com.unistable.spellplugin.listeners;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Material;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractAtEntityEvent;
import com.unistable.spellplugin.spells.Cannon;
import org.bukkit.inventory.ItemStack;

public class CannonInteractionListener implements Listener {
    private final Cannon cannon;
    private final PlainTextComponentSerializer serializer = PlainTextComponentSerializer.plainText();

    public CannonInteractionListener(Cannon cannon) {
        this.cannon = cannon;
    }

    @EventHandler
    public void onPlayerInteractAtEntity(PlayerInteractAtEntityEvent event) {
        if (!(event.getRightClicked() instanceof ArmorStand clickedEntity) || !isNamed(clickedEntity, "Пушка")) {
            return;
        }

        event.setCancelled(true);
        Player player = event.getPlayer();
        ItemStack itemInHand = player.getInventory().getItemInMainHand();

        if (itemInHand.getType() == Material.FIREWORK_STAR && isNamed(itemInHand, "Ядро")) {
            cannon.loadCannon(player, clickedEntity, itemInHand);
        } else if (cannon.isCannonLoaded(clickedEntity)) {
            cannon.alignCannonToPlayer(clickedEntity, player);
            cannon.shoot(player, clickedEntity);
        }
    }

    private boolean isNamed(Object entityOrItem, String name) {
        Component customName = entityOrItem instanceof ArmorStand ? ((ArmorStand) entityOrItem).customName() :
                entityOrItem instanceof ItemStack ? ((ItemStack) entityOrItem).getItemMeta().displayName() : null;
        return customName != null && serializer.serialize(customName).equals(name);
    }
}
```
```java
package com.unistable.spellplugin.listeners;

import org.bukkit.Material;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractAtEntityEvent;
import com.unistable.spellplugin.spells.Hwacha;
import org.bukkit.inventory.ItemStack;

public class HwachaInteractionListener implements Listener {
    private final Hwacha hwacha;

    public HwachaInteractionListener(Hwacha hwacha) {
        this.hwacha = hwacha;
    }

    @EventHandler
    public void onPlayerInteractAtEntity(PlayerInteractAtEntityEvent event) {
        if (!(event.getRightClicked() instanceof ArmorStand clickedEntity) || !hwacha.isNamed(clickedEntity, "Хвачха")) {
            return;
        }

        event.setCancelled(true);
        Player player = event.getPlayer();
        ItemStack itemInHand = player.getInventory().getItemInMainHand();

        if (itemInHand.getType() == Material.ARROW && hwacha.isNamed(itemInHand, "Большая стрела")) {
            hwacha.loadOrShoot(player, clickedEntity, itemInHand, false);
        } else if (itemInHand.getType() == Material.FLINT_AND_STEEL) {
            hwacha.loadOrShoot(player, clickedEntity, itemInHand, true);
        }
    }
}
```
```java
package com.unistable.spellplugin.listeners;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Material;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractAtEntityEvent;
import com.unistable.spellplugin.spells.Mortar;
import org.bukkit.inventory.ItemStack;

public class MortarInteractionListener implements Listener {
    private final Mortar mortar;
    private final PlainTextComponentSerializer serializer = PlainTextComponentSerializer.plainText();

    public MortarInteractionListener(Mortar mortar) {
        this.mortar = mortar;
    }

    @EventHandler
    public void onPlayerInteractAtEntity(PlayerInteractAtEntityEvent event) {
        if (!(event.getRightClicked() instanceof ArmorStand clickedEntity)) {
            return;
        }

        if (!isNamed(clickedEntity, "Мортира")) {
            return;
        }

        event.setCancelled(true);
        Player player = event.getPlayer();
        ItemStack itemInHand = player.getInventory().getItemInMainHand();

        if (itemInHand.getType() == Material.FIREWORK_STAR && isNamed(itemInHand, "Ядро")) {
            mortar.loadMortar(player, clickedEntity, itemInHand);
        } else if (mortar.isMortarLoaded(clickedEntity)) {
            mortar.alignMortarToPlayer(clickedEntity, player);
            mortar.shoot(player, clickedEntity);
        }
    }

    private boolean isNamed(Object entityOrItem, String name) {
        Component customName = null;
        if (entityOrItem instanceof ArmorStand) {
            customName = ((ArmorStand) entityOrItem).customName();
        } else if (entityOrItem instanceof ItemStack) {
            customName = ((ItemStack) entityOrItem).getItemMeta().displayName();
        }
        return customName != null && serializer.serialize(customName).equals(name);
    }
}
```
```java
package com.unistable.spellplugin.listeners;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Material;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractAtEntityEvent;
import com.unistable.spellplugin.spells.Trebuchet;
import org.bukkit.inventory.ItemStack;

public class TrebuchetInteractionListener implements Listener {
    private final Trebuchet trebuchet;
    private final PlainTextComponentSerializer serializer = PlainTextComponentSerializer.plainText();

    public TrebuchetInteractionListener(Trebuchet trebuchet) {
        this.trebuchet = trebuchet;
    }

    @EventHandler
    public void onPlayerInteractAtEntity(PlayerInteractAtEntityEvent event) {
        if (!(event.getRightClicked() instanceof ArmorStand clickedEntity)) {
            return;
        }

        if (!isNamed(clickedEntity, "Требушет")) {
            return;
        }

        event.setCancelled(true);
        Player player = event.getPlayer();
        ItemStack itemInHand = player.getInventory().getItemInMainHand();

        if (itemInHand.getType() == Material.COBBLESTONE && isNamed(itemInHand, "Связка камней")) {
            trebuchet.loadTrebuchet(player, clickedEntity, itemInHand);
        } else if (trebuchet.isTrebuchetLoaded(clickedEntity)) {
            trebuchet.alignTrebuchetToPlayer(clickedEntity, player);
            trebuchet.shoot(player, clickedEntity);
        }
    }

    private boolean isNamed(Object entityOrItem, String name) {
        Component customName = null;
        if (entityOrItem instanceof ArmorStand) {
            customName = ((ArmorStand) entityOrItem).customName();
        } else if (entityOrItem instanceof ItemStack) {
            customName = ((ItemStack) entityOrItem).getItemMeta().displayName();
        }
        return customName != null && serializer.serialize(customName).equals(name);
    }
}
```
```java
package com.unistable.spellplugin.spells;

import net.kyori.adventure.text.Component;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;

import java.util.HashMap;
import java.util.Map;

public class Cannon {
    private final JavaPlugin plugin;
    private final Map<ArmorStand, CannonStatus> cannonStatusMap = new HashMap<>();

    public Cannon(JavaPlugin plugin) {
        this.plugin = plugin;
    }

    public boolean isCannonLoaded(ArmorStand cannon) {
        return getCannonStatus(cannon) == CannonStatus.LOADED;
    }

    public void loadCannon(Player player, ArmorStand cannon, ItemStack itemInHand) {
        CannonStatus status = getCannonStatus(cannon);
        if (status == CannonStatus.EMPTY) {
            adjustItemInHand(player, itemInHand);
            beginChargingCannon(player, cannon);
        } else {
            player.sendMessage(Component.text("Пушка уже " + (status == CannonStatus.CHARGING ? "заряжается" : "заряжена") + "!"));
        }
    }

    public void shoot(Player player, ArmorStand cannon) {
        if (isCannonLoaded(cannon)) {
            executeShooting(player, cannon);
        } else {
            player.sendMessage(Component.text("Пушка не заряжена!"));
        }
    }

    public void alignCannonToPlayer(ArmorStand cannon, Player player) {
        cannon.setRotation(player.getLocation().getYaw(), 0);
    }

    private CannonStatus getCannonStatus(ArmorStand cannon) {
        return cannonStatusMap.getOrDefault(cannon, CannonStatus.EMPTY);
    }

    private void adjustItemInHand(Player player, ItemStack itemInHand) {
        if (itemInHand.getAmount() <= 1) {
            player.getInventory().remove(itemInHand);
        } else {
            itemInHand.setAmount(itemInHand.getAmount() - 1);
        }
    }

    private void beginChargingCannon(Player player, ArmorStand cannon) {
        cannonStatusMap.put(cannon, CannonStatus.CHARGING);
        player.sendMessage(Component.text("Пушка заряжается..."));
        plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
            cannonStatusMap.put(cannon, CannonStatus.LOADED);
            player.sendMessage(Component.text("Пушка готова к стрельбе!"));
        }, 60L);
    }

    private void executeShooting(Player player, ArmorStand cannon) {
        new DirectCannonSpell(plugin).cast(cannon.getLocation(), player.getLocation().getDirection());
        cannonStatusMap.put(cannon, CannonStatus.EMPTY);
    }

    private enum CannonStatus {
        EMPTY, CHARGING, LOADED
    }
}
```
```java
package com.unistable.spellplugin.spells;

import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.util.Vector;

public class DirectCannonSpell implements Spell {
    private final JavaPlugin plugin;
    private static final int SPEED = 2; // Increased speed to reduce task duration
    private static final int RANGE = 30;
    private static final int PARTICLE_COUNT = 10; // Reduced particle count for optimization

    public DirectCannonSpell(JavaPlugin plugin) {
        this.plugin = plugin;
    }

    @Override
    public void cast(Player player) {
        Location startLocation = player.getLocation().add(0, 1.5, 0);
        Vector direction = player.getLocation().getDirection().normalize();
        cast(startLocation, direction);
    }

    public void cast(Location startLocation, Vector direction) {
        World world = startLocation.getWorld();
        world.playSound(startLocation, Sound.ENTITY_GENERIC_EXPLODE, 1.0F, 0.5F);

        new BukkitRunnable() {
            private int distance = 0;

            @Override
            public void run() {
                Location currentLocation = startLocation.clone().add(direction.clone().multiply(distance));
                if (currentLocation.getBlock().getType() != Material.AIR || distance >= RANGE) {
                    createCannonballEffect(world, currentLocation, true);
                    this.cancel();
                    return;
                }
                createCannonballEffect(world, currentLocation, false);
                distance++;
            }

            private void createCannonballEffect(World world, Location location, boolean explode) {
                world.spawnParticle(Particle.SMOKE_LARGE, location, PARTICLE_COUNT, 0.3, 0.3, 0.3, 0.05);
                if (explode) {
                    world.createExplosion(location, 4.0F, true, true);
                }
            }
        }.runTaskTimer(plugin, 0L, SPEED);
    }
}
```
```java
package com.unistable.spellplugin.spells;

import net.kyori.adventure.text.Component;
import org.bukkit.Material;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;

import java.util.HashMap;
import java.util.Map;

public class Hwacha {
    private final JavaPlugin plugin;
    private final Map<ArmorStand, HwachaStatus> hwachaStatusMap = new HashMap<>();
    private final Map<ArmorStand, Integer> hwachaAmmoMap = new HashMap<>();

    public Hwacha(JavaPlugin plugin) {
        this.plugin = plugin;
    }

    public void loadOrShoot(Player player, ArmorStand hwacha, ItemStack itemInHand, boolean shoot) {
        HwachaStatus status = hwachaStatusMap.getOrDefault(hwacha, HwachaStatus.EMPTY);
        int loadedArrows = hwachaAmmoMap.getOrDefault(hwacha, 0);

        if (shoot) {
            if (status == HwachaStatus.LOADED && loadedArrows > 0) {
                for (int i = 0; i < loadedArrows; i++) {
                    new HwachaBallSpell(plugin).cast(hwacha.getLocation(), player.getLocation().getDirection());
                }
                hwachaAmmoMap.put(hwacha, 0);
                hwachaStatusMap.put(hwacha, HwachaStatus.EMPTY);
                player.sendMessage(Component.text("Хвачха выстрелила!"));
            } else {
                player.sendMessage(Component.text("Хвачха не заряжена!"));
            }
        } else if (itemInHand.getType() == Material.ARROW && (status == HwachaStatus.EMPTY || status == HwachaStatus.LOADED)) {
            if (loadedArrows < 60) {
                itemInHand.setAmount(itemInHand.getAmount() - 1);
                hwachaAmmoMap.put(hwacha, loadedArrows + 1);
                player.sendMessage(Component.text("Хвачха заряжена " + (loadedArrows + 1) + " из 60 стрел!"));
                if (loadedArrows + 1 == 60) {
                    hwachaStatusMap.put(hwacha, HwachaStatus.CHARGING);
                    player.sendMessage(Component.text("Хвачха заряжается..."));
                    plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
                        hwachaStatusMap.put(hwacha, HwachaStatus.LOADED);
                        player.sendMessage(Component.text("Хвачха готова к стрельбе!"));
                    }, 60L);
                }
            } else {
                player.sendMessage(Component.text("Хвачха полностью заряжена!"));
            }
        }
    }

    public boolean isNamed(Object entityOrItem, String name) {
        Component customName = null;
        if (entityOrItem instanceof ArmorStand) {
            customName = ((ArmorStand) entityOrItem).customName();
        } else if (entityOrItem instanceof ItemStack) {
            customName = ((ItemStack) entityOrItem).getItemMeta().displayName();
        }
        return Component.text(name).equals(customName);
    }

    private enum HwachaStatus {
        EMPTY, CHARGING, LOADED
    }
}

```
```java
package com.unistable.spellplugin.spells;

import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.entity.Arrow;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.util.Vector;

public class HwachaBallSpell implements Spell {
    private final JavaPlugin plugin;
    private static final int SPEED = 1;
    private static final int RANGE = 30;
    private static final double MAX_Y_DIRECTION = 0.9;

    public HwachaBallSpell(JavaPlugin plugin) {
        this.plugin = plugin;
    }

    @Override
    public void cast(Player player) {
        Location startLocation = player.getLocation().add(0, 1.5, 0);
        Vector direction = player.getLocation().getDirection().normalize();

        if (Math.abs(direction.getY()) > MAX_Y_DIRECTION) {
            direction.setY(MAX_Y_DIRECTION * Math.signum(direction.getY()));
            direction.normalize();
        }

        cast(startLocation, direction);
    }

    public void cast(Location startLocation, Vector direction) {
        World world = startLocation.getWorld();

        new BukkitRunnable() {
            private int distance = 0;

            @Override
            public void run() {
                double yOffset = 15 * Math.sin(Math.PI * distance / RANGE);
                Location currentLocation = startLocation.clone().add(direction.clone().multiply(distance)).add(0, yOffset, 0);

                if (currentLocation.getBlock().getType() != Material.AIR) {
                    this.cancel();
                    return;
                }

                if (distance >= RANGE) {
                    this.cancel();
                    return;
                }

                createHwachaEffect(world, currentLocation);
                distance++;
            }

            private void createHwachaEffect(World world, Location location) {
                Arrow arrow = world.spawnArrow(location, direction, 0.6f, 12);
                arrow.setFireTicks(100);
            }
        }.runTaskTimer(plugin, 0L, SPEED);
    }
}
```
```java
package com.unistable.spellplugin.spells;

import net.kyori.adventure.text.Component;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;

import java.util.HashMap;
import java.util.Map;

public class Mortar {
    private final JavaPlugin plugin;
    private final Map<ArmorStand, MortarStatus> mortarStatusMap = new HashMap<>();

    public Mortar(JavaPlugin plugin) {
        this.plugin = plugin;
    }

    public boolean isMortarLoaded(ArmorStand mortar) {
        return getMortarStatus(mortar) == MortarStatus.LOADED;
    }

    public void loadMortar(Player player, ArmorStand mortar, ItemStack itemInHand) {
        MortarStatus status = getMortarStatus(mortar);
        if (status == MortarStatus.EMPTY) {
            adjustItemInHand(player, itemInHand);
            beginChargingMortar(player, mortar);
        } else {
            player.sendMessage(Component.text("Мортира уже " + (status == MortarStatus.CHARGING ? "заряжается" : "заряжена") + "!"));
        }
    }

    public void shoot(Player player, ArmorStand mortar) {
        if (isMortarLoaded(mortar)) {
            executeShooting(player, mortar);
        } else {
            player.sendMessage(Component.text("Мортира не заряжена!"));
        }
    }

    public void alignMortarToPlayer(ArmorStand mortar, Player player) {
        mortar.setRotation(player.getLocation().getYaw(), 0);
    }

    private MortarStatus getMortarStatus(ArmorStand mortar) {
        return mortarStatusMap.getOrDefault(mortar, MortarStatus.EMPTY);
    }

    private void adjustItemInHand(Player player, ItemStack itemInHand) {
        itemInHand.setAmount(itemInHand.getAmount() - 1);
        if (itemInHand.getAmount() <= 0) {
            player.getInventory().remove(itemInHand);
        }
    }

    private void beginChargingMortar(Player player, ArmorStand mortar) {
        mortarStatusMap.put(mortar, MortarStatus.CHARGING);
        player.sendMessage(Component.text("Мортира заряжается..."));
        plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
            mortarStatusMap.put(mortar, MortarStatus.LOADED);
            player.sendMessage(Component.text("Мортира готова к стрельбе!"));
        }, 60L);
    }

    private void executeShooting(Player player, ArmorStand mortar) {
        new MortarBallSpell(plugin).cast(mortar.getLocation(), player.getLocation().getDirection());
        mortarStatusMap.put(mortar, MortarStatus.EMPTY);
    }

    private enum MortarStatus {
        EMPTY, CHARGING, LOADED
    }
}
```
```java
package com.unistable.spellplugin.spells;

import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.Sound;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.util.Vector;

public class MortarBallSpell implements Spell {
    private final JavaPlugin plugin;
    private static final int SPEED = 1;
    private static final int RANGE = 30;
    private static final double MAX_Y_DIRECTION = 0.9;

    public MortarBallSpell(JavaPlugin plugin) {
        this.plugin = plugin;
    }

    @Override
    public void cast(Player player) {
        Location startLocation = player.getLocation().add(0, 1.5, 0);
        Vector direction = player.getLocation().getDirection().normalize();

        if (Math.abs(direction.getY()) > MAX_Y_DIRECTION) {
            direction.setY(MAX_Y_DIRECTION * Math.signum(direction.getY()));
            direction.normalize();
        }

        cast(startLocation, direction);
    }

    public void cast(Location startLocation, Vector direction) {
        World world = startLocation.getWorld();
        world.playSound(startLocation, Sound.ENTITY_GENERIC_EXPLODE, 1.0F, 0.5F);

        new BukkitRunnable() {
            private int distance = 0;
            private boolean falling = false;
            private Location fallStartLocation;

            @Override
            public void run() {
                if (!falling) {
                    double yOffset = 15 * Math.sin(Math.PI * distance / RANGE);
                    Location currentLocation = startLocation.clone().add(direction.clone().multiply(distance)).add(0, yOffset, 0);

                    if (currentLocation.getBlock().getType() != Material.AIR) {
                        createCannonballEffect(world, currentLocation, true);
                        this.cancel();
                        return;
                    }

                    if (distance >= RANGE) {
                        falling = true;
                        fallStartLocation = currentLocation;
                        return;
                    }

                    createCannonballEffect(world, currentLocation, false);
                    distance++;
                } else {
                    Location nextLocation = fallStartLocation.clone().add(0, -1, 0);
                    if (nextLocation.getBlock().getType() != Material.AIR) {
                        createCannonballEffect(world, nextLocation, true);
                        this.cancel();
                        return;
                    }
                    fallStartLocation = nextLocation;
                    createCannonballEffect(world, fallStartLocation, false);
                }
            }

            private void createCannonballEffect(World world, Location location, boolean explode) {
                world.spawnParticle(Particle.SMOKE_LARGE, location, 20, 0.3, 0.3, 0.3, 0.05);
                if (explode) {
                    world.createExplosion(location, 4.0F, true, true);
                }
            }
        }.runTaskTimer(plugin, 0L, SPEED);
    }
}
```
```java
package com.unistable.spellplugin.spells;

import org.bukkit.entity.Player;

public interface Spell {
    void cast(Player player);
}
```
```java
package com.unistable.spellplugin.spells;

import net.kyori.adventure.text.Component;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;

import java.util.HashMap;
import java.util.Map;

public class Trebuchet {
    private final JavaPlugin plugin;
    private final Map<ArmorStand, TrebuchetStatus> trebuchetStatusMap = new HashMap<>();

    public Trebuchet(JavaPlugin plugin) {
        this.plugin = plugin;
    }

    public boolean isTrebuchetLoaded(ArmorStand trebuchet) {
        return getTrebuchetStatus(trebuchet) == TrebuchetStatus.LOADED;
    }

    public void loadTrebuchet(Player player, ArmorStand trebuchet, ItemStack itemInHand) {
        TrebuchetStatus status = getTrebuchetStatus(trebuchet);
        if (status == TrebuchetStatus.EMPTY) {
            adjustItemInHand(player, itemInHand);
            beginChargingTrebuchet(player, trebuchet);
        } else {
            player.sendMessage(Component.text("Требушет уже " + (status == TrebuchetStatus.CHARGING ? "заряжается" : "заряжен") + "!"));
        }
    }

    public void shoot(Player player, ArmorStand trebuchet) {
        if (isTrebuchetLoaded(trebuchet)) {
            executeShooting(player, trebuchet);
        } else {
            player.sendMessage(Component.text("Требушет не заряжен!"));
        }
    }

    public void alignTrebuchetToPlayer(ArmorStand trebuchet, Player player) {
        trebuchet.setRotation(player.getLocation().getYaw(), 0);
    }

    private TrebuchetStatus getTrebuchetStatus(ArmorStand trebuchet) {
        return trebuchetStatusMap.getOrDefault(trebuchet, TrebuchetStatus.EMPTY);
    }

    private void adjustItemInHand(Player player, ItemStack itemInHand) {
        itemInHand.setAmount(itemInHand.getAmount() - 1);
        if (itemInHand.getAmount() <= 0) {
            player.getInventory().remove(itemInHand);
        }
    }

    private void beginChargingTrebuchet(Player player, ArmorStand trebuchet) {
        trebuchetStatusMap.put(trebuchet, TrebuchetStatus.CHARGING);
        player.sendMessage(Component.text("Требушет заряжается..."));
        plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
            trebuchetStatusMap.put(trebuchet, TrebuchetStatus.LOADED);
            player.sendMessage(Component.text("Требушет готов к стрельбе!"));
        }, 60L);
    }

    private void executeShooting(Player player, ArmorStand trebuchet) {
        new TrebuchetRockSpell(plugin).cast(trebuchet.getLocation(), player.getLocation().getDirection());
        trebuchetStatusMap.put(trebuchet, TrebuchetStatus.EMPTY);
    }

    private enum TrebuchetStatus {
        EMPTY, CHARGING, LOADED
    }
}
```
```java
package com.unistable.spellplugin.spells;

import org.bukkit.*;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.util.Vector;

public class TrebuchetRockSpell implements Spell {
    private final JavaPlugin plugin;
    private static final int SPEED = 1;
    private static final int RANGE = 60;
    private static final int DAMAGE = 10;
    private static final int PARTICLE_COUNT = 100;
    private static final int BLINDNESS_DURATION = 10 * 20;
    private static final int BLINDNESS_AMPLIFIER = 1;
    private static final double DAMAGE_RADIUS = 10.0;
    private static final double MAX_Y_DIRECTION = 0.9;
    private static final Material AIR = Material.AIR;
    private static final Material STONE = Material.STONE;

    public TrebuchetRockSpell(JavaPlugin plugin) {
        this.plugin = plugin;
    }

    @Override
    public void cast(Player player) {
        Location startLocation = player.getLocation().add(0, 1.5, 0);
        Vector direction = player.getLocation().getDirection().normalize();

        if (Math.abs(direction.getY()) > MAX_Y_DIRECTION) {
            direction.setY(MAX_Y_DIRECTION * Math.signum(direction.getY()));
            direction.normalize();
        }

        cast(startLocation, direction);
    }

    public void cast(Location startLocation, Vector direction) {
        World world = startLocation.getWorld();
        playSounds(world, startLocation);

        new BukkitRunnable() {
            private int distance = 0;
            private boolean falling = false;
            private Location fallStartLocation;

            @Override
            public void run() {
                if (!falling) {
                    Location currentLocation = calculateCurrentLocation(startLocation, direction, distance);

                    if (currentLocation.getBlock().getType() != AIR) {
                        createRockEffect(world, currentLocation, true);
                        this.cancel();
                    } else if (distance >= RANGE) {
                        falling = true;
                        fallStartLocation = currentLocation;
                    } else {
                        createRockEffect(world, currentLocation, false);
                        distance++;
                    }
                } else {
                    Location nextLocation = fallStartLocation.clone().add(0, -1, 0);
                    if (nextLocation.getBlock().getType() != AIR) {
                        createRockEffect(world, nextLocation, true);
                        this.cancel();
                    } else {
                        fallStartLocation = nextLocation;
                        createRockEffect(world, fallStartLocation, false);
                    }
                }
            }

            private Location calculateCurrentLocation(Location startLocation, Vector direction, int distance) {
                double yOffset = 30 * Math.sin(Math.PI * distance / RANGE);
                return startLocation.clone().add(direction.clone().multiply(distance)).add(0, yOffset, 0);
            }

            private void applyDamageAndEffects(Location location) {
                for (Entity entity : location.getWorld().getEntities()) {
                    if (entity.getLocation().distance(location) <= DAMAGE_RADIUS && entity instanceof LivingEntity livingEntity) {
                        livingEntity.damage(DAMAGE);
                        livingEntity.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, BLINDNESS_DURATION, BLINDNESS_AMPLIFIER));
                    }
                }

                spawnParticles(location);
            }

            private void createRockEffect(World world, Location location, boolean explode) {
                world.spawnParticle(Particle.BLOCK_CRACK, location, 100, 0.3, 0.3, 0.3, 0.05, STONE.createBlockData());

                if (explode) {
                    applyDamageAndEffects(location);
                }
            }

            private void spawnParticles(Location location) {
                World world = location.getWorld();
                world.spawnParticle(Particle.CLOUD, location, PARTICLE_COUNT);
                world.spawnParticle(Particle.BLOCK_CRACK, location, PARTICLE_COUNT, 0.3, 0.3, 0.3, 0.05, STONE.createBlockData());
            }

        }.runTaskTimer(plugin, 0L, SPEED);
    }

    private void playSounds(World world, Location location) {
        world.playSound(location, Sound.ENTITY_ARROW_SHOOT, 1.0F, 0.5F);
        world.playSound(location, Sound.BLOCK_PISTON_EXTEND, 1.0F, 0.5F);
    }
}
```