diff --git a/TNTLeague/build.gradle.kts b/TNTLeague/build.gradle.kts new file mode 100644 index 00000000..b69d5117 --- /dev/null +++ b/TNTLeague/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + steamwar.kotlin +} + +dependencies { + compileOnly(libs.paperapi21) + compileOnly(project(":SpigotCore")) +} \ No newline at end of file diff --git a/TNTLeague/src/config.yml b/TNTLeague/src/config.yml new file mode 100644 index 00000000..d2e0a1e6 --- /dev/null +++ b/TNTLeague/src/config.yml @@ -0,0 +1,100 @@ +startDelay: 10 +gameTime: 1200 + +prices: + TNT: + price: 4 + amount: 4 + REDSTONE: + price: 4 + amount: 4 + REPEATER: + price: 4 + amount: 2 + COMPARATOR: + price: 4 + amount: 1 + REDSTONE_BLOCK: + price: 4 + amount: 1 + REDSTONE_TORCH: + price: 4 + amount: 2 + END_STONE: + price: 4 + amount: 8 + ICE: + price: 8 + amount: 1 + LEVER: + price: 4 + amount: 1 + OAK_BUTTON: + price: 4 + amount: 1 + STONE_BUTTON: + price: 4 + amount: 1 + OAK_TRAPDOOR: + price: 4 + amount: 2 + IRON_TRAPDOOR: + price: 4 + amount: 1 + PISTON: + price: 4 + amount: 2 + STICKY_PISTON: + price: 4 + amount: 1 + GLASS: + price: 4 + amount: 2 + OAK_FENCE: + price: 4 + amount: 4 + LADDER: + price: 6 + amount: 2 + WHITE_GLAZED_TERRACOTTA: + price: 4 + amount: 3 + JUKEBOX: + price: 4 + amount: 2 + OBSERVER: + price: 10 + amount: 4 + BREWING_STAND: + price: 4 + amount: 1 + STRING: + price: 4 + amount: 2 + END_STONE_BRICK_SLAB: + price: 4 + amount: 2 + TARGET: + price: 4 + amount: 1 + COPPER_BULB: + price: 4 + amount: 1 + SLIME_BLOCK: + price: 6 + amount: 2 + HONEY_BLOCK: + price: 6 + amount: 2 + STONE_PRESSURE_PLATE: + price: 4 + amount: 1 + NOTE_BLOCK: + price: 3 + amount: 1 + HOPPER: + price: 3 + amount: 1 + GRAVEL: + price: 4 + amount: 3 diff --git a/TNTLeague/src/de/steamwar/tntleague/TNTLeague.kt b/TNTLeague/src/de/steamwar/tntleague/TNTLeague.kt new file mode 100644 index 00000000..5479e4d7 --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/TNTLeague.kt @@ -0,0 +1,48 @@ +package de.steamwar.tntleague + +import de.steamwar.tntleague.command.AcceptCommand +import de.steamwar.tntleague.command.InviteCommand +import de.steamwar.tntleague.command.LeaveCommand +import de.steamwar.tntleague.command.RemoveCommand +import de.steamwar.tntleague.events.GlobalListener +import de.steamwar.tntleague.events.LobbyListener +import net.kyori.adventure.key.Key +import net.kyori.adventure.translation.GlobalTranslator +import net.kyori.adventure.translation.TranslationRegistry +import net.kyori.adventure.util.UTF8ResourceBundleControl +import org.bukkit.plugin.java.JavaPlugin +import java.util.* + +lateinit var plugin: TNTLeague + +class TNTLeague : JavaPlugin() { + init { + plugin = this + } + + override fun onEnable() { + saveResource("config.yml", false) + saveDefaultConfig() + + val registry = TranslationRegistry.create(Key.key("steamwar:tntleague")) + + val bundleDe = ResourceBundle.getBundle("de.steamwar.tntleague.TNTLeague", Locale.GERMAN, UTF8ResourceBundleControl()) + val bundleEn = ResourceBundle.getBundle("de.steamwar.tntleague.TNTLeague", Locale.US, UTF8ResourceBundleControl()) + registry.defaultLocale(Locale.US) + + registry.registerAll(Locale.GERMAN, bundleDe, true) + registry.registerAll(Locale.US, bundleEn, true) + + GlobalTranslator.translator().addSource(registry) + + server.pluginManager.registerEvents(LobbyListener, this) + server.pluginManager.registerEvents(GlobalListener, this) + + logger.info("TNTLeague enabled") + + InviteCommand.register() + AcceptCommand.register() + RemoveCommand.register() + LeaveCommand.register() + } +} diff --git a/TNTLeague/src/de/steamwar/tntleague/TNTLeague_de_DE.properties b/TNTLeague/src/de/steamwar/tntleague/TNTLeague_de_DE.properties new file mode 100644 index 00000000..795db334 --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/TNTLeague_de_DE.properties @@ -0,0 +1,18 @@ +join={0} ist dem Spiel beigetreten! +joinTeam={0} ist dem {1} team begetreten! +quit={0} hat das Spiel verlassen! +blue=Blau +red=Rot +shutdown=Der Server fährt in {0} sekunden herunter! +teamWin=Team {0} gewinnt! +notEnoughCoins=Du hast nicht genug Coins um dir das zu kaufen! +gameStarting=Das Spiel beginnt in {0} Sekunden! +gameStart=Start in {0} +gameStarted=Das Spiel beginnt! +gameEnded=Das Spiel ist aus! +dealer=Händler +dealerItem= +dealerPrice=Kosten: {0} Coins +scoreboardTarget=Ziel: {0} +scoreboardTime=Zeit: {0}:{1} +scoreboardTeam= \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/TNTLeague_en_US.properties b/TNTLeague/src/de/steamwar/tntleague/TNTLeague_en_US.properties new file mode 100644 index 00000000..2f7c11e2 --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/TNTLeague_en_US.properties @@ -0,0 +1,37 @@ +join={0} joined the game! +joinTeam={0} joined the {1} team! +quit={0} left the game! +quitTeam={0} left the {1} team! +blue=Blue +red=Red +shutdown=The server stops in {0} seconds! +teamWin=Team {0} wins! + +notEnoughCoins=You don't have enough coins to buy this item! + +gameStarting=The game starts in {0} seconds! +gameStart=Starting in {0} +gameStarted=The game has started! + +timeRemaining={0} minutes remaining! + +gameEnded=The game has ended! +draw=The game ended in a draw! +chat={0}» {1} + +dealer=Shopkeeper +dealerItem={0} {1} +dealerPrice=Price: {0} Coins + +scoreboardTarget=Target: {0} +scoreboardTime=Time: {0}:{1} +scoreboardTeam=Team {0}: {1} + +ready=Ready +notReady=Not ready +isReady=Team {0} is ready! +isNotReady=Team {0} is not ready! + +invited={0} invited you to join the {1} team! *Click* +invitedHover=Click to join the {0} team! +invitedPlayer=Invited {0} to join your team! \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/command/AcceptCommand.kt b/TNTLeague/src/de/steamwar/tntleague/command/AcceptCommand.kt new file mode 100644 index 00000000..3606bdbd --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/command/AcceptCommand.kt @@ -0,0 +1,40 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.tntleague.command + +import de.steamwar.command.SWCommand +import de.steamwar.tntleague.game.TNTLeagueGame +import org.bukkit.entity.Player + +object AcceptCommand: SWCommand("accept") { + + @Register + fun acceptInvite(sender: Player, @Validator("isLeader") target: Player) { + if (TNTLeagueGame.state != TNTLeagueGame.GameState.LOBBY) return + + val team = TNTLeagueGame.getTeam(target) ?: return + if (team.leader != target) return + if (sender !in team.invites) return + + team.invites.remove(target) + team.opposite.invites.remove(target) + team.join(sender) + } +} \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/command/InviteCommand.kt b/TNTLeague/src/de/steamwar/tntleague/command/InviteCommand.kt new file mode 100644 index 00000000..fdfaf359 --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/command/InviteCommand.kt @@ -0,0 +1,56 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.tntleague.command + +import de.steamwar.command.SWCommand +import de.steamwar.command.TypeValidator +import de.steamwar.tntleague.game.TNTLeagueGame +import de.steamwar.tntleague.util.* +import net.kyori.adventure.text.event.ClickEvent +import net.kyori.adventure.text.event.HoverEvent +import org.bukkit.entity.Player + +object InviteCommand: SWCommand("invite") { + + @Register + fun invitePlayer(@Validator("isLeader") sender: Player, target: Player) { + if (TNTLeagueGame.state != TNTLeagueGame.GameState.LOBBY) return + if (TNTLeagueGame.getTeam(target) != null) return + + val team = TNTLeagueGame.getTeam(sender)!! + team.invites.add(target) + + target.sendMessage(translate("invited", sender.name.yellow(), translate(team.name).colorByTeam(team)).basic().clickEvent( + ClickEvent.callback { + if (target !in team.invites) return@callback + + team.invites.remove(target) + team.opposite.invites.remove(target) + team.join(target) + }) + .hoverEvent(HoverEvent.showText(translate("invitedHover", translate(team.name).colorByTeam(team)).green()))) + sender.sendMessage(translate("invitedPlayer", target.name.yellow()).basic()) + } + + @Validator("isLeader", local = false) + fun isLeader(): TypeValidator { + return TypeValidator { _, player, _ -> TNTLeagueGame.getTeam(player)?.leader == player} + } +} \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/command/LeaveCommand.kt b/TNTLeague/src/de/steamwar/tntleague/command/LeaveCommand.kt new file mode 100644 index 00000000..aeb96492 --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/command/LeaveCommand.kt @@ -0,0 +1,30 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.tntleague.command + +import de.steamwar.command.SWCommand +import de.steamwar.tntleague.game.TNTLeagueGame +import org.bukkit.entity.Player + +object LeaveCommand: SWCommand("leave", "l") { + + @Register + fun leave(player: Player) = TNTLeagueGame.getTeam(player)?.remove(player) +} \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/command/RemoveCommand.kt b/TNTLeague/src/de/steamwar/tntleague/command/RemoveCommand.kt new file mode 100644 index 00000000..d889f31a --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/command/RemoveCommand.kt @@ -0,0 +1,39 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.tntleague.command + +import de.steamwar.command.SWCommand +import de.steamwar.tntleague.game.TNTLeagueGame +import org.bukkit.entity.Player + +object RemoveCommand: SWCommand("remove") { + + @Register + fun removePlayer(@Validator("isLeader") sender: Player, target: Player) { + if (TNTLeagueGame.state != TNTLeagueGame.GameState.LOBBY) return + + if (sender == target) return + val team = TNTLeagueGame.getTeam(sender) ?: return + if (team.leader != sender) return + if (target !in team.members) return + + team.remove(target) + } +} \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/config/TNTLeagueConfig.kt b/TNTLeague/src/de/steamwar/tntleague/config/TNTLeagueConfig.kt new file mode 100644 index 00000000..30fb8353 --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/config/TNTLeagueConfig.kt @@ -0,0 +1,35 @@ +package de.steamwar.tntleague.config + +import de.steamwar.tntleague.plugin +import org.bukkit.Material +import org.bukkit.configuration.ConfigurationSection +import org.bukkit.configuration.file.FileConfiguration + +data class TNTLeagueConfig( + val startDelay: Int = 10, + val gameTime: Int = 60 * 20, + + val prices: Map +) { + companion object { + val config: TNTLeagueConfig by lazy { loadConfig(plugin.config) } + + private fun loadConfig(config: FileConfiguration): TNTLeagueConfig { + return TNTLeagueConfig(config.getInt("startDelay"), config.getInt("gameTime"), loadPrices(config.getConfigurationSection("prices")!!)) + } + + private fun loadPrices(config: ConfigurationSection): Map { + return config.getKeys(false).associateWith { + Price( + config.getInt("$it.amount"), + config.getInt("$it.price") + ) + }.mapKeys { Material.getMaterial(it.key)!! } + } + } + + data class Price( + val amount: Int, + val price: Int, + ) +} diff --git a/TNTLeague/src/de/steamwar/tntleague/config/TNTLeagueWorldConfig.kt b/TNTLeague/src/de/steamwar/tntleague/config/TNTLeagueWorldConfig.kt new file mode 100644 index 00000000..faaf73aa --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/config/TNTLeagueWorldConfig.kt @@ -0,0 +1,75 @@ +package de.steamwar.tntleague.config + +import de.steamwar.tntleague.plugin +import de.steamwar.tntleague.util.Area +import de.steamwar.tntleague.util.translate +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.configuration.ConfigurationSection +import org.bukkit.configuration.file.YamlConfiguration +import org.bukkit.entity.Villager +import org.bukkit.entity.WanderingTrader +import java.io.File + +val world by lazy { plugin.server.worlds.first()!! } + +private val targetedBlocksRed by lazy { TNTLeagueWorldConfig.redTeam.target.blocks.count { block -> block.type == TNTLeagueWorldConfig.targetMaterial } } + +private val targetedBlocksBlue by lazy { TNTLeagueWorldConfig.blueTeam.target.blocks.count { block -> block.type == TNTLeagueWorldConfig.targetMaterial } } + +private val targetedBlocksAll: Int + get() = if (targetedBlocksBlue == targetedBlocksRed) targetedBlocksBlue else error("Targeted blocks are not equal") + +val targetedBlocks: Int + get() = if (TNTLeagueWorldConfig.target != -1) TNTLeagueWorldConfig.target else targetedBlocksAll + +object TNTLeagueWorldConfig { + private val config: YamlConfiguration by lazy { + YamlConfiguration.loadConfiguration( + File( + plugin.server.worlds.first().worldFolder, + "tntleague.yml" + ) + ) + } + + val blueTeam: TeamConfig = TeamConfig.fromConfig(config.getConfigurationSection("blueTeam")!!) + val redTeam: TeamConfig = TeamConfig.fromConfig(config.getConfigurationSection("redTeam")!!) + val lobby: Location = config.getLocation("lobby", blueTeam.spawnLocation.clone().add(redTeam.spawnLocation).multiply(0.5))!! + val targetMaterial: Material = Material.matchMaterial(config.getString("targetMaterial", "IRON_BLOCK")!!)!! + val minHeight: Int = config.getInt("minHeight", 0) + val target: Int = config.getInt("target", -1) + + @JvmRecord + data class TeamConfig( + val spawnLocation: Location, + val dealerSpawn: Location, + val itemSpawn: Location, + val target: Area + ) { + companion object { + fun fromConfig(config: ConfigurationSection): TeamConfig { + val spawnLocation = config.getLocation("spawn")!! + val dealerSpawn = config.getLocation("dealerSpawn")!! + val itemSpawn = config.getLocation("itemSpawn")!! + val targetPos1 = config.getLocation("targetMin")!! + val targetPos2 = config.getLocation("targetMax")!! + + spawnDealer(dealerSpawn) + + return TeamConfig(spawnLocation, dealerSpawn, itemSpawn, Area(targetPos1, targetPos2)) + } + + private fun spawnDealer(loc: Location) = world.spawn(loc, WanderingTrader::class.java) + .apply { + customName(translate("dealer")) + isCustomNameVisible = false + isInvulnerable = true + isSilent = true + isCollidable = false + isAware = false + setAI(false) + } + } + } +} diff --git a/TNTLeague/src/de/steamwar/tntleague/events/DummyListener.kt b/TNTLeague/src/de/steamwar/tntleague/events/DummyListener.kt new file mode 100644 index 00000000..f9f616de --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/events/DummyListener.kt @@ -0,0 +1,25 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.tntleague.events + +import org.bukkit.event.Listener + +object DummyListener: Listener { +} \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/events/GlobalListener.kt b/TNTLeague/src/de/steamwar/tntleague/events/GlobalListener.kt new file mode 100644 index 00000000..f2930adb --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/events/GlobalListener.kt @@ -0,0 +1,88 @@ +package de.steamwar.tntleague.events + +import de.steamwar.tntleague.config.TNTLeagueWorldConfig +import de.steamwar.tntleague.game.TNTLeagueGame +import de.steamwar.tntleague.game.TNTLeagueTeam +import de.steamwar.tntleague.inventory.SWInventoryHolder +import de.steamwar.tntleague.plugin +import de.steamwar.tntleague.util.* +import io.papermc.paper.event.player.AsyncChatEvent +import org.bukkit.GameMode +import org.bukkit.Material +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.entity.PlayerDeathEvent +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerMoveEvent +import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.event.player.PlayerRespawnEvent + +object GlobalListener: Listener { + + @EventHandler(priority = EventPriority.LOW) + fun onPlayerJoin(e: PlayerJoinEvent) { + e.joinMessage(null) + with(e.player) { + teleport(TNTLeagueWorldConfig.lobby) + inventory.clear() + plugin.server.broadcast(translate("join", name.bold()).basic()) + isOp = false + gameMode = GameMode.SPECTATOR + respawnLocation = TNTLeagueWorldConfig.lobby + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + fun onPlayerQuit(e: PlayerQuitEvent) { + e.quitMessage(null) + plugin.server.broadcast(translate("quit", e.player.name.bold().colorByTeam(TNTLeagueGame.getTeam(e.player))).basic()) + TNTLeagueGame.playerLeave(e.player) + } + + @EventHandler(priority = EventPriority.LOWEST) + fun onPlayerClick(e: InventoryClickEvent) { + val holder = e.inventory.getHolder(false) + if (holder is SWInventoryHolder && e.clickedInventory == holder._inventory) { + e.isCancelled = true + holder.handleInventoryClick(e) + } + } + + @EventHandler(priority = EventPriority.LOWEST) + fun onPlayerMove(e: PlayerMoveEvent) { + if (e.to.blockY < TNTLeagueWorldConfig.minHeight) { + when (val team = TNTLeagueGame.getTeam(e.player)) { + is TNTLeagueTeam -> e.player.teleport(team.config.spawnLocation) + null -> e.player.teleport(TNTLeagueWorldConfig.blueTeam.spawnLocation) + } + } + + e.player.foodLevel = 20 + e.player.saturation = 20f + } + + @EventHandler + fun onPlayerDeath(e: PlayerDeathEvent) { + e.deathMessage(null) + e.drops.clear() + + e.itemsToKeep.removeIf { it.type != Material.DIAMOND_PICKAXE } + } + + @EventHandler + fun onPlayerRespawn(e: PlayerRespawnEvent) { + when (val team = TNTLeagueGame.getTeam(e.player)) { + is TNTLeagueTeam -> e.respawnLocation = team.config.spawnLocation + null -> e.respawnLocation = TNTLeagueWorldConfig.lobby + } + } + + @EventHandler + fun onChat(e: AsyncChatEvent) { + e.renderer { source, sourceDisplayName, message, _ -> + translate("chat", sourceDisplayName.colorByTeam(TNTLeagueGame.getTeam(source)), message).basic() + } + } +} \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/events/IngameListener.kt b/TNTLeague/src/de/steamwar/tntleague/events/IngameListener.kt new file mode 100644 index 00000000..bb9b0415 --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/events/IngameListener.kt @@ -0,0 +1,77 @@ +package de.steamwar.tntleague.events + +import de.steamwar.scoreboard.SWScoreboard +import de.steamwar.tntleague.config.TNTLeagueWorldConfig +import de.steamwar.tntleague.game.TNTLeagueGame +import de.steamwar.tntleague.game.TNTLeagueTeam +import de.steamwar.tntleague.inventory.DealerInventory +import de.steamwar.tntleague.util.TNTLeagueScoreboard +import org.bukkit.GameMode +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.entity.EntityType +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.entity.EntityExplodeEvent +import org.bukkit.event.entity.PlayerDeathEvent +import org.bukkit.event.player.PlayerDropItemEvent +import org.bukkit.event.player.PlayerInteractEntityEvent +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerMoveEvent +import org.bukkit.event.player.PlayerQuitEvent + +object IngameListener: Listener { + + @EventHandler + fun onEntityInteract(e: PlayerInteractEntityEvent) { + if (e.player.gameMode == GameMode.SPECTATOR) return + + if(e.rightClicked.type == EntityType.VILLAGER) { + e.isCancelled = true + e.player.openInventory(DealerInventory(e.player).getInventory()) + } + } + + @EventHandler + fun onExplode(e: EntityExplodeEvent) { + e.blockList().filter { it.type == TNTLeagueWorldConfig.targetMaterial } + .groupBy { getTeamByTargetLocation(it.location) } + .filterKeysNotNull() + .mapValues { it.value.size } + .forEach { it.key.damagedBlocks += it.value } + } + + @EventHandler + fun onJoin(e: PlayerJoinEvent) { + SWScoreboard.createScoreboard(e.player, TNTLeagueScoreboard(e.player)) + } + + @EventHandler + fun onMove(e: PlayerMoveEvent) { + if (TNTLeagueGame.getTeam(e.player) != null) { + if (e.to.blockX >= TNTLeagueWorldConfig.lobby.blockX && e.to.blockX <= TNTLeagueWorldConfig.lobby.blockX + 1 || + e.to.blockZ >= TNTLeagueWorldConfig.lobby.blockZ && e.to.blockZ <= TNTLeagueWorldConfig.lobby.blockZ + 1) { + e.isCancelled = true + } + } + } + + @EventHandler + fun onDropPickaxe(e: PlayerDropItemEvent) { + if (e.itemDrop.itemStack.type == Material.DIAMOND_PICKAXE) { + e.isCancelled = true + } + } + + private fun getTeamByTargetLocation(location: Location): TNTLeagueTeam? = + when (location) { + in TNTLeagueWorldConfig.redTeam.target -> TNTLeagueGame.redTeam + in TNTLeagueWorldConfig.blueTeam.target -> TNTLeagueGame.blueTeam + else -> null + } + + private fun Map.filterKeysNotNull(destination: MutableMap = mutableMapOf()): Map { + this.forEach { (t, u) -> if(t != null) destination[t] = u } + return destination + } +} \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/events/LobbyListener.kt b/TNTLeague/src/de/steamwar/tntleague/events/LobbyListener.kt new file mode 100644 index 00000000..e88cfbaa --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/events/LobbyListener.kt @@ -0,0 +1,61 @@ +package de.steamwar.tntleague.events + +import de.steamwar.tntleague.game.TNTLeagueGame +import de.steamwar.tntleague.plugin +import de.steamwar.tntleague.util.basic +import de.steamwar.tntleague.util.colorByTeam +import de.steamwar.tntleague.util.translate +import de.steamwar.tntleague.util.yellow +import io.papermc.paper.util.Tick +import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority +import org.bukkit.event.Listener +import org.bukkit.event.block.Action +import org.bukkit.event.entity.EntityDamageEvent +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.player.PlayerDropItemEvent +import org.bukkit.event.player.PlayerInteractEvent +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerQuitEvent + +object LobbyListener: Listener { + + @EventHandler + fun onPlayerJoin(e: PlayerJoinEvent) { + TNTLeagueGame.getFreeTeam()?.run { + join(e.player) + TNTLeagueGame.checkStart() + } + } + + @EventHandler(priority = EventPriority.LOWEST) + fun onPlayerQuit(e: PlayerQuitEvent) { + val team = TNTLeagueGame.getTeam(e.player) ?: return + team.leave(e.player) + } + + @EventHandler + fun onPlayerDamage(e: EntityDamageEvent) { + e.isCancelled = true + } + + @EventHandler + fun onDropEvent(e: PlayerDropItemEvent) { + e.isCancelled = true + } + + @EventHandler + fun interactEvent(e: PlayerInteractEvent) { + val team = TNTLeagueGame.getTeam(e.player) + if (e.action.isRightClick && team != null && e.item?.isSimilar(team.readyItem()) == true && team.opposite.leader != null) { + team.isReady = !team.isReady + } + } + + @EventHandler + fun inventoryClick(e: InventoryClickEvent) { + if (e.clickedInventory == e.whoClicked.inventory) { + e.isCancelled = true + } + } +} \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/game/TNTLeagueGame.kt b/TNTLeague/src/de/steamwar/tntleague/game/TNTLeagueGame.kt new file mode 100644 index 00000000..59d4e65c --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/game/TNTLeagueGame.kt @@ -0,0 +1,259 @@ +package de.steamwar.tntleague.game + +import de.steamwar.scoreboard.SWScoreboard +import de.steamwar.sql.Fight +import de.steamwar.sql.FightPlayer +import de.steamwar.sql.SteamwarUser +import de.steamwar.tntleague.config.TNTLeagueConfig +import de.steamwar.tntleague.config.TNTLeagueWorldConfig +import de.steamwar.tntleague.config.world +import de.steamwar.tntleague.events.DummyListener +import de.steamwar.tntleague.events.IngameListener +import de.steamwar.tntleague.events.LobbyListener +import de.steamwar.tntleague.inventory.DealerInventory +import de.steamwar.tntleague.plugin +import de.steamwar.tntleague.util.* +import net.kyori.adventure.bossbar.BossBar +import net.kyori.adventure.sound.Sound +import org.bukkit.GameMode +import org.bukkit.Location +import org.bukkit.Material +import org.bukkit.block.data.type.TNT +import org.bukkit.entity.Entity +import org.bukkit.entity.Item +import org.bukkit.entity.Player +import org.bukkit.entity.TNTPrimed +import org.bukkit.entity.Villager +import org.bukkit.event.HandlerList +import org.bukkit.event.Listener +import org.bukkit.inventory.ItemStack +import org.bukkit.scheduler.BukkitTask +import java.sql.Timestamp +import java.time.Instant + +object TNTLeagueGame { + var state: GameState = GameState.LOBBY + set(value) { + if (field.listener != value.listener) { + HandlerList.unregisterAll(field.listener) + plugin.server.pluginManager.registerEvents(value.listener, plugin) + } + field = value + } + + var gameTimeRemaining: Int = TNTLeagueConfig.config.gameTime + + val blueTeam = TNTLeagueTeam(TNTLeagueWorldConfig.blueTeam, TNTLeagueTeam.Team.BLUE) + val redTeam = TNTLeagueTeam(TNTLeagueWorldConfig.redTeam, TNTLeagueTeam.Team.RED) + + private lateinit var start: Timestamp + + private var task: Int? = null + private lateinit var spawnerTask: BukkitTask + private lateinit var timerTask: BukkitTask + + private fun setup() { + assert(state == GameState.STARTING) { "Game is already running" } + + state = GameState.RUNNING + + plugin.server.onlinePlayers.forEach { SWScoreboard.createScoreboard(it, TNTLeagueScoreboard(it)) } + + blueTeam.start() + redTeam.start() + + plugin.server.broadcast(translate("gameStarted").success()) + + val tnt = ItemStack(Material.TNT) + + start = Timestamp.from(Instant.now()) + + spawnerTask = plugin.server.scheduler.runTaskTimer(plugin, bukkit { + if (world.getNearbyEntitiesByType(Item::class.java, TNTLeagueWorldConfig.blueTeam.itemSpawn, 3.0).sumOf { it.itemStack.amount } <= 256) { + spawnItems(TNTLeagueWorldConfig.blueTeam.itemSpawn, tnt) + spawnItems(TNTLeagueWorldConfig.blueTeam.itemSpawn, DealerInventory.coins) + } + if (world.getNearbyEntitiesByType(Item::class.java, TNTLeagueWorldConfig.redTeam.itemSpawn, 3.0).sumOf { it.itemStack.amount } <= 256) { + spawnItems(TNTLeagueWorldConfig.redTeam.itemSpawn, tnt) + spawnItems(TNTLeagueWorldConfig.redTeam.itemSpawn, DealerInventory.coins) + } + }, 5, 10) + + timerTask = plugin.server.scheduler.runTaskTimer(plugin, bukkit { + gameTimeRemaining-- + if (gameTimeRemaining == 0) { + draw(WinReason.TIMEOUT) + return@bukkit + } + + if (gameTimeRemaining % 300 == 0) { + plugin.server.broadcast(translate("timeRemaining", (gameTimeRemaining / 60).toString().yellow()).basic()) + plugin.server.onlinePlayers.forEach { it.playSound(Sound.sound(org.bukkit.Sound.BLOCK_NOTE_BLOCK_PLING.key, Sound.Source.MASTER, 1f, 1f)) } + } + }, 20, 20) + } + + private fun bukkit(f: () -> Unit): () -> Unit = f + + private fun end() { + if(state != GameState.RUNNING) return + state = GameState.END + + plugin.server.onlinePlayers.forEach { + it.gameMode = GameMode.SPECTATOR + SWScoreboard.removeScoreboard(it) + it.playSound(Sound.sound(org.bukkit.Sound.ENTITY_ENDER_DRAGON_DEATH.key, Sound.Source.MASTER, 1f, 1f)) + } + + plugin.server.broadcast(translate("gameEnded").success()) + + spawnerTask.cancel() + + var shutdown = 10 + + plugin.server.scheduler.runTaskTimer(plugin, bukkit { + if (shutdown == 0) { + plugin.server.shutdown() + } + + plugin.server.broadcast(translate("shutdown", shutdown.toString().yellow()).basic()) + + shutdown-- + }, 20, 20) + } + + private fun spawnItems(loc: Location, item: ItemStack) = plugin.server.worlds.first().dropItem(loc, item) + + fun getTeam(player: Player) = if (player in blueTeam.members) blueTeam else if (player in redTeam.members) redTeam else null + + fun getFreeTeam() = if (blueTeam.leader == null) blueTeam else if (redTeam.leader == null) redTeam else null + + fun checkStart() { + if (blueTeam.isReady && redTeam.isReady) { + blueTeam.leader?.inventory?.clear() + redTeam.leader?.inventory?.clear() + state = GameState.STARTING + + var countdown = TNTLeagueConfig.config.startDelay + plugin.server.broadcast(translate("gameStarting", countdown.toString().yellow()).basic()) + val bar = BossBar.bossBar(translate("gameStart", countdown.toString().yellow()).gray(), (TNTLeagueConfig.config.startDelay - countdown) / TNTLeagueConfig.config.startDelay.toFloat(), BossBar.Color.GREEN, BossBar.Overlay.NOTCHED_10) + plugin.server.onlinePlayers.forEach { bar.addViewer(it) } + task = plugin.server.scheduler.scheduleSyncRepeatingTask(plugin, { + plugin.server.onlinePlayers.forEach { it.playSound(Sound.sound(org.bukkit.Sound.ENTITY_EXPERIENCE_ORB_PICKUP.key, Sound.Source.MASTER, 1f, 1f)) } + if (countdown-- == 0) { + plugin.server.onlinePlayers.forEach { it.hideBossBar(bar) } + task = task?.also { plugin.server.scheduler.cancelTask(it) }.let { null } + setup() + } else { + bar.name(translate("gameStart", countdown.toString().yellow()).gray()) + bar.progress((TNTLeagueConfig.config.startDelay - countdown) / TNTLeagueConfig.config.startDelay.toFloat()) + plugin.server.onlinePlayers.filter { !it.activeBossBars().contains(bar) }.forEach { bar.addViewer(it) } + } + }, 20, 20) + + if (task == -1) { + error("Failed to start countdown task") + } + } + } + + fun playerLeave(player: Player) { + blueTeam.invites.remove(player) + redTeam.invites.remove(player) + getTeam(player)?.apply { + members.remove(player) + if (leader == player) { + win(this.opposite, WinReason.LEAVE) + } + } + } + + fun reset() { + assert(state == GameState.LOBBY || state == GameState.STARTING) { "Game is not in lobby or starting state" } + + if (state == GameState.STARTING) { + task = task?.also { plugin.server.scheduler.cancelTask(it) }.let { null } + plugin.server.onlinePlayers.forEach { p -> p.activeBossBars().forEach { it.removeViewer(p) } } + state = GameState.LOBBY + } + } + + fun win(tntLeagueTeam: TNTLeagueTeam, reason: WinReason) { + if (state != GameState.RUNNING) return + end() + plugin.server.broadcast(translate("teamWin", translate(tntLeagueTeam.name).color(tntLeagueTeam.color)).success()) + statistic(tntLeagueTeam, reason) + explode(tntLeagueTeam.opposite) + } + + fun draw(reason: WinReason) { + if (state != GameState.RUNNING) return + end() + plugin.server.broadcast(translate("draw").success()) + statistic(null, reason) + } + + fun explode(team: TNTLeagueTeam) { + Area(team.config.spawnLocation.clone().add(20.0, 30.0, 20.0), team.config.spawnLocation.clone().subtract(20.0, 0.0, 20.0).add(0.0, 30.0, 0.0)) + .locations + .filterIndexed { index, _ -> index % 7 == 0 } + .forEachIndexed { index, location -> + world.spawn(location, TNTPrimed::class.java).apply { + fuseTicks = index + 40 + } + } + } + + private fun statistic(winTeam: TNTLeagueTeam?, reason: WinReason) { + val fightId = Fight.create( + "TNTLeague", + world.name, + start, + TNTLeagueConfig.config.gameTime - gameTimeRemaining, + SteamwarUser.get(blueTeam.leader!!.uniqueId).id, + SteamwarUser.get(redTeam.leader!!.uniqueId).id, + null, + null, + when (winTeam) { + blueTeam -> 1 + redTeam -> 2 + else -> 0 + }, + when (reason) { + WinReason.TIMEOUT -> "TIMEOUT" + WinReason.DESTROYED -> "DESTROYED" + WinReason.LEAVE -> "LEAVE" + } + ) + + addTeamMember(blueTeam, fightId) + addTeamMember(redTeam, fightId) + } + + private fun addTeamMember(team: TNTLeagueTeam, fightId: Int) { + team.members.filter { team.leader != it } + .forEach { + FightPlayer.create( + fightId, + SteamwarUser.get(it.uniqueId).id, + team == blueTeam, + "TNTLeague", + 0, + false + ) + } + } + + enum class GameState(val listener: Listener) { + LOBBY(LobbyListener), + STARTING(LobbyListener), + RUNNING(IngameListener), + END(DummyListener); + } + + enum class WinReason { + TIMEOUT, + DESTROYED, + LEAVE + } +} \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/game/TNTLeagueTeam.kt b/TNTLeague/src/de/steamwar/tntleague/game/TNTLeagueTeam.kt new file mode 100644 index 00000000..cafa70c2 --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/game/TNTLeagueTeam.kt @@ -0,0 +1,146 @@ +package de.steamwar.tntleague.game + +import de.steamwar.tntleague.config.TNTLeagueWorldConfig +import de.steamwar.tntleague.config.targetedBlocks +import de.steamwar.tntleague.plugin +import de.steamwar.tntleague.util.* +import net.kyori.adventure.sound.Sound +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextColor +import org.bukkit.GameMode +import org.bukkit.Material +import org.bukkit.enchantments.Enchantment +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack +import java.awt.Color.green + +data class TNTLeagueTeam(val config: TNTLeagueWorldConfig.TeamConfig, private val team: Team) { + + var leader: Player? = null + set(player) { + field = player + if (player != null) { + with(player.inventory) { + clear() + setItem(4, readyItem()) + } + } + } + + val members = mutableListOf() + val invites = mutableListOf() + + val name: String + get() = team.name.lowercase() + + val color: TextColor + get() = team.color + + var isReady: Boolean = false + set(value) { + field = value + leader?.inventory?.setItem(4, readyItem()) + leader?.playSound(Sound.sound(org.bukkit.Sound.BLOCK_NOTE_BLOCK_PLING.key, Sound.Source.MASTER, 1f, 1f)) + + plugin.server.onlinePlayers.forEach { it.sendActionBar(translate(if (value) "isReady" else "isNotReady", translate(this.name).colorByTeam(this)).let { cmp -> + if (value) { + cmp.green() + } else { + cmp.red() + } + }) } + + if (value && opposite.isReady) { + TNTLeagueGame.checkStart() + } + } + + var damagedBlocks: Int = 0 + set(value) { + field = value + if (value >= targetedBlocks) { + TNTLeagueGame.win(this, TNTLeagueGame.WinReason.DESTROYED) + } + } + + val opposite: TNTLeagueTeam + get() = when (team) { + Team.BLUE -> TNTLeagueGame.redTeam + Team.RED -> TNTLeagueGame.blueTeam + } + + fun join(player: Player): Boolean { + members.add(player) + + with(player) { + teleport(config.spawnLocation) + gameMode = GameMode.ADVENTURE + inventory.clear() + plugin.server.broadcast(translate("joinTeam", name().colorByTeam(this@TNTLeagueTeam), translate(this@TNTLeagueTeam.name).colorByTeam(this@TNTLeagueTeam)).basic()) + } + + if (leader == null) { + leader = player + } + + return true + } + + fun readyItem() = if (isReady) { + ItemStack.of(Material.LIME_DYE).apply { + itemMeta = itemMeta.apply { + displayName(translate("ready").green().translate(leader!!)) + } + } + } else { + ItemStack.of(Material.RED_DYE).apply { + itemMeta = itemMeta.apply { + displayName(translate("notReady").red().translate(leader!!)) + } + } + } + + fun start() = members.forEach { + with(it) { + gameMode = GameMode.SURVIVAL + inventory.addItem(ItemStack.of(Material.DIAMOND_PICKAXE).apply { + itemMeta = itemMeta.apply { + isUnbreakable = true + addEnchant(Enchantment.EFFICIENCY, 1, false) + } + }) + } + } + + fun leave(player: Player) { + if (TNTLeagueGame.state == TNTLeagueGame.GameState.RUNNING) { + TNTLeagueGame.playerLeave(player) + } else { + members.remove(player) + + if (members.isEmpty()) { + plugin.server.onlinePlayers.firstOrNull { it != player && TNTLeagueGame.getTeam(it) == null }?.run { + members.add(this) + } + } + if (leader == player) { + leader = members.firstOrNull() + } + } + } + + fun remove(player: Player) { + leave(player) + with(player) { + teleport(TNTLeagueWorldConfig.lobby) + gameMode = GameMode.SPECTATOR + inventory.clear() + plugin.server.broadcast(translate("quitTeam", name().colorByTeam(this@TNTLeagueTeam), translate(this@TNTLeagueTeam.name).colorByTeam(this@TNTLeagueTeam)).basic()) + } + } + + enum class Team(val color: TextColor) { + BLUE(NamedTextColor.BLUE), + RED(NamedTextColor.RED); + } +} \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/inventory/DealerInventory.kt b/TNTLeague/src/de/steamwar/tntleague/inventory/DealerInventory.kt new file mode 100644 index 00000000..04c1c288 --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/inventory/DealerInventory.kt @@ -0,0 +1,75 @@ +package de.steamwar.tntleague.inventory + +import de.steamwar.tntleague.config.TNTLeagueConfig +import de.steamwar.tntleague.plugin +import de.steamwar.tntleague.util.* +import net.kyori.adventure.sound.Sound +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.Style +import net.kyori.adventure.text.format.TextDecoration +import org.bukkit.Material +import org.bukkit.NamespacedKey +import org.bukkit.entity.Player +import org.bukkit.inventory.Inventory +import org.bukkit.inventory.ItemStack +import org.bukkit.persistence.PersistentDataType +import java.util.* +import kotlin.math.ceil + +class DealerInventory(player: Player): SWInventoryHolder() { + + init { + items.forEachIndexed { index, item -> + this[index] = item.first to { + val price = item.second.price * if (it.isShiftClick) 5 else 1 + val amount = item.second.amount * if (it.isShiftClick) 5 else 1 + + if (!player.inventory.containsAtLeast(coins, price)) { + player.sendMessage(translate("notEnoughCoins").error()) + player.playSound(Sound.sound(org.bukkit.Sound.ENTITY_VILLAGER_HURT.key, net.kyori.adventure.sound.Sound.Source.MASTER, 1f, 1f)) + return@to + } + + player.inventory.removeItem(coins.asQuantity(price)) + player.inventory.addItem(ItemStack.of(item.first.type, amount)) + } + } + } + + override fun createInventory(): Inventory = plugin.server.createInventory(this, ceil(TNTLeagueConfig.config.prices.size / 9f).toInt() * 9, translate("dealer").reset()) + + companion object { + private val priceKey = NamespacedKey(plugin, "price") + private val amountKey = NamespacedKey(plugin, "amount") + private val coinKey = NamespacedKey(plugin, "coin") + + val coins = ItemStack(Material.RAW_GOLD).apply { + itemMeta = itemMeta.apply { + displayName(Component.text("Coins").bold().gold()) + persistentDataContainer.apply { + set(coinKey, PersistentDataType.BOOLEAN, true) + } + } + } + + val items by lazy { + val prices = TNTLeagueConfig.config.prices + + prices.map { (material, price) -> + ItemStack(material).apply { + itemMeta = itemMeta.apply { + displayName(material.name.lowercase().replace("_", " ") + .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + .component().gray().appendSpace().append(price.amount.toString().component().yellow())) + amount = price.amount + lore(listOf(price.price.toString().component().yellow().bold().appendSpace().append(Component.text("Coins").yellow()))) + persistentDataContainer.apply { + set(priceKey, PersistentDataType.INTEGER, price.price) + set(amountKey, PersistentDataType.INTEGER, price.amount) + } + } + } to price + } + } + } +} diff --git a/TNTLeague/src/de/steamwar/tntleague/inventory/SWInventoryHolder.kt b/TNTLeague/src/de/steamwar/tntleague/inventory/SWInventoryHolder.kt new file mode 100644 index 00000000..5d89e541 --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/inventory/SWInventoryHolder.kt @@ -0,0 +1,41 @@ +package de.steamwar.tntleague.inventory + +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.InventoryCloseEvent +import org.bukkit.inventory.Inventory +import org.bukkit.inventory.InventoryHolder +import org.bukkit.inventory.ItemStack + +abstract class SWInventoryHolder: InventoryHolder { + + val _inventory: Inventory by lazy { createInventory() } + + private val callbacks = mutableMapOf Unit>() + + override fun getInventory(): Inventory = _inventory + + abstract fun createInventory(): Inventory + + open fun handleInventoryClick(event: InventoryClickEvent) { + callbacks[event.slot]?.invoke(event) + } + + fun addItem(item: ItemStack, slot: Int, callback: (event: InventoryClickEvent) -> Unit) { + _inventory.setItem(slot, item) + addCallback(slot, callback) + } + + fun addCallback(slot: Int, callback: (event: InventoryClickEvent) -> Unit) { + callbacks[slot] = callback + } + + open fun handleClose(event: InventoryCloseEvent) { } + + operator fun set(slot: Int, item: Pair Unit>) { + addItem(item.first, slot, item.second) + } + + operator fun set(slot: Int, item: ItemStack) { + addItem(item, slot) { } + } +} \ No newline at end of file diff --git a/TNTLeague/src/de/steamwar/tntleague/util/Area.kt b/TNTLeague/src/de/steamwar/tntleague/util/Area.kt new file mode 100644 index 00000000..0b27d227 --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/util/Area.kt @@ -0,0 +1,46 @@ +package de.steamwar.tntleague.util + +import org.bukkit.Location +import org.bukkit.block.Block + +class Area(loc1: Location, loc2: Location) { + + val min: Location + val max: Location + + init { + require(loc1.world == loc2.world) { "Locations must be in the same world" } + this.min = loc1 min loc2 + this.max = loc1 max loc2 + } + + operator fun contains(loc: Location): Boolean { + return loc.world == min.world && loc.x >= min.x && loc.x <= max.x && loc.y >= min.y && loc.y <= max.y && loc.z >= min.z && loc.z <= max.z + } + + val blocks: Sequence + inline get() = sequence { + for (x in locations) { + yield(x.block) + } + } + + val locations: Sequence + inline get() = sequence { + for (x in min.blockX..max.blockX) { + for (y in min.blockY..max.blockY) { + for (z in min.blockZ..max.blockZ) { + yield(Location(min.world, x.toDouble(), y.toDouble(), z.toDouble())) + } + } + } + } +} + +infix fun Location.max(other: Location): Location { + return Location(world, x.coerceAtLeast(other.x), y.coerceAtLeast(other.y), z.coerceAtLeast(other.z)) +} + +infix fun Location.min(other: Location): Location { + return Location(world, x.coerceAtMost(other.x), y.coerceAtMost(other.y), z.coerceAtMost(other.z)) +} diff --git a/TNTLeague/src/de/steamwar/tntleague/util/Style.kt b/TNTLeague/src/de/steamwar/tntleague/util/Style.kt new file mode 100644 index 00000000..3ef28b9a --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/util/Style.kt @@ -0,0 +1,73 @@ +package de.steamwar.tntleague.util + +import de.steamwar.tntleague.game.TNTLeagueTeam +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.ComponentLike +import net.kyori.adventure.text.TranslatableComponent +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.Style +import net.kyori.adventure.text.format.TextDecoration +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer +import net.kyori.adventure.translation.GlobalTranslator +import org.bukkit.entity.Player +import java.util.Locale + +val prefix = Component.text("Steam").yellow() + .append(Component.text("War").darkGray()) + .appendSpace() + +val tntLeaguePrefix = Component.text("TNT").color(NamedTextColor.DARK_RED) + .append(Component.text("League").color(NamedTextColor.GOLD)) + +val tntLeagueChatPrefix: Component = tntLeaguePrefix + .append(Component.text("»").darkGray()) + .appendSpace() + +fun TranslatableComponent.basic(): Component = tntLeagueChatPrefix.append(this.gray()) + +fun TranslatableComponent.error(): Component = tntLeagueChatPrefix.append(this.red()) + +fun TranslatableComponent.success(): Component = tntLeagueChatPrefix.append(this.green()) + +fun String.component(): Component = Component.text(this) + +fun Component.bold(): Component = this.decorate(TextDecoration.BOLD) + +fun String.bold(): Component = this.component().bold() + +fun Component.yellow(): Component = this.color(NamedTextColor.YELLOW) + +fun String.yellow(): Component = this.component().yellow() + +fun Component.red(): Component = this.color(NamedTextColor.RED) + +fun String.red(): Component = this.component().red() + +fun Component.green(): Component = this.color(NamedTextColor.GREEN) + +fun String.green(): Component = this.component().green() + +fun Component.gray(): Component = this.color(NamedTextColor.GRAY) + +fun String.gray(): Component = this.component().gray() + +fun Component.darkGray(): Component = this.color(NamedTextColor.DARK_GRAY) + +fun String.darkGray(): Component = this.component().darkGray() + +fun Component.gold(): Component = this.color(NamedTextColor.GOLD) + +fun translate(key: String, vararg args: ComponentLike): TranslatableComponent = Component.translatable(key, *args).decoration(TextDecoration.ITALIC, false) + +fun Component.reset(): Component = this.style(Style.empty()) + +fun Component.colorByTeam(team: TNTLeagueTeam?) = when (team) { + null -> this.gray() + else -> this.color(team.color) +} + +fun Component.translate(locale: Locale): Component = GlobalTranslator.render(this, locale) + +fun Component.translate(p: Player): Component = this.translate(p.locale()) + +fun Component.toLegacy(): String = LegacyComponentSerializer.legacySection().serialize(this) diff --git a/TNTLeague/src/de/steamwar/tntleague/util/TNTLeagueScoreboard.kt b/TNTLeague/src/de/steamwar/tntleague/util/TNTLeagueScoreboard.kt new file mode 100644 index 00000000..7b8da592 --- /dev/null +++ b/TNTLeague/src/de/steamwar/tntleague/util/TNTLeagueScoreboard.kt @@ -0,0 +1,57 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.tntleague.util + +import de.steamwar.scoreboard.ScoreboardCallback +import de.steamwar.tntleague.config.targetedBlocks +import de.steamwar.tntleague.game.TNTLeagueGame +import net.kyori.adventure.text.Component +import org.bukkit.entity.Player +import kotlin.collections.HashMap + +private val scoreboardTitle by lazy { tntLeaguePrefix.toLegacy() } + +data class TNTLeagueScoreboard(val p: Player): ScoreboardCallback { + override fun getData(): HashMap { + val lines = mutableListOf() + + lines.add(Component.space().green()) + + val minutes = TNTLeagueGame.gameTimeRemaining.floorDiv(60) + val seconds = TNTLeagueGame.gameTimeRemaining.rem(60).toString().padStart(2, '0') + lines.add(translate("scoreboardTime", minutes.toString().yellow(), seconds.yellow()).gray()) + + lines.add(Component.space().yellow()) + + with(TNTLeagueGame.blueTeam) { + lines.add(translate("scoreboardTeam", translate(name).colorByTeam(this), (targetedBlocks - damagedBlocks).toString().yellow()).gray()) + } + with(TNTLeagueGame.redTeam) { + lines.add(translate("scoreboardTeam", translate(name).colorByTeam(this), (targetedBlocks - damagedBlocks).toString().yellow()).gray()) + } + + lines.add(Component.space().gray()) + + return lines + .foldIndexed(HashMap()) { index, acc, component -> acc.also { it[component.translate(p).toLegacy()] = index } } + } + + override fun getTitle(): String = scoreboardTitle +} diff --git a/TNTLeague/src/paper-plugin.yml b/TNTLeague/src/paper-plugin.yml new file mode 100644 index 00000000..8687424a --- /dev/null +++ b/TNTLeague/src/paper-plugin.yml @@ -0,0 +1,10 @@ +name: TNTLeague +version: '1.0.0' +main: de.steamwar.tntleague.TNTLeague +load: POSTWORLD +api-version: '1.21' +dependencies: + - name: SpigotCore + required: true + - name: KotlinCore + required: true \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 75762792..ad769fd8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -216,3 +216,5 @@ include( "VelocityCore", "VelocityCore:Persistent" ) + +include("TNTLeague")