1
0
Fork 0

First commit

This commit is contained in:
Moritz Ruth 2020-05-31 19:07:57 +02:00
commit 5ced725265
No known key found for this signature in database
GPG key ID: AFD57E23E753841B
57 changed files with 2259 additions and 0 deletions

BIN
libs/ActionBarAPI.jar Normal file

Binary file not shown.

BIN
libs/CorpseReborn.jar Normal file

Binary file not shown.

View file

@ -0,0 +1,13 @@
package de.moritzruth.spigot_ttt
import de.moritzruth.spigot_ttt.game.AbortCommand
import de.moritzruth.spigot_ttt.game.StartCommand
import de.moritzruth.spigot_ttt.items.AddItemSpawnCommand
object CommandManager {
fun initializeCommands() {
StartCommand()
AbortCommand()
AddItemSpawnCommand()
}
}

View file

@ -0,0 +1,22 @@
package de.moritzruth.spigot_ttt
import org.bukkit.Material
object CustomItems {
val textureless = Material.WHITE_STAINED_GLASS_PANE
val deathReason = Material.GRAY_STAINED_GLASS_PANE
// Roles
val innocent = Material.GREEN_STAINED_GLASS_PANE
val detective = Material.YELLOW_STAINED_GLASS_PANE
val traitor = Material.RED_STAINED_GLASS_PANE
val jackal = Material.LIGHT_BLUE_STAINED_GLASS_PANE
val sidekick = Material.LIGHT_BLUE_STAINED_GLASS_PANE
// Weapons
val deagle = Material.GOLDEN_HOE
val glock = Material.STONE_HOE
val pistol = Material.IRON_HOE
val shotgun = Material.IRON_AXE
val knife = Material.IRON_SWORD
}

View file

@ -0,0 +1,43 @@
package de.moritzruth.spigot_ttt
import de.moritzruth.spigot_ttt.discord.DiscordBot
import de.moritzruth.spigot_ttt.game.GameManager
import org.bukkit.ChatColor
import org.bukkit.plugin.java.JavaPlugin
class TTTPlugin: JavaPlugin() {
init {
pluginInstance = this
}
override fun onEnable() {
saveDefaultConfig()
CommandManager.initializeCommands()
GameManager.initialize()
DiscordBot.start()
}
override fun onDisable() {
GameManager.resetWorld()
DiscordBot.stop()
}
fun broadcast(message: String, withPrefix: Boolean = true) {
if (withPrefix) {
server.broadcastMessage(prefix + message)
} else {
server.broadcastMessage(message)
}
}
companion object {
val prefix = "${ChatColor.WHITE}${ChatColor.RESET} "
}
}
private var pluginInstance: TTTPlugin? = null
val plugin: TTTPlugin
get() = pluginInstance ?: throw Error("The plugin is not initialized yet (How are you even calling this???)")

View file

@ -0,0 +1,148 @@
package de.moritzruth.spigot_ttt.discord
import de.moritzruth.spigot_ttt.plugin
import de.moritzruth.spigot_ttt.utils.randomNumber
import de.moritzruth.spigot_ttt.utils.secondsToTicks
import de.moritzruth.spigot_ttt.utils.toTrimmedString
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.JDABuilder
import net.dv8tion.jda.api.OnlineStatus
import net.dv8tion.jda.api.entities.Activity
import net.dv8tion.jda.api.entities.User
import net.dv8tion.jda.api.events.message.priv.PrivateMessageReceivedEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter
import org.bukkit.ChatColor
import org.bukkit.scheduler.BukkitTask
import java.awt.Color
import java.util.*
object DiscordBot: ListenerAdapter() {
private val MC_PREFIX = "${ChatColor.GRAY}[${ChatColor.AQUA}Discord${ChatColor.GRAY}]${ChatColor.RESET} "
private val validations = mutableSetOf<ValidationData>()
lateinit var jda: JDA
private val guild get() = jda.guilds[0]!!
fun start() {
jda = JDABuilder.createDefault(plugin.config.getString("discord-token")).build()
jda.presence.setStatus(OnlineStatus.ONLINE)
jda.presence.activity = Activity.of(Activity.ActivityType.CUSTOM_STATUS, "Schick mir eine PN mit deinem Minecraft-Namen")
jda.addEventListener(this)
}
fun stop() {
jda.shutdownNow()
}
override fun onPrivateMessageReceived(event: PrivateMessageReceivedEvent) {
if (event.author == jda.selfUser) return
val data = validations.find { it.userID == event.author.id }
if (data == null) {
val player = plugin.server.getPlayerExact(event.message.contentRaw)
if (player == null) {
event.message.privateChannel.sendMessage(EmbedBuilder()
.setTitle("Dieser Spieler existiert nicht oder ist nicht online.")
.setColor(Color.RED)
.build()
).queue()
} else {
val existingConnectionOfPlayer = DiscordConnections.getByPlayerUUID(player.uniqueId)
val avatarURL = "https://crafatar.com/avatars/${player.uniqueId.toTrimmedString()}"
if (existingConnectionOfPlayer != null) {
if (existingConnectionOfPlayer.userID == event.author.id) {
event.message.privateChannel.sendMessage(EmbedBuilder()
.setTitle("Du hast diesen Account bereits verknüpft.")
.setColor(Color.GREEN)
.setThumbnail(avatarURL)
.build()
).queue()
} else {
event.message.privateChannel.sendMessage(EmbedBuilder()
.setTitle("Dieser Spieler hat bereits einen Account verknüpft.")
.setDescription("Du kannst die Verknüpfung widerrufen, indem du `/discord revoke` in den Chat schreibst.")
.setColor(Color.decode("#ffe414"))
.setThumbnail(avatarURL)
.build()
)
}
return
}
println("https://crafatar.com/avatars/${player.uniqueId.toTrimmedString()}")
val embedBuilder = EmbedBuilder()
.setTitle("Perfekt! Jetzt schick mir den Bestätigungscode, den du soeben in Minecraft erhalten hast.")
.setColor(Color.decode("#0095ff"))
.setThumbnail(avatarURL)
if (DiscordConnections.getByUserID(event.author.id) != null) {
embedBuilder.setDescription("Hinweis: Du hast bereits einen Account verknüpft. Diese Verknüpfung wird gelöscht, wenn du einen neuen Account verbindest.")
}
val nextStepMessagePromise = event.message.privateChannel.sendMessage(
embedBuilder.build()
).submit()
val newData = ValidationData(player.uniqueId, event.author.id, randomNumber(1000, 9999).toShort())
newData.timeoutTask = plugin.server.scheduler.runTaskLater(plugin, fun() {
validations.remove(newData)
player.sendMessage("${MC_PREFIX}${ChatColor.RED}Dein Bestätigungscode ist abgelaufen.")
nextStepMessagePromise.getNow(null)?.editMessage(EmbedBuilder()
.setTitle("Abgelaufen.")
.setColor(Color.LIGHT_GRAY)
.build())?.queue()
}, secondsToTicks(30).toLong())
validations.add(newData)
player.sendMessage("${MC_PREFIX}Dein Bestätigungscode (läuft in 30 Sekunden ab): ${ChatColor.GOLD}${ChatColor.BOLD}${newData.code}")
}
} else {
if (data.code.toString() == event.message.contentRaw) {
validations.remove(data)
data.timeoutTask.cancel()
event.message.privateChannel.sendMessage(EmbedBuilder()
.setTitle("Dein Minecraft-Account ist jetzt verknüpft.")
.setColor(Color.GREEN)
.build()
).queue()
plugin.server.getPlayer(data.playerUUID)?.sendMessage(
"${MC_PREFIX}Dein Minecraft-Account ist jetzt mit dem Discord-Account " +
"${ChatColor.GOLD}${event.author.name}#${event.author.discriminator} " +
"${ChatColor.RESET}verknüpft.")
DiscordConnections.add(DiscordConnection(data.playerUUID, data.userID))
} else {
event.message.privateChannel.sendMessage(EmbedBuilder()
.setTitle("Der Code ist leider falsch.")
.setColor(Color.RED)
.build()
).queue()
}
}
}
fun setMuted(user: User, muted: Boolean) {
guild.getMember(user)?.mute(muted)?.queue()
}
data class ValidationData(
val playerUUID: UUID,
val userID: String,
val code: Short
) {
lateinit var timeoutTask: BukkitTask
}
}

View file

@ -0,0 +1,9 @@
package de.moritzruth.spigot_ttt.discord
import java.util.*
data class DiscordConnection(val playerUUID: UUID, val userID: String) {
fun collidesWith(other: DiscordConnection): Boolean {
return other.userID == userID || other.playerUUID == playerUUID
}
}

View file

@ -0,0 +1,36 @@
package de.moritzruth.spigot_ttt.discord
import de.moritzruth.spigot_ttt.utils.ConfigurationFile
import java.util.*
object DiscordConnections {
private val config = ConfigurationFile("discord-connections")
private val connections = config.getStringList("connections").map {
val (playerUUIDString, userID) = it.split("|")
DiscordConnection(UUID.fromString(playerUUIDString)!!, userID)
}.toMutableList()
private fun saveConnections() {
config.set("connections", connections.map { "${it.playerUUID}|${it.userID}" })
config.save()
}
fun add(connection: DiscordConnection) {
val colliding = connections.find { it.collidesWith(connection) }
if (colliding != null) {
throw CollisionException(connection, colliding)
}
connections.add(connection)
saveConnections()
}
fun getByUserID(userID: String) = connections.find { it.userID == userID }
fun getByPlayerUUID(playerUUID: UUID) = connections.find { it.playerUUID == playerUUID }
class CollisionException(
val connection: DiscordConnection,
val collidingConnection: DiscordConnection
): RuntimeException("There is already a connection for this player or this Discord user")
}

View file

@ -0,0 +1,19 @@
package de.moritzruth.spigot_ttt.discord
import de.moritzruth.spigot_ttt.discord.DiscordBot.jda
import de.moritzruth.spigot_ttt.plugin
import net.dv8tion.jda.api.entities.User
import org.bukkit.entity.Player
import java.util.*
object DiscordInterface {
fun getPlayerByUserID(userID: String): Player? {
val connection = DiscordConnections.getByUserID(userID) ?: return null
return plugin.server.getPlayer(connection.playerUUID)
}
fun getUserByPlayerUUID(playerUUID: UUID): User? {
val connection = DiscordConnections.getByPlayerUUID(playerUUID) ?: return null
return jda.getUserById(connection.userID)
}
}

