summaryrefslogtreewikicommitdiff
path: root/core/src/game/Game.kt
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/game/Game.kt')
-rw-r--r--core/src/game/Game.kt757
1 files changed, 757 insertions, 0 deletions
diff --git a/core/src/game/Game.kt b/core/src/game/Game.kt
new file mode 100644
index 0000000..70187e3
--- /dev/null
+++ b/core/src/game/Game.kt
@@ -0,0 +1,757 @@
+package cat.freya.khs.game
+
+import cat.freya.khs.Khs
+import cat.freya.khs.config.ConfigCountdownDisplay
+import cat.freya.khs.config.ConfigLeaveType
+import cat.freya.khs.config.ConfigScoringMode
+import cat.freya.khs.inv.createBlockHuntPicker
+import cat.freya.khs.player.Player
+import cat.freya.khs.world.Item
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.math.min
+import kotlin.math.round
+import kotlin.random.Random
+import kotlin.synchronized
+
+class Game(val plugin: Khs) {
+ /// represents what state the game is in
+ enum class Status {
+ LOBBY,
+ HIDING,
+ SEEKING,
+ FINISHED;
+
+ fun inProgress(): Boolean =
+ when (this) {
+ LOBBY -> false
+ HIDING -> true
+ SEEKING -> true
+ FINISHED -> false
+ }
+ }
+
+ /// what team a player is on
+ enum class Team {
+ HIDER,
+ SEEKER,
+ SPECTATOR,
+ }
+
+ /// why was the game stopped?
+ enum class WinType {
+ NONE,
+ SEEKER_WIN,
+ HIDER_WIN,
+ }
+
+ @Volatile
+ /// the state the game is in
+ var status: Status = Status.LOBBY
+ private set
+
+ @Volatile
+ /// timer for current game status (lobby, hiding, seeking, finished)
+ var timer: ULong? = null
+ private set
+
+ @Volatile
+ /// keep track till next second
+ private var gameTick: UInt = 0u
+ private val isSecond: Boolean
+ get() = gameTick % 20u == 0u
+
+ @Volatile
+ /// if the last event was a hider leaving the game
+ private var hiderLeft: Boolean = false
+
+ @Volatile
+ /// the current game round
+ private var round: Int = 0
+
+ val glow: Glow = Glow(this)
+ val taunt: Taunt = Taunt(this)
+ val border: Border = Border(this)
+
+ @Volatile
+ var map: KhsMap? = null
+ private set
+
+ private val mappings: MutableMap<UUID, Team> = ConcurrentHashMap<UUID, Team>()
+
+ val players: List<Player>
+ get() = mappings.keys.map { plugin.shim.getPlayer(it) }.filterNotNull()
+
+ val UUIDs: Set<UUID>
+ get() = mappings.keys.toSet()
+
+ val hiderUUIDs: Set<UUID>
+ get() = mappings.filter { it.value == Team.HIDER }.keys
+
+ val hiderPlayers: List<Player>
+ get() = hiderUUIDs.map { plugin.shim.getPlayer(it) }.filterNotNull()
+
+ val seekerUUIDs: Set<UUID>
+ get() = mappings.filter { it.value == Team.SEEKER }.keys
+
+ val seekerPlayers: List<Player>
+ get() = seekerUUIDs.map { plugin.shim.getPlayer(it) }.filterNotNull()
+
+ val spectatorUUIDs: Set<UUID>
+ get() = mappings.filter { it.value == Team.SPECTATOR }.keys
+
+ val spectatorPlayers: List<Player>
+ get() = spectatorUUIDs.map { plugin.shim.getPlayer(it) }.filterNotNull()
+
+ val size: UInt
+ get() = mappings.size.toUInt()
+
+ val hiderSize: UInt
+ get() = hiderUUIDs.size.toUInt()
+
+ val seekerSize: UInt
+ get() = seekerUUIDs.size.toUInt()
+
+ val spectatorsSize: UInt
+ get() = spectatorUUIDs.size.toUInt()
+
+ fun hasPlayer(uuid: UUID): Boolean = mappings.containsKey(uuid)
+
+ fun hasPlayer(player: Player): Boolean = hasPlayer(player.uuid)
+
+ fun isHider(uuid: UUID): Boolean = mappings[uuid] == Team.HIDER
+
+ fun isHider(player: Player): Boolean = isHider(player.uuid)
+
+ fun isSeeker(uuid: UUID): Boolean = mappings[uuid] == Team.SEEKER
+
+ fun isSeeker(player: Player): Boolean = isSeeker(player.uuid)
+
+ fun isSpectator(uuid: UUID): Boolean = mappings[uuid] == Team.SPECTATOR
+
+ fun isSpectator(player: Player): Boolean = isSpectator(player.uuid)
+
+ fun getTeam(uuid: UUID): Team? = mappings.get(uuid)
+
+ fun setTeam(uuid: UUID, team: Team) {
+ mappings[uuid] = team
+ }
+
+ fun sameTeam(a: UUID, b: UUID): Boolean = mappings[a] == mappings[b]
+
+ // what round was the uuid last picked to be seeker
+ private val lastPicked: MutableMap<UUID, Int> = ConcurrentHashMap<UUID, Int>()
+
+ @Volatile
+ // teams at the start of the game
+ private var initialTeams: Map<UUID, Team> = emptyMap()
+
+ @Volatile
+ // stores saved inventories
+ private var savedInventories: MutableMap<UUID, List<Item?>> =
+ ConcurrentHashMap<UUID, List<Item?>>()
+
+ // status for this round
+ private var hiderKills: MutableMap<UUID, UInt> = ConcurrentHashMap<UUID, UInt>()
+ private var seekerKills: MutableMap<UUID, UInt> = ConcurrentHashMap<UUID, UInt>()
+ private var hiderDeaths: MutableMap<UUID, UInt> = ConcurrentHashMap<UUID, UInt>()
+ private var seekerDeaths: MutableMap<UUID, UInt> = ConcurrentHashMap<UUID, UInt>()
+
+ fun doTick() {
+ if (map?.setup != true) return
+
+ when (status) {
+ Status.LOBBY -> whileWaiting()
+ Status.HIDING -> whileHiding()
+ Status.SEEKING -> whileSeeking()
+ Status.FINISHED -> whileFinished()
+ }
+
+ gameTick++
+ }
+
+ fun selectMap(): KhsMap? {
+ map = map ?: plugin.maps.values.filter { it.setup }.randomOrNull()
+ return map
+ }
+
+ fun setMap(map: KhsMap?) {
+ if (status != Status.LOBBY) return
+
+ if (map == null && size > 0u) return
+
+ this.map = map
+ players.forEach { player -> joinPlayer(player) }
+ }
+
+ fun getSeekerWeight(uuid: UUID): Double {
+ val last = lastPicked[uuid] ?: -1000
+ val weight = (round - last).toDouble()
+ return weight
+ }
+
+ fun getSeekerChance(uuid: UUID): Double {
+ val weights = mappings.keys.map { getSeekerWeight(it) }
+ val totalWeight = weights.sum()
+ val weight = getSeekerWeight(uuid)
+ if (totalWeight == 0.0) return 0.0
+ return weight / totalWeight
+ }
+
+ private fun randomSeeker(pool: Set<UUID>): UUID {
+ val weights = pool.map { uuid -> uuid to getSeekerWeight(uuid) }
+
+ val totalWeight = weights.sumOf { it.second }
+ var r = Random.nextDouble() * totalWeight
+
+ for ((uuid, weight) in weights) {
+ r -= weight
+ if (r <= 0) {
+ lastPicked[uuid] = round
+ return uuid
+ }
+ }
+
+ return pool.random()
+ }
+
+ fun start() {
+ start(emptySet())
+ }
+
+ fun start(requestedPool: Collection<UUID>) {
+ val seekers = mutableSetOf<UUID>()
+ val pool = if (requestedPool.isEmpty()) mappings.keys else requestedPool.toMutableSet()
+
+ while (pool.size >= 2 && seekers.size.toUInt() < plugin.config.startingSeekerCount) {
+ val uuid = randomSeeker(pool)
+ pool.remove(uuid)
+ seekers.add(uuid)
+ }
+
+ if (seekers.isEmpty()) // warning here?
+ return
+
+ startWithSeekers(seekers)
+ }
+
+ private fun startWithSeekers(seekers: Set<UUID>) {
+ if (status != Status.LOBBY) return
+
+ if (plugin.config.mapSaveEnabled) map?.loader?.rollback()
+
+ synchronized(this) {
+ // set teams
+ mappings.forEach { mappings[it.key] = Team.HIDER }
+ seekers.forEach { mappings[it] = Team.SEEKER }
+
+ // reset game state
+ initialTeams = mappings.toMap()
+ hiderKills.clear()
+ seekerKills.clear()
+ hiderDeaths.clear()
+ seekerDeaths.clear()
+
+ // give items
+ loadHiders()
+ loadSeekers()
+
+ // reload sidebar
+ reloadGameBoards()
+
+ glow.reset()
+ taunt.reset()
+ border.reset()
+
+ status = Status.HIDING
+ timer = null
+ }
+ }
+
+ private fun updatePlayerInfo(uuid: UUID, reason: WinType) {
+ val team = initialTeams.get(uuid) ?: return
+ val data = plugin.database?.getPlayer(uuid) ?: return
+
+ when (reason) {
+ WinType.SEEKER_WIN -> {
+ if (team == Team.SEEKER) data.seekerWins++
+ if (team == Team.HIDER) data.hiderLosses++
+ }
+ WinType.HIDER_WIN -> {
+ if (team == Team.SEEKER) data.seekerLosses++
+ if (team == Team.HIDER) data.hiderWins++
+ }
+ WinType.NONE -> {}
+ }
+
+ data.seekerKills += seekerKills.getOrDefault(uuid, 0u)
+ data.hiderKills += hiderKills.getOrDefault(uuid, 0u)
+ data.seekerDeaths += seekerDeaths.getOrDefault(uuid, 0u)
+ data.hiderDeaths += hiderDeaths.getOrDefault(uuid, 0u)
+
+ plugin.database?.upsertPlayer(data)
+ }
+
+ fun stop(reason: WinType) {
+ if (!status.inProgress()) return
+
+ // update database
+ mappings.keys.forEach { updatePlayerInfo(it, reason) }
+
+ round++
+ status = Status.FINISHED
+ timer = null
+
+ if (plugin.config.leaveOnEnd) {
+ mappings.keys.forEach { leave(it) }
+ }
+ }
+
+ fun join(uuid: UUID) {
+ val player = plugin.shim.getPlayer(uuid) ?: return
+
+ if (map == null) selectMap()
+
+ if (map == null) {
+ player.message(plugin.locale.prefix.error + plugin.locale.map.none)
+ return
+ }
+
+ if (status != Status.LOBBY) {
+ mappings[uuid] = Team.SPECTATOR
+ loadSpectator(player)
+ reloadGameBoards()
+ player.message(plugin.locale.prefix.default + plugin.locale.game.join)
+ return
+ }
+
+ if (plugin.config.saveInventory) savedInventories[uuid] = player.inventory.contents
+
+ mappings[uuid] = Team.HIDER
+ joinPlayer(player)
+ reloadLobbyBoards()
+
+ broadcast(plugin.locale.prefix.default + plugin.locale.lobby.join.with(player.name))
+ }
+
+ fun leave(uuid: UUID) {
+ val player = plugin.shim.getPlayer(uuid) ?: return
+
+ broadcast(plugin.locale.prefix.default + plugin.locale.game.leave.with(player.name))
+
+ mappings.remove(uuid)
+ resetPlayer(player)
+
+ if (plugin.config.saveInventory)
+ savedInventories.get(uuid)?.let { player.inventory.contents = it }
+
+ // reload sidebar
+ player.hideBoards()
+ if (status.inProgress()) {
+ reloadGameBoards()
+ } else {
+ reloadLobbyBoards()
+ }
+
+ when (plugin.config.leaveType) {
+ ConfigLeaveType.EXIT -> plugin.config.exit?.let { player.teleport(it) }
+ ConfigLeaveType.PROXY -> player.sendToServer(plugin.config.leaveServer)
+ }
+ }
+
+ fun addKill(uuid: UUID) {
+ val team = mappings.get(uuid) ?: return
+ when (team) {
+ Team.HIDER -> hiderKills[uuid] = hiderKills.getOrDefault(uuid, 0u) + 1u
+ Team.SEEKER -> seekerKills[uuid] = seekerKills.getOrDefault(uuid, 0u) + 1u
+ else -> {}
+ }
+ }
+
+ fun addDeath(uuid: UUID) {
+ val team = mappings.get(uuid) ?: return
+ when (team) {
+ Team.HIDER -> hiderDeaths[uuid] = hiderDeaths.getOrDefault(uuid, 0u) + 1u
+ Team.SEEKER -> seekerDeaths[uuid] = seekerDeaths.getOrDefault(uuid, 0u) + 1u
+ else -> {}
+ }
+ }
+
+ private fun reloadLobbyBoards() {
+ mappings.keys.forEach { reloadLobbyBoard(plugin, it) }
+ }
+
+ private fun reloadGameBoards() {
+ mappings.keys.forEach { reloadGameBoard(plugin, it) }
+ }
+
+ /// during Status.LOBBY
+ private fun whileWaiting() {
+ val countdown = plugin.config.lobby.countdown
+ val changeCountdown = plugin.config.lobby.changeCountdown
+
+ synchronized(this) {
+ // countdown is disabled when set to at 0s
+ if (countdown == 0UL || size < plugin.config.lobby.min) {
+ timer = null
+ return@synchronized
+ }
+
+ var time = timer ?: countdown
+ if (size >= changeCountdown && changeCountdown != 0u) time = min(time, 10UL)
+ if (isSecond && time > 0UL) time--
+ timer = time
+ }
+
+ if (isSecond) reloadLobbyBoards()
+
+ if (timer == 0UL) start()
+ }
+
+ /// during Status.HIDING
+ private fun whileHiding() {
+ if (!isSecond) return
+
+ if (timer != 0UL) checkWinConditions()
+
+ if (isSecond) reloadGameBoards()
+
+ val time: ULong
+ val message: String
+ synchronized(this) {
+ time = timer ?: plugin.config.hidingLength
+ when (time) {
+ 0UL -> {
+ message = plugin.locale.game.start
+ status = Status.SEEKING
+ timer = null
+ seekerPlayers.forEach {
+ giveSeekerItems(it)
+ map?.gameSpawn?.teleport(it)
+ }
+ hiderPlayers.forEach { giveHiderItems(it) }
+ }
+ 1UL -> message = plugin.locale.game.countdown.last
+ else -> message = plugin.locale.game.countdown.notify.with(time)
+ }
+
+ if (status == Status.HIDING) timer = if (time > 0UL) (time - 1UL) else time
+ }
+
+ if (time % 5UL == 0UL || time <= 5UL) {
+ val prefix = plugin.locale.prefix.default
+ players.forEach { player ->
+ when (plugin.config.countdownDisplay) {
+ ConfigCountdownDisplay.CHAT -> player.message(prefix + message)
+ ConfigCountdownDisplay.ACTIONBAR -> player.actionBar(prefix + message)
+ ConfigCountdownDisplay.TITLE -> {
+ if (time != 30UL) player.title(" ", message)
+ }
+ }
+ }
+ }
+ }
+
+ /// @returns distnace to closest seeker to the player
+ private fun distanceToSeeker(player: Player): Double {
+ return seekerPlayers.map { seeker -> player.location.distance(seeker.location) }.minOrNull()
+ ?: Double.POSITIVE_INFINITY
+ }
+
+ /// plays the seeker ping for a hider
+ private fun playSeekerPing(hider: Player) {
+ val distance = distanceToSeeker(hider)
+
+ // read config values
+ val distances = plugin.config.seekerPing.distances
+ val sounds = plugin.config.seekerPing.sounds
+
+ when (gameTick % 10u) {
+ 0u -> {
+ if (distance < distances.level1.toDouble())
+ hider.playSound(sounds.heartbeatNoise, sounds.leadingVolume, sounds.pitch)
+ if (distance < distances.level3.toDouble())
+ hider.playSound(sounds.ringingNoise, sounds.volume, sounds.pitch)
+ }
+ 3u -> {
+ if (distance < distances.level1.toDouble())
+ hider.playSound(sounds.heartbeatNoise, sounds.volume, sounds.pitch)
+ if (distance < distances.level3.toDouble())
+ hider.playSound(sounds.ringingNoise, sounds.volume, sounds.pitch)
+ }
+ 6u -> {
+ if (distance < distances.level3.toDouble())
+ hider.playSound(sounds.ringingNoise, sounds.volume, sounds.pitch)
+ }
+ 9u -> {
+ if (distance < distances.level2.toDouble())
+ hider.playSound(sounds.ringingNoise, sounds.volume, sounds.pitch)
+ }
+ }
+ }
+
+ private fun checkWinConditions() {
+ var stopReason: WinType? = null
+
+ val scoreMode = plugin.config.scoringMode
+ val notEnoughHiders =
+ when (scoreMode) {
+ ConfigScoringMode.ALL_HIDERS_FOUND -> hiderSize == 0u
+ ConfigScoringMode.LAST_HIDER_WINS -> hiderSize == 1u
+ }
+ val lastHider = hiderPlayers.firstOrNull()
+
+ val doTitle = plugin.config.gameOverTitle
+ val prefix = plugin.locale.prefix
+
+ when {
+ // time ran out
+ timer == 0UL -> {
+ broadcast(prefix.gameOver + plugin.locale.game.gameOver.time)
+ if (doTitle)
+ broadcastTitle(
+ plugin.locale.game.title.hidersWin,
+ plugin.locale.game.gameOver.time,
+ )
+ stopReason = WinType.HIDER_WIN
+ }
+ // all seekers quit
+ seekerSize < 1u -> {
+ broadcast(prefix.abort + plugin.locale.game.gameOver.seekerQuit)
+ if (doTitle)
+ broadcastTitle(
+ plugin.locale.game.title.noWin,
+ plugin.locale.game.gameOver.seekerQuit,
+ )
+ stopReason = if (plugin.config.dontRewardQuit) WinType.NONE else WinType.HIDER_WIN
+ }
+ // hiders quit
+ notEnoughHiders && hiderLeft -> {
+ broadcast(prefix.abort + plugin.locale.game.gameOver.hiderQuit)
+ if (doTitle)
+ broadcastTitle(
+ plugin.locale.game.title.noWin,
+ plugin.locale.game.gameOver.hiderQuit,
+ )
+ stopReason = if (plugin.config.dontRewardQuit) WinType.NONE else WinType.SEEKER_WIN
+ }
+ // all hiders found
+ notEnoughHiders && lastHider == null -> {
+ broadcast(prefix.gameOver + plugin.locale.game.gameOver.hidersFound)
+ if (doTitle)
+ broadcastTitle(
+ plugin.locale.game.title.seekersWin,
+ plugin.locale.game.gameOver.hidersFound,
+ )
+ stopReason = WinType.SEEKER_WIN
+ }
+ // last hider wins (depends on scoring more)
+ notEnoughHiders && lastHider != null -> {
+ val msg = plugin.locale.game.gameOver.lastHider.with(lastHider.name)
+ broadcast(prefix.gameOver + msg)
+ if (doTitle)
+ broadcastTitle(
+ plugin.locale.game.title.singleHiderWin.with(lastHider.name),
+ msg,
+ )
+ stopReason = WinType.HIDER_WIN
+ }
+ }
+
+ if (stopReason != null) stop(stopReason)
+
+ hiderLeft = false
+ }
+
+ /// during Status.SEEKING
+ private fun whileSeeking() {
+ if (plugin.config.seekerPing.enabled) hiderPlayers.forEach { playSeekerPing(it) }
+
+ synchronized(this) {
+ var time = timer
+ if (time == null && plugin.config.gameLength != 0UL) time = plugin.config.gameLength
+
+ if (isSecond) {
+ if (time != null && time > 0UL) time--
+
+ taunt.update()
+ glow.update()
+ border.update()
+ }
+
+ timer = time
+ }
+
+ if (isSecond) reloadGameBoards()
+
+ // update spectator flight
+ // (the toggle they have only changed allowed flight)
+ spectatorPlayers.forEach { it.flying = it.allowFlight }
+
+ checkWinConditions()
+ }
+
+ /// during Status.FINISHED
+ private fun whileFinished() {
+ synchronized(this) {
+ var time = timer ?: plugin.config.endGameDelay
+ if (time > 0UL) time--
+
+ timer = time
+
+ if (time == 0UL) {
+ timer = null
+ map = null
+ selectMap()
+
+ if (map == null) {
+ broadcast(plugin.locale.prefix.warning + plugin.locale.map.none)
+ return
+ }
+
+ status = Status.LOBBY
+
+ players.forEach { joinPlayer(it) }
+ }
+ }
+ }
+
+ fun broadcast(message: String) {
+ players.forEach { it.message(message) }
+ }
+
+ fun broadcastTitle(title: String, subTitle: String) {
+ players.forEach { it.title(title, subTitle) }
+ }
+
+ private fun loadHiders() = hiderPlayers.forEach { loadHider(it) }
+
+ private fun loadSeekers() = seekerPlayers.forEach { loadSeeker(it) }
+
+ private fun hidePlayer(player: Player, hidden: Boolean) {
+ players.forEach { other -> if (other.uuid != player.uuid) other.setHidden(player, hidden) }
+ }
+
+ fun resetPlayer(player: Player) {
+ player.flying = false
+ player.allowFlight = false
+ player.setGameMode(Player.GameMode.ADVENTURE)
+ player.inventory.clear()
+ player.clearEffects()
+ player.hunger = 20u
+ player.heal()
+ player.revealDisguise()
+ hidePlayer(player, false)
+ }
+
+ fun loadHider(hider: Player) {
+ map?.gameSpawn?.teleport(hider)
+ resetPlayer(hider)
+ hider.setSpeed(5u)
+ hider.title(plugin.locale.game.team.hider, plugin.locale.game.team.hiderSubtitle)
+
+ // open block hunt picker
+ if (map?.config?.blockHunt?.enabled == true) {
+ val map = map ?: return
+ val inv = createBlockHuntPicker(plugin, map) ?: return
+ hider.showInventory(inv)
+ }
+ }
+
+ fun giveHiderItems(hider: Player) {
+ var items = plugin.itemsConfig.hiderItems.map { plugin.shim.parseItem(it) }.filterNotNull()
+ var effects =
+ plugin.itemsConfig.hiderEffects.map { plugin.shim.parseEffect(it) }.filterNotNull()
+
+ hider.inventory.clear()
+ for ((i, item) in items.withIndex()) hider.inventory.set(i.toUInt(), item)
+
+ // glow powerup
+ if (!plugin.config.alwaysGlow && plugin.config.glow.enabled) {
+ val item = plugin.shim.parseItem(plugin.config.glow.item)
+ item?.let { hider.inventory.set(items.size.toUInt(), it) }
+ }
+
+ plugin.itemsConfig.hiderHelmet
+ ?.let { plugin.shim.parseItem(it) }
+ ?.let { hider.inventory.helmet = it }
+ plugin.itemsConfig.hiderChestplate
+ ?.let { plugin.shim.parseItem(it) }
+ ?.let { hider.inventory.chestplate = it }
+ plugin.itemsConfig.hiderLeggings
+ ?.let { plugin.shim.parseItem(it) }
+ ?.let { hider.inventory.leggings = it }
+ plugin.itemsConfig.hiderBoots
+ ?.let { plugin.shim.parseItem(it) }
+ ?.let { hider.inventory.boots = it }
+
+ hider.clearEffects()
+ for (effect in effects) hider.giveEffect(effect)
+ }
+
+ fun loadSeeker(seeker: Player) {
+ map?.seekerLobbySpawn?.teleport(seeker)
+ resetPlayer(seeker)
+ seeker.title(plugin.locale.game.team.seeker, plugin.locale.game.team.seekerSubtitle)
+ }
+
+ fun giveSeekerItems(seeker: Player) {
+ var items = plugin.itemsConfig.seekerItems.map { plugin.shim.parseItem(it) }.filterNotNull()
+ var effects =
+ plugin.itemsConfig.seekerEffects.map { plugin.shim.parseEffect(it) }.filterNotNull()
+
+ seeker.inventory.clear()
+ for ((i, item) in items.withIndex()) seeker.inventory.set(i.toUInt(), item)
+
+ plugin.itemsConfig.seekerHelmet
+ ?.let { plugin.shim.parseItem(it) }
+ ?.let { seeker.inventory.helmet = it }
+ plugin.itemsConfig.seekerChestplate
+ ?.let { plugin.shim.parseItem(it) }
+ ?.let { seeker.inventory.chestplate = it }
+ plugin.itemsConfig.seekerLeggings
+ ?.let { plugin.shim.parseItem(it) }
+ ?.let { seeker.inventory.leggings = it }
+ plugin.itemsConfig.seekerBoots
+ ?.let { plugin.shim.parseItem(it) }
+ ?.let { seeker.inventory.boots = it }
+
+ seeker.clearEffects()
+ for (effect in effects) seeker.giveEffect(effect)
+ }
+
+ fun loadSpectator(spectator: Player) {
+ map?.gameSpawn?.teleport(spectator)
+ resetPlayer(spectator)
+ spectator.allowFlight = true
+ spectator.flying = true
+
+ plugin.config.spectatorItems.teleport
+ .let { plugin.shim.parseItem(it) }
+ ?.let { spectator.inventory.set(3u, it) }
+
+ plugin.config.spectatorItems.flight
+ .let { plugin.shim.parseItem(it) }
+ ?.let { spectator.inventory.set(6u, it) }
+
+ hidePlayer(spectator, true)
+ }
+
+ private fun joinPlayer(player: Player) {
+ map?.lobbySpawn?.teleport(player)
+ resetPlayer(player)
+
+ plugin.config.lobby.leaveItem
+ .let { plugin.shim.parseItem(it) }
+ ?.let { player.inventory.set(0u, it) }
+
+ if (player.hasPermission("hs.start")) {
+ plugin.config.lobby.startItem
+ .let { plugin.shim.parseItem(it) }
+ ?.let { player.inventory.set(8u, it) }
+ }
+
+ if (getTeam(player.uuid) != Team.SPECTATOR)
+ spectatorPlayers.forEach { player.setHidden(it, true) }
+ }
+}