Add 1.8 knockback + various gameplay fixes

Introduce a Paper EntityKnockback-based KnockbackListener implementing classic 1.8-style horizontal knockback (configurable via kits.knockback.extras) and register it in the main plugin.

Other changes:
- Reduce default freeze duration from 200 to 60 ticks in CustomGameSettings and TheWorldKit (3s). Remove the per-freeze hit cap and related logic from TheWorldKit; freezes are now purely timer-based and the FrozenData no longer tracks hitsRemaining. Refactor freeze task/timer logic and cleanup unused imports.
- Fix FeastManager chest creation race by reading chest BlockState on the next tick (schedule runTaskLater) before filling/updating the chest.
- Refactor GameStateListener block handling: always-block diamond ore, block iron before feast, restrict allowed blocks during INVINCIBILITY to a whitelist, and unify INGAME pickup behavior to directly add drops to player inventory. Use block.getDrops(tool, player) to preserve correct vanilla drop context (fortune/silk/age). Rework pickupBlock to use those drops and ensure sound selection logic remains correct.
- Add mushroom harvesting handling: track active mushroom breaker players and handle mushroom chain physics in BlockPhysicsEvent to attribute drops to the correct player (activeMushroomBreaker map + monitor handler). Remove item-despawn cancellation logic.

These changes address physics/race bugs, improve combat feel (1.8 KB), simplify freeze behavior for The World kit, and ensure drop attribution and vanilla drop mechanics are preserved.
This commit is contained in:
TDSTOS
2026-04-13 00:38:32 +02:00
parent 753aeb6dc1
commit bcfe42b1a3
6 changed files with 351 additions and 91 deletions

View File

@@ -1,6 +1,7 @@
package club.mcscrims.speedhg
import club.mcscrims.speedhg.client.LunarClientManager
import club.mcscrims.speedhg.combat.KnockbackListener
import club.mcscrims.speedhg.command.HelpCommand
import club.mcscrims.speedhg.command.KitCommand
import club.mcscrims.speedhg.command.LeaderboardCommand
@@ -312,6 +313,7 @@ class SpeedHG : JavaPlugin() {
pm.registerEvents( TeamListener(), this )
pm.registerEvents( lobbyItemManager, this )
pm.registerEvents(ChatListener( this, VolcanoServerRankProvider() ), this )
pm.registerEvents(KnockbackListener( this ), this )
}
private fun registerRecipes()

View File