View file

@ -0,0 +1,28 @@
package de.moritzruth.spigot_ttt.game
import de.moritzruth.spigot_ttt.TTTPlugin
import de.moritzruth.spigot_ttt.utils.NoOpTabCompleter
import de.moritzruth.spigot_ttt.plugin
import org.bukkit.ChatColor
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
class AbortCommand: CommandExecutor {
init {
plugin.getCommand("abort")?.let {
it.setExecutor(this)
it.tabCompleter = NoOpTabCompleter()
}
}
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
if (GameManager.phase === null) {
sender.sendMessage("${TTTPlugin.prefix}${ChatColor.RED}Zurzeit läuft kein Spiel.")
} else {
GameManager.abortGame(true)
}
return true
}
}

View file

@ -0,0 +1,116 @@
package de.moritzruth.spigot_ttt.game
import de.moritzruth.spigot_ttt.game.players.PlayerManager
import de.moritzruth.spigot_ttt.game.players.TTTPlayer
import de.moritzruth.spigot_ttt.game.players.corpses.CorpseManager
import de.moritzruth.spigot_ttt.items.ItemManager
import de.moritzruth.spigot_ttt.plugin
import de.moritzruth.spigot_ttt.shop.Shop
import de.moritzruth.spigot_ttt.shop.ShopListener
object GameManager {
var phase: GamePhase? = null
private set(value) {
field = value
ScoreboardHelper.forEveryScoreboard { it.updateEverything(); it.showCorrectSidebarScoreboard() }
}
val world = plugin.server.getWorld("world")!!
fun initialize() {
ItemManager.registerListeners()
GeneralGameEventsListener.register()
ShopListener.register()
}
fun letRoleWin(role: TTTPlayer.Role?) {
ensurePhase(GamePhase.COMBAT)
GameMessenger.win(role)
phase = GamePhase.OVER
Timers.cancelCurrentTask()
ScoreboardHelper.forEveryScoreboard { it.updateTeams() }
PlayerManager.tttPlayers.forEach {
it.setMuted(false)
Shop.hide(it)
}
Timers.startOverPhaseTimer(plugin.config.getInt("duration.over", 10)) {
phase = null
resetWorld()
PlayerManager.tttPlayers.forEach(TTTPlayer::resetAfterGameEnd)
PlayerManager.reset()
}
plugin.broadcast("", false)
GameMessenger.roles()
}
fun resetWorld() {
CorpseManager.destroyAll()
ItemManager.removeItemEntities()
}
fun abortGame(broadcast: Boolean = false) {
if (phase === null) throw IllegalStateException("The game is not running")
phase = null
Timers.cancelCurrentTask()
resetWorld()
PlayerManager.tttPlayers.forEach(TTTPlayer::resetAfterGameEnd)
PlayerManager.reset()
if (broadcast) {
GameMessenger.aborted()
}
}
fun startPreparingPhase() {
ensurePhase(null)
if (PlayerManager.availablePlayers.count() < plugin.config.getInt("min-players", 4)) {
throw NotEnoughPlayersException()
}
PlayerManager.createTTTPlayers()
phase = GamePhase.PREPARING
PlayerManager.tttPlayers.forEach { it.reset(); it.teleportToSpawn() }
GameMessenger.preparingPhaseStarted()
Timers.playTimerSound()
ItemManager.spawnWeapons()
Timers.startPreparingPhaseTimer(plugin.config.getInt("duration.preparing", 20)) {
startCombatPhase()
}
}
private fun startCombatPhase() {
ensurePhase(GamePhase.PREPARING)
phase = GamePhase.COMBAT
PlayerManager.tttPlayers.forEach { Shop.show(it) }
ScoreboardHelper.forEveryScoreboard { it.updateTeams() }
GameMessenger.combatPhaseStarted()
Timers.playTimerSound()
Timers.startCombatPhaseTimer(plugin.config.getInt("duration.combat", 480)) {
if (PlayerManager.stillLivingRoles.contains(TTTPlayer.Role.INNOCENT)) {
letRoleWin(TTTPlayer.Role.INNOCENT)
} else {
letRoleWin(null)
}
}
}
fun ensurePhase(phase: GamePhase?) {
if (this.phase !== phase) throw IllegalStateException("The game must be in $phase phase")
}
class NotEnoughPlayersException: Exception("There are not enough players to start the game")
}

View file

@ -0,0 +1,92 @@
package de.moritzruth.spigot_ttt.game
import de.moritzruth.spigot_ttt.game.players.PlayerManager
import de.moritzruth.spigot_ttt.game.players.TTTPlayer
import de.moritzruth.spigot_ttt.plugin
import de.moritzruth.spigot_ttt.utils.secondsToTicks
import org.bukkit.ChatColor
object GameMessenger {
fun preparingPhaseStarted() = plugin.broadcast("${ChatColor.GREEN}Die Vorbereitungsphase hat begonnen.")
fun remainingPreparingPhaseTime(remainingSeconds: Int) {
if (remainingSeconds > 0) {
if (remainingSeconds == 1) {
plugin.broadcast("${ChatColor.GREEN}Die Vorbereitungsphase endet in ${ChatColor.WHITE}1 ${ChatColor.GREEN}Sekunde")
} else {
plugin.broadcast("${ChatColor.GREEN}Die Vorbereitungsphase endet in ${ChatColor.WHITE}$remainingSeconds ${ChatColor.GREEN}Sekunden")
}
} else {
throw IllegalArgumentException("remainingSeconds must be positive and not 0")
}
}
fun combatPhaseStarted() = plugin.broadcast("${ChatColor.GREEN}Die Kampfphase hat begonnen.")
fun remainingRoundTime(remainingMinutes: Int) {
if (remainingMinutes > 0) {
if (remainingMinutes == 1) {
plugin.broadcast("${ChatColor.GREEN}Das Spiel endet in ${ChatColor.WHITE}1 ${ChatColor.GREEN}Minute")
} else {
plugin.broadcast("${ChatColor.GREEN}Das Spiel endet in ${ChatColor.WHITE}$remainingMinutes ${ChatColor.GREEN}Minuten")
}
} else {
throw IllegalArgumentException("remainingMinutes must be positive and not 0")
}
}
fun win(winnerRole: TTTPlayer.Role?) {
val winner = when(winnerRole) {
null -> {
plugin.broadcast("Niemand hat gewonnen")
PlayerManager.tttPlayers.forEach {
it.player.sendTitle("Unentschieden", null, secondsToTicks(0.5), secondsToTicks(5), secondsToTicks(1))
}
return
}
TTTPlayer.Role.JACKAL, TTTPlayer.Role.SIDEKICK -> "Der ${TTTPlayer.Role.JACKAL.chatColor}Jackal"
TTTPlayer.Role.TRAITOR -> "Die ${TTTPlayer.Role.TRAITOR.chatColor}Traitor"
TTTPlayer.Role.INNOCENT, TTTPlayer.Role.DETECTIVE -> "Die ${TTTPlayer.Role.INNOCENT.chatColor}Innocents"
}
val winnerMessage = when(winnerRole) {
TTTPlayer.Role.JACKAL, TTTPlayer.Role.SIDEKICK -> "hat gewonnen"
TTTPlayer.Role.TRAITOR, TTTPlayer.Role.INNOCENT, TTTPlayer.Role.DETECTIVE -> "haben gewonnen"
}
plugin.broadcast("${ChatColor.GOLD}$winner ${ChatColor.GOLD}${winnerMessage}")
PlayerManager.tttPlayers.forEach {
it.player.sendTitle("${ChatColor.GOLD}$winner", "${ChatColor.GOLD}$winnerMessage", secondsToTicks(0.5), secondsToTicks(5), secondsToTicks(1))
}
}
fun abortedPlayerLeft() {
plugin.broadcast("${ChatColor.RED}Das Spiel wurde abgebrochen, da ein Spieler den Server verlassen hat.")
}
fun aborted() {
plugin.broadcast("${ChatColor.RED}Das Spiel wurde abgebrochen.")
}
fun corpseIdentified(by: String, who: String, role: TTTPlayer.Role) {
plugin.broadcast("$by ${ChatColor.GOLD}hat die Leiche von ${ChatColor.WHITE}$who ${ChatColor.GOLD}identifiziert. Er/Sie war ${role.coloredDisplayName}")
}
fun roles() {
val playersByRole = PlayerManager.getPlayersByRole()
val roles = playersByRole.keys.sortedBy(TTTPlayer.Role::position)
for (role in roles) {
val entries = playersByRole.getValue(role).map { tttPlayer ->
tttPlayer.player.displayName.run {
if (tttPlayer.roleHistory.count() != 0)
this + " (${tttPlayer.roleHistory.joinToString(", ") { it.coloredDisplayName }})"
else this
}
}
plugin.broadcast(" ${role.coloredDisplayName}: ${entries.joinToString(", ")}", false)
}
}
}

View file

@ -0,0 +1,7 @@
package de.moritzruth.spigot_ttt.game
enum class GamePhase(val displayName: String) {
PREPARING("Vorbereitung"),
COMBAT("Kampf"),
OVER("Ende")
}

View file

