Add team system and team-based win condition

Introduce a full in-memory team system and integrate it into gameplay and UI.

- Add Team, TeamManager, TeamListener and TeamCommand to manage teams, invites, accept/deny, leave, kick and info. TeamManager stores teams and pending invites (INVITE_TTL_MS) using concurrent maps and exposes helper/query methods (areInSameTeam, allAliveInSameTeam, getInviterFor, cleanExpiredInvites, reset).
- Wire TeamManager into SpeedHG: initialize, schedule periodic invite cleanup, register command and listener, and reset teams on plugin disable.
- Update GameManager to consider teams for win conditions, build team winner names, exclude teammates from random teleport/compass logic, and credit all winning team members for stats/ranking.
- Move ability charge feedback out of individual kits into KitEventDispatcher: remove per-kit onFullyCharged overrides and add centralized ActionBar/sound feedback (sendChargeUpdateActionBar/sendChargeReadyActionBar). ActiveAbility no longer defines onFullyCharged.
- Make OraclePerk ignore teammates when finding nearest enemy.
- Add config entries for teams and corresponding language strings; tweak some default values in CustomGameSettings (gladiator arena radius/height and minor formatting changes).

These changes enable 2-player teams (configurable), friendly-fire prevention, invite lifecycle handling, and team-aware endgame logic.
This commit is contained in:
TDSTOS
2026-04-09 02:33:14 +02:00
parent a6675c882b
commit 4d32fe677c
23 changed files with 788 additions and 110 deletions

View File

@@ -4,6 +4,7 @@ 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.CustomGameSettings import club.mcscrims.speedhg.config.CustomGameSettings
@@ -35,6 +36,8 @@ import club.mcscrims.speedhg.perk.impl.VampirePerk
import club.mcscrims.speedhg.perk.listener.PerkEventDispatcher import club.mcscrims.speedhg.perk.listener.PerkEventDispatcher
import club.mcscrims.speedhg.ranking.RankingManager import club.mcscrims.speedhg.ranking.RankingManager
import club.mcscrims.speedhg.scoreboard.ScoreboardManager import club.mcscrims.speedhg.scoreboard.ScoreboardManager
import club.mcscrims.speedhg.team.TeamListener
import club.mcscrims.speedhg.team.TeamManager
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
@@ -100,6 +103,9 @@ class SpeedHG : JavaPlugin() {
lateinit var dataPackManager: DataPackManager lateinit var dataPackManager: DataPackManager
private set private set
lateinit var teamManager: TeamManager
private set
override fun onLoad() override fun onLoad()
{ {
instance = this instance = this
@@ -157,6 +163,13 @@ class SpeedHG : JavaPlugin() {
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()
@@ -173,6 +186,7 @@ class SpeedHG : JavaPlugin() {
{ {
podiumManager.cleanup() podiumManager.cleanup()
if ( ::perkManager.isInitialized ) perkManager.shutdown() if ( ::perkManager.isInitialized ) perkManager.shutdown()
if ( ::teamManager.isInitialized ) teamManager.reset()
if ( ::statsManager.isInitialized ) statsManager.shutdown() if ( ::statsManager.isInitialized ) statsManager.shutdown()
if ( ::databaseManager.isInitialized ) databaseManager.disconnect() if ( ::databaseManager.isInitialized ) databaseManager.disconnect()
if ( ::dataPackManager.isInitialized ) dataPackManager.uninstall() if ( ::dataPackManager.isInitialized ) dataPackManager.uninstall()
@@ -230,6 +244,12 @@ 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() )
} }
@@ -245,6 +265,7 @@ 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 )
} }
private fun registerRecipes() private fun registerRecipes()

View File

@@ -0,0 +1,232 @@
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("§cNur Spieler können diesen Befehl nutzen.")
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()
}
}

View File