@@ -0,0 +1,242 @@
package club.mcscrims.speedhg.combat
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.game.GameState
import io.papermc.paper.event.entity.EntityKnockbackEvent
import org.bukkit.Bukkit
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
import org.bukkit.event.Listener
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.util.Vector
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.cos
import kotlin.math.sin
/**
* ## KnockbackListener
*
* Ersetzt das Standard-1.9+-Knockback durch klassisches 1.8-Knockback:
* horizontal-fokussiert, kombo-freundlich und W-Tap/Sprint-Reset-sensitiv.
*
* ## Warum 1.9+ sich falsch anfühlt
* Post-1.9-Knockback schleudert Opfer stark nach oben. Das unterbricht Kombos,
* weil das Opfer außer Reichweite fliegt bevor der Angreifer nachhaken kann.
* 1.8 behielt Knockback fast rein horizontal — das Opfer bleibt am Boden
* und der Angreifer kann direkt nachsetzen.
*
* ## Two-Event-Pattern
* `EntityKnockbackEvent` (Paper) kennt **keinen Angreifer** — `event.cause`
* ist ein Enum (`Cause.ENTITY_ATTACK`), keine Entity-Referenz.
* Der Angreifer wird deshalb in einem separaten `EntityDamageByEntityEvent`
* (LOWEST-Priority, läuft **vor** dem Knockback-Event) gecacht und in
* `lastAttacker` gespeichert. `onKnockback` liest ihn daraus.
*
* ```
* EntityDamageByEntityEvent (LOWEST)
* └─ lastAttacker[victim] = attacker ← Cache befüllen
*
* EntityKnockbackEvent (HIGH)
* └─ lastAttacker[victim] ← Cache auslesen
* └─ isCancelled = true ← Vanilla-KB unterdrücken
* └─ victim.velocity = buildKnockbackVector() ← 1.8-KB anwenden
* ```
*
* ## Konfiguration (`extras` unter Key `"knockback"` in `SPEEDHG_CUSTOM_SETTINGS`)
* ```json
* {
* "kits": {
* "knockback": {
* "extras": {
* "kb_horizontal": 0.4,
* "kb_vertical": 0.35,
* "kb_sprint_multiplier": 1.5
* }
* }
* }
* }
* ```
* Fehlende Keys fallen auf die Defaults im [companion object] zurück.
*
* ## Prioritätenstrategie
* | Handler | Priorität | ignoreCancelled | Grund |
* |---------------|-----------|-----------------|-----------------------------------------------------|
* | [onDamage] | LOWEST | false | Läuft als Erstes — Cache befüllen bevor KB feuert |
* | [onKnockback] | HIGH | true | Überschreibt Vanilla-KB, Kit-Abilities laufen davor |
*/
class KnockbackListener(
private val plugin: SpeedHG
) : Listener {
companion object {
/** Einzel-Settings-Key — Entries liegen unter `kits.knockback.extras`. */
private const val SETTINGS_KEY = "knockback"
/** Basis-Horizontalkraft — entspricht klassischem 1.8-Feeling. */
const val DEFAULT_KB_HORIZONTAL: Double = 0.4
/**
* Vertikalkraft.
* Bewusst niedrig gehalten — Opfer sollen nicht aufsteigen,
* damit Kombos am Boden bleiben.
*/
const val DEFAULT_KB_VERTICAL: Double = 0.35
/**
* Multiplikator wenn der Angreifer sprintet.
* Belohnt W-Tapping und Sprint-Resets mit mehr Schubkraft.
*/
const val DEFAULT_KB_SPRINT_MULTIPLIER: Double = 1.5
}
// ── Angreifer-Cache ───────────────────────────────────────────────────────
/**
* Speichert pro Opfer-UUID die UUID des letzten Angreifers.
*
* Wird in [onDamage] (LOWEST) befüllt und in [onKnockback] (HIGH)
* ausgelesen. Beide Events feuern im gleichen Tick synchron auf dem
* Main-Thread — [ConcurrentHashMap] schützt gegen etwaige parallele
* Scheduler-Tasks oder Plugin-Threads die ggf. auf die Map zugreifen.
*/
private val lastAttacker: ConcurrentHashMap<UUID, UUID> = ConcurrentHashMap()
// ── Live-Config-Accessors ─────────────────────────────────────────────────
private val extras
get() = plugin.customGameManager.settings.kits.kits[ SETTINGS_KEY ]
private val kbHorizontal: Double
get() = extras?.getDouble( "kb_horizontal" ) ?: DEFAULT_KB_HORIZONTAL
private val kbVertical: Double
get() = extras?.getDouble( "kb_vertical" ) ?: DEFAULT_KB_VERTICAL
private val kbSprintMultiplier: Double
get() = extras?.getDouble( "kb_sprint_multiplier" ) ?: DEFAULT_KB_SPRINT_MULTIPLIER
// =========================================================================
// Phase 1 — Angreifer cachen (LOWEST, vor dem Knockback-Event)
// =========================================================================
/**
* Läuft mit LOWEST-Priority, also als allererstes aller Damage-Handler.
*
* `ignoreCancelled = false` ist bewusst gesetzt: Auch wenn ein anderer
* Handler das Event später cancelt (z.B. Invincibility), wollen wir den
* Cache trotzdem befüllen — [onKnockback] arbeitet sonst mit einem
* veralteten Eintrag aus dem vorherigen Treffer.
*/
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = false)
fun onDamage(
event: EntityDamageByEntityEvent
) {
val attacker = event.damager as? Player ?: return
val victim = event.entity as? Player ?: return
if ( !isIngame() ) return
lastAttacker[ victim.uniqueId ] = attacker.uniqueId
}
// =========================================================================
// Phase 2 — 1.8-Knockback anwenden (HIGH, nach Kit-Abilities)
// =========================================================================
/**
* Feuert wenn Paper kurz davor ist, Knockback auf eine Entity anzuwenden.
*
* Wir canceln das Event (unterdrückt Vanilla-1.9+-KB) und setzen stattdessen
* unsere eigene, horizontal-fokussierte Velocity.
*
* HIGH-Priority stellt sicher, dass Kit-Abilities (z.B. Black Panther Push)
* auf NORMAL bereits gelaufen sind und wir deren Ergebnis nicht überschreiben.
*/
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
fun onKnockback(
event: EntityKnockbackEvent
) {
if ( !isIngame() ) return
if ( event.cause != EntityKnockbackEvent.Cause.ENTITY_ATTACK ) return
val victim = event.entity as? Player ?: return
if ( !plugin.gameManager.alivePlayers.contains( victim.uniqueId ) ) return
// ── Angreifer aus Cache lesen ─────────────────────────────────────────
val attackerUUID = lastAttacker[ victim.uniqueId ] ?: return
val attacker = Bukkit.getPlayer( attackerUUID ) ?: return
// ── Vanilla-Knockback unterdrücken ────────────────────────────────────
event.isCancelled = true
// ── 1.8-Velocity berechnen und setzen ────────────────────────────────
val kb = buildKnockbackVector( attacker )
val current = victim.velocity
// Vertikale Velocity ist additiv wie in 1.8: ein laufender Sprung wird
// nicht abrupt gestoppt, sondern der KB-Anteil addiert sich oben drauf.
// coerceAtMost(0.4) verhindert dass gestapelte Hits ins Skybase-Terrain schießen.
victim.velocity = Vector(
kb.x,
( current.y + kb.y ).coerceAtMost( 0.4 ),
kb.z
)
}
// =========================================================================
// Knockback-Mathematik
// =========================================================================
/**
* Berechnet den 1.8-Knockback-[Vector] anhand des Angreifers.
*
* ## Richtung: Yaw-basiert, nicht positions-basiert
* In 1.8 wurde die Knockback-Richtung vom **Yaw des Angreifers** abgeleitet
* (wohin er schaut), nicht vom Vektor zwischen Angreifer und Opfer.
* Das ist der Kern des W-Tap-Gefühls: Wenn der Angreifer beim Schlag
* genau auf das Opfer zielt, trifft der KB punktgenau nach vorne.
* Sprint-Resets (kurz S drücken, dann wieder W) lohnen sich, weil der
* Sprint-Bonus nur bei aktiv gedrücktem Sprint zählt.
*
* ## Sprint-Bonus
* Wenn [attacker] sprintet, wird [kbHorizontal] mit [kbSprintMultiplier]
* multipliziert. Das belohnt konsequentes W-Tapping gegenüber Spam-Clicking.
*
* @param attacker Der Spieler, der den Treffer ausgeführt hat.
* @return Fertiger Knockback-[Vector] bereit zur Anwendung auf das Opfer.
*/
private fun buildKnockbackVector(
attacker: Player
): Vector
{
// Minecraft-Koordinatensystem: Yaw 0° = Süden (+Z), 90° = Westen (-X)
val yawRad = Math.toRadians( attacker.location.yaw.toDouble() )
val dirX = -sin( yawRad )
val dirZ = cos( yawRad )
val horizontal = kbHorizontal * ( if ( attacker.isSprinting ) kbSprintMultiplier else 1.0 )
return Vector(
dirX * horizontal,
kbVertical,
dirZ * horizontal
)
}
// =========================================================================
// Hilfsmethoden
// =========================================================================
private fun isIngame(): Boolean = when ( plugin.gameManager.currentState )
{
GameState.INGAME, GameState.INVINCIBILITY -> true
else -> false
}
}