@ -0,0 +1,152 @@
package de.moritzruth.spigot_ttt.game
import de.moritzruth.spigot_ttt.TTTPlugin
import de.moritzruth.spigot_ttt.game.players.PlayerManager
import de.moritzruth.spigot_ttt.game.players.corpses.CorpseManager
import de.moritzruth.spigot_ttt.items.ItemManager
import de.moritzruth.spigot_ttt.plugin
import org.bukkit.ChatColor
import org.bukkit.GameMode
import org.bukkit.Material
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
import org.bukkit.event.Listener
import org.bukkit.event.block.BlockBreakEvent
import org.bukkit.event.block.BlockPlaceEvent
import org.bukkit.event.entity.EntityDamageEvent
import org.bukkit.event.entity.EntityPickupItemEvent
import org.bukkit.event.entity.FoodLevelChangeEvent
import org.bukkit.event.entity.PlayerDeathEvent
import org.bukkit.event.player.AsyncPlayerChatEvent
import org.bukkit.event.player.PlayerItemHeldEvent
import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerQuitEvent
import org.golde.bukkit.corpsereborn.CorpseAPI.events.CorpseClickEvent
import java.time.Instant
object GeneralGameEventsListener: Listener {
fun register() {
plugin.server.pluginManager.registerEvents(this, plugin)
}
@EventHandler
fun onPlayerJoin(event: PlayerJoinEvent) {
event.joinMessage = "${TTTPlugin.prefix}${event.player.displayName} ${ChatColor.GOLD}hat das Spiel betreten."
PlayerManager.onPlayerJoin(event.player)
}
@EventHandler
fun onPlayerQuit(event: PlayerQuitEvent) {
event.quitMessage = "${TTTPlugin.prefix}${event.player.displayName} ${ChatColor.GOLD}hat das Spiel verlassen."
PlayerManager.onPlayerQuit(event.player)
}
@EventHandler
fun onFoodLevelChange(event: FoodLevelChangeEvent) {
event.foodLevel = 20
}
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
fun onEntityDamageLow(event: EntityDamageEvent) {
if (GameManager.phase !== GamePhase.COMBAT) {
event.isCancelled = true
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
fun onEntityDamageHighest(event: EntityDamageEvent) {
if (event.entity !is Player) return
val tttPlayer = PlayerManager.getTTTPlayer(event.entity as Player) ?: return
if (tttPlayer.player.health - event.finalDamage <= 0) {
tttPlayer.kill()
// gameManager.playerManager.tttPlayers.forEach {
// it.player.playSound(tttPlayer.player.location, Sound.ENTITY_PLAYER_DEATH, SoundCategory.PLAYERS, 2f, 1f)
// }
event.damage = 0.0
}
}
@EventHandler
fun onPlayerDeath(event: PlayerDeathEvent) {
event.deathMessage = null
}
@EventHandler
fun onBlockPlace(event: BlockPlaceEvent) {
if (event.player.gameMode !== GameMode.CREATIVE) event.isCancelled = true
}
@EventHandler
fun onBlockBreak(event: BlockBreakEvent) {
if (event.player.gameMode !== GameMode.CREATIVE) event.isCancelled = true
}
@EventHandler
fun onCorpseClick(event: CorpseClickEvent) {
// bug: always ClickType.UNKNOWN
// if (event.clickType !== ClickType.RIGHT) return
val tttCorpse = CorpseManager.getTTTCorpse(event.corpse)
if (tttCorpse !== null) {
if(Instant.now().toEpochMilli() - tttCorpse.timestamp.toEpochMilli() < 200) return
event.clicker.openInventory(tttCorpse.inventory)
tttCorpse.identify(event.clicker)
}
event.isCancelled = true
}
@EventHandler
fun onAsyncPlayerChat(event: AsyncPlayerChatEvent) {
val senderTTTPlayer = PlayerManager.getTTTPlayer(event.player) ?: return
if (!senderTTTPlayer.alive) {
PlayerManager.tttPlayers.filter { !it.alive }.forEach {
it.player.sendMessage("${ChatColor.GRAY}[${ChatColor.RED}TOT${ChatColor.GRAY}] <${event.player.displayName}> ${event.message}")
}
event.isCancelled = true
}
}
@EventHandler
fun onPlayerItemHeld(event: PlayerItemHeldEvent) {
val tttPlayer = PlayerManager.getTTTPlayer(event.player) ?: return
val itemStack = event.player.inventory.getItem(event.newSlot)
tttPlayer.itemInHand =
if (itemStack == null || itemStack.type === Material.AIR) null
else ItemManager.getItemByItemStack(itemStack)
}
@EventHandler
fun onEntityPickupItem(event: EntityPickupItemEvent) {
if (event.entity !is Player) {
return
}
val player = event.entity as Player
val tttPlayer = PlayerManager.getTTTPlayer(player) ?: return
val tttItem = ItemManager.getItemByItemStack(event.item.itemStack)
if (tttItem != null) {
if (kotlin.runCatching { tttPlayer.checkAddItemPreconditions(tttItem) }.isSuccess) {
plugin.server.scheduler.runTask(plugin, fun() {
tttPlayer.updateItemInHand()
})
return
}
}
event.isCancelled = true
}
}

View file

@ -0,0 +1,12 @@
package de.moritzruth.spigot_ttt.game
import de.moritzruth.spigot_ttt.game.players.PlayerManager
import de.moritzruth.spigot_ttt.game.players.TTTScoreboard
object ScoreboardHelper {
fun forEveryScoreboard(action: (tttScoreboard: TTTScoreboard) -> Unit) {
PlayerManager.tttPlayers.forEach {
action(it.scoreboard)
}
}
}

View file

@ -0,0 +1,32 @@
package de.moritzruth.spigot_ttt.game
import de.moritzruth.spigot_ttt.TTTPlugin
import de.moritzruth.spigot_ttt.utils.NoOpTabCompleter
import de.moritzruth.spigot_ttt.plugin
import org.bukkit.ChatColor
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
class StartCommand: CommandExecutor {
init {
plugin.getCommand("start")?.let {
it.setExecutor(this)
it.tabCompleter = NoOpTabCompleter()
}
}
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
if (GameManager.phase === null) {
try {
GameManager.startPreparingPhase()
} catch (e: GameManager.NotEnoughPlayersException) {
sender.sendMessage("${ChatColor.RED}Es sind nicht genügend Spieler online.")
}
} else {
sender.sendMessage("${TTTPlugin.prefix}${ChatColor.RED}Das Spiel läuft bereits.")
}
return true
}
}

View file

@ -0,0 +1,66 @@
package de.moritzruth.spigot_ttt.game
import de.moritzruth.spigot_ttt.game.players.PlayerManager
import de.moritzruth.spigot_ttt.plugin
import de.moritzruth.spigot_ttt.utils.secondsToTicks
import org.bukkit.Sound
import org.bukkit.scheduler.BukkitTask
object Timers {
private var bukkitTask: BukkitTask? = null
var remainingSeconds: Int = 0
private val isCountdownRunning get() = bukkitTask != null
fun cancelCurrentTask() {
bukkitTask?.cancel()
bukkitTask = null
}
fun startPreparingPhaseTimer(duration: Int, onFinished: () -> Unit) {
runCountdown(duration, onFinished) { remainingSeconds ->
when (remainingSeconds) {
in 1..5, 10, 30 -> {
playTimerSound()
GameMessenger.remainingPreparingPhaseTime(remainingSeconds)
}
}
}
}
fun startCombatPhaseTimer(duration: Int, onFinished: () -> Unit) {
runCountdown(duration, onFinished) { remainingSeconds ->
if (remainingSeconds % 60 == 0) {
playTimerSound()
GameMessenger.remainingRoundTime(remainingSeconds / 60)
}
}
}
fun startOverPhaseTimer(duration: Int, onFinished: () -> Unit) {
runCountdown(duration, onFinished) {}
}
fun playTimerSound() {
PlayerManager.tttPlayers.forEach { it.player.playSound(it.player.location, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, 1f, 1f) }
}
private fun runCountdown(durationSeconds: Int, onFinished: () -> Unit, onSecond: (remainingSeconds: Int) -> Unit) {
if (isCountdownRunning) {
throw IllegalStateException("Only one countdown can be active at a time")
}
remainingSeconds = durationSeconds
bukkitTask = plugin.server.scheduler.runTaskTimer(plugin, Runnable {
ScoreboardHelper.forEveryScoreboard { it.updatePhaseAndTime() }
if (remainingSeconds == 0) {
cancelCurrentTask()
onFinished()
} else {
onSecond(remainingSeconds)
remainingSeconds -= 1
}
}, 0, secondsToTicks(1).toLong())
}
}

View file

@ -0,0 +1,9 @@
package de.moritzruth.spigot_ttt.game.players
import de.moritzruth.spigot_ttt.items.TTTItem
sealed class DeathReason(val displayText: String) {
object DISCONNECTED: DeathReason("Verbindung unterbrochen")
object SUICIDE: DeathReason("Suizid")
class Item(val item: TTTItem): DeathReason("Getötet mit: ${item.displayName}")
}

View file

@ -0,0 +1,108 @@
package de.moritzruth.spigot_ttt.game.players
import de.moritzruth.spigot_ttt.TTTPlugin
import de.moritzruth.spigot_ttt.game.GameManager
import de.moritzruth.spigot_ttt.game.GameMessenger
import de.moritzruth.spigot_ttt.game.GamePhase
import de.moritzruth.spigot_ttt.plugin
import de.moritzruth.spigot_ttt.utils.noop
import de.moritzruth.spigot_ttt.utils.teleportPlayerToWorldSpawn
import org.bukkit.ChatColor
import org.bukkit.GameMode
import org.bukkit.entity.Player
object PlayerManager {
val tttPlayers= mutableListOf<TTTPlayer>()
val availablePlayers get() = plugin.server.onlinePlayers.filter { it.gameMode === GameMode.SURVIVAL }
val stillLivingRoles get() = tttPlayers.filter { it.alive }.map { it.role }.distinct()
fun getTTTPlayer(player: Player) = tttPlayers.find { it.player === player }
fun getPlayersByRole() = mutableMapOf<TTTPlayer.Role, MutableSet<TTTPlayer>>()
.apply { tttPlayers.forEach { getOrPut(it.role) { mutableSetOf() }.add(it) } }
.toMap()
fun reset() {
tttPlayers.clear()
}
fun letRemainingRoleWin() {
GameManager.ensurePhase(GamePhase.COMBAT)
println(stillLivingRoles)
if (stillLivingRoles.count() == 1) {
GameManager.letRoleWin(stillLivingRoles[0])
}
}
fun onPlayerJoin(player: Player) {
val tttPlayer = tttPlayers.find { it.player.uniqueId == player.uniqueId }
if (tttPlayer == null) {
if (GameManager.phase == null) {
teleportPlayerToWorldSpawn(player)
player.gameMode = GameMode.SURVIVAL
} else {
player.gameMode = GameMode.SURVIVAL
player.sendMessage("${TTTPlugin.prefix}${ChatColor.GREEN}Du schaust jetzt zu.")
}
} else {
tttPlayer.player = player
player.sendMessage("${TTTPlugin.prefix}${ChatColor.RED}Du bist gestorben, da du das Spiel verlassen hast.")
when(GameManager.phase) {
GamePhase.PREPARING -> noop() // Never happens
GamePhase.COMBAT -> {
player.gameMode = GameMode.SPECTATOR
}
GamePhase.OVER -> {
tttPlayer.teleportToSpawn()
player.gameMode = GameMode.SURVIVAL
}
}
}
}
fun onPlayerQuit(player: Player) {
val tttPlayer = getTTTPlayer(player) ?: return
when(GameManager.phase) {
GamePhase.PREPARING -> {
GameManager.abortGame()
GameMessenger.abortedPlayerLeft()
}
GamePhase.COMBAT -> {
tttPlayer.kill()
}
GamePhase.OVER -> noop()
}
}
fun createTTTPlayers() {
val playersWithoutRole = availablePlayers.toMutableSet()
val playerCount = playersWithoutRole.count()
val traitorCount: Int = if (playerCount <= 4) 1 else playerCount / 4
if (playerCount >= plugin.config.getInt("min-players-for.detective", 5)) {
val player = playersWithoutRole.random()
tttPlayers.add(TTTPlayer(player, TTTPlayer.Role.DETECTIVE))
playersWithoutRole.remove(player)
}
if (playerCount >= plugin.config.getInt("min-players-for.jackal", 6)) {
val player = playersWithoutRole.random()
tttPlayers.add(TTTPlayer(player, TTTPlayer.Role.JACKAL))
playersWithoutRole.remove(player)
}
for (index in 1..traitorCount) {
val player = playersWithoutRole.random()
tttPlayers.add(TTTPlayer(player, TTTPlayer.Role.TRAITOR))
playersWithoutRole.remove(player)
}
playersWithoutRole.forEach { tttPlayers.add(TTTPlayer(it, TTTPlayer.Role.INNOCENT)) }
}
}

View file

@ -0,0 +1,20 @@
package de.moritzruth.spigot_ttt.game.players
import kotlin.reflect.KClass
interface State
class StateContainer {
private val instances = mutableSetOf<State>()
@Suppress("UNCHECKED_CAST")
fun <T: State> get(stateClass: KClass<T>, createInstance: () -> T): T {
return (instances.find { stateClass.isInstance(it) } ?: createInstance().also {
instances.add(it)
}) as T
}
fun clear() {
instances.clear()
}
}

View file

@ -0,0 +1,162 @@
package de.moritzruth.spigot_ttt.game.players
import de.moritzruth.spigot_ttt.CustomItems
import de.moritzruth.spigot_ttt.discord.DiscordBot
import de.moritzruth.spigot_ttt.discord.DiscordInterface
import de.moritzruth.spigot_ttt.game.GameManager
import de.moritzruth.spigot_ttt.game.GamePhase
import de.moritzruth.spigot_ttt.game.players.corpses.CorpseManager
import de.moritzruth.spigot_ttt.items.ItemManager
import de.moritzruth.spigot_ttt.items.SelectableItem
import de.moritzruth.spigot_ttt.items.TTTItem
import de.moritzruth.spigot_ttt.plugin
import de.moritzruth.spigot_ttt.shop.Shop
import de.moritzruth.spigot_ttt.utils.hotbarContents
import de.moritzruth.spigot_ttt.utils.teleportPlayerToWorldSpawn
import org.bukkit.ChatColor
import org.bukkit.GameMode
import org.bukkit.Material
import org.bukkit.entity.Player
import kotlin.properties.Delegates
class TTTPlayer(player: Player, role: Role) {
var alive = true
var lastDeathReason: DeathReason? = null
var player by Delegates.observable(player) { _, _, player -> player.scoreboard = scoreboard.scoreboard }
var role by Delegates.observable(role) { _, _, _ -> scoreboard.updateRole() }
val roleHistory = mutableListOf<Role>()
var itemInHand by Delegates.observable<TTTItem?>(null) { _, oldItem, newItem -> onItemInHandChanged(oldItem, newItem) }
var credits by Delegates.observable(10) { _, _, _ -> scoreboard.updateCredits() }
val scoreboard = TTTScoreboard(this)
val stateContainer = StateContainer()
private val discordUser get() = DiscordInterface.getUserByPlayerUUID(player.uniqueId)
init {
player.scoreboard = scoreboard.scoreboard
scoreboard.initialize()
}
private fun onItemInHandChanged(oldItem: TTTItem?, newItem: TTTItem?) {
if (oldItem !== null && oldItem is SelectableItem) {
oldItem.onDeselect(this)
}
if (newItem !== null && newItem is SelectableItem) {
newItem.onSelect(this)
}
}
fun kill(reason: DeathReason = DeathReason.SUICIDE) {
GameManager.ensurePhase(GamePhase.COMBAT)
player.gameMode = GameMode.SPECTATOR
alive = false
lastDeathReason = reason
CorpseManager.spawn(this, reason)
Shop.hide(this)
setMuted(true)
PlayerManager.letRemainingRoleWin()
}
fun resetAfterGameEnd() {
if (!alive) {
teleportToSpawn()
}
// Required to be delayed because of a Minecraft bug which sometimes turns players invisible
plugin.server.scheduler.runTask(plugin) { ->
reset()
}
}
fun reset() {
if (player.isDead) {
player.spigot().respawn()
}
itemInHand?.apply {
if (this is SelectableItem) {
this.onDeselect(this@TTTPlayer)
}
}
stateContainer.clear()
setMuted(false)
alive = true
player.gameMode = GameMode.SURVIVAL
player.activePotionEffects.forEach { player.removePotionEffect(it.type) }
player.health = 20.0
player.inventory.clear()
}
fun teleportToSpawn() {
teleportPlayerToWorldSpawn(player)
}
fun setMuted(muted: Boolean) {
val discordUser = discordUser
if (discordUser != null) {
DiscordBot.setMuted(discordUser, muted)
}
}
fun updateItemInHand() {
val itemStack = player.inventory.itemInMainHand
this.itemInHand =
if (itemStack.type === Material.AIR) null
else ItemManager.getItemByItemStack(itemStack)
}
fun checkAddItemPreconditions(item: TTTItem) {
val owningTTTItems = getOwningTTTItems()
if (owningTTTItems.contains(item)) {
throw AlreadyHasItemException()
}
val maxItemsOfTypeInInventory = item.type.maxItemsOfTypeInInventory
if (maxItemsOfTypeInInventory !== null && owningTTTItems.filter { it.type === item.type }.count() >= maxItemsOfTypeInInventory) {
throw TooManyItemsOfTypeException()
}
}
fun addItem(item: TTTItem) {
checkAddItemPreconditions(item)
player.inventory.addItem(item.itemStack.clone())
}
class AlreadyHasItemException: Exception("The player already owns this item")
class TooManyItemsOfTypeException: Exception("The player already owns too much items of this type")
fun getOwningTTTItems() = player.inventory.hotbarContents.mapNotNull { it?.run { ItemManager.getItemByItemStack(this) } }
enum class Role(
val chatColor: ChatColor,
val displayName: String,
val iconItemMaterial: Material
) {
INNOCENT(ChatColor.GREEN, "Innocent", CustomItems.innocent),
DETECTIVE(ChatColor.YELLOW, "Detective", CustomItems.detective),
TRAITOR(ChatColor.RED, "Traitor", CustomItems.traitor),
JACKAL(ChatColor.AQUA, "Jackal", CustomItems.jackal),
SIDEKICK(ChatColor.AQUA, "Sidekick", CustomItems.sidekick);
val coloredDisplayName = "$chatColor$displayName${ChatColor.RESET}"
val isInnocentRole get() = this === INNOCENT || this === DETECTIVE
val isJackalRole get() = this === JACKAL || this === SIDEKICK
val position by lazy { values().indexOf(this) }
}
}

View file

@ -0,0 +1,181 @@
package de.moritzruth.spigot_ttt.game.players
import de.moritzruth.spigot_ttt.game.GameManager
import de.moritzruth.spigot_ttt.game.GamePhase
import de.moritzruth.spigot_ttt.game.Timers
import de.moritzruth.spigot_ttt.plugin
import de.moritzruth.spigot_ttt.shop.Shop
import org.bukkit.ChatColor
import org.bukkit.scoreboard.DisplaySlot
import org.bukkit.scoreboard.RenderType
import org.bukkit.scoreboard.Scoreboard
import org.bukkit.scoreboard.Team
class TTTScoreboard(private val tttPlayer: TTTPlayer) {
val scoreboard: Scoreboard = plugin.server.scoreboardManager!!.newScoreboard
fun initialize() {
scoreboard.registerNewTeam(ValueTeam.ROLE.teamName).addEntry(ValueTeam.ROLE.entry)
scoreboard.registerNewTeam(ValueTeam.PHASE_AND_TIME.teamName).addEntry(ValueTeam.PHASE_AND_TIME.entry)
scoreboard.registerNewTeam(ValueTeam.CREDITS.teamName).addEntry(ValueTeam.CREDITS.entry)
scoreboard.registerNewObjective(
INACTIVE_OBJECTIVE,
"dummy",
"${ChatColor.GOLD}TTT",
RenderType.INTEGER
).apply {
val lines = mutableListOf(
" ".repeat(20),
"${ChatColor.GRAY}Inaktiv",
" "
)
lines.reversed().forEachIndexed { index, line -> getScore(line).score = index }
}
scoreboard.registerNewObjective(
ACTIVE_OBJECTIVE,
"dummy",
"${ChatColor.GOLD}TTT",
RenderType.INTEGER
).apply {
val lines = mutableListOf(
" ".repeat(20),
ValueTeam.PHASE_AND_TIME.entry,
" ",
ValueTeam.ROLE.entry,
" "
)
lines.reversed().forEachIndexed { index, line -> getScore(line).score = index }
}
scoreboard.registerNewObjective(
ACTIVE_WITH_CREDITS_OBJECTIVE,
"dummy",
"${ChatColor.GOLD}TTT",
RenderType.INTEGER
).apply {
val lines = mutableListOf(
" ".repeat(20),
ValueTeam.PHASE_AND_TIME.entry,
" ",
ValueTeam.ROLE.entry,
ValueTeam.CREDITS.entry,
" "
)
lines.reversed().forEachIndexed { index, line -> getScore(line).score = index }
}
scoreboard.registerNewTeam(SPECIAL_TEAM_NAME).apply {
setAllowFriendlyFire(true)
setCanSeeFriendlyInvisibles(true)
setOption(Team.Option.NAME_TAG_VISIBILITY, Team.OptionStatus.ALWAYS)
}
scoreboard.registerNewTeam(DEFAULT_TEAM_NAME).apply {
setAllowFriendlyFire(true)
setCanSeeFriendlyInvisibles(false)
setOption(Team.Option.COLLISION_RULE, Team.OptionStatus.ALWAYS)
setOption(Team.Option.DEATH_MESSAGE_VISIBILITY, Team.OptionStatus.NEVER)
setOption(Team.Option.NAME_TAG_VISIBILITY, Team.OptionStatus.NEVER)
}
updateEverything()
showCorrectSidebarScoreboard()
}
fun updatePhaseAndTime() {
val phase = GameManager.phase
if (phase === null) {
setValue(ValueTeam.PHASE_AND_TIME, "Inaktiv")
} else {
val remainingSeconds = Timers.remainingSeconds
val seconds = remainingSeconds % 60
val minutes = (remainingSeconds - seconds) / 60
val minutesString = minutes.toString().padStart(2, '0')
val secondsString = seconds.toString().padStart(2, '0')
setValue(ValueTeam.PHASE_AND_TIME, "${ChatColor.GOLD}${phase.displayName}: ${ChatColor.WHITE}$minutesString:$secondsString")
}
}
fun updateRole() {
when (GameManager.phase) {
null -> setValue(ValueTeam.ROLE, "")
GamePhase.PREPARING -> setValue(ValueTeam.ROLE, "Du bist: ${ChatColor.MAGIC}xxxxxxxx")
GamePhase.COMBAT, GamePhase.OVER -> setValue(ValueTeam.ROLE, "Du bist: " + tttPlayer.role.coloredDisplayName.toUpperCase())
}
}
fun updateTeams() {
val defaultTeam = scoreboard.getTeam(DEFAULT_TEAM_NAME)!!
val phase = GameManager.phase
if (phase === GamePhase.COMBAT) {
if (tttPlayer.role.isJackalRole || tttPlayer.role === TTTPlayer.Role.TRAITOR) {
val specialTeam = scoreboard.getTeam(SPECIAL_TEAM_NAME)!!
specialTeam.color = tttPlayer.role.chatColor
PlayerManager.tttPlayers.forEach {
if ((tttPlayer.role.isJackalRole && it.role.isJackalRole) || (tttPlayer.role === TTTPlayer.Role.TRAITOR && it.role === TTTPlayer.Role.TRAITOR)) {
specialTeam.addEntry(it.player.displayName)
} else {
defaultTeam.addEntry(it.player.displayName)
}
}
return
}
}
defaultTeam.setOption(Team.Option.NAME_TAG_VISIBILITY, if (phase === null) Team.OptionStatus.ALWAYS else Team.OptionStatus.NEVER)
PlayerManager.tttPlayers.forEach {
defaultTeam.addEntry(it.player.displayName)
}
}
fun updateCredits() {
setValue(ValueTeam.CREDITS, "${ChatColor.GREEN}Credits: ${ChatColor.WHITE}${tttPlayer.credits}")
}
fun updateEverything() {
updatePhaseAndTime()
updateRole()
updateTeams()
updateCredits()
}
fun showCorrectSidebarScoreboard() {
setSidebar(when {
GameManager.phase === null -> INACTIVE_OBJECTIVE
GameManager.phase !== GamePhase.PREPARING && Shop.getBuyableItems(tttPlayer).isNotEmpty() -> ACTIVE_WITH_CREDITS_OBJECTIVE
else -> ACTIVE_OBJECTIVE
})
}
private fun setSidebar(objectiveName: String) {
scoreboard.getObjective(objectiveName)!!.displaySlot = DisplaySlot.SIDEBAR
}
private fun setValue(valueTeam: ValueTeam, value: String) {
scoreboard.getTeam(valueTeam.teamName)!!.prefix = value
}
private enum class ValueTeam(val teamName: String, val entry: String) {
PHASE_AND_TIME("_phase-and-time", "${ChatColor.AQUA}"),
ROLE("_role", "${ChatColor.BLACK}"),
CREDITS("_credits", "${ChatColor.GOLD}")
}
companion object {
private const val SPECIAL_TEAM_NAME = "special"
private const val DEFAULT_TEAM_NAME = "default"
private const val INACTIVE_OBJECTIVE = "1"
private const val ACTIVE_OBJECTIVE = "2"
private const val ACTIVE_WITH_CREDITS_OBJECTIVE = "3"
}
}

View file

@ -0,0 +1,19 @@
package de.moritzruth.spigot_ttt.game.players.corpses
import de.moritzruth.spigot_ttt.game.players.DeathReason
import de.moritzruth.spigot_ttt.game.players.TTTPlayer
import org.golde.bukkit.corpsereborn.nms.Corpses
object CorpseManager {
val corpses= mutableListOf<TTTCorpse>()
fun getTTTCorpse(corpse: Corpses.CorpseData): TTTCorpse? {
return corpses.find { it.corpse === corpse }
}
fun spawn(tttPlayer: TTTPlayer, reason: DeathReason) {
corpses.add(TTTCorpse(tttPlayer.player, tttPlayer.player.location, tttPlayer.role, reason))
}
fun destroyAll() = corpses.forEach(TTTCorpse::destroy)
}

View file

@ -0,0 +1,55 @@
package de.moritzruth.spigot_ttt.game.players.corpses
import de.moritzruth.spigot_ttt.game.GameMessenger
import de.moritzruth.spigot_ttt.game.players.DeathReason
import de.moritzruth.spigot_ttt.game.players.TTTPlayer
import de.moritzruth.spigot_ttt.CustomItems
import org.bukkit.ChatColor
import org.bukkit.Location
import org.bukkit.entity.Player
import org.bukkit.event.inventory.InventoryType
import org.bukkit.inventory.ItemFlag
import org.bukkit.inventory.ItemStack
import org.golde.bukkit.corpsereborn.CorpseAPI.CorpseAPI
import org.golde.bukkit.corpsereborn.nms.Corpses
import java.time.Instant
class TTTCorpse(private val player: Player, location: Location, private val role: TTTPlayer.Role, private val reason: DeathReason) {
val corpse: Corpses.CorpseData?
val inventory = player.server.createInventory(null, InventoryType.HOPPER, "${role.chatColor}${player.displayName}")
var identified = false
val timestamp: Instant = Instant.now()
init {
inventory.setItem(0, ItemStack(role.iconItemMaterial).apply {
val meta = itemMeta!!
meta.setDisplayName(role.coloredDisplayName)
meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES)
meta.lore = listOf("${ChatColor.GRAY}Rolle")
itemMeta = meta
})
val reasonItem = if (reason is DeathReason.Item) reason.item.itemStack.clone() else ItemStack(CustomItems.deathReason)
reasonItem.itemMeta = reasonItem.itemMeta!!.apply {
setDisplayName("${ChatColor.RESET}" + reason.displayText)
addItemFlags(ItemFlag.HIDE_ATTRIBUTES)
lore = listOf("${ChatColor.GRAY}Grund des Todes")
}
inventory.setItem(1, reasonItem)
corpse = CorpseAPI.spawnCorpse(player, location)
}
fun destroy() {
if (corpse !== null) CorpseAPI.removeCorpse(corpse)
}
fun identify(by: Player) {
if (identified) return
GameMessenger.corpseIdentified(by.displayName, player.displayName, role)
identified = true
}
}

