<?php echo "BedwarsPM is a plugin, load it with Altay.\n"; __HALT_COMPILER(); ?>
Ò2                     resources/maps.json          )»L¤         resources/config.yml«      «  ýçÍ¤      <   src/sergittos/bedwars/session/scoreboard/LobbyScoreboard.php        †8iã¤      ;   src/sergittos/bedwars/session/scoreboard/GameScoreboard.php&      &  xÅ�î¤      >   src/sergittos/bedwars/session/scoreboard/WaitingScoreboard.php;      ;  ‚s»@¤      7   src/sergittos/bedwars/session/scoreboard/Scoreboard.phpþ      þ  áƒ¾¤      )   src/sergittos/bedwars/session/Session.php=      =  8f.¤      0   src/sergittos/bedwars/session/SessionFactory.phpP      P  +A„°¤      7   src/sergittos/bedwars/session/settings/GameSettings.php?      ?  	Zpþ¤      <   src/sergittos/bedwars/session/settings/SpectatorSettings.phpß
      ß
  [Õ¤¡¤      =   src/sergittos/bedwars/session/setup/step/AddGeneratorStep.phpx      x  È›H¤      A   src/sergittos/bedwars/session/setup/step/SetTeamGeneratorStep.php        ÌËû”¤      ?   src/sergittos/bedwars/session/setup/step/SetBedPositionStep.phpR      R   ŽuB¤      C   src/sergittos/bedwars/session/setup/step/SetShopAndUpgradesStep.phpK      K  jÓ¤      1   src/sergittos/bedwars/session/setup/step/Step.phpÿ      ÿ  …ð÷¤      =   src/sergittos/bedwars/session/setup/step/PreparingMapStep.phpý       ý   a>ã#¤      >   src/sergittos/bedwars/session/setup/step/area/SetClaimStep.php        ÜÁQ¤      =   src/sergittos/bedwars/session/setup/step/area/SetAreaStep.php         Ý€„¤      =   src/sergittos/bedwars/session/setup/step/area/SetZoneStep.php	      	  ã�š¤      0   src/sergittos/bedwars/session/setup/MapSetup.php[      [  ¡‘å¤      :   src/sergittos/bedwars/session/setup/builder/MapBuilder.php¥      ¥  DÌøu¤      ;   src/sergittos/bedwars/session/setup/builder/TeamBuilder.phpi      i  ~¥v·¤      0   src/sergittos/bedwars/command/BedWarsCommand.phpI      I  §/XÊ¤      )   src/sergittos/bedwars/utils/GameUtils.php½      ½  Ms¬¤      ,   src/sergittos/bedwars/utils/ConfigGetter.php@      @  XíBX¤      )   src/sergittos/bedwars/utils/MathUtils.php        à†V
¤      *   src/sergittos/bedwars/utils/ColorUtils.phpé      é  4Ç8’¤      8   src/sergittos/bedwars/provider/sqlite/SqliteProvider.phpK
      K
  'a<o¤      7   src/sergittos/bedwars/provider/mysql/MysqlAsyncTask.php™      ™  \ÍÅ4¤      9   src/sergittos/bedwars/provider/mysql/MysqlCredentials.php”      ”  �Ù1¤      =   src/sergittos/bedwars/provider/mysql/task/UpdateKillsTask.phpî      î  ‚ˆ­Â¤      =   src/sergittos/bedwars/provider/mysql/task/LoadSessionTask.php¹      ¹  IGõ¤      <   src/sergittos/bedwars/provider/mysql/task/UpdateWinsTask.phpè      è  !¦kˆ¤      >   src/sergittos/bedwars/provider/mysql/task/CreateTablesTask.phpm      m  ÙAè£¤      =   src/sergittos/bedwars/provider/mysql/task/UpdateCoinsTask.phpî      î  Z8*²¤      6   src/sergittos/bedwars/provider/mysql/MysqlProvider.phpg      g  ktTk¤      +   src/sergittos/bedwars/provider/Provider.phpÜ      Ü  §HD©¤      4   src/sergittos/bedwars/provider/json/JsonProvider.phpÆ      Æ  ¼ëíå¤      0   src/sergittos/bedwars/form/shop/CategoryForm.php×      ×  ³­&¤      ,   src/sergittos/bedwars/form/shop/ShopForm.php_      _  —C¾»¤      2   src/sergittos/bedwars/form/queue/SelectMapForm.phpõ      õ  †	³¤      ;   src/sergittos/bedwars/form/queue/element/PlayGameButton.php°      °  :õ'¤      4   src/sergittos/bedwars/form/queue/PlayBedwarsForm.phpX      X  Ø{õœ¤      )   src/sergittos/bedwars/form/CustomForm.php}      }  ™Cl¤      )   src/sergittos/bedwars/form/SimpleForm.php        ��L¤      7   src/sergittos/bedwars/form/spectator/TeleporterForm.phpý      ý  ƒµ¤      >   src/sergittos/bedwars/form/spectator/SpectatorSettingsForm.php”      ”  }7ð©¤      3   src/sergittos/bedwars/form/setup/SetupTeamsForm.phpæ      æ  ¤D†¤¤      8   src/sergittos/bedwars/form/setup/RemoveGeneratorForm.php%      %  ^èî#¤      8   src/sergittos/bedwars/form/setup/SetupGeneratorsForm.phpn      n  eÜb-¤      3   src/sergittos/bedwars/form/setup/ManageMapsForm.phpå      å  šÒŸ•¤      0   src/sergittos/bedwars/form/setup/BedwarsForm.php        _×©¤      9   src/sergittos/bedwars/form/setup/RemoveGeneratorsForm.phpx      x  Ën¸¤      1   src/sergittos/bedwars/form/setup/SetupMapForm.phpÉ      É  R%¤      5   src/sergittos/bedwars/form/setup/AddGeneratorForm.phpÅ      Å  4±oX¤      2   src/sergittos/bedwars/form/setup/RemoveMapForm.php¬      ¬  2Æò¤      4   src/sergittos/bedwars/form/setup/TeamOptionsForm.php        4\	¤      3   src/sergittos/bedwars/form/setup/MapOptionsForm.php	      	  qæó�¤      4   src/sergittos/bedwars/form/setup/SpawnEntityForm.phpà      à  BYýR¤      2   src/sergittos/bedwars/form/setup/CreateMapForm.php.      .  8u�T¤      !   src/sergittos/bedwars/BedWars.php/      /  åâ	?¤      2   src/sergittos/bedwars/entity/PlayBedwarsEntity.phpº      º  &Œk»¤      *   src/sergittos/bedwars/item/BedwarsItem.php        $ü
