diff --git a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt index 8c957b5..073aa02 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt @@ -6,6 +6,7 @@ import club.mcscrims.speedhg.game.GameManager import club.mcscrims.speedhg.game.modules.AntiRunningManager import club.mcscrims.speedhg.kit.KitManager import club.mcscrims.speedhg.kit.impl.GoblinKit +import club.mcscrims.speedhg.kit.impl.IceMageKit import club.mcscrims.speedhg.kit.listener.KitEventDispatcher import club.mcscrims.speedhg.listener.ConnectListener import club.mcscrims.speedhg.listener.GameStateListener @@ -53,6 +54,7 @@ class SpeedHG : JavaPlugin() { kitManager = KitManager( this ) // Register kits kitManager.registerKit( GoblinKit() ) + kitManager.registerKit( IceMageKit() ) registerCommands() registerListener() diff --git a/src/main/kotlin/club/mcscrims/speedhg/command/KitCommand.kt b/src/main/kotlin/club/mcscrims/speedhg/command/KitCommand.kt index ef7aa2f..0f900ff 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/command/KitCommand.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/command/KitCommand.kt @@ -1,6 +1,7 @@ package club.mcscrims.speedhg.command import club.mcscrims.speedhg.SpeedHG +import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.util.legacySerializer import club.mcscrims.speedhg.util.sendMsg import org.bukkit.command.Command @@ -28,7 +29,7 @@ class KitCommand : CommandExecutor, TabCompleter { return true } - if ( args.isNullOrEmpty() ) + if ( args.isNullOrEmpty() || args.size < 2 ) { player.sendMsg( "commands.kit.usage" ) return true @@ -43,8 +44,18 @@ class KitCommand : CommandExecutor, TabCompleter { return true } + val playstyle = Playstyle.entries.firstOrNull { it.name.equals( args[1], true ) } + + if ( playstyle == null ) + { + player.sendMsg( "commands.kit.playstyleNotFound", "playstyle" to args[1] ) + return true + } + plugin.kitManager.selectKit( player, kit ) - player.sendMsg( "commands.kit.selected", "kit" to legacySerializer.serialize( kit.displayName )) + plugin.kitManager.selectPlaystyle( player, playstyle ) + + player.sendMsg( "commands.kit.selected", "playstyle" to playstyle.displayName, "kit" to legacySerializer.serialize( kit.displayName )) return true } @@ -61,6 +72,9 @@ class KitCommand : CommandExecutor, TabCompleter { if ( args.size == 1 ) return plugin.kitManager.getRegisteredKits().map { it.id } + if ( args.size == 2 ) + return Playstyle.entries.map { it.name.lowercase() } + return listOf() } diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt index 3c695da..8cae706 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt @@ -22,7 +22,7 @@ import java.util.concurrent.ConcurrentHashMap class GoblinKit : Kit() { - private val plugin = SpeedHG.instance + private val plugin get() = SpeedHG.instance override val id: String get() = "goblin" @@ -104,7 +104,7 @@ class GoblinKit : Kit() { items.forEach { player.inventory.remove( it ) } } - private inner class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) { + private class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) { private val plugin get() = SpeedHG.instance @@ -175,7 +175,7 @@ class GoblinKit : Kit() { } - private inner class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) { + private class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) { private val plugin get() = SpeedHG.instance diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/IceMageKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/IceMageKit.kt new file mode 100644 index 0000000..59166e5 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/IceMageKit.kt @@ -0,0 +1,203 @@ +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.sendMsg +import club.mcscrims.speedhg.util.trans +import net.kyori.adventure.text.Component +import org.bukkit.Material +import org.bukkit.Sound +import org.bukkit.entity.Player +import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.player.PlayerMoveEvent +import org.bukkit.inventory.ItemStack +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffectType +import java.util.Random +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +class IceMageKit : Kit() { + + private val plugin get() = SpeedHG.instance + + override val id: String + get() = "icemage" + + override val displayName: Component + get() = plugin.languageManager.getDefaultComponent( "kits.icemage.name", mapOf() ) + + override val lore: List + get() = plugin.languageManager.getDefaultRawMessageList( "kits.icemage.lore" ) + + override val icon: Material + get() = Material.SNOWBALL + + // ── Cached ability instances (avoid allocating per event call) ──────────── + private val aggressiveActive = AggressiveActive() + private val defensiveActive = DefensiveActive() + private val aggressivePassive = AggressivePassive() + private val defensivePassive = DefensivePassive() + + // ── Playstyle routing ───────────────────────────────────────────────────── + + 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 + } + + // ── Item distribution ───────────────────────────────────────────────────── + + private val cachedItems = ConcurrentHashMap>() + + override fun giveItems( + player: Player, + playstyle: Playstyle + ) { + if ( playstyle != Playstyle.DEFENSIVE ) + return + + val snowBall = ItemBuilder( Material.SNOWBALL ) + .name( defensiveActive.name ) + .lore(listOf( defensiveActive.description )) + .build() + + cachedItems[ player.uniqueId ] = listOf( snowBall ) + player.inventory.addItem( snowBall ) + } + + // ── Optional lifecycle hooks ────────────────────────────────────────────── + + override fun onRemove( + player: Player + ) { + val items = cachedItems.remove( player.uniqueId ) ?: return + items.forEach { player.inventory.remove( it ) } + } + + private class AggressiveActive : ActiveAbility( Playstyle.AGGRESSIVE ) { + + override val name: String + get() = "None" + + override val description: String + get() = "None" + + override val hitsRequired: Int + get() = 0 + + override val triggerMaterial: Material + get() = Material.BARRIER + + override fun execute( + player: Player + ): AbilityResult + { + return AbilityResult.Success + } + + } + + private class DefensiveActive : ActiveAbility( Playstyle.DEFENSIVE ) { + + private val plugin get() = SpeedHG.instance + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.items.snowball.name" ) + + override val description: String + get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.items.snowball.description" ) + + override val hitsRequired: Int + get() = 15 + + override val triggerMaterial: Material + get() = Material.SNOWBALL + + override fun execute( + player: Player + ): AbilityResult + { + player.playSound( player.location, Sound.ENTITY_PLAYER_HURT_FREEZE, 1f, 1.5f ) + player.sendActionBar(player.trans( "kits.icemage.messages.shoot_snowballs" )) + return AbilityResult.Success + } + + override fun onFullyCharged( + player: Player + ) { + player.playSound( player.location, Sound.BLOCK_ANVIL_USE, 0.8f, 1.5f ) + player.sendActionBar(player.trans( "kits.icemage.messages.ability_charged" )) + } + + } + + private class AggressivePassive : PassiveAbility( Playstyle.AGGRESSIVE ) { + + private val plugin get() = SpeedHG.instance + private val random = Random() + + private val biomeList = listOf( + "taiga_cold", + "snowy_tundra", + "ice_spikes", + "snowy_beach", + "grove", + "snowy_slopes", + "jagged_peaks", + "frozen_peaks" + ) + + override val name: String + get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.passive.name" ) + + override val description: String + get() = plugin.languageManager.getDefaultRawMessage( "kits.icemage.passive.description" ) + + override fun onMove( + player: Player, + event: PlayerMoveEvent + ) { + val biome = player.world.getBiome( player.location ) + if (!biomeList.contains( biome.name.lowercase() )) return + player.addPotionEffect(PotionEffect( PotionEffectType.SPEED, 20, 0 )) + } + + override fun onHitEnemy( + attacker: Player, + victim: Player, + event: EntityDamageByEntityEvent + ) { + if ( random.nextBoolean() ) + victim.addPotionEffect(PotionEffect( PotionEffectType.SLOWNESS, 60, 0 )) + } + + } + + private class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE ) { + + override val name: String + get() = "None" + + override val description: String + get() = "None" + + } + +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt index 9fe1181..668e3d7 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt @@ -4,16 +4,28 @@ import club.mcscrims.speedhg.SpeedHG import club.mcscrims.speedhg.game.GameState import club.mcscrims.speedhg.kit.KitManager import club.mcscrims.speedhg.kit.ability.AbilityResult +import club.mcscrims.speedhg.kit.impl.IceMageKit import net.kyori.adventure.text.Component import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.NamespacedKey +import org.bukkit.entity.LivingEntity import org.bukkit.entity.Player +import org.bukkit.entity.Snowball import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority import org.bukkit.event.Listener import org.bukkit.event.entity.EntityDamageByEntityEvent +import org.bukkit.event.entity.ProjectileHitEvent +import org.bukkit.event.entity.ProjectileLaunchEvent import org.bukkit.event.player.PlayerInteractEvent import org.bukkit.event.player.PlayerMoveEvent import org.bukkit.inventory.EquipmentSlot +import org.bukkit.persistence.PersistentDataType +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffectType +import org.bukkit.util.Vector +import kotlin.math.cos +import kotlin.math.sin /** * The *single* Bukkit [Listener] responsible for all kit-related event handling. @@ -171,6 +183,60 @@ class KitEventDispatcher( kit.getPassiveAbility(kitManager.getSelectedPlaystyle( player )).onMove( player, event ) } + // ========================================================================= + // IceMage Listener + // ========================================================================= + + private val iceMageKey = NamespacedKey( plugin, "icemage_snowball" ) + + @EventHandler + fun onSnowballThrow( + event: ProjectileLaunchEvent + ) { + val projectile = event.entity as? Snowball ?: return + val shooter = projectile.shooter as? Player ?: return + if (kitManager.getSelectedKit( shooter ) !is IceMageKit ) return + if (kitManager.getChargeData( shooter )?.isReady == false ) return + + val amountOfSnowballs = 16 + val playerLocation = shooter.location + val baseSpeed = 1.5 + + for ( i in 0 until amountOfSnowballs ) + { + val angle = i * ( 2 * Math.PI / amountOfSnowballs ) + + val x = cos( angle ) + val z = sin( angle ) + + val direction = Vector( x, 0.0, z ).normalize().multiply( baseSpeed ) + + val snowBall = shooter.world.spawn( playerLocation, Snowball::class.java ) + snowBall.shooter = shooter + snowBall.velocity = direction + + snowBall.persistentDataContainer.set( iceMageKey, PersistentDataType.BYTE, 1.toByte() ) + } + + event.isCancelled = true + } + + @EventHandler + fun onSnowballHit( + event: ProjectileHitEvent + ) { + val projectile = event.entity as? Snowball ?: return + if (!projectile.persistentDataContainer.has( iceMageKey, PersistentDataType.BYTE )) return + + val hitEntity = event.hitEntity + + if ( hitEntity is LivingEntity && hitEntity != projectile.shooter ) + { + hitEntity.freezeTicks = 60 + hitEntity.addPotionEffect(PotionEffect( PotionEffectType.SLOWNESS, 40, 1 )) + } + } + // ========================================================================= // Helpers // ========================================================================= diff --git a/src/main/resources/languages/en_US.yml b/src/main/resources/languages/en_US.yml index 71c4bf4..f472cdf 100644 --- a/src/main/resources/languages/en_US.yml +++ b/src/main/resources/languages/en_US.yml @@ -45,9 +45,10 @@ craft: commands: kit: - usage: 'Usage: /kit ' + usage: 'Usage: /kit ' kitNotFound: ' is not a registered kit!' - selected: 'You have selected as your Kit!' + playstyleNotFound: ' is not an available playstyle!' + selected: 'You have selected as your Kit with playstyle !' scoreboard: title: 'SpeedHG' diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 347f106..2a39f79 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -9,4 +9,4 @@ depend: commands: kit: description: 'Select kits via command' - usage: '/kit ' \ No newline at end of file + usage: '/kit ' \ No newline at end of file