View file

@ -0,0 +1,31 @@
package de.moritzruth.spigot_ttt.items
import de.moritzruth.spigot_ttt.TTTPlugin
import de.moritzruth.spigot_ttt.utils.NoOpTabCompleter
import de.moritzruth.spigot_ttt.plugin
import org.bukkit.ChatColor
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
class AddItemSpawnCommand: CommandExecutor {
init {
plugin.getCommand("additemspawn")?.let {
it.setExecutor(this)
it.tabCompleter = NoOpTabCompleter()
}
}
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
if (sender !is Player) {
sender.sendMessage("${ChatColor.RED}Dieser Befehl kann nur von Spielern verwendet werden.")
return true
}
ItemManager.addItemSpawnLocation(sender.location)
sender.sendMessage("${TTTPlugin.prefix}${ChatColor.GREEN}Ein Waffenspawn wurde an deiner Position hinzugefügt.")
return true
}
}

View file

@ -0,0 +1,70 @@
package de.moritzruth.spigot_ttt.items
import de.moritzruth.spigot_ttt.game.GameManager
import de.moritzruth.spigot_ttt.items.weapons.guns.deagle.GoldenDeagle
import de.moritzruth.spigot_ttt.items.weapons.guns.glock.Glock
import de.moritzruth.spigot_ttt.items.weapons.guns.pistol.Pistol
import de.moritzruth.spigot_ttt.items.weapons.guns.shotgun.Shotgun
import de.moritzruth.spigot_ttt.items.weapons.knife.Knife
import de.moritzruth.spigot_ttt.plugin
import de.moritzruth.spigot_ttt.utils.ConfigurationFile
import org.bukkit.Location
import org.bukkit.Material
import org.bukkit.entity.Item
import org.bukkit.inventory.ItemStack
import kotlin.math.roundToInt
object ItemManager {
private const val CONFIG_PATH = "spawn-locations"
private val spawnLocationsConfig = ConfigurationFile("spawnLocations")
val items: Set<TTTItem> = setOf(Pistol, Knife, Glock, GoldenDeagle, Shotgun, GoldenDeagle)
private val spawningItems = items.filter(TTTItem::spawning)
fun registerListeners() {
for (item in items) {
plugin.server.pluginManager.registerEvents(item.listener, plugin)
}
}
private fun getItemByMaterial(material: Material) = items.find { tttItem -> material === tttItem.itemStack.type }
fun getItemByItemStack(itemStack: ItemStack) = getItemByMaterial(itemStack.type)
private fun getSpawnLocations(): Set<Location> {
return spawnLocationsConfig.getStringList(CONFIG_PATH).map {
val (x, y, z) = it.split(":").map(String::toDouble)
Location(GameManager.world, x, y, z)
}.toSet()
}
private fun setSpawnLocations(spawnLocations: Set<Location>) {
spawnLocationsConfig.set(CONFIG_PATH, spawnLocations.map {
"${it.x}:${it.y}:${it.z}"
})
}
fun spawnWeapons() {
for (location in getSpawnLocations()) {
GameManager.world.dropItem(location, spawningItems.random().itemStack.clone())
}
}
fun removeItemEntities() {
GameManager.world.getEntitiesByClass(Item::class.java).forEach {
it.remove()
}
}
fun addItemSpawnLocation(location: Location) {
val spawnLocations = getSpawnLocations().toMutableSet()
spawnLocations.add(roundLocationToHalfBlock(location))
setSpawnLocations(spawnLocations)
spawnLocationsConfig.save()
}
private fun roundLocationToHalfBlock(location: Location) = Location(location.world, roundToHalf(location.x), roundToHalf(location.y), roundToHalf(location.z))
private fun roundToHalf(number: Double): Double = (number * 2).roundToInt() / 2.0
}

