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:
@@ -217,13 +217,16 @@ class SpeedHG : JavaPlugin() {
|
|||||||
kitManager.registerKit( ArmorerKit() )
|
kitManager.registerKit( ArmorerKit() )
|
||||||
kitManager.registerKit( BackupKit() )
|
kitManager.registerKit( BackupKit() )
|
||||||
kitManager.registerKit( BlackPantherKit() )
|
kitManager.registerKit( BlackPantherKit() )
|
||||||
|
kitManager.registerKit( BlitzcrankKit() )
|
||||||
kitManager.registerKit( GladiatorKit() )
|
kitManager.registerKit( GladiatorKit() )
|
||||||
kitManager.registerKit( GoblinKit() )
|
kitManager.registerKit( GoblinKit() )
|
||||||
kitManager.registerKit( IceMageKit() )
|
kitManager.registerKit( IceMageKit() )
|
||||||
|
kitManager.registerKit( NinjaKit() )
|
||||||
kitManager.registerKit( PuppetKit() )
|
kitManager.registerKit( PuppetKit() )
|
||||||
kitManager.registerKit( RattlesnakeKit() )
|
kitManager.registerKit( RattlesnakeKit() )
|
||||||
kitManager.registerKit( TeslaKit() )
|
kitManager.registerKit( TeslaKit() )
|
||||||
kitManager.registerKit( TheWorldKit() )
|
kitManager.registerKit( TheWorldKit() )
|
||||||
|
kitManager.registerKit( TridentKit() )
|
||||||
kitManager.registerKit( VenomKit() )
|
kitManager.registerKit( VenomKit() )
|
||||||
kitManager.registerKit( VoodooKit() )
|
kitManager.registerKit( VoodooKit() )
|
||||||
}
|
}
|
||||||
|
|||||||
368
src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlitzcrankKit.kt
Normal file
368
src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlitzcrankKit.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
271
src/main/kotlin/club/mcscrims/speedhg/kit/impl/NinjaKit.kt
Normal file
271
src/main/kotlin/club/mcscrims/speedhg/kit/impl/NinjaKit.kt
Normal 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"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
339
src/main/kotlin/club/mcscrims/speedhg/kit/impl/TridentKit.kt
Normal file
339
src/main/kotlin/club/mcscrims/speedhg/kit/impl/TridentKit.kt
Normal 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"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -152,6 +152,7 @@ class VenomKit : Kit() {
|
|||||||
player.playSound( player.location, Sound.ENTITY_BLAZE_SHOOT, 1f, 0.8f )
|
player.playSound( player.location, Sound.ENTITY_BLAZE_SHOOT, 1f, 0.8f )
|
||||||
|
|
||||||
AbilityUtils.createBeam(
|
AbilityUtils.createBeam(
|
||||||
|
player,
|
||||||
player.location,
|
player.location,
|
||||||
player.eyeLocation.toVector(),
|
player.eyeLocation.toVector(),
|
||||||
Particle.DRAGON_BREATH,
|
Particle.DRAGON_BREATH,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ object AbilityUtils {
|
|||||||
private val plugin = SpeedHG.instance
|
private val plugin = SpeedHG.instance
|
||||||
|
|
||||||
fun createBeam(
|
fun createBeam(
|
||||||
|
player: Player,
|
||||||
startLocation: Location,
|
startLocation: Location,
|
||||||
direction: Vector,
|
direction: Vector,
|
||||||
particle: Particle,
|
particle: Particle,
|
||||||
@@ -22,37 +23,36 @@ object AbilityUtils {
|
|||||||
step: Double,
|
step: Double,
|
||||||
onHit: (Player) -> Unit
|
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 traveledDistance = 0.0
|
||||||
var currentLocation = startLocation.clone()
|
|
||||||
|
|
||||||
override fun run()
|
override fun run()
|
||||||
{
|
{
|
||||||
if ( traveledDistance >= range)
|
if ( traveledDistance >= range )
|
||||||
{
|
{
|
||||||
this.cancel()
|
this.cancel()
|
||||||
return
|
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 )
|
val hitEntities = currentLocation.world.getNearbyEntities( currentLocation, 0.5, 0.5, 0.5 ) {
|
||||||
.filterIsInstance<Player>().minByOrNull { it.location.distance( currentLocation ) }
|
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()
|
this.cancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
currentLocation.add(normalizedDirection.multiply( step ))
|
currentLocation.add( stepVector )
|
||||||
traveledDistance += step
|
traveledDistance += step
|
||||||
}
|
}
|
||||||
|
|
||||||
}.runTaskTimer( plugin, 0L, 1L )
|
}.runTaskTimer( plugin, 0L, 1L )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -528,12 +528,81 @@ kits:
|
|||||||
- 'DEFENSIVE: 8-block anchor + Resistance I'
|
- 'DEFENSIVE: 8-block anchor + Resistance I'
|
||||||
items:
|
items:
|
||||||
chain:
|
chain:
|
||||||
name: '<gray>⚓ Deploy Anchor'
|
name: '<gray>⚓ Deploy Anchor</gray>'
|
||||||
description: 'Summon an Iron Golem anchor. Enemies can destroy it!'
|
description: 'Summon an Iron Golem anchor. Enemies can destroy it!'
|
||||||
passive:
|
passive:
|
||||||
name: '<gray>Anchored'
|
name: '<gray>Anchored</gray>'
|
||||||
description: 'NoKnock + bonus within anchor radius'
|
description: 'NoKnock + bonus within anchor radius'
|
||||||
messages:
|
messages:
|
||||||
anchor_placed: '<gray>Anchor deployed! Radius: <radius> blocks.'
|
anchor_placed: '<gray>Anchor deployed! Radius: <radius> blocks.'
|
||||||
anchor_destroyed: '<red>⚓ Your anchor was destroyed!'
|
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!'
|
||||||
Reference in New Issue
Block a user