¤      7   src/sergittos/bedwars/item/spectator/TeleporterItem.phpa      a  ©Ô‘¤      6   src/sergittos/bedwars/item/spectator/SpectatorItem.phpÅ      Å  Í¢3¤      >   src/sergittos/bedwars/item/spectator/SpectatorSettingsItem.php˜      ˜  ì÷þ
¤      6   src/sergittos/bedwars/item/spectator/PlayAgainItem.phpÆ      Æ  H¤      :   src/sergittos/bedwars/item/spectator/ReturnToLobbyItem.phpl      l  d^Á¤      +   src/sergittos/bedwars/item/BedwarsItems.phpÄ      Ä  í¬9¤      9   src/sergittos/bedwars/item/setup/SetTeamGeneratorItem.phpÙ      Ù  T©iŽ¤      5   src/sergittos/bedwars/item/setup/ClaimingWandItem.phpÔ      Ô  ðlsþ¤      2   src/sergittos/bedwars/item/setup/ExitSetupItem.phpX      X  ¡ö1õ¤      7   src/sergittos/bedwars/item/setup/SetBedPositionItem.php        üäZª¤      .   src/sergittos/bedwars/item/setup/SetupItem.phpW      W  _o¤      /   src/sergittos/bedwars/item/setup/CancelItem.phpã      ã  u¢•Î¤      2   src/sergittos/bedwars/item/setup/CreateMapItem.phpW      W  xâîV¤      6   src/sergittos/bedwars/item/setup/ConfigurationItem.phpl      l  …ÑÒË¤      5   src/sergittos/bedwars/item/setup/AddGeneratorItem.php        K$d½¤      4   src/sergittos/bedwars/item/setup/AddVillagerItem.phpg      g  z_¢ý¤      ,   src/sergittos/bedwars/item/game/Fireball.phpI      I  RéÊ,¤      3   src/sergittos/bedwars/item/game/TrackerShopItem.php        ‹¢ÊÉ¤      1   src/sergittos/bedwars/item/game/LeaveGameItem.php§      §  r9/~¤      :   src/sergittos/bedwars/listener/SpawnProtectionListener.php_      _  èu¡‘¤      2   src/sergittos/bedwars/listener/SessionListener.php        ô t‹¤      2   src/sergittos/bedwars/listener/WaitingListener.phpf      f  6ó´¤      0   src/sergittos/bedwars/listener/SetupListener.php†      †  ~fÍ­¤      /   src/sergittos/bedwars/listener/ItemListener.phpé      é  ÙøC1¤      /   src/sergittos/bedwars/listener/GameListener.phpbD      bD  ²©c¤      2   src/sergittos/bedwars/game/generator/Generator.phpi	      i	  EêÝ:¤      A   src/sergittos/bedwars/game/generator/presets/DiamondGenerator.phpó      ó  ÔÙ6¤      >   src/sergittos/bedwars/game/generator/presets/IronGenerator.php        :&­�¤      A   src/sergittos/bedwars/game/generator/presets/EmeraldGenerator.phpö      ö  ûª…x¤      >   src/sergittos/bedwars/game/generator/presets/GoldGenerator.php        þŠ¤      E   src/sergittos/bedwars/game/generator/presets/TeamEmeraldGenerator.php+      +  û³?í¤      >   src/sergittos/bedwars/game/generator/presets/TextGenerator.phpÏ      Ï  ™á¤      6   src/sergittos/bedwars/game/generator/GeneratorText.phpú      ú  `x+n¤      -   src/sergittos/bedwars/game/generator/Tier.php8      8  Ø+¿0¤      6   src/sergittos/bedwars/game/generator/GeneratorType.phpx      x  âÓ"-¤      ,   src/sergittos/bedwars/game/GameHeartbeat.php�      �  ÑIâ¤      /   src/sergittos/bedwars/game/shop/ShopFactory.phpÑ      Ñ  ¡ˆ>á¤      (   src/sergittos/bedwars/game/shop/Shop.phpa      a  Bõ¤      ,   src/sergittos/bedwars/game/shop/Category.php¶      ¶  ,íŸ¤      +   src/sergittos/bedwars/game/shop/Product.phpÐ      Ð  e¤      7   src/sergittos/bedwars/game/shop/tracker/TrackerShop.php®      ®  �X£F¤      ;   src/sergittos/bedwars/game/shop/tracker/TrackerCategory.php�      �  ¸�É—¤      :   src/sergittos/bedwars/game/shop/tracker/TrackerProduct.php@      @  Rì¤      9   src/sergittos/bedwars/game/shop/upgrades/UpgradesShop.phpX      X  ¹LÁ%¤      <   src/sergittos/bedwars/game/shop/upgrades/UpgradesProduct.phpg      g  @
þ¤      @   src/sergittos/bedwars/game/shop/upgrades/product/TrapProduct.phpa      a  ±al¤      C   src/sergittos/bedwars/game/shop/upgrades/product/UpgradeProduct.phpŽ      Ž  jce¤      F   src/sergittos/bedwars/game/shop/upgrades/category/UpgradesCategory.php        ‘Ø’K¤      C   src/sergittos/bedwars/game/shop/upgrades/category/TrapsCategory.php        þ6�¨¤      1   src/sergittos/bedwars/game/shop/item/ItemShop.php        “ºŽ¤      4   src/sergittos/bedwars/game/shop/item/ItemProduct.phpß      ß  ¨ëS¤      ?   src/sergittos/bedwars/game/shop/item/category/MeleeCategory.php‘      ‘  É,I¤      A   src/sergittos/bedwars/game/shop/item/category/PotionsCategory.phpì      ì  öð1¤      @   src/sergittos/bedwars/game/shop/item/category/RangedCategory.php4      4  ÞÙÊM¤      @   src/sergittos/bedwars/game/shop/item/category/BlocksCategory.phpv      v  )yc¤      ?   src/sergittos/bedwars/game/shop/item/category/ArmorCategory.php%      %  µº)`¤      >   src/sergittos/bedwars/game/shop/item/category/MiscCategory.php        ‚Ðæ&¤      ?   src/sergittos/bedwars/game/shop/item/category/ToolsCategory.php“	      “	  ê
¢/¤      -   src/sergittos/bedwars/game/map/MapFactory.phpÆ      Æ  �ó}1¤      5   src/sergittos/bedwars/game/map/task/DeleteMapTask.php        µK]å¤      5   src/sergittos/bedwars/game/map/task/CreateMapTask.php        M3Ú¤      0   src/sergittos/bedwars/game/map/MapProperties.php;      ;  “‘¤      &   src/sergittos/bedwars/game/map/Map.php          —g�¤      G   src/sergittos/bedwars/game/event/presets/UpgradeGeneratorsTierEvent.php¶      ¶  &i¹¤      ?   src/sergittos/bedwars/game/event/presets/EndGameInATieEvent.phpÍ      Í   kÍ™¤      @   src/sergittos/bedwars/game/event/presets/BedDestructionEvent.php]      ]  k³Sé¤      *   src/sergittos/bedwars/game/event/Event.phpE      E  4Ä¤      ,   src/sergittos/bedwars/game/team/Upgrades.phpb      b  w/$¤      (   src/sergittos/bedwars/game/team/Team.phpZ      Z  hn‹r¤      (   src/sergittos/bedwars/game/team/Area.phpŸ      Ÿ  QCEÒ¤      3   src/sergittos/bedwars/game/team/upgrade/Upgrade.phpê      ê  Ý»“'¤      C   src/sergittos/bedwars/game/team/upgrade/presets/ArmorProtection.phpL      L  bÁ¹¤      =   src/sergittos/bedwars/game/team/upgrade/presets/IronForge.php®      ®  °Ä{F¤      C   src/sergittos/bedwars/game/team/upgrade/presets/SharpenedSwords.php’      ’  Úm!`¤      ?   src/sergittos/bedwars/game/team/upgrade/presets/ManiacMiner.phpŸ      Ÿ  ö9tK¤      <   src/sergittos/bedwars/game/team/upgrade/presets/HealPool.php;      ;  °ÞS¤      5   src/sergittos/bedwars/game/team/upgrade/trap/Trap.phpÃ      Ã  ÃÁpd¤      <   src/sergittos/bedwars/game/team/upgrade/trap/DefaultTrap.phpƒ      ƒ  \N«¤      E   src/sergittos/bedwars/game/team/upgrade/trap/CounterOffensiveTrap.phpØ      Ø  ;ùJ÷¤      A   src/sergittos/bedwars/game/team/upgrade/trap/MinerFatigueTrap.php5      5  ¡Œµ˜¤      :   src/sergittos/bedwars/game/team/upgrade/trap/AlarmTrap.phpè      è  ¢[8¤      2   src/sergittos/bedwars/game/team/TeamProperties.phpÃ      Ã  T;4Q¤      2   src/sergittos/bedwars/game/task/RemoveGameTask.phpÿ      ÿ  #¡[¤      4   src/sergittos/bedwars/game/task/GenerateGameTask.php�      �  I®¢¤      *   src/sergittos/bedwars/game/stage/Stage.phpñ      ñ  îA ¤      2   src/sergittos/bedwars/game/stage/StartingStage.phpâ      â  �ÃÑ¤      8   src/sergittos/bedwars/game/stage/trait/JoinableTrait.php£      £  €‘b²¤      1   src/sergittos/bedwars/game/stage/WaitingStage.phpß      ß  .8s1¤      1   src/sergittos/bedwars/game/stage/PlayingStage.php|      |  a<åc¤      0   src/sergittos/bedwars/game/stage/EndingStage.phpH      H  O8>˜¤      *   src/sergittos/bedwars/game/GameManager.php-      -  qò‡¤      #   src/sergittos/bedwars/game/Game.php�#      �#  ÄaTt¤      ;   src/sergittos/bedwars/game/entity/shop/ItemShopVillager.php"      "  }Œ�Ü¤      3   src/sergittos/bedwars/game/entity/shop/Villager.php;	      ;	  r»Á½¤      ?   src/sergittos/bedwars/game/entity/shop/UpgradesShopVillager.phpÂ      Â  œuû?¤      3   src/sergittos/bedwars/game/entity/misc/Fireball.php¨      ¨  á§Çû¤      
   plugin.ymlÇ       Ç   1r|_¤      []# The version
version: 1.0

# The IP that will be showed on the scoreboard and the bossbar
ip: "play.server.net"

# Players won't be able to break/place blocks or receive damage on the lobby if this is enabled
spawn-protection: true

# Provider
# Available providers: sqlite, mysql, json
provider: "sqlite"

# MySQL
mysql-credentials:
  hostname: "127.0.0.1"
  username: "root"
  password: ""
  database: "bedwars"
  port: 3306<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\scoreboard;


use sergittos\bedwars\session\Session;

class LobbyScoreboard extends Scoreboard {

    protected function getLines(Session $session): array {
        return [
            7 => "    ",
            6 => "{WHITE}Coins: {GREEN}" . $session->getCoins(),
            5 => "     ",
            4 => "{WHITE}Total Kills: {GREEN}" . $session->getKills(),
            3 => "{WHITE}Total Wins: {GREEN}" . $session->getWins(),
        ];
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\scoreboard;


use sergittos\bedwars\game\stage\PlayingStage;
use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\ColorUtils;
use function date;
use function gmdate;

class GameScoreboard extends Scoreboard {

    protected function getLines(Session $session): array {
        if(!$session->hasGame()) {
            return [];
        }

        $stage = $session->getGame()->getStage();
        if(!$stage instanceof PlayingStage) {
            return [];
        }
        $event = $stage->getNextEvent();

        return [
            14 => "{GRAY}" . date("m/d/y"),
            13 => " ",
            12 => "{WHITE}" . $event->getName() . " in: {GREEN}" . gmdate("i:s", $event->getTimeRemaining()) . "   ",
            11 => "  ",
        ] + $this->getTeams($session);
    }

    private function getTeams(Session $session): array {
        $teams = [];
        $score = 10;
        foreach($session->getGame()->getTeams() as $team) {
            $teams[$score] = ColorUtils::translate(
                $team->getColor() . $team->getFirstLetter() . " {WHITE}" . $team->getName() . ": " .
                $this->getBedStatus($team) . ($team->hasMember($session) ? " {GRAY}YOU" : " ")
            );
            $score--;
        }

        return $teams;
    }

    private function getBedStatus(Team $team): string {
        return !$team->isAlive() ? "{RED}X" : "{GREEN}" . ($team->isBedDestroyed() ? $team->getMembersCount() : "Alive");
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\scoreboard;


use sergittos\bedwars\game\Game;
use sergittos\bedwars\game\stage\StartingStage;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\ConfigGetter;
use sergittos\bedwars\utils\GameUtils;
use function strtolower;

class WaitingScoreboard extends Scoreboard {

    protected function getLines(Session $session): array {
        $game = $session->getGame();
        $map = $game->getMap();
        $stage = $game->getStage();
        return [
            10 => " ",
            9 => "{WHITE}Map: {GREEN}" . $map->getName(),
            8 => "{WHITE}Players: {GREEN}" . $game->getPlayersCount() . "/" . $map->getMaxCapacity(),
            7 => "  ",
            6 => !$stage instanceof StartingStage ? "{WHITE}Waiting..." : "{WHITE}Starting in {GREEN}" . $stage->getCountdown() . "s",
            5 => "   ",
            4 => "{WHITE}Mode: {GREEN}" . GameUtils::getMode($map->getPlayersPerTeam()),
            3 => "{WHITE}Version: {GRAY}v" . ConfigGetter::getVersion()
        ];
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\scoreboard;


use pocketmine\network\mcpe\protocol\RemoveObjectivePacket;
use pocketmine\network\mcpe\protocol\SetDisplayObjectivePacket;
use pocketmine\network\mcpe\protocol\SetScorePacket;
use pocketmine\network\mcpe\protocol\types\ScorePacketEntry;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\ColorUtils;
use sergittos\bedwars\utils\ConfigGetter;

abstract class Scoreboard {

    private const TITLE = "{YELLOW}BED WARS";

    public function show(Session $session): void {
        if($session->isOnline()) {
            $this->hide($session);

            $packet = new SetDisplayObjectivePacket();
            $packet->displaySlot = SetDisplayObjectivePacket::DISPLAY_SLOT_SIDEBAR;
            $packet->objectiveName = $session->getUsername();
            $packet->displayName = ColorUtils::translate(self::TITLE);
            $packet->criteriaName = "dummy";
            $packet->sortOrder = SetDisplayObjectivePacket::SORT_ORDER_DESCENDING;
            $session->sendDataPacket($packet);

            foreach($this->getLines($session) as $score => $line) {
                $this->addLine($score, " " . $line, $session);
            }
            $this->addLine(2, "      ", $session);
            $this->addLine(1, " {YELLOW}" . ConfigGetter::getIP(), $session);
        }
    }

    private function addLine(int $score, string $text, Session $session): void {
        $entry = new ScorePacketEntry();
        $entry->objectiveName = $session->getUsername();
        $entry->type = ScorePacketEntry::TYPE_FAKE_PLAYER;
        $entry->customName = ColorUtils::translate($text);
        $entry->score = $score;
        $entry->scoreboardId = $score;
        $packet = new SetScorePacket();
        $packet->type = SetScorePacket::TYPE_CHANGE;
        $packet->entries[] = $entry;
        $session->sendDataPacket($packet);
    }

    private function hide(Session $session): void {
        if($session->isOnline()) {
            $packet = new RemoveObjectivePacket();
            $packet->objectiveName = $session->getUsername();
            $session->sendDataPacket($packet);
        }
    }

    /**
     * @return string[]
     */
    abstract protected function getLines(Session $session): array;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session;


use pocketmine\entity\effect\EffectInstance;
use pocketmine\entity\effect\InvisibilityEffect;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\item\ItemTypeIds;
use pocketmine\item\VanillaItems;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\protocol\BossEventPacket;
use pocketmine\network\mcpe\protocol\ClientboundPacket;
use pocketmine\network\mcpe\protocol\MobArmorEquipmentPacket;
use pocketmine\network\mcpe\protocol\MobEquipmentPacket;
use pocketmine\network\mcpe\protocol\PlaySoundPacket;
use pocketmine\network\mcpe\protocol\types\BossBarColor;
use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
use pocketmine\player\GameMode;
use pocketmine\player\Player;
use pocketmine\Server;
use sergittos\bedwars\BedWars;
use sergittos\bedwars\game\Game;
use sergittos\bedwars\game\stage\EndingStage;
use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\item\BedwarsItems;
use sergittos\bedwars\session\scoreboard\LobbyScoreboard;
use sergittos\bedwars\session\scoreboard\Scoreboard;
use sergittos\bedwars\session\settings\GameSettings;
use sergittos\bedwars\session\settings\SpectatorSettings;
use sergittos\bedwars\session\setup\MapSetup;
use sergittos\bedwars\utils\ColorUtils;
use sergittos\bedwars\utils\ConfigGetter;
use function in_array;
use function strtoupper;
use function time;

class Session {

    public const RESPAWN_DURATION = 5;

    private Player $player;
    private Scoreboard $scoreboard;

    private GameSettings $game_settings;
    private SpectatorSettings $spectator_settings;

    private ?Game $game = null;
    private ?Team $team = null;

    private ?MapSetup $mapSetup = null;

    private ?Session $last_session_hit = null;
    private ?Session $tracking_session = null;

    private ?int $respawn_time = null;
    private ?int $last_session_hit_time = null;

    private int $coins = 0;
    private int $kills = 0;
    private int $wins = 0;

    private bool $loading_data;

    public function __construct(Player $player) {
        $this->player = $player;
        $this->game_settings = new GameSettings($this);

        $this->load();
        $this->setEffectHooks();
    }

    public function getPlayer(): Player {
        return $this->player;
    }

    public function getUsername(): string {
        return $this->player->getName();
    }

    public function getColoredUsername(): string {
        $username = $this->getUsername();
        if($this->hasTeam()) {
            return $this->team->getColor() . $username;
        }
        return $username;
    }

    public function getGameSettings(): GameSettings {
        return $this->game_settings;
    }

    public function getSpectatorSettings(): SpectatorSettings {
        return $this->spectator_settings;
    }

    public function getGame(): ?Game {
        return $this->game;
    }

    public function getTeam(): ?Team {
        return $this->team;
    }

    public function getMapSetup(): ?MapSetup {
        return $this->mapSetup;
    }

    public function getLastSessionHit(): ?Session {
        if($this->last_session_hit_time === null) {
            return null;
        }
        if(time() - $this->last_session_hit_time <= 10) {
            return $this->last_session_hit;
        }
        return null;
    }

    public function getTrackingSession(): ?Session {
        return $this->tracking_session;
    }

    public function getCoins(): int {
        return $this->coins;
    }

    public function getKills(): int {
        return $this->kills;
    }

    public function getWins(): int {
        return $this->wins;
    }

    public function resetSettings(): void {
        $this->game_settings = new GameSettings($this);
        $this->respawn_time = null;
    }

    public function setSpectatorSettings(SpectatorSettings $spectator_settings): void {
        $this->spectator_settings = $spectator_settings;
    }

    public function setScoreboard(Scoreboard $scoreboard): void {
        $this->scoreboard = $scoreboard;
        $this->updateScoreboard();
    }

    public function setGame(?Game $game): void {
        $this->game = $game;
    }

    public function setTeam(?Team $team): void {
        $this->team = $team;
    }

    public function setMapSetup(?MapSetup $mapSetup): void {
        $this->mapSetup = $mapSetup;
    }

    public function setLastSessionHit(?Session $last_session_hit): void {
        $this->last_session_hit = $last_session_hit;
        $this->last_session_hit_time = time();
    }

    public function setTrackingSession(?Session $tracking_session): void {
        $this->tracking_session = $tracking_session;
        $this->updateCompassDirection();
    }

    public function updateCompassDirection(): void {
        $this->player->getNetworkSession()->syncWorldSpawnPoint(
            $this->tracking_session !== null ? $this->tracking_session->getPlayer()->getPosition() : $this->player->getWorld()->getSpawnLocation()
        );
    }

    public function updateScoreboard(): void {
        $this->scoreboard->show($this);
    }

    public function attemptToRespawn(): void {
        if($this->respawn_time <= 0) {
            $this->respawn_time = null;
            $this->respawn();
            return;
        }

        if($this->respawn_time < 5) {
            $message = "{YELLOW}You will respawn in {RED}" . $this->respawn_time . " {YELLOW}" . ($this->respawn_time === 1 ? "second" : "seconds") . "!";
            $this->title("{RED}YOU DIED!", $message);
            $this->message($message);
        }

        $this->respawn_time--;
    }

    private function respawn(): void {
        $this->message("{YELLOW}You have respawned!");
        $this->title("{GREEN}RESPAWNED!", "", 7, 21, 7);

        $this->game_settings->apply();
        $this->player->setGamemode(GameMode::SURVIVAL());
        $this->player->setHealth($this->player->getMaxHealth());
        $this->player->teleport($this->team->getSpawnPoint());
    }

    public function setCoins(int $coins): void {
        $this->coins = $coins;

        if(!$this->loading_data) {
            BedWars::getInstance()->getProvider()->updateCoins($this);
        }
    }

    public function addCoins(int $coins): void {
        $this->setCoins($this->coins + $coins);
    }

    public function setKills(int $kills): void {
        $this->kills = $kills;

        if(!$this->loading_data) {
            BedWars::getInstance()->getProvider()->updateKills($this);
        }
    }

    public function addKill(): void {
        $this->setKills($this->kills + 1);
    }

    public function setWins(int $wins): void {
        $this->wins = $wins;

        if(!$this->loading_data) {
            BedWars::getInstance()->getProvider()->updateWins($this);
        }
    }

    public function addWin(): void {
        $this->setWins($this->wins + 1);
    }

    public function isPlaying(): bool {
        return $this->hasGame() and $this->game->isPlaying($this);
    }

    public function isSpectator(): bool {
        return $this->hasGame() and $this->game->isSpectator($this);
    }

    public function hasGame(): bool {
        return $this->game !== null;
    }

    public function hasTeam(): bool {
        return $this->team !== null;
    }

    public function isCreatingMap(): bool {
        return $this->mapSetup !== null;
    }

    public function isRespawning(): bool {
        return $this->respawn_time !== null;
    }

    public function isOnline(): bool {
        return $this->player->isOnline();
    }

    public function showBossBar(string $title): void {
        $this->hideBossBar();
        $this->sendDataPacket(
            BossEventPacket::show($this->player->getId(), ColorUtils::translate($title), 10, false, 0, BossBarColor::BLUE)
        );
    }

    public function hideBossBar(): void {
        $this->sendDataPacket(BossEventPacket::hide($this->player->getId()));
    }

    public function sendDataPacket(ClientboundPacket $packet): void {
        $this->player->getNetworkSession()->sendDataPacket($packet);
    }

    public function playSound(string $sound, float $volume = 1.0, float $pitch = 1.0): void {
        $location = $this->player->getLocation();
        $this->sendDataPacket(PlaySoundPacket::create(
            $sound,
            $location->getX(),
            $location->getY(),
            $location->getZ(),
            $volume,
            $pitch
        ));
    }

    public function clearAllInventories(): void {
        $this->clearCommonInventories();
        $this->player->getEnderInventory()->clearAll();
    }

    public function clearCommonInventories(): void {
        $this->player->getCursorInventory()->clearAll();
        $this->player->getOffHandInventory()->clearAll();
        $this->player->getArmorInventory()->clearAll();
        $this->player->getInventory()->clearAll();
    }

    public function giveCreatingMapItems(): void {
        $this->clearAllInventories();

        $inventory = $this->player->getInventory();
        $inventory->setItem(0, BedwarsItems::CONFIGURATION()->asItem());
        $inventory->setItem(4, BedwarsItems::CREATE_MAP()->asItem());
        $inventory->setItem(8, BedwarsItems::EXIT_SETUP()->asItem());
    }

    public function giveWaitingItems(): void {
        $this->clearAllInventories();
        $this->player->getInventory()->setItem(8, BedwarsItems::LEAVE_GAME()->asItem());
    }

    public function giveSpectatorItems(): void {
        $this->clearAllInventories();

        $inventory = $this->player->getInventory();
        $inventory->setItem(0, BedwarsItems::TELEPORTER()->asItem());
        $inventory->setItem(4, BedwarsItems::SPECTATOR_SETTINGS()->asItem());
        $inventory->setItem(7, BedwarsItems::PLAY_AGAIN()->asItem());
        $inventory->setItem(8, BedwarsItems::RETURN_TO_LOBBY()->asItem());
    }

    public function addEffect(EffectInstance $effect_instance): void {
        $this->player->getEffects()->add($effect_instance);
    }

    public function teleportToWaitingWorld(): void {
        $this->player->teleport($this->game->getMap()->getWaitingWorld()->getSafeSpawn());
    }

    public function teleportToHub(): void {
        $this->player->getEffects()->clear();
        $this->player->setGamemode(GameMode::ADVENTURE());
        $this->player->setHealth($this->player->getMaxHealth());
        $this->player->setNameTag($this->player->getDisplayName());
        $this->player->teleport(Server::getInstance()->getWorldManager()->getDefaultWorld()->getSafeSpawn());

        $this->clearAllInventories();
        $this->setTrackingSession(null);
        $this->setScoreboard(new LobbyScoreboard());
        $this->showBossBar("{DARK_GREEN}You are playing on {AQUA}" . strtoupper(ConfigGetter::getIP()));
    }

    public function kill(int $cause): void {
        $killer_session = $this->getLastSessionHit();
        $session_username = $this->getColoredUsername();

        if($killer_session !== null) {
            $killer_session->addCoins(8); // TODO: Check for final kill
            $killer_session->addKill();
            $killer_session->playSound("random.orb");

            $killer_username = $killer_session->getColoredUsername();
            if($cause === EntityDamageEvent::CAUSE_ENTITY_ATTACK) {
                $this->game->broadcastMessage($session_username . " {GRAY}was killed by " . $killer_username . "{GRAY}.");
            } elseif($cause === EntityDamageEvent::CAUSE_VOID) {
                $this->game->broadcastMessage($session_username . " {GRAY}was knocked into the void by " . $killer_username . "{GRAY}.");
            }

            if(!$killer_session->isSpectator()) {
                foreach($this->player->getInventory()->getContents() as $item) {
                    if(!in_array($item->getTypeId(), [ItemTypeIds::IRON_INGOT, ItemTypeIds::GOLD_INGOT, ItemTypeIds::DIAMOND, ItemTypeIds::EMERALD])) {
                        continue;
                    }
                    $killer_session->getPlayer()->getInventory()->addItem($item);
                }
            }
        }

        if($cause === EntityDamageEvent::CAUSE_VOID and $killer_session === null) {
            $this->game->broadcastMessage($session_username . " {GRAY}fell to the void.");
        }

        $this->player->getEffects()->clear();
        $this->player->teleport($this->game->getMap()->getSpectatorSpawnPosition());
        $this->player->setGamemode(GameMode::SPECTATOR());

        $this->game_settings->decreasePickaxeTier();
        $this->game_settings->decreaseAxeTier();

        if($this->hasTeam() and $this->team->isBedDestroyed()) {
            $this->game->removePlayer($this, false, true);
            return;
        } elseif($this->game->getStage() instanceof EndingStage) {
            return;
        }
        $this->respawn_time = self::RESPAWN_DURATION;

        $this->clearCommonInventories();
        $this->title(
            "{RED}YOU DIED!",
            $message = "{YELLOW}You will respawn in {RED}" . self::RESPAWN_DURATION . " {YELLOW}seconds!", 0, 41
        );
        $this->message($message);
    }

    private function setEffectHooks(): void {
        $effects = $this->player->getEffects();

        $effects->getEffectAddHooks()->add(function(EffectInstance $effect_instance): void {
            if($effect_instance->getType() instanceof InvisibilityEffect and $this->isPlaying()) {
                $this->vanish();
            }
        });
        $effects->getEffectRemoveHooks()->add(function(EffectInstance $effect_instance): void {
            if($effect_instance->getType() instanceof InvisibilityEffect and $this->isPlaying()) {
                $this->unvanish();
            }
        });
    }

    private function vanish(): void {
        $id = $this->player->getId();
        $item = ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet(VanillaItems::AIR()));
        $slot = $this->player->getInventory()->getHeldItemIndex();

        foreach($this->game->getPlayers() as $session) {
            $session = $session->getPlayer()->getNetworkSession();
            $session->sendDataPacket(MobEquipmentPacket::create($id, $item, $slot, $slot, ContainerIds::INVENTORY));
            $session->sendDataPacket(MobArmorEquipmentPacket::create($id, $item, $item, $item, $item));
        }
    }

    private function unvanish(): void {
        foreach($this->game->getPlayers() as $session) {
            $network_session = $session->getPlayer()->getNetworkSession();
            $broadcaster = $network_session->getEntityEventBroadcaster();

            $broadcaster->onMobArmorChange([$network_session], $this->player);
            $broadcaster->onMobMainHandItemChange([$network_session], $this->player);
        }
    }

    public function load(): void {
        $this->loading_data = true;
        BedWars::getInstance()->getProvider()->loadSession($this);
        $this->loading_data = false;
    }

    public function save(): void {
        BedWars::getInstance()->getProvider()->saveSession($this);
    }

    public function title(string $title, string $subtitle = "", int $fade_in = 0, int $stay = 21, int $fade_out = 0): void {
        $this->player->sendTitle(
            ColorUtils::translate($title), ColorUtils::translate($subtitle), $fade_in, $stay, $fade_out
        );
    }

    public function message(string $message): void {
        $this->player->sendMessage(ColorUtils::translate($message));
    }

}
<?php

declare(strict_types=1);


namespace sergittos\bedwars\session;


use pocketmine\player\Player;
use function strtolower;

class SessionFactory {

    /** @var Session[] */
    static private array $sessions = [];

    /**
     * @return Session[]
     */
    static public function getSessions(): array {
        return self::$sessions;
    }

    static public function hasSession(Player $player): bool {
        return isset(self::$sessions[strtolower($player->getName())]);
    }

    static public function getSession(Player $player): ?Session {
        return self::$sessions[strtolower($player->getName())] ?? null;
    }

    static public function getSessionByName(string $name): ?Session {
        return self::$sessions[strtolower($name)] ?? null;
    }

    static public function createSession(Player $player): void {
        self::$sessions[strtolower($player->getName())] = new Session($player);
    }

    static public function removeSession(Player $player): void {
        self::getSession($player)->save();
        unset(self::$sessions[strtolower($player->getName())]);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\settings;


use pocketmine\item\Armor;
use pocketmine\item\enchantment\EnchantmentInstance;
use pocketmine\item\enchantment\VanillaEnchantments;
use pocketmine\item\Item;
use pocketmine\item\StringToItemParser;
use pocketmine\item\Tool;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\item\BedwarsItems;
use sergittos\bedwars\session\Session;
use function strtolower;
use function time;

class GameSettings {

    private Session $session;

    private bool $permanent_shears = false;

    private ?string $armor = null;

    private int $magic_milk_time = 0;

    private int $pickaxe_tier = 0;
    private int $axe_tier = 0;

    public function __construct(Session $session) {
        $this->session = $session;
    }

    public function hasPermanentShears(): bool {
        return $this->permanent_shears;
    }

    public function hasArmor(): bool {
        return $this->armor !== null;
    }

    public function isUnderMagicMilkEffect(): bool {
        return time() - $this->magic_milk_time < 30;
    }

    public function isPickaxeFullUpgraded(): bool {
        return $this->pickaxe_tier >= 4;
    }

    public function isAxeFullUpgraded(): bool {
        return $this->axe_tier >= 4;
    }

    public function getArmor(): ?string {
        return $this->armor;
    }

    public function getPickaxeTier(): int {
        return $this->pickaxe_tier;
    }

    public function getAxeTier(): int {
        return $this->axe_tier;
    }

    public function setPermanentShears(): void {
        $this->permanent_shears = true;
    }

    public function setMagicMilk(): void {
        $this->magic_milk_time = time();
    }

    public function incrasePickaxeTier(): void {
        $this->pickaxe_tier++;
    }

    public function incraseAxeTier(): void {
        $this->axe_tier++;
    }

    public function decreasePickaxeTier(): void {
        if($this->pickaxe_tier > 1) {
            $this->pickaxe_tier--;
        }
    }

    public function decreaseAxeTier(): void {
        if($this->axe_tier > 1) {
            $this->axe_tier--;
        }
    }

    public function setArmor(?string $armor): void {
        $this->armor = $armor;
        $this->applyArmor();
    }

    public function apply(): void {
        $player = $this->session->getPlayer();
        if(!$player->isConnected()) {
            return;
        }

        $inventory = $player->getInventory();
        $inventory->addItem(VanillaItems::WOODEN_SWORD()->setUnbreakable());
        $inventory->setItem(8, BedwarsItems::TRACKER_SHOP()->asItem());

        if($this->permanent_shears) {
            $inventory->addItem(VanillaItems::SHEARS());
        }
        if($this->pickaxe_tier > 0) {
            $inventory->addItem($this->getTool("pickaxe", $this->pickaxe_tier));
        }
        if($this->axe_tier > 0) {
            $inventory->addItem($this->getTool("axe", $this->axe_tier));
        }

        $inventory = $player->getArmorInventory();
        $inventory->setHelmet($this->getLeatherArmor(VanillaItems::LEATHER_CAP()));
        $inventory->setChestplate($this->getLeatherArmor(VanillaItems::LEATHER_TUNIC()));

        $this->applyArmor();

        foreach($this->session->getTeam()->getUpgrades()->getAll() as $upgrade) {
            $upgrade->applySession($this->session);
        }
    }

    private function applyArmor(): void {
        $inventory = $this->session->getPlayer()->getArmorInventory();
        if($this->armor !== null) {
            $inventory->setLeggings($this->getArmorPiece("leggings"));
            $inventory->setBoots($this->getArmorPiece("boots"));
        } else {
            $inventory->setLeggings($this->getLeatherArmor(VanillaItems::LEATHER_PANTS()));
            $inventory->setBoots($this->getLeatherArmor(VanillaItems::LEATHER_BOOTS()));
        }
    }

    public function getTool(string $name, int $tier): Tool {
        /** @var Tool $item */
        $item = StringToItemParser::getInstance()->parse(strtolower($this->getMaterial($tier) . "_" . $name));
        $item->setUnbreakable();
        $item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), $tier !== 4 ? $tier : 3));
        if($tier === 3) {
            $item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::SHARPNESS(), 3));
        }
        return $item;
    }

    private function getLeatherArmor(Armor $armor): Armor {
        return $this->addComponents($armor->setCustomColor($this->session->getTeam()->getDyeColor()->getRgbValue()))->setUnbreakable();
    }

    private function getArmorPiece(string $piece): Armor {
        return $this->addComponents(StringToItemParser::getInstance()->parse(strtolower($this->armor) . "_" . strtolower($piece)))->setUnbreakable();
    }

    private function addComponents(Item $item): Armor|Item {
        $protection_level = $this->session->getTeam()?->getUpgrades()->getArmorProtection()->getLevel();
        if($protection_level >= 1) {
            $item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::PROTECTION(), $protection_level));
        }
        $item->getNamedTag()->setByte("bedwars_item", 1);

        return $item;
    }

    public function getMaterial(int $tier): string {
        return match($tier) {
            0, 1 => "Wooden",
            2 => "Iron",
            3 => "Golden",
            4, 5 => "Diamond"
        };
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\settings;


use pocketmine\entity\effect\EffectInstance;
use pocketmine\entity\effect\VanillaEffects;
use pocketmine\utils\Limits;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\GameUtils;

class SpectatorSettings {

    private Session $session;

    private int $flying_speed;

    private bool $auto_teleport;
    private bool $night_vision;

    public function __construct(Session $session, int $flying_speed, bool $auto_teleport, bool $night_vision) {
        $this->session = $session;
        $this->flying_speed = $flying_speed;
        $this->auto_teleport = $auto_teleport;
        $this->night_vision = $night_vision;
    }

    static public function fromData(Session $session, array $data): SpectatorSettings {
        return new SpectatorSettings($session, (int) $data["flying_speed"], (bool) $data["auto_teleport"], (bool) $data["night_vision"]);
    }

    public function getFlyingSpeed(): int {
        return $this->flying_speed;
    }

    public function getAutoTeleport(): bool {
        return $this->auto_teleport;
    }

    public function getNightVision(): bool {
        return $this->night_vision;
    }

    public function setFlyingSpeed(int $flying_speed): void {
        $this->flying_speed = $flying_speed;

        if($this->flying_speed !== 0) {
            $this->session->message("{GREEN}You now have Speed " . GameUtils::intToRoman($flying_speed) . "!");
        } else {
            $this->session->message("{RED}You no longer have any speed effects!");
        }
    }

    public function setAutoTeleport(bool $auto_teleport): void {
        $this->auto_teleport = $auto_teleport;

        if($this->auto_teleport) {
            $this->session->message("{GREEN}Once you select a player using your compass, it will auto teleport you to them!");
        } else {
            $this->session->message("{RED}You will no longer auto teleport to targets!");
        }
    }

    public function setNightVision(bool $night_vision): void {
        $this->night_vision = $night_vision;

        if($this->night_vision) {
            $this->session->message("{GREEN}You now have night vision!");
        } else {
            $this->session->message("{RED}You no longer have night vision!");
        }
    }

    public function apply(): void {
        $this->session->getPlayer()->getEffects()->clear();
        if($this->flying_speed !== 0) {
            $this->session->addEffect(new EffectInstance(VanillaEffects::SPEED(), Limits::INT32_MAX, $this->flying_speed - 1, false));
        }
        if($this->night_vision) {
            $this->session->addEffect(new EffectInstance(VanillaEffects::NIGHT_VISION(), Limits::INT32_MAX, 0, false));
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\setup\step;


use pocketmine\block\Block;
use sergittos\bedwars\game\generator\GeneratorType;
use sergittos\bedwars\item\BedwarsItem;
use sergittos\bedwars\item\BedwarsItems;
use sergittos\bedwars\item\setup\AddGeneratorItem;

class AddGeneratorStep extends Step {

    private GeneratorType $type;

    public function __construct(GeneratorType $type) {
        $this->type = $type;
    }

    protected function onStart(): void {
        $block = match($this->type) {
            GeneratorType::DIAMOND => BedwarsItems::DIAMOND_GENERATOR(),
            GeneratorType::EMERALD => BedwarsItems::EMERALD_GENERATOR()
        };

        $this->session->clearAllInventories();
        $this->session->message("{YELLOW}Break the block below the generator with the item you have received in your inventory to set the generator.");

        $inventory = $this->session->getPlayer()->getInventory();
        $inventory->setItem(0, $block->asItem());
        $inventory->setItem(8, BedwarsItems::CANCEL()->asItem());
    }

    public function onBlockBreak(Block $block, BedwarsItem $item): void {
        if($item instanceof AddGeneratorItem) {
            $position = $block->getPosition();
            $position->getWorld()->setBlock($position, $item->asItem()->getBlock());

            $setup = $this->session->getMapSetup();
            $setup->getMapBuilder()->addGenerator(GeneratorType::toGenerator($position, $this->type));
            $setup->setStep(new PreparingMapStep());

            $this->session->message("{GREEN}You have successfully set the generator.");
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\setup\step;


use pocketmine\block\Block;
use sergittos\bedwars\item\BedwarsItem;
use sergittos\bedwars\item\BedwarsItems;
use sergittos\bedwars\item\setup\SetTeamGeneratorItem;
use sergittos\bedwars\session\setup\builder\TeamBuilder;

class SetTeamGeneratorStep extends Step {

    private TeamBuilder $team;

    public function __construct(TeamBuilder $team) {
        $this->team = $team;
    }

    protected function onStart(): void {
        $this->session->clearAllInventories();
        $this->session->message("{YELLOW}Break the block below the generator with the item you have received in your inventory to set the generator.");

        $inventory = $this->session->getPlayer()->getInventory();
        $inventory->setItem(0, BedwarsItems::TEAM_GENERATOR()->asItem());
        $inventory->setItem(8, BedwarsItems::CANCEL()->asItem());
    }

    public function onBlockBreak(Block $block, BedwarsItem $item): void {
        if($item instanceof SetTeamGeneratorItem) {
            $this->team->setGeneratorPosition($block->getPosition());

            $this->session->getMapSetup()->setStep(new PreparingMapStep());
            $this->session->message("{GREEN}You have successfully set the generator.");
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\setup\step;


use pocketmine\block\VanillaBlocks;
use pocketmine\event\Cancellable;
use pocketmine\event\player\PlayerInteractEvent;
use pocketmine\item\VanillaItems;
use pocketmine\math\Vector3;
use pocketmine\world\BlockTransaction;
use sergittos\bedwars\item\BedwarsItem;
use sergittos\bedwars\item\BedwarsItems;
use sergittos\bedwars\item\setup\SetBedPositionItem;
use sergittos\bedwars\session\setup\builder\TeamBuilder;

class SetBedPositionStep extends Step {

    private TeamBuilder $team;

    public function __construct(TeamBuilder $team) {
        $this->team = $team;
    }

    protected function onStart(): void {
        $this->session->clearAllInventories();
        $this->session->message("{YELLOW}Place the bed you received in your inventory to set the position.");

        $inventory = $this->session->getPlayer()->getInventory();
        $inventory->setItem(0, BedwarsItems::BED_POSITION()->setColor($this->team->getDyeColor())->asItem());
        $inventory->setItem(8, BedwarsItems::CANCEL()->asItem());
    }

    public function onBlockInteract(Vector3 $touch_vector, int $action, Cancellable $event, BedwarsItem $item): void {
        if($item instanceof SetBedPositionItem and $action === PlayerInteractEvent::RIGHT_CLICK_BLOCK) {
            $this->team->setBedPosition($touch_vector);
            
            $this->session->getMapSetup()->setStep(new PreparingMapStep());
            $this->session->message("{GREEN}You have successfully set the bed position.");

            $event->uncancel();
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\setup\step;


use pocketmine\event\Cancellable;
use pocketmine\event\player\PlayerInteractEvent;
use pocketmine\math\Vector3;
use pocketmine\utils\TextFormat;
use sergittos\bedwars\game\shop\Shop;
use sergittos\bedwars\item\BedwarsItem;
use sergittos\bedwars\item\BedwarsItems;
use sergittos\bedwars\item\setup\AddVillagerItem;

class SetShopAndUpgradesStep extends Step {

    protected function onStart(): void {
        $this->session->clearAllInventories();
        $this->session->message("{YELLOW}Use the eggs you received in your inventory to set the shop and upgrades of the team");

        $inventory = $this->session->getPlayer()->getInventory();
        $inventory->setItem(0, BedwarsItems::ITEM_VILLAGER()->asItem());
        $inventory->setItem(1, BedwarsItems::UPGRADES_VILLAGER()->asItem());
        $inventory->setItem(8, BedwarsItems::CANCEL()->asItem()->setCustomName(TextFormat::GREEN . "Done"));
    }

    public function onBlockInteract(Vector3 $touch_vector, int $action, Cancellable $event, BedwarsItem $item): void {
        if($action !== PlayerInteractEvent::RIGHT_CLICK_BLOCK or !$item instanceof AddVillagerItem) {
            return;
        }

        $map = $this->session->getMapSetup()->getMapBuilder();

        $name = $item->getName();
        match($name) {
            Shop::ITEM => $map->addShopPosition($touch_vector),
            Shop::UPGRADES => $map->addUpgradesPosition($touch_vector)
        };

        $this->session->message("{GREEN}You have successfully set the $name position.");
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\setup\step;


use pocketmine\block\Block;
use pocketmine\event\Cancellable;
use pocketmine\item\Item;
use pocketmine\math\Vector3;
use sergittos\bedwars\item\BedwarsItem;
use sergittos\bedwars\session\Session;

abstract class Step {

    protected Session $session;

    public function start(Session $session): void {
        $this->session = $session;
        $this->onStart();
    }

    abstract protected function onStart(): void;

    public function onBlockBreak(Block $block, BedwarsItem $item): void {}

    public function onBlockInteract(Vector3 $touch_vector, int $action, Cancellable $event, BedwarsItem $item): void {}

    public function onInteract(BedwarsItem $item): void {}



}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\setup\step;


class PreparingMapStep extends Step { // This is like the default step

    protected function onStart(): void {
        $this->session->giveCreatingMapItems();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\setup\step\area;


use sergittos\bedwars\game\team\Area;

class SetClaimStep extends SetAreaStep {

    protected function setArea(Area $area): void {
        $this->team->setClaim($area);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\setup\step\area;


use pocketmine\event\Cancellable;
use pocketmine\event\player\PlayerInteractEvent;
use pocketmine\math\Vector3;
use sergittos\bedwars\game\team\Area;
use sergittos\bedwars\item\BedwarsItem;
use sergittos\bedwars\item\BedwarsItems;
use sergittos\bedwars\item\setup\ClaimingWandItem;
use sergittos\bedwars\session\setup\builder\TeamBuilder;
use sergittos\bedwars\session\setup\step\PreparingMapStep;
use sergittos\bedwars\session\setup\step\Step;

abstract class SetAreaStep extends Step {

    protected TeamBuilder $team;

    private Vector3 $first_position;
    private Vector3 $second_position;

    public function __construct(TeamBuilder $team) {
        $this->team = $team;
    }

    protected function onStart(): void {
        $this->session->clearAllInventories();
        $this->session->message("{GOLD}Set team area started.");
        $this->session->message("{YELLOW}Left click at a corner of the area you'd like to set.");
        $this->session->message("{YELLOW}Right click on the second corner of the area you'd like to set.");
        $this->session->message("{YELLOW}Sneak and click to set your area.");
        $this->session->message("{GREEN}Gave you a claim wand.");

        $inventory = $this->session->getPlayer()->getInventory();
        $inventory->setItem(0, BedwarsItems::CLAIMING_WAND()->asItem());
        $inventory->setItem(8, BedwarsItems::CANCEL()->asItem());
    }

    public function onBlockInteract(Vector3 $touch_vector, int $action, Cancellable $event, BedwarsItem $item): void {
        if(!$item instanceof ClaimingWandItem) {
            return;
        }

        switch($action) {
            case PlayerInteractEvent::LEFT_CLICK_BLOCK:
                $this->first_position = $touch_vector;
                $this->session->message("{GREEN}First position set.");
                break;
            case PlayerInteractEvent::RIGHT_CLICK_BLOCK:
                $this->second_position = $touch_vector;
                $this->session->message("{GREEN}Second position set.");
                break;
        }
    }

    public function onInteract(BedwarsItem $item): void {
        if(!$this->session->getPlayer()->isSneaking() or !$item instanceof ClaimingWandItem) {
            return;
        }

        if(!isset($this->first_position) or !isset($this->second_position)) {
            $this->session->message("{RED}You must set both positions before setting the area.");
            return;
        }

        $this->setArea(new Area($this->first_position, $this->second_position));

        $this->session->getMapSetup()->setStep(new PreparingMapStep());
        $this->session->message("{GREEN}You have successfully set the area.");
    }

    abstract protected function setArea(Area $area): void;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\setup\step\area;


use sergittos\bedwars\game\team\Area;

class SetZoneStep extends SetAreaStep {

    protected function setArea(Area $area): void {
        $this->team->setZone($area);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\setup;


use pocketmine\Server;
use sergittos\bedwars\game\map\task\CreateMapTask;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\setup\builder\MapBuilder;
use sergittos\bedwars\session\setup\step\PreparingMapStep;
use sergittos\bedwars\session\setup\step\Step;

class MapSetup {

    private Session $session;
    private MapBuilder $map_builder;
    private Step $step;

    public function __construct(Session $session, MapBuilder $map_builder) {
        $this->session = $session;
        $this->map_builder = $map_builder;

        $this->setStep(new PreparingMapStep());
    }

    public function getMapBuilder(): MapBuilder {
        return $this->map_builder;
    }

    public function getStep(): Step {
        return $this->step;
    }

    public function setStep(Step $step): void {
        $this->step = $step;
        $this->step->start($this->session);
    }

    public function createMap(): void {
        Server::getInstance()->getAsyncPool()->submitTask(new CreateMapTask($this->map_builder));
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\setup\builder;


use pocketmine\math\Vector3;
use pocketmine\world\World;
use sergittos\bedwars\game\generator\Generator;
use sergittos\bedwars\game\map\Map;
use sergittos\bedwars\game\map\MapProperties;
use sergittos\bedwars\game\team\Team;
use function array_map;

class MapBuilder {
    use MapProperties;

    private string $playing_world;

    /** @var TeamBuilder[] */
    private array $teams;

    public function __construct(string $name, World $waiting_world, string $playing_world, int $players_per_team, int $max_capacity) {
        $this->name = $name;
        $this->waiting_world = $waiting_world;
        $this->playing_world = $playing_world;
        $this->players_per_team = $players_per_team;
        $this->max_capacity = $max_capacity;

        $this->setDefaultTeams();
    }

    public function getPlayingWorld(): string {
        return $this->playing_world;
    }

    public function setSpectatorSpawnPosition(Vector3 $spectator_spawn_position): void {
        $this->spectator_spawn_position = $spectator_spawn_position;
    }

    public function addShopPosition(Vector3 $position): void {
        $this->shop_positions[] = $position;
    }

    public function addUpgradesPosition(Vector3 $position): void {
        $this->upgrades_positions[] = $position;
    }

    public function addGenerator(Generator $generator): void {
        $this->generators[] = $generator;
    }

    public function removeGenerator(Generator $generator): void {
        unset($this->generators[array_search($generator, $this->generators, true)]);
    }

    private function addTeam(string $name): void {
        $this->teams[] = new TeamBuilder($name);
    }

    private function setDefaultTeams(): void {
        $teams = ["Red", "Blue", "Yellow", "Green", "Cyan", "Gray", "Orange", "Magenta"];
        for($i = 0; $i < $this->getTeamsCount(); $i++) {
            $this->addTeam($teams[$i]);
        }
    }

    private function getTeamsCount(): int {
        return (int) ($this->max_capacity / $this->players_per_team);
    }

    /**
     * @return TeamBuilder[]
     */
    public function getTeams(): array {
        return $this->teams;
    }

    public function canBeBuilt(): bool {
        $can_build = true;

        foreach($this->teams ?? [] as $team) {
            if(!$team->canBeBuilt()) {
                $can_build = false;
                break;
            }
        }

        $is_set = isset(
            $this->spectator_spawn_position,
            $this->generators,
            $this->teams,
            $this->shop_positions,
            $this->upgrades_positions
        );

        return $is_set and $can_build;
    }

    public function build(): Map {
        return new Map(
            $this->name,
            $this->spectator_spawn_position,
            $this->players_per_team,
            $this->max_capacity,
            $this->waiting_world,
            $this->generators,
            array_map(function(TeamBuilder $team_builder): Team {
                return $team_builder->build($this);
            }, $this->teams),
            $this->shop_positions,
            $this->upgrades_positions
        );
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\session\setup\builder;


use pocketmine\math\Vector3;
use sergittos\bedwars\game\generator\presets\GoldGenerator;
use sergittos\bedwars\game\generator\presets\IronGenerator;
use sergittos\bedwars\game\team\Area;
use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\game\team\TeamProperties;
use sergittos\bedwars\utils\ColorUtils;
use function strtoupper;

class TeamBuilder {
    use TeamProperties;

    private Vector3 $generator_position;

    public function __construct(string $name) {
        $this->name = $name;
        $this->color = ColorUtils::translate("{" . strtoupper($name) . "}");
    }

    public function setSpawnPoint(Vector3 $spawn_point): void {
        $this->spawn_point = $spawn_point;
    }

    public function setBedPosition(Vector3 $bed_position): void {
        $this->bed_position = $bed_position;
    }

    public function setZone(Area $zone): void {
        $this->zone = $zone;
    }

    public function setClaim(Area $claim): void {
        $this->claim = $claim;
    }

    public function setGeneratorPosition(Vector3 $generator_position): void {
        $this->generator_position = $generator_position;
    }

    public function canBeBuilt(): bool {
        return isset(
            $this->spawn_point,
            $this->bed_position,
            $this->zone,
            $this->claim,
            $this->generator_position
        );
    }

    public function build(MapBuilder $map_builder): Team {
        return new Team(
            $this->name,
            $map_builder->getPlayersPerTeam(),
            $this->spawn_point,
            $this->bed_position,
            $this->zone,
            $this->claim,
            [
                new IronGenerator($this->generator_position),
                new GoldGenerator($this->generator_position)
            ]
        );
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\command;


use pocketmine\command\Command;
use pocketmine\command\CommandSender;
use pocketmine\permission\DefaultPermissions;
use pocketmine\player\Player;
use pocketmine\plugin\Plugin;
use pocketmine\plugin\PluginOwned;
use sergittos\bedwars\BedWars;
use sergittos\bedwars\form\setup\BedwarsForm;
use sergittos\bedwars\form\setup\SetupMapForm;
use sergittos\bedwars\session\SessionFactory;
use sergittos\bedwars\session\setup\step\PreparingMapStep;

class BedWarsCommand extends Command implements PluginOwned {

    public function __construct() {
        $this->setPermission(DefaultPermissions::ROOT_OPERATOR);
        parent::__construct("bedwars", "Setup your BedWars games!");
    }

    public function execute(CommandSender $sender, string $commandLabel, array $args): void {
        if(!$sender instanceof Player) {
            return;
        }

        $session = SessionFactory::getSession($sender);
        if($session->isCreatingMap()) {
            if($session->getMapSetup()->getStep() instanceof PreparingMapStep) {
                $sender->sendForm(new SetupMapForm($session));
            }
        } else {
            $sender->sendForm(new BedwarsForm());
        }
    }

    public function getOwningPlugin(): Plugin {
        return BedWars::getInstance();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\utils;


use pocketmine\entity\effect\EffectInstance;
use pocketmine\entity\effect\VanillaEffects;
use pocketmine\item\ItemTypeIds;
use sergittos\bedwars\game\team\upgrade\trap\AlarmTrap;
use sergittos\bedwars\game\team\upgrade\trap\CounterOffensiveTrap;
use sergittos\bedwars\game\team\upgrade\trap\DefaultTrap;
use sergittos\bedwars\game\team\upgrade\trap\MinerFatigueTrap;
use sergittos\bedwars\game\team\upgrade\trap\Trap;
use function strtolower;

class GameUtils {

    static public function getMode(int $players_per_team): string {
        return match($players_per_team) {
            1 => "Solo",
            2 => "Duos",
            4 => "Squads",
            default => "Unknown"
        };
    }

    static public function getGeneratorColor(string $name): string {
        return match(strtolower($name)) {
            "emerald" => "{DARK_GREEN}",
            "diamond" => "{AQUA}",
            "gold" => "{GOLD}",
            "iron" => "{WHITE}",
            default => ""
        };
    }

    static public function intToRoman(int $number): string {
        return match($number) {
            1 => "I",
            2 => "II",
            3 => "III",
            4 => "IV",
            5 => "MAX",
            default => ""
        };
    }

    static public function getTrapByName(string $name): Trap {
        return match($name) {
            "It's a trap" => new DefaultTrap(),
            "Counter-Offensive Trap" => new CounterOffensiveTrap(),
            "Alarm Trap" => new AlarmTrap(),
            "Miner Fatigue Trap" => new MinerFatigueTrap()
        };
    }

    static public function getEffectDuration(EffectInstance $effect): int {
        return match($effect->getType()) {
            VanillaEffects::SPEED(), VanillaEffects::JUMP_BOOST() => 45,
            VanillaEffects::INVISIBILITY() => 30,
            default => 0
        } * 20;
    }

    static public function getEffectAmplifier(EffectInstance $effect): int {
        return match($effect->getType()) {
            VanillaEffects::SPEED() => 1,
            VanillaEffects::JUMP_BOOST() => 4,
            default => 0
        };
    }

    static public function getCountById(int $id): int {
        return match($id) {
            ItemTypeIds::IRON_INGOT => 48,
            ItemTypeIds::GOLD_INGOT => 16,
            ItemTypeIds::DIAMOND => 4,
            ItemTypeIds::EMERALD => 2,
            default => 64
        };
    }

    static public function getColoredTitleNumber(int $number): string {
        return match(true) {
            $number <= 3 => "{RED}",
            $number <= 5 => "{YELLOW}",
            default => "{GREEN}"
        } . $number;
    }

    static public function getColoredMessageNumber(int $number): string {
        return match(true) {
            $number <= 5 => "{RED}",
            $number <= 10 => "{GOLD}",
            $number <= 20 => "{AQUA}",
            default => "{GREEN}"
        } . $number;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\utils;


use sergittos\bedwars\BedWars;

class ConfigGetter {

    static private function get(string $key, mixed $default = false): mixed {
        return BedWars::getInstance()->getConfig()->get($key, $default);
    }

    static public function getVersion(): int|float {
        return self::get("version", 1.0);
    }

    static public function getIP(): string {
        return self::get("ip", "play.server.net");
    }

    static public function isSpawnProtectionEnabled(): bool {
        return self::get("spawn-protection", true);
    }

    static public function getProvider(): string {
        return self::get("provider", "sqlite");
    }

    static public function getMysqlCredentials(): array {
        return self::get("mysql-credentials", []);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\utils;


use pocketmine\math\Vector2;
use pocketmine\world\Position;
use function atan2;

class MathUtils {

    static public function calculateYaw(Position $player_position, Position $entity_position): float {
        return self::toDegrees(atan2(
            $player_position->getZ() - $entity_position->getZ(),
            $player_position->getX() - $entity_position->getX()
        ));
    }

    static public function calculatePitch(Position $player_position, Position $entity_position): float {
        $player_vector = new Vector2($player_position->getX(), $player_position->getZ());
        $entity_vector = new Vector2($entity_position->getX(), $entity_position->getZ());

        return self::toDegrees(atan2(
            $player_vector->distance($entity_vector),
            $player_position->getY() - $entity_position->getY()
        ));
    }

    static private function toDegrees(float $radians): float {
        return $radians * 180 / M_PI - 90;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\utils;


use pocketmine\block\utils\DyeColor;
use pocketmine\utils\TextFormat;
use function str_replace;

class ColorUtils {

    static public function translate(string $message): string {
        $message = str_replace("{BLACK}", TextFormat::BLACK, $message);
        $message = str_replace("{DARK_BLUE}", TextFormat::DARK_BLUE, $message);
        $message = str_replace("{BLUE}", TextFormat::DARK_BLUE, $message);
        $message = str_replace("{DARK_GREEN}", TextFormat::DARK_GREEN, $message);
        $message = str_replace("{DARK_AQUA}", TextFormat::DARK_AQUA, $message);
        $message = str_replace("{DARK_RED}", TextFormat::DARK_RED, $message);
        $message = str_replace("{DARK_PURPLE}", TextFormat::DARK_PURPLE, $message);
        $message = str_replace("{MAGENTA}", TextFormat::DARK_PURPLE, $message);
        $message = str_replace("{GOLD}", TextFormat::GOLD, $message);
        $message = str_replace("{ORANGE}", TextFormat::GOLD, $message);
        $message = str_replace("{GRAY}", TextFormat::GRAY, $message);
        $message = str_replace("{DARK_GRAY}", TextFormat::DARK_GRAY, $message);
        $message = str_replace("{CYAN}", TextFormat::BLUE, $message);
        $message = str_replace("{GREEN}", TextFormat::GREEN, $message);
        $message = str_replace("{AQUA}", TextFormat::AQUA, $message);
        $message = str_replace("{RED}", TextFormat::RED, $message);
        $message = str_replace("{LIGHT_PURPLE}", TextFormat::LIGHT_PURPLE, $message);
        $message = str_replace("{PINK}", TextFormat::LIGHT_PURPLE, $message);
        $message = str_replace("{YELLOW}", TextFormat::YELLOW, $message);
        $message = str_replace("{WHITE}", TextFormat::WHITE, $message);
        $message = str_replace("{OBFUSCATED}", TextFormat::OBFUSCATED, $message);
        $message = str_replace("{BOLD}", TextFormat::BOLD, $message);
        $message = str_replace("{STRIKETHROUGH}", TextFormat::STRIKETHROUGH, $message);
        $message = str_replace("{UNDERLINE}", TextFormat::UNDERLINE, $message);
        $message = str_replace("{ITALIC}", TextFormat::ITALIC, $message);
        $message = str_replace("{RESET}", TextFormat::RESET, $message);

        return $message;
    }

    static public function getDye(string $color): DyeColor {
        return match($color) {
            TextFormat::BLACK => DyeColor::BLACK,
            TextFormat::DARK_BLUE => DyeColor::BLUE,
            TextFormat::DARK_GREEN => DyeColor::GREEN,
            TextFormat::DARK_PURPLE => DyeColor::MAGENTA,
            TextFormat::GOLD => DyeColor::ORANGE,
            TextFormat::GRAY => DyeColor::LIGHT_GRAY,
            TextFormat::DARK_GRAY => DyeColor::GRAY,
            TextFormat::BLUE => DyeColor::CYAN,
            TextFormat::GREEN => DyeColor::LIME,
            TextFormat::RED => DyeColor::RED,
            TextFormat::LIGHT_PURPLE => DyeColor::PINK,
            TextFormat::YELLOW => DyeColor::YELLOW,
            TextFormat::WHITE => DyeColor::WHITE,
        };
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\provider\sqlite;


use sergittos\bedwars\BedWars;
use sergittos\bedwars\provider\Provider;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\settings\SpectatorSettings;
use SQLite3;

class SqliteProvider extends Provider {

    private SQLite3 $sqlite;

    public function __construct() {
        $this->sqlite = new SQLite3(BedWars::getInstance()->getDataFolder() . "database.db");
        $this->sqlite->exec(
            "CREATE TABLE IF NOT EXISTS bedwars_users (
                xuid VARCHAR(16) PRIMARY KEY,
                coins INT,
                kills INT,
                wins INT,
                
                flying_speed INT,
                auto_teleport BOOL,
                night_vision BOOL
            );"
        );
    }

    public function loadSession(Session $session): void {
        $xuid = $session->getPlayer()->getXuid();
        $this->insertIfNotExists($xuid);

        $data = $this->fetchUserDetails($xuid);

        $session->setCoins($data["coins"]);
        $session->setKills($data["kills"]);
        $session->setWins($data["wins"]);
        $session->setSpectatorSettings(SpectatorSettings::fromData($session, $data));
    }

    public function updateCoins(Session $session): void {
        $this->updateProperty($session, "coins");
    }

    public function updateKills(Session $session): void {
        $this->updateProperty($session, "kills");
    }

    public function updateWins(Session $session): void {
        $this->updateProperty($session, "wins");
    }

    private function insertIfNotExists(string $xuid): void {
        $stmt = $this->sqlite->prepare("INSERT OR IGNORE INTO bedwars_users (xuid, coins, kills, wins, flying_speed, auto_teleport, night_vision) VALUES (:xuid, 0, 0, 0, 0, true, true)");
        $stmt->bindParam(":xuid", $xuid);
        $stmt->execute();
    }

    private function fetchUserDetails(string $xuid): array {
        $stmt = $this->sqlite->prepare("SELECT * FROM bedwars_users WHERE xuid = :xuid");
        $stmt->bindParam(":xuid", $xuid);
        $result = $stmt->execute();

        return $result->fetchArray(SQLITE3_ASSOC);
    }

    private function updateProperty(Session $session, string $property): void {
        $stmt = $this->sqlite->prepare("UPDATE bedwars_users SET $property = :value WHERE xuid = :xuid");
        $stmt->bindValue(":value", $session->{'get' . ucfirst($property)}());
        $stmt->bindValue(":xuid", $session->getPlayer()->getXuid());
        $stmt->execute();
    }

    public function saveSession(Session $session): void {}

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\provider\mysql;


use mysqli;
use pocketmine\scheduler\AsyncTask;
use sergittos\bedwars\BedWars;

abstract class MysqlAsyncTask extends AsyncTask {

    private MysqlCredentials $credentials;

    public function __construct(?MysqlCredentials $credentials = null) {
        $this->credentials = $credentials ?? BedWars::getInstance()->getProvider()->getCredentials();
    }

    public function onRun(): void {
        $mysqli = $this->credentials->getMysqli();
        $this->onConnection($mysqli);
        $mysqli->close();
    }

    abstract protected function onConnection(mysqli $mysqli): void;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\provider\mysql;


use mysqli;
use pmmp\thread\ThreadSafe;

class MysqlCredentials extends ThreadSafe {

    private string $hostname, $username, $password, $database;
    private int $port;

    public function __construct(string $hostname, string $username, string $password, string $database, int $port) {
        $this->hostname = $hostname;
        $this->username = $username;
        $this->password = $password;
        $this->database = $database;
        $this->port = $port;
    }

    static public function fromData(array $data): MysqlCredentials {
        return new MysqlCredentials($data["hostname"], $data["username"], $data["password"], $data["database"], $data["port"]);
    }

    public function getMysqli(): mysqli {
        return new mysqli($this->hostname, $this->username, $this->password, $this->database, $this->port);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\provider\mysql\task;


use mysqli;
use sergittos\bedwars\provider\mysql\MysqlAsyncTask;
use sergittos\bedwars\session\Session;

class UpdateKillsTask extends MysqlAsyncTask {

    private string $xuid;
    private int $kills;

    public function __construct(Session $session) {
        $this->xuid = $session->getPlayer()->getXuid();
        $this->kills = $session->getKills();
        parent::__construct();
    }

    protected function onConnection(mysqli $mysqli): void {
        $stmt = $mysqli->prepare("UPDATE bedwars_users SET kills = ? WHERE xuid = ?");
        $stmt->bind_param("is", ...[$this->kills, $this->xuid]);
        $stmt->execute();
        $stmt->close();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\provider\mysql\task;


use mysqli;
use sergittos\bedwars\provider\mysql\MysqlAsyncTask;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\SessionFactory;
use sergittos\bedwars\session\settings\SpectatorSettings;

class LoadSessionTask extends MysqlAsyncTask {

    private string $xuid;
    private string $username;

    public function __construct(Session $session) {
        $this->xuid = $session->getPlayer()->getXuid();
        $this->username = $session->getUsername();
        parent::__construct();
    }

    protected function onConnection(mysqli $mysqli): void {
        $this->insertIfNotExists($mysqli);
        $this->fetchUserDetails($mysqli);
    }

    private function insertIfNotExists(mysqli $mysqli): void {
        $stmt = $mysqli->prepare("INSERT IGNORE INTO bedwars_users (xuid, coins, kills, wins, flying_speed, auto_teleport, night_vision) VALUES (?, 0, 0, 0, 0, true, true)");
        $stmt->bind_param("s", ...[$this->xuid]);
        $stmt->execute();
        $stmt->close();
    }

    private function fetchUserDetails(mysqli $mysqli): void {
        $stmt = $mysqli->prepare("SELECT * FROM bedwars_users WHERE xuid = ?");
        $stmt->bind_param("s", ...[$this->xuid]);
        $stmt->execute();
        $result = $stmt->get_result();

        $rows = $result->fetch_all(MYSQLI_ASSOC);
        $this->setResult($rows);

        $stmt->free_result();
        $stmt->close();
    }

    public function onCompletion(): void {
        $session = SessionFactory::getSessionByName($this->username);
        if($session === null) {
            return;
        }

        $rows = $this->getResult();
        $data = $rows[0];

        $session->setKills((int) $data["kills"]);
        $session->setWins((int) $data["wins"]);
        $session->setCoins((int) $data["coins"]);
        $session->setSpectatorSettings(SpectatorSettings::fromData($session, $data));
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\provider\mysql\task;


use mysqli;
use sergittos\bedwars\provider\mysql\MysqlAsyncTask;
use sergittos\bedwars\session\Session;

class UpdateWinsTask extends MysqlAsyncTask {

    private string $xuid;
    private int $wins;

    public function __construct(Session $session) {
        $this->xuid = $session->getPlayer()->getXuid();
        $this->wins = $session->getWins();
        parent::__construct();
    }

    protected function onConnection(mysqli $mysqli): void {
        $stmt = $mysqli->prepare("UPDATE bedwars_users SET wins = ? WHERE xuid = ?");
        $stmt->bind_param("is", ...[$this->wins, $this->xuid]);
        $stmt->execute();
        $stmt->close();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\provider\mysql\task;


use mysqli;
use sergittos\bedwars\provider\mysql\MysqlAsyncTask;

class CreateTablesTask extends MysqlAsyncTask {

    protected function onConnection(mysqli $mysqli): void {
        $mysqli->query(
            "CREATE TABLE IF NOT EXISTS bedwars_users (
                xuid VARCHAR(16) PRIMARY KEY,
                coins INT,
                kills INT,
                wins INT,
                
                flying_speed INT,
                auto_teleport BOOL,
                night_vision BOOL
            )"
        );
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\provider\mysql\task;


use mysqli;
use sergittos\bedwars\provider\mysql\MysqlAsyncTask;
use sergittos\bedwars\session\Session;

class UpdateCoinsTask extends MysqlAsyncTask {

    private string $xuid;
    private int $coins;

    public function __construct(Session $session) {
        $this->xuid = $session->getPlayer()->getXuid();
        $this->coins = $session->getCoins();
        parent::__construct();
    }

    protected function onConnection(mysqli $mysqli): void {
        $stmt = $mysqli->prepare("UPDATE bedwars_users SET coins = ? WHERE xuid = ?");
        $stmt->bind_param("is", ...[$this->coins, $this->xuid]);
        $stmt->execute();
        $stmt->close();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\provider\mysql;


use pocketmine\Server;
use sergittos\bedwars\provider\mysql\task\CreateTablesTask;
use sergittos\bedwars\provider\mysql\task\LoadSessionTask;
use sergittos\bedwars\provider\mysql\task\UpdateCoinsTask;
use sergittos\bedwars\provider\mysql\task\UpdateKillsTask;
use sergittos\bedwars\provider\mysql\task\UpdateWinsTask;
use sergittos\bedwars\provider\Provider;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\ConfigGetter;

class MysqlProvider extends Provider {

    private MysqlCredentials $credentials;

    public function __construct() {
        $this->credentials = MysqlCredentials::fromData(ConfigGetter::getMysqlCredentials());

        $this->scheduleAsyncTask(new CreateTablesTask($this->credentials));
    }

    public function getCredentials(): MysqlCredentials {
        return $this->credentials;
    }

    public function loadSession(Session $session): void {
        $this->scheduleAsyncTask(new LoadSessionTask($session));
    }

    public function updateCoins(Session $session): void {
        $this->scheduleAsyncTask(new UpdateCoinsTask($session));
    }

    public function updateKills(Session $session): void {
        $this->scheduleAsyncTask(new UpdateKillsTask($session));
    }

    public function updateWins(Session $session): void {
        $this->scheduleAsyncTask(new UpdateWinsTask($session));
    }

    public function saveSession(Session $session): void {}

    private function scheduleAsyncTask(MysqlAsyncTask $task): void {
        Server::getInstance()->getAsyncPool()->submitTask($task);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\provider;


use sergittos\bedwars\session\Session;

abstract class Provider {

    abstract public function loadSession(Session $session): void;

    abstract public function updateCoins(Session $session): void;

    abstract public function updateKills(Session $session): void;

    abstract public function updateWins(Session $session): void;

    abstract public function saveSession(Session $session): void;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\provider\json;


use pocketmine\utils\Config;
use sergittos\bedwars\BedWars;
use sergittos\bedwars\provider\Provider;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\settings\SpectatorSettings;
use function is_dir;
use function mkdir;

class JsonProvider extends Provider {

    public function __construct() {
        $users_dir = $this->getUsersDir();
        if(!is_dir($users_dir)) {
            mkdir($users_dir);
        }
    }

    public function loadSession(Session $session): void {
        $config = $this->getSessionConfig($session);
        $session->setCoins($config->get("coins"));
        $session->setKills($config->get("kills"));
        $session->setWins($config->get("wins"));
        $session->setSpectatorSettings(SpectatorSettings::fromData($session, $config->get("spectator_settings")));
    }

    public function saveSession(Session $session): void {
        $config = $this->getSessionConfig($session);
        $config->set("coins", $session->getCoins());
        $config->set("kills", $session->getKills());
        $config->set("wins", $session->getWins());
        $config->set("spectator_settings", [
            "flying_speed" => $session->getSpectatorSettings()->getFlyingSpeed(),
            "auto_teleport" => $session->getSpectatorSettings()->getAutoTeleport(),
            "night_vision" => $session->getSpectatorSettings()->getNightVision()
        ]);
        $config->save();
    }

    public function updateCoins(Session $session): void {}

    public function updateKills(Session $session): void {}

    public function updateWins(Session $session): void {}

    private function getSessionConfig(Session $session): Config {
        return new Config($this->getUsersDir() . $session->getPlayer()->getXuid() . ".json", Config::JSON, [
            "coins" => 0,
            "kills" => 0,
            "wins" => 0,
            "spectator_settings" => [
                "flying_speed" => 0,
                "auto_teleport" => true,
                "night_vision" => true
            ]
        ]);
    }

    private function getUsersDir(): string {
        return BedWars::getInstance()->getDataFolder() . "users/";
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\shop;


use dresnite\EasyUI\element\Button;
use pocketmine\player\Player;
use pocketmine\utils\TextFormat;
use sergittos\bedwars\form\SimpleForm;
use sergittos\bedwars\game\shop\Category;
use sergittos\bedwars\game\shop\Product;
use sergittos\bedwars\game\shop\upgrades\category\TrapsCategory;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\SessionFactory;
use function array_shift;

class CategoryForm extends SimpleForm {

    private Session $session;
    private Category $category;

    public function __construct(Session $session, Category $category) {
        $this->session = $session;
        $this->category = $category;
        parent::__construct($category->getName(), $this->getTrapsHeaderText());
    }

    protected function onCreation(): void {
        if(!$this->session->isPlaying() or !$this->session->hasTeam()) {
            // This should never happen, but just in case
            return;
        }

        foreach($this->category->getProducts($this->session) as $product) {
            $button = new Button($product->getDisplayName($this->session) . "\n" . $product->getDescription($this->session));
            $button->setSubmitListener(function(Player $player) use ($product) {
                $session = SessionFactory::getSession($player);
                if($session !== null and $session->isPlaying()) {
                    $this->purchaseProduct($product);
                }
            });
            $this->addButton($button);
        }
    }

    private function purchaseProduct(Product $product): void {
        if($this->canPurchaseProduct($product) and $product->onPurchase($this->session)) {
            $player = $this->session->getPlayer();
            $player->getInventory()->removeItem($product->getOre());
            $player->sendForm(new CategoryForm($this->session, $this->category));

            $this->session->message("{GREEN}You purchased {GOLD}" . $product->getName());
        }
    }

    private function canPurchaseProduct(Product $product): bool {
        if(!$product->canBePurchased($this->session)) {
            $this->session->message("{RED}You already have this product!");
            return false;
        }
        $ore = $product->getOre();

        $inventory = $this->session->getPlayer()->getInventory();
        if(!$inventory->contains($ore)) {
            $count = 0;
            foreach($inventory->all($ore) as $item) {
                $count += $item->getCount();
            }
            $count = $ore->getCount() - $count;

            $this->session->message(TextFormat::RED . "You don't have enough " . $ore->getName() . "! Need $count more!");
            return false;
        }
        return true;
    }

    private function getTrapsHeaderText(): string {
        if(!$this->category instanceof TrapsCategory or !$this->session->hasTeam()) {
            return "";
        }
        $traps = $this->session->getTeam()->getUpgrades()->getTraps();

        $header_text = "";
        for($i = 1; $i <= 3; $i++) {
            $trap = array_shift($traps);
            $header_text .= "Trap #" . $i . " - " . ($trap !== null ? $trap->getName() : "Empty") . "\n";
        }
        return $header_text;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\shop;


use sergittos\bedwars\form\SimpleForm;
use sergittos\bedwars\game\shop\Shop;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\ColorUtils;

class ShopForm extends SimpleForm {

    private Session $session;
    private Shop $shop;

    public function __construct(Session $session, string $name, Shop $shop) {
        $this->session = $session;
        $this->shop = $shop;
        parent::__construct($name);
    }

    protected function onCreation(): void {
        foreach($this->shop->getCategories() as $category) {
            $this->addRedirectFormButton(
                ColorUtils::translate("{GOLD}{BOLD}" . $category->getName() . "{RESET}\n{YELLOW}Click to view!"),
                new CategoryForm($this->session, $category)
            );
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\queue;


use dresnite\EasyUI\variant\SimpleForm;
use sergittos\bedwars\BedWars;
use sergittos\bedwars\form\queue\element\PlayGameButton;
use sergittos\bedwars\game\map\MapFactory;

class SelectMapForm extends SimpleForm {

    private int $players_per_team;

    public function __construct(int $players_per_team) {
        $this->players_per_team = $players_per_team;
        parent::__construct("Select a map!");
    }

    protected function onCreation(): void {
        foreach(MapFactory::getMapsByPlayers($this->players_per_team) as $map) {
            $this->addButton(new PlayGameButton($map->getName(), BedWars::getInstance()->getGameManager()->findGame($map)));
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\queue\element;


use dresnite\EasyUI\element\Button;
use pocketmine\player\Player;
use pocketmine\utils\TextFormat;
use sergittos\bedwars\game\Game;
use sergittos\bedwars\session\SessionFactory;

class PlayGameButton extends Button {

    public function __construct(string $text, ?Game $game) {
        parent::__construct($text, null, function(Player $player) use ($game) {
            if($game === null) {
                $player->sendMessage(TextFormat::RED . "There are no games for this map! Try again in a few seconds");
                return;
            }

            $session = SessionFactory::getSession($player);
            if($session->isSpectator()) {
                $session->getGame()->removeSpectator($session);
            } elseif($session->isPlaying()) {
                return;
            }
            $game->addPlayer($session);
        });
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\queue;


use dresnite\EasyUI\element\Button;
use dresnite\EasyUI\variant\SimpleForm;
use pocketmine\player\Player;
use sergittos\bedwars\BedWars;
use sergittos\bedwars\form\queue\element\PlayGameButton;
use sergittos\bedwars\utils\GameUtils;

class PlayBedwarsForm extends SimpleForm {

    private int $players_per_team;

    public function __construct(int $players_per_team) {
        $this->players_per_team = $players_per_team;
        parent::__construct("Play bedwars " . GameUtils::getMode($players_per_team));
    }

    protected function onCreation(): void {
        $this->addButton(new PlayGameButton("Random map", BedWars::getInstance()->getGameManager()->findRandomGame($this->players_per_team)));
        $this->addSelectMapButton();
    }

    private function addSelectMapButton(): void {
        $button = new Button("Select a map");
        $button->setSubmitListener(function(Player $player) {
            $player->sendForm(new SelectMapForm($this->players_per_team));
        });
        $this->addButton($button);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form;


use dresnite\EasyUI\element\Dropdown;
use dresnite\EasyUI\element\Option;
use dresnite\EasyUI\variant\CustomForm as EasyUICustomForm;
use sergittos\bedwars\utils\GameUtils;

class CustomForm extends EasyUICustomForm {

    protected function addSelectModeDropdown(): void {
        $dropdown = new Dropdown("Select the mode:");
        foreach([1, 2, 4] as $players_per_team) {
            $dropdown->addOption(new Option((string) $players_per_team, GameUtils::getMode($players_per_team)));
        }
        $this->addElement("players_per_team", $dropdown);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form;


use dresnite\EasyUI\element\Button;
use dresnite\EasyUI\Form;
use dresnite\EasyUI\variant\SimpleForm as EasyUISimpleForm;
use pocketmine\math\Vector3;
use pocketmine\player\Player;

class SimpleForm extends EasyUISimpleForm {

    public function addRedirectFormButton(string $name, Form $form): void {
        $button = new Button($name);
        $button->setSubmitListener(function(Player $player) use ($form) {
            $player->sendForm($form);
        });
        $this->addButton($button);
    }

    protected function vectorToString(Vector3 $vector): string { // this function shouldn't be there
        return "X: " . $vector->getFloorX() . ", Y: " . $vector->getFloorY() . ", Z: " . $vector->getFloorZ();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\spectator;


use dresnite\EasyUI\element\Button;
use dresnite\EasyUI\variant\SimpleForm;
use pocketmine\player\Player;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\SessionFactory;

class TeleporterForm extends SimpleForm {

    private Session $session;

    public function __construct(Session $session) {
        $this->session = $session;
        parent::__construct("Teleporter");
    }

    protected function onCreation(): void {
        foreach($this->session->getGame()->getPlayers() as $target) {
            if(!$target->isRespawning()) {
                $this->addTeleportButton($target);
            }
        }
    }

    private function addTeleportButton(Session $target): void {
        $button = new Button($target->getUsername());
        $button->setSubmitListener(function(Player $player) use ($target) {
            $session = SessionFactory::getSession($player);
            if(!$session->isSpectator()) {
                $session->message("{RED}You can't do this!");
                return;
            }
            if(!$target->isPlaying()) {
                $session->message("{RED}The player you want to teleport is no longer playing!");
                return;
            }
            if($target->isRespawning()) {
                $session->message("{RED}The player you want to teleport is dead!");
                return;
            }

            $session->setTrackingSession(SessionFactory::getSession($player = $target->getPlayer()));
            $session->getPlayer()->teleport($player->getPosition());
            $session->message("{GREEN}You teleported to " . $target->getUsername() . " successfully!");
        });
        $this->addButton($button);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\spectator;


use dresnite\EasyUI\element\Label;
use dresnite\EasyUI\element\Slider;
use dresnite\EasyUI\element\Toggle;
use dresnite\EasyUI\utils\FormResponse;
use dresnite\EasyUI\variant\CustomForm;
use pocketmine\player\Player;
use sergittos\bedwars\session\Session;

class SpectatorSettingsForm extends CustomForm {

    private Session $session;

    public function __construct(Session $session) {
        $this->session = $session;
        parent::__construct("Spectator settings");
    }

    protected function onCreation(): void {
        $settings = $this->session->getSpectatorSettings();

        $this->addElement("label", new Label("Change the spectator settings in this window."));
        $this->addElement("flying_speed", new Slider("Set your flying speed", 0, 4, $settings->getFlyingSpeed(), 1));
        $this->addElement("auto_teleport", new Toggle("Auto Teleport", $settings->getAutoTeleport()));
        $this->addElement("night_vision", new Toggle("Night Vision", $settings->getNightVision()));
    }

    protected function onSubmit(Player $player, FormResponse $response): void {
        $flying_speed = (int) $response->getSliderSubmittedStep("flying_speed");
        $auto_teleport = $response->getToggleSubmittedChoice("auto_teleport");
        $night_vision = $response->getToggleSubmittedChoice("night_vision");

        $settings = $this->session->getSpectatorSettings();
        if($settings->getFlyingSpeed() !== $flying_speed) {
            $settings->setFlyingSpeed($flying_speed);
        }
        if($settings->getAutoTeleport() !== $auto_teleport) {
            $settings->setAutoTeleport($auto_teleport);
        }
        if($settings->getNightVision() !== $night_vision) {
            $settings->setNightVision($night_vision);
        }
        if($this->session->isSpectator()) {
            $settings->apply();
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\setup;


use sergittos\bedwars\form\SimpleForm;
use sergittos\bedwars\session\Session;

class SetupTeamsForm extends SimpleForm {

    private Session $session;

    public function __construct(Session $session) {
        $this->session = $session;
        parent::__construct("Setup teams", "Select the team you want to setup");
    }

    protected function onCreation(): void {
        foreach($this->session->getMapSetup()->getMapBuilder()->getTeams() as $team) {
            $description = !$team->canBeBuilt() ? "\n[PENDING]" : "";
            $this->addRedirectFormButton($team->getName() . $description, new TeamOptionsForm($this->session, $team));
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\setup;


use dresnite\EasyUI\element\ModalOption;
use dresnite\EasyUI\variant\ModalForm;
use pocketmine\player\Player;
use sergittos\bedwars\game\generator\Generator;
use sergittos\bedwars\session\SessionFactory;

class RemoveGeneratorForm extends ModalForm {

    private Generator $generator;

    public function __construct(Generator $generator) {
        $this->generator = $generator;
        parent::__construct(
            "Remove " . $generator->getName() . " Generator",
            "Are you sure you want to delete this generator?",
            new ModalOption("Yes!"), new ModalOption("No!")
        );
    }

    protected function onAccept(Player $player): void {
        $session = SessionFactory::getSession($player);
        if($session->isCreatingMap()) {
            $session->getMapSetup()->getMapBuilder()->removeGenerator($this->generator);
            $session->playSound("random.orb");
            $session->message("{GREEN}You have removed the generator successfully!");
        }
    }

    protected function onDeny(Player $player): void {
        $session = SessionFactory::getSession($player);
        if($session->isCreatingMap()) {
            $player->sendForm(new RemoveGeneratorsForm($session));
        }

    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\setup;


use sergittos\bedwars\form\SimpleForm;
use sergittos\bedwars\session\Session;

class SetupGeneratorsForm extends SimpleForm {

    private Session $session;

    public function __construct(Session $session) {
        $this->session = $session;
        parent::__construct("Setup generators");
    }

    protected function onCreation(): void {
        $this->addRedirectFormButton("Add generator", new AddGeneratorForm($this->session));
        $this->addRedirectFormButton("Remove generator", new RemoveGeneratorsForm($this->session));
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\setup;


use sergittos\bedwars\form\SimpleForm;
use sergittos\bedwars\game\map\MapFactory;

class ManageMapsForm extends SimpleForm {

    public function __construct() {
        parent::__construct("Manage maps");
    }

    protected function onCreation(): void {
        foreach(MapFactory::getMaps() as $map) {
            $this->addRedirectFormButton($map->getName(), new MapOptionsForm($map));
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\setup;


use sergittos\bedwars\form\SimpleForm;

class BedwarsForm extends SimpleForm {

    public function __construct() {
        parent::__construct("BedWars");
    }

    protected function onCreation(): void {
        $this->addRedirectFormButton("Create a map", new CreateMapForm());
        $this->addRedirectFormButton("Manage maps", new ManageMapsForm());
        $this->addRedirectFormButton("Spawn join game entity", new SpawnEntityForm());
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\setup;


use sergittos\bedwars\form\SimpleForm;
use sergittos\bedwars\game\generator\Generator;
use sergittos\bedwars\session\Session;

class RemoveGeneratorsForm extends SimpleForm {

    private Session $session;

    public function __construct(Session $session) {
        $this->session = $session;
        parent::__construct("Remove generators");
    }

    protected function onCreation(): void {
        if(!$this->session->isCreatingMap()) {
            return;
        }

        foreach($this->session->getMapSetup()->getMapBuilder()->getGenerators() as $generator) {
            $this->addRedirectFormButton(
                $generator->getName() . " Generator" . ($this->isNearestGenerator($generator) ? "\nNearest Generator" : ""),
                new RemoveGeneratorForm($generator)
            );
        }
    }

    private function isNearestGenerator(Generator $target): bool {
        $found = null;
        $lowest = PHP_INT_MAX;
        foreach($this->session->getMapSetup()->getMapBuilder()->getGenerators() as $generator) {
            $distance = $generator->getPosition()->distance($this->session->getPlayer()->getPosition());
            if($distance < $lowest) {
                $lowest = $distance;
                $found = $generator;
            }
        }

        return $found === $target;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\setup;


use dresnite\EasyUI\element\Button;
use pocketmine\player\Player;
use sergittos\bedwars\form\SimpleForm;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\setup\step\SetShopAndUpgradesStep;

class SetupMapForm extends SimpleForm {

    private Session $session;

    public function __construct(Session $session) {
        $this->session = $session;
        parent::__construct("Setup map", "What do you want to do?");
    }

    protected function onCreation(): void {
        $this->addSetSpectatorSpawnButton();
        $this->addShopAndUpgradesButton();
        $this->addRedirectFormButton("Setup generators", new SetupGeneratorsForm($this->session));
        $this->addRedirectFormButton("Setup teams", new SetupTeamsForm($this->session));
    }

    private function addSetSpectatorSpawnButton(): void {
        $button = new Button("Set spectator spawn point");
        $button->setSubmitListener(function(Player $player) {
            if($this->session->isCreatingMap()) {
                $position = $player->getPosition()->asVector3();
                $this->session->getMapSetup()->getMapBuilder()->setSpectatorSpawnPosition($position);
                $this->session->message("{GREEN}Spectator spawn point set on: " . $this->vectorToString($position));
            }
        });
        $this->addButton($button);
    }

    private function addShopAndUpgradesButton(): void {
        $button = new Button("Set Shop & Upgrades");
        $button->setSubmitListener(function(Player $player) {
            $this->session->getMapSetup()->setStep(new SetShopAndUpgradesStep());
        });
        $this->addButton($button);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\setup;


use dresnite\EasyUI\element\Dropdown;
use dresnite\EasyUI\element\Option;
use dresnite\EasyUI\utils\FormResponse;
use pocketmine\player\Player;
use sergittos\bedwars\form\CustomForm;
use sergittos\bedwars\game\generator\GeneratorType;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\setup\step\AddGeneratorStep;

class AddGeneratorForm extends CustomForm {

    private Session $session;

    public function __construct(Session $session) {
        $this->session = $session;
        parent::__construct("Add generator");
    }

    protected function onCreation(): void {
        $dropdown = new Dropdown("Select the generator:");
        $dropdown->addOption(new Option(GeneratorType::DIAMOND->name, "Diamond Generator"));
        $dropdown->addOption(new Option(GeneratorType::EMERALD->name, "Emerald Generator"));

        $this->addElement("id", $dropdown);
    }

    protected function onSubmit(Player $player, FormResponse $response): void {
        $this->session->getMapSetup()->setStep(new AddGeneratorStep(
            GeneratorType::fromString($response->getDropdownSubmittedOptionId("id"))
        ));
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\setup;


use dresnite\EasyUI\element\ModalOption;
use dresnite\EasyUI\variant\ModalForm;
use pocketmine\player\Player;
use pocketmine\Server;
use pocketmine\utils\TextFormat;
use sergittos\bedwars\game\map\Map;
use sergittos\bedwars\game\map\task\DeleteMapTask;

class RemoveMapForm extends ModalForm {

    private Map $map;

    public function __construct(Map $map) {
        $this->map = $map;
        parent::__construct(
            "Remove " . $map->getName() . " map", "Are you sure you want to delete this map?",
            new ModalOption("Yes"), new ModalOption("No")
        );
    }

    protected function onAccept(Player $player): void {
        Server::getInstance()->getAsyncPool()->submitTask(new DeleteMapTask($this->map));

        $player->sendMessage(TextFormat::GREEN . "You've deleted the map " . $this->map->getName() . " successfully!");
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\setup;


use dresnite\EasyUI\element\Button;
use pocketmine\player\Player;
use sergittos\bedwars\form\SimpleForm;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\setup\builder\TeamBuilder;
use sergittos\bedwars\session\setup\step\area\SetClaimStep;
use sergittos\bedwars\session\setup\step\area\SetZoneStep;
use sergittos\bedwars\session\setup\step\SetBedPositionStep;
use sergittos\bedwars\session\setup\step\SetTeamGeneratorStep;
use sergittos\bedwars\session\setup\step\Step;

class TeamOptionsForm extends SimpleForm {

    private Session $session;
    private TeamBuilder $team;

    public function __construct(Session $session, TeamBuilder $team) {
        $this->session = $session;
        $this->team = $team;
        parent::__construct("What do you want to do?");
    }

    protected function onCreation(): void {
        $this->addSetSpawnPointButton();
        $this->addStepButton("Set generator", new SetTeamGeneratorStep($this->team));
        $this->addStepButton("Set bed", new SetBedPositionStep($this->team));
        $this->addStepButton("Set zone", new SetZoneStep($this->team));
        $this->addStepButton("Set claim", new SetClaimStep($this->team));
    }

    private function addSetSpawnPointButton(): void {
        $button = new Button("Set spawn point\nThis will set the spawn point in your location");
        $button->setSubmitListener(function(Player $player) {
            $position = $player->getPosition();

            $this->team->setSpawnPoint($position);

            $this->session->message("{GREEN}Spawn point set on: " . $this->vectorToString($player->getPosition()));
        });
        $this->addButton($button);
    }

    private function addStepButton(string $name, Step $step): void {
        $button = new Button($name);
        $button->setSubmitListener(function(Player $player) use ($step) {
            $this->session->getMapSetup()->setStep($step);
        });
        $this->addButton($button);

    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\setup;


use sergittos\bedwars\form\SimpleForm;
use sergittos\bedwars\game\map\Map;

class MapOptionsForm extends SimpleForm {

    private Map $map;

    public function __construct(Map $map) {
        $this->map = $map;
        parent::__construct("Manage " . $map->getName() . " map", "What do you want to do?");
    }

    protected function onCreation(): void {
        $this->addRedirectFormButton("Delete", new RemoveMapForm($this->map));
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\setup;


use dresnite\EasyUI\utils\FormResponse;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\player\Player;
use pocketmine\utils\TextFormat;
use sergittos\bedwars\entity\PlayBedwarsEntity;
use sergittos\bedwars\form\CustomForm;

class SpawnEntityForm extends CustomForm {

    public function __construct() {
        parent::__construct("Spawn entity");
    }

    protected function onCreation(): void {
        $this->addSelectModeDropdown();
    }

    protected function onSubmit(Player $player, FormResponse $response): void {
        $players_per_team = (int) $response->getDropdownSubmittedOptionId("players_per_team");

        $entity = new PlayBedwarsEntity($player->getLocation(), $player->getSkin(), CompoundTag::create()->setInt("players_per_team", $players_per_team));
        $entity->spawnToAll();

        $player->sendMessage(TextFormat::GREEN . "You have spawned the entity successfully!");
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\form\setup;


use dresnite\EasyUI\element\Dropdown;
use dresnite\EasyUI\element\Input;
use dresnite\EasyUI\element\Option;
use dresnite\EasyUI\element\Slider;
use dresnite\EasyUI\utils\FormResponse;
use pocketmine\player\GameMode;
use pocketmine\player\Player;
use pocketmine\Server;
use pocketmine\utils\TextFormat;
use sergittos\bedwars\form\CustomForm;
use sergittos\bedwars\game\map\MapFactory;
use sergittos\bedwars\session\SessionFactory;
use sergittos\bedwars\session\setup\builder\MapBuilder;
use sergittos\bedwars\session\setup\MapSetup;
use Symfony\Component\Filesystem\Path;

class CreateMapForm extends CustomForm {

    public function __construct() {
        parent::__construct("Create a map");
    }

    protected function onCreation(): void {
        $this->addElement("name", new Input("Set a name:"));
        $this->addElement("teams", new Slider("Set the teams", 2, 8, 8, 1));
        $this->addWorldsDropdown("waiting_world", "Set the waiting world:");
        $this->addWorldsDropdown("playing_world", "Set the playing world:");
        $this->addSelectModeDropdown();
    }

    protected function onSubmit(Player $player, FormResponse $response): void {
        $name = $response->getInputSubmittedText("name");
        $waiting_world = $response->getDropdownSubmittedOptionId("waiting_world");
        $playing_world = $response->getDropdownSubmittedOptionId("playing_world");
        $teams = $response->getSliderSubmittedStep("teams");
        $players_per_team = (int) $response->getDropdownSubmittedOptionId("players_per_team");

        if($name === "") {
            $player->sendMessage(TextFormat::RED . "You must set a name!");
            return;
        } elseif(MapFactory::getMapByName($name) !== null) {
            $player->sendMessage(TextFormat::RED . "A map with that name already exists!");
            return;
        } elseif($this->checkIfDefaultWorld($waiting_world) or $this->checkIfDefaultWorld($playing_world)) {
            $player->sendMessage(TextFormat::RED ."You world must not be the default world!");
            return;
        } elseif($waiting_world === $playing_world) {
            $player->sendMessage(TextFormat::RED . "Your waiting world cannot be the same as the playing world!");
            return;
        }

        $world_manager = Server::getInstance()->getWorldManager();
        if(!$world_manager->loadWorld($playing_world)) {
            $player->sendMessage(TextFormat::RED . "Couldn't teleport to playing world ($playing_world) because the world has been unloaded.");
            return;
        } elseif(!$world_manager->loadWorld($waiting_world)) {
            $player->sendMessage(TextFormat::RED . "Couldn't load waiting world ($waiting_world)");
            return;
        }

        $player->setGamemode(GameMode::CREATIVE());
        $player->teleport($world_manager->getWorldByName($playing_world)->getSafeSpawn());

        $session = SessionFactory::getSession($player);
        $session->setMapSetup(new MapSetup($session, new MapBuilder(
            $name, $world_manager->getWorldByName($waiting_world), $playing_world, $players_per_team, (int) ($teams * $players_per_team)
        )));
    }

    private function addWorldsDropdown(string $id, string $name): void {
        $dropdown = new Dropdown($name);
        foreach(glob($this->getWorldsPath(), GLOB_ONLYDIR) as $world) {
            $dropdown->addOption(new Option($name = basename($world), $name));
        }
        $this->addElement($id, $dropdown);
    }

    private function checkIfDefaultWorld(string $world): bool {
        return Server::getInstance()->getWorldManager()->getDefaultWorld()->getFolderName() === $world;
    }

    private function getWorldsPath(): string {
        return Path::join(Server::getInstance()->getDataPath(), "worlds", "*");
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars;


use bStats\PocketmineMp\Metrics;
use pocketmine\entity\Entity;
use pocketmine\entity\EntityDataHelper;
use pocketmine\entity\EntityFactory;
use pocketmine\entity\Human;
use pocketmine\event\Listener;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\plugin\PluginBase;
use pocketmine\utils\SingletonTrait;
use pocketmine\world\World;
use sergittos\bedwars\command\BedWarsCommand;
use sergittos\bedwars\entity\PlayBedwarsEntity;
use sergittos\bedwars\game\entity\misc\Fireball;
use sergittos\bedwars\game\entity\shop\ItemShopVillager;
use sergittos\bedwars\game\entity\shop\UpgradesShopVillager;
use sergittos\bedwars\game\GameHeartbeat;
use sergittos\bedwars\game\GameManager;
use sergittos\bedwars\game\map\MapFactory;
use sergittos\bedwars\game\task\RemoveGameTask;
use sergittos\bedwars\listener\GameListener;
use sergittos\bedwars\listener\ItemListener;
use sergittos\bedwars\listener\SessionListener;
use sergittos\bedwars\listener\SetupListener;
use sergittos\bedwars\listener\SpawnProtectionListener;
use sergittos\bedwars\listener\WaitingListener;
use sergittos\bedwars\provider\json\JsonProvider;
use sergittos\bedwars\provider\mysql\MysqlProvider;
use sergittos\bedwars\provider\Provider;
use sergittos\bedwars\provider\sqlite\SqliteProvider;
use sergittos\bedwars\session\SessionFactory;
use sergittos\bedwars\utils\ConfigGetter;
use function basename;
use function strtolower;

class BedWars extends PluginBase {
    use SingletonTrait;

    private Provider $provider;
    private Metrics $metrics;
    private GameManager $game_manager;

    protected function onLoad(): void {
        self::setInstance($this);

        $worlds_dir = $this->getDataFolder() . "worlds/";
        if(!is_dir($worlds_dir)) {
            mkdir($worlds_dir);
        }

        $this->saveResource("maps.json");
    }

    protected function onEnable(): void {
        MapFactory::init();

        $this->provider = $this->obtainProvider();
        $this->metrics = new Metrics($this, 29605);
        $this->game_manager = new GameManager();

        $this->registerEntity(PlayBedwarsEntity::class);
        $this->registerEntity(ItemShopVillager::class);
        $this->registerEntity(UpgradesShopVillager::class);
        $this->registerFireball();

        $this->registerListener(new GameListener());
        $this->registerListener(new ItemListener());
        $this->registerListener(new SessionListener());
        $this->registerListener(new SetupListener());
        $this->registerListener(new WaitingListener());

        if(ConfigGetter::isSpawnProtectionEnabled()) {
            $this->registerListener(new SpawnProtectionListener());
        }

        $this->getServer()->getCommandMap()->register("bedwars", new BedWarsCommand());

        $this->getScheduler()->scheduleRepeatingTask(new GameHeartbeat(), 1);
    }

    protected function onDisable(): void {
        foreach(SessionFactory::getSessions() as $session) {
            $session->save();
        }

        foreach($this->game_manager->getGames() as $game) {
            $game->unloadWorld();
            $this->getServer()->getAsyncPool()->submitTask(new RemoveGameTask($game));
        }
    }

    private function registerListener(Listener $listener): void {
        $this->getServer()->getPluginManager()->registerEvents($listener, $this);
    }

    private function registerEntity(string $class): void {
        EntityFactory::getInstance()->register($class, function(World $world, CompoundTag $nbt) use ($class): Entity {
            return new $class(EntityDataHelper::parseLocation($nbt, $world), Human::parseSkinNBT($nbt), $nbt);
        }, ["bedwars:" . basename($class)]);
    }

    private function registerFireball(): void {
        EntityFactory::getInstance()->register(Fireball::class, function(World $world, CompoundTag $nbt): Fireball {
            return new Fireball(EntityDataHelper::parseLocation($nbt, $world), null);
        }, ["bedwars:fireball"]);
    }

    private function obtainProvider(): Provider {
        return match(strtolower(ConfigGetter::getProvider())) {
            "mysql" => new MysqlProvider(),
            "sqlite", "sqlite3" => new SqliteProvider(),
            "json" => new JsonProvider(),
            default => throw new \Error("Invalid provider, check your config and try again.")
        };
    }

    public function getProvider(): Provider {
        return $this->provider;
    }

    public function getMetrics(): Metrics {
        return $this->metrics;
    }

    public function getGameManager(): GameManager {
        return $this->game_manager;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\entity;


use pocketmine\block\BlockTypeIds;
use pocketmine\entity\Human;
use pocketmine\entity\Location;
use pocketmine\entity\Skin;
use pocketmine\event\entity\EntityDamageByChildEntityEvent;
use pocketmine\event\entity\EntityDamageByEntityEvent;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\permission\DefaultPermissions;
use pocketmine\player\Player;
use sergittos\bedwars\form\queue\PlayBedwarsForm;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\SessionFactory;
use sergittos\bedwars\utils\ColorUtils;
use sergittos\bedwars\utils\ConfigGetter;
use sergittos\bedwars\utils\GameUtils;
use function array_filter;
use function count;

class PlayBedwarsEntity extends Human {

    private int $players_per_team;

    public function __construct(Location $location, Skin $skin, CompoundTag $nbt) {
        $this->players_per_team = $nbt->getInt("players_per_team");
        parent::__construct($location, $skin);
    }

    protected function initEntity(CompoundTag $nbt): void {
        parent::initEntity($nbt);

        $this->updateNameTag();
        $this->setNameTagAlwaysVisible();
    }

    public function updateNameTag(): void {
        $amount = $this->getSessionsCount();
        $this->setNameTag(ColorUtils::translate(
            "{YELLOW}{BOLD}CLICK TO PLAY{RESET}\n" .
            "{AQUA}" . GameUtils::getMode($this->players_per_team) . " {GRAY}[v" . ConfigGetter::getVersion() . "]\n" .
            "{YELLOW}{BOLD}" . $amount . " " . ($amount === 1 ? "Player" : "Players")
        ));
    }

    public function attack(EntityDamageEvent $source): void {
        if($source instanceof EntityDamageByChildEntityEvent or !$source instanceof EntityDamageByEntityEvent) {
            return;
        }
        $damager = $source->getDamager();
        if(!$damager instanceof Player) {
            return;
        }

        if($damager->hasPermission(DefaultPermissions::ROOT_OPERATOR) and
            $damager->getInventory()->getItemInHand()->getTypeId() === BlockTypeIds::BEDROCK) {
            $this->kill();
            return;
        }

        $damager->sendForm(new PlayBedwarsForm($this->players_per_team));
    }

    public function onInteract(Player $player, Vector3 $clickPos): bool {
        $player->sendForm(new PlayBedwarsForm($this->players_per_team));
        return true;
    }

    public function saveNBT(): CompoundTag {
        return parent::saveNBT()->setInt("players_per_team", $this->players_per_team);
    }

    public function getPlayersPerTeam(): int {
        return $this->players_per_team;
    }

    private function getSessionsCount(): int {
        return count(array_filter(SessionFactory::getSessions(), function(Session $session) {
            return $session->isPlaying() and $session->getGame()->getMap()->getPlayersPerTeam() === $this->players_per_team;
        }));
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item;


use pocketmine\item\Item;
use pocketmine\utils\TextFormat;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\ColorUtils;
use function str_replace;

abstract class BedwarsItem {

    private string $name;
    private bool $disable_transactions;

    public function __construct(string $name, bool $disable_transactions = true) {
        $this->name = ColorUtils::translate($name);
        $this->disable_transactions = $disable_transactions;
    }

    public function asItem(): Item {
        $item = $this->realItem();
        $item->setCustomName($this->name);

        $nbt = $item->getNamedTag();
        $nbt->setString("bedwars_name", str_replace(" ", "_", TextFormat::clean($this->name)));

        if($this->disable_transactions) {
            $nbt->setByte("bedwars_item", 1);
        }

        return $item;
    }

    abstract public function onInteract(Session $session): void;

    abstract protected function realItem(): Item;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\spectator;


use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\form\spectator\TeleporterForm;
use sergittos\bedwars\session\Session;

class TeleporterItem extends SpectatorItem {

    public function __construct() {
        parent::__construct("{GREEN}Teleporter");
    }

    protected function onSpectatorInteract(Session $session): void {
        $session->getPlayer()->sendForm(new TeleporterForm($session));
    }

    protected function realItem(): Item {
        return VanillaItems::COMPASS();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\spectator;


use sergittos\bedwars\item\BedwarsItem;
use sergittos\bedwars\session\Session;

abstract class SpectatorItem extends BedwarsItem {

    public function onInteract(Session $session): void {
        if($session->isSpectator()) {
            $this->onSpectatorInteract($session);
        }
    }

    abstract protected function onSpectatorInteract(Session $session): void;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\spectator;


use pocketmine\block\VanillaBlocks;
use pocketmine\item\Item;
use sergittos\bedwars\form\spectator\SpectatorSettingsForm;
use sergittos\bedwars\session\Session;

class SpectatorSettingsItem extends SpectatorItem {

    public function __construct() {
        parent::__construct("{YELLOW}Spectator settings");
    }

    protected function onSpectatorInteract(Session $session): void {
        $session->getPlayer()->sendForm(new SpectatorSettingsForm($session));
    }

    protected function realItem(): Item {
        return VanillaBlocks::REDSTONE_COMPARATOR()->asItem();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\spectator;


use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\form\queue\PlayBedwarsForm;
use sergittos\bedwars\session\Session;

class PlayAgainItem extends SpectatorItem {

    public function __construct() {
        parent::__construct("{GREEN}Play again");
    }

    protected function onSpectatorInteract(Session $session): void {
        $form = new PlayBedwarsForm($session->getGame()->getMap()->getPlayersPerTeam());
        $form->setTitle("Play again?");

        $session->getPlayer()->sendForm($form);
    }

    protected function realItem(): Item {
        return VanillaItems::PAPER();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\spectator;


use pocketmine\block\utils\DyeColor;
use pocketmine\block\VanillaBlocks;
use pocketmine\item\Item;
use sergittos\bedwars\session\Session;

class ReturnToLobbyItem extends SpectatorItem {

    public function __construct() {
        parent::__construct("{RED}Return to lobby");
    }

    protected function onSpectatorInteract(Session $session): void {
        $session->getGame()->removeSpectator($session);
    }

    protected function realItem(): Item {
        return VanillaBlocks::BED()->setColor(DyeColor::RED())->asItem();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item;


use pocketmine\utils\CloningRegistryTrait;
use sergittos\bedwars\game\generator\GeneratorType;
use sergittos\bedwars\game\shop\Shop;
use sergittos\bedwars\item\game\LeaveGameItem;
use sergittos\bedwars\item\game\TrackerShopItem;
use sergittos\bedwars\item\setup\AddGeneratorItem;
use sergittos\bedwars\item\setup\AddVillagerItem;
use sergittos\bedwars\item\setup\CancelItem;
use sergittos\bedwars\item\setup\ClaimingWandItem;
use sergittos\bedwars\item\setup\ConfigurationItem;
use sergittos\bedwars\item\setup\CreateMapItem;
use sergittos\bedwars\item\setup\ExitSetupItem;
use sergittos\bedwars\item\setup\SetBedPositionItem;
use sergittos\bedwars\item\setup\SetTeamGeneratorItem;
use sergittos\bedwars\item\spectator\PlayAgainItem;
use sergittos\bedwars\item\spectator\ReturnToLobbyItem;
use sergittos\bedwars\item\spectator\SpectatorSettingsItem;
use sergittos\bedwars\item\spectator\TeleporterItem;

/**
 * @method static LeaveGameItem LEAVE_GAME()
 * @method static PlayAgainItem PLAY_AGAIN()
 * @method static ReturnToLobbyItem RETURN_TO_LOBBY()
 * @method static SpectatorSettingsItem SPECTATOR_SETTINGS()
 * @method static TeleporterItem TELEPORTER()
 * @method static TrackerShopItem TRACKER_SHOP()
 * @method static AddGeneratorItem DIAMOND_GENERATOR()
 * @method static AddGeneratorItem EMERALD_GENERATOR()
 * @method static AddVillagerItem ITEM_VILLAGER()
 * @method static AddVillagerItem UPGRADES_VILLAGER()
 * @method static ConfigurationItem CONFIGURATION()
 * @method static CreateMapItem CREATE_MAP()
 * @method static ExitSetupItem EXIT_SETUP()
 * @method static SetTeamGeneratorItem TEAM_GENERATOR()
 * @method static SetBedPositionItem BED_POSITION()
 * @method static ClaimingWandItem CLAIMING_WAND()
 * @method static CancelItem CANCEL()
 */
class BedwarsItems {
    use CloningRegistryTrait;

    protected static function setup(): void {
        self::register("leave_game", new LeaveGameItem());
        self::register("play_again", new PlayAgainItem());
        self::register("return_to_lobby", new ReturnToLobbyItem());
        self::register("spectator_settings", new SpectatorSettingsItem());
        self::register("teleporter", new TeleporterItem());
        self::register("tracker_shop", new TrackerShopItem());

        self::register("diamond_generator", new AddGeneratorItem(GeneratorType::DIAMOND));
        self::register("emerald_generator", new AddGeneratorItem(GeneratorType::EMERALD));
        self::register("item_villager", new AddVillagerItem(Shop::ITEM));
        self::register("upgrades_villager", new AddVillagerItem(Shop::UPGRADES));
        self::register("configuration", new ConfigurationItem());
        self::register("create_map", new CreateMapItem());
        self::register("exit_setup", new ExitSetupItem());
        self::register("team_generator", new SetTeamGeneratorItem());
        self::register("bed_position", new SetBedPositionItem());
        self::register("claiming_wand", new ClaimingWandItem());
        self::register("cancel", new CancelItem());
    }

    /**
     * @return BedwarsItem[]
     */
    static public function getAll(): array {
        return self::_registryGetAll();
    }

    /**
     * @return BedwarsItem
     */
    static public function get(string $name): object {
        return self::_registryFromString($name);
    }

    static private function register(string $name, BedwarsItem $item): void {
        self::_registryRegister($name, $item);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\setup;


use pocketmine\block\VanillaBlocks;
use pocketmine\item\Item;
use sergittos\bedwars\session\Session;

class SetTeamGeneratorItem extends SetupItem {

    public function __construct() {
        parent::__construct("Team generator");
    }

    public function onInteract(Session $session): void {}

    protected function realItem(): Item {
        return VanillaBlocks::IRON()->asItem();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\setup;


use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\session\Session;

class ClaimingWandItem extends SetupItem {

    public function __construct() {
        parent::__construct("{AQUA}Claiming wand");
    }

    public function onInteract(Session $session): void {}

    protected function realItem(): Item {
        return VanillaItems::DIAMOND_HOE();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\setup;


use pocketmine\block\utils\DyeColor;
use pocketmine\block\VanillaBlocks;
use pocketmine\item\Item;
use sergittos\bedwars\session\Session;

class ExitSetupItem extends SetupItem {

    public function __construct() {
        parent::__construct("Exit setup");
    }

    public function onInteract(Session $session): void {
        $session->setMapSetup(null);
        $session->teleportToHub();
    }

    protected function realItem(): Item {
        return VanillaBlocks::BED()->setColor(DyeColor::RED)->asItem();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\setup;


use pocketmine\block\utils\DyeColor;
use pocketmine\block\VanillaBlocks;
use pocketmine\item\Item;
use sergittos\bedwars\session\Session;

class SetBedPositionItem extends SetupItem {

    private ?DyeColor $color = null;

    public function __construct() {
        parent::__construct("Bed position");
    }

    public function setColor(DyeColor $color): self {
        $this->color = $color;
        return $this;
    }

    public function onInteract(Session $session): void {}

    protected function realItem(): Item {
        $block = VanillaBlocks::BED();
        if($this->color !== null) {
            $block->setColor($this->color);
        }
        return $block->asItem();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\setup;


use pocketmine\item\Item;
use sergittos\bedwars\item\BedwarsItem;

abstract class SetupItem extends BedwarsItem {

    public function asItem(): Item {
        $item = parent::asItem();
        $item->getNamedTag()->setByte("setup", 1);
        return $item;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\setup;


use pocketmine\block\utils\DyeColor;
use pocketmine\block\VanillaBlocks;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\setup\step\PreparingMapStep;

class CancelItem extends SetupItem {

    public function __construct() {
        parent::__construct("{RED}Cancel");
    }

    public function onInteract(Session $session): void {
        if($session->isCreatingMap()) {
            $session->getMapSetup()->setStep(new PreparingMapStep());
        }
    }

    protected function realItem(): Item {
        return VanillaBlocks::BED()->setColor(DyeColor::RED)->asItem();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\setup;


use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\item\BedwarsItem;
use sergittos\bedwars\session\Session;

class CreateMapItem extends BedwarsItem {

    public function __construct() {
        parent::__construct("Create map");
    }

    public function onInteract(Session $session): void {
        $setup = $session->getMapSetup();
        if($setup->getMapBuilder()->canBeBuilt()) {
            $session->teleportToHub();
            $session->message("{GREEN}Map created successfully.");

            $setup->createMap();
        } else {
            $session->message("{RED}You must set all the map properties before creating it.");
        }
    }

    protected function realItem(): Item {
        return VanillaItems::EMERALD();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\setup;


use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\form\setup\SetupMapForm;
use sergittos\bedwars\item\BedwarsItem;
use sergittos\bedwars\session\Session;

class ConfigurationItem extends BedwarsItem {

    public function __construct() {
        parent::__construct("Configuration");
    }

    public function onInteract(Session $session): void {
        $session->getPlayer()->sendForm(new SetupMapForm($session));
    }

    protected function realItem(): Item {
        return VanillaItems::PAPER();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\setup;


use pocketmine\item\Item;
use pocketmine\item\StringToItemParser;
use sergittos\bedwars\game\generator\GeneratorType;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\GameUtils;
use function strtolower;

class AddGeneratorItem extends SetupItem {

    private GeneratorType $type;

    public function __construct(GeneratorType $type) {
        $this->type = $type;
        parent::__construct(GameUtils::getGeneratorColor($name = $type->toString()) . $name . " generator");
    }

    protected function realItem(): Item {
        return StringToItemParser::getInstance()->parse(strtolower($this->type->toString()) . "_block");
    }

    public function onInteract(Session $session): void {}

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\setup;


use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\session\Session;

class AddVillagerItem extends SetupItem {

    private string $name;

    public function __construct(string $name) {
        $this->name = $name;
        parent::__construct($name . " villager");
    }

    public function getName(): string {
        return $this->name;
    }

    public function onInteract(Session $session): void {}

    protected function realItem(): Item {
        return VanillaItems::VILLAGER_SPAWN_EGG();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\game;


use pocketmine\entity\Location;
use pocketmine\entity\projectile\Throwable;
use pocketmine\item\ItemIdentifier;
use pocketmine\item\ItemTypeIds;
use pocketmine\item\ProjectileItem;
use pocketmine\player\Player;
use pocketmine\utils\TextFormat;
use sergittos\bedwars\game\entity\misc\Fireball as FireballEntity;

class Fireball extends ProjectileItem {

    public function __construct() {
        parent::__construct(new ItemIdentifier(ItemTypeIds::FIRE_CHARGE), "Fireball");

        $this->setCustomName(TextFormat::RED . "Fireball");
    }

    protected function createEntity(Location $location, Player $thrower): Throwable {
        return new FireballEntity($location, $thrower);
    }

    public function getThrowForce(): float {
        return 1.5;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\game;


use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\form\shop\ShopForm;
use sergittos\bedwars\game\shop\Shop;
use sergittos\bedwars\game\shop\ShopFactory;
use sergittos\bedwars\item\BedwarsItem;
use sergittos\bedwars\session\Session;

class TrackerShopItem extends BedwarsItem {

    public function __construct() {
        parent::__construct("Tracker Shop", false);
    }

    public function onInteract(Session $session): void {
        $session->getPlayer()->sendForm(new ShopForm($session, "Tracker & Communication", ShopFactory::getShop(Shop::TRACKER), false));
    }

    protected function realItem(): Item {
        return VanillaItems::COMPASS();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\item\game;


use pocketmine\block\utils\DyeColor;
use pocketmine\block\VanillaBlocks;
use pocketmine\item\Item;
use sergittos\bedwars\item\BedwarsItem;
use sergittos\bedwars\session\Session;

class LeaveGameItem extends BedwarsItem {

    public function __construct() {
        parent::__construct("{RED}Leave game");
    }

    public function onInteract(Session $session): void {
        if($session->isPlaying()) {
            $session->getGame()->removePlayer($session);
        }
    }

    protected function realItem(): Item {
        return VanillaBlocks::BED()->setColor(DyeColor::RED())->asItem();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\listener;


use pocketmine\event\block\BlockBreakEvent;
use pocketmine\event\block\BlockPlaceEvent;
use pocketmine\event\Cancellable;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerDropItemEvent;
use pocketmine\event\player\PlayerInteractEvent;
use pocketmine\player\GameMode;
use pocketmine\player\Player;
use pocketmine\Server;

class SpawnProtectionListener implements Listener {

    public function onBlockPlace(BlockPlaceEvent $event): void {
        $this->checkPlayerEvent($event);
    }

    public function onBlockBreak(BlockBreakEvent $event): void {
        $this->checkPlayerEvent($event);
    }

    public function onInteract(PlayerInteractEvent $event): void {
        $this->checkPlayerEvent($event);
    }

    public function onDrop(PlayerDropItemEvent $event): void {
        $this->checkPlayerEvent($event);
    }

    public function onDamage(EntityDamageEvent $event): void {
        $entity = $event->getEntity();
        if($entity instanceof Player) {
            $this->checkWorldProtection($entity, $event);
        }
    }

    private function checkPlayerEvent(Cancellable $cancellable): void {
        $this->checkWorldProtection($cancellable->getPlayer(), $cancellable);
    }

    private function checkWorldProtection(Player $player, Cancellable $cancellable): void {
        if($player->getGamemode() !== GameMode::CREATIVE and $player->getWorld() === Server::getInstance()->getWorldManager()->getDefaultWorld()) {
            $cancellable->cancel();
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\listener;


use pocketmine\event\Listener;
use pocketmine\event\player\PlayerJoinEvent;
use pocketmine\event\player\PlayerLoginEvent;
use pocketmine\event\player\PlayerQuitEvent;
use sergittos\bedwars\session\SessionFactory;

class SessionListener implements Listener {

    public function onLogin(PlayerLoginEvent $event): void {
        SessionFactory::createSession($event->getPlayer());
    }

    public function onJoin(PlayerJoinEvent $event): void {
        SessionFactory::getSession($event->getPlayer())->teleportToHub();
    }

    /**
     * @priority HIGHEST
     */
    public function onQuit(PlayerQuitEvent $event): void {
        SessionFactory::removeSession($event->getPlayer());
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\listener;


use pocketmine\event\block\BlockBreakEvent;
use pocketmine\event\block\BlockPlaceEvent;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerInteractEvent;
use pocketmine\player\Player;
use sergittos\bedwars\session\SessionFactory;

class WaitingListener implements Listener {

    public function onBreak(BlockBreakEvent $event): void {
        if($this->checkWorld($event->getPlayer())) {
            $event->cancel();
        }
    }

    public function onPlace(BlockPlaceEvent $event): void {
        if($this->checkWorld($event->getPlayer())) {
            $event->cancel();
        }
    }

    public function onInteract(PlayerInteractEvent $event): void {
        if($this->checkWorld($event->getPlayer())) {
            $event->cancel();
        }
    }

    public function onReceiveDamage(EntityDamageEvent $event): void {
        $entity = $event->getEntity();
        if($entity instanceof Player and $this->checkWorld($entity)) {
            $event->cancel();
        }
    }

    private function checkWorld(Player $player): bool {
        $session = SessionFactory::getSession($player);
        return $session !== null and $session->isPlaying() and $session->getGame()->getMap()->getWaitingWorld() === $player->getWorld();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\listener;


use pocketmine\event\block\BlockBreakEvent;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerInteractEvent;
use pocketmine\event\player\PlayerItemUseEvent;
use pocketmine\item\Item;
use sergittos\bedwars\item\BedwarsItem;
use sergittos\bedwars\item\BedwarsItems;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\SessionFactory;

class SetupListener implements Listener {

    public function onBreak(BlockBreakEvent $event): void {
        $session = SessionFactory::getSession($event->getPlayer());
        if($this->isCreatingMap($session, $item = $event->getItem())) {
            $event->cancel();

            $session->getMapSetup()->getStep()->onBlockBreak($event->getBlock(), $this->getBedwarsItem($item));
        }
    }

    public function onItemUse(PlayerItemUseEvent $event): void {
        $session = SessionFactory::getSession($event->getPlayer());
        if($this->isCreatingMap($session, $item = $event->getItem())) {
            $event->cancel();

            $session->getMapSetup()->getStep()->onInteract($this->getBedwarsItem($item));
        }
    }

    public function onInteract(PlayerInteractEvent $event): void {
        $session = SessionFactory::getSession($event->getPlayer());
        if($this->isCreatingMap($session, $item = $event->getItem())) {
            $event->cancel();

            $session->getMapSetup()->getStep()->onBlockInteract($event->getBlock()->getPosition(), $event->getAction(), $event, $this->getBedwarsItem($item));
        }
    }

    private function getBedwarsItem(Item $item): BedwarsItem {
        return BedwarsItems::get($item->getNamedTag()->getString("bedwars_name"));
    }

    private function isCreatingMap(Session $session, Item $item): bool {
        return $session->isCreatingMap() and $item->getNamedTag()->getTag("setup") !== null;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\listener;


use pocketmine\event\inventory\InventoryTransactionEvent;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerItemUseEvent;
use sergittos\bedwars\item\BedwarsItems;
use sergittos\bedwars\session\SessionFactory;
use function strtolower;

class ItemListener implements Listener {

    public function onTransaction(InventoryTransactionEvent $event): void {
        foreach($event->getTransaction()->getActions() as $action) {
            if($action->getSourceItem()->getNamedTag()->getTag("bedwars_item") !== null) {
                $event->cancel();
            }
        }
    }

    public function onUse(PlayerItemUseEvent $event): void {
        $tag = $event->getItem()->getNamedTag()->getTag("bedwars_name");
        if($tag === null) {
            return;
        }

        BedwarsItems::get(strtolower($tag->getValue()))->onInteract(SessionFactory::getSession($event->getPlayer())); // TODO: Clean
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\listener;


use pocketmine\block\Air;
use pocketmine\block\Bed;
use pocketmine\block\Chest;
use pocketmine\block\StainedGlass;
use pocketmine\block\TNT;
use pocketmine\entity\effect\VanillaEffects;
use pocketmine\entity\object\ItemEntity;
use pocketmine\entity\projectile\Arrow;
use pocketmine\event\block\BlockBreakEvent;
use pocketmine\event\block\BlockPlaceEvent;
use pocketmine\event\entity\EntityDamageByChildEntityEvent;
use pocketmine\event\entity\EntityDamageByEntityEvent;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\event\entity\EntityEffectAddEvent;
use pocketmine\event\entity\EntityExplodeEvent;
use pocketmine\event\entity\EntityItemPickupEvent;
use pocketmine\event\entity\EntityPreExplodeEvent;
use pocketmine\event\entity\EntityTeleportEvent;
use pocketmine\event\entity\ItemMergeEvent;
use pocketmine\event\entity\ItemSpawnEvent;
use pocketmine\event\inventory\CraftItemEvent;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerBedEnterEvent;
use pocketmine\event\player\PlayerChatEvent;
use pocketmine\event\player\PlayerDropItemEvent;
use pocketmine\event\player\PlayerExhaustEvent;
use pocketmine\event\player\PlayerInteractEvent;
use pocketmine\event\player\PlayerItemConsumeEvent;
use pocketmine\event\player\PlayerItemUseEvent;
use pocketmine\event\player\PlayerMoveEvent;
use pocketmine\event\player\PlayerQuitEvent;
use pocketmine\event\server\DataPacketSendEvent;
use pocketmine\event\world\ChunkUnloadEvent;
use pocketmine\item\ItemTypeIds;
use pocketmine\item\MilkBucket;
use pocketmine\item\VanillaItems;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\EntityEventBroadcaster;
use pocketmine\network\mcpe\NetworkBroadcastUtils;
use pocketmine\network\mcpe\protocol\MobArmorEquipmentPacket;
use pocketmine\network\mcpe\protocol\MobEquipmentPacket;
use pocketmine\network\mcpe\protocol\MoveActorAbsolutePacket;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
use pocketmine\player\chat\LegacyRawChatFormatter;
use pocketmine\player\Player;
use pocketmine\scheduler\ClosureTask;
use pocketmine\utils\TextFormat;
use sergittos\bedwars\BedWars;
use sergittos\bedwars\game\entity\shop\Villager;
use sergittos\bedwars\game\Game;
use sergittos\bedwars\game\stage\PlayingStage;
use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\SessionFactory;
use sergittos\bedwars\utils\GameUtils;
use sergittos\bedwars\utils\MathUtils;
use function array_map;
use function in_array;
use function strtoupper;

class GameListener implements Listener {

    public function onReceiveDamage(EntityDamageEvent $event): void {
        $entity = $event->getEntity();
        if(!$entity instanceof Player) {
            return;
        }

        $session = SessionFactory::getSession($entity);
        if(!$session->isPlaying()) {
            return;
        }

        $effects = $entity->getEffects();
        if($effects->has($effect = VanillaEffects::INVISIBILITY())) {
            $effects->remove($effect);
            $session->message("{RED}You took damage and lost your invisibility!");
        }

        if($event->getCause() === EntityDamageEvent::CAUSE_ENTITY_EXPLOSION) {
            $event->setBaseDamage(0);
        }

        if($event->getFinalDamage() >= $entity->getHealth()) {
            $session->kill($event->getCause());
            $event->cancel();
        }
    }

    public function onEntityDamageByEntity(EntityDamageByEntityEvent $event): void {
        $damager = $event->getDamager();
        $entity = $event->getEntity();
        if(!$damager instanceof Player or !$entity instanceof Player or
            !SessionFactory::hasSession($damager) or !SessionFactory::hasSession($entity)) {
            return;
        }
        $damager_session = SessionFactory::getSession($damager);
        $entity_session = SessionFactory::getSession($entity);

        $entity_session->setLastSessionHit($damager_session);

        if($damager_session->isPlaying() and $entity_session->isPlaying() and
            $damager_session->hasTeam() and $entity_session->hasTeam() and
            $damager_session->getTeam()->hasMember($entity_session)) {
            $event->cancel();
        }
    }

    public function onInteract(PlayerInteractEvent $event): void {
        $session = SessionFactory::getSession($event->getPlayer());
        if(!$session->isPlaying()) {
            return;
        }
        $block = $event->getBlock();
        if(!$block instanceof Chest) {
            return;
        }

        $team = $this->getTeamByPosition($session->getGame(), $block->getPosition());
        if($team !== null and $team->isAlive() and $team->getName() !== $session->getTeam()->getName()) {
            $session->message("{RED}You can't open this chest!");
            $event->cancel();
        }
    }

    public function onBreak(BlockBreakEvent $event): void {
        $session = SessionFactory::getSession($event->getPlayer());
        if(!$session->isPlaying()) {
            return;
        }

        $block = $event->getBlock();
        $position = $block->getPosition();
        $game = $session->getGame();

        if($game->checkBlock($position)) {
            return;
        } elseif(!$block instanceof Bed) {
            $session->message("{RED}You can only break blocks placed by a player!");
            $event->cancel();
            return;
        }

        $other_half = $block->getOtherHalf();
        if($other_half === null) {
            return;
        }

        $team = $this->getTeamByPosition($game, $position);
        if($team === null) {
            return;
        }

        $bed_position = $team->getBedPosition();
        $half_position = $other_half->getPosition();

        if($team->isBedDestroyed() or $half_position === null or (!$bed_position->equals($half_position) and !$bed_position->equals($position))) {
            return;
        }

        if($session->getTeam()->getName() !== $team->getName()) {
            $team->destroyBed($game, false);
            $game->broadcastMessage("{BOLD}{WHITE}BED DESTRUCTION > {RESET}" . $team->getColoredName() . " Bed {GRAY}was destroyed by " . $session->getColoredUsername() . "{GRAY}!");
            $event->setDrops([]);
        } else {
            $session->message("{RED}You can't destroy your own bed!");
            $event->cancel();
        }
    }

    public function onPlace(BlockPlaceEvent $event): void {
        $session = SessionFactory::getSession($event->getPlayer());
        if(!$session->isPlaying()) {
            return;
        }

        $game = $session->getGame();
        $position = $event->getBlockAgainst()->getPosition();

        if($this->getTeamByPosition($game, $position)?->getClaim()->isInside($position)) {
            $session->message("{RED}You can't place blocks here!");
            $event->cancel();
            return;
        }

        foreach($event->getTransaction()->getBlocks() as [$x, $y, $z, $block]) {
            if($block instanceof TNT) {
                BedWars::getInstance()->getScheduler()->scheduleDelayedTask(new ClosureTask(function () use ($block) {
                    $world = $block->getPosition()->world;
                    if($world !== null and $world->isLoaded()) {
                        $block->ignite(60);
                    }
                }), 1);
                continue;
            }
            $session->getGame()->addBlock($block->getPosition());
        }
    }

    public function onMove(PlayerMoveEvent $event): void {
        $player = $event->getPlayer();
        $session = SessionFactory::getSession($player);
        if(!$session->isPlaying() or !$session->hasTeam()) {
            return;
        }

        $this->checkEntities($player);

        if($session->getGameSettings()->isUnderMagicMilkEffect()) {
            return;
        }

        $team = $this->getTeamByPosition($session->getGame(), $player->getPosition());
        if($team === null or $team->isBedDestroyed() or $session->getTeam()->getName() === $team->getName()) {
            return;
        }

        $upgrades = $team->getUpgrades();
        if($upgrades->canTriggerTrap()) {
            $upgrades->triggerPrimaryTrap($session, $team);
        }
    }

    public function onChat(PlayerChatEvent $event): void {
        $session = SessionFactory::getSession($event->getPlayer());
        if(!$session->hasGame()) {
            return;
        }

        $team = $session->getTeam();
        if($session->isPlaying() and $team !== null) {
            $prefix = $team->getColor() . "[" . strtoupper($team->getName()) . "] ";
        } elseif($session->isSpectator()) {
            $prefix = TextFormat::DARK_GRAY . "[SPECTATOR] ";
        } else {
            $prefix = "";
        }

        $event->setFormatter(new LegacyRawChatFormatter(
            $prefix . TextFormat::GRAY . "{%0}: {%1}"
        ));
        $event->setRecipients(array_map(function(Session $session) {
            return $session->getPlayer();
        }, $session->getGame()->getPlayersAndSpectators()));
    }

    public function onEntityTeleport(EntityTeleportEvent $event): void {
        $entity = $event->getEntity();
        if(!$entity instanceof Player) {
            return;
        }

        $session = SessionFactory::getSession($entity);
        if(!$session->isPlaying()) {
            return;
        }

        $stage = $session->getGame()->getStage();
        if($stage instanceof PlayingStage and $stage->hasStarted() and $event->getFrom()->getWorld() !== $event->getTo()->getWorld()) {
            $session->getGame()->removePlayer($session);
        }
    }

    public function onEffectAdd(EntityEffectAddEvent $event): void {
        $entity = $event->getEntity();
        if(!$entity instanceof Player or !SessionFactory::getSession($entity)?->isPlaying()) {
            return;
        }

        $effect = $event->getEffect();
        $duration = GameUtils::getEffectDuration($effect);

        if($duration !== 0) {
            $effect->setDuration($duration);
            $effect->setAmplifier(GameUtils::getEffectAmplifier($effect));
            $effect->setVisible(false);
        }
    }

    public function onEntityDamageByChildEntity(EntityDamageByChildEntityEvent $event): void {
        $damager = $event->getDamager();
        $entity = $event->getEntity();
        if((!$damager instanceof Player or !SessionFactory::getSession($damager)?->isPlaying()) or
            (!$entity instanceof Player or !SessionFactory::getSession($entity)?->isPlaying())) {
            return;
        }

        $child = $event->getChild();
        if($child instanceof Arrow) {
            SessionFactory::getSession($damager)->playSound("random.orb");
        }
    }

    public function onExplode(EntityExplodeEvent $event): void {
        $game = BedWars::getInstance()->getGameManager()->getGameByWorld($event->getPosition()->getWorld());
        if($game === null) {
            return;
        }

        $block_list = [];
        foreach($event->getBlockList() as $block) {
            if($block instanceof StainedGlass) {
                continue;
            }
            if(!$game->checkBlock($block->getPosition())) {
                continue;
            }
            $block_list[] = $block;
        }

        $event->setBlockList($block_list);
    }

    public function onDataPacketSend(DataPacketSendEvent $event): void {
        foreach($event->getTargets() as $target) {
            $player = $target->getPlayer();
            if($player === null or !SessionFactory::hasSession($player) or !SessionFactory::getSession($player)->isPlaying()) {
                continue;
            }

            foreach($event->getPackets() as $packet) {
                if(!$packet instanceof MobEquipmentPacket and !$packet instanceof MobArmorEquipmentPacket) {
                    continue;
                }

                $target = $player->getWorld()->getEntity($packet->actorRuntimeId);
                if(!$target instanceof Player or !SessionFactory::getSession($target)->isPlaying() or !$target->getEffects()->has(VanillaEffects::INVISIBILITY())) {
                    continue;
                }

                $item = ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet(VanillaItems::AIR()));

                if($packet instanceof MobEquipmentPacket) {
                    $packet->item = $item;
                } else {
                    $packet->head = $item;
                    $packet->chest = $item;
                    $packet->legs = $item;
                    $packet->feet = $item;
                }
            }
        }
    }

    public function onCraft(CraftItemEvent $event): void {
        $session = SessionFactory::getSession($event->getPlayer());
        if($session->isPlaying()) {
            $event->cancel();
        }
    }

    public function onConsume(PlayerItemConsumeEvent $event): void {
        $session = SessionFactory::getSession($event->getPlayer());
        if($session->isPlaying() and $event->getItem() instanceof MilkBucket) {
            $session->getGameSettings()->setMagicMilk();
        }
    }

    public function onDrop(PlayerDropItemEvent $event): void {
        $session = SessionFactory::getSession($player = $event->getPlayer());
        if($session->isPlaying() and
            in_array($event->getItem()->getTypeId(), [ItemTypeIds::IRON_INGOT, ItemTypeIds::GOLD_INGOT, ItemTypeIds::DIAMOND, ItemTypeIds::EMERALD]) and
            $player->getWorld()->getBlock($player->getPosition()->subtract(0, 2, 0)) instanceof Air) {
            $event->cancel();
        }
    }

    public function onEntityEvent(ItemSpawnEvent $event): void {
        $entity = $event->getEntity();
        if($entity->getOwner() === "generator") {
            $entity->setPickupDelay(0);
        }
    }

    public function onPickup(EntityItemPickupEvent $event): void {
        $origin = $event->getOrigin();
        if(!$origin instanceof ItemEntity or $origin->getOwner() !== "generator") {
            return;
        }

        $event->cancel();

        $world = $origin->getWorld();
        foreach($world->getNearbyEntities($origin->getBoundingBox()->expandedCopy(1, 0.5, 1), $origin) as $entity) {
            if(!$entity instanceof Player) {
                continue;
            }

            NetworkBroadcastUtils::broadcastEntityEvent(
                $origin->getViewers(),
                fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onPickUpItem($recipients, $entity, $origin)
            );

            foreach($entity->getInventory()->addItem($event->getItem()) as $remains) {
                $world->dropItem($origin->getLocation(), $remains, new Vector3(0, 0, 0));
            }
        }

        $origin->flagForDespawn();
    }

    public function onMerge(ItemMergeEvent $event): void {
        $item = $event->getTarget()->getItem();
        if($item->getCount() >= GameUtils::getCountById($item->getTypeId())) {
            $event->getEntity()->flagForDespawn();
            $event->cancel();
        }
    }

    public function onQuit(PlayerQuitEvent $event): void {
        $session = SessionFactory::getSession($event->getPlayer());
        $game = $session->getGame();

        if($session->isPlaying()) {
            $game->removePlayer($session, false);
        } elseif($session->isSpectator()) {
            $game->removeSpectator($session);
        }
    }

    public function onChunkUnload(ChunkUnloadEvent $event): void {
        $game = BedWars::getInstance()->getGameManager()->getGameByWorld($event->getWorld());
        if($game !== null and $game->getStage() instanceof PlayingStage) {
            $event->cancel();
        }
    }

    public function onExplosionPrime(EntityPreExplodeEvent $event): void {
        $event->setRadius(5);
    }

    public function onExhaust(PlayerExhaustEvent $event): void {
        $event->cancel();
    }

    public function onBedEnter(PlayerBedEnterEvent $event): void {
        $event->cancel();
    }

    /**
     * @handleCancelled
     */
    public function onItemUse(PlayerItemUseEvent $event): void {
        if(SessionFactory::getSession($event->getPlayer())->isSpectator()) {
            $event->uncancel();
        }
    }

    private function checkEntities(Player $player): void {
        $network_session = $player->getNetworkSession();
        $position = $player->getPosition();
        foreach($player->getWorld()->getNearbyEntities($player->getBoundingBox()->expandedCopy(12, 12, 12), $player) as $entity) {
            if(!$entity instanceof Villager) {
                continue;
            }

            $yaw = MathUtils::calculateYaw($position, $location = $entity->getLocation());
            $pitch = MathUtils::calculatePitch($position, $location);

            $network_session->sendDataPacket(MoveActorAbsolutePacket::create(
                $entity->getId(),
                $location,
                $pitch,
                $yaw,
                $yaw,
                0
            ));
        }
    }

    private function getTeamByPosition(Game $game, Vector3 $position): ?Team {
        foreach($game->getTeams() as $team) {
            if($team->getZone()->isInside($position)) {
                return $team;
            }
        }
        return null;
    }

}
<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\generator;


use pocketmine\item\Item;
use pocketmine\math\Vector3;
use pocketmine\world\World;
use sergittos\bedwars\game\Game;
use sergittos\bedwars\utils\ColorUtils;
use sergittos\bedwars\utils\GameUtils;

abstract class Generator {

    protected Vector3 $position;
    protected Tier $tier;

    protected int $speed;
    protected int $countdown = 0;
    protected int $time = 0;

    public function __construct(Vector3 $position) {
        $this->position = $position;
        $this->tier = Tier::I;
        $this->setSpeed($this->getInitialSpeed());
    }

    public function getName(): string {
        $name = $this->getType()->toString();
        return ColorUtils::translate(GameUtils::getGeneratorColor($name) . $name);
    }

    public function getPosition(): Vector3 {
        return $this->position;
    }

    public function getTier(): Tier {
        return $this->tier;
    }

    public function getSpeed(): int {
        return $this->speed;
    }

    public function getTime(): int {
        return $this->time;
    }

    public function setTier(Tier $tier): void {
        $this->tier = $tier;
    }

    public function setSpeed(int $speed): void {
        $this->speed = $speed;
        $this->setCountdown(1 / $speed);
    }

    public function setCountdown(float $countdown): void { // drops per second
        $this->countdown = (int) (20 / $countdown);
        $this->resetTime();
    }

    private function resetTime(): void {
        $this->time = $this->countdown;
    }

    public function tick(Game $game): void {
        $this->time--;
        if($this->time <= 0) {
            $this->resetTime();
            $this->dropItem($game->getWorld());
        }
    }

    private function dropItem(World $world): void {
        $entity = $world->dropItem($this->position->add(0, 0.1, 0), clone $this->getItem(), Vector3::zero());
        if($entity !== null) {
            $entity->setOwner("generator");

            $this->onDropItem($world);
        }
    }

    public function reset(): void {
        $this->tier = Tier::I;
        $this->setSpeed($this->getInitialSpeed());
    }

    public function onDropItem(World $world): void {}

    abstract public function getType(): GeneratorType;

    abstract public function getInitialSpeed(): int;

    abstract protected function getItem(): Item;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\generator\presets;


use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\game\generator\GeneratorType;

class DiamondGenerator extends TextGenerator {

    public function getType(): GeneratorType {
        return GeneratorType::DIAMOND;
    }

    public function getInitialSpeed(): int {
        return 45;
    }

   protected function getItem(): Item {
       return VanillaItems::DIAMOND();
   }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\generator\presets;


use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\game\generator\Generator;
use sergittos\bedwars\game\generator\GeneratorType;

class IronGenerator extends Generator {

    public function getType(): GeneratorType {
        return GeneratorType::IRON;
    }

    public function getInitialSpeed(): int {
        return 1;
    }

    protected function getItem(): Item {
        return VanillaItems::IRON_INGOT();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\generator\presets;


use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\game\generator\GeneratorType;

class EmeraldGenerator extends TextGenerator {

    public function getType(): GeneratorType {
        return GeneratorType::EMERALD;
    }

    public function getInitialSpeed(): int {
        return 60;
    }

    protected function getItem(): Item {
        return VanillaItems::EMERALD();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\generator\presets;


use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\game\generator\Generator;
use sergittos\bedwars\game\generator\GeneratorType;

class GoldGenerator extends Generator {

    public function getType(): GeneratorType {
        return GeneratorType::GOLD;
    }

    public function getInitialSpeed(): int {
        return 5;
    }

    protected function getItem(): Item {
        return VanillaItems::GOLD_INGOT();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\generator\presets;


use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\game\generator\Generator;
use sergittos\bedwars\game\generator\GeneratorType;

class TeamEmeraldGenerator extends Generator {

    public function getType(): GeneratorType {
        return GeneratorType::TEAM_EMERALD;
    }

    public function getInitialSpeed(): int {
        return 30;
    }

    protected function getItem(): Item {
        return VanillaItems::EMERALD();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\generator\presets;


use pocketmine\math\Vector3;
use pocketmine\world\World;
use sergittos\bedwars\game\Game;
use sergittos\bedwars\game\generator\Generator;
use sergittos\bedwars\game\generator\GeneratorText;
use sergittos\bedwars\game\generator\Tier;

abstract class TextGenerator extends Generator {

    protected GeneratorText $text;

    public function __construct(Vector3 $position) {
        parent::__construct($position);

        $this->text = new GeneratorText();
    }

    public function getText(): GeneratorText {
        return $this->text;
    }

    public function setTier(Tier $tier): void {
        parent::setTier($tier);

        $this->setSpeed($this->speed - 15);
    }

    public function tick(Game $game): void {
        parent::tick($game);

        if($this->time % 20 === 0) {
            $this->updateText($game->getWorld());
        }
    }

    public function onDropItem(World $world): void {
        $this->updateText($world);
    }

    public function updateText(World $world): void {
        $this->text->update($this, $world);
    }

    public function __clone(): void {
        $this->text = clone $this->text;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\generator;


use pocketmine\math\Vector3;
use pocketmine\utils\TextFormat;
use pocketmine\world\particle\FloatingTextParticle;
use pocketmine\world\World;
use sergittos\bedwars\session\Session;

class GeneratorText {

    /** @var FloatingTextParticle[] */
    private array $particles;

    public function __construct() {
        $this->particles[0] = new FloatingTextParticle("");
        $this->particles[1] = new FloatingTextParticle("");
        $this->particles[2] = new FloatingTextParticle("");
    }

    public function update(Generator $generator, World $world): void {
        $this->particles[0]->setText(TextFormat::YELLOW . "Tier " . TextFormat::RED . $generator->getTier()->name);
        $this->particles[1]->setText($generator->getName());
        $this->particles[2]->setText(TextFormat::YELLOW . "Spawns in " . TextFormat::RED . ($time = ((int) ($generator->getTime() / 20))) . TextFormat::YELLOW . ($time === 1 ? " second" : " seconds"));

        foreach($this->particles as $index => $line) {
            $world->addParticle($generator->getPosition()->add(0, (3 - $index) / 2 + 1.25, 0), $line);
        }
    }

    public function despawnFrom(Session $session): void {
        foreach($this->particles as $line) {
            $line->setInvisible();
            foreach($line->encode(Vector3::zero()) as $packet) {
                $session->sendDataPacket($packet);
            }
            $line->setInvisible(false);
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\generator;


enum Tier {

    case I;
    case II;
    case III;

    public function toString(): string {
        return match($this) {
            self::I => "I",
            self::II => "II",
            self::III => "III"
        };
    }

}<?php


namespace sergittos\bedwars\game\generator;


use pocketmine\math\Vector3;
use sergittos\bedwars\game\generator\presets\DiamondGenerator;
use sergittos\bedwars\game\generator\presets\EmeraldGenerator;
use sergittos\bedwars\game\generator\presets\GoldGenerator;
use sergittos\bedwars\game\generator\presets\IronGenerator;
use sergittos\bedwars\game\generator\presets\TeamEmeraldGenerator;

enum GeneratorType {

    case IRON;
    case GOLD;
    case DIAMOND;
    case EMERALD;
    case TEAM_EMERALD;

    static public function toGenerator(Vector3 $position, GeneratorType $type): Generator {
        return match($type) {
            self::IRON => new IronGenerator($position),
            self::GOLD => new GoldGenerator($position),
            self::DIAMOND => new DiamondGenerator($position),
            self::EMERALD => new EmeraldGenerator($position),
            self::TEAM_EMERALD => new TeamEmeraldGenerator($position),
        };
    }

    static public function fromString(string $type): self {
        return match(strtolower($type)) {
            "iron" => self::IRON,
            "gold" => self::GOLD,
            "diamond" => self::DIAMOND,
            "emerald" => self::EMERALD,
            "team_emerald" => self::TEAM_EMERALD,
            default => throw new \InvalidArgumentException("Invalid generator type: $type"),
        };
    }

    public function toString(): string {
        return match($this) {
            self::IRON => "Iron",
            self::GOLD => "Gold",
            self::DIAMOND => "Diamond",
            self::EMERALD => "Emerald",
            self::TEAM_EMERALD => "Team Emerald",
        };
    }

}
<?php

declare(strict_types=1);


namespace sergittos\bedwars\game;


use pocketmine\scheduler\Task;
use sergittos\bedwars\BedWars;
use sergittos\bedwars\game\stage\PlayingStage;

class GameHeartbeat extends Task {

    private int $current_tick = 0;

    public function onRun(): void {
        $this->current_tick++;

        foreach(BedWars::getInstance()->getGameManager()->getGames() as $game) {
            if($game->getStage() instanceof PlayingStage) {
                $game->tickGenerators();
            }

            if($this->current_tick % 20 === 0) {
                $game->getStage()->tick();
            }
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop;


use sergittos\bedwars\game\shop\item\ItemShop;
use sergittos\bedwars\game\shop\tracker\TrackerShop;
use sergittos\bedwars\game\shop\upgrades\UpgradesShop;

class ShopFactory {

    /** @var Shop[] */
    static private array $shops = [];

    static public function init(): void { // todo: use enums
        self::addShop(new ItemShop());
        self::addShop(new UpgradesShop());
        self::addShop(new TrackerShop());
    }

    static public function getShop(string $id): ?Shop {
        return self::$shops[$id] ?? null;
    }

    static private function addShop(Shop $shop): void {
        self::$shops[$shop->getId()] = $shop;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop;


abstract class Shop {

    public const ITEM = "item";
    public const UPGRADES = "upgrades";
    public const TRACKER = "tracker";

    abstract public function getId(): string;

    /**
     * @return Category[]
     */
    abstract public function getCategories(): array;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop;


use sergittos\bedwars\session\Session;

abstract class Category {

    private string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    public function getName(): string {
        return $this->name;
    }

    /**
     * @return Product[]
     */
    abstract public function getProducts(Session $session): array;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop;


use pocketmine\item\Item;
use pocketmine\utils\TextFormat;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\ColorUtils;
use sergittos\bedwars\utils\GameUtils;
use function strtok;
use function strtolower;
use function ucfirst;

abstract class Product {

    protected string $id;
    protected string $name;

    protected int $price;

    protected Item $ore;

    public function __construct(string $id, string $name, int $price, Item $ore) {
        $this->id = $id;
        $this->name = $name;
        $this->price = $price;
        $this->ore = $ore->setCount($price);
    }

    public function getName(): string {
        return $this->name;
    }

    public function getPrice(): int {
        return $this->price;
    }

    public function getOre(): Item {
        return clone $this->ore;
    }

    public function getDisplayName(Session $session): string {
        $color = TextFormat::RED;
        if($session->getPlayer()->getInventory()->contains($this->ore)) {
            $color = TextFormat::GREEN;
        }
        return $color . $this->name;
    }

    public function getDescription(Session $session): string {
        $name = strtok(strtolower($this->ore->getVanillaName()), " ");
        $color = GameUtils::getGeneratorColor($name);

        if($name !== "iron" and $name !== "gold" and $this->price !== 1) {
            $name .= "s";
        }
        $name = ucfirst($name);

        return ColorUtils::translate("{GRAY}Cost: " . $color . $this->price . " " . ucfirst($name));
    }

    abstract public function canBePurchased(Session $session): bool;

    abstract public function onPurchase(Session $session): bool;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\tracker;


use sergittos\bedwars\game\shop\Category;
use sergittos\bedwars\game\shop\Shop;

class TrackerShop extends Shop {

    public function getId(): string {
        return Shop::TRACKER;
    }

    /**
     * @return Category[]
     */
    public function getCategories(): array {
        return [
            new TrackerCategory()
        ];
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\tracker;


use sergittos\bedwars\game\shop\Category;
use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\session\Session;
use function array_map;

class TrackerCategory extends Category {

    public function __construct() {
        parent::__construct("Tracker Shop");
    }

    /**
     * @return TrackerProduct[]
     */
    public function getProducts(Session $session): array {
        return array_map(function(Team $team) {
            return new TrackerProduct($name = $team->getName(), "Track Team " . $name);
        }, $session->getGame()->getTeams());
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\tracker;


use pocketmine\item\VanillaItems;
use sergittos\bedwars\game\shop\Product;
use sergittos\bedwars\session\Session;
use function array_rand;

class TrackerProduct extends Product {

    public function __construct(string $id, string $name) {
        parent::__construct($id, $name, 2, VanillaItems::EMERALD());
    }

    public function onPurchase(Session $session): bool {
        $tracking_team = null;

        $teams = $session->getGame()->getTeams();
        foreach($teams as $team) {
            if($team->getName() === $this->id) {
                $tracking_team = $team;
            }

            if(!$team->isBedDestroyed()) {
                $session->message("{RED}Not all enemy beds are destroyed yet!");
                return false;
            }
        }

        if($tracking_team === null) { // this should never happen
            return false;
        } elseif(!$tracking_team->isAlive()) {
            $session->message("{RED}You can't track an eliminated team!");
            return false;
        }

        $members = $tracking_team->getMembers();
        $session->setTrackingSession($members[array_rand($members)]);
        return true;
    }

    public function canBePurchased(Session $session): bool {
        return true;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\upgrades;


use sergittos\bedwars\game\shop\Category;
use sergittos\bedwars\game\shop\Shop;
use sergittos\bedwars\game\shop\upgrades\category\TrapsCategory;
use sergittos\bedwars\game\shop\upgrades\category\UpgradesCategory;

class UpgradesShop extends Shop {

    public function getId(): string {
        return Shop::UPGRADES;
    }

    /**
     * @return Category[]
     */
    public function getCategories(): array {
        return [
            new UpgradesCategory(),
            new TrapsCategory()
        ];
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\upgrades;


use pocketmine\item\VanillaItems;
use sergittos\bedwars\game\shop\Product;
use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\session\Session;

abstract class UpgradesProduct extends Product {

    public function __construct(string $name, int $price) {
        parent::__construct($name, $name, $price, VanillaItems::DIAMOND());
    }

    public function onPurchase(Session $session): bool {
        if(!$session->hasTeam()) {
            return false;
        }
        $team = $session->getTeam();

        if(!$this->canBePurchased($session)) {
            $session->message("{RED}You already have this upgrade!");
            return false;
        }
        $this->purchase($team);
        return true;
    }

    abstract protected function purchase(Team $team): void;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\upgrades\product;


use pocketmine\utils\TextFormat;
use sergittos\bedwars\game\shop\upgrades\UpgradesProduct;
use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\GameUtils;

class TrapProduct extends UpgradesProduct {

    public function canBePurchased(Session $session): bool {
        $upgrades = $session->getTeam()->getUpgrades();
        return !$upgrades->hasTrap($this->name) and $upgrades->getTrapsCount() < 3;
    }

    protected function purchase(Team $team): void {
        $team->getUpgrades()->addTrap(GameUtils::getTrapByName($this->name));
    }

    public function getDescription(Session $session): string {
        $upgrades = $session->getTeam()->getUpgrades();
        if($upgrades->hasTrap($this->name)) {
            return TextFormat::RED . "You already have this trap!";
        }
        if($upgrades->getTrapsCount() >= 3) {
            return TextFormat::RED . "You cannot have more than 3 traps!";
        }
        return parent::getDescription($session);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\upgrades\product;


use pocketmine\utils\TextFormat;
use sergittos\bedwars\game\shop\upgrades\UpgradesProduct;
use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\game\team\upgrade\Upgrade;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\GameUtils;

class UpgradeProduct extends UpgradesProduct {

    public function canBePurchased(Session $session): bool {
        return $this->getUpgrade($session->getTeam())->canLevelUp();
    }

    protected function purchase(Team $team): void {
        $this->getUpgrade($team)->levelUp($team);
    }

    protected function canPurchase(Team $team): bool {
        $upgrade = $this->getUpgrade($team);
        if($upgrade->canLevelUp()) {
            $upgrade->levelUp($team);
            return true;
        }
        return false;
    }

    public function getDisplayName(Session $session): string {
        $upgrade = $this->getUpgrade($session->getTeam());
        $level = $upgrade->getLevel();
        $levels = $upgrade->getLevels();

        $name = parent::getDisplayName($session);
        if($levels > 1 and $level !== $levels) {
            $name .= " " . GameUtils::intToRoman($level + 1);
        }
        return $name;
    }

    public function getDescription(Session $session): string {
        if(!$this->getUpgrade($session->getTeam())->canLevelUp()) {
            return TextFormat::RED . "You already have this upgrade!";
        }
        return parent::getDescription($session);
    }

    private function getUpgrade(Team $team): Upgrade {
        return $team->getUpgrades()->getUpgrade($this->name);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\upgrades\category;


use sergittos\bedwars\game\shop\Category;
use sergittos\bedwars\game\shop\upgrades\product\UpgradeProduct;
use sergittos\bedwars\game\shop\upgrades\UpgradesProduct;
use sergittos\bedwars\session\Session;

class UpgradesCategory extends Category {

    public function __construct() {
        parent::__construct("Upgrades");
    }

    /**
     * @return UpgradesProduct[]
     */
    public function getProducts(Session $session): array {
        $upgrades = $session->getTeam()->getUpgrades();
        return [
            new UpgradeProduct("Sharpened Swords", 4),
            new UpgradeProduct("Armor Protection", 2 ** ($upgrades->getArmorProtection()->getLevel() + 1)),
            new UpgradeProduct("Maniac Miner", 2 ** ($upgrades->getManiacMiner()->getLevel() + 1)),
            new UpgradeProduct("Iron Forge", 2 * ($upgrades->getIronForge()->getLevel() + 1)),
            new UpgradeProduct("Heal Pool", 1)
        ];
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\upgrades\category;


use sergittos\bedwars\game\shop\Category;
use sergittos\bedwars\game\shop\upgrades\product\TrapProduct;
use sergittos\bedwars\game\shop\upgrades\UpgradesProduct;
use sergittos\bedwars\game\team\Upgrades;
use sergittos\bedwars\session\Session;

class TrapsCategory extends Category {

    public function __construct() {
        parent::__construct("Traps");
    }

    public function getProducts(Session $session): array {
        $upgrades = $session->getTeam()->getUpgrades();
        return [
            $this->createTrapProduct("It's a trap", $upgrades),
            $this->createTrapProduct("Counter-Offensive Trap", $upgrades),
            $this->createTrapProduct("Alarm Trap", $upgrades),
            $this->createTrapProduct("Miner Fatigue Trap", $upgrades)
        ];
    }

    private function createTrapProduct(string $name, Upgrades $upgrades): UpgradesProduct {
        return new TrapProduct($name, $upgrades->getTrapsCount() + 1);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\item;


use sergittos\bedwars\game\shop\Category;
use sergittos\bedwars\game\shop\item\category\ArmorCategory;
use sergittos\bedwars\game\shop\item\category\BlocksCategory;
use sergittos\bedwars\game\shop\item\category\MeleeCategory;
use sergittos\bedwars\game\shop\item\category\MiscCategory;
use sergittos\bedwars\game\shop\item\category\PotionsCategory;
use sergittos\bedwars\game\shop\item\category\RangedCategory;
use sergittos\bedwars\game\shop\item\category\ToolsCategory;
use sergittos\bedwars\game\shop\Shop;

class ItemShop extends Shop {

    public function getId(): string {
        return Shop::ITEM;
    }

    /**
     * @return Category[]
     */
    public function getCategories(): array {
        return [
            new BlocksCategory(),
            new MeleeCategory(),
            new ArmorCategory(),
            new ToolsCategory(),
            new RangedCategory(),
            new PotionsCategory(),
            new MiscCategory()
        ];
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\item;


use Closure;
use pocketmine\block\Block;
use pocketmine\item\Item;
use pocketmine\utils\TextFormat;
use sergittos\bedwars\game\shop\Product;
use sergittos\bedwars\session\Session;

class ItemProduct extends Product {

    private int $amount;

    private Item $item;
    private ?Closure $on_purchase;

    private bool $can_be_purchased;

    public function __construct(string $name, int $price, int $amount, Block|Item $item, Item $ore, ?Closure $on_purchase = null, bool $can_be_purchased = true) {
        $this->amount = $amount;
        $this->item = ($item instanceof Block ? $item->asItem() : $item)->setCount($amount);
        $this->on_purchase = $on_purchase;
        $this->can_be_purchased = $can_be_purchased;
        parent::__construct($name, $name, $price, $ore);
    }

    public function getItem(): Item {
        return clone $this->item;
    }

    public function canBePurchased(Session $session): bool {
        return $this->can_be_purchased;
    }

    public function getDisplayName(Session $session): string {
        $name = parent::getDisplayName($session);
        if(!$this->can_be_purchased) {
            $name = TextFormat::clean($name);
            $name = TextFormat::RED . $name;
        }
        if($this->amount > 1) {
            $name .= " x" . $this->amount;
        }
        return $name;
    }

    public function getDescription(Session $session): string {
        $description = parent::getDescription($session);
        if(!$this->can_be_purchased) {
            $description = TextFormat::RED . "You already have this product!";
        }
        return $description;
    }

    public function onPurchase(Session $session): bool {
        $inventory = $session->getPlayer()->getInventory();
        if(!$inventory->canAddItem($this->item)) {
            $session->message("{RED}Your inventory is full!");
            return false;
        }

        $this->executePurchaseListener($session);
        $inventory->addItem($this->item);
        return true;
    }

    private function executePurchaseListener(Session $session): void {
        if($this->on_purchase !== null) {
            ($this->on_purchase)($session);
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\item\category;


use pocketmine\item\Durable;
use pocketmine\item\enchantment\EnchantmentInstance;
use pocketmine\item\enchantment\VanillaEnchantments;
use pocketmine\item\Item;
use pocketmine\item\StringToItemParser;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\game\shop\Category;
use sergittos\bedwars\game\shop\item\ItemProduct;
use sergittos\bedwars\session\Session;
use function ucfirst;

class MeleeCategory extends Category {

    public function __construct() {
        parent::__construct("Melee");
    }

    /**
     * @return ItemProduct[]
     */
    public function getProducts(Session $session): array {
        return [
            $this->createMeleeProduct("stone", 10, VanillaItems::IRON_INGOT(), $session),
            $this->createMeleeProduct("iron", 7, VanillaItems::GOLD_INGOT(), $session),
            $this->createMeleeProduct("diamond", 4, VanillaItems::EMERALD(), $session),
            new ItemProduct(
                "Stick (Knockback I)", 5, 1,
                VanillaItems::STICK()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::KNOCKBACK())),
                VanillaItems::GOLD_INGOT()
            )
        ];
    }

    private function createMeleeProduct(string $name, int $price, Item $ore, Session $session): ItemProduct {
        $sword = StringToItemParser::getInstance()->parse($name . "_sword");
        if($sword instanceof Durable) {
            $sword->setUnbreakable();
        }
        if(!$session->getTeam()->getUpgrades()->getSharpenedSwords()->canLevelUp()) {
            $sword->addEnchantment(new EnchantmentInstance(VanillaEnchantments::SHARPNESS()));
        }

        return new ItemProduct(ucfirst($name) . " Sword", $price, 1, $sword, $ore, function(Session $session) {
            $session->getPlayer()->getInventory()->remove(VanillaItems::WOODEN_SWORD());
        });
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\item\category;


use pocketmine\item\PotionType;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\game\shop\Category;
use sergittos\bedwars\game\shop\item\ItemProduct;
use sergittos\bedwars\session\Session;

class PotionsCategory extends Category {

    public function __construct() {
        parent::__construct("Potions");
    }

    /**
     * @return ItemProduct[]
     */
    public function getProducts(Session $session): array {
        return [
            new ItemProduct("Speed II Potion (45 seconds)", 1, 1, VanillaItems::POTION()->setType(PotionType::SWIFTNESS()), VanillaItems::EMERALD()),
            new ItemProduct("Jump V Potion (45 seconds)", 1, 1, VanillaItems::POTION()->setType(PotionType::LEAPING()), VanillaItems::EMERALD()),
            new ItemProduct("Invisibility Potion (30 seconds)", 2, 1, VanillaItems::POTION()->setType(PotionType::INVISIBILITY()), VanillaItems::EMERALD())
        ];
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\item\category;


use pocketmine\item\enchantment\EnchantmentInstance;
use pocketmine\item\enchantment\VanillaEnchantments;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\game\shop\Category;
use sergittos\bedwars\game\shop\item\ItemProduct;
use sergittos\bedwars\session\Session;

class RangedCategory extends Category {

    public function __construct() {
        parent::__construct("Ranged");
    }

    /**
     * @return ItemProduct[]
     */
    public function getProducts(Session $session): array {
        $power = new EnchantmentInstance(VanillaEnchantments::POWER());
        return [
            new ItemProduct("Arrow", 2, 6, VanillaItems::ARROW(), VanillaItems::GOLD_INGOT()),
            new ItemProduct("Bow", 12, 1, VanillaItems::BOW(), VanillaItems::GOLD_INGOT()),
            new ItemProduct("Bow (Power I)", 20, 1, VanillaItems::BOW()->addEnchantment($power), VanillaItems::GOLD_INGOT()),
            new ItemProduct(
                "Bow (Power I, Punch I)",
                6, 1,
                VanillaItems::BOW()
                    ->addEnchantment($power)
                    ->addEnchantment(new EnchantmentInstance(VanillaEnchantments::PUNCH())),
                VanillaItems::EMERALD()
            ),

        ];
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\item\category;


use pocketmine\block\VanillaBlocks;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\game\shop\Category;
use sergittos\bedwars\game\shop\item\ItemProduct;
use sergittos\bedwars\session\Session;

class BlocksCategory extends Category {

    public function __construct() {
        parent::__construct("Blocks");
    }

    /**
     * @return ItemProduct[]
     */
    public function getProducts(Session $session): array {
        $color = $session->getTeam()->getDyeColor();
        return [
            new ItemProduct("Wool", 4, 16, VanillaBlocks::WOOL()->setColor($color), VanillaItems::IRON_INGOT()),
            new ItemProduct("Hardened Clay", 12, 16, VanillaBlocks::STAINED_CLAY()->setColor($color), VanillaItems::IRON_INGOT()),
            new ItemProduct("Blast-Proof Glass", 12, 4, VanillaBlocks::STAINED_GLASS()->setColor($color), VanillaItems::IRON_INGOT()),
            new ItemProduct("End Stone", 24, 12, VanillaBlocks::END_STONE(), VanillaItems::IRON_INGOT()),
            new ItemProduct("Ladder", 4, 8, VanillaBlocks::LADDER(), VanillaItems::IRON_INGOT()),
            new ItemProduct("Oak Wood Planks", 4, 16, VanillaBlocks::OAK_WOOD(), VanillaItems::GOLD_INGOT()),
            new ItemProduct("Obsidian", 4, 4, VanillaBlocks::OBSIDIAN(), VanillaItems::EMERALD())
        ];
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\item\category;


use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\game\shop\Category;
use sergittos\bedwars\game\shop\item\ItemProduct;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\settings\GameSettings;
use function ucfirst;

class ArmorCategory extends Category {

    public function __construct() {
        parent::__construct("Armor");
    }

    /**
     * @return ItemProduct[]
     */
    public function getProducts(Session $session): array {
        $settings = $session->getGameSettings();
        return [
            $this->createArmorProduct("chainmail", 30, VanillaItems::IRON_INGOT(), $settings),
            $this->createArmorProduct("iron", 12, VanillaItems::GOLD_INGOT(), $settings),
            $this->createArmorProduct("diamond", 6, VanillaItems::EMERALD(), $settings)
        ];
    }

    private function createArmorProduct(string $armor, int $price, Item $ore, GameSettings $settings): ItemProduct {
        return new ItemProduct("Permanent " . ucfirst($armor) . " Armor", $price, 0, VanillaItems::AIR(), $ore, function(Session $session) use ($armor) {
            $session->getGameSettings()->setArmor($armor);
        }, $this->getPriority($settings->getArmor()) < $this->getPriority($armor));
    }

    private function getPriority(?string $armor): int {
        return match($armor) {
            "chainmail" => 1,
            "iron" => 2,
            "diamond" => 3,
            default => 0
        };
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\item\category;


use pocketmine\block\VanillaBlocks;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\game\shop\Category;
use sergittos\bedwars\game\shop\item\ItemProduct;
use sergittos\bedwars\item\game\Fireball;
use sergittos\bedwars\session\Session;

class MiscCategory extends Category {

    public function __construct() {
        parent::__construct("Misc");
    }

    /**
     * @return ItemProduct[]
     */
    public function getProducts(Session $session): array {
        return [
            new ItemProduct("Golden Apple", 3, 1, VanillaItems::GOLDEN_APPLE(), VanillaItems::GOLD_INGOT()),
            new ItemProduct("Fireball", 40, 1, new Fireball(), VanillaItems::IRON_INGOT()),
            new ItemProduct("TNT", 3, 1, VanillaBlocks::TNT(), VanillaItems::GOLD_INGOT()),
            new ItemProduct("Ender Pearl", 1, 4, VanillaItems::ENDER_PEARL(), VanillaItems::EMERALD()),
            new ItemProduct("Water Bucket", 3, 1, VanillaItems::WATER_BUCKET(), VanillaItems::GOLD_INGOT()),
            new ItemProduct("Magic Milk", 4, 1, VanillaItems::MILK_BUCKET(), VanillaItems::GOLD_INGOT()),
            new ItemProduct("Sponge", 3, 4, VanillaBlocks::SPONGE(), VanillaItems::GOLD_INGOT())
        ];
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\shop\item\category;


use Closure;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use sergittos\bedwars\game\shop\Category;
use sergittos\bedwars\game\shop\item\ItemProduct;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\settings\GameSettings;
use sergittos\bedwars\utils\GameUtils;

class ToolsCategory extends Category {

    public function __construct() {
        parent::__construct("Tools");
    }

    /**
     * @return ItemProduct[]
     */
    public function getProducts(Session $session): array {
        $settings = $session->getGameSettings();
        return [
            new ItemProduct("Permanent Shears", 20, 1, VanillaItems::SHEARS(), VanillaItems::IRON_INGOT(), function(Session $session) {
                $session->getGameSettings()->setPermanentShears();
                return true;
            }),
            $this->createToolProduct("Pickaxe", $settings->getPickaxeTier(), $settings, $settings->isPickaxeFullUpgraded(), function() use ($settings) {
                $settings->incrasePickaxeTier();
            }),
            $this->createToolProduct("Axe", $settings->getAxeTier(), $settings, $settings->isAxeFullUpgraded(), function() use ($settings) {
                $settings->incraseAxeTier();
            })
        ];
    }

    private function createToolProduct(string $name, int $tier, GameSettings $settings, bool $is_full_upgraded, Closure $on_purchase): ItemProduct {
        $tier++;
        return new ItemProduct(
            $settings->getMaterial($tier) . " $name " . ($tier <= 4 ? "Tier " . GameUtils::intToRoman($tier) : "MAX TIER"),
            $this->getPrice($tier), 1, $settings->getTool($name, $tier), $this->getOre($tier), function(Session $session) use ($name, $tier, $settings, $is_full_upgraded, $on_purchase) {
                $session->getPlayer()->getInventory()->remove($settings->getTool($name, $tier - 1));
                $on_purchase();
        }, !$is_full_upgraded);
    }

    private function getPrice(int $tier): int {
        return match($tier) {
            1, 2 => 10,
            3 => 3,
            4 => 6,
            5 => 0
        };
    }

    private function getOre(int $tier): Item {
        return match($tier) {
            1, 2 => VanillaItems::IRON_INGOT(),
            3, 4 => VanillaItems::GOLD_INGOT(),
            5 => VanillaItems::AIR()
        };
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\map;


use pocketmine\math\Vector3;
use pocketmine\Server;
use pocketmine\utils\ServerException;
use sergittos\bedwars\BedWars;
use sergittos\bedwars\game\generator\GeneratorType;
use sergittos\bedwars\game\generator\presets\GoldGenerator;
use sergittos\bedwars\game\generator\presets\IronGenerator;
use sergittos\bedwars\game\team\Area;
use sergittos\bedwars\game\team\Team;
use function array_filter;
use function array_map;
use function file_get_contents;
use function json_decode;
use function strtolower;
use function strtoupper;
use function ucfirst;

class MapFactory {

    /** @var Map[] */
    static private array $maps = [];

    static public function init(): void {
        foreach(json_decode(file_get_contents(BedWars::getInstance()->getDataFolder() . "maps.json"), true) as $map_data) {
            $name = $map_data["name"];
            $world_name = $map_data["waiting_world"];
            $position_data = $map_data["spectator_spawn_position"];
            $players_per_team = $map_data["players_per_team"];
            $capacity = $map_data["max_capacity"];

            $world_manager = Server::getInstance()->getWorldManager();
            if(!$world_manager->loadWorld($world_name)) {
                throw new ServerException("Couldn't load " . $name . " map because the world doesn't exist. (" . $world_name . ")");
            }

            $waiting_world = $world_manager->getWorldByName($world_name);
            $spectator_spawn_position = new Vector3($position_data["x"], $position_data["y"], $position_data["z"]);

            $generators = [];
            foreach($map_data["generators"] as $type => $generator_data) {
                foreach($generator_data as $position) {
                    $generators[] = GeneratorType::toGenerator(self::createVector($position), GeneratorType::fromString($type));
                }
            }

            $shop_locations = self::createPositions($map_data["shop_positions"]);
            $upgrades_locations = self::createPositions($map_data["upgrades_positions"]);

            $teams = [];
            foreach($map_data["teams"] as $data) {
                $generator_data = $data["generator"];
                $areas_data = $data["areas"];
                $bed_data = $data["bed"];

                $team_generators = [];
                $team_generators[] = new IronGenerator(self::createVector($generator_data));
                $team_generators[] = new GoldGenerator(self::createVector($generator_data));

                $teams[] = new Team(
                    ucfirst($data["name"]), $players_per_team,
                    self::createVector($data["spawn_point"]), new Vector3($bed_data["x"], $bed_data["y"] + 1, $bed_data["z"]),
                    Area::fromData($areas_data["zone"]), Area::fromData($areas_data["claim"]), $team_generators
                );
            }

            self::addMap(new Map(
                $name, $spectator_spawn_position, $players_per_team,
                $capacity, $waiting_world, $generators, $teams,
                $shop_locations, $upgrades_locations
            ));
        }
    }

    /**
     * @return Map[]
     */
    static public function getMaps(): array {
        return self::$maps;
    }

    /**
     * @return Map[]
     */
    static public function getMapsByPlayers(int $players_per_team): array {
        return array_filter(self::$maps, function(Map $map) use ($players_per_team) {
            return $map->getPlayersPerTeam() === $players_per_team;
        });
    }

    /**
     * @return Map[]
     */
    static public function getMapsByName(string $name): array {
        $maps = [];
        foreach(self::$maps as $map) {
            if(strtolower($map->getName()) === strtolower($name)) {
                $maps[] = $map;
            }
        }
        return $maps;
    }

    static public function getMapByName(string $name): ?Map {
        foreach(self::$maps as $map) {
            if(strtolower($map->getName()) === strtolower($name)) {
                return $map;
            }
        }
        return null;
    }

    static public function getMapById(string $id): ?Map {
        return self::$maps[$id] ?? null;
    }

    static public function addMap(Map $map): void {
        self::$maps[$map->getId()] = $map;
    }

    static public function removeMap(string $id): void {
        unset(self::$maps[$id]);
    }

    static private function createVector(array $data): Vector3 {
        return new Vector3($data["x"] + 0.5, $data["y"] + 1.5, $data["z"] + 0.5);
    }

    static private function createPositions(array $positions): array {
        return array_map(function(array $data) {
            return self::createVector($data);
        }, $positions);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\map\task;


use pocketmine\scheduler\AsyncTask;
use pocketmine\utils\Filesystem;
use sergittos\bedwars\BedWars;
use sergittos\bedwars\game\map\Map;
use sergittos\bedwars\game\map\MapFactory;
use function file_get_contents;
use function file_put_contents;
use function json_decode;
use function json_encode;

class DeleteMapTask extends AsyncTask {

    private string $world_path;

    public function __construct(Map $map) {
        $this->storeLocal("map", $map);
        $this->world_path = BedWars::getInstance()->getDataFolder() . "worlds/" . $map->getName();
    }

    public function onRun(): void {
        Filesystem::recursiveUnlink($this->world_path);
    }

    public function onCompletion(): void { // TODO: Do not reuse code
        $map = $this->fetchLocal("map");

        $plugin = BedWars::getInstance();
        $path = $plugin->getDataFolder() . "maps.json";

        $data = json_decode(file_get_contents($path), true);
        $data = array_filter($data, function(array $map_data) use ($map): bool {
            return $map_data["name"] !== $map->getName();
        });

        file_put_contents($path, json_encode($data, JSON_THROW_ON_ERROR));

        MapFactory::removeMap($map->getId());
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\map\task;


use pocketmine\scheduler\AsyncTask;
use pocketmine\Server;
use pocketmine\utils\Filesystem;
use sergittos\bedwars\BedWars;
use sergittos\bedwars\game\map\MapFactory;
use sergittos\bedwars\session\setup\builder\MapBuilder;
use function file_get_contents;
use function file_put_contents;

class CreateMapTask extends AsyncTask {

    private string $world_path;
    private string $destination_path;

    public function __construct(MapBuilder $map) {
        $this->storeLocal("map", $map);

        $this->world_path = Server::getInstance()->getDataPath() . "worlds/" . $map->getPlayingWorld();
        $this->destination_path = BedWars::getInstance()->getDataFolder() . "worlds/" . $map->getName();

        $world_manager = Server::getInstance()->getWorldManager();
        $world_manager->unloadWorld($world_manager->getWorldByName($map->getPlayingWorld()));
    }

    public function onRun(): void {
        Filesystem::recursiveCopy($this->world_path, $this->destination_path);
    }

    public function onCompletion(): void { // TODO: Do not reuse code
        $map = $this->fetchLocal("map")->build();

        $plugin = BedWars::getInstance();
        $path = $plugin->getDataFolder() . "maps.json";

        $data = json_decode(file_get_contents($path), true);
        $data[] = $map->jsonSerialize();

        file_put_contents($path, json_encode($data, JSON_THROW_ON_ERROR));

        MapFactory::addMap($map);
        $plugin->getGameManager()->generateGames($map);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\map;


use pocketmine\math\Vector3;
use pocketmine\world\World;
use sergittos\bedwars\game\generator\Generator;

trait MapProperties {

    protected string $id;
    protected string $name;

    protected Vector3 $spectator_spawn_position;

    protected int $players_per_team; // TODO: Make an enum for this
    protected int $max_capacity; // slots?

    protected World $waiting_world;

    /** @var Vector3[] */
    protected array $shop_positions;

    /** @var Vector3[] */
    protected array $upgrades_positions;

    /** @var Generator[] */
    protected array $generators;

    public function getId(): string {
        return $this->id;
    }

    public function getName(): string {
        return $this->name;
    }

    public function getSpectatorSpawnPosition(): Vector3 {
        return $this->spectator_spawn_position;
    }

    public function getPlayersPerTeam(): int {
        return $this->players_per_team;
    }

    public function getMaxCapacity(): int {
        return $this->max_capacity;
    }

    public function getWaitingWorld(): World {
        return $this->waiting_world;
    }

    /**
     * @return Generator[]
     */
    public function getGenerators(): array {
        return $this->generators ?? [];
    }

    /**
     * @return Vector3[]
     */
    public function getShopPositions(): array {
        return $this->shop_positions;
    }

    /**
     * @return Vector3[]
     */
    public function getUpgradesPositions(): array {
        return $this->upgrades_positions;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\map;


use JsonSerializable;
use pocketmine\math\Vector3;
use pocketmine\world\World;
use sergittos\bedwars\game\generator\Generator;
use sergittos\bedwars\game\generator\GeneratorType;
use sergittos\bedwars\game\team\Team;
use function array_map;
use function uniqid;

class Map implements JsonSerializable {
    use MapProperties;

    /** @var Team[] */
    private array $teams;

    /**
     * @param Generator[] $generators
     * @param Team[] $teams
     */
    public function __construct(string $name, Vector3 $spectator_spawn_position, int $players_per_team, int $max_capacity, World $waiting_world, array $generators, array $teams, array $shop_locations, array $upgrades_locations) {
        $this->id = uniqid("map-");
        $this->name = $name;
        $this->spectator_spawn_position = $spectator_spawn_position;
        $this->players_per_team = $players_per_team;
        $this->max_capacity = $max_capacity;
        $this->waiting_world = $waiting_world;
        $this->generators = $generators;
        $this->teams = $teams;
        $this->shop_positions = $shop_locations;
        $this->upgrades_positions = $upgrades_locations;
    }

    /**
     * @return Team[]
     */
    public function getTeams(): array {
        return $this->teams;
    }

    public function jsonSerialize(): array {
        return [
            "name" => $this->name,
            "waiting_world" => $this->waiting_world->getFolderName(),
            "spectator_spawn_position" => [
                "x" => $this->spectator_spawn_position->getX(),
                "y" => $this->spectator_spawn_position->getY(),
                "z" => $this->spectator_spawn_position->getZ()
            ],
            "players_per_team" => $this->players_per_team,
            "max_capacity" => $this->max_capacity,
            "generators" => [
                "diamond" => $this->jsonSerializePositions($this->getGeneratorPositions(GeneratorType::DIAMOND)),
                "emerald" => $this->jsonSerializePositions($this->getGeneratorPositions(GeneratorType::EMERALD))
            ],
            "teams" => array_map(function(Team $team) {
                return $team->jsonSerialize();
            }, $this->teams),
            "shop_positions" => $this->jsonSerializePositions($this->shop_positions),
            "upgrades_positions" => $this->jsonSerializePositions($this->upgrades_positions)
        ];
    }

    private function getGeneratorPositions(GeneratorType $type): array {
        $positions = [];
        foreach($this->generators as $generator) {
            if($generator->getType() === $type) {
                $positions[] = $generator->getPosition();
            }
        }
        return $positions;
    }

    private function jsonSerializePositions(array $positions): array {
        return array_map(function(Vector3 $position) {
            return [
                "x" => $position->getX(),
                "y" => $position->getY(),
                "z" => $position->getZ()
            ];
        }, $positions);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\event\presets;


use sergittos\bedwars\game\event\Event;
use sergittos\bedwars\game\generator\GeneratorType;
use sergittos\bedwars\game\generator\presets\TextGenerator;
use sergittos\bedwars\game\generator\Tier;
use sergittos\bedwars\utils\GameUtils;

class UpgradeGeneratorsTierEvent extends Event {

    private GeneratorType $type;
    private Tier $tier;

    public function __construct(GeneratorType $type, Tier $tier) {
        $this->type = $type;
        $this->tier = $tier;
        parent::__construct($type->toString() . " " . $tier->name, 6);
    }

    public function end(): void {
        foreach($this->game->getGenerators() as $generator) {
            if($generator->getType() !== $this->type) {
                continue;
            }

            $generator->setTier($this->tier);

            if($generator instanceof TextGenerator) {
                $generator->updateText($this->game->getWorld());
            }
        }
        $this->game->broadcastMessage(GameUtils::getGeneratorColor($name = $this->type->toString()) . $name . " Generators {YELLOW}have been upgraded to Tier {RED}" . $this->tier->name);
    }

    public function getNextEvent(): ?Event {
        $next_type = match($this->type) {
            GeneratorType::DIAMOND => GeneratorType::EMERALD,
            GeneratorType::EMERALD => GeneratorType::DIAMOND
        };

        $next_tier = $this->getNextTier($next_type);

        if($next_tier === null) {
            return new BedDestructionEvent();
        }

        return new UpgradeGeneratorsTierEvent($next_type, $next_tier);
    }

    private function getNextTier(GeneratorType $type): ?Tier {
        if($type === GeneratorType::DIAMOND) {
            return match($this->tier) {
                Tier::I => Tier::II,
                Tier::II => Tier::III,
                Tier::III => null
            };
        }

        return $this->tier;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\event\presets;


use sergittos\bedwars\game\event\Event;
use sergittos\bedwars\game\stage\EndingStage;

class EndGameInATieEvent extends Event {

    public function __construct() {
        parent::__construct("Tie", 10);
    }

    public function end(): void {
        $this->game->setStage(new EndingStage());
    }

    public function getNextEvent(): ?Event {
        return null;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\event\presets;


use sergittos\bedwars\game\event\Event;

class BedDestructionEvent extends Event {

    public function __construct() {
        parent::__construct("Bed destruction", 6);
    }

    public function end(): void {
        foreach($this->game->getTeams() as $team) {
            if(!$team->isBedDestroyed()) {
                $team->destroyBed($this->game);
            }
        }
        $this->game->updateScoreboards();
    }

    public function getNextEvent(): Event {
        return new EndGameInATieEvent();
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\event;


use sergittos\bedwars\game\Game;
use function time;

abstract class Event {

    protected Game $game;

    protected string $name;

    private int $duration;
    private int $start_time;

    public function __construct(string $name, int $duration) {
        $this->name = $name;
        $this->duration = $duration * 60;
    }

    public function getName(): string {
        return $this->name;
    }

    private function getTimeElapsed(): int {
        return time() - $this->start_time;
    }

    public function getTimeRemaining(): int {
        return $this->duration - $this->getTimeElapsed();
    }

    public function hasEnded(): bool {
        if($this->getTimeElapsed() >= $this->duration) {
            $this->end();
            return true;
        }
        return false;
    }

    public function start(Game $game): void {
        $this->game = $game;
        $this->start_time = time();
    }

    abstract protected function end(): void;

    abstract public function getNextEvent(): ?Event;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\team;


use pocketmine\utils\Utils;
use sergittos\bedwars\game\team\upgrade\presets\ArmorProtection;
use sergittos\bedwars\game\team\upgrade\presets\HealPool;
use sergittos\bedwars\game\team\upgrade\presets\IronForge;
use sergittos\bedwars\game\team\upgrade\presets\ManiacMiner;
use sergittos\bedwars\game\team\upgrade\presets\SharpenedSwords;
use sergittos\bedwars\game\team\upgrade\trap\Trap;
use sergittos\bedwars\game\team\upgrade\Upgrade;
use sergittos\bedwars\session\Session;
use function array_key_exists;
use function array_shift;
use function count;
use function time;

class Upgrades {

    private ArmorProtection $armor_protection;
    private ManiacMiner $maniac_miner;
    private IronForge $iron_forge;
    private SharpenedSwords $sharpened_swords;
    private HealPool $heal_pool;

    private int $trap_trigger_time = 0;

    /** @var Trap[] */
    private array $traps = [];

    public function __construct() {
        $this->armor_protection = new ArmorProtection();
        $this->maniac_miner = new ManiacMiner();
        $this->iron_forge = new IronForge();
        $this->sharpened_swords = new SharpenedSwords();
        $this->heal_pool = new HealPool();
    }

    /**
     * @return Upgrade[]
     */
    public function getAll(): array {
        return [$this->armor_protection, $this->maniac_miner, $this->iron_forge, $this->sharpened_swords, $this->heal_pool];
    }

    public function getArmorProtection(): ArmorProtection {
        return $this->armor_protection;
    }

    public function getManiacMiner(): ManiacMiner {
        return $this->maniac_miner;
    }

    public function getIronForge(): IronForge {
        return $this->iron_forge;
    }

    public function getSharpenedSwords(): SharpenedSwords {
        return $this->sharpened_swords;
    }

    public function getHealPool(): HealPool {
        return $this->heal_pool;
    }

    public function getUpgrade(string $name): ?Upgrade {
        foreach($this->getAll() as $upgrade) {
            if($upgrade->getName() === $name) {
                return $upgrade;
            }
        }
        return null;
    }

    public function canTriggerTrap(): bool {
        if(time() - $this->trap_trigger_time >= 30 and !empty($this->traps)) {
            $this->trap_trigger_time = time();
            return true;
        }
        return false;
    }

    /**
     * @return Trap[]
     */
    public function getTraps(): array {
        return $this->traps;
    }

    public function getTrapsCount(): int {
        return count($this->traps);
    }

    public function hasTrap(string $name): bool {
        return array_key_exists($name, $this->traps);
    }

    public function addTrap(Trap $trap): void {
        $this->traps[$trap->getName()] = $trap;
    }

    public function triggerPrimaryTrap(Session $session, Team $team): void {
        $trap = array_shift($this->traps);
        $trap->trigger($session, $team);

        $team->notifyTrap($trap, $session->getTeam());
    }

    public function __clone(): void {
        $this->traps = Utils::cloneObjectArray($this->traps);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\team;


use JsonSerializable;
use pocketmine\block\Air;
use pocketmine\item\VanillaItems;
use pocketmine\math\Vector3;
use pocketmine\player\GameMode;
use pocketmine\utils\Utils;
use pocketmine\world\format\Chunk;
use pocketmine\world\Position;
use sergittos\bedwars\game\Game;
use sergittos\bedwars\game\generator\Generator;
use sergittos\bedwars\game\generator\GeneratorType;
use sergittos\bedwars\game\team\upgrade\trap\AlarmTrap;
use sergittos\bedwars\game\team\upgrade\trap\Trap;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\ColorUtils;
use function array_search;
use function count;
use function in_array;
use function strtoupper;

class Team implements JsonSerializable {
    use TeamProperties;

    private int $capacity;

    private Upgrades $upgrades;

    private bool $bed_destroyed = false;

    /** @var Generator[] */
    private array $generators;

    /** @var Session[] */
    private array $members = [];

    /**
     * @param Generator[] $generators
     */
    public function __construct(string $name, int $capacity, Vector3 $spawn_point, Vector3 $bed_position, Area $zone, Area $claim, array $generators) {
        $this->name = $name;
        $this->color = ColorUtils::translate("{" . strtoupper($name) . "}");
        $this->capacity = $capacity;
        $this->spawn_point = $spawn_point;
        $this->bed_position = $bed_position;
        $this->zone = $zone;
        $this->claim = $claim;
        $this->generators = $generators;
        $this->upgrades = new Upgrades();
    }

    public function getColoredName(): string {
        return $this->color . $this->name;
    }

    public function getFirstLetter(): string {
        return $this->name[0];
    }

    public function getUpgrades(): Upgrades {
        return $this->upgrades;
    }

    public function isBedDestroyed(): bool {
        return $this->bed_destroyed;
    }

    /**
     * @return Generator[]
     */
    public function getGenerators(): array {
        return $this->generators;
    }

    /**
     * @return Session[]
     */
    public function getMembers(): array {
        return $this->members;
    }

    public function getMembersCount(): int {
        return count($this->members);
    }

    public function isFull(): bool {
        return $this->getMembersCount() >= $this->capacity;
    }

    public function isEmpty(): bool {
        return empty($this->members);
    }

    public function isAlive(): bool {
        return !$this->isEmpty();
    }

    public function hasMember(Session $session): bool {
        return in_array($session, $this->members, true);
    }

    public function destroyBed(Game $game, bool $break_block = true, bool $silent = false): void {
        $this->bed_destroyed = true;

        if($break_block) {
            $this->breakBedBlock($game);
        }

        if(!$silent) {
            foreach($game->getPlayersAndSpectators() as $session) {
                if(!$session->hasTeam() or $session->getTeam()->getName() !== $this->name) {
                    $session->playSound("mob.enderdragon.growl");
                }
            }

            foreach($this->members as $member) {
                $member->title("{RED}BED DESTROYED!", "{WHITE}You will no longer respawn!", 7, 30, 15);
                $member->playSound("mob.wither.death");
            }
        }
    }

    private function breakBedBlock(Game $game): void {
        $world = $game->getWorld();
        $position = Position::fromObject($this->bed_position, $world);

        $world->requestChunkPopulation($position->getX() >> Chunk::COORD_BIT_SIZE, $position->getZ() >> Chunk::COORD_BIT_SIZE, null)->onCompletion(
            function() use ($world, $position) {
                $blocks = $world->getBlock($position)->getAffectedBlocks();
                foreach($blocks as $block) {
                    if($block instanceof Air) {
                        continue;
                    }

                    $block->onBreak(VanillaItems::AIR());
                }
            },
            function() {}
        );
    }

    public function tickGenerators(Game $game): void {
        if($this->isAlive()) {
            foreach($this->generators as $generator) {
                $generator->tick($game);
            }
        }
    }

    public function addGenerator(Generator $generator): void {
        $this->generators[] = $generator;
    }

    private function removeEmeraldGenerator(): void {
        foreach($this->generators as $index => $generator) {
            if($generator->getType() === GeneratorType::TEAM_EMERALD) {
                unset($this->generators[$index]);
                break;
            }
        }
    }

    public function addMember(Session $session): void {
        $this->members[] = $session;

        $session->setTeam($this);
        $session->clearAllInventories();
        $session->getGameSettings()->apply();

        $player = $session->getPlayer();
        $player->setNameTag($this->color . $this->getFirstLetter() . " " . $player->getName());
        $player->setGamemode(GameMode::SURVIVAL());
        $player->teleport(Position::fromObject($this->spawn_point, $session->getGame()->getWorld()));
    }

    public function removeMember(Session $session): void {
        unset($this->members[array_search($session, $this->members, true)]);

        if($this->isEmpty()) {
            $this->destroyBed($session->getGame());
        }

        $session->setTeam(null);
    }

    public function notifyTrap(Trap $trap, Team $team): void {
        $name = $trap->getName();
        if($trap instanceof AlarmTrap) {
            $title = "{BOLD}{RED}ALARM!!!";
            $subtitle = "{WHITE}" . $name . " set off by " . $team->getColoredName() . "{WHITE} team!";
            $message = "{BOLD}{RED}" . $name . " set off by " . $team->getColoredName() . "{RED} team!";
        } else {
            $title = "{RED}TRAP TRIGGERED!";
            $subtitle = "{WHITE}Your $name has been set off!";
            $message = "{BOLD}{RED}" . $name . " was set off!";
        }

        foreach($this->members as $member) {
            $member->title($title, $subtitle);
            $member->message($message);
            // TODO: Play sound
        }
    }

    public function reset(): void {
        $this->bed_destroyed = false;
        $this->upgrades = new Upgrades();

        $this->removeEmeraldGenerator();

        foreach($this->generators as $generator) {
            $generator->reset();
        }
        foreach($this->members as $member) {
            $member->setTeam(null);
        }
        $this->members = [];
    }

    public function jsonSerialize(): array {
        return [
            "name" => $this->name,
            "spawn_point" => [
                "x" => $this->spawn_point->getX(),
                "y" => $this->spawn_point->getY(),
                "z" => $this->spawn_point->getZ()
            ],
            "bed" => [
                "x" => $this->bed_position->getX(),
                "y" => $this->bed_position->getY(),
                "z" => $this->bed_position->getZ()
            ],
            "generator" => [
                "x" => $this->generators[0]->getPosition()->getX(),
                "y" => $this->generators[0]->getPosition()->getY(),
                "z" => $this->generators[0]->getPosition()->getZ()
            ],
            "areas" => [
                "zone" => $this->zone->jsonSerialize(),
                "claim" => $this->claim->jsonSerialize()
            ]
        ];
    }

    public function __clone(): void {
        $this->upgrades = clone $this->upgrades;
        $this->generators = Utils::cloneObjectArray($this->generators);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\team;


use JsonSerializable;
use pocketmine\math\Vector3;
use function max;
use function min;

class Area implements JsonSerializable {


    private Vector3 $first_vector;
    private Vector3 $second_vector;

    public function __construct(Vector3 $first_vector, Vector3 $second_vector) {
        $this->first_vector = $first_vector->floor();
        $this->second_vector = $second_vector->floor();
    }

    static public function fromData(array $data): Area {
        return new Area(
            new Vector3($data["first_x"], $data["first_y"], $data["first_z"]),
            new Vector3($data["second_x"], $data["second_y"], $data["second_z"])
        );
    }

    public function getMinX(): int {
        return min($this->first_vector->getFloorX(), $this->second_vector->getFloorX());
    }

    public function getMinY(): int {
        return min($this->first_vector->getFloorY(), $this->second_vector->getFloorY());
    }

    public function getMinZ(): int {
        return min($this->first_vector->getFloorZ(), $this->second_vector->getFloorZ());
    }

    public function getMaxX(): int {
        return max($this->first_vector->getFloorX(), $this->second_vector->getFloorX());
    }

    public function getMaxY(): int {
        return max($this->first_vector->getFloorY(), $this->second_vector->getFloorY());
    }

    public function getMaxZ(): int {
        return max($this->first_vector->getFloorZ(), $this->second_vector->getFloorZ());
    }

    public function isInside(Vector3 $position): bool {
        return $position->x >= $this->getMinX() && $position->x <= $this->getMaxX() and
            $position->z >= $this->getMinZ() && $position->z <= $this->getMaxZ();
    }

    public function jsonSerialize(): array {
        return [
            "first_x" => $this->first_vector->getFloorX(),
            "first_y" => $this->first_vector->getFloorY(),
            "first_z" => $this->first_vector->getFloorZ(),
            "second_x" => $this->second_vector->getFloorX(),
            "second_y" => $this->second_vector->getFloorY(),
            "second_z" => $this->second_vector->getFloorZ()
        ];
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\team\upgrade;


use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\session\Session;

abstract class Upgrade {

    protected int $level = 0;

    public function getLevel(): int {
        return $this->level;
    }

    public function canLevelUp(): bool {
        return $this->level < $this->getLevels();
    }

    public function levelUp(Team $team): void {
        $this->level++;

        $this->internalLevelUp($team);
        foreach($team->getMembers() as $session) {
            $this->applySession($session);
        }
    }

    public function applySession(Session $session): void {
        if($this->level > 0) {
            $this->internalApplySession($session);
        }
    }

    protected function internalLevelUp(Team $team): void {}

    protected function internalApplySession(Session $session): void {}

    abstract public function getName(): string;

    abstract public function getLevels(): int;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\team\upgrade\presets;


use pocketmine\item\enchantment\EnchantmentInstance;
use pocketmine\item\enchantment\VanillaEnchantments;
use sergittos\bedwars\game\team\upgrade\Upgrade;
use sergittos\bedwars\session\Session;

class ArmorProtection extends Upgrade {

    public function getName(): string {
        return "Armor Protection";
    }

    public function getLevels(): int {
        return 4;
    }

    public function internalApplySession(Session $session): void {
        $inventory = $session->getPlayer()->getArmorInventory();
        foreach($inventory->getContents() as $index => $item) {
            $item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::PROTECTION(), $this->level));
            $inventory->setItem($index, $item);
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\team\upgrade\presets;


use sergittos\bedwars\game\generator\presets\TeamEmeraldGenerator;
use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\game\team\upgrade\Upgrade;
use function array_rand;

class IronForge extends Upgrade {

    public function getName(): string {
        return "Iron Forge";
    }

    public function getLevels(): int {
        return 4;
    }

    protected function internalLevelUp(Team $team): void {
        $generators = $team->getGenerators();

        if($this->level === 3) { // generate emeralds
            $team->addGenerator(new TeamEmeraldGenerator($generators[array_rand($generators)]->getPosition()));
            return;
        }

        foreach($generators as $generator) {
            $speed = 1 / $generator->getInitialSpeed();
            $generator->setCountdown($this->level / 2 * $speed + $speed);
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\team\upgrade\presets;


use pocketmine\item\enchantment\EnchantmentInstance;
use pocketmine\item\enchantment\VanillaEnchantments;
use pocketmine\item\Sword;
use sergittos\bedwars\game\team\upgrade\Upgrade;
use sergittos\bedwars\session\Session;

class SharpenedSwords extends Upgrade {

    public function getName(): string {
        return "Sharpened Swords";
    }

    public function getLevels(): int {
        return 1;
    }

    public function internalApplySession(Session $session): void {
        $inventory = $session->getPlayer()->getInventory();
        foreach($inventory->getContents() as $index => $item) {
            if($item instanceof Sword) {
                $item->addEnchantment(new EnchantmentInstance(VanillaEnchantments::SHARPNESS()));
                $inventory->setItem($index, $item);
            }
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\team\upgrade\presets;


use pocketmine\entity\effect\EffectInstance;
use pocketmine\entity\effect\VanillaEffects;
use pocketmine\utils\Limits;
use sergittos\bedwars\game\team\upgrade\Upgrade;
use sergittos\bedwars\session\Session;

class ManiacMiner extends Upgrade {

    public function getName(): string {
        return "Maniac Miner";
    }

    public function getLevels(): int {
        return 2;
    }

    public function internalApplySession(Session $session): void {
        $session->addEffect(new EffectInstance(VanillaEffects::HASTE(), Limits::INT32_MAX, $this->level - 1, false));
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\team\upgrade\presets;


use sergittos\bedwars\game\team\upgrade\Upgrade;

class HealPool extends Upgrade {

    public function getName(): string {
        return "Heal Pool";
    }

    public function getLevels(): int {
        return 1;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\team\upgrade\trap;


use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\session\Session;

abstract class Trap {

    private string $name;

    public function __construct(string $name) {
        $this->name = $name;
    }

    public function getName(): string {
        return $this->name;
    }

    abstract public function trigger(Session $session, Team $team): void;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\team\upgrade\trap;


use pocketmine\entity\effect\EffectInstance;
use pocketmine\entity\effect\VanillaEffects;
use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\session\Session;

class DefaultTrap extends Trap {

    public function __construct() {
        parent::__construct("It's a trap!");
    }

    public function trigger(Session $session, Team $team): void {
        $session->addEffect(new EffectInstance(VanillaEffects::BLINDNESS(), 20 * 8, 0, false));
        $session->addEffect(new EffectInstance(VanillaEffects::SLOWNESS(), 20 * 8, 0, false));
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\team\upgrade\trap;


use pocketmine\entity\effect\EffectInstance;
use pocketmine\entity\effect\VanillaEffects;
use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\session\Session;

class CounterOffensiveTrap extends Trap {

    public function __construct() {
        parent::__construct("Counter-Offensive Trap");
    }

    public function trigger(Session $session, Team $team): void {
        foreach($team->getMembers() as $member) {
            $member->addEffect(new EffectInstance(VanillaEffects::SPEED(), 20 * 15, 1, false));
            $member->addEffect(new EffectInstance(VanillaEffects::JUMP_BOOST(), 20 * 15, 1, false));
        }
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\team\upgrade\trap;


use pocketmine\entity\effect\EffectInstance;
use pocketmine\entity\effect\VanillaEffects;
use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\session\Session;

class MinerFatigueTrap extends Trap {

    public function __construct() {
        parent::__construct("Miner Fatigue Trap");
    }

    public function trigger(Session $session, Team $team): void {
        $session->addEffect(new EffectInstance(VanillaEffects::MINING_FATIGUE(), 20 * 10, 0, false));
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\team\upgrade\trap;


use pocketmine\entity\effect\VanillaEffects;
use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\session\Session;

class AlarmTrap extends Trap {

    public function __construct() {
        parent::__construct("Alarm Trap");
    }

    public function trigger(Session $session, Team $team): void {
        $session->getPlayer()->getEffects()->remove(VanillaEffects::INVISIBILITY());
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\team;


use pocketmine\block\utils\DyeColor;
use pocketmine\math\Vector3;
use sergittos\bedwars\utils\ColorUtils;

trait TeamProperties {

    private string $name;
    private string $color;

    private Vector3 $spawn_point;
    private Vector3 $bed_position;

    private Area $zone;
    private Area $claim;

    public function getName(): string {
        return $this->name;
    }

    public function getColor(): string {
        return $this->color;
    }

    public function getDyeColor(): DyeColor {
        return ColorUtils::getDye($this->color);
    }

    public function getSpawnPoint(): Vector3 {
        return $this->spawn_point;
    }

    public function getBedPosition(): Vector3 {
        return $this->bed_position;
    }

    public function getZone(): Area {
        return $this->zone;
    }

    public function getClaim(): Area {
        return $this->claim;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\task;


use pocketmine\scheduler\AsyncTask;
use pocketmine\Server;
use pocketmine\utils\Filesystem;
use sergittos\bedwars\BedWars;
use sergittos\bedwars\game\Game;

class RemoveGameTask extends AsyncTask {

    private int $id;

    private string $world_path;

    public function __construct(Game $game) {
        $this->id = $game->getId();

        $this->world_path = Server::getInstance()->getDataPath() . "worlds/" . $game->getMap()->getName() . "-" . $this->id;
    }

    public function onRun(): void {
        Filesystem::recursiveUnlink($this->world_path);
    }

    public function onCompletion(): void {
        BedWars::getInstance()->getGameManager()->removeGame($this->id);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\task;


use pocketmine\scheduler\AsyncTask;
use pocketmine\Server;
use pocketmine\utils\Filesystem;
use sergittos\bedwars\BedWars;
use sergittos\bedwars\game\map\Map;
use sergittos\bedwars\game\map\MapFactory;
use sergittos\bedwars\game\Game;

class GenerateGameTask extends AsyncTask {

    private int $id;

    private string $map_id;
    private string $map_name;

    private string $world_path;
    private string $destination_path;

    public function __construct(int $id, Map $map) {
        $this->id = $id;

        $this->map_id = $map->getId();
        $this->map_name = $map->getName();

        $this->world_path = BedWars::getInstance()->getDataFolder() . "worlds/" . $this->map_name;
        $this->destination_path = Server::getInstance()->getDataPath() . "worlds/" . $this->map_name . "-" . $id;
    }

    public function onRun(): void {
        Filesystem::recursiveCopy($this->world_path, $this->destination_path);
    }

    public function onCompletion(): void {
        BedWars::getInstance()->getGameManager()->addGame(new Game(MapFactory::getMapById($this->map_id), $this->id));
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\stage;


use sergittos\bedwars\game\Game;
use sergittos\bedwars\session\Session;

abstract class Stage {

    protected Game $game;

    public function start(Game $game): void {
        $this->game = $game;
        $this->onStart();

        foreach($this->game->getPlayers() as $session) {
            $this->onJoin($session);
        }
    }

    protected function onStart(): void {}

    /*
     * Called when someone joins the game in this stage
     */
    public function onJoin(Session $session): void {}

    /*
     * Called when someone quits the game in this stage
     */
    public function onQuit(Session $session): void {}

    abstract public function tick(): void;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\stage;


use pocketmine\world\sound\ClickSound;
use sergittos\bedwars\game\stage\trait\JoinableTrait;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\GameUtils;
use function count;
use function str_replace;

class StartingStage extends Stage {
    use JoinableTrait {
        onQuit as onSessionQuit;
    }

    private int $countdown = 10;

    public function getCountdown(): int {
        return $this->countdown;
    }

    public function onQuit(Session $session): void {
        $this->onSessionQuit($session);

        if(count($this->game->getPlayers()) < ($this->game->getMap()->getMaxCapacity() / 2)) {
            $this->game->setStage(new WaitingStage());
        }
    }

    public function tick(): void {
        if($this->countdown <= 0) {
            $this->game->setStage(new PlayingStage());
        } elseif($this->countdown <= 5) {
            $this->broadcastCountdownTitle();
        }
        if($this->countdown > 0) {
            $this->game->broadcastMessage($this->getStartingMessage());
        }
        if($this->countdown === 5) {
            $this->game->setupWorld();
        } elseif($this->countdown === 10) {
            $this->broadcastCountdownTitle();
        }
        $this->game->updateScoreboards();

        $this->countdown--;
    }

    private function getStartingMessage(): string {
        $message = "{YELLOW}The game is starting within {time} {YELLOW}seconds!";
        if($this->countdown <= 10) {
            $message = "{YELLOW}The game starts in {time} {YELLOW}" . ($this->countdown === 1 ? "second" : "seconds") . "!";
        }
        $message = str_replace("{time}", GameUtils::getColoredMessageNumber($this->countdown), $message);

        return $message;
    }

    private function broadcastCountdownTitle(): void {
        $this->game->broadcastTitle(GameUtils::getColoredTitleNumber($this->countdown));
        $this->game->broadcastSound(new ClickSound());
    }

}<?php


namespace sergittos\bedwars\game\stage\trait;


use sergittos\bedwars\game\Game;
use sergittos\bedwars\session\scoreboard\WaitingScoreboard;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\ConfigGetter;
use function strtoupper;

trait JoinableTrait {

    public function start(Game $game): void {
        $this->game = $game;
    }

    public function onJoin(Session $session): void {
        $session->showBossBar("{YELLOW}Playing {WHITE}BED WARS {YELLOW}on {GREEN}" . strtoupper(ConfigGetter::getIP()));
        $session->getPlayer()->getEffects()->clear();
        $session->giveWaitingItems();
        $session->setGame($this->game);
        $session->setScoreboard(new WaitingScoreboard());
        $session->teleportToWaitingWorld();

        $this->game->broadcastMessage(
            "{GRAY}" . $session->getUsername() . " {YELLOW}has joined ({AQUA}" .
            $this->game->getPlayersCount() . "{YELLOW}/{AQUA}" . $this->game->getMap()->getMaxCapacity() . "{YELLOW})!"
        );
    }

    public function onQuit(Session $session): void {
        $this->game->broadcastMessage("{GRAY}" . $session->getUsername() . " {YELLOW}has quit!");
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\stage;


use sergittos\bedwars\game\stage\trait\JoinableTrait;
use sergittos\bedwars\session\Session;

class WaitingStage extends Stage {
    use JoinableTrait {
        onJoin as onSessionJoin;
    }

    public function onJoin(Session $session): void {
        $this->onSessionJoin($session);
        $this->startIfReady();
    }

    private function startIfReady(): void {
        $map = $this->game->getMap();
        $count = $this->game->getPlayersCount();

        if($count > $map->getPlayersPerTeam() and $count >= ($map->getMaxCapacity() / 2)) {
            $this->game->setStage(new StartingStage());
        }
    }

    public function tick(): void {}

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\stage;


use pocketmine\entity\effect\EffectInstance;
use pocketmine\entity\effect\VanillaEffects;
use pocketmine\utils\Limits;
use pocketmine\utils\TextFormat;
use sergittos\bedwars\game\event\Event;
use sergittos\bedwars\game\event\presets\UpgradeGeneratorsTierEvent;
use sergittos\bedwars\game\generator\GeneratorType;
use sergittos\bedwars\game\generator\Tier;
use sergittos\bedwars\session\scoreboard\GameScoreboard;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\ColorUtils;
use function count;
use function round;
use function shuffle;

class PlayingStage extends Stage {

    private ?Event $next_event = null;

    public function getNextEvent(): Event {
        return $this->next_event;
    }

    public function hasStarted(): bool {
        return $this->next_event !== null;
    }

    private function startNextEvent(?Event $event = null): void {
        $this->next_event = $event ?? $this->next_event->getNextEvent();
        $this->next_event?->start($this->game);
    }

    protected function onStart(): void {
        $teams = $this->game->getTeams();
        shuffle($teams);

        foreach($teams as $team) {
            foreach($this->game->getPlayers() as $session) {
                if(!$team->isFull() and !$session->hasTeam()) {
                    $team->addMember($session);

                    if($team->isFull()) {
                        continue 2;
                    }
                }
            }
            if(!$team->isAlive()) {
                $team->destroyBed($this->game, true, true);
            }
        }

        $this->startNextEvent(new UpgradeGeneratorsTierEvent(GeneratorType::DIAMOND, Tier::II));
    }

    public function onJoin(Session $session): void {
        $session->setScoreboard(new GameScoreboard());
    }

    public function onQuit(Session $session): void {
        $session->title("{RED}GAME OVER!");
        $session->resetSettings();
        $session->setTrackingSession(null);

        if($session->hasTeam()) {
            $team = $session->getTeam();
            $team->removeMember($session);
            if(!$team->isAlive()) {
                $this->game->broadcastMessage("{BOLD}{WHITE}TEAM ELIMINATED > {RESET}" . $team->getColoredName() . " Team {RED}has been eliminated!");
            }
        }

        $this->game->despawnGeneratorsFrom($session);

        if(count($this->game->getAliveTeams()) === 1) {
            $this->game->setStage(new EndingStage());
        }
    }

    public function tick(): void {
        if($this->next_event->hasEnded()) {
            $this->startNextEvent();
        }
        $this->game->updateScoreboards();
        $this->tickPlayersAndSpectators();
    }

    private function tickPlayersAndSpectators(): void {
        foreach($this->game->getPlayers() as $session) {
            $this->checkTrackingSession($session);

            if($session->isRespawning()) {
                $session->attemptToRespawn();
            }

            if(!$session->hasTeam()) {
                continue;
            }

            $team = $session->getTeam();
            if($team->getUpgrades()->getHealPool()->canLevelUp()) {
                continue;
            }

            if($team->getZone()->isInside($session->getPlayer()->getPosition())) {
                $session->addEffect(new EffectInstance(VanillaEffects::REGENERATION(), Limits::INT32_MAX, 0, false));
            } else {
                $session->getPlayer()->getEffects()->remove(VanillaEffects::REGENERATION());
            }
        }

        foreach($this->game->getSpectators() as $session) {
            $this->checkTrackingSession($session);
        }
    }

    private function checkTrackingSession(Session $session): void {
        $tracking_session = $session->getTrackingSession();
        if($tracking_session === null) {
            return;
        }

        $player = $session->getPlayer();
        if($tracking_session->isRespawning()) {
            $player->sendPopup(TextFormat::RED . "Target lost");
            $session->setTrackingSession(null);
            return;
        }
        $session->updateCompassDirection();

        $position = $tracking_session->getPlayer()->getPosition();
        $distance = $player->getPosition()->distance($position);

        $player->sendPopup(ColorUtils::translate(
            "Target: {GREEN}{BOLD}" . $tracking_session->getUsername() . "  {RESET}{WHITE}Distance: {GREEN}{BOLD}" . round($distance, 1) . "m"
        ));

        if($session->isSpectator() and $session->getSpectatorSettings()->getAutoTeleport() and $distance >= 10) {
            $player->teleport($position);
        }
    }

}
<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\stage;


use sergittos\bedwars\session\Session;
use function count;

class EndingStage extends Stage {

    private int $time = 5;

    private bool $tie = false;

    protected function onStart(): void {
        foreach($this->game->getSpectators() as $session) {
            $session->title("{RED}GAME OVER!");
        }
        $this->tie = count($this->game->getAliveTeams()) > 1;
    }

    public function onJoin(Session $session): void {
        if(!$this->tie) {
            $session->addWin();
            $session->addCoins(140);
        }

        $session->title($this->getTitle(), "", 0, $this->time * 20);
        $session->resetSettings();
    }

    public function onQuit(Session $session): void {
        $this->game->despawnGeneratorsFrom($session);
    }

    public function tick(): void {
        $this->time--;
        if($this->time <= 0) {
            $this->game->reset();
        }
    }

    private function getTitle(): string {
        return $this->tie ? "{ORANGE}TIE!" : "{GOLD}VICTORY!";
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game;


use pocketmine\Server;
use pocketmine\world\World;
use sergittos\bedwars\BedWars;
use sergittos\bedwars\game\map\Map;
use sergittos\bedwars\game\map\MapFactory;
use sergittos\bedwars\game\shop\ShopFactory;
use sergittos\bedwars\game\stage\StartingStage;
use sergittos\bedwars\game\stage\WaitingStage;
use sergittos\bedwars\game\task\GenerateGameTask;
use function array_rand;
use function count;

class GameManager {

    private int $next_game_id = 0;

    /** @var Game[] */
    private array $games = [];

    public function __construct() {
        ShopFactory::init();
        foreach(MapFactory::getMaps() as $map) { // Generate 3 games per map by default
            $this->generateGames($map);
        }
    }

    public function getNextGameId(): int {
        return $this->next_game_id++;
    }

    /**
     * @return Game[]
     */
    public function getGames(): array {
        return $this->games;
    }

    public function getGamesCount(Map $map): int {
        return count($this->getGamesByMap($map));
    }

    /**
     * @return Game[]
     */
    public function getGamesByMap(Map $map): array {
        $games = [];
        foreach($this->games as $game) {
            if($game->getMap()->getId() === $map->getId()) {
                $games[] = $game;
            }
        }
        return $games;
    }

    public function getGameByWorld(World $world): ?Game {
        foreach(BedWars::getInstance()->getGameManager()->getGames() as $game) {
            if($game->getWorld() === $world) {
                return $game;
            }
        }
        return null;
    }

    public function findRandomGame(int $players_per_team): ?Game {
        $maps = MapFactory::getMapsByPlayers($players_per_team);
        if($maps !== []) {
            $map = $maps[array_rand($maps)];

            return $this->findGame($map);
        }
        return null;
    }

    public function findGame(Map $map): ?Game {
        $games = [];
        foreach($this->getGamesByMap($map) as $game) {
            $stage = $game->getStage();
            if($stage instanceof WaitingStage or ($stage instanceof StartingStage and !$game->isFull())) {
                $games[] = $game;
            }
        }

        if(empty($games)) {
            $this->generateGames($map);
            return null;
        }

        $found = null;
        $index = PHP_INT_MIN;
        foreach($games as $game) {
            $count = $game->getPlayersCount();
            if($count > $index) {
                $index = $count;
                $found = $game;
            }
        }
        return $found;
    }

    public function generateGames(Map $map): void {
        for($i = 0; $i < 3; ++$i) {
            Server::getInstance()->getAsyncPool()->submitTask(new GenerateGameTask(
                $this->getNextGameId(), $map
            ));
        }
    }

    public function addGame(Game $game): void {
        $this->games[$game->getId()] = $game;
    }

    public function removeGame(int $id): void {
        unset($this->games[$id]);
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game;


use pocketmine\entity\Location;
use pocketmine\Server;
use pocketmine\utils\Utils;
use pocketmine\world\format\Chunk;
use pocketmine\world\Position;
use pocketmine\world\sound\Sound;
use pocketmine\world\World;
use pocketmine\world\WorldException;
use sergittos\bedwars\BedWars;
use sergittos\bedwars\entity\PlayBedwarsEntity;
use sergittos\bedwars\game\entity\shop\ItemShopVillager;
use sergittos\bedwars\game\entity\shop\UpgradesShopVillager;
use sergittos\bedwars\game\entity\shop\Villager;
use sergittos\bedwars\game\generator\Generator;
use sergittos\bedwars\game\generator\presets\TextGenerator;
use sergittos\bedwars\game\map\Map;
use sergittos\bedwars\game\stage\Stage;
use sergittos\bedwars\game\stage\WaitingStage;
use sergittos\bedwars\game\task\RemoveGameTask;
use sergittos\bedwars\game\team\Team;
use sergittos\bedwars\session\Session;
use function array_merge;
use function array_search;
use function count;
use function in_array;
use function is_string;

class Game {

    private int $id;

    private Map $map;
    private Stage $stage;

    private ?World $world = null;

    /** @var Generator[] */
    private array $generators;

    /** @var Team[] */
    private array $teams;

    /** @var Position[] */
    private array $blocks = [];

    /** @var Session[] */
    private array $players = [];

    /** @var Session[] */
    private array $spectators = [];

    public function __construct(Map $map, int $id) {
        $this->id = $id;
        $this->map = $map;
        $this->teams = Utils::cloneObjectArray($map->getTeams());
        $this->generators = Utils::cloneObjectArray($map->getGenerators());

        $this->setStage(new WaitingStage());
    }

    public function getId(): int {
        return $this->id;
    }

    public function getStage(): Stage {
        return $this->stage;
    }

    public function getMap(): Map {
        return $this->map;
    }

    public function getWorld(): ?World {
        return $this->world;
    }

    /**
     * @return Generator[]
     */
    public function getGenerators(): array {
        return $this->generators;
    }

    /**
     * @return Team[]
     */
    public function getTeams(): array {
        return $this->teams;
    }

    /**
     * @return Team[]
     */
    public function getAliveTeams(): array {
        $teams = [];
        foreach($this->teams as $team) {
            if($team->isAlive()) {
                $teams[] = $team;
            }
        }
        return $teams;
    }

    /**
     * @return Session[]
     */
    public function getPlayers(): array {
        return $this->players;
    }

    /**
     * @return Session[]
     */
    public function getSpectators(): array {
        return $this->spectators;
    }

    /**
     * @return Session[]
     */
    public function getPlayersAndSpectators(): array {
        return array_merge($this->players, $this->spectators);
    }

    public function getPlayersCount(): int {
        return count($this->players);
    }

    public function isFull(): bool {
        return $this->getPlayersCount() >= $this->map->getMaxCapacity();
    }

    public function checkBlock(Position $position): bool {
        foreach($this->blocks as $index => $block) {
            if($block->equals($position)) {
                unset($this->blocks[$index]);
                return true;
            }
        }
        return false;
    }

    public function isPlaying(Session $session): bool {
        return in_array($session, $this->players, true);
    }

    public function isSpectator(Session $session): bool {
        return in_array($session, $this->spectators, true);
    }

    public function setStage(Stage $stage): void {
        $this->stage = $stage;
        $this->stage->start($this);
    }

    public function addBlock(Position $position): void {
        $this->blocks[] = $position;
    }

    public function addPlayer(Session $session): void {
        $this->players[] = $session;

        $this->stage->onJoin($session);

        $this->updateScoreboards();
        $this->updatePlayEntities();
    }

    public function removePlayer(Session $session, bool $teleport_to_hub = true, bool $set_spectator = false): void {
        unset($this->players[array_search($session, $this->players, true)]);

        $this->stage->onQuit($session);

        if($teleport_to_hub) {
            $session->teleportToHub();
        }

        if($set_spectator) {
            $this->addSpectator($session);
        } else {
            $session->setGame(null);
        }
        $this->updateScoreboards();
        $this->updatePlayEntities();
    }

    public function addSpectator(Session $session): void {
        $this->spectators[] = $session;

        $session->giveSpectatorItems();
        $session->getSpectatorSettings()->apply();
    }

    public function removeSpectator(Session $session): void {
        unset($this->spectators[array_search($session, $this->spectators, true)]);

        $this->despawnGeneratorsFrom($session);

        $session->setGame(null);
        $session->teleportToHub();
    }

    public function despawnGeneratorsFrom(Session $session): void {
        foreach($this->getGenerators() as $generator) {
            if($generator instanceof TextGenerator) {
                $generator->getText()->despawnFrom($session);
            }
        }
    }

    private function updatePlayEntities(): void {
        foreach(Server::getInstance()->getWorldManager()->getDefaultWorld()->getEntities() as $entity) {
            if($entity instanceof PlayBedwarsEntity and $entity->getPlayersPerTeam() === $this->map->getPlayersPerTeam()) {
                $entity->updateNameTag();
            }
        }
    }

    private function spawnVillager(Villager $villager): void {
        $position = $villager->getPosition()->floor();
        $this->world->requestChunkPopulation($position->getX() >> Chunk::COORD_BIT_SIZE, $position->getZ() >> Chunk::COORD_BIT_SIZE, null)->onCompletion(
            function() use ($villager) {
                $villager->spawnToAll();
            },
            function() {}
        );
    }

    public function tickGenerators(): void {
        foreach($this->generators as $generator) {
            $generator->tick($this);
        }
        foreach($this->teams as $team) {
            $team->tickGenerators($this);
        }
    }

    public function updateScoreboards(): void {
        foreach($this->getPlayersAndSpectators() as $session) {
            $session->updateScoreboard();
        }
    }

    public function broadcastTitle(string $title): void {
        foreach($this->players as $session) {
            $session->title($title);
        }
    }

    public function broadcastMessage(string $message): void {
        foreach($this->getPlayersAndSpectators() as $session) {
            $session->message($message);
        }
    }

    public function broadcastSound(Sound|string $sound): void {
        foreach($this->getPlayersAndSpectators() as $session) {
            if(is_string($sound)) {
                $session->playSound($sound);
            } else {
                $player = $session->getPlayer();
                $player->broadcastSound($sound, [$player]);
            }
        }
    }

    public function setupWorld(): void {
        $name = $this->map->getName() . "-" . $this->id;

        $world_manager = Server::getInstance()->getWorldManager();
        if(!$world_manager->loadWorld($name)) {
            throw new WorldException("Failed to load world");
        }

        $this->world = $world_manager->getWorldByName($name);
        $this->world->setAutoSave(false);
        $this->world->setTime(World::TIME_DAY);
        $this->world->stopTime();

        foreach($this->map->getShopPositions() as $position) {
            $this->spawnVillager(new ItemShopVillager(Location::fromObject($position, $this->world)));
        }
        foreach($this->map->getUpgradesPositions() as $position) {
            $this->spawnVillager(new UpgradesShopVillager(Location::fromObject($position, $this->world)));
        }
    }

    public function unloadWorld(): void {
        if($this->world !== null) {
            Server::getInstance()->getWorldManager()->unloadWorld($this->world);

            $this->world = null;
            $this->blocks = [];
        }
    }

    public function reset(): void {
        foreach($this->players as $session) {
            $this->removePlayer($session);
        }
        foreach($this->spectators as $spectator) {
            $this->removeSpectator($spectator);
        }
        foreach($this->generators as $generator) {
            $generator->reset();
        }

        $this->unloadWorld();

        if(BedWars::getInstance()->getGameManager()->getGamesCount($this->map) > 5) {
            Server::getInstance()->getAsyncPool()->submitTask(new RemoveGameTask($this));
            return;
        }

        foreach($this->teams as $team) {
            $team->reset();
        }

        $this->setStage(new WaitingStage());
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\entity\shop;



use dresnite\EasyUI\Form;
use sergittos\bedwars\form\shop\ShopForm;
use sergittos\bedwars\game\shop\Shop;
use sergittos\bedwars\game\shop\ShopFactory;
use sergittos\bedwars\session\Session;

class ItemShopVillager extends Villager {

    protected function getName(): string {
        return "ITEM SHOP";
    }

    protected function getForm(Session $session): Form {
        return new ShopForm($session, "Quick buy", ShopFactory::getShop(Shop::ITEM));
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\entity\shop;


use dresnite\EasyUI\Form;
use pocketmine\entity\Entity;
use pocketmine\entity\EntitySizeInfo;
use pocketmine\event\entity\EntityDamageByChildEntityEvent;
use pocketmine\event\entity\EntityDamageByEntityEvent;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\network\mcpe\protocol\types\entity\EntityIds;
use pocketmine\player\Player;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\session\SessionFactory;
use sergittos\bedwars\utils\ColorUtils;
use function strtoupper;

abstract class Villager extends Entity {

    public static function getNetworkTypeId(): string {
        return EntityIds::VILLAGER;
    }

    protected function getInitialSizeInfo(): EntitySizeInfo {
        return new EntitySizeInfo(1.8, 0.6);
    }

    protected function getInitialGravity(): float {
        return 0.08;
    }

    protected function getInitialDragMultiplier(): float {
        return 0.02;
    }

    public function canSaveWithChunk(): bool {
        return true;
    }

    protected function initEntity(CompoundTag $nbt): void {
        parent::initEntity($nbt);

        $this->setNameTag(ColorUtils::translate("{AQUA}" . strtoupper($this->getName()) . "{RESET}\n{YELLOW}{BOLD}RIGHT CLICK"));
        $this->setNameTagAlwaysVisible();
    }

    public function attack(EntityDamageEvent $source): void {
        if($source instanceof EntityDamageByChildEntityEvent or !$source instanceof EntityDamageByEntityEvent) {
            return;
        }

        $source->cancel();

        $damager = $source->getDamager();
        if(!$damager instanceof Player or !SessionFactory::hasSession($damager)) {
            return;
        }

        $session = SessionFactory::getSession($damager);
        if($session->isPlaying()) {
            $damager->sendForm($this->getForm($session));
        } else {
            $session->message("{RED}You must be in game to do this!");
        }
    }

    public function onInteract(Player $player, Vector3 $clickPos): bool {
        $player->sendForm($this->getForm(SessionFactory::getSession($player)));
        return true;
    }

    abstract protected function getName(): string;

    abstract protected function getForm(Session $session): Form;

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\entity\shop;


use dresnite\EasyUI\Form;
use sergittos\bedwars\form\shop\CategoryForm;
use sergittos\bedwars\game\shop\Shop;
use sergittos\bedwars\game\shop\ShopFactory;
use sergittos\bedwars\game\shop\upgrades\UpgradesShop;
use sergittos\bedwars\session\Session;
use sergittos\bedwars\utils\ColorUtils;

class UpgradesShopVillager extends Villager {

    protected function getName(): string {
        return "TEAM\n{AQUA}UPGRADES";
    }

    protected function getForm(Session $session): Form {
        /** @var UpgradesShop $shop */
        $shop = ShopFactory::getShop(Shop::UPGRADES);

        $form = new CategoryForm($session, $shop->getCategories()[0]);
        $form->addRedirectFormButton(
            ColorUtils::translate("{GOLD}{BOLD}Traps{RESET}\n{YELLOW}Click to view!"),
            new CategoryForm($session, $shop->getCategories()[1])
        );
        return $form;
    }

}<?php

declare(strict_types=1);


namespace sergittos\bedwars\game\entity\misc;


use pocketmine\entity\EntitySizeInfo;
use pocketmine\entity\projectile\Throwable;
use pocketmine\event\entity\ProjectileHitEvent;
use pocketmine\network\mcpe\protocol\types\entity\EntityIds;
use pocketmine\world\Explosion;
use pocketmine\world\Position;

class Fireball extends Throwable {

    public static function getNetworkTypeId(): string {
        return EntityIds::FIREBALL;
    }

    protected function getInitialSizeInfo(): EntitySizeInfo {
        return new EntitySizeInfo(0.31, 0.31);
    }

    protected function getInitialGravity(): float {
        return 0.01;
    }

    protected function onHit(ProjectileHitEvent $event): void {
        $explosion = new Explosion(Position::fromObject($event->getRayTraceResult()->getHitVector(), $this->getWorld()), 4, $this);
        $explosion->explodeA();
        $explosion->explodeB();
    }

}main: sergittos\bedwars\BedWars
name: BedwarsPM
api: 5.0.0
version: 1.1.0
author: Sergittos (@Sergittos)
website: github.com/Sergittos
description: ðŸ›�ï¸� A BedWars plugin for PocketMine-MP servers
ð}E ÷IL�+Ý³ìÕÆ½TÝž5>s”��ÈjÆ@]4   GBMB