Add chat listener and fix ability height/fall logic

Add a ChatListener and register it to render chat with rank prefixes/colors. Enforce a maximum knockback altitude (MAX_KNOCKBACK_HEIGHT_Y) and block height-restricted ability use in Blitzcrank and BlackPanther; surface a localized height_restriction message. Add per-kit noFallDamage tracking for BlackPanther and Trident and cancel fall damage for flagged players in the KitEventDispatcher. Improve hitsRequired resolution: CustomGameSettings will treat a hardcoded 0 as an explicit cooldown-only value (never overridden) and the ActiveAbility getter now only accepts positive override values. Misc fixes: allow throwable items (potions/pearls) to bypass active-item handling, tighten ninja last-hit tracking, fix kit item drop detection in GameStateListener, tweak tablist prefix/suffix handling, and bump TheWorld ability cooldown from 20000ms to 25000ms. Also update language entries and minor formatting/cleanup.
This commit is contained in:
TDSTOS
2026-04-12 02:08:09 +02:00
parent 92260d90cb
commit 55a00ee15c
11 changed files with 179 additions and 27 deletions

View File

@@ -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()

View File

@@ -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,

View File

@@ -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.

View File

@@ -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<UUID, Long> = ConcurrentHashMap()
/** Players currently in a pounce — fall damage is suppressed on landing. */
internal val noFallDamagePlayers: MutableSet<UUID> = 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<Player>()
@@ -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
}
}

View File

@@ -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<Player>()

View File

@@ -62,6 +62,9 @@ class TridentKit : Kit() {
private val diveMonitors: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
private val lastSequenceTime: MutableMap<UUID, Long> = ConcurrentHashMap()
/** Players who have recently launched a dive and should not receive fall damage. */
internal val noFallDamagePlayers: MutableSet<UUID> = 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() ))

View File

@@ -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

View File

@@ -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}<reset>"
)
Component.empty()
.append( prefix )
.append( Component.space() )
.append( coloredName )
.append(mm.deserialize( "<dark_gray>: <gray>" ))
.append(message.colorIfAbsent( NamedTextColor.GRAY ))
}
}
}

View File

@@ -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
@@ -164,10 +163,12 @@ class GameStateListener : Listener {
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 )

View File

@@ -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 <reset> hier. Das <reset> machen wir am Anfang des Suffixes!
val nameColor = rankProvider.getRankColor( player )
player.playerListName(mm.deserialize( "${nameColor}${player.name}<reset>" ))
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( " <dark_gray>[<reset>${rankTag}<dark_gray>]</dark_gray>" )
// Führendes <reset> stellt sicher, dass die Spielerfarbe nicht in den Suffix blutet
// und erzwingt einen Cut, den Bukkit/Paper als neues Suffix-Objekt erkennt.
mm.deserialize( "<reset> <dark_gray>[<reset>${rankTag}<dark_gray>]</dark_gray>" )
}
/** Entfernt das Scoreboard-Team des Spielers vollständig. */

View File

@@ -307,6 +307,7 @@ perks:
kits:
needed_hits: '<gold>⚡ Ability: <white><current>/<required> Hits</white></gold>'
ability_charged: '<green><bold>⚡ ABILITY READY!</bold></green>'
height_restriction: '<red>⚠ This ability cannot be used at high altitudes!'
backup:
name: '<gradient:gold:#ff841f><bold>Backup</bold></gradient>'
@@ -624,8 +625,8 @@ kits:
name: '<gradient:gold:yellow><bold>Spielo</bold></gradient>'
lore:
- ' '
- '<gray>AGGRESSIVE: Gambling at the push of a button</gray>'
- '<gray>DEFENSIVE: Slot machine - no instant death</gray>'
- 'AGGRESSIVE: Gambling at the push of a button'
- 'DEFENSIVE: Slot machine - no instant death'
items:
automat:
name: '<gold><bold>Slot Machine</bold></gold>'