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( 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() )
|
||||
}
|
||||
|
||||
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 )
|
||||
|
||||
AbilityUtils.createBeam(
|
||||
player,
|
||||
player.location,
|
||||
player.eyeLocation.toVector(),
|
||||
Particle.DRAGON_BREATH,
|
||||
|
||||
@@ -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 )
|
||||
}
|
||||
|
||||
|
||||
@@ -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!'
|
||||
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