Add Blitzcrank, Ninja and Trident kits

Add three new kits (Blitzcrank, Ninja, Trident) and register them in SpeedHG. Each kit includes full ability implementations (hook/stun/ult for Blitzcrank, teleport/smoke for Ninja, dive/parry for Trident) with their internal cooldowns, tasks and item handling. Update AbilityUtils.createBeam signature/logic (now accepts Player and improved beam stepping/hit detection) and adjust VenomKit to pass the player to createBeam. Extend en_US.yml with translations for the new kits and fix a few markup strings.
This commit is contained in:
TDSTOS
2026-04-11 05:36:31 +02:00
parent 13f6ce5638
commit f6eb654d47
7 changed files with 1066 additions and 15 deletions

View File

@@ -217,13 +217,16 @@ class SpeedHG : JavaPlugin() {
kitManager.registerKit( ArmorerKit() )
kitManager.registerKit( BackupKit() )
kitManager.registerKit( BlackPantherKit() )
kitManager.registerKit( BlitzcrankKit() )
kitManager.registerKit( GladiatorKit() )
kitManager.registerKit( GoblinKit() )
kitManager.registerKit( IceMageKit() )
kitManager.registerKit( NinjaKit() )
kitManager.registerKit( PuppetKit() )
kitManager.registerKit( RattlesnakeKit() )
kitManager.registerKit( TeslaKit() )
kitManager.registerKit( TheWorldKit() )
kitManager.registerKit( TridentKit() )
kitManager.registerKit( VenomKit() )
kitManager.registerKit( VoodooKit() )
}

View File