View File

@@ -134,8 +134,7 @@ data class CustomGameSettings(
@SerialName("shockwave_radius") val shockwaveRadius: Double = 6.0,
@SerialName("teleport_range") val teleportRange: Double = 10.0,
@SerialName("max_teleport_charges") val maxTeleportCharges: Int = 3,
@SerialName("freeze_duration_ticks") val freezeDurationTicks: Int = 200,
@SerialName("max_hits_on_frozen") val maxHitsOnFrozen: Int = 5,
@SerialName("freeze_duration_ticks") val freezeDurationTicks: Int = 60,
/**
* Generische Erweiterungs-Map für kit-spezifische Einstellungen, die

View File

@@ -126,10 +126,13 @@ class FeastManager(
val chestBlock = world.getBlockAt( cx, platformY + 1, cz )
chestBlock.type = Material.CHEST
( chestBlock.state as? Chest )?.let { chest ->
// State erst im nächsten Tick lesen — Block-Commit braucht einen Tick
Bukkit.getScheduler().runTaskLater( plugin, { ->
val chest = chestBlock.state as? Chest ?: return@runTaskLater
fillChestWithLoot( chest )
chest.update( true )
}
}, 1L )
}
}, 5L )

View File

@@ -13,7 +13,6 @@ import club.mcscrims.speedhg.util.trans
import net.kyori.adventure.text.Component
import org.bukkit.*
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
@@ -82,7 +81,6 @@ import kotlin.math.sin
* ### DEFENSIVE active
* Radiale Schockwelle + [applyFreeze] auf jeden nahen lebenden Gegner. Ein 1-Tick-Task
* setzt die Velocity gefrorener Spieler für [freezeDurationTicks] Ticks auf 0.
* [DefensivePassive] beendet den Freeze nach [maxHitsOnFrozen] Treffern.
*
* ### Warum hitsRequired = 0?
* Beide Aktiv-Fähigkeiten steuern intern wann [execute] feuert. Das eingebaute
@@ -110,8 +108,7 @@ class TheWorldKit : Kit() {
const val DEFAULT_SHOCKWAVE_RADIUS = 6.0
const val DEFAULT_TELEPORT_RANGE = 10.0
const val DEFAULT_MAX_TELEPORT_CHARGES = 3
const val DEFAULT_FREEZE_DURATION_TICKS = 200
const val DEFAULT_MAX_HITS_ON_FROZEN = 5
const val DEFAULT_FREEZE_DURATION_TICKS = 60 // ← 3 Sekunden statt 200
const val DEFAULT_SHOCKWAVE_KNOCKBACK_SPEED = 2.0
const val DEFAULT_SHOCKWAVE_KNOCKBACK_Y = 0.45
const val DEFAULT_BLINK_STEP_SIZE = 0.4
@@ -161,13 +158,6 @@ class TheWorldKit : Kit() {
private val freezeDurationTicks: Int
get() = override().freezeDurationTicks
/**
* Maximale Anzahl an Treffern gegen einen gefrorenen Gegner.
* Quelle: typisiertes Feld `tw_max_hits_on_frozen`.
*/
private val maxHitsOnFrozen: Int
get() = override().maxHitsOnFrozen
/**
* Horizontaler Velocity-Multiplikator der Schockwelle.
* Quelle: `extras["shockwave_knockback_speed"]`.
@@ -227,7 +217,6 @@ class TheWorldKit : Kit() {
internal val frozenEnemies: MutableMap<UUID, Pair<UUID, FrozenData>> = ConcurrentHashMap()
data class FrozenData(
var hitsRemaining: Int,
val task: BukkitTask
)
@@ -278,12 +267,11 @@ class TheWorldKit : Kit() {
) {
teleportCharges.remove( player.uniqueId )
// Alle vom verlassenden Spieler verursachten Freezes auftauen
frozenEnemies.entries
.filter { ( _, pair ) -> pair.first == player.uniqueId }
.map { ( victimUUID, pair ) -> victimUUID to pair.second }
.forEach { ( victimUUID, data ) ->
data.task.cancel()
data.task.cancel() // ← identisch, funktioniert weiterhin
frozenEnemies.remove( victimUUID )
Bukkit.getPlayer( victimUUID )?.clearFreezeEffects()
}
@@ -386,7 +374,7 @@ class TheWorldKit : Kit() {
}
/**
* Immobilisiert [target] und begrenzt Treffer von [attacker] auf [maxHitsOnFrozen].
* Immobilisiert [target] und beendet den Freeze nach [freezeDurationTicks].
* Ein 1-Tick-Task setzt horizontale + aufwärts gerichtete Velocity auf 0.
*
* Konfigurationswerte werden zum Aktivierungszeitpunkt gesnapshot, damit
@@ -396,13 +384,11 @@ class TheWorldKit : Kit() {
attacker: Player,
target: Player
) {
// Vorhandenen Freeze überschreiben
frozenEnemies.remove( target.uniqueId )?.second?.task?.cancel()
val capturedDurationTicks = freezeDurationTicks
val capturedRefreshTicks = freezeRefreshTicks
val capturedPowderSnowTicks = freezePowderSnowTicks
val capturedMaxHits = maxHitsOnFrozen
target.applyFreezeEffects()
@@ -423,16 +409,13 @@ class TheWorldKit : Kit() {
return
}
// Horizontale + aufwärts Velocity jedes Tick nullen
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 }
// Slowness jede Sekunde refreshen damit sie nicht ausläuft
if ( ticks % capturedRefreshTicks == 0 )
target.applyFreezeEffects()
// Powder-Snow-Visuals (rein kosmetisch)
if ( target.freezeTicks < capturedPowderSnowTicks )
target.freezeTicks = capturedPowderSnowTicks
}
@@ -440,7 +423,7 @@ class TheWorldKit : Kit() {
frozenEnemies[ target.uniqueId ] = Pair(
attacker.uniqueId,
FrozenData( hitsRemaining = capturedMaxHits, task = task )
FrozenData( task = task ) // ← kein hitsRemaining mehr
)
target.sendActionBar( target.trans( "kits.theworld.messages.frozen_received" ) )
@@ -630,10 +613,10 @@ class TheWorldKit : Kit() {
}
// =========================================================================
// DEFENSIVE passive 5-Treffer-Cap auf gefrorene Gegner
// DEFENSIVE no-passive
// =========================================================================
private inner class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE ) {
private class DefensivePassive : PassiveAbility( Playstyle.DEFENSIVE ) {
private val plugin get() = SpeedHG.instance
@@ -646,35 +629,7 @@ class TheWorldKit : Kit() {
"kits.theworld.passive.defensive.description"
)
/**
* Wird nur aufgerufen wenn der TheWorld-Spieler (Angreifer) jemanden trifft.
* Wenn das Opfer vom gleichen Angreifer eingefroren wurde, Treffer-Cap dekrementieren.
*/
override fun onHitEnemy(
attacker: Player,
victim: Player,
event: EntityDamageByEntityEvent
) {
val ( frozenBy, data ) = frozenEnemies[ victim.uniqueId ] ?: return
// Nur Treffer vom Spieler zählen, der den Freeze ausgelöst hat
if ( frozenBy != attacker.uniqueId ) return
data.hitsRemaining--
if ( data.hitsRemaining <= 0 )
{
doUnfreeze( victim )
attacker.sendActionBar( attacker.trans( "kits.theworld.messages.freeze_broken" ) )
}
else
{
attacker.sendActionBar(attacker.trans(
"kits.theworld.messages.freeze_hits_left",
mapOf( "hits" to data.hitsRemaining.toString() )
))
}
}
// Hit-Cap entfernt — Freeze endet jetzt rein timer-basiert nach 3 Sekunden
}
// =========================================================================

