Integrate Lunar Client and gameplay tweaks

Add Lunar Client support and make various kit/listener/ui adjustments.

- Add Apollo (Lunar Client) compileOnly dependencies and new LunarClientManager wired into plugin initialization. Update plugin.yml (depend Apollo-Bukkit, add speedhg.admin.staff permission) and config.yml (lunarclient name/variantName) for rich presence and mod settings.
- Rework Tesla kit passive: remove periodic aura task in favor of chance-on-hit effect (AURA_CHANCE) to reduce continuous scheduling and simplify behavior; adjust constants and particle/sound logic accordingly.
- Add Anchor golem death handler to KitEventDispatcher to suppress drops/exp for golems spawned by AnchorKit.
- Update AnvilSearchMenu to clear item-on-cursor before closing and ensure search tracker is unregistered in the right order.
- Remove join/quit attack-speed cooldown handling in GameStateListener and tidy up some permission/command descriptions in plugin.yml.

These changes enable Lunar features, improve performance by removing repeated scheduled tasks, and fix gameplay/drop/UI edge cases.
This commit is contained in:
TDSTOS
2026-04-11 00:49:42 +02:00
parent 9ad3a8f342
commit cd8e3e37a7
9 changed files with 158 additions and 96 deletions

View File

@@ -30,6 +30,9 @@ dependencies {
implementation(libs.kotlinxCoroutines) implementation(libs.kotlinxCoroutines)
implementation(libs.kotlinxSerialization) implementation(libs.kotlinxSerialization)
compileOnly("com.lunarclient:apollo-api:1.2.4")
compileOnly("com.lunarclient:apollo-extra-adventure4:1.2.4")
compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT") compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT")
compileOnly("com.sk89q.worldedit:worldedit-core:7.2.17-SNAPSHOT") compileOnly("com.sk89q.worldedit:worldedit-core:7.2.17-SNAPSHOT")
compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.2.17-SNAPSHOT") compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.2.17-SNAPSHOT")

View File