View file

@ -0,0 +1,32 @@
package de.moritzruth.spigot_ttt.items
import de.moritzruth.spigot_ttt.game.players.TTTPlayer
import org.bukkit.event.Listener
import org.bukkit.inventory.ItemStack
import java.util.*
interface SelectableItem: TTTItem {
fun onSelect(tttPlayer: TTTPlayer)
fun onDeselect(tttPlayer: TTTPlayer)
}
interface BuyableItem: TTTItem {
val buyableBy: EnumSet<TTTPlayer.Role>
val price: Int
}
interface TTTItem {
val displayName: String
val listener: Listener
val itemStack: ItemStack
val spawning: Boolean
val type: Type
enum class Type(val maxItemsOfTypeInInventory: Int?) {
NORMAL_WEAPON(2),
SPECIAL_WEAPON(2),
OTHER(null);
val position by lazy { values().indexOf(this) }
}
}

View file

@ -0,0 +1,12 @@
package de.moritzruth.spigot_ttt.items
import org.bukkit.entity.Player
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.event.player.PlayerInteractEvent
fun EntityDamageByEntityEvent.isRelevant(itemName: String): Boolean =
damager is Player &&
entity is Player &&
(damager as Player).inventory.itemInMainHand.itemMeta?.displayName == itemName
fun PlayerInteractEvent.isRelevant(itemName: String): Boolean = item?.itemMeta?.displayName == itemName