@@ -0,0 +1,368 @@
package club.mcscrims.speedhg.kit.impl
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.kit.Kit
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.util.ItemBuilder
import club.mcscrims.speedhg.util.trans
import net.kyori.adventure.text.Component
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.NamespacedKey
import org.bukkit.Particle
import org.bukkit.Sound
import org.bukkit.entity.Player
import org.bukkit.event.player.PlayerInteractEvent
import org.bukkit.inventory.ItemStack
import org.bukkit.persistence.PersistentDataType
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
import org.bukkit.scheduler.BukkitRunnable
import org.bukkit.scheduler.BukkitTask
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.cos
import kotlin.math.sin
/**
* ## BlitzcrankKit
*
* | Playstyle | Aktive Fähigkeit |
* |-------------|---------------------------------------------------------------|
* | AGGRESSIVE | **Hook** zieht ersten Feind in der Schusslinie heran |
* | DEFENSIVE | **Stun** friert alle nahen Feinde für 3 s ein |
* | Beide | **Ult** expandierende Schockwelle + AoE-Schaden |
*
* ### Hook synchroner Raycast
* 0,4-Block-Schritte von `eyeLocation` entlang `eyeLocation.direction`.
* Erster Feind getroffen → Velocity-Pull Richtung Caster. Alle Partikel werden
* synchron im selben Tick gezeichnet.
*
* ### Stun Freeze-Mechanismus
* Slowness 127 + Mining Fatigue 127 für [STUN_DURATION_TICKS] Ticks.
* Zusätzlich setzt ein BukkitTask die Velocity aller gestunnten Spieler auf 0.
*
* ### Ult passive onInteract als Auslöser
* Das Ult-Item (BLAZE_POWDER) besitzt einen PDC-Tag ([ultItemKey]).
* `KitEventDispatcher.onInteract` ruft **zuerst** `passive.onInteract` auf,
* dann erst den triggerMaterial-Check. [UltPassive.onInteract] fängt das
* BLAZE_POWDER-Rechtsklick-Event ab und cancelt es, bevor der Dispatcher
* etwas unternimmt → kein Dispatcher-Umbau notwendig.
*/
class BlitzcrankKit : Kit() {
private val plugin get() = SpeedHG.instance
override val id: String
get() = "blitzcrank"
override val displayName: Component
get() = plugin.languageManager.getDefaultComponent( "kits.blitzcrank.name", mapOf() )
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList( "kits.blitzcrank.lore" )
override val icon
get() = Material.PISTON
/** PDC-Key für das Ult-Item (BLAZE_POWDER), damit es sicher identifiziert wird. */
val ultItemKey: NamespacedKey = NamespacedKey( plugin, "blitzcrank_ult_item" )
/** Laufende Stun-Freeze-Tasks pro gestunntem Spieler. */
private val stunTasks: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
/** Ult-Cooldown: UUID des Casters → letzter Auslöse-Timestamp. */
private val ultCooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
companion object {
const val HOOK_RANGE = 10.0 // Blöcke
const val HOOK_PULL_STRENGTH = 2.7 // Velocity-Multiplikator
const val STUN_RADIUS = 5.0 // Blöcke
const val STUN_DURATION_TICKS = 60 // 3 Sekunden
const val ULT_RADIUS = 6.0 // Blöcke
const val ULT_DAMAGE = 5.0 // 2,5 Herzen
const val ULT_COOLDOWN_MS = 30_000L
}
private val aggressiveActive = HookActive()
private val defensiveActive = StunActive()
private val aggressivePassive = UltPassive( Playstyle.AGGRESSIVE )
private val defensivePassive = UltPassive( Playstyle.DEFENSIVE )
override fun getActiveAbility(
playstyle: Playstyle
) = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressiveActive
Playstyle.DEFENSIVE -> defensiveActive
}
override fun getPassiveAbility(
playstyle: Playstyle
) = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressivePassive
Playstyle.DEFENSIVE -> defensivePassive
}
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
override fun giveItems(
player: Player,
playstyle: Playstyle
) {
val mainItem = when (playstyle) {
Playstyle.AGGRESSIVE -> ItemBuilder( Material.FISHING_ROD )
.name( aggressiveActive.name )
.lore(listOf( aggressiveActive.description ))
.build()
Playstyle.DEFENSIVE -> ItemBuilder( Material.PISTON )
.name( defensiveActive.name )
.lore(listOf( defensiveActive.description ))
.build()
}
val ultItem = ItemBuilder( Material.BLAZE_POWDER )
.name(plugin.languageManager.getDefaultRawMessage( "kits.blitzcrank.items.ult.name" ))
.lore(listOf(plugin.languageManager.getDefaultRawMessage( "kits.blitzcrank.items.ult.description" )))
.pdc( ultItemKey, PersistentDataType.BYTE, 1 )
.build()
cachedItems[ player.uniqueId ] = listOf( mainItem, ultItem )
player.inventory.addItem( mainItem, ultItem )
}
override fun onRemove(
player: Player
) {
stunTasks.remove( player.uniqueId )?.cancel()
ultCooldowns.remove( player.uniqueId )
cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) }
}
// =========================================================================
// Ult Schockwelle + AoE (beide Playstyles via UltPassive.onInteract)
// =========================================================================
private fun fireUlt(
caster: Player
) {
val now = System.currentTimeMillis()
val lastUlt = ultCooldowns[ caster.uniqueId ] ?: 0L
if ( now - lastUlt < ULT_COOLDOWN_MS )
{
val secLeft = ( ULT_COOLDOWN_MS - ( now - lastUlt )) / 1000
caster.sendActionBar(caster.trans( "kits.blitzcrank.messages.ult_cooldown", "time" to secLeft.toString() ))
return
}
val targets = caster.world
.getNearbyEntities( caster.location, ULT_RADIUS, ULT_RADIUS, ULT_RADIUS )
.filterIsInstance<Player>()
.filter { it != caster && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
if ( targets.isEmpty() )
{
caster.sendActionBar(caster.trans( "kits.blitzcrank.messages.ult_no_targets" ))
return
}
object : BukkitRunnable() {
var r = 0.5
override fun run()
{
if ( r > ULT_RADIUS + 1.0 ) { cancel(); return }
val steps = ( 2 * Math.PI * r * 5 ).toInt().coerceAtLeast( 8 )
repeat( steps ) { i ->
val angle = 2.0 * Math.PI * i / steps
caster.world.spawnParticle(
Particle.ELECTRIC_SPARK,
caster.location.clone().add(cos( angle ) * r, 1.0, sin( angle ) * r ),
1, 0.0, 0.0, 0.0, 0.0
)
}
r += 0.65
}
}.runTaskTimer( plugin, 0L, 1L )
targets.forEach { target ->
target.damage( ULT_DAMAGE, caster )
target.velocity = target.location.toVector()
.subtract( caster.location.toVector() )
.normalize()
.multiply( 1.6 )
.setY( 0.5 )
}
caster.world.playSound( caster.location, Sound.ENTITY_GENERIC_EXPLODE, 1f, 1.5f )
caster.world.playSound( caster.location, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 0.8f, 1.8f )
caster.sendActionBar(caster.trans( "kits.blitzcrank.messages.ult_fired", "count" to targets.size.toString() ))
ultCooldowns[ caster.uniqueId ] = now
}
// =========================================================================
// AGGRESSIVE active Hook (synchroner Raycast)
// =========================================================================
private inner class HookActive : ActiveAbility(Playstyle.AGGRESSIVE) {
private val plugin get() = SpeedHG.instance
override val kitId = "blitzcrank"
override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.blitzcrank.items.hook.name")
override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.blitzcrank.items.hook.description")
override val hardcodedHitsRequired = 15
override val triggerMaterial = Material.FISHING_ROD
override fun execute(player: Player): AbilityResult
{
val eyeLoc = player.eyeLocation
val dir = eyeLoc.direction.normalize()
var hookTarget: Player? = null
var dist = 0.4
// Synchroner Scan: trivial schnell (max ~25 Iterationen)
while (dist <= HOOK_RANGE && hookTarget == null) {
val point = eyeLoc.clone().add(dir.clone().multiply(dist))
// Block im Weg → Hook stoppt hier
if (point.block.type.isSolid) break
// Partikel-Trail entlang des Strahls
player.world.spawnParticle(Particle.ELECTRIC_SPARK, point, 1, 0.0, 0.0, 0.0, 0.0)
hookTarget = point.world
?.getNearbyEntities(point, 0.6, 0.6, 0.6)
?.filterIsInstance<Player>()
?.filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) }
?.minByOrNull { it.location.distanceSquared(point) }
dist += 0.4
}
if (hookTarget == null) {
// Kein Treffer Funken am Strahlende
val endPt = eyeLoc.clone().add(dir.multiply(dist.coerceAtMost(HOOK_RANGE)))
player.world.spawnParticle(Particle.ELECTRIC_SPARK, endPt, 10, 0.3, 0.3, 0.3, 0.06)
return AbilityResult.ConditionNotMet("Kein Ziel in Reichweite!")
}
val target = hookTarget
// Pull: Velocity in Richtung Caster
target.velocity = player.location.toVector()
.subtract(target.location.toVector())
.normalize()
.multiply(HOOK_PULL_STRENGTH)
.setY(0.65)
target.world.spawnParticle(Particle.ELECTRIC_SPARK,
target.location.clone().add(0.0, 1.0, 0.0), 22, 0.4, 0.4, 0.4, 0.14)
target.world.playSound(target.location, Sound.ENTITY_IRON_GOLEM_HURT, 0.9f, 1.6f)
target.sendActionBar(target.trans("kits.blitzcrank.messages.hooked"))
player.playSound(player.location, Sound.ENTITY_FISHING_BOBBER_RETRIEVE, 1f, 0.4f)
player.sendActionBar(player.trans("kits.blitzcrank.messages.hook_hit"))
return AbilityResult.Success
}
}
// =========================================================================
// DEFENSIVE active Stun (AoE-Freeze)
// =========================================================================
private inner class StunActive : ActiveAbility(Playstyle.DEFENSIVE) {
private val plugin get() = SpeedHG.instance
override val kitId = "blitzcrank"
override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.blitzcrank.items.stun.name")
override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.blitzcrank.items.stun.description")
override val hardcodedHitsRequired = 15
override val triggerMaterial = Material.PISTON
override fun execute(player: Player): AbilityResult {
val targets = player.world
.getNearbyEntities(player.location, STUN_RADIUS, STUN_RADIUS, STUN_RADIUS)
.filterIsInstance<Player>()
.filter { it != player && plugin.gameManager.alivePlayers.contains(it.uniqueId) }
if (targets.isEmpty())
return AbilityResult.ConditionNotMet("Keine Feinde in ${STUN_RADIUS.toInt()} Blöcken!")
targets.forEach { target ->
// Potion-Effekte für maximales Einfrieren (Amplifier 127 = sofortiger Stopp)
target.addPotionEffect(
PotionEffect(PotionEffectType.SLOWNESS, STUN_DURATION_TICKS, 127, false, false, true)
)
target.addPotionEffect(
PotionEffect(PotionEffectType.MINING_FATIGUE, STUN_DURATION_TICKS, 127, false, false, false)
)
// Velocity-Reset-Task: verhindert Springen und Rutschen
var stunTick = 0
val task = Bukkit.getScheduler().runTaskTimer(plugin, { ->
stunTick++
if (stunTick >= STUN_DURATION_TICKS || !target.isOnline ||
!plugin.gameManager.alivePlayers.contains(target.uniqueId)) {
stunTasks.remove(target.uniqueId)?.cancel()
return@runTaskTimer
}
val v = target.velocity
target.velocity = v.setX(0.0).setZ(0.0).let { if (it.y > 0.0) it.setY(0.0) else it }
}, 0L, 1L)
stunTasks[target.uniqueId] = task
target.world.spawnParticle(Particle.ELECTRIC_SPARK,
target.location.clone().add(0.0, 1.5, 0.0), 25, 0.3, 0.5, 0.3, 0.14)
target.sendActionBar(target.trans("kits.blitzcrank.messages.stunned"))
}
player.world.playSound(player.location, Sound.ENTITY_LIGHTNING_BOLT_IMPACT, 1f, 0.7f)
player.world.spawnParticle(Particle.ELECTRIC_SPARK,
player.location.clone().add(0.0, 1.0, 0.0), 35, 2.0, 0.5, 2.0, 0.14)
player.sendActionBar(player.trans("kits.blitzcrank.messages.stun_cast",
"count" to targets.size.toString()))
return AbilityResult.Success
}
}
// =========================================================================
// Shared Ult-Passive fängt BLAZE_POWDER-Rechtsklick via onInteract ab
// =========================================================================
inner class UltPassive(playstyle: Playstyle) : PassiveAbility(playstyle) {
private val plugin get() = SpeedHG.instance
override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.blitzcrank.passive.name")
override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.blitzcrank.passive.description")
/**
* Wird vom KitEventDispatcher **vor** dem triggerMaterial-Check aufgerufen.
* Prüft PDC-Tag → falls Ult-Item: Event canceln + Ult feuern.
*/
override fun onInteract(player: Player, event: PlayerInteractEvent) {
if (!event.action.isRightClick) return
val pdc = player.inventory.itemInMainHand.itemMeta
?.persistentDataContainer ?: return
if (!pdc.has(ultItemKey, PersistentDataType.BYTE)) return
event.isCancelled = true // Vanilla-Interaktion (Feuer-Charge) unterbinden
fireUlt(player)
}
}
}