@@ -54,19 +54,19 @@ data class CustomGameSettings(
*/ */
@Serializable @Serializable
data class KitOverride( data class KitOverride(
@SerialName("hits_required") val hitsRequired: Int = -1, @SerialName("hits_required") val hitsRequired: Int = -1,
// Goblin // Goblin
@SerialName("steal_duration_seconds") val stealDuration: Int = 60, @SerialName("steal_duration_seconds") val stealDuration: Int = 60,
@SerialName("bunker_radius") val bunkerRadius: Double = 10.0, @SerialName("bunker_radius") val bunkerRadius: Double = 10.0,
// Gladiator // Gladiator
@SerialName("arena_radius") val arenaRadius: Int = 23, @SerialName("arena_radius") val arenaRadius: Int = 11,
@SerialName("arena_height") val arenaHeight: Int = 10, @SerialName("arena_height") val arenaHeight: Int = 7,
@SerialName("wither_after_seconds") val witherAfterSeconds: Int = 180, @SerialName("wither_after_seconds") val witherAfterSeconds: Int = 180,
// Venom // Venom
@SerialName("shield_duration_ticks") val shieldDurationTicks: Long = 160L, @SerialName("shield_duration_ticks") val shieldDurationTicks: Long = 160L,
@SerialName("shield_capacity") val shieldCapacity: Double = 15.0, @SerialName("shield_capacity") val shieldCapacity: Double = 15.0,
// Voodoo // Voodoo
@@ -82,10 +82,10 @@ data class CustomGameSettings(
// Rattlesnake // Rattlesnake
@SerialName("pounce_cooldown_ms") val pounceCooldownMs: Long = 20_000L, @SerialName("pounce_cooldown_ms") val pounceCooldownMs: Long = 20_000L,
@SerialName("pounce_max_sneak_ms") val pounceMaxSneakMs: Long = 3_000L, @SerialName("pounce_max_sneak_ms") val pounceMaxSneakMs: Long = 3_000L,
@SerialName("pounce_min_range") val pounceMinRange: Double = 3.0, @SerialName("pounce_min_range") val pounceMinRange: Double = 3.0,
@SerialName("pounce_max_range") val pounceMaxRange: Double = 10.0, @SerialName("pounce_max_range") val pounceMaxRange: Double = 10.0,
@SerialName("pounce_timeout_ticks") val pounceTimeoutTicks: Long = 30L, @SerialName("pounce_timeout_ticks") val pounceTimeoutTicks: Long = 30L,
// TheWorld // TheWorld
@SerialName("tw_ability_cooldown_ms") val abilityCooldownMs: Long = 20_000L, @SerialName("tw_ability_cooldown_ms") val abilityCooldownMs: Long = 20_000L,

View File

@@ -274,15 +274,27 @@ class GameManager(
checkWin() checkWin()
} }
private fun checkWin() private fun checkWin() {
{ if (currentState != GameState.INGAME && currentState != GameState.INVINCIBILITY) return
if ( currentState != GameState.INGAME && currentState != GameState.INVINCIBILITY ) return
if ( alivePlayers.size <= 1 ) val teamManager = plugin.teamManager
{
val roundOver = when {
// Nur noch 0 oder 1 Spieler übrig → immer Ende
alivePlayers.size <= 1 -> true
// Teams aktiv: Alle Überlebenden im selben Team → Team gewinnt
teamManager.isEnabled && teamManager.allAliveInSameTeam(alivePlayers) -> true
else -> false
}
if (roundOver) {
val winnerUUID = alivePlayers.firstOrNull() val winnerUUID = alivePlayers.firstOrNull()
val winnerName = if ( winnerUUID != null ) Bukkit.getPlayer( winnerUUID )?.name ?: "N/A" else "N/A" // Den sichtbaren Gewinner-Namen ermitteln:
endGame( winnerName ) // Bei Team-Sieg alle Mitglieder des Gewinner-Teams anzeigen.
val winnerName = buildWinnerName(winnerUUID)
endGame(winnerName)
} }
} }
@@ -296,18 +308,18 @@ class GameManager(
val winnerUUID = alivePlayers.firstOrNull() val winnerUUID = alivePlayers.firstOrNull()
val winnerTeam = if ( plugin.teamManager.isEnabled && winnerUUID != null )
plugin.teamManager.getTeam( winnerUUID ) else null
Bukkit.getOnlinePlayers().forEach { p -> Bukkit.getOnlinePlayers().forEach { p ->
if ( p.uniqueId == winnerUUID ) val isWinner = winnerTeam?.contains( p.uniqueId ) ?: ( p.uniqueId == winnerUUID )
if ( isWinner )
{ {
plugin.statsManager.addWin( p.uniqueId ) plugin.statsManager.addWin( p.uniqueId )
plugin.rankingManager.onPlayerResult( p, isWinner = true ) plugin.rankingManager.onPlayerResult( p, isWinner = true )
} }
}
plugin.kitManager.clearAll()
plugin.perkManager.removeAllActivePerks()
Bukkit.getOnlinePlayers().forEach { p ->
p.showTitle(Title.title( p.showTitle(Title.title(
p.trans( "title.win-main", "winner" to winnerName ), p.trans( "title.win-main", "winner" to winnerName ),
p.trans( "title.win-sub" ) p.trans( "title.win-sub" )
@@ -315,6 +327,9 @@ class GameManager(
p.sendMsg( "game.win-chat", "winner" to winnerName ) p.sendMsg( "game.win-chat", "winner" to winnerName )
} }
plugin.kitManager.clearAll()
plugin.perkManager.removeAllActivePerks()
plugin.discordWebhookManager.broadcastEmbed( plugin.discordWebhookManager.broadcastEmbed(
title = "🏆 Spiel beendet!", title = "🏆 Spiel beendet!",
description = "**$winnerName** hat das Spiel gewonnen! GG!", description = "**$winnerName** hat das Spiel gewonnen! GG!",
@@ -328,6 +343,25 @@ class GameManager(
// --- Helfer Methoden --- // --- Helfer Methoden ---
private fun buildWinnerName(anyAliveUUID: UUID?): String {
anyAliveUUID ?: return "N/A"
val teamManager = plugin.teamManager
if (!teamManager.isEnabled) {
return Bukkit.getPlayer(anyAliveUUID)?.name ?: "N/A"
}
val winnerTeam = teamManager.getTeam(anyAliveUUID)
return if (winnerTeam != null && winnerTeam.size > 1) {
// "PlayerA & PlayerB" als Team-Gewinner-String
winnerTeam.members
.mapNotNull { Bukkit.getPlayer(it)?.name }
.joinToString(" & ")
} else {
Bukkit.getPlayer(anyAliveUUID)?.name ?: "N/A"
}
}
private fun teleportRandomly( private fun teleportRandomly(
player: Player, player: Player,
world: World, world: World,
@@ -368,6 +402,10 @@ class GameManager(
{ {
if ( p == target ) continue if ( p == target ) continue
if ( plugin.teamManager.isEnabled &&
plugin.teamManager.areInSameTeam( p, target ))
continue
val dist = p.location.distanceSquared( target.location ) val dist = p.location.distanceSquared( target.location )
if ( dist < minDistance ) if ( dist < minDistance )
{ {

View File

@@ -72,10 +72,4 @@ abstract class ActiveAbility(
*/ */
abstract fun execute( player: Player ): AbilityResult abstract fun execute( player: Player ): AbilityResult
/**
* Called when the ability's charge completes ([hitsRequired] hits landed).
* Override for sounds, particles, ActionBar feedback, etc.
*/
open fun onFullyCharged( player: Player ) {}
} }

View File

@@ -224,11 +224,6 @@ class AnchorKit : Kit() {
) )
return AbilityResult.Success return AbilityResult.Success
} }
override fun onFullyCharged(player: Player) {
player.playSound(player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 0.8f)
player.sendActionBar(player.trans("kits.anchor.messages.ability_charged"))
}
} }
// ========================================================================= // =========================================================================

View File

@@ -179,11 +179,6 @@ class BlackPantherKit : Kit()
return AbilityResult.Success return AbilityResult.Success
} }
override fun onFullyCharged(player: Player) {
player.playSound(player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f)
player.sendActionBar(player.trans("kits.blackpanther.messages.ability_charged"))
}
} }
// ========================================================================= // =========================================================================

View File

@@ -10,7 +10,6 @@ import club.mcscrims.speedhg.kit.ability.ActiveAbility
import club.mcscrims.speedhg.kit.ability.PassiveAbility import club.mcscrims.speedhg.kit.ability.PassiveAbility
import club.mcscrims.speedhg.util.ItemBuilder import club.mcscrims.speedhg.util.ItemBuilder
import club.mcscrims.speedhg.util.WorldEditUtils import club.mcscrims.speedhg.util.WorldEditUtils
import club.mcscrims.speedhg.util.trans
import com.sk89q.worldedit.bukkit.BukkitAdapter import com.sk89q.worldedit.bukkit.BukkitAdapter
import com.sk89q.worldedit.math.Vector2 import com.sk89q.worldedit.math.Vector2
import com.sk89q.worldedit.regions.CylinderRegion import com.sk89q.worldedit.regions.CylinderRegion
@@ -154,13 +153,6 @@ class GladiatorKit : Kit() {
return AbilityResult.Success return AbilityResult.Success
} }
override fun onFullyCharged(
player: Player
) {
player.playSound( player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f )
player.sendActionBar(player.trans( "kits.gladiator.messages.ability_charged" ))
}
} }
private class NoPassive( playstyle: Playstyle ) : PassiveAbility( playstyle ) { private class NoPassive( playstyle: Playstyle ) : PassiveAbility( playstyle ) {

View File

@@ -172,13 +172,6 @@ class GoblinKit : Kit() {
return AbilityResult.Success return AbilityResult.Success
} }
override fun onFullyCharged(
player: Player
) {
player.playSound( player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f )
player.sendActionBar(player.trans( "kits.goblin.messages.ability_charged" ))
}
fun cancelStealTask( fun cancelStealTask(
player: Player player: Player
) { ) {
@@ -237,13 +230,6 @@ class GoblinKit : Kit() {
return AbilityResult.Success return AbilityResult.Success
} }
override fun onFullyCharged(
player: Player
) {
player.playSound( player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f )
player.sendActionBar(player.trans( "kits.goblin.messages.ability_charged" ))
}
} }
private class AggressiveNoPassive : PassiveAbility( Playstyle.AGGRESSIVE ) { private class AggressiveNoPassive : PassiveAbility( Playstyle.AGGRESSIVE ) {

View File

@@ -143,13 +143,6 @@ class IceMageKit : Kit() {
return AbilityResult.Success return AbilityResult.Success
} }
override fun onFullyCharged(
player: Player
) {
player.playSound( player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f )
player.sendActionBar(player.trans( "kits.icemage.messages.ability_charged" ))
}
} }
private class AggressivePassive : PassiveAbility( Playstyle.AGGRESSIVE ) { private class AggressivePassive : PassiveAbility( Playstyle.AGGRESSIVE ) {

View File

@@ -218,11 +218,6 @@ class PuppetKit : Kit() {
player.sendActionBar(player.trans("kits.puppet.messages.drain_start")) player.sendActionBar(player.trans("kits.puppet.messages.drain_start"))
return AbilityResult.Success return AbilityResult.Success
} }
override fun onFullyCharged(player: Player) {
player.playSound(player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f)
player.sendActionBar(player.trans("kits.puppet.messages.ability_charged"))
}
} }
// ========================================================================= // =========================================================================
@@ -277,11 +272,6 @@ class PuppetKit : Kit() {
) )
return AbilityResult.Success return AbilityResult.Success
} }
override fun onFullyCharged(player: Player) {
player.playSound(player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f)
player.sendActionBar(player.trans("kits.puppet.messages.ability_charged"))
}
} }
class NoPassive(playstyle: Playstyle) : PassiveAbility(playstyle) { class NoPassive(playstyle: Playstyle) : PassiveAbility(playstyle) {

View File

@@ -184,8 +184,6 @@ class RattlesnakeKit : Kit() {
return AbilityResult.Success return AbilityResult.Success
} }
override fun onFullyCharged(player: Player) { /* not used hitsRequired = 0 */ }
} }
// ========================================================================= // =========================================================================