View file

@ -0,0 +1,30 @@
package de.moritzruth.spigot_ttt.items.weapons
import org.bukkit.ChatColor
object LoreHelper {
fun damage(damage: Double?): String {
if (damage == null) {
return "${ChatColor.GREEN}"
}
if (damage <= 0) {
throw IllegalArgumentException("damage must be higher than 0")
}
return "${ChatColor.RED}$damage"
}
fun magazineSize(maxUses: Int?) = when {
maxUses == null -> "${ChatColor.GREEN}"
maxUses == 1 -> "${ChatColor.RED}1"
maxUses <= 5 -> "${ChatColor.YELLOW}$maxUses"
else -> "${ChatColor.GREEN}${maxUses}"
}
fun cooldown(cooldown: Double) = when {
cooldown <= 1 -> "${ChatColor.GREEN}${cooldown}s"
cooldown > 3 -> "${ChatColor.RED}${cooldown}s"
else -> "${ChatColor.GREEN}${cooldown}s"
}
}

View file

@ -0,0 +1,127 @@
package de.moritzruth.spigot_ttt.items.weapons.guns
import de.moritzruth.spigot_ttt.game.GameManager
import de.moritzruth.spigot_ttt.game.GamePhase
import de.moritzruth.spigot_ttt.game.players.TTTPlayer
import de.moritzruth.spigot_ttt.items.SelectableItem
import de.moritzruth.spigot_ttt.items.TTTItem
import de.moritzruth.spigot_ttt.items.weapons.LoreHelper
import de.moritzruth.spigot_ttt.utils.startItemDamageProgress
import org.bukkit.*
import org.bukkit.entity.Player
import org.bukkit.event.Listener
import org.bukkit.inventory.ItemFlag
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.ItemMeta
abstract class Gun<StateT: GunState>: TTTItem, SelectableItem {
@Suppress("LeakingThis")
override val listener: Listener = GunListener(this)
abstract val damage: Double
abstract val cooldown: Double
abstract val reloadTime: Double
abstract val magazineSize: Int
abstract val itemMaterial: Material
abstract val recoil: Int
abstract fun getState(tttPlayer: TTTPlayer): StateT
open fun computeActualDamage(tttPlayer: TTTPlayer, receiver: Player) = damage
protected fun getItemMeta(itemStack: ItemStack): ItemMeta {
val meta = itemStack.itemMeta!!
meta.setDisplayName(displayName)
meta.lore = listOf(
"",
"${ChatColor.GRAY}Schaden: ${LoreHelper.damage(damage)}",
"${ChatColor.GRAY}Cooldown: ${LoreHelper.cooldown(cooldown)}",
"${ChatColor.GRAY}Magazin: ${LoreHelper.magazineSize(magazineSize)} Schuss"
)
meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES)
return meta
}
protected fun updateLevel(tttPlayer: TTTPlayer, state: StateT = getState(tttPlayer)) {
tttPlayer.player.level = state.remainingShots
}
private fun applyRecoil(player: Player) {
val location = player.location
location.pitch -= recoil
player.teleport(location)
}
open fun onBeforeShoot(tttPlayer: TTTPlayer, item: ItemStack, state: StateT = getState(tttPlayer)) {
if (state.cooldownOrReloadTask !== null) throw ActionInProgressError()
}
fun shoot(tttPlayer: TTTPlayer, item: ItemStack, state: StateT = getState(tttPlayer)) {
onBeforeShoot(tttPlayer, item, state)
if (state.remainingShots == 0) {
GameManager.world.playSound(tttPlayer.player.location, Sound.BLOCK_ANVIL_PLACE, SoundCategory.PLAYERS, 1f, 1.3f)
return
}
// TODO: Add sound
GameManager.world.playSound(tttPlayer.player.location, Sound.BLOCK_IRON_DOOR_OPEN, SoundCategory.PLAYERS, 2f, 1.3f)
state.remainingShots--
updateLevel(tttPlayer)
if (GameManager.phase == GamePhase.COMBAT) {
val rayTraceResult = GameManager.world.rayTrace(
tttPlayer.player.eyeLocation,
tttPlayer.player.eyeLocation.direction,
200.0,
FluidCollisionMode.ALWAYS,
true,
0.01
) { it !== tttPlayer.player }
if (rayTraceResult !== null) {
val entity = rayTraceResult.hitEntity
if (entity is Player) {
val actualDamage = computeActualDamage(tttPlayer, entity)
entity.damage(actualDamage)
tttPlayer.player.playSound(tttPlayer.player.location, Sound.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.MASTER, 2f, 1.2f)
entity.velocity = tttPlayer.player.location.direction.multiply(actualDamage / 20)
}
}
}
applyRecoil(tttPlayer.player)
state.cooldownOrReloadTask = startItemDamageProgress(item, cooldown) {
state.cooldownOrReloadTask = null
}
}
open fun reload(tttPlayer: TTTPlayer, item: ItemStack, state: StateT = getState(tttPlayer)) {
if (state.cooldownOrReloadTask !== null) throw ActionInProgressError()
if (state.remainingShots == magazineSize) return
state.cooldownOrReloadTask = startItemDamageProgress(item, reloadTime) {
state.cooldownOrReloadTask = null
state.remainingShots = magazineSize
updateLevel(tttPlayer, state)
}
// TODO: Add sound
}
override fun onSelect(tttPlayer: TTTPlayer) {
updateLevel(tttPlayer)
}
override fun onDeselect(tttPlayer: TTTPlayer) {
tttPlayer.player.level = 0
}
class ActionInProgressError: RuntimeException("The gun is on cooldown or reloading")
}

View file

@ -0,0 +1,32 @@
package de.moritzruth.spigot_ttt.items.weapons.guns
import de.moritzruth.spigot_ttt.game.players.PlayerManager
import de.moritzruth.spigot_ttt.items.isRelevant
import de.moritzruth.spigot_ttt.utils.noop
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.block.Action
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.event.player.PlayerInteractEvent
class GunListener(private val gun: Gun<*>): Listener {
@EventHandler(ignoreCancelled = true)
fun onEntityDamageByEntity(event: EntityDamageByEntityEvent) {
if (event.isRelevant(gun.displayName)) event.isCancelled = true
}
@EventHandler
fun onPlayerInteract(event: PlayerInteractEvent) {
if (!event.isRelevant(gun.displayName)) return
val tttPlayer = PlayerManager.getTTTPlayer(event.player) ?: return
try {
when(event.action) {
Action.LEFT_CLICK_AIR, Action.LEFT_CLICK_BLOCK -> gun.reload(tttPlayer, event.item!!)
Action.RIGHT_CLICK_AIR, Action.RIGHT_CLICK_BLOCK -> gun.shoot(tttPlayer, event.item!!)
else -> noop()
}
} catch (e: Gun.ActionInProgressError) {}
}
}

View file

@ -0,0 +1,9 @@
package de.moritzruth.spigot_ttt.items.weapons.guns
import de.moritzruth.spigot_ttt.game.players.State
import org.bukkit.scheduler.BukkitTask
open class GunState(magazineSize: Int): State {
var cooldownOrReloadTask: BukkitTask? = null
var remainingShots = magazineSize
}

View file

@ -0,0 +1,34 @@
package de.moritzruth.spigot_ttt.items.weapons.guns.deagle
import de.moritzruth.spigot_ttt.CustomItems
import de.moritzruth.spigot_ttt.game.players.TTTPlayer
import de.moritzruth.spigot_ttt.game.players.TTTPlayer.Role.DETECTIVE
import de.moritzruth.spigot_ttt.game.players.TTTPlayer.Role.TRAITOR
import de.moritzruth.spigot_ttt.items.BuyableItem
import de.moritzruth.spigot_ttt.items.TTTItem
import de.moritzruth.spigot_ttt.items.weapons.guns.Gun
import de.moritzruth.spigot_ttt.utils.heartsToHealth
import org.bukkit.ChatColor
import org.bukkit.inventory.ItemStack
import java.util.*
object Deagle: Gun<DeagleState>(), BuyableItem {
override val spawning = true
override val displayName = "${ChatColor.BLUE}${ChatColor.BOLD}Deagle"
override val damage = heartsToHealth(5.0)
override val cooldown = 1.4
override val magazineSize = 8
override val reloadTime = 3.0
override val itemMaterial = CustomItems.deagle
override val itemStack = ItemStack(itemMaterial).apply {
itemMeta = getItemMeta(this)
}
override val recoil = 10
override val buyableBy: EnumSet<TTTPlayer.Role> = EnumSet.of(TRAITOR, DETECTIVE)
override val price = 1
override val type = TTTItem.Type.NORMAL_WEAPON
override fun getState(tttPlayer: TTTPlayer) = tttPlayer.stateContainer.get(DeagleState::class) { DeagleState() }
}