View File

@@ -0,0 +1,271 @@
package club.mcscrims.speedhg.kit.impl
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.kit.Kit
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.util.ItemBuilder
import club.mcscrims.speedhg.util.trans
import net.kyori.adventure.text.Component
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.Particle
import org.bukkit.Sound
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
import org.bukkit.scheduler.BukkitTask
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.cos
import kotlin.math.sin
/**
* ## NinjaKit
*
* | Playstyle | Aktive Fähigkeit | Passive |
* |-------------|------------------------------------------------------------------|-------------------------------------|
* | AGGRESSIVE | Sneak → teleportiert hinter den letzten Gegner (10-s-Fenster) | - |
* | DEFENSIVE | Smoke-Aura (Blindness I + Slow I) | - |
*
* ### Teleport-Mechanismus
* `onToggleSneak` wird vom [KitEventDispatcher] aufgerufen. Er prüft das
* [lastHitEnemy]-Fenster (10 s) und berechnet eine Position 1,8 Blöcke
* hinter dem Feind (entgegen seiner Blickrichtung).
*
* ### Smoke-Mechanismus
* Ein BukkitTask (10 Ticks) spawnt einen Partikelring mit [SMOKE_RADIUS] Blöcken
* Radius. Jeder Feind im Ring erhält Blindness I + Slowness I (30 Ticks),
* die alle 0,5 s erneuert werden, solange er im Rauch bleibt.
*/
class NinjaKit : Kit() {
private val plugin get() = SpeedHG.instance
override val id: String
get() = "ninja"
override val displayName: Component
get() = plugin.languageManager.getDefaultComponent( "kits.ninja.name", mapOf() )
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList( "kits.ninja.lore" )
override val icon: Material
get() = Material.FEATHER
/** ninjaUUID → (enemyUUID, System.currentTimeMillis() des letzten Treffers) */
internal val lastHitEnemy: MutableMap<UUID, Pair<UUID, Long>> = ConcurrentHashMap()
private val smokeTasks: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
private val teleportCooldowns: MutableMap<UUID, Long> = ConcurrentHashMap()
companion object {
const val HIT_WINDOW_MS = 10_000L // 10s - Gültigkeit des Teleport-Ziels
const val SMOKE_RADIUS = 3.0 // Blöcke
const val SMOKE_MAX_DURATION = 10_000L // 10s
const val TELEPORT_COOLDOWN_MS = 12_000L // 12s zwischen Teleports
}
// ── Gecachte Instanzen ────────────────────────────────────────────────────
private val aggressiveActive = NoActive( Playstyle.AGGRESSIVE )
private val defensiveActive = DefensiveActive()
private val aggressivePassive = NoPassive( Playstyle.AGGRESSIVE )
private val defensivePassive = NoPassive( Playstyle.DEFENSIVE )
override fun getActiveAbility(
playstyle: Playstyle
): ActiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressiveActive
Playstyle.DEFENSIVE -> defensiveActive
}
override fun getPassiveAbility(
playstyle: Playstyle
): PassiveAbility = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressivePassive
Playstyle.DEFENSIVE -> defensivePassive
}
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
override fun giveItems(
player: Player,
playstyle: Playstyle
) {
if ( playstyle != Playstyle.DEFENSIVE )
return
val item = ItemBuilder( Material.FEATHER )
.name( defensiveActive.name )
.lore(listOf( defensiveActive.description ))
.build()
cachedItems[ player.uniqueId ] = listOf( item )
player.inventory.addItem( item )
}
override fun onRemove(
player: Player
) {
lastHitEnemy.remove( player.uniqueId )
smokeTasks.remove( player.uniqueId )?.cancel()
teleportCooldowns.remove( player.uniqueId )
cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) }
}
// =========================================================================
// Sneak → Teleport (nur AGGRESSIVE, via KitEventDispatcher)
// =========================================================================
override fun onToggleSneak(
player: Player,
isSneaking: Boolean
) {
if ( !isSneaking ) return
if (plugin.kitManager.getSelectedPlaystyle( player ) != Playstyle.AGGRESSIVE ) return
val now = System.currentTimeMillis()
val lastUse = teleportCooldowns[ player.uniqueId ] ?: 0L
if ( now - lastUse < TELEPORT_COOLDOWN_MS )
{
val secLeft = ( TELEPORT_COOLDOWN_MS - ( now - lastUse )) / 1000
player.sendActionBar(player.trans( "kits.ninja.messages.cooldown", "time" to secLeft.toString() ))
return
}
val ( enemyUUID, hitTime ) = lastHitEnemy[ player.uniqueId ] ?: run {
player.sendActionBar(player.trans( "kits.ninja.messages.no_target" ))
return
}
if ( now - hitTime > HIT_WINDOW_MS )
{
lastHitEnemy.remove( player.uniqueId )
player.sendActionBar(player.trans( "kits.ninja.messages.target_expired" ))
return
}
val enemy = Bukkit.getPlayer( enemyUUID ) ?: return
if (!plugin.gameManager.alivePlayers.contains( enemy.uniqueId )) return
performTeleport( player, enemy )
teleportCooldowns[ player.uniqueId ] = now
}
private fun performTeleport(
player: Player,
enemy: Player
) {
val enemyDir = enemy.location.direction.normalize()
var dest = enemy.location.clone()
.subtract(enemyDir.multiply( 1.8 ))
.add( 0.0, 0.1, 0.0 )
if ( !dest.block.type.isAir ) dest = dest.add( 0.0, 1.0, 0.0 )
dest.yaw = enemy.location.yaw
dest.pitch = 0f
player.world.spawnParticle(
Particle.LARGE_SMOKE,
player.location.clone().add( 0.0, 1.0, 0.0 ),
25, 0.3, 0.5, 0.3, 0.05
)
player.teleport( dest )
player.world.spawnParticle(
Particle.LARGE_SMOKE,
dest.clone().add( 0.0, 1.0, 0.0 ),
25, 0.3, 0.5, 0.3, 0.05
)
player.playSound( player.location, Sound.ENTITY_ENDERMAN_TELEPORT, 0.7f, 1.8f )
enemy.playSound( enemy.location, Sound.ENTITY_ENDERMAN_TELEPORT, 0.4f, 0.7f )
player.sendActionBar(player.trans( "kits.ninja.messages.teleported" ))
}
private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) {
private val plugin get() = SpeedHG.instance
override val kitId: String
get() = "ninja"
override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.ninja.items.smoke.name" )
override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.ninja.items.smoke.description" )
override val triggerMaterial: Material
get() = Material.FEATHER
override val hardcodedHitsRequired: Int
get() = 15
override fun execute(
player: Player
): AbilityResult
{
smokeTasks.remove( player.uniqueId )?.cancel()
val task = Bukkit.getScheduler().runTaskTimer( plugin, { ->
if ( !player.isOnline || !plugin.gameManager.alivePlayers.contains( player.uniqueId ))
{
smokeTasks.remove( player.uniqueId )?.cancel()
return@runTaskTimer
}
val center = player.location
for ( i in 0 until 10 )
{
val angle = i * ( 2.0 * Math.PI / 10.0 )
center.world.spawnParticle(
Particle.CAMPFIRE_COSY_SMOKE,
center.clone().add(cos( angle ) * SMOKE_RADIUS, 0.8, sin( angle ) * SMOKE_RADIUS),
1, 0.05, 0.12, 0.05, 0.004
)
}
center.world
.getNearbyEntities( center, SMOKE_RADIUS, 2.0, SMOKE_RADIUS )
.filterIsInstance<Player>()
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
.forEach { enemy ->
enemy.addPotionEffect(PotionEffect(
PotionEffectType.BLINDNESS, 30, 0, false, false, true
))
enemy.addPotionEffect(PotionEffect(
PotionEffectType.SLOWNESS, 30, 0, false, false, true
))
}
}, 0L, 10L )
smokeTasks[ player.uniqueId ] = task
Bukkit.getScheduler().runTaskLater( plugin, { ->
smokeTasks.remove( player.uniqueId )?.cancel()
}, SMOKE_MAX_DURATION * 20L )
return AbilityResult.Success
}
}
private class NoActive( playstyle: Playstyle ) : ActiveAbility( playstyle ) {
override val kitId = "ninja"
override val name = "None"
override val description = "None"
override val hardcodedHitsRequired = 0
override val triggerMaterial = Material.BARRIER
override fun execute( player: Player ) = AbilityResult.Success
}
private class NoPassive( playstyle: Playstyle ) : PassiveAbility( playstyle ) {
override val name = "None"
override val description = "None"
}
}