View File

@@ -209,13 +209,6 @@ class TeslaKit : Kit() {
return AbilityResult.Success return AbilityResult.Success
} }
override fun onFullyCharged(
player: Player
) {
player.playSound( player.location, Sound.BLOCK_BEACON_ACTIVATE, 0.8f, 1.8f )
player.sendActionBar(player.trans( "kits.tesla.messages.ability_charged" ))
}
} }
// ========================================================================= // =========================================================================

View File

@@ -169,13 +169,6 @@ class VenomKit : Kit() {
return AbilityResult.Success return AbilityResult.Success
} }
override fun onFullyCharged(
player: Player
) {
player.playSound( player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f )
player.sendActionBar(player.trans( "kits.venom.messages.ability_charged" ))
}
} }
private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) { private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) {
@@ -257,13 +250,6 @@ class VenomKit : Kit() {
return AbilityResult.Success return AbilityResult.Success
} }
override fun onFullyCharged(
player: Player
) {
player.playSound( player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f )
player.sendActionBar(player.trans( "kits.venom.messages.ability_charged" ))
}
} }
private inner class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE ) { private inner class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE ) {

View File

@@ -159,11 +159,6 @@ class VoodooKit : Kit() {
return AbilityResult.Success return AbilityResult.Success
} }
override fun onFullyCharged(player: Player) {
player.playSound(player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f)
player.sendActionBar(player.trans("kits.voodoo.messages.ability_charged"))
}
} }
// ========================================================================= // =========================================================================
@@ -209,11 +204,6 @@ class VoodooKit : Kit() {
return AbilityResult.Success return AbilityResult.Success
} }
override fun onFullyCharged(player: Player) {
player.playSound(player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f)
player.sendActionBar(player.trans("kits.voodoo.messages.ability_charged"))
}
} }
// ========================================================================= // =========================================================================