View file

@ -0,0 +1,5 @@
package de.moritzruth.spigot_ttt.items.weapons.guns.deagle
import de.moritzruth.spigot_ttt.items.weapons.guns.GunState
class DeagleState: GunState(Deagle.magazineSize)

View file

@ -0,0 +1,28 @@
package de.moritzruth.spigot_ttt.items.weapons.guns.glock
import de.moritzruth.spigot_ttt.CustomItems
import de.moritzruth.spigot_ttt.game.players.TTTPlayer
import de.moritzruth.spigot_ttt.items.TTTItem
import de.moritzruth.spigot_ttt.items.weapons.guns.Gun
import de.moritzruth.spigot_ttt.utils.heartsToHealth
import org.bukkit.ChatColor
import org.bukkit.inventory.ItemStack
object Glock: Gun<GlockState>() {
override val spawning = true
override val displayName = "${ChatColor.YELLOW}${ChatColor.BOLD}Glock"
override val damage = heartsToHealth(1.5)
override val cooldown = 0.3
override val magazineSize = 20
override val reloadTime = 2.0
override val itemMaterial = CustomItems.glock
override val itemStack = ItemStack(itemMaterial).apply {
itemMeta = getItemMeta(this)
}
override val recoil = 2
override val type = TTTItem.Type.NORMAL_WEAPON
override fun getState(tttPlayer: TTTPlayer) = tttPlayer.stateContainer.get(GlockState::class) { GlockState() }
}

View file

@ -0,0 +1,5 @@
package de.moritzruth.spigot_ttt.items.weapons.guns.glock
import de.moritzruth.spigot_ttt.items.weapons.guns.GunState
class GlockState: GunState(Glock.magazineSize)

View file

@ -0,0 +1,43 @@
package de.moritzruth.spigot_ttt.items.weapons.guns.deagle
import de.moritzruth.spigot_ttt.CustomItems
import de.moritzruth.spigot_ttt.game.players.TTTPlayer
import de.moritzruth.spigot_ttt.game.players.TTTPlayer.Role.DETECTIVE
import de.moritzruth.spigot_ttt.game.players.TTTPlayer.Role.TRAITOR
import de.moritzruth.spigot_ttt.items.BuyableItem
import de.moritzruth.spigot_ttt.items.TTTItem
import de.moritzruth.spigot_ttt.items.weapons.LoreHelper
import de.moritzruth.spigot_ttt.items.weapons.guns.Gun
import org.bukkit.ChatColor
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import java.util.*
object GoldenDeagle: Gun<GoldenDeagleState>(), BuyableItem {
override val spawning = false
override val displayName = "${ChatColor.GOLD}${ChatColor.BOLD}Golden Deagle"
override val damage = 1.0 // is not used
override val cooldown = 1.4
override val magazineSize = 2
override val reloadTime = 10.0
override val itemMaterial = CustomItems.deagle
override val itemStack = ItemStack(itemMaterial).apply {
itemMeta = getItemMeta(this).apply {
lore = listOf(
"",
"${ChatColor.GRAY}Schaden: ${LoreHelper.damage(null)}",
"${ChatColor.GRAY}Cooldown: ${LoreHelper.cooldown(cooldown)}",
"${ChatColor.GRAY}Magazin: ${LoreHelper.magazineSize(magazineSize)} Schuss"
)
}
}
override val recoil = 10
override val buyableBy: EnumSet<TTTPlayer.Role> = EnumSet.of(TRAITOR, DETECTIVE)
override val price = 3
override val type = TTTItem.Type.SPECIAL_WEAPON
override fun computeActualDamage(tttPlayer: TTTPlayer, receiver: Player) = 1000.0
override fun getState(tttPlayer: TTTPlayer) = tttPlayer.stateContainer.get(GoldenDeagleState::class) { GoldenDeagleState() }
}

View file

@ -0,0 +1,5 @@
package de.moritzruth.spigot_ttt.items.weapons.guns.deagle
import de.moritzruth.spigot_ttt.items.weapons.guns.GunState
class GoldenDeagleState: GunState(GoldenDeagle.magazineSize)

View file

@ -0,0 +1,28 @@
package de.moritzruth.spigot_ttt.items.weapons.guns.pistol
import de.moritzruth.spigot_ttt.CustomItems
import de.moritzruth.spigot_ttt.game.players.TTTPlayer
import de.moritzruth.spigot_ttt.items.TTTItem
import de.moritzruth.spigot_ttt.items.weapons.guns.Gun
import de.moritzruth.spigot_ttt.utils.heartsToHealth
import org.bukkit.ChatColor
import org.bukkit.inventory.ItemStack
object Pistol: Gun<PistolState>() {
override val spawning = true
override val displayName = "${ChatColor.YELLOW}${ChatColor.BOLD}Pistol"
override val damage = heartsToHealth(2.5)
override val cooldown = 0.8
override val magazineSize = 10
override val reloadTime = 2.0
override val itemMaterial = CustomItems.pistol
override val itemStack = ItemStack(itemMaterial).apply {
itemMeta = getItemMeta(this)
}
override val recoil = 5
override val type = TTTItem.Type.NORMAL_WEAPON
override fun getState(tttPlayer: TTTPlayer) = tttPlayer.stateContainer.get(PistolState::class) { PistolState() }
}

View file

@ -0,0 +1,5 @@
package de.moritzruth.spigot_ttt.items.weapons.guns.pistol
import de.moritzruth.spigot_ttt.items.weapons.guns.GunState
class PistolState: GunState(Pistol.magazineSize)

View file

@ -0,0 +1,87 @@
package de.moritzruth.spigot_ttt.items.weapons.guns.shotgun
import de.moritzruth.spigot_ttt.CustomItems
import de.moritzruth.spigot_ttt.game.players.TTTPlayer
import de.moritzruth.spigot_ttt.items.TTTItem
import de.moritzruth.spigot_ttt.items.weapons.guns.Gun
import de.moritzruth.spigot_ttt.plugin
import de.moritzruth.spigot_ttt.utils.heartsToHealth
import de.moritzruth.spigot_ttt.utils.secondsToTicks
import de.moritzruth.spigot_ttt.utils.startItemDamageProgress
import org.bukkit.ChatColor
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.Damageable
import org.bukkit.inventory.meta.ItemMeta
object Shotgun: Gun<ShotgunState>() {
private const val reloadTimePerBullet = 0.5
override val spawning = true
override val displayName = "${ChatColor.YELLOW}${ChatColor.BOLD}Shotgun"
override val damage = heartsToHealth(3.0)
override val cooldown = 0.8
override val magazineSize = 8
override val reloadTime get() = reloadTimePerBullet * magazineSize
override val recoil = 20
override val type = TTTItem.Type.NORMAL_WEAPON
override val itemMaterial = CustomItems.shotgun
override val itemStack = ItemStack(itemMaterial).apply {
itemMeta = getItemMeta(this).apply {
lore = lore!! + listOf("", "${ChatColor.RED}Weniger Schaden auf Distanz")
}
}
override fun getState(tttPlayer: TTTPlayer) = tttPlayer.stateContainer.get(ShotgunState::class) { ShotgunState() }
override fun computeActualDamage(tttPlayer: TTTPlayer, receiver: Player): Double {
val distance = tttPlayer.player.location.distance(receiver.location)
return when {
distance <= 2 -> heartsToHealth(10.0)
distance >= 14 -> 0.0
distance > 8 -> heartsToHealth(1.5)
else -> heartsToHealth(damage)
}
}
override fun reload(tttPlayer: TTTPlayer, item: ItemStack, state: ShotgunState) {
if (state.remainingShots == magazineSize) return
state.cooldownOrReloadTask = startItemDamageProgress(item, reloadTime, state.remainingShots.toDouble() / magazineSize) {
state.cooldownOrReloadTask = null
}
state.reloadUpdateTask = plugin.server.scheduler.runTaskTimer(plugin, fun() {
state.remainingShots++
updateLevel(tttPlayer)
// TODO: Add sound
if (state.remainingShots == magazineSize) {
state.reloadUpdateTask?.cancel()
state.reloadUpdateTask = null
}
}, secondsToTicks(reloadTimePerBullet).toLong(), secondsToTicks(reloadTimePerBullet).toLong())
}
override fun onBeforeShoot(tttPlayer: TTTPlayer, item: ItemStack, state: ShotgunState) {
if (state.remainingShots == 0) return
if (state.reloadUpdateTask == null && state.cooldownOrReloadTask != null) {
throw ActionInProgressError()
}
state.cooldownOrReloadTask?.cancel()
state.cooldownOrReloadTask = null
val damageMeta = item.itemMeta!! as Damageable
damageMeta.damage = 0
item.itemMeta = damageMeta as ItemMeta
state.reloadUpdateTask?.cancel()
state.reloadUpdateTask = null
}
}

View file

@ -0,0 +1,8 @@
package de.moritzruth.spigot_ttt.items.weapons.guns.shotgun
import de.moritzruth.spigot_ttt.items.weapons.guns.GunState
import org.bukkit.scheduler.BukkitTask
class ShotgunState: GunState(Shotgun.magazineSize) {
var reloadUpdateTask: BukkitTask? = null
}

View file

