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.PerksCommand
|
||||
import club.mcscrims.speedhg.command.RankingCommand
|
||||
import club.mcscrims.speedhg.command.TeamCommand
|
||||
import club.mcscrims.speedhg.command.TimerCommand
|
||||
import club.mcscrims.speedhg.config.CustomGameManager
|
||||
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.TablistManager
|
||||
import club.mcscrims.speedhg.scoreboard.VolcanoServerRankProvider
|
||||
import club.mcscrims.speedhg.team.TeamListener
|
||||
import club.mcscrims.speedhg.team.TeamManager
|
||||
import club.mcscrims.speedhg.team.gui.PresetTeamManager
|
||||
import club.mcscrims.speedhg.team.gui.TeamSelectionListener
|
||||
import club.mcscrims.speedhg.webhook.DiscordWebhookManager
|
||||
import club.mcscrims.speedhg.world.DataPackManager
|
||||
import club.mcscrims.speedhg.world.SurfaceBlockPopulator
|
||||
@@ -115,9 +114,6 @@ class SpeedHG : JavaPlugin() {
|
||||
lateinit var dataPackManager: DataPackManager
|
||||
private set
|
||||
|
||||
lateinit var teamManager: TeamManager
|
||||
private set
|
||||
|
||||
lateinit var lunarClientManager: LunarClientManager
|
||||
private set
|
||||
|
||||
@@ -130,6 +126,9 @@ class SpeedHG : JavaPlugin() {
|
||||
lateinit var lobbyAnnouncer: LobbyAnnouncer
|
||||
private set
|
||||
|
||||
lateinit var presetTeamManager: PresetTeamManager
|
||||
private set
|
||||
|
||||
override fun onLoad()
|
||||
{
|
||||
instance = this
|
||||
@@ -189,17 +188,11 @@ class SpeedHG : JavaPlugin() {
|
||||
lunarClientManager = LunarClientManager( this )
|
||||
lobbyItemManager = LobbyItemManager( this )
|
||||
tablistManager = TablistManager( this, VolcanoServerRankProvider() )
|
||||
presetTeamManager = PresetTeamManager( this )
|
||||
|
||||
perkManager = PerkManager( this )
|
||||
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.start()
|
||||
|
||||
@@ -220,7 +213,6 @@ class SpeedHG : JavaPlugin() {
|
||||
podiumManager.cleanup()
|
||||
if ( ::lobbyAnnouncer.isInitialized ) lobbyAnnouncer.stop()
|
||||
if ( ::perkManager.isInitialized ) perkManager.shutdown()
|
||||
if ( ::teamManager.isInitialized ) teamManager.reset()
|
||||
if ( ::tablistManager.isInitialized ) tablistManager.shutdown()
|
||||
if ( ::statsManager.isInitialized ) statsManager.shutdown()
|
||||
if ( ::databaseManager.isInitialized ) databaseManager.disconnect()
|
||||
@@ -288,12 +280,6 @@ class SpeedHG : JavaPlugin() {
|
||||
tabCompleter = rankingCommand
|
||||
}
|
||||
|
||||
val teamCommand = TeamCommand()
|
||||
getCommand( "team" )?.apply {
|
||||
setExecutor( teamCommand )
|
||||
tabCompleter = teamCommand
|
||||
}
|
||||
|
||||
getCommand( "leaderboard" )?.setExecutor( LeaderboardCommand() )
|
||||
getCommand( "perks" )?.setExecutor( PerksCommand() )
|
||||
getCommand( "help" )?.setExecutor( HelpCommand() )
|
||||
@@ -310,10 +296,10 @@ class SpeedHG : JavaPlugin() {
|
||||
pm.registerEvents( StatsListener(), this )
|
||||
pm.registerEvents( MenuListener(), this )
|
||||
pm.registerEvents(PerkEventDispatcher( this, perkManager ), this )
|
||||
pm.registerEvents( TeamListener(), this )
|
||||
pm.registerEvents( lobbyItemManager, this )
|
||||
pm.registerEvents(ChatListener( this, VolcanoServerRankProvider() ), this )
|
||||
pm.registerEvents(KnockbackListener( this ), this )
|
||||
pm.registerEvents(TeamSelectionListener( this ), this )
|
||||
}
|
||||
|
||||
private fun registerRecipes()
|
||||
|
||||
@@ -48,7 +48,7 @@ class LunarClientManager(
|
||||
private fun setRichPresence(
|
||||
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 presence = ServerRichPresence.builder()
|
||||
@@ -57,7 +57,7 @@ class LunarClientManager(
|
||||
.gameVariantName(plugin.config.getString( "lunarclient.variantName" ))
|
||||
.playerState( currentState )
|
||||
.teamCurrentSize( teamSize )
|
||||
.teamMaxSize( plugin.teamManager.maxTeamSize )
|
||||
.teamMaxSize( plugin.presetTeamManager.maxTeamSize )
|
||||
.build()
|
||||
|
||||
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() {
|
||||
if (currentState != GameState.INGAME && currentState != GameState.INVINCIBILITY) return
|
||||
|
||||
val teamManager = plugin.teamManager
|
||||
val teamManager = plugin.presetTeamManager
|
||||
|
||||
val roundOver = when {
|
||||
// Nur noch 0 oder 1 Spieler übrig → immer Ende
|
||||
@@ -326,8 +326,8 @@ class GameManager(
|
||||
|
||||
val winnerUUID = alivePlayers.firstOrNull()
|
||||
|
||||
val winnerTeam = if ( plugin.teamManager.isEnabled && winnerUUID != null )
|
||||
plugin.teamManager.getTeam( winnerUUID ) else null
|
||||
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 )
|
||||
@@ -364,7 +364,7 @@ class GameManager(
|
||||
private fun buildWinnerName(anyAliveUUID: UUID?): String {
|
||||
anyAliveUUID ?: return "N/A"
|
||||
|
||||
val teamManager = plugin.teamManager
|
||||
val teamManager = plugin.presetTeamManager
|
||||
if (!teamManager.isEnabled) {
|
||||
return Bukkit.getPlayer(anyAliveUUID)?.name ?: "N/A"
|
||||
}
|
||||
@@ -420,8 +420,8 @@ class GameManager(
|
||||
{
|
||||
if ( p == target ) continue
|
||||
|
||||
if ( plugin.teamManager.isEnabled &&
|
||||
plugin.teamManager.areInSameTeam( p, target ))
|
||||
if ( plugin.presetTeamManager.isEnabled &&
|
||||
plugin.presetTeamManager.areInSameTeam( p, target ))
|
||||
continue
|
||||
|
||||
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.PerkSelectorMenu
|
||||
import club.mcscrims.speedhg.gui.menu.StatsMenu
|
||||
import club.mcscrims.speedhg.team.gui.TeamSelectionMenu
|
||||
import club.mcscrims.speedhg.util.ItemBuilder
|
||||
import net.kyori.adventure.text.format.TextDecoration
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
@@ -61,9 +62,10 @@ class LobbyItemManager(
|
||||
companion object {
|
||||
const val TAG_KITS = "kits"
|
||||
const val TAG_PERKS = "perks"
|
||||
const val TAG_TUTORIAL = "tutorial"
|
||||
const val TAG_STATS = "stats"
|
||||
const val TAG_TUTORIAL = "tutorial"
|
||||
const val TAG_STATS = "stats"
|
||||
const val TAG_LEADERBOARD = "leaderboard"
|
||||
const val TAG_TEAMS = "teams"
|
||||
}
|
||||
|
||||
// ── Item definitions ──────────────────────────────────────────────────────
|
||||
@@ -145,6 +147,17 @@ class LobbyItemManager(
|
||||
.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 ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -157,11 +170,15 @@ class LobbyItemManager(
|
||||
player: Player
|
||||
) {
|
||||
player.inventory.clear()
|
||||
player.inventory.setItem( 0, buildKitItem( player ))
|
||||
player.inventory.setItem( 1, buildPerkItem( player ))
|
||||
player.inventory.setItem( 4, buildTutorialItem( player ))
|
||||
player.inventory.setItem( 7, buildStatsItem( player ))
|
||||
player.inventory.setItem( 8, buildLeaderboardItem( player ))
|
||||
player.inventory.setItem( 0, buildKitItem( 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( 7, buildStatsItem( player ) )
|
||||
player.inventory.setItem( 8, buildLeaderboardItem( player ) )
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -290,6 +307,7 @@ class LobbyItemManager(
|
||||
{
|
||||
TAG_KITS -> KitSelectorMenu( 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_STATS -> StatsMenu( event.player ).open( event.player )
|
||||
TAG_LEADERBOARD -> LeaderboardMenu( event.player ).open( event.player )
|
||||
|
||||
@@ -74,7 +74,7 @@ class OraclePerk : Perk() {
|
||||
.filter { it != player.uniqueId }
|
||||
.mapNotNull { plugin.server.getPlayer(it) }
|
||||
.filter { !plugin.perkManager.isGhost(it) }
|
||||
.filter { !plugin.teamManager.areInSameTeam( player, it ) }
|
||||
.filter { !plugin.presetTeamManager.areInSameTeam( player, it ) }
|
||||
.minByOrNull { it.location.distanceSquared(player.location) }
|
||||
|
||||
private fun buildTrackerComponent(player: Player, nearest: Player): Component {
|
||||
|
||||
@@ -58,21 +58,29 @@ class ScoreboardManager(
|
||||
player: Player,
|
||||
board: FastBoard
|
||||
) {
|
||||
val gm = plugin.gameManager
|
||||
val gm = plugin.gameManager
|
||||
val state = gm.currentState
|
||||
|
||||
board.updateTitle(player.trans( "scoreboard.title" ))
|
||||
board.updateTitle( player.trans( "scoreboard.title" ) )
|
||||
|
||||
val online = Bukkit.getOnlinePlayers().size.toString()
|
||||
val max = Bukkit.getMaxPlayers().toString()
|
||||
val online = Bukkit.getOnlinePlayers().size.toString()
|
||||
val max = Bukkit.getMaxPlayers().toString()
|
||||
val variantName = plugin.config.getString( "lunarclient.variantName" ).toString()
|
||||
|
||||
val kitName = plugin.kitManager.getSelectedKit( player )?.displayName ?: Component.text( "None" )
|
||||
val styleName = plugin.kitManager.getSelectedPlaystyle( player ).displayName
|
||||
val kitName = plugin.kitManager.getSelectedKit( player )?.displayName ?: Component.text( "None" )
|
||||
val styleName = plugin.kitManager.getSelectedPlaystyle( player ).displayName
|
||||
|
||||
val stats = plugin.statsManager.getCachedStats( player.uniqueId )
|
||||
val score = stats?.scrimScore ?: 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 lines: List<Component>
|
||||
@@ -81,22 +89,44 @@ class ScoreboardManager(
|
||||
{
|
||||
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(
|
||||
player, "scoreboard.lobby",
|
||||
mapOf( "online" to online, "max" to max, "time" to timeString, "style" to styleName, "variant" to variantName ),
|
||||
player, key,
|
||||
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 )
|
||||
)
|
||||
}
|
||||
else
|
||||
{
|
||||
val timeString = formatTime( gm.timer )
|
||||
val alive = gm.alivePlayers.size.toString()
|
||||
val kills = player.getStatistic( Statistic.PLAYER_KILLS ).toString()
|
||||
val border = String.format( "%.0f", player.world.worldBorder.size )
|
||||
val alive = gm.alivePlayers.size.toString()
|
||||
val kills = player.getStatistic( Statistic.PLAYER_KILLS ).toString()
|
||||
val border = String.format( "%.0f", player.world.worldBorder.size )
|
||||
|
||||
val key = if ( teamsEnabled ) "scoreboard.ingame_teams" else "scoreboard.ingame"
|
||||
|
||||
lines = plugin.languageManager.getMessageList(
|
||||
player, "scoreboard.ingame",
|
||||
mapOf( "timer" to timeString, "alive" to alive, "kills" to kills, "border" to border, "style" to styleName, "variant" to variantName ),
|
||||
player, key,
|
||||
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 )
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package club.mcscrims.speedhg.scoreboard
|
||||
|
||||
import club.mcscrims.speedhg.SpeedHG
|
||||
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.tag.resolver.Placeholder
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.DyeColor
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.EventHandler
|
||||
import org.bukkit.event.EventPriority
|
||||
@@ -166,6 +168,16 @@ class TablistManager(
|
||||
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
|
||||
// =========================================================================
|
||||
@@ -238,7 +250,7 @@ class TablistManager(
|
||||
}
|
||||
|
||||
// ── Prefix: Server-Rang (z. B. "[Admin]") ─────────────────────────
|
||||
team.prefix(rankProvider.getRankPrefix( player ))
|
||||
team.prefix(buildPrefix( player ))
|
||||
|
||||
// ── playerListName: farbiger Spielername ───────────────────────────
|
||||
// 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>" )
|
||||
}
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
private fun removePlayerTeam(
|
||||
uuid: UUID
|
||||
@@ -337,10 +416,16 @@ class TablistManager(
|
||||
{
|
||||
updateTask = plugin.server.scheduler.runTaskTimer( plugin, { ->
|
||||
Bukkit.getOnlinePlayers().forEach { player ->
|
||||
// Footer mit aktuellen Ping-Werten neu senden
|
||||
updateHeaderFooter( player )
|
||||
// SpeedHG-Suffix synchronisieren (falls Rang sich geändert hat)
|
||||
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 )
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
enabled: false
|
||||
max-size: 2
|
||||
preset-count: 10 # Anzahl der Team-Slots
|
||||
max-size: 2 # Spieler pro Team
|
||||
|
||||
recraftNerf:
|
||||
enabled: false
|
||||
|
||||
@@ -32,6 +32,9 @@ game:
|
||||
leaderboard:
|
||||
name: '<gold><bold>Top 10 Leaderboard</bold></gold>'
|
||||
lore: '<gray>Who are the best players?</gray>'
|
||||
teams:
|
||||
name: '<gold><bold>Teams'
|
||||
lore: '<gray>Click to choose your team.'
|
||||
|
||||
ranking:
|
||||
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:
|
||||
title: '<gradient:red:gold><bold>SpeedHG</bold></gradient>'
|
||||
|
||||
# ── Ohne Teams ────────────────────────────────────────────────────────────
|
||||
|
||||
lobby:
|
||||
- " <variant>"
|
||||
- "<gray><st> "
|
||||
- "Players: <green><online>/<max>"
|
||||
- "Kit: <yellow><kit>"
|
||||
- "Style: <yellow><style>"
|
||||
- "Rank: <rank>"
|
||||
- ""
|
||||
- "<gray>Waiting for start..."
|
||||
- ""
|
||||
- "<yellow>play.mcscrims.club"
|
||||
- ''
|
||||
- '<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>Rank: <rank></gray>'
|
||||
- ''
|
||||
- '<dark_gray>play.mcscrims.club</dark_gray>'
|
||||
- ''
|
||||
|
||||
ingame:
|
||||
- "<gray><st> "
|
||||
- "Time: <green><timer>"
|
||||
- "Players: <red><alive>"
|
||||
- "Kills: <green><kills>"
|
||||
- "Rank: <rank>"
|
||||
- ""
|
||||
- "Border: <red><border>"
|
||||
- ""
|
||||
- "<yellow>play.mcscrims.club"
|
||||
- ''
|
||||
- '<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>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:
|
||||
kit_selector:
|
||||
@@ -235,6 +281,19 @@ gui:
|
||||
title: '<aqua><bold>Profile & Stats</bold></aqua>'
|
||||
leaderboard_menu:
|
||||
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:
|
||||
oracle:
|
||||
|
||||
@@ -42,6 +42,3 @@ commands:
|
||||
perks:
|
||||
description: 'Perk-Auswahl öffnen'
|
||||
usage: '/perks'
|
||||
team:
|
||||
description: 'Team-System for SpeedHG'
|
||||
usage: '/team <invite|accept|deny|leave|kick|info> [player]'
|
||||
Reference in New Issue
Block a user