Introduce a complete passive perk system: Perk base class, PerkManager (registration, selection, lifecycle, persistence), and PlayerPerksRepository (DB schema + upsert/find). Add four example perks (Oracle, Vampire, Featherweight, Bloodlust) and a single PerkEventDispatcher to route combat/environment/kill events to active perks. Provide PerkSelectorMenu GUI and /perks command, integrate perk initialization, registration, application and cleanup into SpeedHG and GameManager, and hook load/evict into StatsListener. Also add language entries and register the command in plugin.yml. This change enables players to select up to two passive perks, persists selections, and dispatches relevant events to perk implementations.
442 lines
11 KiB
Kotlin
442 lines
11 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)
|
|
|
|
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()
|
|
}
|
|
|
|
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()
|
|
|
|
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,
|
|
timer,
|
|
0,
|
|
false,
|
|
false,
|
|
true
|
|
)
|
|
|
|
val hasteEffect = PotionEffect(
|
|
PotionEffectType.HASTE,
|
|
timer,
|
|
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, startBorder / 2 )
|
|
|
|
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() )
|
|
}
|
|
|
|
plugin.discordWebhookManager.broadcastEmbed(
|
|
title = "🎮 Spiel gestartet!",
|
|
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().forEach {
|
|
player.world.dropItemNaturally( player.location, it )
|
|
}
|
|
|
|
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
|
|
|
|
if ( alivePlayers.size <= 1 )
|
|
{
|
|
val winnerUUID = alivePlayers.firstOrNull()
|
|
val winnerName = if ( winnerUUID != null ) Bukkit.getPlayer( winnerUUID )?.name ?: "N/A" else "N/A"
|
|
endGame( winnerName )
|
|
}
|
|
}
|
|
|
|
private fun endGame(
|
|
winnerName: String
|
|
) {
|
|
setGameState( GameState.ENDING )
|
|
timer = 15
|
|
|
|
pitManager.reset()
|
|
|
|
val winnerUUID = alivePlayers.firstOrNull()
|
|
|
|
Bukkit.getOnlinePlayers().forEach { p ->
|
|
if ( p.uniqueId == winnerUUID )
|
|
{
|
|
plugin.statsManager.addWin( p.uniqueId )
|
|
plugin.rankingManager.onPlayerResult( p, isWinner = true )
|
|
}
|
|
}
|
|
|
|
plugin.kitManager.clearAll()
|
|
plugin.perkManager.removeAllActivePerks()
|
|
|
|
Bukkit.getOnlinePlayers().forEach { p ->
|
|
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.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 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 ) }
|
|
|
|
for ( p in players )
|
|
{
|
|
var nearest: Player? = null
|
|
var minDistance = Double.MAX_VALUE
|
|
|
|
for ( target in players )
|
|
{
|
|
if ( 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
|
|
}
|
|
|
|
} |