View File

@@ -3,14 +3,17 @@ package club.mcscrims.speedhg.listener
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.game.GameState
import club.mcscrims.speedhg.util.sendMsg
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.Sound
import org.bukkit.attribute.Attribute
import org.bukkit.entity.Player
import org.bukkit.event.Event
import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
import org.bukkit.event.Listener
import org.bukkit.event.block.BlockBreakEvent
import org.bukkit.event.block.BlockPhysicsEvent
import org.bukkit.event.block.BlockPlaceEvent
import org.bukkit.event.block.LeavesDecayEvent
import org.bukkit.event.enchantment.EnchantItemEvent
@@ -24,6 +27,7 @@ import org.bukkit.inventory.EnchantingInventory
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.Damageable
import java.util.*
import java.util.concurrent.ConcurrentHashMap
class GameStateListener : Listener {
@@ -90,36 +94,42 @@ class GameStateListener : Listener {
val block = event.block
if ( gameManager.currentState == GameState.INVINCIBILITY &&
beforeInvisMaterials.containsKey( block.type ))
{
pickupBlock( event, player )
return
}
if (alwaysMaterials.containsKey( block.type ))
{
pickupBlock( event, player )
return
}
// Diamant-Erz: immer blockieren
if ( block.type == Material.DIAMOND_ORE )
{
event.isCancelled = true
event.block.type = Material.AIR
event.block.tick()
player.sendMsg( "build.no_diamonds" )
player.playSound( player.location, Sound.ENTITY_VILLAGER_NO, 1f, 1f )
return
}
// Eisen-Erz: vor Feast blockieren
if ( block.type == Material.IRON_ORE && !feastStarted )
{
event.isCancelled = true
player.sendMsg( "build.no_iron_before_feast" )
player.playSound( player.location, Sound.ENTITY_VILLAGER_NO, 1f, 1f )
return
}
// Invincibility: nur Whitelist-Blöcke erlaubt
if ( gameManager.currentState == GameState.INVINCIBILITY )
{
if ( beforeInvisMaterials.containsKey( block.type ) ||
alwaysMaterials.containsKey( block.type ) )
{
pickupBlock( event, player )
} else {
event.isCancelled = true
}
return
}
// INGAME: alle Blöcke direkt in Inventar — nie auf den Boden fallen lassen
// (ItemDespawnEvent cancelt sonst alle Drops sofort)
pickupBlock( event, player )
}
private fun pickupBlock(
@@ -128,21 +138,34 @@ class GameStateListener : Listener {
) {
val block = event.block
// In pickupBlock, direkt vor event.isCancelled = true:
if ( block.type == Material.RED_MUSHROOM ||
block.type == Material.BROWN_MUSHROOM )
{
activeMushroomBreaker[ player.uniqueId ] = player
Bukkit.getScheduler().runTask( plugin ) { ->
activeMushroomBreaker.remove( player.uniqueId )
}
}
event.isCancelled = true
val sound = if (beforeInvisMaterials.containsKey( block.type ))
beforeInvisMaterials[ block.type ]!!
else alwaysMaterials[ block.type ]!!
val sound = beforeInvisMaterials[ block.type ]
?: alwaysMaterials[ block.type ]!!
// block.getDrops( tool, player ) gibt den korrekten Vanilla-Drop-Context
// inkl. Age-State bei Cocoa, Fortune-Enchant, Silk-Touch usw.
val drops = block.getDrops( player.inventory.itemInMainHand, player )
if ( !hasInventorySpace( player ) )
{
block.drops.forEach { player.world.dropItem( block.location, it ) }
drops.forEach { player.world.dropItem( block.location, it ) }
player.playSound( player.location, sound, 1f, 1f )
block.type = Material.AIR
return
}
block.drops.forEach { player.inventory.addItem( it ) }
drops.forEach { player.inventory.addItem( it ) }
player.playSound( player.location, sound, 1f, 1f )
block.type = Material.AIR
}
@@ -150,6 +173,49 @@ class GameStateListener : Listener {
private fun hasInventorySpace( player: Player ): Boolean
= player.inventory.any { it == null || it.type == Material.AIR }
/**
* Tracks welcher Spieler gerade einen Pilz abbaut.
* UUID des Spielers → Location des abgebauten Blocks.
* Wird in onBlockBreak gesetzt und in onBlockPhysics konsumiert.
*
* Notwendig damit der Physics-Handler weiß, wessen Inventory die
* Kettenreaktion-Drops bekommen soll.
*/
private val activeMushroomBreaker: MutableMap<UUID, Player> = ConcurrentHashMap()
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = false)
fun onBlockPhysics(
event: BlockPhysicsEvent
) {
val block = event.block
if ( block.type != Material.RED_MUSHROOM &&
block.type != Material.BROWN_MUSHROOM ) return
if ( gameManager.currentState != GameState.INVINCIBILITY &&
gameManager.currentState != GameState.INGAME ) return
// Einen Spieler in der Nähe finden der gerade einen Pilz abbaut
val harvester = activeMushroomBreaker.values
.firstOrNull { it.location.distanceSquared( block.location ) <= 25.0 }
?: return // Keine aktive Spieleraktion — normales Physik-Event, ignorieren
// Block vor dem Pop einsammeln
event.isCancelled = true
val drops = block.getDrops( harvester.inventory.itemInMainHand, harvester )
if ( !hasInventorySpace( harvester ) )
{
drops.forEach { harvester.world.dropItem( block.location, it ) }
}
else
{
drops.forEach { harvester.inventory.addItem( it ) }
}
block.type = Material.AIR
}
@EventHandler
fun onDropItem(
event: PlayerDropItemEvent
@@ -190,13 +256,6 @@ class GameStateListener : Listener {
player.playSound( player.location, Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f )
}
@EventHandler
fun onItemDespawn(
event: ItemDespawnEvent
) {
event.isCancelled = true
}
private val swordNerf = 0.75
private val otherNerf = 0.4