View File

@@ -6,9 +6,11 @@ import club.mcscrims.speedhg.kit.KitManager
import club.mcscrims.speedhg.kit.KitMetaData import club.mcscrims.speedhg.kit.KitMetaData
import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.Playstyle
import club.mcscrims.speedhg.kit.ability.AbilityResult import club.mcscrims.speedhg.kit.ability.AbilityResult
import club.mcscrims.speedhg.kit.charge.ChargeState
import club.mcscrims.speedhg.kit.impl.BlackPantherKit import club.mcscrims.speedhg.kit.impl.BlackPantherKit
import club.mcscrims.speedhg.kit.impl.IceMageKit import club.mcscrims.speedhg.kit.impl.IceMageKit
import club.mcscrims.speedhg.kit.impl.VenomKit import club.mcscrims.speedhg.kit.impl.VenomKit
import club.mcscrims.speedhg.util.trans
import net.kyori.adventure.text.Component import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.Material import org.bukkit.Material
@@ -92,7 +94,10 @@ class KitEventDispatcher(
// ── 1. Increment charge counter ────────────────────────────────────── // ── 1. Increment charge counter ──────────────────────────────────────
val justFullyCharged = chargeData.registerHit() val justFullyCharged = chargeData.registerHit()
if ( justFullyCharged ) { if ( justFullyCharged ) {
attackerKit.getActiveAbility( attackerPlaystyle ).onFullyCharged( attacker ) sendChargeReadyActionBar( attacker )
} else if ( chargeData.state == ChargeState.CHARGING ) {
val currentHits = chargeData.hitsRequired - chargeData.hitsRemaining
sendChargeUpdateActionBar( attacker, currentHits, chargeData.hitsRequired )
} }
// ── 2. Attacker passive hook ───────────────────────────────────────── // ── 2. Attacker passive hook ─────────────────────────────────────────
@@ -405,4 +410,31 @@ class KitEventDispatcher(
return plugin.gameManager.alivePlayers.contains( player.uniqueId ) return plugin.gameManager.alivePlayers.contains( player.uniqueId )
} }
// ── Charge-Feedback ───────────────────────────────────────────────────────
/**
* Sendet eine Actionbar-Anzeige über den aktuellen Ladestand der Fähigkeit.
* Wird nur aufgerufen wenn die Fähigkeit noch im CHARGING-Zustand ist —
* also niemals bei hitsRequired == 0 (Always READY) und niemals wenn
* die Fähigkeit gerade erst vollständig aufgeladen wurde.
*/
private fun sendChargeUpdateActionBar(
player: Player,
currentHits: Int,
requiredHits: Int
) {
// Guard: sollte nie 0 sein, aber defensiv absichern
if ( requiredHits <= 0 ) return
val component = player.trans( "kits.needed_hits", "current" to currentHits.toString(), "required" to requiredHits.toString() )
player.sendActionBar( component )
}
private fun sendChargeReadyActionBar(
player: Player
) {
val component = player.trans( "kits.ability_charged" )
player.sendActionBar( component )
player.playSound( player.location, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 0.7f, 1.4f )
}
} }

