Add perk system with GUI and integrations
Introduce a complete passive perk system: Perk base class, PerkManager (registration, selection, lifecycle, persistence), and PlayerPerksRepository (DB schema + upsert/find). Add four example perks (Oracle, Vampire, Featherweight, Bloodlust) and a single PerkEventDispatcher to route combat/environment/kill events to active perks. Provide PerkSelectorMenu GUI and /perks command, integrate perk initialization, registration, application and cleanup into SpeedHG and GameManager, and hook load/evict into StatsListener. Also add language entries and register the command in plugin.yml. This change enables players to select up to two passive perks, persists selections, and dispatches relevant events to perk implementations.
This commit is contained in:
@@ -2,6 +2,7 @@ package club.mcscrims.speedhg
|
||||
|
||||
import club.mcscrims.speedhg.command.KitCommand
|
||||
import club.mcscrims.speedhg.command.LeaderboardCommand
|
||||
import club.mcscrims.speedhg.command.PerksCommand
|
||||
import club.mcscrims.speedhg.command.RankingCommand
|
||||
import club.mcscrims.speedhg.command.TimerCommand
|
||||
import club.mcscrims.speedhg.config.CustomGameManager
|
||||
@@ -20,6 +21,12 @@ import club.mcscrims.speedhg.listener.ConnectListener
|
||||
import club.mcscrims.speedhg.listener.GameStateListener
|
||||
import club.mcscrims.speedhg.listener.SoupListener
|
||||
import club.mcscrims.speedhg.listener.StatsListener
|
||||
import club.mcscrims.speedhg.perk.PerkManager
|
||||
import club.mcscrims.speedhg.perk.impl.BloodlustPerk
|
||||
import club.mcscrims.speedhg.perk.impl.FeatherweightPerk
|
||||
import club.mcscrims.speedhg.perk.impl.OraclePerk
|
||||
import club.mcscrims.speedhg.perk.impl.VampirePerk
|
||||
import club.mcscrims.speedhg.perk.listener.PerkEventDispatcher
|
||||
import club.mcscrims.speedhg.ranking.RankingManager
|
||||
import club.mcscrims.speedhg.scoreboard.ScoreboardManager
|
||||
import club.mcscrims.speedhg.webhook.DiscordWebhookManager
|
||||
@@ -55,6 +62,9 @@ class SpeedHG : JavaPlugin() {
|
||||
lateinit var kitManager: KitManager
|
||||
private set
|
||||
|
||||
lateinit var perkManager: PerkManager
|
||||
private set
|
||||
|
||||
lateinit var databaseManager: DatabaseManager
|
||||
private set
|
||||
|
||||
@@ -111,7 +121,11 @@ class SpeedHG : JavaPlugin() {
|
||||
kitManager = KitManager( this )
|
||||
discordWebhookManager = DiscordWebhookManager( this )
|
||||
|
||||
perkManager = PerkManager( this )
|
||||
perkManager.initialize()
|
||||
|
||||
registerKits()
|
||||
registerPerks()
|
||||
registerCommands()
|
||||
registerListener()
|
||||
registerRecipes()
|
||||
@@ -122,6 +136,7 @@ class SpeedHG : JavaPlugin() {
|
||||
override fun onDisable()
|
||||
{
|
||||
podiumManager.cleanup()
|
||||
if ( ::perkManager.isInitialized ) perkManager.shutdown()
|
||||
if ( ::statsManager.isInitialized ) statsManager.shutdown()
|
||||
if ( ::databaseManager.isInitialized ) databaseManager.disconnect()
|
||||
kitManager.clearAll()
|
||||
@@ -142,6 +157,14 @@ class SpeedHG : JavaPlugin() {
|
||||
kitManager.registerKit( VoodooKit() )
|
||||
}
|
||||
|
||||
private fun registerPerks()
|
||||
{
|
||||
perkManager.registerPerk( OraclePerk() )
|
||||
perkManager.registerPerk( VampirePerk() )
|
||||
perkManager.registerPerk( FeatherweightPerk() )
|
||||
perkManager.registerPerk( BloodlustPerk() )
|
||||
}
|
||||
|
||||
private fun registerCommands()
|
||||
{
|
||||
val kitCommand = KitCommand()
|
||||
@@ -163,6 +186,7 @@ class SpeedHG : JavaPlugin() {
|
||||
}
|
||||
|
||||
getCommand( "leaderboard" )?.setExecutor( LeaderboardCommand() )
|
||||
getCommand( "perks" )?.setExecutor( PerksCommand() )
|
||||
}
|
||||
|
||||
private fun registerListener()
|
||||
@@ -175,6 +199,7 @@ class SpeedHG : JavaPlugin() {
|
||||
pm.registerEvents(KitEventDispatcher( this, kitManager ), this )
|
||||
pm.registerEvents( StatsListener(), this )
|
||||
pm.registerEvents( MenuListener(), this )
|
||||
pm.registerEvents(PerkEventDispatcher( this, perkManager ), this )
|
||||
}
|
||||
|
||||
private fun registerRecipes()
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package club.mcscrims.speedhg.command
|
||||
|
||||
import club.mcscrims.speedhg.SpeedHG
|
||||
import club.mcscrims.speedhg.gui.menu.PerkSelectorMenu
|
||||
import org.bukkit.command.Command
|
||||
import org.bukkit.command.CommandExecutor
|
||||
import org.bukkit.command.CommandSender
|
||||
import org.bukkit.entity.Player
|
||||
|
||||
class PerksCommand : CommandExecutor {
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
override fun onCommand(
|
||||
sender: CommandSender,
|
||||
command: Command,
|
||||
label: String,
|
||||
args: Array<out String>
|
||||
): Boolean {
|
||||
val player = sender as? Player ?: run {
|
||||
sender.sendMessage("§cNur Spieler können diesen Befehl nutzen.")
|
||||
return true
|
||||
}
|
||||
|
||||
PerkSelectorMenu(player).open(player)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -200,6 +200,7 @@ class GameManager(
|
||||
teleportRandomly( player, world, startBorder / 2 )
|
||||
|
||||
plugin.kitManager.applyKit( player ) // verteilt Items + ruft onAssign + passive.onActivate
|
||||
plugin.perkManager.applyPerks( player )
|
||||
|
||||
player.inventory.addItem(ItemStack( Material.COMPASS ))
|
||||
player.sendMsg( "game.started" )
|
||||
@@ -304,6 +305,7 @@ class GameManager(
|
||||
}
|
||||
|
||||
plugin.kitManager.clearAll()
|
||||
plugin.perkManager.removeAllActivePerks()
|
||||
|
||||
Bukkit.getOnlinePlayers().forEach { p ->
|
||||
p.showTitle(Title.title(
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
package club.mcscrims.speedhg.gui.menu
|
||||
|
||||
import club.mcscrims.speedhg.SpeedHG
|
||||
import club.mcscrims.speedhg.game.GameState
|
||||
import club.mcscrims.speedhg.perk.Perk
|
||||
import club.mcscrims.speedhg.util.trans
|
||||
import net.kyori.adventure.text.Component
|
||||
import net.kyori.adventure.text.format.NamedTextColor
|
||||
import net.kyori.adventure.text.format.TextDecoration
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.enchantments.Enchantment
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.inventory.InventoryClickEvent
|
||||
import org.bukkit.inventory.Inventory
|
||||
import org.bukkit.inventory.ItemFlag
|
||||
import org.bukkit.inventory.ItemStack
|
||||
|
||||
/**
|
||||
* Perk-Selector-GUI (3 Reihen × 9 = 27 Slots).
|
||||
*
|
||||
* ## Layout
|
||||
* ```
|
||||
* [ P ][ P ][ P ][ P ][ P ][ P ][ P ][ P ][ P ] Reihe 1: Perks (Slots 0–8)
|
||||
* [ P ][ P ][ P ][ P ][ P ][ P ][ P ][ P ][ P ] Reihe 2: Perks (Slots 9–17)
|
||||
* [ F ][ F ][ F ][ F ][ F ][ F ][ F ][ F ][ F ] Reihe 3: Info (Slots 18–26)
|
||||
* ↑ Slot 20 ↑ Slot 24 ↑ Slot 26
|
||||
* S1 (Perk) S2 (Perk) X (Schließen)
|
||||
* ```
|
||||
*
|
||||
* ## Klick-Verhalten
|
||||
* - **Linksklick auf Perk**: Ausrüsten wenn Slot frei; Abwählen wenn bereits aktiv; Fehler wenn voll
|
||||
* - **Rechtsklick auf Perk**: Identisch zu Linksklick (Toggle-Logik)
|
||||
* - **Klick auf Slot-Indikator (S1/S2)**: Deaktiviert den jeweiligen Perk
|
||||
* - **Klick auf X**: Inventar schließen
|
||||
*/
|
||||
class PerkSelectorMenu(
|
||||
private val player: Player
|
||||
) : Menu(
|
||||
rows = 3,
|
||||
title = player.trans("gui.perk_selector.title")
|
||||
) {
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
private val mm = MiniMessage.miniMessage()
|
||||
private val lm get() = plugin.languageManager
|
||||
|
||||
private val PERK_SLOTS = 0 until 18
|
||||
private val FILLER_ROW = 18 until 27
|
||||
private val SLOT_FIRST = 20 // Indikator: Perk-Slot 1
|
||||
private val SLOT_SECOND = 24 // Indikator: Perk-Slot 2
|
||||
private val SLOT_CLOSE = 26 // Schließen-Button
|
||||
|
||||
// ── Aufbau ────────────────────────────────────────────────────────────────
|
||||
|
||||
override fun build(): Inventory = createInventory(title).also { populate(it) }
|
||||
|
||||
private fun populate(inv: Inventory) {
|
||||
inv.clear()
|
||||
|
||||
// Filler-Basis für Reihe 3
|
||||
val filler = buildFiller()
|
||||
FILLER_ROW.forEach { inv.setItem(it, filler) }
|
||||
|
||||
// Perks in Reihen 1 + 2
|
||||
plugin.perkManager.getRegisteredPerks().forEachIndexed { index, perk ->
|
||||
if (index in PERK_SLOTS) inv.setItem(index, buildPerkItem(perk))
|
||||
}
|
||||
|
||||
// Ausgerüstete Perks als Slot-Indikatoren
|
||||
val selected = plugin.perkManager.getSelectedPerks(player)
|
||||
inv.setItem(SLOT_FIRST, buildSlotIndicator(perk = selected.getOrNull(0), slotNumber = 1))
|
||||
inv.setItem(SLOT_SECOND, buildSlotIndicator(perk = selected.getOrNull(1), slotNumber = 2))
|
||||
|
||||
inv.setItem(SLOT_CLOSE, buildCloseItem())
|
||||
}
|
||||
|
||||
// ── Klick-Handling ────────────────────────────────────────────────────────
|
||||
|
||||
override fun onClick(event: InventoryClickEvent, player: Player) {
|
||||
val slot = event.rawSlot
|
||||
if (slot !in 0 until size) return
|
||||
|
||||
when (slot) {
|
||||
SLOT_CLOSE -> player.closeInventory()
|
||||
SLOT_FIRST -> deselectIndicatorSlot(index = 0)
|
||||
SLOT_SECOND -> deselectIndicatorSlot(index = 1)
|
||||
in PERK_SLOTS -> handlePerkClick(slot)
|
||||
// Filler-Slots → bereits gecancelt durch MenuListener, nichts zu tun
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePerkClick(slot: Int) {
|
||||
// Spiel läuft? Änderungen blockieren.
|
||||
val state = plugin.gameManager.currentState
|
||||
if (state == GameState.INGAME || state == GameState.INVINCIBILITY) {
|
||||
player.sendActionBar(mm.deserialize(lm.getRawMessage(player, "gui.perk_selector.game_running")))
|
||||
return
|
||||
}
|
||||
|
||||
val perk = getPerkAtSlot(slot) ?: return
|
||||
|
||||
when {
|
||||
plugin.perkManager.hasPerk(player, perk.id) -> {
|
||||
// Bereits ausgerüstet → abwählen
|
||||
plugin.perkManager.deselectPerk(player, perk)
|
||||
player.sendActionBar(mm.deserialize(
|
||||
lm.getRawMessage(player, "gui.perk_selector.deselected"),
|
||||
Placeholder.component("perk", perk.displayName)
|
||||
))
|
||||
refresh()
|
||||
}
|
||||
|
||||
plugin.perkManager.isSlotsFull(player) -> {
|
||||
// Kein freier Slot
|
||||
player.sendActionBar(mm.deserialize(lm.getRawMessage(player, "gui.perk_selector.slots_full")))
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Freier Slot → ausrüsten
|
||||
plugin.perkManager.selectPerk(player, perk)
|
||||
player.sendActionBar(mm.deserialize(
|
||||
lm.getRawMessage(player, "gui.perk_selector.selected"),
|
||||
Placeholder.component("perk", perk.displayName)
|
||||
))
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deselectIndicatorSlot(index: Int) {
|
||||
val perk = plugin.perkManager.getSelectedPerks(player).getOrNull(index) ?: return
|
||||
plugin.perkManager.deselectPerk(player, perk)
|
||||
player.sendActionBar(mm.deserialize(
|
||||
lm.getRawMessage(player, "gui.perk_selector.deselected"),
|
||||
Placeholder.component("perk", perk.displayName)
|
||||
))
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun refresh() = populate(inventory)
|
||||
|
||||
// ── Item-Builder ──────────────────────────────────────────────────────────
|
||||
|
||||
private fun buildPerkItem(perk: Perk): ItemStack {
|
||||
val isEquipped = plugin.perkManager.hasPerk(player, perk.id)
|
||||
val item = ItemStack(perk.icon)
|
||||
|
||||
item.editMeta { meta ->
|
||||
meta.displayName(perk.displayName.decoration(TextDecoration.ITALIC, false))
|
||||
|
||||
val lore = buildList<Component> {
|
||||
add(separator())
|
||||
perk.lore.forEach { rawLine ->
|
||||
add(mm.deserialize(rawLine).decoration(TextDecoration.ITALIC, false))
|
||||
}
|
||||
add(Component.empty())
|
||||
if (isEquipped) {
|
||||
add(mm.deserialize(lm.getRawMessage(player, "gui.perk_selector.equipped_label"))
|
||||
.decoration(TextDecoration.ITALIC, false))
|
||||
add(mm.deserialize(lm.getRawMessage(player, "gui.perk_selector.click_deselect"))
|
||||
.decoration(TextDecoration.ITALIC, false))
|
||||
} else {
|
||||
add(mm.deserialize(lm.getRawMessage(player, "gui.perk_selector.click_equip"))
|
||||
.decoration(TextDecoration.ITALIC, false))
|
||||
}
|
||||
add(separator())
|
||||
}
|
||||
meta.lore(lore)
|
||||
|
||||
if (isEquipped) {
|
||||
meta.addEnchant(Enchantment.UNBREAKING, 1, true)
|
||||
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS)
|
||||
}
|
||||
meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_ADDITIONAL_TOOLTIP)
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
private fun buildSlotIndicator(perk: Perk?, slotNumber: Int): ItemStack {
|
||||
return if (perk == null) {
|
||||
ItemStack(Material.GRAY_STAINED_GLASS_PANE).apply {
|
||||
editMeta { meta ->
|
||||
meta.displayName(
|
||||
mm.deserialize(
|
||||
lm.getRawMessage(player, "gui.perk_selector.slot_empty"),
|
||||
Placeholder.unparsed("slot", slotNumber.toString())
|
||||
).decoration(TextDecoration.ITALIC, false)
|
||||
)
|
||||
meta.lore(listOf(
|
||||
Component.empty(),
|
||||
mm.deserialize(lm.getRawMessage(player, "gui.perk_selector.slot_hint"))
|
||||
.decoration(TextDecoration.ITALIC, false),
|
||||
Component.empty()
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ItemStack(perk.icon).apply {
|
||||
editMeta { meta ->
|
||||
// "Slot 1: <Perk-Name>"
|
||||
val slotPrefix = mm.deserialize(
|
||||
lm.getRawMessage(player, "gui.perk_selector.slot_title"),
|
||||
Placeholder.unparsed("slot", slotNumber.toString())
|
||||
)
|
||||
meta.displayName(
|
||||
slotPrefix.append(perk.displayName).decoration(TextDecoration.ITALIC, false)
|
||||
)
|
||||
meta.lore(listOf(
|
||||
Component.empty(),
|
||||
mm.deserialize(lm.getRawMessage(player, "gui.perk_selector.click_deselect"))
|
||||
.decoration(TextDecoration.ITALIC, false),
|
||||
Component.empty()
|
||||
))
|
||||
meta.addEnchant(Enchantment.UNBREAKING, 1, true)
|
||||
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS, ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_ADDITIONAL_TOOLTIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildFiller() = ItemStack(Material.GRAY_STAINED_GLASS_PANE).apply {
|
||||
editMeta { it.displayName(Component.text(" ").decoration(TextDecoration.ITALIC, false)) }
|
||||
}
|
||||
|
||||
private fun buildCloseItem() = ItemStack(Material.DARK_OAK_DOOR).apply {
|
||||
editMeta { meta ->
|
||||
meta.displayName(
|
||||
mm.deserialize(lm.getRawMessage(player, "gui.perk_selector.close"))
|
||||
.decoration(TextDecoration.ITALIC, false)
|
||||
)
|
||||
meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_ADDITIONAL_TOOLTIP)
|
||||
}
|
||||
}
|
||||
|
||||
private fun separator(): Component =
|
||||
Component.text("────────────────────", NamedTextColor.DARK_GRAY)
|
||||
.decoration(TextDecoration.ITALIC, false)
|
||||
|
||||
private fun getPerkAtSlot(slot: Int): Perk? =
|
||||
plugin.perkManager.getRegisteredPerks().getOrNull(slot)
|
||||
}
|
||||
@@ -39,6 +39,7 @@ class StatsListener : Listener {
|
||||
// Wir sind auf einem Async-Thread — runBlocking ist hier korrekt.
|
||||
runBlocking {
|
||||
plugin.statsManager.loadStats(event.uniqueId, event.name)
|
||||
plugin.perkManager.loadPerks(event.uniqueId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,5 +48,6 @@ class StatsListener : Listener {
|
||||
// MONITOR: läuft nach allen anderen Listenern — sicherstellen,
|
||||
// dass alle anderen Plugins ihre finalen Änderungen gemacht haben.
|
||||
plugin.statsManager.saveAndEvict(event.player.uniqueId)
|
||||
plugin.perkManager.evict(event.player.uniqueId)
|
||||
}
|
||||
}
|
||||
60
src/main/kotlin/club/mcscrims/speedhg/perk/Perk.kt
Normal file
60
src/main/kotlin/club/mcscrims/speedhg/perk/Perk.kt
Normal file
@@ -0,0 +1,60 @@
|
||||
package club.mcscrims.speedhg.perk
|
||||
|
||||
import net.kyori.adventure.text.Component
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent
|
||||
import org.bukkit.event.entity.EntityDamageEvent
|
||||
|
||||
/**
|
||||
* Basisklasse für alle passiven Perks in SpeedHG.
|
||||
*
|
||||
* Perks sind passive Boni, die Spieler neben ihrem Kit ausrüsten.
|
||||
* Ein Spieler darf maximal [PerkManager.MAX_PERKS] Perks gleichzeitig aktiv haben.
|
||||
*
|
||||
* ## Neuen Perk erstellen
|
||||
* 1. Diese Klasse erweitern.
|
||||
* 2. Alle abstrakten Member implementieren.
|
||||
* 3. Nur die Hooks überschreiben, die tatsächlich gebraucht werden — alle Defaults sind No-Ops.
|
||||
* 4. Via `plugin.perkManager.registerPerk(DeinPerk())` in [SpeedHG.onEnable] registrieren.
|
||||
*/
|
||||
abstract class Perk {
|
||||
|
||||
/** Eindeutiger snake_case Bezeichner. Muss dem Sprachschlüssel-Prefix `perks.<id>.*` entsprechen. */
|
||||
abstract val id: String
|
||||
|
||||
/** Angezeigter Name im GUI, aus der Sprachdatei geladen. */
|
||||
abstract val displayName: Component
|
||||
|
||||
/** Kurze Lore-Zeilen als MiniMessage-Strings aus der Sprachdatei. */
|
||||
abstract val lore: List<String>
|
||||
|
||||
/** Icon-Material im Perk-Selector-GUI. */
|
||||
abstract val icon: Material
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Einmalig aufgerufen wenn das Spiel startet und dieser Perk für [player] aktiv ist. */
|
||||
open fun onActivate(player: Player) {}
|
||||
|
||||
/** Aufgerufen wenn die Runde endet oder der Perk entfernt wird. Tasks hier abbrechen. */
|
||||
open fun onDeactivate(player: Player) {}
|
||||
|
||||
// ── Combat Hooks (dispatched by PerkEventDispatcher) ──────────────────────
|
||||
|
||||
/** [attacker] (dieser Spieler) hat gerade [victim] per Nahkampf getroffen. */
|
||||
open fun onHitEnemy(attacker: Player, victim: Player, event: EntityDamageByEntityEvent) {}
|
||||
|
||||
/** Dieser Spieler hat gerade einen Nahkampfhit von [attacker] erhalten. */
|
||||
open fun onHitByEnemy(victim: Player, attacker: Player, event: EntityDamageByEntityEvent) {}
|
||||
|
||||
/** Dieser Spieler hat einen anderen Spieler getötet. */
|
||||
open fun onKillEnemy(killer: Player, victim: Player) {}
|
||||
|
||||
/**
|
||||
* Aufgerufen bei Umgebungsschaden (Fall, Feuer, etc.) — KEIN Nahkampf.
|
||||
* Wird vom Dispatcher mit HIGH-Priority verarbeitet, damit Featherweight canceln kann.
|
||||
*/
|
||||
open fun onEnvironmentalDamage(player: Player, event: EntityDamageEvent) {}
|
||||
|
||||
}
|
||||
192
src/main/kotlin/club/mcscrims/speedhg/perk/PerkManager.kt
Normal file
192
src/main/kotlin/club/mcscrims/speedhg/perk/PerkManager.kt
Normal file
@@ -0,0 +1,192 @@
|
||||
package club.mcscrims.speedhg.perk
|
||||
|
||||
import club.mcscrims.speedhg.SpeedHG
|
||||
import club.mcscrims.speedhg.perk.database.PlayerPerksRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.bukkit.entity.Player
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Verwaltet Perk-Registrierung, Spielerauswahl und den Spiel-Lifecycle.
|
||||
*
|
||||
* ## Typischer Ablauf
|
||||
* ```
|
||||
* // Startup
|
||||
* perkManager.registerPerk(OraclePerk())
|
||||
*
|
||||
* // Spieler-Login (AsyncPlayerPreLoginEvent – Async-Thread)
|
||||
* perkManager.loadPerks(uuid)
|
||||
*
|
||||
* // GUI-Klick (Lobby-Phase)
|
||||
* perkManager.selectPerk(player, perk)
|
||||
* perkManager.deselectPerk(player, perk)
|
||||
*
|
||||
* // Spielstart
|
||||
* perkManager.applyPerks(player) // ruft onActivate auf
|
||||
*
|
||||
* // Spielende
|
||||
* perkManager.removeAllActivePerks() // ruft onDeactivate für alle Online-Spieler auf
|
||||
*
|
||||
* // Spieler-Quit
|
||||
* perkManager.evict(uuid)
|
||||
* ```
|
||||
*/
|
||||
class PerkManager(
|
||||
private val plugin: SpeedHG
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val MAX_PERKS = 2
|
||||
}
|
||||
|
||||
// ── Registrierung ─────────────────────────────────────────────────────────
|
||||
|
||||
private val registeredPerks = ConcurrentHashMap<String, Perk>()
|
||||
|
||||
/**
|
||||
* Separate geordnete Liste für konsistentes GUI-Layout.
|
||||
* Wird nur in onEnable() (single-threaded) befüllt → kein Synchronisierungsbedarf.
|
||||
*/
|
||||
private val registrationOrder = mutableListOf<String>()
|
||||
|
||||
fun registerPerk(
|
||||
perk: Perk
|
||||
) {
|
||||
if (registeredPerks.putIfAbsent( perk.id, perk ) == null ) {
|
||||
registrationOrder += perk.id
|
||||
}
|
||||
plugin.logger.info( "[PerkManager] Registriert: ${perk.id}" )
|
||||
}
|
||||
|
||||
/** Gibt alle Perks in stabiler Registrierungsreihenfolge zurück (für GUI-Slots). */
|
||||
fun getRegisteredPerks(): List<Perk> =
|
||||
registrationOrder.mapNotNull { registeredPerks[ it ] }
|
||||
|
||||
fun getPerk(id: String): Perk? = registeredPerks[ id ]
|
||||
|
||||
// ── Spielerauswahl (Lobby) ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* UUID → geordnete Liste der gewählten Perk-IDs (max [MAX_PERKS]).
|
||||
* Index 0 = ältere Auswahl, Index 1 = neuere Auswahl.
|
||||
*/
|
||||
private val selectedPerkIds = ConcurrentHashMap<UUID, MutableList<String>>()
|
||||
|
||||
fun getSelectedPerkIds( uuid: UUID ): List<String> =
|
||||
selectedPerkIds[ uuid ]?.toList() ?: emptyList()
|
||||
|
||||
fun getSelectedPerks( player: Player ): List<Perk> =
|
||||
getSelectedPerkIds( player.uniqueId ).mapNotNull { registeredPerks[ it ] }
|
||||
|
||||
fun hasPerk( player: Player, perkId: String ): Boolean =
|
||||
selectedPerkIds[ player.uniqueId ]?.contains( perkId ) == true
|
||||
|
||||
fun isSlotsFull( player: Player ): Boolean =
|
||||
(selectedPerkIds[ player.uniqueId ]?.size ?: 0 ) >= MAX_PERKS
|
||||
|
||||
/**
|
||||
* Rüstet einen Perk für den Spieler aus.
|
||||
* @return `true` wenn erfolgreich; `false` wenn bereits ausgerüstet oder Slots voll.
|
||||
*/
|
||||
fun selectPerk(
|
||||
player: Player,
|
||||
perk: Perk
|
||||
): Boolean
|
||||
{
|
||||
val list = selectedPerkIds.getOrPut( player.uniqueId ) { mutableListOf() }
|
||||
if (list.contains( perk.id ) || list.size >= MAX_PERKS ) return false
|
||||
list += perk.id
|
||||
saveAsync( player.uniqueId )
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt einen Perk aus der Spielerauswahl.
|
||||
* @return `true` wenn er ausgerüstet war und entfernt wurde.
|
||||
*/
|
||||
fun deselectPerk(
|
||||
player: Player,
|
||||
perk: Perk
|
||||
): Boolean
|
||||
{
|
||||
val removed = selectedPerkIds[ player.uniqueId ]?.remove( perk.id ) == true
|
||||
if ( removed ) saveAsync( player.uniqueId )
|
||||
return removed
|
||||
}
|
||||
|
||||
// ── Spiel-Lifecycle ───────────────────────────────────────────────────────
|
||||
|
||||
/** Aktiviert alle gewählten Perks für [player]. Nach Teleport in startGame() aufrufen. */
|
||||
fun applyPerks(
|
||||
player: Player
|
||||
) {
|
||||
getSelectedPerks( player ).forEach { it.onActivate( player ) }
|
||||
}
|
||||
|
||||
/** Deaktiviert alle aktiven Perks für [player]. */
|
||||
fun removePerks(
|
||||
player: Player
|
||||
) {
|
||||
getSelectedPerks( player ).forEach { it.onDeactivate( player ) }
|
||||
}
|
||||
|
||||
/** Ruft [removePerks] für jeden aktuell online Spieler auf. In endGame() aufrufen. */
|
||||
fun removeAllActivePerks()
|
||||
{
|
||||
selectedPerkIds.keys
|
||||
.mapNotNull { plugin.server.getPlayer( it ) }
|
||||
.forEach { removePerks( it ) }
|
||||
}
|
||||
|
||||
// ── Persistenz ────────────────────────────────────────────────────────────
|
||||
|
||||
private val repository = PlayerPerksRepository( plugin.databaseManager )
|
||||
private val scope = CoroutineScope( Dispatchers.IO + SupervisorJob() )
|
||||
|
||||
fun initialize()
|
||||
{
|
||||
repository.createTableIfNotExists()
|
||||
plugin.logger.info( "[PerkManager] Initialisiert." )
|
||||
}
|
||||
|
||||
fun shutdown()
|
||||
{
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt die Perks des Spielers aus der DB in den Cache.
|
||||
* Muss in einem Async-Kontext aufgerufen werden (z.B. AsyncPlayerPreLoginEvent).
|
||||
*/
|
||||
suspend fun loadPerks(
|
||||
uuid: UUID
|
||||
): Unit = withContext( Dispatchers.IO ) {
|
||||
val ids = repository.findByUUID( uuid )
|
||||
val valid = ids.filter { registeredPerks.containsKey( it ) }.take( MAX_PERKS )
|
||||
selectedPerkIds[ uuid ] = valid.toMutableList()
|
||||
}
|
||||
|
||||
/** Entfernt den Spieler aus dem In-Memory-Cache. Beim Quit aufrufen. */
|
||||
fun evict(
|
||||
uuid: UUID
|
||||
) {
|
||||
selectedPerkIds.remove( uuid )
|
||||
}
|
||||
|
||||
private fun saveAsync(
|
||||
uuid: UUID
|
||||
) {
|
||||
val ids = selectedPerkIds[ uuid ]?.toList() ?: emptyList()
|
||||
scope.launch {
|
||||
runCatching { repository.upsert( uuid, ids ) }
|
||||
.onFailure { plugin.logger.severe( "[PerkManager] Save fehlgeschlagen: ${it.message}" ) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package club.mcscrims.speedhg.perk.database
|
||||
|
||||
import club.mcscrims.speedhg.database.DatabaseManager
|
||||
import java.sql.Types
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Kapselt alle SQL-Operationen für die `player_perks`-Tabelle.
|
||||
*
|
||||
* Das Schema nutzt zwei nullable Spalten (perk1, perk2), da die Slot-Reihenfolge
|
||||
* semantisch ist: perk1 ist die ältere (zuerst gewählte) Auswahl.
|
||||
* Entspricht direkt der geordneten Liste im [PerkManager].
|
||||
*/
|
||||
class PlayerPerksRepository(private val db: DatabaseManager) {
|
||||
|
||||
fun createTableIfNotExists() {
|
||||
db.getConnection().use { conn ->
|
||||
conn.createStatement().use { stmt ->
|
||||
stmt.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS player_perks (
|
||||
uuid VARCHAR(36) NOT NULL,
|
||||
perk1 VARCHAR(64) DEFAULT NULL,
|
||||
perk2 VARCHAR(64) DEFAULT NULL,
|
||||
PRIMARY KEY (uuid)
|
||||
) ENGINE=InnoDB
|
||||
DEFAULT CHARSET=utf8mb4
|
||||
COLLATE=utf8mb4_unicode_ci;
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die gespeicherten Perk-IDs des Spielers in Auswahlreihenfolge zurück.
|
||||
* Gibt eine leere Liste zurück wenn keine Daten existieren.
|
||||
*/
|
||||
fun findByUUID(uuid: UUID): List<String> {
|
||||
db.getConnection().use { conn ->
|
||||
conn.prepareStatement(
|
||||
"SELECT perk1, perk2 FROM player_perks WHERE uuid = ?"
|
||||
).use { stmt ->
|
||||
stmt.setString(1, uuid.toString())
|
||||
stmt.executeQuery().use { rs ->
|
||||
if (!rs.next()) return emptyList()
|
||||
return listOfNotNull(rs.getString("perk1"), rs.getString("perk2"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Speichert oder aktualisiert die beiden Perk-Slots des Spielers. */
|
||||
fun upsert(uuid: UUID, perkIds: List<String>) {
|
||||
val perk1 = perkIds.getOrNull(0)
|
||||
val perk2 = perkIds.getOrNull(1)
|
||||
|
||||
db.getConnection().use { conn ->
|
||||
conn.prepareStatement(
|
||||
"""
|
||||
INSERT INTO player_perks (uuid, perk1, perk2) VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE perk1 = VALUES(perk1), perk2 = VALUES(perk2)
|
||||
""".trimIndent()
|
||||
).use { stmt ->
|
||||
stmt.setString(1, uuid.toString())
|
||||
if (perk1 != null) stmt.setString(2, perk1) else stmt.setNull(2, Types.VARCHAR)
|
||||
if (perk2 != null) stmt.setString(3, perk2) else stmt.setNull(3, Types.VARCHAR)
|
||||
stmt.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package club.mcscrims.speedhg.perk.impl
|
||||
|
||||
import club.mcscrims.speedhg.SpeedHG
|
||||
import club.mcscrims.speedhg.perk.Perk
|
||||
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.potion.PotionEffect
|
||||
import org.bukkit.potion.PotionEffectType
|
||||
|
||||
/**
|
||||
* ## Blutrausch
|
||||
*
|
||||
* Nach einem Kill erhält der Spieler für **5 Sekunden Speed I + Regeneration I**.
|
||||
*/
|
||||
class BloodlustPerk : Perk() {
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
override val id = "bloodlust"
|
||||
override val displayName: Component
|
||||
get() = plugin.languageManager.getDefaultComponent("perks.bloodlust.name", mapOf())
|
||||
override val lore: List<String>
|
||||
get() = plugin.languageManager.getDefaultRawMessageList("perks.bloodlust.lore")
|
||||
override val icon = Material.BLAZE_POWDER
|
||||
|
||||
private val DURATION_TICKS = 5 * 20
|
||||
|
||||
override fun onKillEnemy(killer: Player, victim: Player) {
|
||||
killer.addPotionEffect(
|
||||
PotionEffect(PotionEffectType.SPEED, DURATION_TICKS, 0, false, false, true)
|
||||
)
|
||||
killer.addPotionEffect(
|
||||
PotionEffect(PotionEffectType.REGENERATION, DURATION_TICKS, 0, false, false, true)
|
||||
)
|
||||
|
||||
killer.playSound(killer.location, Sound.ENTITY_PLAYER_LEVELUP, 0.6f, 1.8f)
|
||||
killer.sendActionBar(killer.trans("perks.bloodlust.message"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package club.mcscrims.speedhg.perk.impl
|
||||
|
||||
import club.mcscrims.speedhg.SpeedHG
|
||||
import club.mcscrims.speedhg.perk.Perk
|
||||
import net.kyori.adventure.text.Component
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.entity.EntityDamageEvent
|
||||
|
||||
/**
|
||||
* ## Federgewicht
|
||||
*
|
||||
* Macht den Träger vollständig immun gegen Fallschaden.
|
||||
* Das Event wird auf HIGH-Priority gecancelt, bevor andere Listener
|
||||
* den Schaden verarbeiten.
|
||||
*/
|
||||
class FeatherweightPerk : Perk() {
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
|
||||
override val id = "featherweight"
|
||||
override val displayName: Component
|
||||
get() = plugin.languageManager.getDefaultComponent("perks.featherweight.name", mapOf())
|
||||
override val lore: List<String>
|
||||
get() = plugin.languageManager.getDefaultRawMessageList("perks.featherweight.lore")
|
||||
override val icon = Material.FEATHER
|
||||
|
||||
override fun onEnvironmentalDamage(player: Player, event: EntityDamageEvent) {
|
||||
if (event.cause == EntityDamageEvent.DamageCause.FALL) {
|
||||
event.isCancelled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
103
src/main/kotlin/club/mcscrims/speedhg/perk/impl/OraclePerk.kt
Normal file
103
src/main/kotlin/club/mcscrims/speedhg/perk/impl/OraclePerk.kt
Normal file
@@ -0,0 +1,103 @@
|
||||
package club.mcscrims.speedhg.perk.impl
|
||||
|
||||
import club.mcscrims.speedhg.SpeedHG
|
||||
import club.mcscrims.speedhg.game.GameState
|
||||
import club.mcscrims.speedhg.perk.Perk
|
||||
import net.kyori.adventure.text.Component
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.scheduler.BukkitRunnable
|
||||
import org.bukkit.scheduler.BukkitTask
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* ## Orakel
|
||||
*
|
||||
* Zeigt das Kit und die Entfernung des nächsten lebenden Gegners per Actionbar an,
|
||||
* sobald der Träger **schleicht** oder **einen Kompass hält**.
|
||||
*
|
||||
* **Spielo-Synergie (Platzhalter):** Wenn der Träger das "spielo"-Kit besitzt,
|
||||
* wird ein Hinweis auf das nächste Gamble-Ergebnis angezeigt. Logik muss beim
|
||||
* Implementieren des Spielo-Kits befüllt werden.
|
||||
*/
|
||||
class OraclePerk : Perk() {
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
private val mm = MiniMessage.miniMessage()
|
||||
|
||||
override val id = "oracle"
|
||||
override val displayName: Component
|
||||
get() = plugin.languageManager.getDefaultComponent("perks.oracle.name", mapOf())
|
||||
override val lore: List<String>
|
||||
get() = plugin.languageManager.getDefaultRawMessageList("perks.oracle.lore")
|
||||
override val icon = Material.SPYGLASS
|
||||
|
||||
/** Laufende Tracker-Tasks pro Spieler. */
|
||||
private val trackerTasks = ConcurrentHashMap<UUID, BukkitTask>()
|
||||
|
||||
override fun onActivate(player: Player) {
|
||||
val task = object : BukkitRunnable() {
|
||||
override fun run() {
|
||||
if (!player.isOnline) { cancel(); return }
|
||||
|
||||
// Nur während aktiver Spielphasen anzeigen
|
||||
val state = plugin.gameManager.currentState
|
||||
if (state != GameState.INGAME && state != GameState.INVINCIBILITY) return
|
||||
|
||||
// Träger muss selbst noch leben
|
||||
if (!plugin.gameManager.alivePlayers.contains(player.uniqueId)) return
|
||||
|
||||
// Nur anzeigen wenn Bedingung erfüllt
|
||||
val isHoldingCompass = player.inventory.itemInMainHand.type == Material.COMPASS
|
||||
if (!isHoldingCompass && !player.isSneaking) return
|
||||
|
||||
val nearest = findNearestEnemy(player) ?: return
|
||||
player.sendActionBar(buildTrackerComponent(player, nearest))
|
||||
}
|
||||
}.runTaskTimer(plugin, 0L, 10L) // Refresh alle 0.5 Sekunden
|
||||
|
||||
trackerTasks[player.uniqueId] = task
|
||||
}
|
||||
|
||||
override fun onDeactivate(player: Player) {
|
||||
trackerTasks.remove(player.uniqueId)?.cancel()
|
||||
}
|
||||
|
||||
// ── Intern ────────────────────────────────────────────────────────────────
|
||||
|
||||
private fun findNearestEnemy(player: Player): Player? =
|
||||
plugin.gameManager.alivePlayers
|
||||
.asSequence()
|
||||
.filter { it != player.uniqueId }
|
||||
.mapNotNull { plugin.server.getPlayer(it) }
|
||||
.minByOrNull { it.location.distanceSquared(player.location) }
|
||||
|
||||
private fun buildTrackerComponent(player: Player, nearest: Player): Component {
|
||||
val distance = player.location.distance(nearest.location).toInt()
|
||||
val kitDisplay = plugin.kitManager.getSelectedKit(nearest)?.displayName
|
||||
?: mm.deserialize("<gray>???")
|
||||
|
||||
val base = mm.deserialize(
|
||||
"<gold>⚲ <yellow><name></yellow> <dark_gray>·</dark_gray> " +
|
||||
"<white>Kit: </white><kit><dark_gray> · </dark_gray><yellow><distance>m</yellow>",
|
||||
Placeholder.unparsed("name", nearest.name),
|
||||
Placeholder.component("kit", kitDisplay),
|
||||
Placeholder.unparsed("distance", distance.toString())
|
||||
)
|
||||
|
||||
// ── Spielo-Synergie (Platzhalter) ─────────────────────────────────
|
||||
val playerKit = plugin.kitManager.getSelectedKit(player)
|
||||
if (playerKit?.id != "spielo") return base
|
||||
|
||||
// TODO: Mit echter Spielo-Logik ersetzen sobald das Kit existiert.
|
||||
// Temporär: alterniert sekündlich als visueller Platzhalter.
|
||||
val isGoodGamble = (System.currentTimeMillis() / 3_000L) % 2 == 0L
|
||||
val hint = if (isGoodGamble) mm.deserialize(" <green>⬆ Good Gamble")
|
||||
else mm.deserialize(" <red>⬇ Bad Gamble")
|
||||
|
||||
return base.append(hint)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package club.mcscrims.speedhg.perk.impl
|
||||
|
||||
import club.mcscrims.speedhg.SpeedHG
|
||||
import club.mcscrims.speedhg.perk.Perk
|
||||
import net.kyori.adventure.text.Component
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.Particle
|
||||
import org.bukkit.Sound
|
||||
import org.bukkit.attribute.Attribute
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent
|
||||
import java.util.Random
|
||||
|
||||
/**
|
||||
* ## Vampir
|
||||
*
|
||||
* Jeder Nahkampftreffer gegen einen Gegner hat eine **10%-Chance**, dem Angreifer
|
||||
* **1 HP (0.5 Herzen)** zu heilen. Kann nicht über das Maximum geheilt werden.
|
||||
*/
|
||||
class VampirePerk : Perk() {
|
||||
|
||||
private val plugin get() = SpeedHG.instance
|
||||
private val rng = Random()
|
||||
|
||||
override val id = "vampire"
|
||||
override val displayName: Component
|
||||
get() = plugin.languageManager.getDefaultComponent("perks.vampire.name", mapOf())
|
||||
override val lore: List<String>
|
||||
get() = plugin.languageManager.getDefaultRawMessageList("perks.vampire.lore")
|
||||
override val icon = Material.RED_DYE
|
||||
|
||||
override fun onHitEnemy(attacker: Player, victim: Player, event: EntityDamageByEntityEvent) {
|
||||
if (rng.nextDouble() > 0.10) return // 90% verfehlen
|
||||
|
||||
val maxHp = attacker.getAttribute(Attribute.GENERIC_MAX_HEALTH)?.value ?: 20.0
|
||||
if (attacker.health >= maxHp) return // Bereits voll
|
||||
|
||||
attacker.health = (attacker.health + 1.0).coerceAtMost(maxHp)
|
||||
|
||||
// Visuelles + akustisches Feedback
|
||||
attacker.world.spawnParticle(
|
||||
Particle.HEART,
|
||||
attacker.location.clone().add(0.0, 2.2, 0.0),
|
||||
3, 0.3, 0.2, 0.3, 0.0
|
||||
)
|
||||
attacker.playSound(attacker.location, Sound.ENTITY_GENERIC_DRINK, 0.5f, 1.8f)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package club.mcscrims.speedhg.perk.listener
|
||||
|
||||
import club.mcscrims.speedhg.SpeedHG
|
||||
import club.mcscrims.speedhg.game.GameState
|
||||
import club.mcscrims.speedhg.perk.PerkManager
|
||||
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.event.entity.EntityDamageEvent
|
||||
import org.bukkit.event.entity.PlayerDeathEvent
|
||||
|
||||
/**
|
||||
* Einziger registrierter Listener für alle perk-bezogenen Events.
|
||||
*
|
||||
* Folgt dem gleichen Single-Dispatcher-Muster wie [KitEventDispatcher]:
|
||||
* Einmalig registriert, delegiert an die aktiven Perks des jeweiligen Spielers.
|
||||
*
|
||||
* ## Prioritätenstrategie
|
||||
* | Handler | Priorität | ignoreCancelled | Grund |
|
||||
* |--------------------------|-----------|-----------------|---------------------------------------------|
|
||||
* | [onMeleeDamage] | MONITOR | true | Liest den finalen Schadenswert nach Modifikation |
|
||||
* | [onEnvironmentalDamage] | HIGH | false | Featherweight muss canceln bevor Schaden gesetzt wird |
|
||||
* | [onPlayerKill] | HIGH | true | Vor Drop/Respawn-Verarbeitung |
|
||||
*/
|
||||
class PerkEventDispatcher(
|
||||
private val plugin: SpeedHG,
|
||||
private val perkManager: PerkManager
|
||||
) : Listener {
|
||||
|
||||
// ── Nahkampf: onHitEnemy + onHitByEnemy ──────────────────────────────────
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
fun onMeleeDamage(event: EntityDamageByEntityEvent) {
|
||||
val attacker = event.damager as? Player ?: return
|
||||
val victim = event.entity as? Player ?: return
|
||||
if (!isIngame()) return
|
||||
// Nur bei lebenden Spielern — kein Hit-Farming an Zuschauern
|
||||
if (!plugin.gameManager.alivePlayers.contains(victim.uniqueId)) return
|
||||
|
||||
perkManager.getSelectedPerks(attacker).forEach { it.onHitEnemy(attacker, victim, event) }
|
||||
perkManager.getSelectedPerks(victim).forEach { it.onHitByEnemy(victim, attacker, event) }
|
||||
}
|
||||
|
||||
// ── Umgebungsschaden: onEnvironmentalDamage ───────────────────────────────
|
||||
|
||||
/**
|
||||
* Wir überspringen [EntityDamageByEntityEvent] explizit — Nahkampftreffer
|
||||
* werden oben in [onMeleeDamage] verarbeitet. Da EntityDamageByEntityEvent
|
||||
* eine Unterklasse von EntityDamageEvent ist, würde Paper sonst diesen
|
||||
* Handler für Nahkampfereignisse zusätzlich aufrufen.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = false)
|
||||
fun onEnvironmentalDamage(event: EntityDamageEvent) {
|
||||
if (event is EntityDamageByEntityEvent) return // Separater Handler oben
|
||||
val player = event.entity as? Player ?: return
|
||||
if (!isIngame()) return
|
||||
if (!plugin.gameManager.alivePlayers.contains(player.uniqueId)) return
|
||||
|
||||
perkManager.getSelectedPerks(player).forEach { it.onEnvironmentalDamage(player, event) }
|
||||
}
|
||||
|
||||
// ── Kill: onKillEnemy ─────────────────────────────────────────────────────
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
|
||||
fun onPlayerKill(event: PlayerDeathEvent) {
|
||||
if (!isIngame()) return
|
||||
val killer = event.entity.killer ?: return
|
||||
|
||||
perkManager.getSelectedPerks(killer).forEach { it.onKillEnemy(killer, event.entity) }
|
||||
}
|
||||
|
||||
// ── Helper ────────────────────────────────────────────────────────────────
|
||||
|
||||
private fun isIngame(): Boolean = when (plugin.gameManager.currentState) {
|
||||
GameState.INGAME, GameState.INVINCIBILITY -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,48 @@ gui:
|
||||
input_placeholder: '<gray>Enter search term...</gray>'
|
||||
confirm_name: '<green>✔ Confirm Search</green>'
|
||||
confirm_click: '<gray>Click to confirm</gray>'
|
||||
perk_selector:
|
||||
title: '<gradient:dark_purple:light_purple><bold>Perk-Auswahl</bold></gradient>'
|
||||
slot_empty: '<gray>Perk-Slot <slot>: <red>Leer</red></gray>'
|
||||
slot_hint: '<dark_gray>Klicke einen Perk zum Ausrüsten'
|
||||
slot_title: '<gray>Slot <slot>: </gray>'
|
||||
equipped_label: '<green>✔ Ausgerüstet</green>'
|
||||
click_equip: '<gray>Klick zum Ausrüsten</gray>'
|
||||
click_deselect: '<gray>Klick zum Abwählen</gray>'
|
||||
slots_full: '<red>Slots voll! Klicke einen aktiven Perk zum Abwählen.'
|
||||
game_running: '<red>Perks können während des Spiels nicht geändert werden!'
|
||||
selected: '<green>Ausgerüstet: <perk><green>!'
|
||||
deselected: '<red>Abgewählt: <perk><red>!'
|
||||
close: '<red>✕ Schließen</red>'
|
||||
|
||||
perks:
|
||||
oracle:
|
||||
name: '<gradient:gold:yellow><bold>Oracle</bold></gradient>'
|
||||
lore:
|
||||
- ' '
|
||||
- '<gray>Zeigt Kit + Distanz des nächsten</gray>'
|
||||
- '<gray>Gegners (Schleichen / Kompass).</gray>'
|
||||
- ' '
|
||||
- '<yellow>Synergie: <gray>Spielo-Kit zeigt Gamble-Ausgang.'
|
||||
vampire:
|
||||
name: '<gradient:dark_red:red><bold>Vampire</bold></gradient>'
|
||||
lore:
|
||||
- ' '
|
||||
- '<gray>10% Chance bei Nahkampftreffer:</gray>'
|
||||
- '<red>½ Herz</red> <gray>heilen.</gray>'
|
||||
featherweight:
|
||||
name: '<gradient:white:aqua><bold>Featherweight</bold></gradient>'
|
||||
lore:
|
||||
- ' '
|
||||
- '<gray>Vollständig immun gegen</gray>'
|
||||
- '<gray>Fallschaden.</gray>'
|
||||
bloodlust:
|
||||
name: '<gradient:dark_red:gold><bold>Bloodlust</bold></gradient>'
|
||||
lore:
|
||||
- ' '
|
||||
- '<gray>Nach einem Kill:</gray>'
|
||||
- '<yellow>Speed I</yellow> <gray>+</gray> <green>Regen I</green> <gray>für 5 Sekunden.</gray>'
|
||||
message: '<red>⚔ Blutrausch! <yellow>Speed I</yellow> + <green>Regen I</green> für 5 Sekunden!</red>'
|
||||
|
||||
kits:
|
||||
backup:
|
||||
|
||||
@@ -32,3 +32,6 @@ commands:
|
||||
description: 'Manage the SpeedHG ranking system'
|
||||
usage: '/ranking <toggle|status|rank [player]>'
|
||||
permission: speedhg.admin.ranking
|
||||
perks:
|
||||
description: 'Perk-Auswahl öffnen'
|
||||
usage: '/perks'
|
||||
Reference in New Issue
Block a user