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:
@@ -4,6 +4,7 @@ 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.CustomGameSettings
|
||||
@@ -35,6 +36,8 @@ import club.mcscrims.speedhg.perk.impl.VampirePerk
|
||||
import club.mcscrims.speedhg.perk.listener.PerkEventDispatcher
|
||||
import club.mcscrims.speedhg.ranking.RankingManager
|
||||
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.world.DataPackManager
|
||||
import club.mcscrims.speedhg.world.SurfaceBlockPopulator
|
||||
@@ -100,6 +103,9 @@ class SpeedHG : JavaPlugin() {
|
||||
lateinit var dataPackManager: DataPackManager
|
||||
private set
|
||||
|
||||
lateinit var teamManager: TeamManager
|
||||
private set
|
||||
|
||||
override fun onLoad()
|
||||
{
|
||||
instance = this
|
||||
@@ -157,6 +163,13 @@ class SpeedHG : JavaPlugin() {
|
||||
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()
|
||||
|
||||
@@ -173,6 +186,7 @@ class SpeedHG : JavaPlugin() {
|
||||
{
|
||||
podiumManager.cleanup()
|
||||
if ( ::perkManager.isInitialized ) perkManager.shutdown()
|
||||
if ( ::teamManager.isInitialized ) teamManager.reset()
|
||||
if ( ::statsManager.isInitialized ) statsManager.shutdown()
|
||||
if ( ::databaseManager.isInitialized ) databaseManager.disconnect()
|
||||
if ( ::dataPackManager.isInitialized ) dataPackManager.uninstall()
|
||||
@@ -230,6 +244,12 @@ class SpeedHG : JavaPlugin() {
|
||||
tabCompleter = rankingCommand
|
||||
}
|
||||
|
||||
val teamCommand = TeamCommand()
|
||||
getCommand( "team" )?.apply {
|
||||
setExecutor( teamCommand )
|
||||
tabCompleter = teamCommand
|
||||
}
|
||||
|
||||
getCommand( "leaderboard" )?.setExecutor( LeaderboardCommand() )
|
||||
getCommand( "perks" )?.setExecutor( PerksCommand() )
|
||||
}
|
||||
@@ -245,6 +265,7 @@ class SpeedHG : JavaPlugin() {
|
||||
pm.registerEvents( StatsListener(), this )
|
||||
pm.registerEvents( MenuListener(), this )
|
||||
pm.registerEvents(PerkEventDispatcher( this, perkManager ), this )
|
||||
pm.registerEvents( TeamListener(), this )
|
||||
}
|
||||
|
||||
private fun registerRecipes()
|
||||
|
||||
232
src/main/kotlin/club/mcscrims/speedhg/command/TeamCommand.kt
Normal file
232
src/main/kotlin/club/mcscrims/speedhg/command/TeamCommand.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -61,8 +61,8 @@ data class CustomGameSettings(
|
||||
@SerialName("bunker_radius") val bunkerRadius: Double = 10.0,
|
||||
|
||||
// Gladiator
|
||||
@SerialName("arena_radius") val arenaRadius: Int = 23,
|
||||
@SerialName("arena_height") val arenaHeight: Int = 10,
|
||||
@SerialName("arena_radius") val arenaRadius: Int = 11,
|
||||
@SerialName("arena_height") val arenaHeight: Int = 7,
|
||||
@SerialName("wither_after_seconds") val witherAfterSeconds: Int = 180,
|
||||
|
||||
// Venom
|
||||
|
||||
@@ -274,15 +274,27 @@ class GameManager(
|
||||
checkWin()
|
||||
}
|
||||
|
||||
private fun checkWin()
|
||||
{
|
||||
if ( currentState != GameState.INGAME && currentState != GameState.INVINCIBILITY ) return
|
||||
private fun checkWin() {
|
||||
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 winnerName = if ( winnerUUID != null ) Bukkit.getPlayer( winnerUUID )?.name ?: "N/A" else "N/A"
|
||||
endGame( winnerName )
|
||||
// Den sichtbaren Gewinner-Namen ermitteln:
|
||||
// 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 winnerTeam = if ( plugin.teamManager.isEnabled && winnerUUID != null )
|
||||
plugin.teamManager.getTeam( winnerUUID ) else null
|
||||
|
||||
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.rankingManager.onPlayerResult( p, isWinner = true )
|
||||
}
|
||||
}
|
||||
|
||||
plugin.kitManager.clearAll()
|
||||
plugin.perkManager.removeAllActivePerks()
|
||||
|
||||
Bukkit.getOnlinePlayers().forEach { p ->
|
||||
p.showTitle(Title.title(
|
||||
p.trans( "title.win-main", "winner" to winnerName ),
|
||||
p.trans( "title.win-sub" )
|
||||
@@ -315,6 +327,9 @@ class GameManager(
|
||||
p.sendMsg( "game.win-chat", "winner" to winnerName )
|
||||
}
|
||||
|
||||
plugin.kitManager.clearAll()
|
||||
plugin.perkManager.removeAllActivePerks()
|
||||
|
||||
plugin.discordWebhookManager.broadcastEmbed(
|
||||
title = "🏆 Spiel beendet!",
|
||||
description = "**$winnerName** hat das Spiel gewonnen! GG!",
|
||||
@@ -328,6 +343,25 @@ class GameManager(
|
||||
|
||||
// --- 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(
|
||||
player: Player,
|
||||
world: World,
|
||||
@@ -368,6 +402,10 @@ class GameManager(
|
||||
{
|
||||
if ( p == target ) continue
|
||||
|
||||
if ( plugin.teamManager.isEnabled &&
|
||||
plugin.teamManager.areInSameTeam( p, target ))
|
||||
continue
|
||||
|
||||
val dist = p.location.distanceSquared( target.location )
|
||||
if ( dist < minDistance )
|
||||
{
|
||||
|
||||
@@ -72,10 +72,4 @@ abstract class ActiveAbility(
|
||||
*/
|
||||
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 ) {}
|
||||
|
||||
}
|
||||
@@ -224,11 +224,6 @@ class AnchorKit : Kit() {
|
||||
)
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -179,11 +179,6 @@ class BlackPantherKit : Kit()
|
||||
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -10,7 +10,6 @@ import club.mcscrims.speedhg.kit.ability.ActiveAbility
|
||||
import club.mcscrims.speedhg.kit.ability.PassiveAbility
|
||||
import club.mcscrims.speedhg.util.ItemBuilder
|
||||
import club.mcscrims.speedhg.util.WorldEditUtils
|
||||
import club.mcscrims.speedhg.util.trans
|
||||
import com.sk89q.worldedit.bukkit.BukkitAdapter
|
||||
import com.sk89q.worldedit.math.Vector2
|
||||
import com.sk89q.worldedit.regions.CylinderRegion
|
||||
@@ -154,13 +153,6 @@ class GladiatorKit : Kit() {
|
||||
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 ) {
|
||||
|
||||
@@ -172,13 +172,6 @@ class GoblinKit : Kit() {
|
||||
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(
|
||||
player: Player
|
||||
) {
|
||||
@@ -237,13 +230,6 @@ class GoblinKit : Kit() {
|
||||
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 ) {
|
||||
|
||||
@@ -143,13 +143,6 @@ class IceMageKit : Kit() {
|
||||
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 ) {
|
||||
|
||||
@@ -218,11 +218,6 @@ class PuppetKit : Kit() {
|
||||
player.sendActionBar(player.trans("kits.puppet.messages.drain_start"))
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -184,8 +184,6 @@ class RattlesnakeKit : Kit() {
|
||||
|
||||
return AbilityResult.Success
|
||||
}
|
||||
|
||||
override fun onFullyCharged(player: Player) { /* not used – hitsRequired = 0 */ }
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -209,13 +209,6 @@ class TeslaKit : Kit() {
|
||||
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" ))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -169,13 +169,6 @@ class VenomKit : Kit() {
|
||||
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 ) {
|
||||
@@ -257,13 +250,6 @@ class VenomKit : Kit() {
|
||||
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 ) {
|
||||
|
||||
@@ -159,11 +159,6 @@ class VoodooKit : Kit() {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -6,9 +6,11 @@ import club.mcscrims.speedhg.kit.KitManager
|
||||
import club.mcscrims.speedhg.kit.KitMetaData
|
||||
import club.mcscrims.speedhg.kit.Playstyle
|
||||
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.IceMageKit
|
||||
import club.mcscrims.speedhg.kit.impl.VenomKit
|
||||
import club.mcscrims.speedhg.util.trans
|
||||
import net.kyori.adventure.text.Component
|
||||
import net.kyori.adventure.text.format.NamedTextColor
|
||||
import org.bukkit.Material
|
||||
@@ -92,7 +94,10 @@ class KitEventDispatcher(
|
||||
// ── 1. Increment charge counter ──────────────────────────────────────
|
||||
val justFullyCharged = chargeData.registerHit()
|
||||
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 ─────────────────────────────────────────
|
||||
@@ -405,4 +410,31 @@ class KitEventDispatcher(
|
||||
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 )
|
||||
}
|
||||
|
||||
}
|
||||
@@ -74,6 +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 ) }
|
||||
.minByOrNull { it.location.distanceSquared(player.location) }
|
||||
|
||||
private fun buildTrackerComponent(player: Player, nearest: Player): Component {
|
||||
|
||||
36
src/main/kotlin/club/mcscrims/speedhg/team/Team.kt
Normal file
36
src/main/kotlin/club/mcscrims/speedhg/team/Team.kt
Normal 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
|
||||
}
|
||||
55
src/main/kotlin/club/mcscrims/speedhg/team/TeamListener.kt
Normal file
55
src/main/kotlin/club/mcscrims/speedhg/team/TeamListener.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
294
src/main/kotlin/club/mcscrims/speedhg/team/TeamManager.kt
Normal file
294
src/main/kotlin/club/mcscrims/speedhg/team/TeamManager.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@ anti-runner:
|
||||
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
|
||||
|
||||
teams:
|
||||
enabled: true
|
||||
max-size: 2
|
||||
|
||||
recraftNerf:
|
||||
enabled: false
|
||||
beforeFeast: true
|
||||
|
||||
@@ -81,6 +81,53 @@ disasters:
|
||||
warning-main: '<yellow><bold>THUNDERSTORM!</bold></yellow>'
|
||||
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:
|
||||
kit:
|
||||
usage: '<red>Usage: /kit <kitName> <playstyle></red>'
|
||||
@@ -239,6 +286,9 @@ perks:
|
||||
message: '<gold>🍎 Scavenged a Golden Apple!</gold>'
|
||||
|
||||
kits:
|
||||
needed_hits: '<gold>⚡ Ability: <white><current>/<required> Hits</white></gold>'
|
||||
ability_charged: '<green><bold>⚡ ABILITY READY!</bold></green>'
|
||||
|
||||
backup:
|
||||
name: '<gradient:gold:#ff841f><bold>Backup</bold></gradient>'
|
||||
lore:
|
||||
|
||||
@@ -35,3 +35,6 @@ 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