View File

@@ -74,6 +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 ) }
.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 {

View File

@@ -0,0 +1,36 @@
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
}

View File

@@ -0,0 +1,55 @@
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)
}
}
}

View File

@@ -0,0 +1,294 @@
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)
}
}

View File

@@ -21,6 +21,10 @@ anti-runner:
ignore-vertical-distance: 15.0 # Wenn Höhenunterschied > 15, Timer ignorieren ignore-vertical-distance: 15.0 # Wenn Höhenunterschied > 15, Timer ignorieren
ignore-cave-surface-mix: true # Ignorieren, wenn einer Sonne hat und der andere nicht ignore-cave-surface-mix: true # Ignorieren, wenn einer Sonne hat und der andere nicht
teams:
enabled: true
max-size: 2
recraftNerf: recraftNerf:
enabled: false enabled: false
beforeFeast: true beforeFeast: true

View File

@@ -81,6 +81,53 @@ disasters:
warning-main: '<yellow><bold>THUNDERSTORM!</bold></yellow>' warning-main: '<yellow><bold>THUNDERSTORM!</bold></yellow>'
warning-sub: '<gray>Your height attracts lightning!</gray>' warning-sub: '<gray>Your height attracts lightning!</gray>'
team:
disabled: '<prefix><red>Teams are disabled in this round.</red>'
usage: '<prefix><gray>Usage: /team <invite|accept|deny|leave|kick|info> [player]</gray>'
game_running: '<prefix><red>Teams cannot be changed while the game is running!</red>'
not_in_team: '<prefix><red>You are not in a team.</red>'
already_in_team: '<prefix><red>You are already in a team!</red>'
player_not_found: '<prefix><red>Player <name> is not online.</red>'
invite:
usage: '<prefix><red>Usage: /team invite <player></red>'
sent: '<prefix><green>✉ Invite sent to <yellow><name></yellow>. (Expires in <time>s)</green>'
received: '<prefix><gold>✉ <yellow><name></yellow> invited you to their team! <green>/team accept <name></green> or <red>/team deny <name></red> (expires in <time>s)</gold>'
self: '<prefix><red>You cannot invite yourself.</red>'
already_teammate: '<prefix><red>This player is already your teammate.</red>'
target_has_team: '<prefix><red><name> is already in a team.</red>'
team_full: '<prefix><red>Your team is full!</red>'
already_pending: '<prefix><red>There is already a pending invite for <name>.</red>'
accept:
usage: '<prefix><red>Usage: /team accept <player></red>'
no_invite: '<prefix><red>You have no pending invite from <name>.</red>'
expired: '<prefix><red>The invite from <name> has expired.</red>'
joined: '<prefix><green>✔ <yellow><name></yellow> joined the team! Members: <white><members></white></green>'
deny:
usage: '<prefix><red>Usage: /team deny <player></red>'
success: '<prefix><gray>Invite from <name> declined.</gray>'
received: '<prefix><gray><name> declined your team invite.</gray>'
leave:
success: '<prefix><gray>You left the team.</gray>'
disbanded: '<prefix><red>The team has been disbanded.</red>'
member_left: '<prefix><gray><name> left the team.</gray>'
kick:
usage: '<prefix><red>Usage: /team kick <player></red>'
success: '<prefix><yellow>Kicked <name> from the team.</yellow>'
received: '<prefix><red>You were kicked from the team by <name>.</red>'
not_leader: '<prefix><red>Only the team leader can kick players.</red>'
not_in_your_team: '<prefix><red><name> is not in your team.</red>'
self: '<prefix><red>You cannot kick yourself. Use /team leave.</red>'
info:
header: '<gray>━━━━━ <gold>Team Members</gold> ━━━━━</gray>'
member: ' <gray>#<index></gray> <white><name></white><yellow><leader></yellow>'
footer: '<gray>━━━━━━━━━━━━━━━━━━━━━ (<size>/<max>)</gray>'
commands: commands:
kit: kit:
usage: '<red>Usage: /kit <kitName> <playstyle></red>' usage: '<red>Usage: /kit <kitName> <playstyle></red>'
@@ -239,6 +286,9 @@ perks:
message: '<gold>🍎 Scavenged a Golden Apple!</gold>' message: '<gold>🍎 Scavenged a Golden Apple!</gold>'
kits: kits:
needed_hits: '<gold>⚡ Ability: <white><current>/<required> Hits</white></gold>'
ability_charged: '<green><bold>⚡ ABILITY READY!</bold></green>'
backup: backup:
name: '<gradient:gold:#ff841f><bold>Backup</bold></gradient>' name: '<gradient:gold:#ff841f><bold>Backup</bold></gradient>'
lore: lore:

View File

@@ -34,4 +34,7 @@ commands:
permission: speedhg.admin.ranking permission: speedhg.admin.ranking
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]'