Add TNTLeague Module

This commit is contained in:
2024-08-16 11:38:54 +02:00
parent f8f184514b
commit 66f5b6491c
24 changed files with 1446 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
plugins {
steamwar.kotlin
}
dependencies {
compileOnly(libs.paperapi21)
compileOnly(project(":SpigotCore"))
}
+100
View File
@@ -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
@@ -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()
}
}
@@ -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=
@@ -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!
@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Player> {
return TypeValidator<Player> { _, player, _ -> TNTLeagueGame.getTeam(player)?.leader == player}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}
@@ -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<Material, Price>
) {
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<Material, Price> {
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,
)
}
@@ -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)
}
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
package de.steamwar.tntleague.events
import org.bukkit.event.Listener
object DummyListener: Listener {
}
@@ -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()
}
}
}
@@ -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 <K, T> Map<K?, T>.filterKeysNotNull(destination: MutableMap<K, T> = mutableMapOf()): Map<K, T> {
this.forEach { (t, u) -> if(t != null) destination[t] = u }
return destination
}
}
@@ -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
}
}
}
@@ -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
}
}
@@ -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<Player>()
val invites = mutableListOf<Player>()
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);
}
}
@@ -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
}
}
}
}
@@ -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<Int, (event: InventoryClickEvent) -> 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<ItemStack, (event: InventoryClickEvent) -> Unit>) {
addItem(item.first, slot, item.second)
}
operator fun set(slot: Int, item: ItemStack) {
addItem(item, slot) { }
}
}
@@ -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<Block>
inline get() = sequence {
for (x in locations) {
yield(x.block)
}
}
val locations: Sequence<Location>
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))
}
@@ -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)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, Int> {
val lines = mutableListOf<Component>()
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
}
+10
View File
@@ -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
+2
View File
@@ -216,3 +216,5 @@ include(
"VelocityCore",
"VelocityCore:Persistent"
)
include("TNTLeague")