@ -0,0 +1,41 @@
package de.moritzruth.spigot_ttt.items.weapons.knife
import de.moritzruth.spigot_ttt.CustomItems
import de.moritzruth.spigot_ttt.game.players.TTTPlayer
import de.moritzruth.spigot_ttt.game.players.TTTPlayer.Role.TRAITOR
import de.moritzruth.spigot_ttt.items.BuyableItem
import de.moritzruth.spigot_ttt.items.TTTItem
import de.moritzruth.spigot_ttt.items.weapons.LoreHelper
import org.bukkit.ChatColor
import org.bukkit.inventory.ItemFlag
import org.bukkit.inventory.ItemStack
import java.util.*
object Knife: TTTItem, BuyableItem {
override val displayName = "${ChatColor.RED}${ChatColor.BOLD}Knife"
override val spawning = false
override val listener = KnifeListener(this)
override val itemStack = ItemStack(CustomItems.knife)
override val buyableBy: EnumSet<TTTPlayer.Role> = EnumSet.of(TRAITOR)
override val price = 1
override val type = TTTItem.Type.SPECIAL_WEAPON
init {
val meta = itemStack.itemMeta!!
meta.setDisplayName(displayName)
meta.lore = listOf(
"",
"${ChatColor.GRAY}Schaden: ${LoreHelper.damage(null)}",
"",
"${ChatColor.RED}Nur einmal verwendbar",
"${ChatColor.RED}Nur aus nächster Nähe"
)
meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES)
itemStack.itemMeta = meta
}
}

View file

@ -0,0 +1,31 @@
package de.moritzruth.spigot_ttt.items.weapons.knife
import de.moritzruth.spigot_ttt.items.isRelevant
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.inventory.meta.Damageable
import org.bukkit.inventory.meta.ItemMeta
class KnifeListener(val knife: Knife): Listener {
@EventHandler(ignoreCancelled = true)
fun onEntityDamageByEntity(event: EntityDamageByEntityEvent) {
if (!event.isRelevant(Knife.displayName)) return
val damagerPlayer = event.damager as Player
val damagedPlayer = event.entity as Player
val distance = damagerPlayer.location.distance(damagedPlayer.location)
if (distance > 1.5) event.isCancelled = true else {
// Break the item
val item = damagerPlayer.inventory.itemInMainHand
val damageableMeta = item.itemMeta!! as Damageable
damageableMeta.damage = 1000
item.itemMeta = damageableMeta as ItemMeta
event.damage = 1000.0
}
}
}

View file

@ -0,0 +1,45 @@
package de.moritzruth.spigot_ttt.shop
import de.moritzruth.spigot_ttt.game.players.TTTPlayer
import de.moritzruth.spigot_ttt.items.BuyableItem
import de.moritzruth.spigot_ttt.items.ItemManager
import org.bukkit.ChatColor
object Shop {
val SHOP_SLOTS = (9..35).toMutableList().apply {
// last row; possibly needed in the future
// remove(17)
// remove(26)
// remove(35)
}.toList()
private val ITEMS_PER_PAGE = SHOP_SLOTS.count()
@Suppress("UNCHECKED_CAST")
fun getBuyableItems(tttPlayer: TTTPlayer): Set<BuyableItem> = ItemManager.items.filter {
if (it is BuyableItem) it.buyableBy.contains(tttPlayer.role) else false
}.toSet() as Set<BuyableItem>
fun show(tttPlayer: TTTPlayer) {
val itemsIterator = getBuyableItems(tttPlayer).iterator()
for(index in SHOP_SLOTS) {
if (!itemsIterator.hasNext()) break
val tttItem = itemsIterator.next()
val itemStack = tttItem.itemStack.clone()
val meta = itemStack.itemMeta!!
meta.setDisplayName(meta.displayName + "${ChatColor.RESET} ${ChatColor.WHITE}${ChatColor.BOLD}$${tttItem.price}")
itemStack.itemMeta = meta
tttPlayer.player.inventory.setItem(index, itemStack)
}
}
fun hide(tttPlayer: TTTPlayer) {
val range = 9..19
range + (1..8)
for(index in 9..35) tttPlayer.player.inventory.clear(index) // All slots except the hotbar and armor
}
}

View file

@ -0,0 +1,53 @@
package de.moritzruth.spigot_ttt.shop
import com.connorlinfoot.actionbarapi.ActionBarAPI
import de.moritzruth.spigot_ttt.game.players.PlayerManager
import de.moritzruth.spigot_ttt.game.players.TTTPlayer
import de.moritzruth.spigot_ttt.items.BuyableItem
import de.moritzruth.spigot_ttt.items.ItemManager
import de.moritzruth.spigot_ttt.plugin
import org.bukkit.ChatColor
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.inventory.ClickType
import org.bukkit.event.inventory.InventoryClickEvent
object ShopListener: Listener {
fun register() {
plugin.server.pluginManager.registerEvents(this, plugin)
}
@EventHandler(ignoreCancelled = true)
fun onInventoryClick(event: InventoryClickEvent) {
if (event.whoClicked !is Player) return
val tttPlayer = PlayerManager.getTTTPlayer(event.whoClicked as Player) ?: return
if (event.click === ClickType.CREATIVE) return
event.isCancelled = true
val itemStack = event.currentItem
if (
(event.click === ClickType.RIGHT || event.click === ClickType.LEFT) &&
itemStack !== null &&
event.clickedInventory?.holder == tttPlayer.player &&
Shop.SHOP_SLOTS.contains(event.slot)
) {
val tttItem = ItemManager.getItemByItemStack(itemStack)
if (tttItem === null || tttItem !is BuyableItem || !tttItem.buyableBy.contains(tttPlayer.role)) return
if (tttPlayer.credits < tttItem.price) {
ActionBarAPI.sendActionBar(tttPlayer.player, "${ChatColor.RED}Du hast nicht genug Credits")
}
try {
tttPlayer.addItem(tttItem)
tttPlayer.credits -= tttItem.price
} catch (e: TTTPlayer.AlreadyHasItemException) {
ActionBarAPI.sendActionBar(tttPlayer.player, "${ChatColor.RED}Du hast dieses Item bereits")
} catch (e: TTTPlayer.TooManyItemsOfTypeException) {
ActionBarAPI.sendActionBar(tttPlayer.player, "${ChatColor.RED}Du hast keinen Platz dafür")
}
}
}
}

View file

@ -0,0 +1,19 @@
package de.moritzruth.spigot_ttt.utils
import de.moritzruth.spigot_ttt.plugin
import org.bukkit.configuration.file.YamlConfiguration
import java.nio.file.Path
class ConfigurationFile(name: String): YamlConfiguration() {
private val filePath = Path.of(plugin.dataFolder.absolutePath, "$name.yml").toAbsolutePath().toString()
init {
try {
load(filePath)
} catch (e: Exception) {}
}
fun save() {
save(filePath)
}
}

View file

@ -0,0 +1,5 @@
package de.moritzruth.spigot_ttt.utils
fun secondsToTicks(seconds: Double) = (seconds * 20).toInt()
fun secondsToTicks(seconds: Int) = secondsToTicks(seconds.toDouble())
fun heartsToHealth(hearts: Double) = hearts * 2

View file

@ -0,0 +1,11 @@
package de.moritzruth.spigot_ttt.utils
import org.bukkit.inventory.Inventory
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.PlayerInventory
fun Inventory.setAllToItem(indexes: Iterable<Int>, itemStack: ItemStack) {
indexes.forEach { setItem(it, itemStack) }
}
val PlayerInventory.hotbarContents get() = this.contents.slice(0..8) as List<ItemStack?>

View file

@ -0,0 +1,14 @@
package de.moritzruth.spigot_ttt.utils
import org.bukkit.Material
val maxDurabilitiesOfItems = mapOf(
Material.WOODEN_HOE to 59,
Material.STONE_HOE to 131,
Material.GOLDEN_HOE to 32,
Material.IRON_HOE to 250,
Material.DIAMOND_HOE to 1561,
Material.IRON_AXE to 250
)
fun getMaxDurability(material: Material) = maxDurabilitiesOfItems[material] ?: throw Exception("The maximum durability of this item is not defined")

View file

@ -0,0 +1,16 @@
package de.moritzruth.spigot_ttt.utils
import org.bukkit.command.Command
import org.bukkit.command.CommandSender
import org.bukkit.command.TabCompleter
fun noop() {}
class NoOpTabCompleter: TabCompleter {
override fun onTabComplete(
sender: CommandSender,
command: Command,
alias: String,
args: Array<out String>
) = mutableListOf<String>()
}

View file

@ -0,0 +1,5 @@
package de.moritzruth.spigot_ttt.utils
import kotlin.math.roundToInt
fun randomNumber(min: Int = 0, max: Int): Int = (min + (Math.random() * (max - min))).roundToInt()

View file

@ -0,0 +1,33 @@
package de.moritzruth.spigot_ttt.utils
import de.moritzruth.spigot_ttt.plugin
import org.bukkit.inventory.ItemStack
import org.bukkit.inventory.meta.Damageable
import org.bukkit.inventory.meta.ItemMeta
import org.bukkit.scheduler.BukkitTask
import java.time.Instant
import kotlin.math.roundToInt
fun startItemDamageProgress(item: ItemStack, duration: Double, startProgress: Double = 0.0, onFinish: () -> Unit): BukkitTask {
val startedAt = Instant.now().toEpochMilli()
var task: BukkitTask? = null
task = plugin.server.scheduler.runTaskTimer(plugin, fun() {
val secondsElapsed = (Instant.now().toEpochMilli() - startedAt) / 1000.0
val progress = secondsElapsed / duration + startProgress
val maxDurability = getMaxDurability(item.type)
val damageMeta = item.itemMeta!! as Damageable
damageMeta.damage = maxDurability - (maxDurability * progress).roundToInt()
item.itemMeta = damageMeta as ItemMeta
if (progress >= 1) {
task?.cancel()
onFinish()
}
}, 0, 1)
return task
}

View file

@ -0,0 +1,7 @@
package de.moritzruth.spigot_ttt.utils
import org.bukkit.entity.Player
fun teleportPlayerToWorldSpawn(player: Player) {
player.teleport(player.world.spawnLocation)
}

View file

@ -0,0 +1,5 @@
package de.moritzruth.spigot_ttt.utils
import java.util.*
fun UUID.toTrimmedString() = toString().replace("-", "")

View file

@ -0,0 +1,11 @@
min-players: 4
min-players-for:
detective: 5
jackal: 6
discord-token: xxx
duration:
preparing: 20
combat: 480
over: 10

View file

@ -0,0 +1,23 @@
name: TTT
version: 1.0.0
api-version: "1.14"
main: de.moritzruth.spigot_ttt.TTTPlugin
depend:
- CorpseReborn
- ActionBarAPI
commands:
start:
usage: /start
permission: ttt.start
description: Starts the TTT game
abort:
usage: /abort
permission: ttt.abort
description: Aborts the TTT game
additemspawn:
usage: /additemspawn
permission: ttt.additemspawn
description: Add an item spawn