Add new game states

This commit is contained in:
Laurin
2025-12-04 15:01:10 +01:00
parent ac185e35ce
commit 2c10e3e7fd
10 changed files with 724 additions and 26 deletions

View File

@@ -37,8 +37,8 @@ dependencies {
compileOnly("net.luckperms:api:5.4")
implementation("club.mcscrims:core:1.4.1")
implementation("club.mcscrims:spigot:1.4.1")
implementation("club.mcscrims:core:1.4.2")
implementation("club.mcscrims:spigot:1.4.2")
compileOnly("io.papermc.paper:paper-api:1.21.1-R0.1-SNAPSHOT")
paperweight.paperDevBundle("1.21.1-R0.1-SNAPSHOT")

View File

@@ -12,6 +12,7 @@ import club.mcscrims.speedhg.game.GameManager
import club.mcscrims.spigot.chat.ChatFormatter
import club.mcscrims.spigot.chat.ChatManager
import club.mcscrims.spigot.network.SpigotNetworkManager
import club.mcscrims.spigot.scheduler.SchedulerManager
import com.mongodb.client.model.Indexes
import kotlinx.coroutines.runBlocking
import net.luckperms.api.LuckPerms
@@ -35,6 +36,7 @@ class SpeedHG : JavaPlugin() {
internal lateinit var statsRepository: StatsRepository
internal lateinit var networkManager: SpigotNetworkManager
internal lateinit var schedulerManager: SchedulerManager
internal lateinit var gameManager: GameManager
@@ -57,7 +59,10 @@ class SpeedHG : JavaPlugin() {
chatManager = ChatManager.withCustomConfig( this, chatFormatter )
chatManager.initialize()
schedulerManager = SchedulerManager( this )
gameManager = GameManager( this )
gameManager.initialize()
setupLuckPerms()
}

View File

@@ -1,9 +1,13 @@
package club.mcscrims.speedhg.config
import club.mcscrims.core.config.ConfigData
import club.mcscrims.core.config.DurationEntry
import club.mcscrims.core.config.DurationType
import club.mcscrims.core.config.annotations.ConfigClass
import club.mcscrims.core.config.annotations.ConfigField
import club.mcscrims.core.config.annotations.DefaultValue
import club.mcscrims.core.network.NetworkConfig
import club.mcscrims.speedhg.SpeedHG
@ConfigClass(
name = "config",
@@ -55,7 +59,7 @@ data class PluginConfig(
val minimumPlayers: Int = 8,
val competitiveGame: Boolean = false,
val competitiveCommands: List<String> = emptyList(),
val playerState: Map<String, String> = getPlayerStates(),
val playerStates: Map<String, StateConfig> = getPlayerStates(),
val recraftNerf: Map<String, Any> = getRecraftNerf(),
val teams: Map<String, Any> = getTeams(),
val blockedKits: List<String> = emptyList(),
@@ -63,16 +67,56 @@ data class PluginConfig(
val perks: Map<String, Any> = getPerks()
)
fun isDurationIncreasing(
entry: DurationEntry
): Boolean
{
return entry.type == DurationType.INCREASING
}
fun getDuration(
playerState: String
): DurationEntry
{
return SpeedHG.instance.pluginConfig.parseDuration( "game.playerStates.$playerState.duration" )
}
data class StateConfig(
val duration: Any,
val scoreboard: String
)
}
private fun getPlayerStates() = mapOf(
"waiting" to "Waiting - %time%",
"pre_start" to "Waiting - %time%",
"immunity" to "Playing - %time%",
"battle" to "Playing - %time%",
"feast" to "Playing - %time%",
"deathmatch" to "Playing - %time%",
"end" to "Ending - %time%",
"waiting" to PluginConfig.StateConfig(
DurationEntry( DurationType.FIXED, -1 ),
"Waiting - %time%"
),
"pre_start" to PluginConfig.StateConfig(
DurationEntry( DurationType.FIXED, 300 ),
"Waiting - %time%"
),
"immunity" to PluginConfig.StateConfig(
DurationEntry( DurationType.FIXED, 90 ),
"Playing - %time%"
),
"battle" to PluginConfig.StateConfig(
DurationEntry( DurationType.INCREASING ),
"Playing - %time%"
),
"feast" to PluginConfig.StateConfig(
DurationEntry( DurationType.FIXED, 300 ),
"Playing - %time%"
),
"deathmatch" to PluginConfig.StateConfig(
DurationEntry( DurationType.INCREASING ),
"Playing - %time%"
),
"end" to PluginConfig.StateConfig(
DurationEntry( DurationType.FIXED, 60 ),
"Ending - %time%"
),
)
private fun getRecraftNerf() = mapOf(

View File

@@ -1,13 +1,31 @@
package club.mcscrims.speedhg.game
import org.bukkit.plugin.java.JavaPlugin
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.game.impl.WaitingState
class GameManager(
private val plugin: JavaPlugin
private val plugin: SpeedHG
) {
private var currentState: GameState? = null
fun initialize()
{
currentState = WaitingState(
this,
plugin,
plugin.schedulerManager,
plugin.pluginConfig.data.getDuration( "waiting" ).seconds
)
try {
currentState?.onEnter( null )
} catch ( e: Exception ) {
plugin.logger.severe("Error during onEnter for state ${currentState?.name}: ${e.message}")
e.printStackTrace()
}
}
fun transitionTo(
nextState: GameState
) {

View File

@@ -8,7 +8,7 @@ import org.bukkit.entity.Player
import org.bukkit.event.HandlerList
import org.bukkit.event.Listener
open class GameState(
abstract class GameState(
val name: String,
protected val gameManager: GameManager,
protected val plugin: SpeedHG,
@@ -31,9 +31,9 @@ open class GameState(
startTicking()
}
open fun onTick() {}
abstract fun onTick()
open fun onEndOfDuration() {}
abstract fun onEndOfDuration()
open fun onExit(
next: GameState?
@@ -64,7 +64,7 @@ open class GameState(
{
remainingSeconds--
if ( remainingSeconds <= 0)
if ( remainingSeconds == 0)
try {
onEndOfDuration()
} catch ( e: Exception ) {

View File

@@ -0,0 +1,252 @@
package club.mcscrims.speedhg.game.impl
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.game.GameManager
import club.mcscrims.speedhg.game.GameState
import club.mcscrims.speedhg.game.impl.PreStartState.AnnouncementType
import club.mcscrims.speedhg.util.DirectionUtil
import club.mcscrims.spigot.scheduler.SchedulerManager
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.Sound
import org.bukkit.entity.Player
import org.bukkit.event.Event
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.block.Action
import org.bukkit.event.block.BlockBreakEvent
import org.bukkit.event.entity.EntityDamageEvent
import org.bukkit.event.entity.FoodLevelChangeEvent
import org.bukkit.event.inventory.CraftItemEvent
import org.bukkit.event.player.PlayerDropItemEvent
import org.bukkit.event.player.PlayerInteractEvent
import org.bukkit.inventory.ItemStack
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
class ImmunityState(
gameManager: GameManager,
plugin: SpeedHG,
schedulerManager: SchedulerManager,
durationSeconds: Int? = null
) : GameState( "immunity", gameManager, plugin, schedulerManager, durationSeconds ), Listener {
override fun onEnter(
previous: GameState?
) {
super.onEnter( previous )
registerListener( this )
val effects = listOf(
PotionEffect( PotionEffectType.HASTE, durationSeconds?.times( 20 ) ?: 0, 0 ),
PotionEffect( PotionEffectType.SPEED, durationSeconds?.times( 20 ) ?: 0, 0 )
)
val players = Bukkit.getOnlinePlayers()
players.forEach { it.addPotionEffects( effects ) }
for ( player in players )
{
player.inventory.clear()
player.inventory.armorContents = emptyArray()
player.inventory.setItem( 8, ItemStack( Material.COMPASS ))
TODO( "Give kits and perks" )
}
broadcast( "gameStates.immunity.warnings.butterfly" )
TODO( "register kits & perks" )
}
override fun onTick()
{
when( durationSeconds )
{
180 -> announce( AnnouncementType.MINUTES, 3 )
120 -> announce( AnnouncementType.MINUTES, 2 )
60 -> announce( AnnouncementType.MINUTES, 1 )
30 -> announce( AnnouncementType.SECONDS, 30 )
15 -> announce( AnnouncementType.SECONDS, 15 )
10 -> announce( AnnouncementType.SECONDS, 10 )
5 -> announce( AnnouncementType.SECONDS, 5 )
4 -> announce( AnnouncementType.SECONDS, 4 )
3 -> announce( AnnouncementType.SECONDS, 3 )
2 -> announce( AnnouncementType.SECONDS, 2 )
1 -> announce( AnnouncementType.SECONDS, 1 )
0 ->
{
broadcast( "gameStates.immunity.ended" )
gameManager.transitionTo( TODO() )
}
}
}
override fun onEndOfDuration() {}
private fun announce(
type: AnnouncementType,
time: Int
) {
val arg = if ( type == AnnouncementType.MINUTES ) "M" else "S"
broadcast( "gameStates.immunity.ending$arg", "{time}" to time.toString() )
}
@EventHandler
fun onInteractCompass(
event: PlayerInteractEvent
) {
val player = event.player
val action = event.action
if ( action != Action.RIGHT_CLICK_BLOCK &&
action != Action.RIGHT_CLICK_AIR )
return
val item = event.item
?: return
if ( item.type != Material.COMPASS )
return
val nearestPlayer = Bukkit.getOnlinePlayers().stream()
.filter { other -> other != player }
.map { other -> other.location.distance( player.location ) to other }
.filter { pair -> pair.first > 5.0 }
.toList()
.firstOrNull()
?.second
if ( nearestPlayer == null )
{
plugin.chatManager.sendMessage( player, "compass.no_nearby_players" )
return
}
player.compassTarget = nearestPlayer.location
val direction = DirectionUtil.getDirectionToPlayer( player, nearestPlayer )
val actionBar = plugin.chatFormatter.format( "compass.actionBar" )
actionBar.replaceText { it.match( "<direction>" ).replacement( direction ).once() }
plugin.chatManager.sendInteractiveMessage( player, actionBar )
}
@EventHandler
fun onDropItem(
event: PlayerDropItemEvent
) {
val player = event.player
TODO( "Add kit & perk check" )
}
@EventHandler
fun onCraftItem(
event: CraftItemEvent
) {
val player = event.whoClicked
if ( player !is Player )
return
val item = event.recipe.result
if ( item.type == Material.SHIELD )
{
if ( event.isShiftClick )
{
plugin.chatManager.sendMessage( player, "craft.no_siftclick" )
player.playSound( player, Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f )
return
}
event.result = Event.Result.DENY
plugin.chatManager.sendMessage( player, "craft.no_shield" )
player.playSound( player, Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f )
return
}
if (!item.type.name.contains( "iron", true ))
return
if ( event.isShiftClick )
{
plugin.chatManager.sendMessage( player, "craft.no_siftclick" )
player.playSound( player, Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f )
return
}
event.result = Event.Result.DENY
plugin.chatManager.sendMessage( player, "craft.no_iron_before_feast" )
player.playSound( player, Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f )
}
@EventHandler
fun onBlockBreak(
event: BlockBreakEvent
) {
val player = event.player
val block = event.block
if ( block.type == Material.DIAMOND_ORE )
{
event.isCancelled = true
block.type = Material.AIR
block.tick()
plugin.chatManager.sendMessage( player, "build.no_diamonds" )
player.playSound( player, Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f )
return
}
if ( block.type == Material.IRON_ORE )
{
event.isCancelled = true
plugin.chatManager.sendMessage( player, "build.no_iron_before_feast" )
player.playSound( player, Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f )
return
}
}
@EventHandler
fun onInteract(
event: PlayerInteractEvent
) {
val player = event.player
val action = event.action
if ( action != Action.RIGHT_CLICK_BLOCK &&
action != Action.RIGHT_CLICK_AIR )
return
val item = event.item
?: return
TODO( "Check for kit items" )
}
@EventHandler
fun onEntityDamage(
event: EntityDamageEvent
) {
if ( gameManager.getCurrentState() != this )
return
event.isCancelled = true
}
@EventHandler
fun onFoodLevelChange(
event: FoodLevelChangeEvent
) {
if ( gameManager.getCurrentState() != this )
return
event.isCancelled = true
}
}

View File

@@ -0,0 +1,194 @@
package club.mcscrims.speedhg.game.impl
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.game.GameManager
import club.mcscrims.speedhg.game.GameState
import club.mcscrims.spigot.scheduler.SchedulerManager
import org.bukkit.Bukkit
import org.bukkit.Location
import org.bukkit.Sound
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.block.BlockBreakEvent
import org.bukkit.event.block.BlockPlaceEvent
import org.bukkit.event.block.LeavesDecayEvent
import org.bukkit.event.entity.EntityDamageEvent
import org.bukkit.event.entity.FoodLevelChangeEvent
import org.bukkit.event.player.PlayerAttemptPickupItemEvent
import org.bukkit.event.player.PlayerDropItemEvent
class PreStartState(
gameManager: GameManager,
plugin: SpeedHG,
schedulerManager: SchedulerManager,
durationSeconds: Int? = null
) : GameState( "pre_start", gameManager, plugin, schedulerManager, durationSeconds ), Listener {
private lateinit var previous: GameState
override fun onEnter(
previous: GameState?
) {
super.onEnter( previous )
registerListener( this )
if ( previous != null )
this.previous = previous
}
var isStarting: Boolean = false
override fun onTick()
{
val playerSize = Bukkit.getOnlinePlayers().size
if ( playerSize < plugin.pluginConfig.data.game.minimumPlayers )
{
isStarting = false
transitionToPrevious()
}
else isStarting = true
if ( !isStarting )
return
if ( durationSeconds == 15 )
teleport()
when( durationSeconds )
{
300 -> announce( AnnouncementType.MINUTES, 5 )
240 -> announce( AnnouncementType.MINUTES, 4 )
180 -> announce( AnnouncementType.MINUTES, 3 )
120 -> announce( AnnouncementType.MINUTES, 2 )
60 -> announce( AnnouncementType.MINUTES, 1 )
30 -> announce( AnnouncementType.SECONDS, 30 )
15 -> announce( AnnouncementType.SECONDS, 15 )
10 -> announce( AnnouncementType.SECONDS, 10 )
5 -> announce( AnnouncementType.SECONDS, 5 )
4 -> announce( AnnouncementType.SECONDS, 4 )
3 -> announce( AnnouncementType.SECONDS, 3 )
2 -> announce( AnnouncementType.SECONDS, 2 )
1 -> announce( AnnouncementType.SECONDS, 1 )
0 ->
{
broadcast( "gameStates.preStart.started" )
isStarting = false
gameManager.transitionTo(ImmunityState( gameManager, plugin, schedulerManager, plugin.pluginConfig.data.getDuration( "immunity" ).seconds ))
}
}
}
override fun onExit(
next: GameState?
) {
super.onExit( next )
Bukkit.getOnlinePlayers().forEach { it.inventory.clear() }
}
override fun onEndOfDuration() {}
private fun announce(
type: AnnouncementType,
time: Int
) {
val arg = if ( type == AnnouncementType.MINUTES ) "M" else "S"
broadcast( "gameStates.preStart.starting$arg", "{time}" to time.toString() )
}
private fun teleport()
{
for ( player in Bukkit.getOnlinePlayers() )
{
val world = player.world
val loc = Location( world, 0.0, world.getHighestBlockYAt( 0, 0 ).toDouble(), 0.0 )
player.teleport( loc )
}
}
private fun transitionToPrevious()
{
if ( ::previous.isInitialized )
{
gameManager.transitionTo( previous )
return
}
gameManager.transitionTo(WaitingState( gameManager, plugin, schedulerManager, plugin.pluginConfig.data.getDuration( "waiting" ).seconds ))
}
@EventHandler
fun onLeavesDecay(
event: LeavesDecayEvent
) {
event.isCancelled = true
}
@EventHandler
fun onBlockBreak(
event: BlockBreakEvent
) {
if ( gameManager.getCurrentState() != this )
return
event.isCancelled = true
event.player.playSound( event.player, Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f )
}
@EventHandler
fun onBlockPlace(
event: BlockPlaceEvent
) {
if ( gameManager.getCurrentState() != this )
return
event.isCancelled = true
event.player.playSound( event.player, Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f )
}
@EventHandler
fun onPlayerPickupItem(
event: PlayerAttemptPickupItemEvent
) {
if ( gameManager.getCurrentState() != this )
return
event.isCancelled = true
}
@EventHandler
fun onPlayerDropItem(
event: PlayerDropItemEvent
) {
if ( gameManager.getCurrentState() != this )
return
event.isCancelled = true
event.player.playSound( event.player, Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f )
}
@EventHandler
fun onEntityDamage(
event: EntityDamageEvent
) {
if ( gameManager.getCurrentState() != this )
return
event.isCancelled = true
}
@EventHandler
fun onFoodLevelChange(
event: FoodLevelChangeEvent
) {
if ( gameManager.getCurrentState() != this )
return
event.isCancelled = true
}
internal enum class AnnouncementType {
MINUTES, SECONDS
}
}

View File

@@ -0,0 +1,121 @@
package club.mcscrims.speedhg.game.impl
import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.game.GameManager
import club.mcscrims.speedhg.game.GameState
import club.mcscrims.spigot.scheduler.SchedulerManager
import org.bukkit.Bukkit
import org.bukkit.Sound
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.block.BlockBreakEvent
import org.bukkit.event.block.BlockPlaceEvent
import org.bukkit.event.block.LeavesDecayEvent
import org.bukkit.event.entity.EntityDamageEvent
import org.bukkit.event.entity.FoodLevelChangeEvent
import org.bukkit.event.player.PlayerAttemptPickupItemEvent
import org.bukkit.event.player.PlayerDropItemEvent
class WaitingState(
gameManager: GameManager,
plugin: SpeedHG,
schedulerManager: SchedulerManager,
durationSeconds: Int? = null
) : GameState( "waiting", gameManager, plugin, schedulerManager, durationSeconds ), Listener {
override fun onEnter(
previous: GameState?
) {
super.onEnter( previous )
registerListener( this )
}
private var secondsCounter: Int = 0
override fun onTick()
{
val playerSize = Bukkit.getOnlinePlayers().size
if ( playerSize < plugin.pluginConfig.data.game.minimumPlayers )
if ( secondsCounter == 15 )
{
broadcast( "gameStates.waiting.awaiting_players", "{min_players}" to plugin.pluginConfig.data.game.minimumPlayers.toString() )
secondsCounter = 0
}
else secondsCounter++
else gameManager.transitionTo(PreStartState( gameManager, plugin, schedulerManager, plugin.pluginConfig.data.getDuration( "pre_start" ).seconds ))
}
override fun onEndOfDuration() {}
@EventHandler
fun onLeavesDecay(
event: LeavesDecayEvent
) {
event.isCancelled = true
}
@EventHandler
fun onBlockBreak(
event: BlockBreakEvent
) {
if ( gameManager.getCurrentState() != this )
return
event.isCancelled = true
event.player.playSound( event.player, Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f )
}
@EventHandler
fun onBlockPlace(
event: BlockPlaceEvent
) {
if ( gameManager.getCurrentState() != this )
return
event.isCancelled = true
event.player.playSound( event.player, Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f )
}
@EventHandler
fun onPlayerPickupItem(
event: PlayerAttemptPickupItemEvent
) {
if ( gameManager.getCurrentState() != this )
return
event.isCancelled = true
}
@EventHandler
fun onPlayerDropItem(
event: PlayerDropItemEvent
) {
if ( gameManager.getCurrentState() != this )
return
event.isCancelled = true
event.player.playSound( event.player, Sound.BLOCK_NOTE_BLOCK_BASS, 1f, 1f )
}
@EventHandler
fun onEntityDamage(
event: EntityDamageEvent
) {
if ( gameManager.getCurrentState() != this )
return
event.isCancelled = true
}
@EventHandler
fun onFoodLevelChange(
event: FoodLevelChangeEvent
) {
if ( gameManager.getCurrentState() != this )
return
event.isCancelled = true
}
}

View File

@@ -0,0 +1,50 @@
package club.mcscrims.speedhg.util
import club.mcscrims.speedhg.SpeedHG
import net.kyori.adventure.text.Component
import org.bukkit.entity.Player
import org.bukkit.util.Vector
import kotlin.math.atan2
object DirectionUtil {
private val plugin = SpeedHG.instance
private val directions = arrayOf("north", "northEast", "east", "southEast", "south", "southWest", "west", "northWest")
fun getDirectionToPlayer(
player: Player,
nearestPlayer: Player
): Component
{
val yaw = getDirection( player, nearestPlayer )
var normalizedYaw = yaw % 360
if ( normalizedYaw < 0 )
normalizedYaw += 360
val index = (( normalizedYaw + 22.5 ) / 45 ).toInt() % 8
return plugin.chatFormatter.format( "compass.directions.${directions[ index ]}" )
}
private fun getDirection(
fromPlayer: Player,
toPlayer: Player
): Double
{
val fromLocation = fromPlayer.location
val toLocation = toPlayer.location
val directionVector = Vector( toLocation.x - fromLocation.x, 0.0, toLocation.z - fromLocation.z ).normalize()
val angle = atan2( directionVector.x, directionVector.z )
val yaw = Math.toDegrees( angle )
var adjustedYaw = yaw + 180
if ( adjustedYaw < 0)
adjustedYaw += 360
return adjustedYaw
}
}

View File

@@ -54,14 +54,28 @@ game:
- 'lobby'
- 'l'
playerState:
waiting: 'Waiting - %time%'
preStart: 'Waiting - %time%'
immunity: 'Playing - %time%'
battle: 'Playing - %time%'
feast: 'Playing - %time%'
deathmatch: 'Playing - %time%'
end: 'Ending - %time%'
playerStates:
waiting:
scoreboard: 'Waiting - %time%'
duration: FIXED:-1
preStart:
scoreboard: 'Waiting - %time%'
duration: FIXED:300
immunity:
scoreboard: 'Playing - %time%'
duration: FIXED:90
battle:
scoreboard: 'Playing - %time%'
duration: INCREASING
feast:
scoreboard: 'Playing - %time%'
duration: FIXED:300
deathmatch:
scoreboard: 'Playing - %time%'
duration: INCREASING
end:
scoreboard: 'Ending - %time%'
duration: FIXED:60
recraftNerf:
enabled: false