diff --git a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt index fc1cfaa..c7948bb 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt @@ -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() ) } diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlitzcrankKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlitzcrankKit.kt new file mode 100644 index 0000000..1f89ad3 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlitzcrankKit.kt @@ -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 + 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 = ConcurrentHashMap() + /** Ult-Cooldown: UUID des Casters → letzter Auslöse-Timestamp. */ + private val ultCooldowns: MutableMap = 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>() + + 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() + .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() + ?.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() + .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) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/NinjaKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/NinjaKit.kt new file mode 100644 index 0000000..dbd2505 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/NinjaKit.kt @@ -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 + 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> = ConcurrentHashMap() + private val smokeTasks: MutableMap = ConcurrentHashMap() + private val teleportCooldowns: MutableMap = 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>() + + 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() + .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" + } + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TridentKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TridentKit.kt new file mode 100644 index 0000000..1fd909b --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TridentKit.kt @@ -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 + 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 = ConcurrentHashMap() + private val diveMonitors: MutableMap = ConcurrentHashMap() + private val lastSequenceTime: MutableMap = 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>() + + 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() + .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" + } + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VenomKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VenomKit.kt index 7ee5264..8e0b862 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VenomKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VenomKit.kt @@ -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, diff --git a/src/main/kotlin/club/mcscrims/speedhg/util/AbilityUtils.kt b/src/main/kotlin/club/mcscrims/speedhg/util/AbilityUtils.kt index 07ee07e..d4b479c 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/util/AbilityUtils.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/util/AbilityUtils.kt @@ -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().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 ) } diff --git a/src/main/resources/languages/en_US.yml b/src/main/resources/languages/en_US.yml index 1c1329f..4526d92 100644 --- a/src/main/resources/languages/en_US.yml +++ b/src/main/resources/languages/en_US.yml @@ -528,12 +528,81 @@ kits: - 'DEFENSIVE: 8-block anchor + Resistance I' items: chain: - name: '⚓ Deploy Anchor' + name: '⚓ Deploy Anchor' description: 'Summon an Iron Golem anchor. Enemies can destroy it!' passive: - name: 'Anchored' + name: 'Anchored' description: 'NoKnock + bonus within anchor radius' messages: anchor_placed: 'Anchor deployed! Radius: blocks.' anchor_destroyed: '⚓ Your anchor was destroyed!' - ability_charged: 'Anchor ready to deploy!' \ No newline at end of file + ability_charged: 'Anchor ready to deploy!' + ninja: + name: 'Ninja' + lore: + - ' ' + - 'AGGRESSIVE: Sneak → teleports behind last hit enemy' + - 'DEFENSIVE: Smoke aura – Blindness I + Slowness I' + items: + smoke: + name: 'Smoke Bomb' + description: 'Enemies in 3-block radius get Blindness + Slowness' + messages: + teleported: 'Teleported behind enemy!' + cooldown: 'Teleport on cooldown –