View File

@@ -0,0 +1,339 @@
package club.mcscrims.speedhg.kit.impl
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.kit.Kit
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.util.ItemBuilder
import club.mcscrims.speedhg.util.trans
import net.kyori.adventure.text.Component
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.Particle
import org.bukkit.Sound
import org.bukkit.enchantments.Enchantment
import org.bukkit.entity.Player
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.inventory.ItemStack
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
import org.bukkit.scheduler.BukkitTask
import java.util.Random
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
/**
* ## TridentKit
*
* | Playstyle | Fähigkeit |
* |-------------|----------------------------------------------------------------------------|
* | AGGRESSIVE | **Dive**: 3 Charges Hochsprung, bei Landung schlägt Blitz ein |
* | DEFENSIVE | **Parry**: 20 % Chance Angreifer abprallen + Slowness I (2 s) |
*
* ### Dive-Mechanismus
* `hitsRequired = 0` → Fähigkeit ist immer READY; interne [diveCharges] verwalten
* die 3 Sprünge einer Sequenz. Coodown [SEQUENCE_COOLDOWN_MS] gilt nur zwischen
* vollständigen Sequenzen (wenn alle Charges verbraucht wurden).
*
* Jeder Charge-Verbrauch startet einen 1-Tick-Monitor:
* 1. Warte auf Velocity-Wechsel (aufwärts → abwärts)
* 2. Sobald Block unterhalb solid → [triggerLightningStrike]
*
* ### Parry-Mechanismus
* [onHitByEnemy] mit 20 % Chance + Dreizack-Check (Haupt- oder Offhand).
*/
class TridentKit : Kit() {
private val plugin get() = SpeedHG.instance
override val id: String
get() = "trident"
override val displayName: Component
get() = plugin.languageManager.getDefaultComponent( "kits.trident.name", mapOf() )
override val lore: List<String>
get() = plugin.languageManager.getDefaultRawMessageList( "kits.trident.lore" )
override val icon: Material
get() = Material.TRIDENT
/** Verbleibende Dive-Charges: 0 = neue Sequenz erforderlich. */
internal val diveCharges: MutableMap<UUID, Int> = ConcurrentHashMap()
private val diveMonitors: MutableMap<UUID, BukkitTask> = ConcurrentHashMap()
private val lastSequenceTime: MutableMap<UUID, Long> = ConcurrentHashMap()
companion object {
const val MAX_DIVE_CHARGES = 3
const val SEQUENCE_COOLDOWN_MS = 25_000L // Cooldown zwischen vollst. Sequenzen
const val LIGHTNING_RADIUS = 3.5 // Blöcke um den Einschlagpunkt
const val LIGHTNING_DAMAGE = 4.0 // 2 Herzen
const val PARRY_CHANCE = 0.20 // 20 %
const val PARRY_SLOWNESS_TICKS = 40 // 2 Sekunden
}
// ── Gecachte Instanzen ────────────────────────────────────────────────────
private val aggressiveActive = DiveActive()
private val defensiveActive = NoActive( Playstyle.DEFENSIVE )
private val aggressivePassive = NoPassive( Playstyle.AGGRESSIVE )
private val defensivePassive = ParryPassive()
override fun getActiveAbility(
playstyle: Playstyle
) = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressiveActive
Playstyle.DEFENSIVE -> defensiveActive
}
override fun getPassiveAbility(
playstyle: Playstyle
) = when( playstyle )
{
Playstyle.AGGRESSIVE -> aggressivePassive
Playstyle.DEFENSIVE -> defensivePassive
}
override val cachedItems = ConcurrentHashMap<UUID, List<ItemStack>>()
override fun giveItems(
player: Player,
playstyle: Playstyle
) {
val nameKey = if ( playstyle == Playstyle.AGGRESSIVE )
"kits.trident.items.trident.aggressive.name"
else
"kits.trident.item.trident.defensive.name"
val trident = ItemBuilder( Material.TRIDENT )
.name(plugin.languageManager.getDefaultRawMessage( nameKey ))
.lore(listOf(
plugin.languageManager.getDefaultRawMessage(
if ( playstyle == Playstyle.AGGRESSIVE )
"kits.trident.items.trident.aggressive.description"
else
"kits.trident.items.trident.defensive.description"
)
))
.enchant( Enchantment.LOYALTY, 3 )
.unbreakable( true )
.build()
cachedItems[ player.uniqueId ] = listOf( trident )
player.inventory.addItem( trident )
}
override fun onRemove(
player: Player
) {
diveCharges.remove( player.uniqueId )
diveMonitors.remove( player.uniqueId )?.cancel()
lastSequenceTime.remove( player.uniqueId )
cachedItems.remove( player.uniqueId )?.forEach { player.inventory.remove( it ) }
}
// =========================================================================
// Dive: Landungs-Monitor
// =========================================================================
private fun startDiveMonitor(
player: Player
) {
diveMonitors.remove( player.uniqueId )?.cancel()
var wasAscending = true
var elapsed = 0
val task = Bukkit.getScheduler().runTaskTimer( plugin, { ->
elapsed++
// Safety-Timeout: 10 Sekunden
if ( elapsed > 200 || !player.isOnline ||
!plugin.gameManager.alivePlayers.contains( player.uniqueId ))
{
diveMonitors.remove( player.uniqueId )?.cancel()
return@runTaskTimer
}
val velY = player.velocity.y
if ( wasAscending && velY < -0.15 )
{
wasAscending = false
player.world.spawnParticle(
Particle.ELECTRIC_SPARK,
player.location.clone().add( 0.0, 1.0, 0.0 ),
8, 0.2, 0.2, 0.2, 0.1
)
}
if ( !wasAscending )
{
val blockBelow = player.location.clone().subtract( 0.0, 0.15, 0.0 ).block
if ( blockBelow.type.isSolid )
{
triggerLightningStrike( player )
diveMonitors.remove( player.uniqueId )?.cancel()
}
}
}, 4L, 1L ) // 4 Ticks Anlauf (verhindert Sofort-Trigger auf dem Boden)
diveMonitors[ player.uniqueId ] = task
}
private fun triggerLightningStrike(
player: Player
) {
val loc = player.location
val world = loc.world ?: return
world.strikeLightningEffect( loc )
world.playSound( loc, Sound.ENTITY_LIGHTNING_BOLT_THUNDER, 1f, 0.75f )
world.spawnParticle( Particle.ELECTRIC_SPARK, loc, 45, 1.2, 0.5, 1.2, 0.2 )
world.spawnParticle( Particle.EXPLOSION, loc, 3, 0.4, 0.2, 0.4, 0.0 )
world.getNearbyEntities( loc, LIGHTNING_RADIUS, LIGHTNING_RADIUS, LIGHTNING_RADIUS )
.filterIsInstance<Player>()
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
.forEach { enemy ->
enemy.damage( LIGHTNING_DAMAGE, player )
enemy.addPotionEffect(PotionEffect(
PotionEffectType.SLOWNESS, 40, 0, false, false, true
))
}
val remaining = diveCharges.getOrDefault( player.uniqueId, 0 )
val msgKey = if ( remaining > 0 ) "kits.trident.messages.charges_left"
else "kits.trident.messages.sequence_done"
player.sendActionBar(player.trans( msgKey, "charges" to remaining.toString() ))
}
// =========================================================================
// AGGRESSIVE active Dive-Charges
// =========================================================================
private inner class DiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) {
private val plugin get() = SpeedHG.instance
override val kitId: String
get() = "trident"
override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.trident.items.trident.aggressive.name" )
override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.trident.items.trident.aggressive.description" )
override val triggerMaterial: Material
get() = Material.TRIDENT
override val hardcodedHitsRequired: Int
get() = 0
override fun execute(
player: Player
): AbilityResult
{
val now = System.currentTimeMillis()
val charges = diveCharges.getOrDefault( player.uniqueId, 0 )
if ( charges <= 0 )
{
val lastSeq = lastSequenceTime[ player.uniqueId ] ?: 0L
if ( now - lastSeq < SEQUENCE_COOLDOWN_MS )
{
val secLeft = ( SEQUENCE_COOLDOWN_MS - ( now - lastSeq )) / 1000
return AbilityResult.ConditionNotMet("Cooldown: ${secLeft}s")
}
lastSequenceTime[ player.uniqueId ] = now
diveCharges[ player.uniqueId ] = MAX_DIVE_CHARGES - 1
}
else diveCharges[ player.uniqueId ] = charges - 1
player.velocity = player.velocity.clone().setY( 1.38 )
val remaining = diveCharges.getOrDefault( player.uniqueId, 0 )
player.sendActionBar(player.trans( "kits.trident.messages.dive_launched", "charges" to remaining.toString() ))
player.world.spawnParticle(
Particle.ELECTRIC_SPARK,
player.location.clone().add( 0.0, 0.5, 0.0 ),
15, 0.3, 0.2, 0.3, 0.12
)
player.playSound(
player.location,
Sound.ENTITY_LIGHTNING_BOLT_IMPACT,
0.7f, 1.6f
)
startDiveMonitor( player )
return AbilityResult.Success
}
}
// =========================================================================
// DEFENSIVE passive Parry (20 %)
// =========================================================================
private inner class ParryPassive : PassiveAbility( Playstyle.DEFENSIVE ) {
private val plugin get() = SpeedHG.instance
private val rng = Random()
override val name: String
get() = plugin.languageManager.getDefaultRawMessage("kits.trident.passive.defensive.name")
override val description: String
get() = plugin.languageManager.getDefaultRawMessage("kits.trident.passive.defensive.description")
override fun onHitByEnemy(
victim: Player,
attacker: Player,
event: EntityDamageByEntityEvent
) {
if ( rng.nextDouble() >= PARRY_CHANCE ) return
val mainType = victim.inventory.itemInMainHand.type
val offType = victim.inventory.itemInOffHand.type
if ( mainType != Material.TRIDENT && offType != Material.TRIDENT ) return
attacker.velocity = attacker.location.toVector()
.subtract( victim.location.toVector() )
.normalize()
.multiply( 1.7 )
.setY( 0.45 )
attacker.addPotionEffect(PotionEffect(
PotionEffectType.SLOWNESS, PARRY_SLOWNESS_TICKS, 0, false, false, true
))
victim.world.spawnParticle(
Particle.SWEEP_ATTACK,
victim.location.clone().add( 0.0, 1.0, 0.0 ),
6, 0.3, 0.3, 0.3, 0.0
)
victim.world.playSound( victim.location, Sound.ITEM_SHIELD_BLOCK, 1f, 0.65f )
victim.world.playSound( victim.location, Sound.ENTITY_LIGHTNING_BOLT_IMPACT, 0.3f, 1.9f )
victim.sendActionBar(victim.trans( "kits.trident.messages.parry_success" ))
attacker.sendActionBar(attacker.trans( "kits.trident.messages.parried_by_victim" ))
}
}
// ─── Stubs ────────────────────────────────────────────────────────────────
private class NoActive(playstyle: Playstyle) : ActiveAbility(playstyle) {
override val kitId = "trident"
override val name = "None"
override val description = "None"
override val hardcodedHitsRequired = 0
override val triggerMaterial = Material.BARRIER
override fun execute(player: Player) = AbilityResult.Success
}
private class NoPassive(playstyle: Playstyle) : PassiveAbility(playstyle) {
override val name = "None"
override val description = "None"
}
}

