diff --git a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt index 5310af6..00b7a88 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt @@ -21,6 +21,7 @@ import club.mcscrims.speedhg.gui.listener.MenuListener import club.mcscrims.speedhg.kit.KitManager import club.mcscrims.speedhg.kit.impl.* import club.mcscrims.speedhg.kit.listener.KitEventDispatcher +import club.mcscrims.speedhg.listener.ChatListener import club.mcscrims.speedhg.listener.ConnectListener import club.mcscrims.speedhg.listener.GameStateListener import club.mcscrims.speedhg.listener.SoupListener @@ -288,6 +289,7 @@ class SpeedHG : JavaPlugin() { pm.registerEvents(PerkEventDispatcher( this, perkManager ), this ) pm.registerEvents( TeamListener(), this ) pm.registerEvents( lobbyItemManager, this ) + pm.registerEvents(ChatListener( this, VolcanoServerRankProvider() ), this ) } private fun registerRecipes() diff --git a/src/main/kotlin/club/mcscrims/speedhg/config/CustomGameSettings.kt b/src/main/kotlin/club/mcscrims/speedhg/config/CustomGameSettings.kt index f60e9d9..e6bef43 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/config/CustomGameSettings.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/config/CustomGameSettings.kt @@ -37,9 +37,22 @@ data class CustomGameSettings( /** * Gibt den hitsRequired-Wert für ein Kit zurück. * Priorität: kit-spezifisch > global > hardcoded Default + * + * Wenn hardcodedDefault == 0, ist das Kit explizit als cooldown-only markiert + * und der globale Wert wird niemals angewendet. */ - fun hitsRequired(kitId: String, hardcodedDefault: Int): Int = - kits[kitId]?.hitsRequired ?: globalHitsRequired.takeIf { it >= 0 } ?: hardcodedDefault + fun hitsRequired( + kitId: String, + hardcodedDefault: Int + ): Int + { + // A hardcoded 0 means the kit is explicitly cooldown-based — never override it. + if ( hardcodedDefault == 0 ) return 0 + + return kits[ kitId ]?.hitsRequired?.takeIf { it >= 0 } + ?: globalHitsRequired.takeIf { it >= 0 } + ?: hardcodedDefault + } } // ----------------------------------------------------------------- @@ -88,7 +101,7 @@ data class CustomGameSettings( @SerialName("pounce_timeout_ticks") val pounceTimeoutTicks: Long = 30L, // TheWorld - @SerialName("tw_ability_cooldown_ms") val abilityCooldownMs: Long = 20_000L, + @SerialName("tw_ability_cooldown_ms") val abilityCooldownMs: Long = 25_000L, @SerialName("tw_shockwave_radius") val shockwaveRadius: Double = 6.0, @SerialName("tw_teleport_range") val teleportRange: Double = 10.0, @SerialName("tw_max_teleport_charges") val maxTeleportCharges: Int = 3, diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/ability/ActiveAbility.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/ability/ActiveAbility.kt index 80e7462..f1f2557 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/ability/ActiveAbility.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/ability/ActiveAbility.kt @@ -52,7 +52,7 @@ abstract class ActiveAbility( private var _hitsRequired: Int = -1 val hitsRequired: Int - get() = _hitsRequired.takeIf { it >= 0 } ?: hardcodedHitsRequired + get() = _hitsRequired.takeIf { it > 0 } ?: hardcodedHitsRequired /** * Einmalig beim applyKit() aufgerufen – danach ist der Wert gecacht. diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt index b403f01..ecff76d 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt @@ -7,6 +7,7 @@ import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.ability.AbilityResult import club.mcscrims.speedhg.kit.ability.ActiveAbility import club.mcscrims.speedhg.kit.ability.PassiveAbility +import club.mcscrims.speedhg.kit.listener.KitEventDispatcher.Companion.MAX_KNOCKBACK_HEIGHT_Y import club.mcscrims.speedhg.util.ItemBuilder import club.mcscrims.speedhg.util.WorldEditUtils import club.mcscrims.speedhg.util.trans @@ -61,6 +62,9 @@ class BlackPantherKit : Kit() /** Players currently in Fist Mode: UUID → expiry timestamp (ms). */ internal val fistModeExpiry: MutableMap = ConcurrentHashMap() + /** Players currently in a pounce — fall damage is suppressed on landing. */ + internal val noFallDamagePlayers: MutableSet = ConcurrentHashMap.newKeySet() + companion object { private fun override() = SpeedHG.instance.customGameManager.settings.kits.kits["blackpanther"] @@ -106,9 +110,12 @@ class BlackPantherKit : Kit() player.inventory.addItem(item) } - override fun onRemove(player: Player) { - fistModeExpiry.remove(player.uniqueId) - cachedItems.remove(player.uniqueId)?.forEach { player.inventory.remove(it) } + override fun onRemove( + player: Player + ) { + fistModeExpiry.remove( player.uniqueId ) + noFallDamagePlayers.remove( player.uniqueId ) + cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) } } // ========================================================================= @@ -128,7 +135,15 @@ class BlackPantherKit : Kit() get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.items.push.description") override val triggerMaterial = Material.BLACK_DYE - override fun execute(player: Player): AbilityResult { + override fun execute( + player: Player + ): AbilityResult + { + if ( player.location.y > MAX_KNOCKBACK_HEIGHT_Y ) + return AbilityResult.ConditionNotMet( + plugin.languageManager.getRawMessage( player, "kits.height_restriction" ) + ) + val enemies = player.world .getNearbyEntities(player.location, PUSH_RADIUS, PUSH_RADIUS, PUSH_RADIUS) .filterIsInstance() @@ -236,8 +251,10 @@ class BlackPantherKit : Kit() override val description: String get() = plugin.languageManager.getDefaultRawMessage("kits.blackpanther.passive.defensive.description") - override fun onMove(player: Player, event: PlayerMoveEvent) - { + override fun onMove( + player: Player, + event: PlayerMoveEvent + ) { if ( event.to.y >= event.from.y ) return if ( player.fallDistance < POUNCE_MIN_FALL ) return @@ -258,7 +275,6 @@ class BlackPantherKit : Kit() impactLoc.world.playSound(impactLoc, Sound.ENTITY_GENERIC_EXPLODE, 1f, 0.7f) impactLoc.world.playSound(impactLoc, Sound.ENTITY_IRON_GOLEM_HURT, 1f, 0.5f) - // Async WorldEdit Krater (Vorsicht: Blöcke setzen muss synchron passieren, also normaler Scheduler) Bukkit.getScheduler().runTaskLater(plugin, Runnable { WorldEditUtils.createCylinder( impactLoc.world, impactLoc.clone().subtract(0.0, 1.0, 0.0), @@ -269,7 +285,8 @@ class BlackPantherKit : Kit() player.sendActionBar(player.trans("kits.blackpanther.messages.wakanda_impact", mapOf("count" to splashTargets.size.toString()))) - // Setze die Fall-Distanz auf 0 zurück, damit der Spieler selbst keinen Vanilla-Fallschaden bekommt + // Suppress fall damage for this landing + noFallDamagePlayers.add( player.uniqueId ) player.fallDistance = 0f } } diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlitzcrankKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlitzcrankKit.kt index 1f89ad3..3f0e8b7 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlitzcrankKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlitzcrankKit.kt @@ -6,6 +6,8 @@ import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.ability.AbilityResult import club.mcscrims.speedhg.kit.ability.ActiveAbility import club.mcscrims.speedhg.kit.ability.PassiveAbility +import club.mcscrims.speedhg.kit.listener.KitEventDispatcher +import club.mcscrims.speedhg.kit.listener.KitEventDispatcher.Companion.MAX_KNOCKBACK_HEIGHT_Y import club.mcscrims.speedhg.util.ItemBuilder import club.mcscrims.speedhg.util.trans import net.kyori.adventure.text.Component @@ -146,6 +148,12 @@ class BlitzcrankKit : Kit() { private fun fireUlt( caster: Player ) { + if ( caster.location.y > MAX_KNOCKBACK_HEIGHT_Y ) + { + caster.sendActionBar(caster.trans( "kits.height_restriction" )) + return + } + val now = System.currentTimeMillis() val lastUlt = ultCooldowns[ caster.uniqueId ] ?: 0L @@ -220,6 +228,11 @@ class BlitzcrankKit : Kit() { override fun execute(player: Player): AbilityResult { + if ( player.location.y > MAX_KNOCKBACK_HEIGHT_Y ) + return AbilityResult.ConditionNotMet( + plugin.languageManager.getRawMessage( player, "kits.height_restriction" ) + ) + val eyeLoc = player.eyeLocation val dir = eyeLoc.direction.normalize() @@ -289,7 +302,15 @@ class BlitzcrankKit : Kit() { override val hardcodedHitsRequired = 15 override val triggerMaterial = Material.PISTON - override fun execute(player: Player): AbilityResult { + override fun execute( + player: Player + ): AbilityResult + { + if ( player.location.y > MAX_KNOCKBACK_HEIGHT_Y ) + return AbilityResult.ConditionNotMet( + plugin.languageManager.getRawMessage( player, "kits.height_restriction" ) + ) + val targets = player.world .getNearbyEntities(player.location, STUN_RADIUS, STUN_RADIUS, STUN_RADIUS) .filterIsInstance() diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TridentKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TridentKit.kt index 1fd909b..7db0b97 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TridentKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TridentKit.kt @@ -62,6 +62,9 @@ class TridentKit : Kit() { private val diveMonitors: MutableMap = ConcurrentHashMap() private val lastSequenceTime: MutableMap = ConcurrentHashMap() + /** Players who have recently launched a dive and should not receive fall damage. */ + internal val noFallDamagePlayers: MutableSet = ConcurrentHashMap.newKeySet() + companion object { const val MAX_DIVE_CHARGES = 3 const val SEQUENCE_COOLDOWN_MS = 25_000L // Cooldown zwischen vollst. Sequenzen @@ -129,6 +132,7 @@ class TridentKit : Kit() { diveCharges.remove( player.uniqueId ) diveMonitors.remove( player.uniqueId )?.cancel() lastSequenceTime.remove( player.uniqueId ) + noFallDamagePlayers.remove( player.uniqueId ) cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) } } @@ -248,6 +252,7 @@ class TridentKit : Kit() { else diveCharges[ player.uniqueId ] = charges - 1 player.velocity = player.velocity.clone().setY( 1.38 ) + noFallDamagePlayers.add( player.uniqueId ) val remaining = diveCharges.getOrDefault( player.uniqueId, 0 ) player.sendActionBar(player.trans( "kits.trident.messages.dive_launched", "charges" to remaining.toString() )) diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt index 4ec5f72..e91007e 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt @@ -10,6 +10,8 @@ import club.mcscrims.speedhg.kit.charge.ChargeState import club.mcscrims.speedhg.kit.impl.AnchorKit import club.mcscrims.speedhg.kit.impl.BlackPantherKit import club.mcscrims.speedhg.kit.impl.IceMageKit +import club.mcscrims.speedhg.kit.impl.NinjaKit +import club.mcscrims.speedhg.kit.impl.TridentKit import club.mcscrims.speedhg.kit.impl.VenomKit import club.mcscrims.speedhg.util.trans import net.kyori.adventure.text.Component @@ -26,6 +28,7 @@ import org.bukkit.event.EventPriority import org.bukkit.event.Listener import org.bukkit.event.block.BlockBreakEvent import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.entity.EntityDamageEvent import org.bukkit.event.entity.EntityDeathEvent import org.bukkit.event.entity.EntityExplodeEvent import org.bukkit.event.entity.PlayerDeathEvent @@ -67,6 +70,11 @@ class KitEventDispatcher( private val kitManager: KitManager, ) : Listener { + companion object { + /** Above this Y-level, knockback abilities are disabled to prevent skybasing. */ + const val MAX_KNOCKBACK_HEIGHT_Y = 100.0 + } + // ========================================================================= // Hit tracking + charge system + passive combat hook // ========================================================================= @@ -102,11 +110,19 @@ class KitEventDispatcher( sendChargeUpdateActionBar( attacker, currentHits, chargeData.hitsRequired ) } - // ── 2. Attacker passive hook ───────────────────────────────────────── + // ── 2. Ninja last-hit tracking ─────────────────────────────────────── + if ( attackerKit is NinjaKit && + attackerPlaystyle == Playstyle.AGGRESSIVE ) + { + attackerKit.lastHitEnemy[ attacker.uniqueId ] = + Pair( victim.uniqueId, System.currentTimeMillis() ) + } + + // ── 3. Attacker passive hook ───────────────────────────────────────── attackerKit.getPassiveAbility( attackerPlaystyle ) .onHitEnemy( attacker, victim, event ) - // ── 3. Victim passive hook ──────────────────────────────────────────── + // ── 4. Victim passive hook ──────────────────────────────────────────── kitManager.getSelectedKit( victim ) ?.getPassiveAbility(kitManager.getSelectedPlaystyle( victim )) ?.onHitByEnemy( victim, attacker, event ) @@ -129,7 +145,6 @@ class KitEventDispatcher( ) { val player = event.player - // Only main-hand right-clicks — ignore left-click and off-hand duplicates if ( event.hand != EquipmentSlot.HAND ) return if ( !event.action.isRightClick ) return if ( !isIngame() ) return @@ -144,6 +159,11 @@ class KitEventDispatcher( val itemInHand = player.inventory.itemInMainHand val active = kit.getActiveAbility( playstyle ) + // Allow throwable items (potions, ender pearls, etc.) to pass through + if ( itemInHand.type == Material.SPLASH_POTION || + itemInHand.type == Material.LINGERING_POTION || + itemInHand.type == Material.ENDER_PEARL ) return + if ( itemInHand.type != active.triggerMaterial ) return event.isCancelled = true // prevent vanilla block interaction on ability item @@ -392,10 +412,39 @@ class KitEventDispatcher( event.droppedExp = 0 } + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = false) + fun onLeapFallDamage( + event: EntityDamageEvent + ) { + if ( event.cause != EntityDamageEvent.DamageCause.FALL ) return + if ( !isIngame() ) return + + val player = event.entity as? Player ?: return + + when(val kit = kitManager.getSelectedKit( player )) + { + is TridentKit -> + { + if ( kit.noFallDamagePlayers.remove( player.uniqueId ) ) + event.isCancelled = true + } + is BlackPantherKit -> + { + if ( kit.noFallDamagePlayers.remove( player.uniqueId ) ) + event.isCancelled = true + } + else -> return + } + } + // ========================================================================= // Helpers // ========================================================================= + private fun isAboveKnockbackHeight( + player: Player + ): Boolean = player.location.y > MAX_KNOCKBACK_HEIGHT_Y + private fun changeGladiatorBlock( event: Cancellable, block: Block diff --git a/src/main/kotlin/club/mcscrims/speedhg/listener/ChatListener.kt b/src/main/kotlin/club/mcscrims/speedhg/listener/ChatListener.kt new file mode 100644 index 0000000..b311cc4 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/listener/ChatListener.kt @@ -0,0 +1,42 @@ +package club.mcscrims.speedhg.listener + +import club.mcscrims.speedhg.SpeedHG +import club.mcscrims.speedhg.scoreboard.ServerRankProvider +import io.papermc.paper.event.player.AsyncChatEvent +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.minimessage.MiniMessage +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener + +class ChatListener( + private val plugin: SpeedHG, + private val rankProvider: ServerRankProvider +) : Listener { + + private val mm = MiniMessage.miniMessage() + + @EventHandler + fun onAsyncChat( + event: AsyncChatEvent + ) { + val player = event.player + + val prefix = rankProvider.getRankPrefix( player ) + val nameColor = rankProvider.getRankColor( player ) + + event.renderer { source, _, message, _ -> + val coloredName = mm.deserialize( + "${nameColor}${source.name}" + ) + + Component.empty() + .append( prefix ) + .append( Component.space() ) + .append( coloredName ) + .append(mm.deserialize( ": " )) + .append(message.colorIfAbsent( NamedTextColor.GRAY )) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/listener/GameStateListener.kt b/src/main/kotlin/club/mcscrims/speedhg/listener/GameStateListener.kt index 4506bdf..072f240 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/listener/GameStateListener.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/listener/GameStateListener.kt @@ -5,7 +5,6 @@ import club.mcscrims.speedhg.game.GameState import club.mcscrims.speedhg.util.sendMsg import org.bukkit.Material import org.bukkit.Sound -import org.bukkit.attribute.Attribute import org.bukkit.entity.Player import org.bukkit.event.Event import org.bukkit.event.EventHandler @@ -157,17 +156,19 @@ class GameStateListener : Listener { val player = event.player if ( gameManager.currentState != GameState.INVINCIBILITY && - gameManager.currentState != GameState.INGAME ) + gameManager.currentState != GameState.INGAME ) { event.isCancelled = true player.playSound( player.location, Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f ) return } - val kitItems = plugin.kitManager.getSelectedKit( player )?.cachedItems?.get( player.uniqueId ) ?: return + val kitItems = plugin.kitManager.getSelectedKit( player ) + ?.cachedItems?.get( player.uniqueId ) ?: return val item = event.itemDrop.itemStack - if (kitItems.contains( item )) + val isKitItem = kitItems.any { kitItem -> kitItem.isSimilar( item ) } + if ( isKitItem ) { event.isCancelled = true player.playSound( player.location, Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f ) diff --git a/src/main/kotlin/club/mcscrims/speedhg/scoreboard/TablistManager.kt b/src/main/kotlin/club/mcscrims/speedhg/scoreboard/TablistManager.kt index b6648ec..af1a1bb 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/scoreboard/TablistManager.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/scoreboard/TablistManager.kt @@ -234,16 +234,15 @@ class TablistManager( team.prefix(rankProvider.getRankPrefix( player )) // ── playerListName: farbiger Spielername ─────────────────────────── - // Ersetzt den Standard-Anzeigenamen in der Namens-Spalte. - // Endergebnis: [PREFIX] [NAME] [SUFFIX] + // WICHTIG: KEIN hier. Das machen wir am Anfang des Suffixes! val nameColor = rankProvider.getRankColor( player ) - player.playerListName(mm.deserialize( "${nameColor}${player.name}" )) + player.playerListName(mm.deserialize( "${nameColor}${player.name}" )) // ── Suffix: SpeedHG-Rang (z. B. "[Gold II]") ────────────────────── team.suffix(buildSpeedHGRankSuffix( player )) // Spieler dem Team zuweisen - if (!team.hasEntry( player.name )) team.addEntry( player.name ) + if ( !team.hasEntry( player.name ) ) team.addEntry( player.name ) playerTeams[ player.uniqueId ] = newTeamName // Scoreboard dem Spieler zuweisen (notwendig damit Teams sichtbar sind) @@ -259,7 +258,9 @@ class TablistManager( val games = ( stats?.wins ?: 0 ) + ( stats?.losses ?: 0 ) val rankTag = Rank.getFormattedRankTag( score, games ) - mm.deserialize( " [${rankTag}]" ) + // Führendes stellt sicher, dass die Spielerfarbe nicht in den Suffix blutet + // und erzwingt einen Cut, den Bukkit/Paper als neues Suffix-Objekt erkennt. + mm.deserialize( " [${rankTag}]" ) } /** Entfernt das Scoreboard-Team des Spielers vollständig. */ diff --git a/src/main/resources/languages/en_US.yml b/src/main/resources/languages/en_US.yml index 153ef4c..5b85735 100644 --- a/src/main/resources/languages/en_US.yml +++ b/src/main/resources/languages/en_US.yml @@ -307,6 +307,7 @@ perks: kits: needed_hits: '⚡ Ability: / Hits' ability_charged: '⚡ ABILITY READY!' + height_restriction: '⚠ This ability cannot be used at high altitudes!' backup: name: 'Backup' @@ -624,8 +625,8 @@ kits: name: 'Spielo' lore: - ' ' - - 'AGGRESSIVE: Gambling at the push of a button' - - 'DEFENSIVE: Slot machine - no instant death' + - 'AGGRESSIVE: Gambling at the push of a button' + - 'DEFENSIVE: Slot machine - no instant death' items: automat: name: 'Slot Machine'