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:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -121,15 +121,18 @@ class FeastManager(
|
||||
for ( i in 0 until CHEST_COUNT )
|
||||
{
|
||||
val angle = i * ( 2.0 * Math.PI / CHEST_COUNT )
|
||||
val cx = centerLoc.blockX + round(cos( angle ) * CHEST_ORBIT ).toInt()
|
||||
val cz = centerLoc.blockZ + round(sin( angle ) * CHEST_ORBIT ).toInt()
|
||||
val cx = centerLoc.blockX + round( cos( angle ) * CHEST_ORBIT ).toInt()
|
||||
val cz = centerLoc.blockZ + round( sin( angle ) * CHEST_ORBIT ).toInt()
|
||||
|
||||
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 )
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -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)
|
||||
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 ]!!
|
||||
|
||||
if (!hasInventorySpace( player ))
|
||||
// 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user