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")