Replace TeamManager with PresetTeam GUI system
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.
This commit is contained in:
@@ -7,7 +7,6 @@ import club.mcscrims.speedhg.command.KitCommand
|
|||||||
import club.mcscrims.speedhg.command.LeaderboardCommand
|
import club.mcscrims.speedhg.command.LeaderboardCommand
|
||||||
import club.mcscrims.speedhg.command.PerksCommand
|
import club.mcscrims.speedhg.command.PerksCommand
|
||||||
import club.mcscrims.speedhg.command.RankingCommand
|
import club.mcscrims.speedhg.command.RankingCommand
|
||||||
import club.mcscrims.speedhg.command.TeamCommand
|
|
||||||
import club.mcscrims.speedhg.command.TimerCommand
|
import club.mcscrims.speedhg.command.TimerCommand
|
||||||
import club.mcscrims.speedhg.config.CustomGameManager
|
import club.mcscrims.speedhg.config.CustomGameManager
|
||||||
import club.mcscrims.speedhg.config.LanguageManager
|
import club.mcscrims.speedhg.config.LanguageManager
|
||||||
@@ -48,8 +47,8 @@ import club.mcscrims.speedhg.ranking.RankingManager
|
|||||||
import club.mcscrims.speedhg.scoreboard.ScoreboardManager
|
import club.mcscrims.speedhg.scoreboard.ScoreboardManager
|
||||||
import club.mcscrims.speedhg.scoreboard.TablistManager
|
import club.mcscrims.speedhg.scoreboard.TablistManager
|
||||||
import club.mcscrims.speedhg.scoreboard.VolcanoServerRankProvider
|
import club.mcscrims.speedhg.scoreboard.VolcanoServerRankProvider
|
||||||
import club.mcscrims.speedhg.team.TeamListener
|
import club.mcscrims.speedhg.team.gui.PresetTeamManager
|
||||||
import club.mcscrims.speedhg.team.TeamManager
|
import club.mcscrims.speedhg.team.gui.TeamSelectionListener
|
||||||
import club.mcscrims.speedhg.webhook.DiscordWebhookManager
|
import club.mcscrims.speedhg.webhook.DiscordWebhookManager
|
||||||
import club.mcscrims.speedhg.world.DataPackManager
|
import club.mcscrims.speedhg.world.DataPackManager
|
||||||
import club.mcscrims.speedhg.world.SurfaceBlockPopulator
|
import club.mcscrims.speedhg.world.SurfaceBlockPopulator
|
||||||
@@ -115,9 +114,6 @@ class SpeedHG : JavaPlugin() {
|
|||||||
lateinit var dataPackManager: DataPackManager
|
lateinit var dataPackManager: DataPackManager
|
||||||
private set
|
private set
|
||||||
|
|
||||||
lateinit var teamManager: TeamManager
|
|
||||||
private set
|
|
||||||
|
|
||||||
lateinit var lunarClientManager: LunarClientManager
|
lateinit var lunarClientManager: LunarClientManager
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -130,6 +126,9 @@ class SpeedHG : JavaPlugin() {
|
|||||||
lateinit var lobbyAnnouncer: LobbyAnnouncer
|
lateinit var lobbyAnnouncer: LobbyAnnouncer
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
lateinit var presetTeamManager: PresetTeamManager
|
||||||
|
private set
|
||||||
|
|
||||||
override fun onLoad()
|
override fun onLoad()
|
||||||
{
|
{
|
||||||
instance = this
|
instance = this
|
||||||
@@ -189,17 +188,11 @@ class SpeedHG : JavaPlugin() {
|
|||||||
lunarClientManager = LunarClientManager( this )
|
lunarClientManager = LunarClientManager( this )
|
||||||
lobbyItemManager = LobbyItemManager( this )
|
lobbyItemManager = LobbyItemManager( this )
|
||||||
tablistManager = TablistManager( this, VolcanoServerRankProvider() )
|
tablistManager = TablistManager( this, VolcanoServerRankProvider() )
|
||||||
|
presetTeamManager = PresetTeamManager( this )
|
||||||
|
|
||||||
perkManager = PerkManager( this )
|
perkManager = PerkManager( this )
|
||||||
perkManager.initialize()
|
perkManager.initialize()
|
||||||
|
|
||||||
teamManager = TeamManager( this )
|
|
||||||
|
|
||||||
// Cleanup-Task für abgelaufene Einladungen (1x/Sekunde)
|
|
||||||
Bukkit.getScheduler().runTaskTimer( this, { ->
|
|
||||||
teamManager.cleanExpiredInvites()
|
|
||||||
}, 20L, 20L )
|
|
||||||
|
|
||||||
disasterManager = DisasterManager( this )
|
disasterManager = DisasterManager( this )
|
||||||
disasterManager.start()
|
disasterManager.start()
|
||||||
|
|
||||||
@@ -220,7 +213,6 @@ class SpeedHG : JavaPlugin() {
|
|||||||
podiumManager.cleanup()
|
podiumManager.cleanup()
|
||||||
if ( ::lobbyAnnouncer.isInitialized ) lobbyAnnouncer.stop()
|
if ( ::lobbyAnnouncer.isInitialized ) lobbyAnnouncer.stop()
|
||||||
if ( ::perkManager.isInitialized ) perkManager.shutdown()
|
if ( ::perkManager.isInitialized ) perkManager.shutdown()
|
||||||
if ( ::teamManager.isInitialized ) teamManager.reset()
|
|
||||||
if ( ::tablistManager.isInitialized ) tablistManager.shutdown()
|
if ( ::tablistManager.isInitialized ) tablistManager.shutdown()
|
||||||
if ( ::statsManager.isInitialized ) statsManager.shutdown()
|
if ( ::statsManager.isInitialized ) statsManager.shutdown()
|
||||||
if ( ::databaseManager.isInitialized ) databaseManager.disconnect()
|
if ( ::databaseManager.isInitialized ) databaseManager.disconnect()
|
||||||
@@ -288,12 +280,6 @@ class SpeedHG : JavaPlugin() {
|
|||||||
tabCompleter = rankingCommand
|
tabCompleter = rankingCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
val teamCommand = TeamCommand()
|
|
||||||
getCommand( "team" )?.apply {
|
|
||||||
setExecutor( teamCommand )
|
|
||||||
tabCompleter = teamCommand
|
|
||||||
}
|
|
||||||
|
|
||||||
getCommand( "leaderboard" )?.setExecutor( LeaderboardCommand() )
|
getCommand( "leaderboard" )?.setExecutor( LeaderboardCommand() )
|
||||||
getCommand( "perks" )?.setExecutor( PerksCommand() )
|
getCommand( "perks" )?.setExecutor( PerksCommand() )
|
||||||
getCommand( "help" )?.setExecutor( HelpCommand() )
|
getCommand( "help" )?.setExecutor( HelpCommand() )
|
||||||
@@ -310,10 +296,10 @@ class SpeedHG : JavaPlugin() {
|
|||||||
pm.registerEvents( StatsListener(), this )
|
pm.registerEvents( StatsListener(), this )
|
||||||
pm.registerEvents( MenuListener(), this )
|
pm.registerEvents( MenuListener(), this )
|
||||||
pm.registerEvents(PerkEventDispatcher( this, perkManager ), this )
|
pm.registerEvents(PerkEventDispatcher( this, perkManager ), this )
|
||||||
pm.registerEvents( TeamListener(), this )
|
|
||||||
pm.registerEvents( lobbyItemManager, this )
|
pm.registerEvents( lobbyItemManager, this )
|
||||||
pm.registerEvents(ChatListener( this, VolcanoServerRankProvider() ), this )
|
pm.registerEvents(ChatListener( this, VolcanoServerRankProvider() ), this )
|
||||||
pm.registerEvents(KnockbackListener( this ), this )
|
pm.registerEvents(KnockbackListener( this ), this )
|
||||||
|
pm.registerEvents(TeamSelectionListener( this ), this )
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun registerRecipes()
|
private fun registerRecipes()
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class LunarClientManager(
|
|||||||
private fun setRichPresence(
|
private fun setRichPresence(
|
||||||
player: ApolloPlayer
|
player: ApolloPlayer
|
||||||
) {
|
) {
|
||||||
val teamSize = plugin.teamManager.getTeam( player.uniqueId )?.size ?: 1
|
val teamSize = plugin.presetTeamManager.getTeam( player.uniqueId )?.size ?: 1
|
||||||
val currentState = plugin.gameManager.currentState.name
|
val currentState = plugin.gameManager.currentState.name
|
||||||
|
|
||||||
val presence = ServerRichPresence.builder()
|
val presence = ServerRichPresence.builder()
|
||||||
@@ -57,7 +57,7 @@ class LunarClientManager(
|
|||||||
.gameVariantName(plugin.config.getString( "lunarclient.variantName" ))
|
.gameVariantName(plugin.config.getString( "lunarclient.variantName" ))
|
||||||
.playerState( currentState )
|
.playerState( currentState )
|
||||||
.teamCurrentSize( teamSize )
|
.teamCurrentSize( teamSize )
|
||||||
.teamMaxSize( plugin.teamManager.maxTeamSize )
|
.teamMaxSize( plugin.presetTeamManager.maxTeamSize )
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
richPresenceModule.overrideServerRichPresence( player, presence )
|
richPresenceModule.overrideServerRichPresence( player, presence )
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
package club.mcscrims.speedhg.command
|
|
||||||
|
|
||||||
import club.mcscrims.speedhg.SpeedHG
|
|
||||||
import club.mcscrims.speedhg.game.GameState
|
|
||||||
import club.mcscrims.speedhg.team.TeamManager
|
|
||||||
import club.mcscrims.speedhg.util.sendMsg
|
|
||||||
import org.bukkit.command.Command
|
|
||||||
import org.bukkit.command.CommandExecutor
|
|
||||||
import org.bukkit.command.CommandSender
|
|
||||||
import org.bukkit.command.TabCompleter
|
|
||||||
import org.bukkit.entity.Player
|
|
||||||
|
|
||||||
class TeamCommand : CommandExecutor, TabCompleter {
|
|
||||||
|
|
||||||
private val plugin get() = SpeedHG.instance
|
|
||||||
|
|
||||||
// ── Guard: Teams nur in Lobby-Phasen ändern ──────────────────────────────
|
|
||||||
|
|
||||||
private fun isLobbyPhase(): Boolean = when (plugin.gameManager.currentState) {
|
|
||||||
GameState.LOBBY, GameState.STARTING -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// onCommand dispatch
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
override fun onCommand(
|
|
||||||
sender: CommandSender,
|
|
||||||
command: Command,
|
|
||||||
label: String,
|
|
||||||
args: Array<out String>
|
|
||||||
): Boolean {
|
|
||||||
val player = sender as? Player ?: run {
|
|
||||||
sender.sendMessage("§cOnly players can use this command.")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!plugin.teamManager.isEnabled) {
|
|
||||||
player.sendMsg("team.disabled")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
when (args.firstOrNull()?.lowercase()) {
|
|
||||||
"invite" -> handleInvite(player, args)
|
|
||||||
"accept" -> handleAccept(player, args)
|
|
||||||
"deny" -> handleDeny(player, args)
|
|
||||||
"leave" -> handleLeave(player)
|
|
||||||
"kick" -> handleKick(player, args)
|
|
||||||
"info" -> handleInfo(player)
|
|
||||||
else -> player.sendMsg("team.usage")
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Subcommand Handlers
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
private fun handleInvite(player: Player, args: Array<out String>) {
|
|
||||||
if (!isLobbyPhase()) { player.sendMsg("team.game_running"); return }
|
|
||||||
|
|
||||||
val targetName = args.getOrNull(1) ?: run { player.sendMsg("team.invite.usage"); return }
|
|
||||||
val target = plugin.server.getPlayerExact(targetName) ?: run {
|
|
||||||
player.sendMsg("team.player_not_found", "name" to targetName); return
|
|
||||||
}
|
|
||||||
|
|
||||||
when (plugin.teamManager.invite(player, target)) {
|
|
||||||
is TeamManager.InviteResult.Success -> {
|
|
||||||
player.sendMsg("team.invite.sent", "name" to target.name)
|
|
||||||
target.sendMsg("team.invite.received",
|
|
||||||
"name" to player.name,
|
|
||||||
"time" to (TeamManager.INVITE_TTL_MS / 1000).toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is TeamManager.InviteResult.TeamsDisabled -> player.sendMsg("team.disabled")
|
|
||||||
is TeamManager.InviteResult.InvitedSelf -> player.sendMsg("team.invite.self")
|
|
||||||
is TeamManager.InviteResult.AlreadyInSameTeam -> player.sendMsg("team.invite.already_teammate")
|
|
||||||
is TeamManager.InviteResult.TargetAlreadyInTeam -> player.sendMsg("team.invite.target_has_team", "name" to target.name)
|
|
||||||
is TeamManager.InviteResult.SenderAlreadyInFullTeam -> player.sendMsg("team.invite.team_full")
|
|
||||||
is TeamManager.InviteResult.InviteAlreadyPending -> player.sendMsg("team.invite.already_pending", "name" to target.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleAccept(player: Player, args: Array<out String>) {
|
|
||||||
if (!isLobbyPhase()) { player.sendMsg("team.game_running"); return }
|
|
||||||
|
|
||||||
val inviterName = args.getOrNull(1) ?: run { player.sendMsg("team.accept.usage"); return }
|
|
||||||
val inviter = plugin.server.getPlayerExact(inviterName) ?: run {
|
|
||||||
player.sendMsg("team.player_not_found", "name" to inviterName); return
|
|
||||||
}
|
|
||||||
|
|
||||||
when (val result = plugin.teamManager.accept(player, inviter.uniqueId)) {
|
|
||||||
is TeamManager.AcceptResult.Success -> {
|
|
||||||
val memberNames = result.team.members
|
|
||||||
.mapNotNull { plugin.server.getPlayer(it)?.name }
|
|
||||||
.joinToString(", ")
|
|
||||||
|
|
||||||
// Alle Teammitglieder benachrichtigen
|
|
||||||
result.team.members.forEach { uuid ->
|
|
||||||
plugin.server.getPlayer(uuid)?.sendMsg(
|
|
||||||
"team.accept.joined",
|
|
||||||
"name" to player.name,
|
|
||||||
"members" to memberNames
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is TeamManager.AcceptResult.TeamsDisabled -> player.sendMsg("team.disabled")
|
|
||||||
is TeamManager.AcceptResult.NoInvite -> player.sendMsg("team.accept.no_invite", "name" to inviterName)
|
|
||||||
is TeamManager.AcceptResult.WrongInviter -> player.sendMsg("team.accept.no_invite", "name" to inviterName)
|
|
||||||
is TeamManager.AcceptResult.InviteExpired -> player.sendMsg("team.accept.expired", "name" to inviterName)
|
|
||||||
is TeamManager.AcceptResult.AlreadyInTeam -> player.sendMsg("team.already_in_team")
|
|
||||||
is TeamManager.AcceptResult.TeamFull -> player.sendMsg("team.invite.team_full")
|
|
||||||
is TeamManager.AcceptResult.InviterNotFound -> player.sendMsg("team.player_not_found", "name" to inviterName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleDeny(player: Player, args: Array<out String>) {
|
|
||||||
if (!isLobbyPhase()) { player.sendMsg("team.game_running"); return }
|
|
||||||
|
|
||||||
val inviterName = args.getOrNull(1) ?: run { player.sendMsg("team.deny.usage"); return }
|
|
||||||
val inviter = plugin.server.getPlayerExact(inviterName) ?: run {
|
|
||||||
player.sendMsg("team.player_not_found", "name" to inviterName); return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plugin.teamManager.deny(player, inviter.uniqueId)) {
|
|
||||||
player.sendMsg("team.deny.success", "name" to inviterName)
|
|
||||||
inviter.sendMsg("team.deny.received", "name" to player.name)
|
|
||||||
} else {
|
|
||||||
player.sendMsg("team.accept.no_invite", "name" to inviterName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleLeave(player: Player) {
|
|
||||||
if (!isLobbyPhase()) { player.sendMsg("team.game_running"); return }
|
|
||||||
|
|
||||||
val team = plugin.teamManager.getTeam(player)
|
|
||||||
|
|
||||||
when (plugin.teamManager.leave(player)) {
|
|
||||||
is TeamManager.LeaveResult.NotInTeam -> {
|
|
||||||
player.sendMsg("team.not_in_team")
|
|
||||||
}
|
|
||||||
is TeamManager.LeaveResult.TeamDisbanded -> {
|
|
||||||
// Alle Ex-Mitglieder (außer dem Verlassenden) benachrichtigen
|
|
||||||
team?.members?.forEach { uuid ->
|
|
||||||
plugin.server.getPlayer(uuid)?.sendMsg("team.leave.disbanded")
|
|
||||||
}
|
|
||||||
player.sendMsg("team.leave.disbanded")
|
|
||||||
}
|
|
||||||
is TeamManager.LeaveResult.Success -> {
|
|
||||||
player.sendMsg("team.leave.success")
|
|
||||||
// Verbleibende Mitglieder informieren
|
|
||||||
team?.members?.forEach { uuid ->
|
|
||||||
plugin.server.getPlayer(uuid)?.sendMsg(
|
|
||||||
"team.leave.member_left", "name" to player.name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleKick(player: Player, args: Array<out String>) {
|
|
||||||
if (!isLobbyPhase()) { player.sendMsg("team.game_running"); return }
|
|
||||||
|
|
||||||
val targetName = args.getOrNull(1) ?: run { player.sendMsg("team.kick.usage"); return }
|
|
||||||
val target = plugin.server.getPlayerExact(targetName) ?: run {
|
|
||||||
player.sendMsg("team.player_not_found", "name" to targetName); return
|
|
||||||
}
|
|
||||||
|
|
||||||
when (plugin.teamManager.kick(player, target)) {
|
|
||||||
is TeamManager.KickResult.Success -> {
|
|
||||||
player.sendMsg("team.kick.success", "name" to target.name)
|
|
||||||
target.sendMsg("team.kick.received", "name" to player.name)
|
|
||||||
}
|
|
||||||
is TeamManager.KickResult.NotInTeam -> player.sendMsg("team.not_in_team")
|
|
||||||
is TeamManager.KickResult.NotLeader -> player.sendMsg("team.kick.not_leader")
|
|
||||||
is TeamManager.KickResult.TargetNotInTeam -> player.sendMsg("team.kick.not_in_your_team", "name" to targetName)
|
|
||||||
is TeamManager.KickResult.CannotKickSelf -> player.sendMsg("team.kick.self")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleInfo(player: Player) {
|
|
||||||
val team = plugin.teamManager.getTeam(player) ?: run {
|
|
||||||
player.sendMsg("team.not_in_team"); return
|
|
||||||
}
|
|
||||||
|
|
||||||
player.sendMsg("team.info.header")
|
|
||||||
team.members.forEachIndexed { i, uuid ->
|
|
||||||
val name = plugin.server.getPlayer(uuid)?.name ?: uuid.toString().take(8)
|
|
||||||
val isLeader = team.isLeader(uuid)
|
|
||||||
player.sendMsg(
|
|
||||||
"team.info.member",
|
|
||||||
"index" to (i + 1).toString(),
|
|
||||||
"name" to name,
|
|
||||||
"leader" to if (isLeader) " ★" else ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
player.sendMsg("team.info.footer", "size" to team.size.toString(), "max" to plugin.teamManager.maxTeamSize.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Tab Completion
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
override fun onTabComplete(
|
|
||||||
sender: CommandSender,
|
|
||||||
command: Command,
|
|
||||||
label: String,
|
|
||||||
args: Array<out String>
|
|
||||||
): List<String> {
|
|
||||||
if (sender !is Player) return emptyList()
|
|
||||||
|
|
||||||
if (args.size == 1) {
|
|
||||||
return listOf("invite", "accept", "deny", "leave", "kick", "info")
|
|
||||||
.filter { it.startsWith(args[0], ignoreCase = true) }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.size == 2) {
|
|
||||||
return when (args[0].lowercase()) {
|
|
||||||
"invite", "accept", "deny", "kick" ->
|
|
||||||
plugin.server.onlinePlayers
|
|
||||||
.filter { it != sender }
|
|
||||||
.map { it.name }
|
|
||||||
.filter { it.startsWith(args[1], ignoreCase = true) }
|
|
||||||
else -> emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -295,7 +295,7 @@ class GameManager(
|
|||||||
private fun checkWin() {
|
private fun checkWin() {
|
||||||
if (currentState != GameState.INGAME && currentState != GameState.INVINCIBILITY) return
|
if (currentState != GameState.INGAME && currentState != GameState.INVINCIBILITY) return
|
||||||
|
|
||||||
val teamManager = plugin.teamManager
|
val teamManager = plugin.presetTeamManager
|
||||||
|
|
||||||
val roundOver = when {
|
val roundOver = when {
|
||||||
// Nur noch 0 oder 1 Spieler übrig → immer Ende
|
// Nur noch 0 oder 1 Spieler übrig → immer Ende
|
||||||
@@ -326,8 +326,8 @@ class GameManager(
|
|||||||
|
|
||||||
val winnerUUID = alivePlayers.firstOrNull()
|
val winnerUUID = alivePlayers.firstOrNull()
|
||||||
|
|
||||||
val winnerTeam = if ( plugin.teamManager.isEnabled && winnerUUID != null )
|
val winnerTeam = if ( plugin.presetTeamManager.isEnabled && winnerUUID != null )
|
||||||
plugin.teamManager.getTeam( winnerUUID ) else null
|
plugin.presetTeamManager.getTeam( winnerUUID ) else null
|
||||||
|
|
||||||
Bukkit.getOnlinePlayers().forEach { p ->
|
Bukkit.getOnlinePlayers().forEach { p ->
|
||||||
val isWinner = winnerTeam?.contains( p.uniqueId ) ?: ( p.uniqueId == winnerUUID )
|
val isWinner = winnerTeam?.contains( p.uniqueId ) ?: ( p.uniqueId == winnerUUID )
|
||||||
@@ -364,7 +364,7 @@ class GameManager(
|
|||||||
private fun buildWinnerName(anyAliveUUID: UUID?): String {
|
private fun buildWinnerName(anyAliveUUID: UUID?): String {
|
||||||
anyAliveUUID ?: return "N/A"
|
anyAliveUUID ?: return "N/A"
|
||||||
|
|
||||||
val teamManager = plugin.teamManager
|
val teamManager = plugin.presetTeamManager
|
||||||
if (!teamManager.isEnabled) {
|
if (!teamManager.isEnabled) {
|
||||||
return Bukkit.getPlayer(anyAliveUUID)?.name ?: "N/A"
|
return Bukkit.getPlayer(anyAliveUUID)?.name ?: "N/A"
|
||||||
}
|
}
|
||||||
@@ -420,8 +420,8 @@ class GameManager(
|
|||||||
{
|
{
|
||||||
if ( p == target ) continue
|
if ( p == target ) continue
|
||||||
|
|
||||||
if ( plugin.teamManager.isEnabled &&
|
if ( plugin.presetTeamManager.isEnabled &&
|
||||||
plugin.teamManager.areInSameTeam( p, target ))
|
plugin.presetTeamManager.areInSameTeam( p, target ))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
val dist = p.location.distanceSquared( target.location )
|
val dist = p.location.distanceSquared( target.location )
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import club.mcscrims.speedhg.gui.menu.KitSelectorMenu
|
|||||||
import club.mcscrims.speedhg.gui.menu.LeaderboardMenu
|
import club.mcscrims.speedhg.gui.menu.LeaderboardMenu
|
||||||
import club.mcscrims.speedhg.gui.menu.PerkSelectorMenu
|
import club.mcscrims.speedhg.gui.menu.PerkSelectorMenu
|
||||||
import club.mcscrims.speedhg.gui.menu.StatsMenu
|
import club.mcscrims.speedhg.gui.menu.StatsMenu
|
||||||
|
import club.mcscrims.speedhg.team.gui.TeamSelectionMenu
|
||||||
import club.mcscrims.speedhg.util.ItemBuilder
|
import club.mcscrims.speedhg.util.ItemBuilder
|
||||||
import net.kyori.adventure.text.format.TextDecoration
|
import net.kyori.adventure.text.format.TextDecoration
|
||||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||||
@@ -64,6 +65,7 @@ class LobbyItemManager(
|
|||||||
const val TAG_TUTORIAL = "tutorial"
|
const val TAG_TUTORIAL = "tutorial"
|
||||||
const val TAG_STATS = "stats"
|
const val TAG_STATS = "stats"
|
||||||
const val TAG_LEADERBOARD = "leaderboard"
|
const val TAG_LEADERBOARD = "leaderboard"
|
||||||
|
const val TAG_TEAMS = "teams"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Item definitions ──────────────────────────────────────────────────────
|
// ── Item definitions ──────────────────────────────────────────────────────
|
||||||
@@ -145,6 +147,17 @@ class LobbyItemManager(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildTeamItem(
|
||||||
|
player: Player
|
||||||
|
): ItemStack
|
||||||
|
{
|
||||||
|
return ItemBuilder( Material.WHITE_WOOL )
|
||||||
|
.name( plugin.languageManager.getComponent( player, "game.lobby-items.teams.name", mapOf() ) )
|
||||||
|
.lore( listOf( plugin.languageManager.getRawMessage( player, "game.lobby-items.teams.lore" ) ) )
|
||||||
|
.pdc( key, PersistentDataType.STRING, TAG_TEAMS )
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
// ── Public API ────────────────────────────────────────────────────────────
|
// ── Public API ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -159,6 +172,10 @@ class LobbyItemManager(
|
|||||||
player.inventory.clear()
|
player.inventory.clear()
|
||||||
player.inventory.setItem( 0, buildKitItem( player ) )
|
player.inventory.setItem( 0, buildKitItem( player ) )
|
||||||
player.inventory.setItem( 1, buildPerkItem( player ) )
|
player.inventory.setItem( 1, buildPerkItem( player ) )
|
||||||
|
|
||||||
|
if ( plugin.presetTeamManager.isEnabled )
|
||||||
|
player.inventory.setItem( 2, buildTeamItem( player ) )
|
||||||
|
|
||||||
player.inventory.setItem( 4, buildTutorialItem( player ) )
|
player.inventory.setItem( 4, buildTutorialItem( player ) )
|
||||||
player.inventory.setItem( 7, buildStatsItem( player ) )
|
player.inventory.setItem( 7, buildStatsItem( player ) )
|
||||||
player.inventory.setItem( 8, buildLeaderboardItem( player ) )
|
player.inventory.setItem( 8, buildLeaderboardItem( player ) )
|
||||||
@@ -290,6 +307,7 @@ class LobbyItemManager(
|
|||||||
{
|
{
|
||||||
TAG_KITS -> KitSelectorMenu( event.player ).open( event.player )
|
TAG_KITS -> KitSelectorMenu( event.player ).open( event.player )
|
||||||
TAG_PERKS -> PerkSelectorMenu( event.player ).open( event.player )
|
TAG_PERKS -> PerkSelectorMenu( event.player ).open( event.player )
|
||||||
|
TAG_TEAMS -> TeamSelectionMenu( event.player ).open( event.player )
|
||||||
TAG_TUTORIAL -> openTutorialBook( event.player )
|
TAG_TUTORIAL -> openTutorialBook( event.player )
|
||||||
TAG_STATS -> StatsMenu( event.player ).open( event.player )
|
TAG_STATS -> StatsMenu( event.player ).open( event.player )
|
||||||
TAG_LEADERBOARD -> LeaderboardMenu( event.player ).open( event.player )
|
TAG_LEADERBOARD -> LeaderboardMenu( event.player ).open( event.player )
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class OraclePerk : Perk() {
|
|||||||
.filter { it != player.uniqueId }
|
.filter { it != player.uniqueId }
|
||||||
.mapNotNull { plugin.server.getPlayer(it) }
|
.mapNotNull { plugin.server.getPlayer(it) }
|
||||||
.filter { !plugin.perkManager.isGhost(it) }
|
.filter { !plugin.perkManager.isGhost(it) }
|
||||||
.filter { !plugin.teamManager.areInSameTeam( player, it ) }
|
.filter { !plugin.presetTeamManager.areInSameTeam( player, it ) }
|
||||||
.minByOrNull { it.location.distanceSquared(player.location) }
|
.minByOrNull { it.location.distanceSquared(player.location) }
|
||||||
|
|
||||||
private fun buildTrackerComponent(player: Player, nearest: Player): Component {
|
private fun buildTrackerComponent(player: Player, nearest: Player): Component {
|
||||||
|
|||||||
@@ -73,6 +73,14 @@ class ScoreboardManager(
|
|||||||
val stats = plugin.statsManager.getCachedStats( player.uniqueId )
|
val stats = plugin.statsManager.getCachedStats( player.uniqueId )
|
||||||
val score = stats?.scrimScore ?: 0
|
val score = stats?.scrimScore ?: 0
|
||||||
val games = ( stats?.wins ?: 0 ) + ( stats?.losses ?: 0 )
|
val games = ( stats?.wins ?: 0 ) + ( stats?.losses ?: 0 )
|
||||||
|
|
||||||
|
val teamsEnabled = plugin.presetTeamManager.isEnabled
|
||||||
|
val presetTeam = if ( teamsEnabled ) plugin.presetTeamManager.getTeam( player ) else null
|
||||||
|
val teamName = presetTeam?.name ?: "-"
|
||||||
|
val teamMembers = presetTeam?.members
|
||||||
|
?.mapNotNull { Bukkit.getPlayer( it )?.name }
|
||||||
|
?.joinToString( ", " ) ?: "-"
|
||||||
|
|
||||||
val rankComponent = Rank.getFormattedRankName( score, games )
|
val rankComponent = Rank.getFormattedRankName( score, games )
|
||||||
|
|
||||||
val lines: List<Component>
|
val lines: List<Component>
|
||||||
@@ -81,9 +89,20 @@ class ScoreboardManager(
|
|||||||
{
|
{
|
||||||
val timeString = if ( state == GameState.STARTING ) formatTime( gm.timer ) else "Waiting..."
|
val timeString = if ( state == GameState.STARTING ) formatTime( gm.timer ) else "Waiting..."
|
||||||
|
|
||||||
|
// Wähle den richtigen Scoreboard-Key je nach Team-Status
|
||||||
|
val key = if ( teamsEnabled ) "scoreboard.lobby_teams" else "scoreboard.lobby"
|
||||||
|
|
||||||
lines = plugin.languageManager.getMessageList(
|
lines = plugin.languageManager.getMessageList(
|
||||||
player, "scoreboard.lobby",
|
player, key,
|
||||||
mapOf( "online" to online, "max" to max, "time" to timeString, "style" to styleName, "variant" to variantName ),
|
mapOf(
|
||||||
|
"online" to online,
|
||||||
|
"max" to max,
|
||||||
|
"time" to timeString,
|
||||||
|
"style" to styleName,
|
||||||
|
"variant" to variantName,
|
||||||
|
"team" to teamName,
|
||||||
|
"members" to teamMembers
|
||||||
|
),
|
||||||
mapOf( "kit" to kitName, "rank" to rankComponent )
|
mapOf( "kit" to kitName, "rank" to rankComponent )
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -94,9 +113,20 @@ class ScoreboardManager(
|
|||||||
val kills = player.getStatistic( Statistic.PLAYER_KILLS ).toString()
|
val kills = player.getStatistic( Statistic.PLAYER_KILLS ).toString()
|
||||||
val border = String.format( "%.0f", player.world.worldBorder.size )
|
val border = String.format( "%.0f", player.world.worldBorder.size )
|
||||||
|
|
||||||
|
val key = if ( teamsEnabled ) "scoreboard.ingame_teams" else "scoreboard.ingame"
|
||||||
|
|
||||||
lines = plugin.languageManager.getMessageList(
|
lines = plugin.languageManager.getMessageList(
|
||||||
player, "scoreboard.ingame",
|
player, key,
|
||||||
mapOf( "timer" to timeString, "alive" to alive, "kills" to kills, "border" to border, "style" to styleName, "variant" to variantName ),
|
mapOf(
|
||||||
|
"timer" to timeString,
|
||||||
|
"alive" to alive,
|
||||||
|
"kills" to kills,
|
||||||
|
"border" to border,
|
||||||
|
"style" to styleName,
|
||||||
|
"variant" to variantName,
|
||||||
|
"team" to teamName,
|
||||||
|
"members" to teamMembers
|
||||||
|
),
|
||||||
mapOf( "kit" to kitName, "rank" to rankComponent )
|
mapOf( "kit" to kitName, "rank" to rankComponent )
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package club.mcscrims.speedhg.scoreboard
|
|||||||
|
|
||||||
import club.mcscrims.speedhg.SpeedHG
|
import club.mcscrims.speedhg.SpeedHG
|
||||||
import club.mcscrims.speedhg.ranking.Rank
|
import club.mcscrims.speedhg.ranking.Rank
|
||||||
|
import net.kyori.adventure.text.Component
|
||||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder
|
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder
|
||||||
import org.bukkit.Bukkit
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.DyeColor
|
||||||
import org.bukkit.entity.Player
|
import org.bukkit.entity.Player
|
||||||
import org.bukkit.event.EventHandler
|
import org.bukkit.event.EventHandler
|
||||||
import org.bukkit.event.EventPriority
|
import org.bukkit.event.EventPriority
|
||||||
@@ -166,6 +168,16 @@ class TablistManager(
|
|||||||
updateHeaderFooter( player )
|
updateHeaderFooter( player )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Muss aufgerufen werden wenn ein Spieler einem [PresetTeam] beitritt oder
|
||||||
|
* es verlässt, damit Prefix und Sortierung sofort aktualisiert werden.
|
||||||
|
*/
|
||||||
|
fun refreshTeamPrefix(
|
||||||
|
player: Player
|
||||||
|
) {
|
||||||
|
assignToTeam( player )
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Bukkit-Events
|
// Bukkit-Events
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -238,7 +250,7 @@ class TablistManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Prefix: Server-Rang (z. B. "[Admin]") ─────────────────────────
|
// ── Prefix: Server-Rang (z. B. "[Admin]") ─────────────────────────
|
||||||
team.prefix(rankProvider.getRankPrefix( player ))
|
team.prefix(buildPrefix( player ))
|
||||||
|
|
||||||
// ── playerListName: farbiger Spielername ───────────────────────────
|
// ── playerListName: farbiger Spielername ───────────────────────────
|
||||||
// WICHTIG: KEIN <reset> hier. Das <reset> machen wir am Anfang des Suffixes!
|
// WICHTIG: KEIN <reset> hier. Das <reset> machen wir am Anfang des Suffixes!
|
||||||
@@ -270,6 +282,73 @@ class TablistManager(
|
|||||||
mm.deserialize( "<reset> <dark_gray>[<reset>${rankTag}<dark_gray>]</dark_gray>" )
|
mm.deserialize( "<reset> <dark_gray>[<reset>${rankTag}<dark_gray>]</dark_gray>" )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den Prefix zurück:
|
||||||
|
* - Teams aktiv + Spieler in einem Team → `[Team X]` in Teamfarbe
|
||||||
|
* - Teams aktiv + kein Team → leerer Prefix (kein Rang-Prefix)
|
||||||
|
* - Teams deaktiviert → Server-Rang-Prefix wie gehabt
|
||||||
|
*/
|
||||||
|
private fun buildPrefix(
|
||||||
|
player: Player
|
||||||
|
): Component
|
||||||
|
{
|
||||||
|
val teamManager = plugin.presetTeamManager
|
||||||
|
|
||||||
|
if ( teamManager.isEnabled )
|
||||||
|
{
|
||||||
|
val presetTeam = teamManager.getTeam( player )
|
||||||
|
|
||||||
|
return if ( presetTeam != null )
|
||||||
|
{
|
||||||
|
val colorTag = dyeColorToMiniMessage( presetTeam.color )
|
||||||
|
mm.deserialize( "${colorTag}<bold>[${presetTeam.name}]</bold> " )
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Component.empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rankProvider.getRankPrefix( player )
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konvertiert [DyeColor] zu einem MiniMessage-Farb-Tag für den Tab-Prefix.
|
||||||
|
*
|
||||||
|
* | DyeColor | MiniMessage Tag |
|
||||||
|
* |----------|-----------------|
|
||||||
|
* | RED | `<red>` |
|
||||||
|
* | BLUE | `<blue>` |
|
||||||
|
* | GREEN | `<green>` |
|
||||||
|
* | YELLOW | `<yellow>` |
|
||||||
|
* | ORANGE | `<gold>` |
|
||||||
|
* | PURPLE | `<dark_purple>` |
|
||||||
|
* | CYAN | `<aqua>` |
|
||||||
|
* | PINK | `<light_purple>`|
|
||||||
|
* | (sonst) | `<gray>` |
|
||||||
|
*/
|
||||||
|
private fun dyeColorToMiniMessage(
|
||||||
|
color: DyeColor
|
||||||
|
): String = when( color )
|
||||||
|
{
|
||||||
|
DyeColor.RED -> "<red>"
|
||||||
|
DyeColor.BLUE -> "<blue>"
|
||||||
|
DyeColor.GREEN -> "<green>"
|
||||||
|
DyeColor.LIME -> "<green>"
|
||||||
|
DyeColor.YELLOW -> "<yellow>"
|
||||||
|
DyeColor.ORANGE -> "<gold>"
|
||||||
|
DyeColor.PURPLE -> "<dark_purple>"
|
||||||
|
DyeColor.MAGENTA -> "<light_purple>"
|
||||||
|
DyeColor.CYAN -> "<aqua>"
|
||||||
|
DyeColor.LIGHT_BLUE -> "<aqua>"
|
||||||
|
DyeColor.PINK -> "<light_purple>"
|
||||||
|
DyeColor.WHITE -> "<white>"
|
||||||
|
DyeColor.GRAY -> "<gray>"
|
||||||
|
DyeColor.LIGHT_GRAY -> "<gray>"
|
||||||
|
DyeColor.BLACK -> "<dark_gray>"
|
||||||
|
DyeColor.BROWN -> "<gold>"
|
||||||
|
}
|
||||||
|
|
||||||
/** Entfernt das Scoreboard-Team des Spielers vollständig. */
|
/** Entfernt das Scoreboard-Team des Spielers vollständig. */
|
||||||
private fun removePlayerTeam(
|
private fun removePlayerTeam(
|
||||||
uuid: UUID
|
uuid: UUID
|
||||||
@@ -337,10 +416,16 @@ class TablistManager(
|
|||||||
{
|
{
|
||||||
updateTask = plugin.server.scheduler.runTaskTimer( plugin, { ->
|
updateTask = plugin.server.scheduler.runTaskTimer( plugin, { ->
|
||||||
Bukkit.getOnlinePlayers().forEach { player ->
|
Bukkit.getOnlinePlayers().forEach { player ->
|
||||||
// Footer mit aktuellen Ping-Werten neu senden
|
|
||||||
updateHeaderFooter( player )
|
updateHeaderFooter( player )
|
||||||
// SpeedHG-Suffix synchronisieren (falls Rang sich geändert hat)
|
|
||||||
refreshRankSuffix( player )
|
refreshRankSuffix( player )
|
||||||
|
|
||||||
|
// Team-Prefix bei jedem Tick aktuell halten (Team-Beitritt live sichtbar)
|
||||||
|
if ( plugin.presetTeamManager.isEnabled )
|
||||||
|
{
|
||||||
|
val teamName = playerTeams[ player.uniqueId ] ?: return@forEach
|
||||||
|
val team = scoreboard.getTeam( teamName ) ?: return@forEach
|
||||||
|
team.prefix( buildPrefix( player ) )
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, UPDATE_INTERVAL_TICKS, UPDATE_INTERVAL_TICKS )
|
}, UPDATE_INTERVAL_TICKS, UPDATE_INTERVAL_TICKS )
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package club.mcscrims.speedhg.team
|
|
||||||
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Repräsentiert ein aktives Team in SpeedHG.
|
|
||||||
*
|
|
||||||
* ## Design-Entscheidungen
|
|
||||||
*
|
|
||||||
* **Keine DB-Persistenz:** Teams existieren nur für eine Runde im RAM.
|
|
||||||
* Nach dem Spielende werden alle Teams via [TeamManager.reset] verworfen.
|
|
||||||
*
|
|
||||||
* **Leader-Konzept:** Der erste Spieler ([members].first()) ist stets der
|
|
||||||
* Ersteller. Nur er darf `/team kick` ausführen. Bei [leave] durch den
|
|
||||||
* Leader wird kein automatisches Promoting durchgeführt — das Team löst
|
|
||||||
* sich auf, wenn der Leader geht (Vereinfachung, da max. 2 Spieler).
|
|
||||||
*
|
|
||||||
* @param id Eindeutige Team-ID (UUID, intern generiert).
|
|
||||||
* @param leader UUID des Team-Erstellers.
|
|
||||||
* @param members Geordnete Liste aller Mitglieder (Leader immer an Index 0).
|
|
||||||
*/
|
|
||||||
data class Team(
|
|
||||||
val id: UUID = UUID.randomUUID(),
|
|
||||||
val leader: UUID,
|
|
||||||
val members: MutableList<UUID> = mutableListOf(leader)
|
|
||||||
) {
|
|
||||||
|
|
||||||
/** Gibt `true` zurück wenn [uuid] Mitglied dieses Teams ist. */
|
|
||||||
fun contains(uuid: UUID): Boolean = members.contains(uuid)
|
|
||||||
|
|
||||||
/** Gibt `true` zurück wenn [uuid] der Leader dieses Teams ist. */
|
|
||||||
fun isLeader(uuid: UUID): Boolean = uuid == leader
|
|
||||||
|
|
||||||
/** Anzahl der aktuellen Mitglieder. */
|
|
||||||
val size: Int get() = members.size
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package club.mcscrims.speedhg.team
|
|
||||||
|
|
||||||
import club.mcscrims.speedhg.SpeedHG
|
|
||||||
import club.mcscrims.speedhg.game.GameState
|
|
||||||
import org.bukkit.entity.Player
|
|
||||||
import org.bukkit.event.EventHandler
|
|
||||||
import org.bukkit.event.EventPriority
|
|
||||||
import org.bukkit.event.Listener
|
|
||||||
import org.bukkit.event.entity.EntityDamageByEntityEvent
|
|
||||||
import org.bukkit.event.player.PlayerQuitEvent
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verhindert Friendly Fire zwischen Teammitgliedern und räumt
|
|
||||||
* Teams auf wenn Spieler die Runde verlassen.
|
|
||||||
*
|
|
||||||
* Friendly-Fire-Check läuft auf LOW-Priority, damit er vor allen anderen
|
|
||||||
* Damage-Listenern (Kit-, Perk-Dispatcher) gecancelt wird. So sehen
|
|
||||||
* weder [KitEventDispatcher] noch [PerkEventDispatcher] den Hit überhaupt.
|
|
||||||
*/
|
|
||||||
class TeamListener : Listener {
|
|
||||||
|
|
||||||
private val plugin get() = SpeedHG.instance
|
|
||||||
|
|
||||||
// ── Friendly Fire ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = false)
|
|
||||||
fun onDamage(event: EntityDamageByEntityEvent) {
|
|
||||||
if (!plugin.teamManager.isEnabled) return
|
|
||||||
if (plugin.gameManager.currentState != GameState.INGAME &&
|
|
||||||
plugin.gameManager.currentState != GameState.INVINCIBILITY) return
|
|
||||||
|
|
||||||
val attacker = event.damager as? Player ?: return
|
|
||||||
val victim = event.entity as? Player ?: return
|
|
||||||
|
|
||||||
if (plugin.teamManager.areInSameTeam(attacker, victim)) {
|
|
||||||
event.isCancelled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Team-Cleanup bei Disconnect ───────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wenn ein Spieler das Spiel während der Lobby verlässt, wird er aus
|
|
||||||
* seinem Team entfernt (bzw. das Team aufgelöst wenn er Leader war).
|
|
||||||
* Während INGAME bleibt das Team bestehen — der Spieler zählt weiterhin
|
|
||||||
* zur Teamgruppe für die Win-Condition (er ist ja bereits eliminiert).
|
|
||||||
*/
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR)
|
|
||||||
fun onQuit(event: PlayerQuitEvent) {
|
|
||||||
val state = plugin.gameManager.currentState
|
|
||||||
if (state == GameState.LOBBY || state == GameState.STARTING) {
|
|
||||||
plugin.teamManager.leave(event.player)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
package club.mcscrims.speedhg.team
|
|
||||||
|
|
||||||
import club.mcscrims.speedhg.SpeedHG
|
|
||||||
import org.bukkit.entity.Player
|
|
||||||
import java.util.UUID
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verwaltet alle aktiven Teams und offenen Einladungen für die laufende Runde.
|
|
||||||
*
|
|
||||||
* ## Datenstruktur
|
|
||||||
*
|
|
||||||
* Zwei parallele Maps ermöglichen O(1)-Lookups in beide Richtungen:
|
|
||||||
* - [teamByPlayer]: PlayerUUID → Team (schnelle Abfrage "In welchem Team bin ich?")
|
|
||||||
* - [teamsById]: TeamUUID → Team (schnelle Iteration über alle Teams)
|
|
||||||
*
|
|
||||||
* Einladungen ([pendingInvites]) sind kurzlebig und verfallen nach [INVITE_TTL_MS].
|
|
||||||
* Struktur: InviteeUUID → (InviterUUID, ExpiryTimestamp)
|
|
||||||
*
|
|
||||||
* ## Thread-Safety
|
|
||||||
* Alle Maps sind [ConcurrentHashMap], da Bukkit-Events zwar auf dem Main-Thread
|
|
||||||
* laufen, der Timer-Cleanup ([cleanExpiredInvites]) aber vom Scheduler aufgerufen wird.
|
|
||||||
* Mutationen (invite, accept, leave) laufen ausschließlich auf dem Main-Thread.
|
|
||||||
*/
|
|
||||||
class TeamManager(private val plugin: SpeedHG) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/** Einladung verfällt nach dieser Zeit in Millisekunden. */
|
|
||||||
const val INVITE_TTL_MS = 60_000L
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Konfiguration ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
val isEnabled: Boolean
|
|
||||||
get() = plugin.config.getBoolean("teams.enabled", true)
|
|
||||||
|
|
||||||
val maxTeamSize: Int
|
|
||||||
get() = plugin.config.getInt("teams.max-size", 2)
|
|
||||||
|
|
||||||
// ── State ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/** TeamID → Team */
|
|
||||||
private val teamsById = ConcurrentHashMap<UUID, Team>()
|
|
||||||
|
|
||||||
/** PlayerUUID → Team (Reverse-Lookup, O(1)) */
|
|
||||||
private val teamByPlayer = ConcurrentHashMap<UUID, Team>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Offene Einladungen: InviteeUUID → Pair(InviterUUID, ExpiryMs)
|
|
||||||
* Nur eine offene Einladung pro Spieler gleichzeitig.
|
|
||||||
*/
|
|
||||||
private val pendingInvites = ConcurrentHashMap<UUID, Pair<UUID, Long>>()
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Team-Queries (O(1), Main-Thread safe)
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
/** Gibt das Team des Spielers zurück, oder `null` wenn er keins hat. */
|
|
||||||
fun getTeam(player: Player): Team? = teamByPlayer[player.uniqueId]
|
|
||||||
|
|
||||||
/** Gibt das Team des Spielers zurück, oder `null` wenn er keins hat. */
|
|
||||||
fun getTeam(uuid: UUID): Team? = teamByPlayer[uuid]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gibt `true` zurück wenn beide Spieler im selben Team sind.
|
|
||||||
* Wird verwendet für:
|
|
||||||
* - Friendly-Fire-Check ([TeamListener])
|
|
||||||
* - Kompass-Tracking ([GameManager.updateCompass])
|
|
||||||
* - Orakel-Perk ([OraclePerk.findNearestEnemy])
|
|
||||||
*/
|
|
||||||
fun areInSameTeam(a: Player, b: Player): Boolean {
|
|
||||||
val teamA = teamByPlayer[a.uniqueId] ?: return false
|
|
||||||
return teamA.contains(b.uniqueId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun areInSameTeam(a: UUID, b: UUID): Boolean {
|
|
||||||
val teamA = teamByPlayer[a] ?: return false
|
|
||||||
return teamA.contains(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Gibt alle aktiven Teams zurück (nur lesend). */
|
|
||||||
fun getAllTeams(): Collection<Team> = teamsById.values
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Invite-System
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
sealed class InviteResult {
|
|
||||||
object Success : InviteResult()
|
|
||||||
object TeamsDisabled : InviteResult()
|
|
||||||
object TargetAlreadyInTeam : InviteResult()
|
|
||||||
object SenderAlreadyInFullTeam : InviteResult()
|
|
||||||
object InvitedSelf : InviteResult()
|
|
||||||
object AlreadyInSameTeam : InviteResult()
|
|
||||||
object InviteAlreadyPending : InviteResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Versendet eine Einladung von [sender] an [target].
|
|
||||||
* Erstellt bei Bedarf ein neues Team für [sender].
|
|
||||||
*/
|
|
||||||
fun invite(sender: Player, target: Player): InviteResult {
|
|
||||||
if (!isEnabled) return InviteResult.TeamsDisabled
|
|
||||||
if (sender.uniqueId == target.uniqueId) return InviteResult.InvitedSelf
|
|
||||||
|
|
||||||
val senderTeam = teamByPlayer[sender.uniqueId]
|
|
||||||
val targetTeam = teamByPlayer[target.uniqueId]
|
|
||||||
|
|
||||||
if (senderTeam != null && senderTeam.contains(target.uniqueId))
|
|
||||||
return InviteResult.AlreadyInSameTeam
|
|
||||||
|
|
||||||
if (targetTeam != null)
|
|
||||||
return InviteResult.TargetAlreadyInTeam
|
|
||||||
|
|
||||||
if (senderTeam != null && senderTeam.size >= maxTeamSize)
|
|
||||||
return InviteResult.SenderAlreadyInFullTeam
|
|
||||||
|
|
||||||
if (pendingInvites.containsKey(target.uniqueId))
|
|
||||||
return InviteResult.InviteAlreadyPending
|
|
||||||
|
|
||||||
pendingInvites[target.uniqueId] = Pair(sender.uniqueId, System.currentTimeMillis() + INVITE_TTL_MS)
|
|
||||||
return InviteResult.Success
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class AcceptResult {
|
|
||||||
data class Success(val team: Team) : AcceptResult()
|
|
||||||
object TeamsDisabled : AcceptResult()
|
|
||||||
object NoInvite : AcceptResult()
|
|
||||||
object InviteExpired : AcceptResult()
|
|
||||||
object AlreadyInTeam : AcceptResult()
|
|
||||||
object TeamFull : AcceptResult()
|
|
||||||
object InviterNotFound : AcceptResult()
|
|
||||||
object WrongInviter : AcceptResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nimmt eine Einladung von [inviterName] an.
|
|
||||||
* Wenn [inviterName] null ist, nimmt es die einzige vorhandene Einladung an.
|
|
||||||
*/
|
|
||||||
fun accept(invitee: Player, inviterUUID: UUID): AcceptResult {
|
|
||||||
if (!isEnabled) return AcceptResult.TeamsDisabled
|
|
||||||
if (teamByPlayer.containsKey(invitee.uniqueId)) return AcceptResult.AlreadyInTeam
|
|
||||||
|
|
||||||
val (storedInviterUUID, expiry) = pendingInvites[invitee.uniqueId]
|
|
||||||
?: return AcceptResult.NoInvite
|
|
||||||
|
|
||||||
if (storedInviterUUID != inviterUUID) return AcceptResult.WrongInviter
|
|
||||||
|
|
||||||
if (System.currentTimeMillis() > expiry) {
|
|
||||||
pendingInvites.remove(invitee.uniqueId)
|
|
||||||
return AcceptResult.InviteExpired
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingInvites.remove(invitee.uniqueId)
|
|
||||||
|
|
||||||
// Team des Inviters abrufen oder neu erstellen
|
|
||||||
val team = teamByPlayer.getOrPut(inviterUUID) {
|
|
||||||
val newTeam = Team(leader = inviterUUID)
|
|
||||||
teamsById[newTeam.id] = newTeam
|
|
||||||
newTeam
|
|
||||||
}
|
|
||||||
|
|
||||||
if (team.size >= maxTeamSize) return AcceptResult.TeamFull
|
|
||||||
|
|
||||||
team.members.add(invitee.uniqueId)
|
|
||||||
teamByPlayer[invitee.uniqueId] = team
|
|
||||||
|
|
||||||
return AcceptResult.Success(team)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deny(invitee: Player, inviterUUID: UUID): Boolean {
|
|
||||||
val invite = pendingInvites[invitee.uniqueId] ?: return false
|
|
||||||
if (invite.first != inviterUUID) return false
|
|
||||||
pendingInvites.remove(invitee.uniqueId)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Team-Mutations
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
sealed class LeaveResult {
|
|
||||||
object Success : LeaveResult()
|
|
||||||
object NotInTeam : LeaveResult()
|
|
||||||
/** Leader hat das Team verlassen → Team aufgelöst */
|
|
||||||
object TeamDisbanded : LeaveResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun leave(player: Player): LeaveResult {
|
|
||||||
val team = teamByPlayer[player.uniqueId] ?: return LeaveResult.NotInTeam
|
|
||||||
|
|
||||||
val wasLeader = team.isLeader(player.uniqueId)
|
|
||||||
|
|
||||||
team.members.remove(player.uniqueId)
|
|
||||||
teamByPlayer.remove(player.uniqueId)
|
|
||||||
|
|
||||||
return if (wasLeader || team.size == 0) {
|
|
||||||
// Leader geht → Team auflösen (alle verbleibenden Mitglieder rausnehmen)
|
|
||||||
disbandTeam(team)
|
|
||||||
LeaveResult.TeamDisbanded
|
|
||||||
} else {
|
|
||||||
if (team.size == 0) teamsById.remove(team.id)
|
|
||||||
LeaveResult.Success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class KickResult {
|
|
||||||
object Success : KickResult()
|
|
||||||
object NotInTeam : KickResult()
|
|
||||||
object NotLeader : KickResult()
|
|
||||||
object TargetNotInTeam : KickResult()
|
|
||||||
object CannotKickSelf : KickResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun kick(kicker: Player, target: Player): KickResult {
|
|
||||||
val team = teamByPlayer[kicker.uniqueId] ?: return KickResult.NotInTeam
|
|
||||||
if (!team.isLeader(kicker.uniqueId)) return KickResult.NotLeader
|
|
||||||
if (kicker.uniqueId == target.uniqueId) return KickResult.CannotKickSelf
|
|
||||||
if (!team.contains(target.uniqueId)) return KickResult.TargetNotInTeam
|
|
||||||
|
|
||||||
team.members.remove(target.uniqueId)
|
|
||||||
teamByPlayer.remove(target.uniqueId)
|
|
||||||
|
|
||||||
if (team.size == 0) {
|
|
||||||
teamsById.remove(team.id)
|
|
||||||
teamByPlayer.remove(kicker.uniqueId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return KickResult.Success
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Win-Condition Helper
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft ob alle [aliveUUIDs] im selben Team sind.
|
|
||||||
* Gibt `true` zurück wenn:
|
|
||||||
* - Teams deaktiviert → niemals (nur 1 Spieler = Sieg, Standardlogik)
|
|
||||||
* - Alle Überlebenden exakt dasselbe [Team]-Objekt teilen
|
|
||||||
*
|
|
||||||
* Aufruf in [GameManager.checkWin]:
|
|
||||||
* ```kotlin
|
|
||||||
* if (alivePlayers.size <= 1 || plugin.teamManager.allAliveInSameTeam(alivePlayers))
|
|
||||||
* endGame(...)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
fun allAliveInSameTeam(aliveUUIDs: Set<UUID>): Boolean {
|
|
||||||
if (!isEnabled || aliveUUIDs.size <= 1) return false
|
|
||||||
|
|
||||||
val firstTeam = teamByPlayer[aliveUUIDs.first()] ?: return false
|
|
||||||
return aliveUUIDs.all { uuid ->
|
|
||||||
val team = teamByPlayer[uuid] ?: return false
|
|
||||||
team.id == firstTeam.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Lifecycle
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
/** Räumt abgelaufene Einladungen auf. Einmal pro Sekunde aufrufen. */
|
|
||||||
fun cleanExpiredInvites() {
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
pendingInvites.entries.removeIf { (_, pair) -> now > pair.second }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setzt den gesamten Team-State zurück.
|
|
||||||
* In [GameManager.startGame] aufrufen (vor Spielstart).
|
|
||||||
*/
|
|
||||||
fun reset() {
|
|
||||||
teamsById.clear()
|
|
||||||
teamByPlayer.clear()
|
|
||||||
pendingInvites.clear()
|
|
||||||
plugin.logger.info("[TeamManager] Teams zurückgesetzt.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Pending-Invite Queries ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/** Gibt die UUID des Inviters zurück, falls eine Einladung für [invitee] vorliegt. */
|
|
||||||
fun getInviterFor(invitee: Player): UUID? {
|
|
||||||
val entry = pendingInvites[invitee.uniqueId] ?: return null
|
|
||||||
return if (System.currentTimeMillis() < entry.second) entry.first else null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Private Helpers ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private fun disbandTeam(team: Team) {
|
|
||||||
team.members.forEach { teamByPlayer.remove(it) }
|
|
||||||
team.members.clear()
|
|
||||||
teamsById.remove(team.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
41
src/main/kotlin/club/mcscrims/speedhg/team/gui/PresetTeam.kt
Normal file
41
src/main/kotlin/club/mcscrims/speedhg/team/gui/PresetTeam.kt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package club.mcscrims.speedhg.team.gui
|
||||||
|
|
||||||
|
import org.bukkit.DyeColor
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repräsentiert einen festen Team-Slot im GUI-System.
|
||||||
|
*
|
||||||
|
* ## Unterschied zu [Team]
|
||||||
|
*
|
||||||
|
* [Team] wird dynamisch durch `/team invite` erstellt und hat keine
|
||||||
|
* feste Identität. [PresetTeam] hingegen existiert **immer** — es
|
||||||
|
* ist ein nummerierter Slot mit Farbe und max. Kapazität, der im
|
||||||
|
* [TeamSelectionMenu] als Wolle-Icon sichtbar ist.
|
||||||
|
*
|
||||||
|
* ## Lebenszyklus
|
||||||
|
*
|
||||||
|
* | Phase | Zustand |
|
||||||
|
* |---------|------------------------------------------------|
|
||||||
|
* | Lobby | Alle Slots leer, Spieler können beitreten |
|
||||||
|
* | Running | Slots gesperrt, Änderungen werden blockiert |
|
||||||
|
* | Reset | `members` wird geleert via [PresetTeamManager] |
|
||||||
|
*
|
||||||
|
* @param index Slot-Nummer (0-basiert), für den GUI-Slot-Index.
|
||||||
|
* @param name Angezeigter Name im GUI (z. B. `"Team 1"`).
|
||||||
|
* @param color Wolle-Farbe für das Icon.
|
||||||
|
* @param maxSize Maximale Spieleranzahl.
|
||||||
|
*/
|
||||||
|
data class PresetTeam(
|
||||||
|
val index: Int,
|
||||||
|
val name: String,
|
||||||
|
val color: DyeColor,
|
||||||
|
val maxSize: Int,
|
||||||
|
val members: MutableList<UUID> = mutableListOf()
|
||||||
|
) {
|
||||||
|
val isFull: Boolean get() = members.size >= maxSize
|
||||||
|
val isEmpty: Boolean get() = members.isEmpty()
|
||||||
|
val size: Int get() = members.size
|
||||||
|
|
||||||
|
fun contains( uuid: UUID ): Boolean = members.contains( uuid )
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package club.mcscrims.speedhg.team.gui
|
||||||
|
|
||||||
|
import club.mcscrims.speedhg.SpeedHG
|
||||||
|
import org.bukkit.DyeColor
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verwaltet die festen GUI-Team-Slots für das GommeHD-style Team-System.
|
||||||
|
*
|
||||||
|
* ## Konfiguration (`config.yml`)
|
||||||
|
*
|
||||||
|
* ```yaml
|
||||||
|
* teams:
|
||||||
|
* enabled: true
|
||||||
|
* preset-count: 8
|
||||||
|
* max-size: 2
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Wenn `teams.enabled: false`, werden alle Mutations ([join], [leave]) sofort
|
||||||
|
* mit [JoinResult.TeamsDisabled] abgewiesen und [giveTeamItem] gibt `null` zurück.
|
||||||
|
*
|
||||||
|
* ## Datenstruktur
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* presetTeams: List<PresetTeam> — geordnete Slot-Liste (Index = GUI-Position)
|
||||||
|
* teamByPlayer: UUID → PresetTeam — O(1) Reverse-Lookup
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
class PresetTeamManager(
|
||||||
|
private val plugin: SpeedHG
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TEAM_COLORS = listOf(
|
||||||
|
DyeColor.RED,
|
||||||
|
DyeColor.BLUE,
|
||||||
|
DyeColor.GREEN,
|
||||||
|
DyeColor.YELLOW,
|
||||||
|
DyeColor.ORANGE,
|
||||||
|
DyeColor.PURPLE,
|
||||||
|
DyeColor.CYAN,
|
||||||
|
DyeColor.PINK,
|
||||||
|
DyeColor.WHITE,
|
||||||
|
DyeColor.BLACK
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Konfiguration ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
val isEnabled: Boolean
|
||||||
|
get() = plugin.config.getBoolean( "teams.enabled", true )
|
||||||
|
|
||||||
|
val maxTeamSize: Int
|
||||||
|
get() = plugin.config.getInt( "teams.max-size", 2 )
|
||||||
|
|
||||||
|
// ── State ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
val presetTeams: List<PresetTeam>
|
||||||
|
|
||||||
|
/** Reverse-Lookup: PlayerUUID → PresetTeam. Thread-safe für Scheduler. */
|
||||||
|
private val teamByPlayer = ConcurrentHashMap<UUID, PresetTeam>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
val count = plugin.config.getInt( "teams.preset-count", 8 )
|
||||||
|
|
||||||
|
presetTeams = ( 0 until count ).map { i ->
|
||||||
|
PresetTeam(
|
||||||
|
index = i,
|
||||||
|
name = "Team ${i + 1}",
|
||||||
|
color = TEAM_COLORS[ i % TEAM_COLORS.size ],
|
||||||
|
maxSize = maxTeamSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Queries ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fun getTeam( player: Player ): PresetTeam? = teamByPlayer[player.uniqueId]
|
||||||
|
fun getTeam( uuid: UUID ): PresetTeam? = teamByPlayer[uuid]
|
||||||
|
|
||||||
|
// ── Mutations ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
sealed class JoinResult {
|
||||||
|
object Success : JoinResult()
|
||||||
|
object TeamsDisabled : JoinResult()
|
||||||
|
object TeamFull : JoinResult()
|
||||||
|
/** Spieler war bereits in diesem Team. */
|
||||||
|
object AlreadyHere : JoinResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt `true` zurück wenn beide Spieler im selben Team sind.
|
||||||
|
* Wird verwendet für:
|
||||||
|
* - Friendly-Fire-Check ([TeamSelectionListener])
|
||||||
|
* - Kompass-Tracking ([GameManager.updateCompass])
|
||||||
|
* - Orakel-Perk ([OraclePerk.findNearestEnemy])
|
||||||
|
*/
|
||||||
|
fun areInSameTeam(
|
||||||
|
a: Player,
|
||||||
|
b: Player
|
||||||
|
): Boolean
|
||||||
|
{
|
||||||
|
val teamA = teamByPlayer[ a.uniqueId ] ?: return false
|
||||||
|
return teamA.contains( b.uniqueId )
|
||||||
|
}
|
||||||
|
|
||||||
|
fun areInSameTeam(
|
||||||
|
a: UUID,
|
||||||
|
b: UUID
|
||||||
|
): Boolean
|
||||||
|
{
|
||||||
|
val teamA = teamByPlayer[ a ] ?: return false
|
||||||
|
return teamA.contains( b )
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob alle [aliveUUIDs] im selben Team sind.
|
||||||
|
* Gibt `true` zurück wenn:
|
||||||
|
* - Teams deaktiviert → niemals (nur 1 Spieler = Sieg, Standardlogik)
|
||||||
|
* - Alle Überlebenden exakt dasselbe [Team]-Objekt teilen
|
||||||
|
*
|
||||||
|
* Aufruf in [GameManager.checkWin]:
|
||||||
|
* ```kotlin
|
||||||
|
* if (alivePlayers.size <= 1 || plugin.teamManager.allAliveInSameTeam(alivePlayers))
|
||||||
|
* endGame(...)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
fun allAliveInSameTeam(aliveUUIDs: Set<UUID>): Boolean {
|
||||||
|
if (!isEnabled || aliveUUIDs.size <= 1) return false
|
||||||
|
|
||||||
|
val firstTeam = teamByPlayer[aliveUUIDs.first()] ?: return false
|
||||||
|
return aliveUUIDs.all { uuid ->
|
||||||
|
val team = teamByPlayer[uuid] ?: return false
|
||||||
|
team.index == firstTeam.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lässt [player] dem [team]-Slot beitreten.
|
||||||
|
* Verlässt automatisch das alte Team, falls vorhanden.
|
||||||
|
*/
|
||||||
|
fun join(
|
||||||
|
player: Player,
|
||||||
|
team: PresetTeam
|
||||||
|
): JoinResult
|
||||||
|
{
|
||||||
|
if ( !isEnabled ) return JoinResult.TeamsDisabled
|
||||||
|
if ( team.contains( player.uniqueId ) ) return JoinResult.AlreadyHere
|
||||||
|
if ( team.isFull ) return JoinResult.TeamFull
|
||||||
|
|
||||||
|
teamByPlayer[player.uniqueId]?.members?.remove( player.uniqueId )
|
||||||
|
team.members.add( player.uniqueId )
|
||||||
|
teamByPlayer[player.uniqueId] = team
|
||||||
|
|
||||||
|
// Tab-Prefix sofort aktualisieren
|
||||||
|
plugin.tablistManager.refreshTeamPrefix( player )
|
||||||
|
|
||||||
|
return JoinResult.Success
|
||||||
|
}
|
||||||
|
|
||||||
|
fun leave(
|
||||||
|
player: Player
|
||||||
|
) {
|
||||||
|
val team = teamByPlayer.remove( player.uniqueId ) ?: return
|
||||||
|
team.members.remove( player.uniqueId )
|
||||||
|
|
||||||
|
// Tab-Prefix zurücksetzen
|
||||||
|
plugin.tablistManager.refreshTeamPrefix( player )
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Setzt alle Team-Slots zurück (Spielstart / Runden-Reset). */
|
||||||
|
fun reset()
|
||||||
|
{
|
||||||
|
presetTeams.forEach { it.members.clear() }
|
||||||
|
teamByPlayer.clear()
|
||||||
|
plugin.logger.info( "[PresetTeamManager] Alle Team-Slots zurückgesetzt." )
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package club.mcscrims.speedhg.team.gui
|
||||||
|
|
||||||
|
import club.mcscrims.speedhg.SpeedHG
|
||||||
|
import club.mcscrims.speedhg.game.GameState
|
||||||
|
import club.mcscrims.speedhg.gui.menu.MenuHolder
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.event.EventHandler
|
||||||
|
import org.bukkit.event.EventPriority
|
||||||
|
import org.bukkit.event.Listener
|
||||||
|
import org.bukkit.event.entity.EntityDamageByEntityEvent
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent
|
||||||
|
import org.bukkit.event.inventory.InventoryCloseEvent
|
||||||
|
import org.bukkit.event.player.PlayerQuitEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fängt alle Inventory-Events ab und delegiert sie an das zugehörige [Menu].
|
||||||
|
*
|
||||||
|
* Das Routing funktioniert über den [MenuHolder]-Pattern:
|
||||||
|
* Jedes via [Menu.createInventory] erstellte Inventar hat einen [MenuHolder]
|
||||||
|
* als Holder. Ist der Holder kein [MenuHolder], wird das Event ignoriert —
|
||||||
|
* so werden fremde Inventare (Kisten, Workbenches etc.) nie berührt.
|
||||||
|
*/
|
||||||
|
class TeamSelectionListener(
|
||||||
|
private val plugin: SpeedHG
|
||||||
|
) : Listener {
|
||||||
|
|
||||||
|
@EventHandler( priority = EventPriority.LOW, ignoreCancelled = false )
|
||||||
|
fun onDamage(
|
||||||
|
event: EntityDamageByEntityEvent
|
||||||
|
) {
|
||||||
|
if ( !plugin.presetTeamManager.isEnabled ) return
|
||||||
|
if ( plugin.gameManager.currentState != GameState.INGAME &&
|
||||||
|
plugin.gameManager.currentState != GameState.INVINCIBILITY ) return
|
||||||
|
|
||||||
|
val attacker = event.damager as? Player ?: return
|
||||||
|
val victim = event.entity as? Player ?: return
|
||||||
|
|
||||||
|
if (plugin.presetTeamManager.areInSameTeam( attacker, victim ))
|
||||||
|
event.isCancelled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler( priority = EventPriority.MONITOR )
|
||||||
|
fun onQuit(
|
||||||
|
event: PlayerQuitEvent
|
||||||
|
) {
|
||||||
|
val state = plugin.gameManager.currentState
|
||||||
|
if ( state == GameState.LOBBY || state == GameState.STARTING )
|
||||||
|
plugin.presetTeamManager.leave( event.player )
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
fun onInventoryClick(
|
||||||
|
event: InventoryClickEvent
|
||||||
|
) {
|
||||||
|
val menu = ( event.inventory.holder as? MenuHolder )?.menu
|
||||||
|
as? TeamSelectionMenu ?: return
|
||||||
|
|
||||||
|
event.isCancelled = true
|
||||||
|
|
||||||
|
val player = event.whoClicked as? org.bukkit.entity.Player ?: return
|
||||||
|
menu.onClick( event, player )
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
fun onInventoryClose(
|
||||||
|
event: InventoryCloseEvent
|
||||||
|
) {
|
||||||
|
// Kein State-Cleanup nötig — PresetTeamManager ist unabhängig vom Inventar
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
package club.mcscrims.speedhg.team.gui
|
||||||
|
|
||||||
|
import club.mcscrims.speedhg.SpeedHG
|
||||||
|
import club.mcscrims.speedhg.gui.menu.Menu
|
||||||
|
import club.mcscrims.speedhg.util.trans
|
||||||
|
import net.kyori.adventure.text.Component
|
||||||
|
import net.kyori.adventure.text.format.TextDecoration
|
||||||
|
import org.bukkit.Bukkit
|
||||||
|
import org.bukkit.Material
|
||||||
|
import org.bukkit.enchantments.Enchantment
|
||||||
|
import org.bukkit.entity.Player
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent
|
||||||
|
import org.bukkit.inventory.Inventory
|
||||||
|
import org.bukkit.inventory.ItemFlag
|
||||||
|
import org.bukkit.inventory.ItemStack
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GUI-basiertes Team-Auswahl-Menü im GommeHD-Stil.
|
||||||
|
*
|
||||||
|
* ## Layout (4 Reihen, 36 Slots)
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* [F][F][F][F][F][F][F][F][F] ← Reihe 0: Filler
|
||||||
|
* [F][T1][T2][T3][T4][T5][T6][T7][F]
|
||||||
|
* [F][T8][..][..][..][..][..][..][F]
|
||||||
|
* [F][F][F][F][F][F][F][F][F] ← Reihe 3: Filler
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* ## Wolle-Farb-Logik
|
||||||
|
*
|
||||||
|
* | Zustand | Material |
|
||||||
|
* |----------------------|--------------------------|
|
||||||
|
* | Platz frei | `LIME_WOOL` |
|
||||||
|
* | Team voll | `RED_WOOL` |
|
||||||
|
* | Spieler selbst drin | Teamfarbe + Glanz-Effekt |
|
||||||
|
*
|
||||||
|
* @param viewer Spieler, der das Menü öffnet.
|
||||||
|
*/
|
||||||
|
class TeamSelectionMenu(
|
||||||
|
private val viewer: Player
|
||||||
|
) : Menu(
|
||||||
|
rows = 4,
|
||||||
|
title = viewer.trans( "gui.team_menu.title" )
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val plugin get() = SpeedHG.instance
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TEAM_SLOTS = listOf(
|
||||||
|
10, 11, 12, 13, 14, 15, 16,
|
||||||
|
19, 20, 21, 22, 23, 24, 25
|
||||||
|
)
|
||||||
|
|
||||||
|
private val FILLER_MATERIAL = Material.GRAY_STAINED_GLASS_PANE
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
override fun build(): Inventory = createInventory( title ).also { populate( it ) }
|
||||||
|
|
||||||
|
private fun populate(
|
||||||
|
inv: Inventory
|
||||||
|
) {
|
||||||
|
inv.clear()
|
||||||
|
|
||||||
|
val filler = buildFiller()
|
||||||
|
repeat( size ) { inv.setItem( it, filler ) }
|
||||||
|
|
||||||
|
plugin.presetTeamManager.presetTeams.forEachIndexed { i, team ->
|
||||||
|
val slot = TEAM_SLOTS.getOrNull( i ) ?: return@forEachIndexed
|
||||||
|
inv.setItem( slot, buildTeamItem( team ) )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Click-Handling ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
override fun onClick(
|
||||||
|
event: InventoryClickEvent,
|
||||||
|
player: Player
|
||||||
|
) {
|
||||||
|
val slot = event.rawSlot
|
||||||
|
if ( slot !in 0 until size ) return
|
||||||
|
|
||||||
|
val teamIndex = TEAM_SLOTS.indexOf( slot )
|
||||||
|
if ( teamIndex == -1 ) return
|
||||||
|
|
||||||
|
val team = plugin.presetTeamManager.presetTeams.getOrNull( teamIndex ) ?: return
|
||||||
|
|
||||||
|
when ( plugin.presetTeamManager.join( player, team ) )
|
||||||
|
{
|
||||||
|
is PresetTeamManager.JoinResult.Success -> {
|
||||||
|
player.sendActionBar( player.trans( "gui.team_menu.joined", "team" to team.name ) )
|
||||||
|
refresh( player )
|
||||||
|
}
|
||||||
|
|
||||||
|
is PresetTeamManager.JoinResult.TeamFull -> {
|
||||||
|
player.sendActionBar( player.trans( "gui.team_menu.full" ) )
|
||||||
|
}
|
||||||
|
|
||||||
|
is PresetTeamManager.JoinResult.AlreadyHere -> {
|
||||||
|
player.sendActionBar( player.trans( "gui.team_menu.already_here" ) )
|
||||||
|
}
|
||||||
|
|
||||||
|
is PresetTeamManager.JoinResult.TeamsDisabled -> {
|
||||||
|
player.sendActionBar( player.trans( "gui.team_menu.disabled" ) )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Refresh ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktualisiert das Inventar für alle Spieler, die dieses Menü gerade offen haben.
|
||||||
|
* Wird nach jeder Beitritts-Aktion aufgerufen, damit alle
|
||||||
|
* offenen Instanzen die neue Teamgröße sofort sehen.
|
||||||
|
*/
|
||||||
|
private fun refresh(
|
||||||
|
triggeringPlayer: Player
|
||||||
|
) {
|
||||||
|
populate( inventory )
|
||||||
|
|
||||||
|
Bukkit.getOnlinePlayers()
|
||||||
|
.filter { it != triggeringPlayer }
|
||||||
|
.forEach { other ->
|
||||||
|
val holder = other.openInventory.topInventory.holder
|
||||||
|
if ( holder is club.mcscrims.speedhg.gui.menu.MenuHolder &&
|
||||||
|
holder.menu is TeamSelectionMenu )
|
||||||
|
{
|
||||||
|
populate( other.openInventory.topInventory )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Item-Builder ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private fun buildTeamItem(
|
||||||
|
team: PresetTeam
|
||||||
|
): ItemStack {
|
||||||
|
val isSelf = team.contains( viewer.uniqueId )
|
||||||
|
val woolKey = "${team.color.name}_WOOL"
|
||||||
|
|
||||||
|
val material = when
|
||||||
|
{
|
||||||
|
isSelf -> Material.getMaterial( woolKey ) ?: Material.LIME_WOOL
|
||||||
|
team.isFull -> Material.RED_WOOL
|
||||||
|
else -> Material.LIME_WOOL
|
||||||
|
}
|
||||||
|
|
||||||
|
val item = ItemStack( material )
|
||||||
|
|
||||||
|
item.editMeta { meta ->
|
||||||
|
meta.displayName(
|
||||||
|
viewer.trans( "gui.team_menu.item.name", "team" to team.name )
|
||||||
|
.decoration( TextDecoration.ITALIC, false )
|
||||||
|
)
|
||||||
|
|
||||||
|
val memberNames = team.members
|
||||||
|
.mapNotNull { Bukkit.getPlayer( it )?.name }
|
||||||
|
|
||||||
|
val lore = mutableListOf<Component>()
|
||||||
|
lore += Component.empty()
|
||||||
|
|
||||||
|
lore += viewer.trans(
|
||||||
|
"gui.team_menu.item.size",
|
||||||
|
"current" to team.size.toString(),
|
||||||
|
"max" to team.maxSize.toString()
|
||||||
|
).decoration( TextDecoration.ITALIC, false )
|
||||||
|
|
||||||
|
lore += Component.empty()
|
||||||
|
|
||||||
|
if ( memberNames.isEmpty() )
|
||||||
|
{
|
||||||
|
lore += viewer.trans( "gui.team_menu.item.empty" )
|
||||||
|
.decoration( TextDecoration.ITALIC, false )
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
memberNames.forEach { name ->
|
||||||
|
lore += viewer.trans( "gui.team_menu.item.member", "player" to name )
|
||||||
|
.decoration( TextDecoration.ITALIC, false )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lore += Component.empty()
|
||||||
|
|
||||||
|
lore += if ( team.isFull )
|
||||||
|
viewer.trans( "gui.team_menu.item.full_hint" ).decoration( TextDecoration.ITALIC, false )
|
||||||
|
else
|
||||||
|
viewer.trans( "gui.team_menu.item.join_hint" ).decoration( TextDecoration.ITALIC, false )
|
||||||
|
|
||||||
|
lore += Component.empty()
|
||||||
|
meta.lore( lore )
|
||||||
|
|
||||||
|
if ( isSelf ) {
|
||||||
|
meta.addEnchant( Enchantment.UNBREAKING, 1, true )
|
||||||
|
meta.addItemFlags( ItemFlag.HIDE_ENCHANTS )
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.addItemFlags( ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_ADDITIONAL_TOOLTIP )
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildFiller(): ItemStack {
|
||||||
|
val item = ItemStack( FILLER_MATERIAL )
|
||||||
|
item.editMeta { meta ->
|
||||||
|
meta.displayName(
|
||||||
|
Component.text( " " ).decoration( TextDecoration.ITALIC, false )
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,8 @@ lunarclient:
|
|||||||
|
|
||||||
teams:
|
teams:
|
||||||
enabled: false
|
enabled: false
|
||||||
max-size: 2
|
preset-count: 10 # Anzahl der Team-Slots
|
||||||
|
max-size: 2 # Spieler pro Team
|
||||||
|
|
||||||
recraftNerf:
|
recraftNerf:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ game:
|
|||||||
leaderboard:
|
leaderboard:
|
||||||
name: '<gold><bold>Top 10 Leaderboard</bold></gold>'
|
name: '<gold><bold>Top 10 Leaderboard</bold></gold>'
|
||||||
lore: '<gray>Who are the best players?</gray>'
|
lore: '<gray>Who are the best players?</gray>'
|
||||||
|
teams:
|
||||||
|
name: '<gold><bold>Teams'
|
||||||
|
lore: '<gray>Click to choose your team.'
|
||||||
|
|
||||||
ranking:
|
ranking:
|
||||||
placement_progress: '<prefix><gray>Placement <aqua><current>/<total></aqua> — Placed <aqua>#<placement></aqua> · <aqua><kills></aqua> Kill(s)</gray>'
|
placement_progress: '<prefix><gray>Placement <aqua><current>/<total></aqua> — Placed <aqua>#<placement></aqua> · <aqua><kills></aqua> Kill(s)</gray>'
|
||||||
@@ -175,27 +178,70 @@ commands:
|
|||||||
|
|
||||||
scoreboard:
|
scoreboard:
|
||||||
title: '<gradient:red:gold><bold>SpeedHG</bold></gradient>'
|
title: '<gradient:red:gold><bold>SpeedHG</bold></gradient>'
|
||||||
|
|
||||||
|
# ── Ohne Teams ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
lobby:
|
lobby:
|
||||||
- " <variant>"
|
- ''
|
||||||
- "<gray><st> "
|
- '<gray>Type: <white><variant></white></gray>'
|
||||||
- "Players: <green><online>/<max>"
|
- '<gray>Players: <white><online>/<max></white></gray>'
|
||||||
- "Kit: <yellow><kit>"
|
- '<gray>Time: <white><time></white></gray>'
|
||||||
- "Style: <yellow><style>"
|
- ''
|
||||||
- "Rank: <rank>"
|
- '<gray>Kit: <kit></gray>'
|
||||||
- ""
|
- '<gray>Style: <white><style></white></gray>'
|
||||||
- "<gray>Waiting for start..."
|
- ''
|
||||||
- ""
|
- '<gray>Rank: <rank></gray>'
|
||||||
- "<yellow>play.mcscrims.club"
|
- ''
|
||||||
|
- '<dark_gray>play.mcscrims.club</dark_gray>'
|
||||||
|
- ''
|
||||||
|
|
||||||
ingame:
|
ingame:
|
||||||
- "<gray><st> "
|
- ''
|
||||||
- "Time: <green><timer>"
|
- '<gray>Time: <white><timer></white></gray>'
|
||||||
- "Players: <red><alive>"
|
- '<gray>Alive: <white><alive></white></gray>'
|
||||||
- "Kills: <green><kills>"
|
- '<gray>Border: <white><border>m</white></gray>'
|
||||||
- "Rank: <rank>"
|
- ''
|
||||||
- ""
|
- '<gray>Kit: <kit></gray>'
|
||||||
- "Border: <red><border>"
|
- '<gray>Style: <white><style></white></gray>'
|
||||||
- ""
|
- ''
|
||||||
- "<yellow>play.mcscrims.club"
|
- '<gray>Kills: <white><kills></white></gray>'
|
||||||
|
- ''
|
||||||
|
- '<dark_gray>play.mcscrims.club</dark_gray>'
|
||||||
|
- ''
|
||||||
|
|
||||||
|
# ── Mit Teams ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
lobby_teams:
|
||||||
|
- ''
|
||||||
|
- '<gray>Type: <white><variant></white></gray>'
|
||||||
|
- '<gray>Players: <white><online>/<max></white></gray>'
|
||||||
|
- '<gray>Time: <white><time></white></gray>'
|
||||||
|
- ''
|
||||||
|
- '<gray>Kit: <kit></gray>'
|
||||||
|
- '<gray>Style: <white><style></white></gray>'
|
||||||
|
- ''
|
||||||
|
- '<gray>Team: <white><team></white></gray>'
|
||||||
|
- '<gray>Members: <white><members></white></gray>'
|
||||||
|
- ''
|
||||||
|
- '<dark_gray>play.mcscrims.club</dark_gray>'
|
||||||
|
- ''
|
||||||
|
|
||||||
|
ingame_teams:
|
||||||
|
- ''
|
||||||
|
- '<gray>Time: <white><timer></white></gray>'
|
||||||
|
- '<gray>Alive: <white><alive></white></gray>'
|
||||||
|
- '<gray>Border: <white><border>m</white></gray>'
|
||||||
|
- ''
|
||||||
|
- '<gray>Kit: <kit></gray>'
|
||||||
|
- '<gray>Style: <white><style></white></gray>'
|
||||||
|
- ''
|
||||||
|
- '<gray>Team: <white><team></white></gray>'
|
||||||
|
- '<gray>Members: <white><members></white></gray>'
|
||||||
|
- ''
|
||||||
|
- '<gray>Kills: <white><kills></white></gray>'
|
||||||
|
- ''
|
||||||
|
- '<dark_gray>play.mcscrims.club</dark_gray>'
|
||||||
|
- ''
|
||||||
|
|
||||||
gui:
|
gui:
|
||||||
kit_selector:
|
kit_selector:
|
||||||
@@ -235,6 +281,19 @@ gui:
|
|||||||
title: '<aqua><bold>Profile & Stats</bold></aqua>'
|
title: '<aqua><bold>Profile & Stats</bold></aqua>'
|
||||||
leaderboard_menu:
|
leaderboard_menu:
|
||||||
title: '<gold><bold>Top 10 Leaderboard</bold></gold>'
|
title: '<gold><bold>Top 10 Leaderboard</bold></gold>'
|
||||||
|
team_menu:
|
||||||
|
title: '<dark_gray>Choose your Team'
|
||||||
|
joined: '<green>You joined <white><team></white>!'
|
||||||
|
full: '<red>This team is already full!'
|
||||||
|
already_here: '<yellow>You are already in this team.'
|
||||||
|
disabled: '<red>Teams are disabled.'
|
||||||
|
item:
|
||||||
|
name: '<white><bold><team></bold>'
|
||||||
|
size: '<gray>Players: <white><current>/<max>'
|
||||||
|
empty: ' <dark_gray><i>Nobody yet</i>'
|
||||||
|
member: ' <aqua>• <player>'
|
||||||
|
join_hint: '<green>▶ Click to join'
|
||||||
|
full_hint: '<red>⛔ Team is full'
|
||||||
|
|
||||||
perks:
|
perks:
|
||||||
oracle:
|
oracle:
|
||||||
|
|||||||
@@ -42,6 +42,3 @@ commands:
|
|||||||
perks:
|
perks:
|
||||||
description: 'Perk-Auswahl öffnen'
|
description: 'Perk-Auswahl öffnen'
|
||||||
usage: '/perks'
|
usage: '/perks'
|
||||||
team:
|
|
||||||
description: 'Team-System for SpeedHG'
|
|
||||||
usage: '/team <invite|accept|deny|leave|kick|info> [player]'
|
|
||||||
Reference in New Issue
Block a user