Add timer command, recraft module & recipes

Introduce a /timer admin command with tab completion (speedhg.admin.timer) and a parseTimeToSeconds utility; add CommandSender.sendMsg extension and language strings. Add RecraftManager module that enforces a configurable recraft-nerf (reduces extra recraft points for alive players) and starts from GameManager; wire the new module into GameManager. Register mushroom-stew shapeless recipes for cocoa and cactus in plugin startup and tidy command registration. Update plugin.yml with permission/command entries and adjust imports as needed.
This commit is contained in:
TDSTOS
2026-03-27 16:26:18 +01:00
parent 9682df5bf9
commit 8e96a07a65
8 changed files with 334 additions and 4 deletions

View File

@@ -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.TimerCommand
import club.mcscrims.speedhg.config.LanguageManager
import club.mcscrims.speedhg.database.DatabaseManager
import club.mcscrims.speedhg.database.StatsManager
@@ -18,6 +19,10 @@ import club.mcscrims.speedhg.listener.StatsListener
import club.mcscrims.speedhg.scoreboard.ScoreboardManager
import club.mcscrims.speedhg.webhook.DiscordWebhookManager
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.NamespacedKey
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.ShapelessRecipe
import org.bukkit.plugin.java.JavaPlugin
class SpeedHG : JavaPlugin() {
@@ -81,6 +86,7 @@ class SpeedHG : JavaPlugin() {
registerKits()
registerCommands()
registerListener()
registerRecipes()
logger.info("SpeedHG wurde geladen!")
}
@@ -105,8 +111,16 @@ class SpeedHG : JavaPlugin() {
private fun registerCommands()
{
val kitCommand = KitCommand()
getCommand( "kit" )?.setExecutor( kitCommand )
getCommand( "kit" )?.tabCompleter = kitCommand
getCommand( "kit" )?.apply {
setExecutor( kitCommand )
tabCompleter = kitCommand
}
val timerCommand = TimerCommand( this )
getCommand( "timer" )?.apply {
setExecutor( timerCommand )
tabCompleter = timerCommand
}
getCommand( "leaderboard" )?.setExecutor( LeaderboardCommand() )
}
@@ -123,4 +137,20 @@ class SpeedHG : JavaPlugin() {
pm.registerEvents( MenuListener(), this )
}
private fun registerRecipes()
{
val soup = ItemStack( Material.MUSHROOM_STEW )
val cocoRecipe = ShapelessRecipe(NamespacedKey.minecraft( "cocoa_soup" ), soup )
cocoRecipe.addIngredient( Material.COCOA_BEANS )
cocoRecipe.addIngredient( Material.BOWL )
val cactiRecipe = ShapelessRecipe(NamespacedKey.minecraft( "cacti_soup" ), soup )
cactiRecipe.addIngredient( Material.CACTUS )
cactiRecipe.addIngredient( Material.BOWL )
Bukkit.addRecipe( cocoRecipe )
Bukkit.addRecipe( cactiRecipe )
}
}

View File

@@ -0,0 +1,74 @@
package club.mcscrims.speedhg.command
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.game.GameState
import club.mcscrims.speedhg.util.parseTimeToSeconds
import club.mcscrims.speedhg.util.sendMsg
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.command.TabCompleter
class TimerCommand(
private val plugin: SpeedHG
) : CommandExecutor, TabCompleter {
override fun onCommand(
sender: CommandSender,
command: Command,
label: String,
args: Array<out String>
): Boolean
{
if (!sender.hasPermission( "speedhg.admin.timer" ))
{
sender.sendMsg( "default.no_permission" )
return true
}
if ( args.isEmpty() )
{
sender.sendMsg( "commands.timer.usage" )
return true
}
val newTime = args[0].parseTimeToSeconds()
if ( newTime == null || newTime < 0 )
{
sender.sendMsg( "commands.timer.positiveNumber" )
return true
}
if ( plugin.gameManager.currentState != GameState.INGAME )
{
sender.sendMsg( "commands.timer.onlyIngame" )
return true
}
plugin.gameManager.timer = newTime
val minutes = newTime / 60
val seconds = newTime % 60
val formattedTime = if ( minutes > 0 ) "${minutes}m ${seconds}s" else "${seconds}s"
sender.sendMsg( "commands.timer.set", "time" to formattedTime )
return true
}
override fun onTabComplete(
sender: CommandSender,
command: Command,
label: String,
args: Array<out String>
): List<String?>
{
if ( args.size == 1 && sender.hasPermission( "speedhg.admin.timer" ))
{
val suggestions = listOf("10m", "30m", "1h", "60s", "600", "1800")
return suggestions.filter { it.startsWith( args[0], true ) }.toMutableList()
}
return mutableListOf()
}
}

View File

@@ -3,6 +3,7 @@ package club.mcscrims.speedhg.game
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.game.modules.FeastManager
import club.mcscrims.speedhg.game.modules.PitManager
import club.mcscrims.speedhg.game.modules.RecraftManager
import club.mcscrims.speedhg.util.sendMsg
import club.mcscrims.speedhg.util.trans
import net.kyori.adventure.title.Title
@@ -45,6 +46,7 @@ class GameManager(
val feastManager = FeastManager( plugin )
val pitManager = PitManager( plugin )
val recraftManager = RecraftManager( plugin )
init {
plugin.server.pluginManager.registerEvents( this, plugin )
@@ -52,6 +54,8 @@ class GameManager(
gameTask = Bukkit.getScheduler().runTaskTimer( plugin, { ->
gameLoop()
}, 20L, 20L )
recraftManager.startRunnable()
}
private var lobbyIdleCount: Int = 0

View File

@@ -0,0 +1,179 @@
package club.mcscrims.speedhg.game.modules
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.util.sendMsg
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import org.bukkit.scheduler.BukkitRunnable
class RecraftManager(
private val plugin: SpeedHG
) {
private val beforeFeast = plugin.config.getBoolean( "game.recraftNerf.beforeFeast", true )
private val recraftNerfEnabled = plugin.config.getBoolean( "game.recraftNerf.enabled", false )
private val maxRecraftAmount = plugin.config.getInt( "game.recraftNerf.maxAmount", 64 )
fun startRunnable()
{
if ( !recraftNerfEnabled )
return
object : BukkitRunnable() {
override fun run()
{
if ( beforeFeast &&
plugin.gameManager.feastManager.hasSpawned )
{
this.cancel()
return
}
Bukkit.getOnlinePlayers().stream()
.filter { plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
.forEach {
val recraft = Recraft()
recraft.calcRecraft( *it.inventory.contents )
if ( recraft.getRecraftPoints() > maxRecraftAmount )
{
it.sendMsg( "recraftNerf.tooMuch" )
while ( recraft.getRecraftPoints() > maxRecraftAmount )
recraft.decrease( it, 1 )
}
}
}
}.runTaskTimer( plugin, 10L, 10L )
}
private class Recraft {
private val recraftMaterials = listOf(
RecraftMaterial( 1, arrayOf( Material.RED_MUSHROOM, Material.BROWN_MUSHROOM )),
RecraftMaterial( 1, arrayOf( Material.COCOA_BEANS )),
RecraftMaterial( 1, arrayOf( Material.CACTUS ))
)
fun calcRecraft(
vararg items: ItemStack?
) {
recraftMaterials.forEach( RecraftMaterial::reset )
for ( item in items )
{
if ( item == null )
continue
for ( recraftMaterial in recraftMaterials )
{
val type = item.type
if (recraftMaterial.containsKey( type ))
recraftMaterial[ type ] = recraftMaterial.getOrDefault( type, 0 ) + item.amount
}
}
}
fun decrease(
player: Player,
amount: Int
) {
val lowestMaterials = mutableListOf<Material>()
for ( recraftMaterial in recraftMaterials )
if ( recraftMaterial.getLowestMaterial() != null )
lowestMaterials.add( recraftMaterial.getLowestMaterial()!! )
var highestMaterial: Material? = null
var i = 0f
for ( lowestMaterial in lowestMaterials )
{
val recraftMaterial = byMaterial( lowestMaterial )
?: continue
if (recraftMaterial[ lowestMaterial ]!! * recraftMaterial.getMaterialValue() > i )
{
i = recraftMaterial[ lowestMaterial ]!! * recraftMaterial.getMaterialValue()
highestMaterial = lowestMaterial
}
}
val recraftMaterial = byMaterial( highestMaterial!! )
recraftMaterial?.decrease( highestMaterial, amount )
for ( item in player.inventory.contents )
{
if ( item == null )
continue
if ( item.type == highestMaterial )
{
item.amount -= amount
break
}
}
}
fun getRecraftPoints(): Float
{
var points = 0f
for ( recraftMaterial in recraftMaterials )
points += recraftMaterial.getPoints()
return points
}
private fun byMaterial(
material: Material
): RecraftMaterial?
{
return recraftMaterials.stream()
.filter { it.containsKey( material ) }
.findFirst().orElse( null )
}
}
private class RecraftMaterial(
val maxSoupAmount: Int,
val materials: Array<Material>
) : HashMap<Material, Int>() {
fun getPoints(): Float
{
return getOrDefault( getLowestMaterial(), 0 ).toFloat()
}
fun decrease(
material: Material,
amount: Int
) {
put( material, get( material )!! - amount )
}
fun getLowestMaterial(): Material?
{
if ( size > 1 )
{
if (values.stream().anyMatch { int -> int == 0 })
return null
val materialIntegerEntry = entries.stream().min(Comparator.comparingInt { it.value })
return materialIntegerEntry.map { it.key }.orElse( null )
}
else return keys.stream().findFirst().orElse( null )
}
fun getMaterialValue() = ( maxSoupAmount / size ).toFloat()
fun reset() = replaceAll { _, _ -> 0 }
}
}

View File

@@ -4,6 +4,7 @@ import club.mcscrims.speedhg.SpeedHG
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.minimessage.MiniMessage
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
private val langManager get() = SpeedHG.instance.languageManager
@@ -31,6 +32,14 @@ fun Player.sendMsg(
this.sendMessage( component )
}
fun CommandSender.sendMsg(
key: String,
vararg placeholders: Pair<String, String>
) {
val component = langManager.getDefaultComponent( key, placeholders.toMap() )
this.sendMessage( component )
}
fun Player.trans(
key: String,
vararg placeholders: Pair<String, String>
@@ -71,3 +80,20 @@ fun Component.toLegacyString(): String
val Player.getDisplayName: String
get() = legacySerializer.serialize( this.displayName() )
fun String.parseTimeToSeconds(): Int?
{
val regex = Regex( "^(\\d+)([smh]?)$", RegexOption.IGNORE_CASE )
val match = regex.find( this.trim() ) ?: return null
val value = match.groupValues[ 1 ].toIntOrNull() ?: return null
val unit = match.groupValues[ 2 ].lowercase()
return when( unit )
{
"h" -> value * 3600
"m" -> value * 60
"s", "" -> value
else -> null
}
}

View File

@@ -20,6 +20,11 @@ anti-runner:
ignore-vertical-distance: 15.0 # Wenn Höhenunterschied > 15, Timer ignorieren
ignore-cave-surface-mix: true # Ignorieren, wenn einer Sonne hat und der andere nicht
recraftNerf:
enabled: false
beforeFeast: true
maxAmount: 64
discord:
enabled: false
webhook-url: "DEINE_WEBHOOK_URL_HIER"

View File

@@ -69,6 +69,11 @@ commands:
empty: '<red>There are currently no stats</red>'
line: '#<rank> - <green><name></green> - <aqua><score></aqua>'
footer: '<gray>====== <gold>Leaderboard</gold> ======</gray>'
timer:
usage: '<red>Usage: /timer <seconds></red>'
positiveNumber: '<red>Invalid time format! Use, for example, 10m, 30s, or 600.</red>'
onlyIngame: '<red>Timer can only be set in game.</red>'
set: '<green>The game timer has been set to <time>!</green>'
scoreboard:
title: '<gradient:red:gold><bold>SpeedHG</bold></gradient>'

View File

@@ -10,6 +10,9 @@ permissions:
speedhg.bypass:
description: 'Allows joining the server while its running'
default: false
speedhg.admin.timer:
description: 'Change the current game time'
default: false
commands:
kit:
@@ -18,3 +21,7 @@ commands:
leaderboard:
description: 'View the top 10 players'
usage: '/leaderboard'
timer:
description: 'Change the current game time (Admin Command)'
usage: '/timer <seconds>'
permission: speedhg.admin.timer