View File

@@ -152,6 +152,7 @@ class VenomKit : Kit() {
player.playSound( player.location, Sound.ENTITY_BLAZE_SHOOT, 1f, 0.8f )
AbilityUtils.createBeam(
player,
player.location,
player.eyeLocation.toVector(),
Particle.DRAGON_BREATH,

View File

@@ -15,6 +15,7 @@ object AbilityUtils {
private val plugin = SpeedHG.instance
fun createBeam(
player: Player,
startLocation: Location,
direction: Vector,
particle: Particle,
@@ -22,37 +23,36 @@ object AbilityUtils {
step: Double,
onHit: (Player) -> Unit
) {
val normalizedDirection = direction.normalize()
val stepVector = direction.clone().normalize().multiply( step )
val currentLocation = startLocation.clone().add(stepVector.clone().normalize().multiply( 1.0 ))
object : BukkitRunnable()
{
object : BukkitRunnable() {
var traveledDistance = 0.0
var currentLocation = startLocation.clone()
override fun run()
{
if ( traveledDistance >= range)
if ( traveledDistance >= range )
{
this.cancel()
return
}
currentLocation.world.spawnParticle( particle, currentLocation, 5, 0.0, 0.0, 0.0, 0.0 )
currentLocation.world.spawnParticle( particle, currentLocation, 3, 0.0, 0.0, 0.0, 0.0 )
val nearestPlayer = currentLocation.world.getNearbyEntities( currentLocation, 0.5, 0.5, 0.5 )
.filterIsInstance<Player>().minByOrNull { it.location.distance( currentLocation ) }
val hitEntities = currentLocation.world.getNearbyEntities( currentLocation, 0.5, 0.5, 0.5 ) {
it is Player && it.uniqueId != player.uniqueId && it.gameMode != GameMode.SPECTATOR
}
if ( nearestPlayer != null )
if ( hitEntities.isNotEmpty() )
{
onHit( nearestPlayer )
onHit( hitEntities.first() as Player )
this.cancel()
return
}
currentLocation.add(normalizedDirection.multiply( step ))
currentLocation.add( stepVector )
traveledDistance += step
}
}.runTaskTimer( plugin, 0L, 1L )
}

View File

@@ -528,12 +528,81 @@ kits:
- 'DEFENSIVE: 8-block anchor + Resistance I'
items:
chain:
name: '<gray>⚓ Deploy Anchor'
name: '<gray>⚓ Deploy Anchor</gray>'
description: 'Summon an Iron Golem anchor. Enemies can destroy it!'
passive:
name: '<gray>Anchored'
name: '<gray>Anchored</gray>'
description: 'NoKnock + bonus within anchor radius'
messages:
anchor_placed: '<gray>Anchor deployed! Radius: <radius> blocks.'
anchor_destroyed: '<red>⚓ Your anchor was destroyed!'
ability_charged: '<gray>Anchor ready to deploy!'
ability_charged: '<gray>Anchor ready to deploy!'
ninja:
name: '<gradient:dark_gray:white><bold>Ninja</bold></gradient>'
lore:
- ' '
- 'AGGRESSIVE: Sneak → teleports behind last hit enemy'
- 'DEFENSIVE: Smoke aura Blindness I + Slowness I'
items:
smoke:
name: '<dark_gray>Smoke Bomb</dark_gray>'
description: 'Enemies in 3-block radius get Blindness + Slowness'
messages:
teleported: '<gray>Teleported behind enemy!'
cooldown: '<red>Teleport on cooldown <time>s'
no_target: '<red>No recent target! Hit an enemy first.'
target_expired: '<red>Target expired (10 s window).'
trident:
name: '<gradient:aqua:blue><bold>Trident</bold></gradient>'
lore:
- ' '
- 'AGGRESSIVE: 3 dive charges lightning on landing'
- 'DEFENSIVE: 20% parry bounce back + Slowness I'
items:
trident:
aggressive:
name: '<aqua>⚡ Trident Dive</aqua>'
description: 'Right-click: launch up, lightning strikes on landing'
defensive:
name: '<aqua>⚡ Trident Parry</aqua>'
description: '20% chance: bounce attacker + Slowness I (2s)'
passive:
defensive:
name: 'Trident Parry'
description: '20% chance to bounce attacker with Slowness I'
messages:
dive_launched: '<aqua>⚡ Launched! <charges> charge(s) left.'
charges_left: '<aqua>⚡ <charges> dive charge(s) remaining!'
sequence_done: '<gray>Sequence complete.'
parry_success: '<aqua>⚡ Parry!</aqua>'
parried_by_victim: '<red>Parried!'
blitzcrank:
name: '<gradient:yellow:gold><bold>Blitzcrank</bold></gradient>'
lore:
- ' '
- 'AGGRESSIVE: Hook pull first enemy in line of sight'
- 'DEFENSIVE: Stun freeze all nearby enemies'
- 'Both: Ult AoE discharge'
items:
hook:
name: '<yellow>⚡ Rocket Grab</yellow>'
description: 'Pull the first enemy in your line of sight'
stun:
name: '<yellow>⚡ Power Fist</yellow>'
description: 'Stun all nearby enemies for 3 seconds'
ult:
name: '<gold>⚡ Static Field</gold>'
description: 'AoE discharge damages all enemies in 6 blocks'
passive:
name: 'Static Field'
description: 'Unleash an electric AoE (30s cooldown)'
messages:
hook_hit: '<yellow>⚡ Hook hit!'
hooked: '<red>You were hooked!'
stun_cast: '<yellow>⚡ Stunned <count> enemy(s)!'
stunned: '<red>⚡ You are stunned!'
ult_fired: '<gold>⚡ Static Field hit <count> enemy(s)!'
ult_cooldown: '<red>Ult on cooldown <time>s'
ult_no_targets: '<red>No enemies in range!'