Replace the old dynamic /team invite system with a fixed GUI-based preset team implementation. Removes TeamManager, Team, TeamListener and the TeamCommand, and introduces PresetTeam, PresetTeamManager and GUI listeners/menus (TeamSelectionMenu/TeamSelectionListener). Codepaths that referenced teamManager were migrated to presetTeamManager (GameManager win/compass logic, Lunar rich presence, Oracle perk, scoreboards, tablist, lobby items). Lobby now shows a team wool item when teams are enabled and the tablist/scoreboard display and prefix logic were adapted to reflect preset teams. Configuration supports teams.enabled, teams.preset-count and teams.max-size; scheduled invite cleanup and old team-reset logic were removed accordingly.
499 lines
12 KiB
Kotlin
499 lines
12 KiB
Kotlin
package club.mcscrims.speedhg.game
|
|
|
|
import club.mcscrims.speedhg.SpeedHG
|
|
import club.mcscrims.speedhg.game.modules.FeastManager
|
|
import club.mcscrims.speedhg.game.modules.PitManager
|
|
import club.mcscrims.speedhg.game.modules.RecraftManager
|
|
import club.mcscrims.speedhg.util.sendMsg
|
|
import club.mcscrims.speedhg.util.trans
|
|
import net.kyori.adventure.title.Title
|
|
import org.bukkit.*
|
|
import org.bukkit.attribute.Attribute
|
|
import org.bukkit.entity.Player
|
|
import org.bukkit.event.EventHandler
|
|
import org.bukkit.event.Listener
|
|
import org.bukkit.event.entity.EntityDamageByEntityEvent
|
|
import org.bukkit.event.entity.EntityDamageEvent
|
|
import org.bukkit.event.entity.PlayerDeathEvent
|
|
import org.bukkit.event.player.PlayerQuitEvent
|
|
import org.bukkit.inventory.ItemStack
|
|
import org.bukkit.potion.PotionEffect
|
|
import org.bukkit.potion.PotionEffectType
|
|
import org.bukkit.scheduler.BukkitTask
|
|
import java.util.*
|
|
import kotlin.random.Random
|
|
|
|
class GameManager(
|
|
private val plugin: SpeedHG
|
|
): Listener {
|
|
|
|
var currentState: GameState = GameState.LOBBY
|
|
private set
|
|
|
|
var timer = 0
|
|
|
|
val alivePlayers = mutableSetOf<UUID>()
|
|
|
|
private var gameTask: BukkitTask? = null
|
|
|
|
// Einstellungen aus Config (gecached für Performance)
|
|
private val minPlayers = plugin.config.getInt("game.min-players", 2)
|
|
private val lobbyTime = plugin.config.getInt("game.lobby-time", 60)
|
|
private val invincibilityTime = plugin.config.getInt("game.invincibility-time", 60)
|
|
private val startBorder = plugin.config.getDouble("game.border-start", 300.0)
|
|
private val endBorder = plugin.config.getDouble("game.border-end", 20.0)
|
|
private val borderShrinkTime = plugin.config.getLong("game.border-shrink-time", 600)
|
|
private val maxRadiusTeleport = plugin.config.getDouble("game.max-radius-teleport", 50.0)
|
|
|
|
val feastManager = FeastManager( plugin )
|
|
val pitManager = PitManager( plugin )
|
|
val recraftManager = RecraftManager( plugin )
|
|
|
|
init {
|
|
plugin.server.pluginManager.registerEvents( this, plugin )
|
|
|
|
gameTask = Bukkit.getScheduler().runTaskTimer( plugin, { ->
|
|
gameLoop()
|
|
}, 20L, 20L )
|
|
|
|
recraftManager.startRunnable()
|
|
|
|
Bukkit.getScheduler().runTaskLater( plugin, { ->
|
|
val world = Bukkit.getWorld( "world" )
|
|
|
|
world?.worldBorder?.apply {
|
|
center = Location( world, 0.0, 0.0, 0.0 )
|
|
size = startBorder
|
|
damageBuffer = 0.0
|
|
damageAmount = 1.0
|
|
}
|
|
}, 10L )
|
|
}
|
|
|
|
private var lobbyIdleCount: Int = 0
|
|
|
|
private fun gameLoop()
|
|
{
|
|
when( currentState )
|
|
{
|
|
GameState.LOBBY ->
|
|
{
|
|
if ( Bukkit.getOnlinePlayers().size >= minPlayers )
|
|
{
|
|
setGameState( GameState.STARTING )
|
|
timer = lobbyTime
|
|
}
|
|
else
|
|
{
|
|
if ( lobbyIdleCount >= 15 )
|
|
{
|
|
lobbyIdleCount = 0
|
|
Bukkit.getOnlinePlayers().forEach {
|
|
it.sendMsg( "game.lobby-idle", "current" to Bukkit.getOnlinePlayers().size.toString(), "min" to minPlayers.toString() )
|
|
}
|
|
}
|
|
lobbyIdleCount++
|
|
}
|
|
}
|
|
|
|
GameState.STARTING ->
|
|
{
|
|
if ( Bukkit.getOnlinePlayers().size < minPlayers )
|
|
{
|
|
setGameState( GameState.LOBBY )
|
|
Bukkit.getOnlinePlayers().forEach { player ->
|
|
player.sendMsg( "game.start-aborted" )
|
|
}
|
|
return
|
|
}
|
|
|
|
timer--
|
|
|
|
if (timer in listOf( 60, 30, 10, 5, 4, 3, 2, 1 ))
|
|
{
|
|
Bukkit.getOnlinePlayers().forEach { p ->
|
|
p.sendMsg( "timer.lobby", "time" to timer.toString() )
|
|
p.playSound( p.location, Sound.BLOCK_NOTE_BLOCK_HAT, 1f, 1f )
|
|
}
|
|
}
|
|
|
|
if ( timer <= 0 )
|
|
startGame()
|
|
}
|
|
|
|
GameState.INVINCIBILITY ->
|
|
{
|
|
timer--
|
|
|
|
if ( timer <= 0 )
|
|
startFighting()
|
|
else
|
|
{
|
|
Bukkit.getOnlinePlayers().forEach { p ->
|
|
p.sendActionBar(p.trans( "timer.actionbar-invincibility", "time" to timer.toString() ))
|
|
}
|
|
}
|
|
}
|
|
|
|
GameState.INGAME ->
|
|
{
|
|
timer++
|
|
updateCompass()
|
|
checkWin()
|
|
|
|
feastManager.onTick( timer )
|
|
pitManager.onTick( timer )
|
|
}
|
|
|
|
GameState.ENDING ->
|
|
{
|
|
timer--
|
|
|
|
if ( timer <= 0 )
|
|
Bukkit.shutdown()
|
|
}
|
|
}
|
|
}
|
|
|
|
fun setGameState(
|
|
newState: GameState
|
|
) {
|
|
this.currentState = newState
|
|
}
|
|
|
|
private fun startGame()
|
|
{
|
|
feastManager.reset()
|
|
pitManager.reset()
|
|
|
|
plugin.lobbyItemManager.clearAll()
|
|
|
|
setGameState( GameState.INVINCIBILITY )
|
|
timer = invincibilityTime
|
|
|
|
val world = Bukkit.getWorld( "world" ) ?: return
|
|
world.time = 0
|
|
world.setStorm( false )
|
|
|
|
world.worldBorder.apply {
|
|
center = Location( world, 0.0, 0.0, 0.0 )
|
|
size = startBorder
|
|
damageBuffer = 0.0
|
|
damageAmount = 1.0
|
|
}
|
|
|
|
val speedEffect = PotionEffect(
|
|
PotionEffectType.SPEED,
|
|
invincibilityTime * 20,
|
|
0,
|
|
false,
|
|
false,
|
|
true
|
|
)
|
|
|
|
val hasteEffect = PotionEffect(
|
|
PotionEffectType.HASTE,
|
|
invincibilityTime * 20,
|
|
0,
|
|
false,
|
|
false,
|
|
true
|
|
)
|
|
|
|
alivePlayers.clear()
|
|
Bukkit.getOnlinePlayers().forEach { player ->
|
|
alivePlayers.add( player.uniqueId )
|
|
|
|
player.gameMode = GameMode.SURVIVAL
|
|
player.health = player.getAttribute( Attribute.GENERIC_MAX_HEALTH )?.value ?: 20.0
|
|
player.foodLevel = 20
|
|
player.inventory.clear()
|
|
player.activePotionEffects.forEach { player.removePotionEffect( it.type ) }
|
|
player.addPotionEffects(listOf( speedEffect, hasteEffect ))
|
|
|
|
teleportRandomly( player, world, maxRadiusTeleport )
|
|
|
|
plugin.kitManager.applyKit( player ) // verteilt Items + ruft onAssign + passive.onActivate
|
|
plugin.perkManager.applyPerks( player )
|
|
|
|
player.inventory.addItem(ItemStack( Material.COMPASS ))
|
|
player.sendMsg( "game.started" )
|
|
}
|
|
|
|
plugin.rankingManager.startRound( Bukkit.getOnlinePlayers() )
|
|
|
|
Bukkit.getOnlinePlayers().forEach { player ->
|
|
player.sendMsg( "game.invincibility-start", "time" to invincibilityTime.toString() )
|
|
}
|
|
|
|
val variantName = plugin.config.getString( "lunarclient.variantName" )
|
|
|
|
plugin.discordWebhookManager.broadcastEmbed(
|
|
title = "🎮 Spiel gestartet! (${variantName})",
|
|
description = "Eine neue Runde SpeedHG mit **${Bukkit.getOnlinePlayers().size}** Spielern hat begonnen!",
|
|
colorHex = 0x55FF55 // Grün
|
|
)
|
|
}
|
|
|
|
private fun startFighting()
|
|
{
|
|
setGameState( GameState.INGAME )
|
|
timer = 0 // Reset Timer für "Ingame"
|
|
|
|
val world = Bukkit.getWorld( "world" ) ?: return
|
|
world.worldBorder.setSize( endBorder, borderShrinkTime )
|
|
|
|
plugin.antiRunningManager.resetTimers()
|
|
|
|
Bukkit.getOnlinePlayers().forEach { p ->
|
|
p.sendMsg( "game.fighting-started" )
|
|
p.playSound( p.location, Sound.ENTITY_ENDER_DRAGON_GROWL, 1f, 1f )
|
|
|
|
p.showTitle(Title.title(
|
|
p.trans( "title.fight-main" ),
|
|
p.trans( "title.fight-sub" )
|
|
))
|
|
}
|
|
}
|
|
|
|
fun onPlayerEliminated(
|
|
player: Player,
|
|
killer: Player?
|
|
) {
|
|
if (!alivePlayers.contains( player.uniqueId )) return
|
|
|
|
alivePlayers.remove( player.uniqueId )
|
|
player.gameMode = GameMode.SPECTATOR
|
|
|
|
plugin.statsManager.addDeath( player.uniqueId )
|
|
plugin.statsManager.addLoss( player.uniqueId )
|
|
plugin.rankingManager.onPlayerResult( player, isWinner = false )
|
|
|
|
if ( killer != null )
|
|
{
|
|
killer.exp += 0.5f
|
|
plugin.statsManager.addKill( killer.uniqueId )
|
|
plugin.rankingManager.registerRoundKill( killer.uniqueId )
|
|
}
|
|
|
|
player.inventory.contents.filterNotNull()
|
|
.filter { !plugin.kitManager.isKitItem( player, it ) }
|
|
.forEach { item ->
|
|
player.world.dropItemNaturally( player.location, item )
|
|
}
|
|
|
|
val msgKey = if ( killer != null ) "game.death-killed" else "game.death-pve"
|
|
val killerName = killer?.name ?: "Environment"
|
|
|
|
Bukkit.getOnlinePlayers().forEach { p ->
|
|
p.sendMsg( msgKey, "player" to player.name, "killer" to killerName, "left" to alivePlayers.size.toString() )
|
|
}
|
|
|
|
checkWin()
|
|
}
|
|
|
|
private fun checkWin() {
|
|
if (currentState != GameState.INGAME && currentState != GameState.INVINCIBILITY) return
|
|
|
|
val teamManager = plugin.presetTeamManager
|
|
|
|
val roundOver = when {
|
|
// Nur noch 0 oder 1 Spieler übrig → immer Ende
|
|
alivePlayers.size <= 1 -> true
|
|
|
|
// Teams aktiv: Alle Überlebenden im selben Team → Team gewinnt
|
|
teamManager.isEnabled && teamManager.allAliveInSameTeam(alivePlayers) -> true
|
|
|
|
else -> false
|
|
}
|
|
|
|
if (roundOver) {
|
|
val winnerUUID = alivePlayers.firstOrNull()
|
|
// Den sichtbaren Gewinner-Namen ermitteln:
|
|
// Bei Team-Sieg alle Mitglieder des Gewinner-Teams anzeigen.
|
|
val winnerName = buildWinnerName(winnerUUID)
|
|
endGame(winnerName)
|
|
}
|
|
}
|
|
|
|
private fun endGame(
|
|
winnerName: String
|
|
) {
|
|
setGameState( GameState.ENDING )
|
|
timer = 15
|
|
|
|
pitManager.reset()
|
|
|
|
val winnerUUID = alivePlayers.firstOrNull()
|
|
|
|
val winnerTeam = if ( plugin.presetTeamManager.isEnabled && winnerUUID != null )
|
|
plugin.presetTeamManager.getTeam( winnerUUID ) else null
|
|
|
|
Bukkit.getOnlinePlayers().forEach { p ->
|
|
val isWinner = winnerTeam?.contains( p.uniqueId ) ?: ( p.uniqueId == winnerUUID )
|
|
|
|
if ( isWinner )
|
|
{
|
|
plugin.statsManager.addWin( p.uniqueId )
|
|
plugin.rankingManager.onPlayerResult( p, isWinner = true )
|
|
}
|
|
|
|
p.showTitle(Title.title(
|
|
p.trans( "title.win-main", "winner" to winnerName ),
|
|
p.trans( "title.win-sub" )
|
|
))
|
|
p.sendMsg( "game.win-chat", "winner" to winnerName )
|
|
}
|
|
|
|
plugin.kitManager.clearAll()
|
|
plugin.perkManager.removeAllActivePerks()
|
|
|
|
plugin.discordWebhookManager.broadcastEmbed(
|
|
title = "🏆 Spiel beendet!",
|
|
description = "**$winnerName** hat das Spiel gewonnen! GG!",
|
|
colorHex = 0xFFAA00 // Gold
|
|
)
|
|
|
|
Bukkit.getScheduler().runTaskLater( plugin, { ->
|
|
plugin.podiumManager.launch( winnerUUID )
|
|
}, 60L )
|
|
}
|
|
|
|
// --- Helfer Methoden ---
|
|
|
|
private fun buildWinnerName(anyAliveUUID: UUID?): String {
|
|
anyAliveUUID ?: return "N/A"
|
|
|
|
val teamManager = plugin.presetTeamManager
|
|
if (!teamManager.isEnabled) {
|
|
return Bukkit.getPlayer(anyAliveUUID)?.name ?: "N/A"
|
|
}
|
|
|
|
val winnerTeam = teamManager.getTeam(anyAliveUUID)
|
|
return if (winnerTeam != null && winnerTeam.size > 1) {
|
|
// "PlayerA & PlayerB" als Team-Gewinner-String
|
|
winnerTeam.members
|
|
.mapNotNull { Bukkit.getPlayer(it)?.name }
|
|
.joinToString(" & ")
|
|
} else {
|
|
Bukkit.getPlayer(anyAliveUUID)?.name ?: "N/A"
|
|
}
|
|
}
|
|
|
|
private fun teleportRandomly(
|
|
player: Player,
|
|
world: World,
|
|
maxRadius: Double
|
|
) {
|
|
var loc: Location
|
|
var safe = false
|
|
var attempts = 0
|
|
|
|
do {
|
|
val x = Random.nextDouble( -maxRadius, maxRadius )
|
|
val z = Random.nextDouble( -maxRadius, maxRadius )
|
|
val y = world.getHighestBlockYAt( x.toInt(), z.toInt() ) + 1.0
|
|
loc = Location( world, x, y, z )
|
|
|
|
val block = loc.block.type
|
|
val below = loc.subtract( 0.0, 1.0, 0.0 ).block.type
|
|
|
|
if ( below.isSolid && below != Material.LAVA && below != Material.CACTUS && block == Material.AIR )
|
|
safe = true
|
|
attempts++
|
|
} while ( !safe && attempts < 20 )
|
|
|
|
player.teleport(loc.add( 0.0, 1.0, 0.0 ))
|
|
}
|
|
|
|
private fun updateCompass()
|
|
{
|
|
val players = Bukkit.getOnlinePlayers()
|
|
.filter { alivePlayers.contains( it.uniqueId ) && !plugin.perkManager.isGhost( it ) }
|
|
|
|
for ( p in players )
|
|
{
|
|
var nearest: Player? = null
|
|
var minDistance = Double.MAX_VALUE
|
|
|
|
for ( target in players )
|
|
{
|
|
if ( p == target ) continue
|
|
|
|
if ( plugin.presetTeamManager.isEnabled &&
|
|
plugin.presetTeamManager.areInSameTeam( p, target ))
|
|
continue
|
|
|
|
val dist = p.location.distanceSquared( target.location )
|
|
if ( dist < minDistance )
|
|
{
|
|
minDistance = dist
|
|
nearest = target
|
|
}
|
|
}
|
|
|
|
if ( nearest != null )
|
|
p.compassTarget = nearest.location
|
|
else
|
|
p.compassTarget = p.world.spawnLocation
|
|
}
|
|
}
|
|
|
|
// --- Event Listener Integration ---
|
|
|
|
@EventHandler
|
|
fun onDeath(
|
|
event: PlayerDeathEvent
|
|
) {
|
|
if ( currentState == GameState.INGAME || currentState == GameState.INVINCIBILITY )
|
|
{
|
|
event.deathMessage( null )
|
|
event.drops.clear()
|
|
|
|
Bukkit.getScheduler().runTask( plugin ) { ->
|
|
event.entity.spigot().respawn()
|
|
}
|
|
|
|
onPlayerEliminated( event.entity, event.entity.killer )
|
|
}
|
|
}
|
|
|
|
@EventHandler
|
|
fun onQuit(
|
|
event: PlayerQuitEvent
|
|
) {
|
|
if (alivePlayers.contains( event.player.uniqueId ))
|
|
{
|
|
if ( currentState == GameState.INGAME || currentState == GameState.INVINCIBILITY )
|
|
{
|
|
val lastDamageCause = event.player.lastDamageCause
|
|
|
|
if ( lastDamageCause != null && lastDamageCause is EntityDamageByEntityEvent )
|
|
{
|
|
val attacker = lastDamageCause.damager
|
|
|
|
if ( attacker is Player )
|
|
{
|
|
onPlayerEliminated( event.player, attacker )
|
|
return
|
|
}
|
|
}
|
|
|
|
onPlayerEliminated( event.player, null ) // PVE Tod
|
|
}
|
|
else alivePlayers.remove( event.player.uniqueId )
|
|
}
|
|
}
|
|
|
|
@EventHandler
|
|
fun onDamage(
|
|
event: EntityDamageEvent
|
|
) {
|
|
if ( currentState == GameState.INVINCIBILITY && event.entity is Player )
|
|
event.isCancelled = true
|
|
|
|
if ( currentState == GameState.LOBBY || currentState == GameState.STARTING || currentState == GameState.ENDING )
|
|
event.isCancelled = true // Nie Schaden in Lobby/Ende
|
|
}
|
|
|
|
} |