@@ -1,5 +1,6 @@
package club.mcscrims.speedhg package club.mcscrims.speedhg
import club.mcscrims.speedhg.client.LunarClientManager
import club.mcscrims.speedhg.command.KitCommand import club.mcscrims.speedhg.command.KitCommand
import club.mcscrims.speedhg.command.LeaderboardCommand import club.mcscrims.speedhg.command.LeaderboardCommand
import club.mcscrims.speedhg.command.PerksCommand import club.mcscrims.speedhg.command.PerksCommand
@@ -106,6 +107,9 @@ class SpeedHG : JavaPlugin() {
lateinit var teamManager: TeamManager lateinit var teamManager: TeamManager
private set private set
lateinit var lunarClientManager: LunarClientManager
private set
override fun onLoad() override fun onLoad()
{ {
instance = this instance = this
@@ -159,6 +163,7 @@ class SpeedHG : JavaPlugin() {
scoreboardManager = ScoreboardManager( this ) scoreboardManager = ScoreboardManager( this )
kitManager = KitManager( this ) kitManager = KitManager( this )
discordWebhookManager = DiscordWebhookManager( this ) discordWebhookManager = DiscordWebhookManager( this )
lunarClientManager = LunarClientManager( this )
perkManager = PerkManager( this ) perkManager = PerkManager( this )
perkManager.initialize() perkManager.initialize()

View File

@@ -0,0 +1,85 @@
package club.mcscrims.speedhg.client
import club.mcscrims.speedhg.SpeedHG
import com.lunarclient.apollo.Apollo
import com.lunarclient.apollo.event.ApolloListener
import com.lunarclient.apollo.event.Listen
import com.lunarclient.apollo.event.player.ApolloRegisterPlayerEvent
import com.lunarclient.apollo.mods.impl.ModFreelook
import com.lunarclient.apollo.mods.impl.ModMinimap
import com.lunarclient.apollo.mods.impl.ModSnaplook
import com.lunarclient.apollo.mods.impl.ModTeamView
import com.lunarclient.apollo.mods.impl.ModWaypoints
import com.lunarclient.apollo.module.modsetting.ModSettingModule
import com.lunarclient.apollo.module.richpresence.RichPresenceModule
import com.lunarclient.apollo.module.richpresence.ServerRichPresence
import com.lunarclient.apollo.module.staffmod.StaffMod
import com.lunarclient.apollo.module.staffmod.StaffModModule
import com.lunarclient.apollo.player.ApolloPlayer
import org.bukkit.Bukkit
class LunarClientManager(
private val plugin: SpeedHG
) : ApolloListener {
private val modSettingModule: ModSettingModule
= Apollo.getModuleManager().getModule( ModSettingModule::class.java )
private val staffModModule: StaffModModule
= Apollo.getModuleManager().getModule( StaffModModule::class.java )
private val richPresenceModule: RichPresenceModule
= Apollo.getModuleManager().getModule( RichPresenceModule::class.java )
init {
this.handle( ApolloRegisterPlayerEvent::class.java, this::onApolloRegister )
Bukkit.getScheduler().runTaskTimer( plugin, { -> Apollo.getPlayerManager().players.forEach( this::setRichPresence ) }, 20L, 20L )
}
@Listen
fun onApolloRegister(
event: ApolloRegisterPlayerEvent
) {
val player = event.player
setMods( player )
setRichPresence( player )
}
private fun setRichPresence(
player: ApolloPlayer
) {
val teamSize = plugin.teamManager.getTeam( player.uniqueId )?.size ?: 1
val currentState = plugin.gameManager.currentState.name
val presence = ServerRichPresence.builder()
.gameName(plugin.config.getString( "lunarclient.name" ))
.gameState( currentState )
.gameVariantName(plugin.config.getString( "lunarclient.variantName" ))
.playerState( currentState )
.teamCurrentSize( teamSize )
.teamMaxSize( plugin.teamManager.maxTeamSize )
.build()
richPresenceModule.overrideServerRichPresence( player, presence )
}
private fun setMods(
player: ApolloPlayer
) {
if (player.hasPermission( "speedhg.admin.staff" ))
{
staffModModule.enableStaffMods( player, StaffMod.entries )
}
else
{
staffModModule.disableAllStaffMods( player )
}
modSettingModule.options.set( player, ModMinimap.ENABLED, false )
modSettingModule.options.set( player, ModFreelook.ENABLED, false )
modSettingModule.options.set( player, ModSnaplook.ENABLED, false )
modSettingModule.options.set( player, ModWaypoints.ENABLED, false )
modSettingModule.options.set( player, ModTeamView.ENABLED, true )
}
}

View File

@@ -35,12 +35,17 @@ class AnvilSearchMenu(
fun onClick(event: InventoryClickEvent, view: AnvilView) { fun onClick(event: InventoryClickEvent, view: AnvilView) {
event.isCancelled = true event.isCancelled = true
if (event.rawSlot != 2) return if ( event.rawSlot != 2 ) return
val query = view.renameText ?: "" val query = view.renameText ?: ""
if ( !player.itemOnCursor.type.isAir ) {
player.setItemOnCursor(ItemStack( Material.AIR ))
}
AnvilSearchTracker.unregister( player )
player.closeInventory() player.closeInventory()
AnvilSearchTracker.unregister(player) returnMenu.applySearch( query )
returnMenu.applySearch(query)
} }
fun onClose() { fun onClose() {

View File

@@ -14,6 +14,7 @@ import org.bukkit.Material
import org.bukkit.Particle import org.bukkit.Particle
import org.bukkit.Sound import org.bukkit.Sound
import org.bukkit.entity.Player import org.bukkit.entity.Player
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ItemStack
import org.bukkit.scheduler.BukkitTask import org.bukkit.scheduler.BukkitTask
import org.bukkit.util.Vector import org.bukkit.util.Vector
@@ -72,12 +73,8 @@ class TeslaKit : Kit() {
const val BOLT_STAGGER_TICKS = 8L const val BOLT_STAGGER_TICKS = 8L
// Passive Aura // Passive Aura
const val AURA_RADIUS_AGGRESSIVE = 4.0 const val AURA_CHANCE = 0.05
const val AURA_RADIUS_DEFENSIVE = 6.0
const val AURA_INTERVAL_TICKS = 60L
const val AURA_FIRE_TICKS = 60 const val AURA_FIRE_TICKS = 60
const val KNOCKBACK_AGGRESSIVE = 1.6
const val KNOCKBACK_DEFENSIVE = 2.3
} }
// ── Gecachte Instanzen ──────────────────────────────────────────────────── // ── Gecachte Instanzen ────────────────────────────────────────────────────
@@ -85,14 +82,10 @@ class TeslaKit : Kit() {
private val aggressiveActive = AggressiveActive() private val aggressiveActive = AggressiveActive()
private val defensiveActive = NoActive(Playstyle.DEFENSIVE) private val defensiveActive = NoActive(Playstyle.DEFENSIVE)
private val aggressivePassive = TeslaPassive( private val aggressivePassive = TeslaPassive(
playstyle = Playstyle.AGGRESSIVE, playstyle = Playstyle.AGGRESSIVE
auraRadius = AURA_RADIUS_AGGRESSIVE,
knockbackStrength = KNOCKBACK_AGGRESSIVE
) )
private val defensivePassive = TeslaPassive( private val defensivePassive = TeslaPassive(
playstyle = Playstyle.DEFENSIVE, playstyle = Playstyle.DEFENSIVE
auraRadius = AURA_RADIUS_DEFENSIVE,
knockbackStrength = KNOCKBACK_DEFENSIVE
) )
override fun getActiveAbility( override fun getActiveAbility(
@@ -216,83 +209,44 @@ class TeslaKit : Kit() {
// ========================================================================= // =========================================================================
class TeslaPassive( class TeslaPassive(
playstyle: Playstyle, playstyle: Playstyle
private val auraRadius: Double,
private val knockbackStrength: Double
) : PassiveAbility( playstyle ) { ) : PassiveAbility( playstyle ) {
private val plugin get() = SpeedHG.instance private val plugin get() = SpeedHG.instance
private val auraTasks = ConcurrentHashMap<UUID, BukkitTask>() private val rng = Random()
override val name: String override val name: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.passive.name" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.passive.name" )
override val description: String override val description: String
get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.passive.description" ) get() = plugin.languageManager.getDefaultRawMessage( "kits.tesla.passive.description" )
override fun onActivate( override fun onHitByEnemy(
player: Player victim: Player,
attacker: Player,
event: EntityDamageByEntityEvent
) { ) {
val task = Bukkit.getScheduler().runTaskTimer( plugin, { -> if ( rng.nextDouble() > AURA_CHANCE )
return
// Spieler oder Spielstatus nicht mehr gültig -> Task beenden attacker.fireTicks = AURA_FIRE_TICKS
if ( !player.isOnline ||
!plugin.gameManager.alivePlayers.contains( player.uniqueId ))
{
auraTasks.remove( player.uniqueId )?.cancel()
return@runTaskTimer
}
// Höhen-Check; kein Effekt über der Grenze attacker.world.spawnParticle(
if ( player.location.y > MAX_HEIGHT_Y ) Particle.ELECTRIC_SPARK,
return@runTaskTimer attacker.location.clone().add( 0.0, 1.0, 0.0 ),
10, 0.3, 0.4, 0.3, 0.06
)
val nearbyEnemies = player.world victim.world.spawnParticle(
.getNearbyEntities( player.location, auraRadius, auraRadius, auraRadius ) Particle.ELECTRIC_SPARK,
.filterIsInstance<Player>() victim.location.clone().add( 0.0, 1.0, 0.0 ),
.filter { it != player && plugin.gameManager.alivePlayers.contains( it.uniqueId ) } 6, 0.6, 0.6, 0.6, 0.02
)
if ( nearbyEnemies.isEmpty() ) victim.world.playSound(
return@runTaskTimer victim.location,
Sound.ENTITY_LIGHTNING_BOLT_IMPACT,
nearbyEnemies.forEach { enemy -> 0.4f, 1.9f
// Velocity-basierter Rückschlag (radial nach außen) )
val pushDir: Vector = enemy.location.toVector()
.subtract( player.location.toVector() )
.normalize()
.multiply( knockbackStrength )
.setY( 0.3 )
enemy.velocity = enemy.velocity.add( pushDir )
enemy.fireTicks = AURA_FIRE_TICKS
enemy.world.spawnParticle(
Particle.ELECTRIC_SPARK,
enemy.location.clone().add( 0.0, 1.0, 0.0 ),
10, 0.3, 0.4, 0.3, 0.06
)
}
// Visuelles Feedback am Tesla-Spieler
player.world.spawnParticle(
Particle.ELECTRIC_SPARK,
player.location.clone().add( 0.0, 1.0, 0.0 ),
6, 0.6, 0.6, 0.6, 0.02
)
player.world.playSound(
player.location,
Sound.ENTITY_LIGHTNING_BOLT_IMPACT,
0.4f, 1.9f
)
}, AURA_INTERVAL_TICKS, AURA_INTERVAL_TICKS )
auraTasks[ player.uniqueId ] = task
}
override fun onDeactivate(
player: Player
) {
auraTasks.remove( player.uniqueId )?.cancel()
} }
} }

View File

@@ -7,6 +7,7 @@ import club.mcscrims.speedhg.kit.KitMetaData
import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.Playstyle
import club.mcscrims.speedhg.kit.ability.AbilityResult import club.mcscrims.speedhg.kit.ability.AbilityResult
import club.mcscrims.speedhg.kit.charge.ChargeState import club.mcscrims.speedhg.kit.charge.ChargeState
import club.mcscrims.speedhg.kit.impl.AnchorKit
import club.mcscrims.speedhg.kit.impl.BlackPantherKit import club.mcscrims.speedhg.kit.impl.BlackPantherKit
import club.mcscrims.speedhg.kit.impl.IceMageKit import club.mcscrims.speedhg.kit.impl.IceMageKit
import club.mcscrims.speedhg.kit.impl.VenomKit import club.mcscrims.speedhg.kit.impl.VenomKit
@@ -25,6 +26,7 @@ import org.bukkit.event.EventPriority
import org.bukkit.event.Listener import org.bukkit.event.Listener
import org.bukkit.event.block.BlockBreakEvent import org.bukkit.event.block.BlockBreakEvent
import org.bukkit.event.entity.EntityDamageByEntityEvent import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.event.entity.EntityDeathEvent
import org.bukkit.event.entity.EntityExplodeEvent import org.bukkit.event.entity.EntityExplodeEvent
import org.bukkit.event.entity.PlayerDeathEvent import org.bukkit.event.entity.PlayerDeathEvent
import org.bukkit.event.entity.ProjectileHitEvent import org.bukkit.event.entity.ProjectileHitEvent
@@ -377,6 +379,19 @@ class KitEventDispatcher(
kit.onItemBreak( event.player, event.brokenItem ) kit.onItemBreak( event.player, event.brokenItem )
} }
@EventHandler(priority = EventPriority.HIGH)
fun onAnchorGolemDeath(
event: EntityDeathEvent
) {
val golem = event.entity as? IronGolem ?: return
val pdcKey = NamespacedKey( plugin, AnchorKit.PDC_KEY )
if (!golem.persistentDataContainer.has( pdcKey, PersistentDataType.STRING )) return
event.drops.clear()
event.droppedExp = 0
}
// ========================================================================= // =========================================================================
// Helpers // Helpers
// ========================================================================= // =========================================================================

View File

@@ -235,19 +235,6 @@ class GameStateListener : Listener {
event.isCancelled = true event.isCancelled = true
} }
@EventHandler
fun onJoin( event: PlayerJoinEvent ) { disableHitCooldown( event.player ) }
@EventHandler
fun onQuit( event: PlayerQuitEvent ) { disableHitCooldown( event.player ) }
private fun disableHitCooldown(
player: Player
) {
val attackSpeed = player.getAttribute( Attribute.GENERIC_ATTACK_SPEED )
if ( attackSpeed != null ) attackSpeed.baseValue = 40.0
}
private val lapisLazuli = Material.LAPIS_LAZULI private val lapisLazuli = Material.LAPIS_LAZULI
@EventHandler @EventHandler

View File

@@ -21,6 +21,10 @@ anti-runner:
ignore-vertical-distance: 15.0 # Wenn Höhenunterschied > 15, Timer ignorieren ignore-vertical-distance: 15.0 # Wenn Höhenunterschied > 15, Timer ignorieren
ignore-cave-surface-mix: true # Ignorieren, wenn einer Sonne hat und der andere nicht ignore-cave-surface-mix: true # Ignorieren, wenn einer Sonne hat und der andere nicht
lunarclient:
name: 'McScrims Network'
variantName: 'SpeedHG - Solo'
teams: teams:
enabled: true enabled: true
max-size: 2 max-size: 2

View File

@@ -5,16 +5,20 @@ api-version: '1.21'
depend: depend:
- "WorldEdit" - "WorldEdit"
- "Apollo-Bukkit"
permissions: permissions:
speedhg.bypass: speedhg.bypass:
description: 'Allows joining the server while its running' description: 'Allows joining the server while its running'
default: false default: false
speedhg.admin.timer: speedhg.admin.timer:
description: 'Change the current game time' description: 'Allows changing the game timer'
default: false default: op
speedhg.admin.ranking: speedhg.admin.ranking:
description: 'Manage the ranking system (toggle unranked mode, inspect ranks)' description: 'Allows managing the ranking system'
default: op
speedhg.admin.staff:
description: 'Staff permission for Lunar Client'
default: false default: false
commands: commands:
@@ -25,11 +29,11 @@ commands:
description: 'View the top 10 players' description: 'View the top 10 players'
usage: '/leaderboard' usage: '/leaderboard'
timer: timer:
description: 'Change the current game time (Admin Command)' description: 'Change the current game time (Admin)'
usage: '/timer <seconds>' usage: '/timer <time>'
permission: speedhg.admin.timer permission: speedhg.admin.timer
ranking: ranking:
description: 'Manage the SpeedHG ranking system' description: 'Manage the ranking system'
usage: '/ranking <toggle|status|rank [player]>' usage: '/ranking <toggle|status|rank [player]>'
permission: speedhg.admin.ranking permission: speedhg.admin.ranking
perks: perks: