Lots of things

master
kalle 2023-04-08 23:40:13 +02:00
parent cbf1e64db9
commit 95391de3b7
89 changed files with 3872 additions and 559 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/build/
/.gradle/
/.idea/

View File

@ -31,5 +31,10 @@
<option name="name" value="maven2" />
<option name="url" value="https://repo.aikar.co/content/groups/aikar/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
</component>
</project>

View File

@ -14,6 +14,9 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="PWA">
<option name="wasEnabledAtLeastOnce" value="true" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_16" default="true" project-jdk-name="16" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>

View File

@ -1,8 +1,9 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.5.31"
kotlin("jvm") version "1.6.10"
id("com.github.johnrengelman.shadow") version "7.1.0"
kotlin("plugin.serialization") version "1.6.10"
}
group = "nl.kallestruik"
@ -11,6 +12,7 @@ version = "2.0"
repositories {
mavenCentral()
mavenLocal()
jcenter()
maven("https://papermc.io/repo/repository/maven-public/")
maven("https://repo.aikar.co/content/groups/aikar/")
@ -19,7 +21,9 @@ repositories {
dependencies {
implementation("co.aikar:acf-paper:0.5.0-SNAPSHOT")
compileOnly("com.destroystokyo.paper:paper-api:1.16.5-R0.1-SNAPSHOT")
compileOnly("nl.kallestruik:DLib:1.4.1")
compileOnly(kotlin("stdlib-jdk8"))
compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
}
tasks.compileJava {

178
gradlew.bat vendored
View File

@ -1,89 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -1,25 +1,174 @@
package nl.kallestruik.darena
import nl.kallestruik.darena.managers.ConfigManager
import nl.kallestruik.darena.managers.PointsManager
import nl.kallestruik.darena.managers.TeamManager
import co.aikar.commands.BukkitCommandExecutionContext
import co.aikar.commands.InvalidCommandArgument
import co.aikar.commands.MessageType
import co.aikar.commands.PaperCommandManager
import nl.kallestruik.darena.arenas.Arena
import nl.kallestruik.darena.commands.CommandDArena
import nl.kallestruik.darena.exceptions.TeamNotFoundException
import nl.kallestruik.darena.listeners.*
import nl.kallestruik.darena.managers.*
import nl.kallestruik.darena.types.Reloadable
import nl.kallestruik.darena.types.Team
import org.bukkit.ChatColor
import org.bukkit.plugin.java.JavaPlugin
import java.io.File
class DArena : JavaPlugin() {
//TODO: Add documentation on a wiki page or something for this whole thing.
class DArena: JavaPlugin() {
private lateinit var commandManager: PaperCommandManager
private lateinit var configManager: ConfigManager
private lateinit var pointsManager: PointsManager
private lateinit var teamManager: TeamManager
private lateinit var arenaManager: ArenaManager
private lateinit var editManager: EditManager
private lateinit var gameManager: GameManager
private lateinit var reloadable: Map<String, Reloadable>
override fun onEnable() {
commandManager = PaperCommandManager(this)
commandManager.enableUnstableAPI("help")
commandManager.enableUnstableAPI("brigadier")
configManager = ConfigManager(File(dataFolder, "config.yml"))
configManager.load()
teamManager = TeamManager(File(dataFolder, "teams.yml"))
teamManager.load()
pointsManager = PointsManager(teamManager, File(dataFolder, "points.yml"))
pointsManager.load()
arenaManager = ArenaManager(teamManager, pointsManager, configManager, this, File(dataFolder, "arenas"), File(dataFolder, "arena.schema.json"))
arenaManager.loadArenas()
editManager = EditManager(arenaManager, this)
arenaManager.editManager = editManager
gameManager = GameManager(arenaManager, File(dataFolder, "games.yml"))
gameManager.load()
reloadable = mapOf(
Pair("arenas", arenaManager),
Pair("config", configManager),
Pair("games", gameManager),
)
setupCommandContexts()
setupCommandCompletions()
setupACFFormatting()
commandManager.registerCommand(CommandDArena(arenaManager, editManager, pointsManager, teamManager, gameManager))
server.pluginManager.registerEvents(CheckpointListener(arenaManager), this)
server.pluginManager.registerEvents(TeamListener(teamManager), this)
server.pluginManager.registerEvents(KillListener(arenaManager), this)
server.pluginManager.registerEvents(FireballListener(arenaManager, configManager), this)
server.pluginManager.registerEvents(FallListener(arenaManager), this)
server.pluginManager.registerEvents(BorderListener(arenaManager), this)
server.pluginManager.registerEvents(BuildRestrictionListener(arenaManager), this)
server.pluginManager.registerEvents(SectionListener(arenaManager), this)
server.pluginManager.registerEvents(CheckpointSoundListener(arenaManager), this)
server.pluginManager.registerEvents(CheckpointMessageListener(arenaManager), this)
server.pluginManager.registerEvents(GrapplingHookListener(arenaManager), this)
server.pluginManager.registerEvents(EditListener(editManager, this), this)
}
override fun onDisable() {
teamManager.save()
pointsManager.save()
}
//TODO: Move this to a more suitable location
private fun setupACFFormatting() {
commandManager.setFormat(
MessageType.INFO,
ChatColor.GRAY,
ChatColor.YELLOW,
ChatColor.RED
)
commandManager.setFormat(
MessageType.SYNTAX,
ChatColor.YELLOW,
ChatColor.GREEN,
ChatColor.WHITE
)
commandManager.setFormat(
MessageType.ERROR,
ChatColor.RED,
ChatColor.DARK_GREEN,
ChatColor.GREEN
)
commandManager.setFormat(
MessageType.HELP,
ChatColor.YELLOW,
ChatColor.AQUA,
ChatColor.GRAY
)
}
//TODO: Move this to a more suitable location
private fun setupCommandContexts() {
commandManager.commandContexts.registerContext(
Arena::class.java
) { context: BukkitCommandExecutionContext ->
return@registerContext arenaManager.getArena(context.popFirstArg())
?: throw InvalidCommandArgument("Arena does not exist!")
}
commandManager.commandContexts.registerContext(
Team::class.java
) { context: BukkitCommandExecutionContext ->
try {
return@registerContext teamManager.getTeamByName(context.popFirstArg())
} catch (e: TeamNotFoundException) {
throw InvalidCommandArgument("Arena does not exist!")
}
}
commandManager.commandContexts.registerContext(
Reloadable::class.java
) { context: BukkitCommandExecutionContext ->
return@registerContext reloadable[context.popFirstArg()]
?: throw InvalidCommandArgument("That is not a reloadable component!")
}
}
//TODO: Move this to a more suitable location
private fun setupCommandCompletions() {
// Arena
commandManager.commandCompletions.registerAsyncCompletion("arena") {
arenaManager.getAllArenaNames()
}
commandManager.commandCompletions.setDefaultCompletion("arena", Arena::class.java)
// Team
commandManager.commandCompletions.registerAsyncCompletion("team") {
teamManager.getAllTeamNames()
}
commandManager.commandCompletions.setDefaultCompletion("team", Team::class.java)
// teamOptions
commandManager.commandCompletions.registerCompletion("teamOptions") {
listOf(
"color",
"prefix"
)
}
// Reloadable
commandManager.commandCompletions.registerAsyncCompletion("reloadable") {
reloadable.keys
}
commandManager.commandCompletions.setDefaultCompletion("reloadable", Reloadable::class.java)
}
}

View File

@ -1,62 +1,192 @@
package nl.kallestruik.darena.arenas
import net.kyori.adventure.text.Component
import nl.kallestruik.darena.arenas.world.ArenaWorld
import nl.kallestruik.darena.exceptions.ArenaStartAbortedException
import nl.kallestruik.darena.managers.ArenaManager
import nl.kallestruik.darena.managers.ConfigManager
import nl.kallestruik.darena.managers.PointsManager
import nl.kallestruik.darena.managers.TeamManager
import nl.kallestruik.darena.types.ArenaFeature
import nl.kallestruik.darena.types.arena.ProcessedArenaCheckpoint
import nl.kallestruik.darena.types.border.MCBorder
import nl.kallestruik.darena.types.countdown.AsyncCountdown
import nl.kallestruik.darena.types.countdown.AsyncCountdownWithSyncTask
import nl.kallestruik.darena.util.ArenaUtil
import nl.kallestruik.darena.util.Logger
import nl.kallestruik.darena.util.RenderUtil
import org.bukkit.Color
import org.bukkit.GameMode
import org.bukkit.World
import org.bukkit.entity.Player
import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
import org.bukkit.scheduler.BukkitRunnable
class Arena(
private val config: ArenaConfig,
private val arenaUtil: ArenaUtil,
val config: ArenaConfig,
private val plugin: JavaPlugin,
private val world: ArenaWorld = ArenaWorld(config.name, plugin),
private val configManager: ConfigManager,
private val arenaManager: ArenaManager,
private val teamManager: TeamManager,
private val pointsManager: PointsManager,
val world: ArenaWorld = ArenaWorld(config.name, configManager, plugin),
) {
private lateinit var session: ArenaSession
// Simple stuff done: 0.001474s
// createArena start: 0.0001026s
// Loaded schematic: 0.000162s
// Created new world: 3.122034s
// Loaded world: 0.0075989s
// Placed arena: 2.3205267s
// Fixed light: 4.2093585s
// createArena done: 0.0002953
// Countdown done: 5.0126158
// Fully done: 0.0982557
var session: ArenaSession? = null
var pointsSession: PointsSession? = null
fun start(loadingTimeOverride: Int, spectatorTimeOverride: Int, beforeStartTimeOverride: Int) {
val loadingTime = if (loadingTimeOverride != -1) loadingTimeOverride else config.countdown.loadingTime
val spectatorTime = if (spectatorTimeOverride != -1) spectatorTimeOverride else config.countdown.spectatorTime
val beforeStartTime = if (beforeStartTimeOverride != -1) beforeStartTimeOverride else config.countdown.beforeStartTime
fun start() {
//TODO: Redo everything in here.
// Create a new session
session = ArenaSession()
pointsSession = PointsSession(teamManager, pointsManager, this)
// Add all participants and spectators
session.participants.addAll(arenaUtil.getPossibleParticipants());
session.spectators.addAll(arenaUtil.getPossibleSpectators());
// Reset the world
world.reset()
session!!.originalParticipants.addAll(ArenaUtil.getPossibleParticipants())
session!!.participants.addAll(session!!.originalParticipants)
session!!.spectators.addAll(ArenaUtil.getPossibleSpectators())
session!!.allPlayers.addAll(session!!.originalParticipants.union(session!!.spectators))
session!!.currentCountdown = AsyncCountdownWithSyncTask(plugin, loadingTime, "Teleporting in", { //TODO: Make the subtitle configurable
// Reset the world
world.reset()
}, {
session!!.processSpawnRules(config.spawns, config.spawnRules, config.loadouts)
session!!.processSpawnPools(config.spawnPools)
session!!.processCheckpoints(config.checkpoints)
startBorders()
// Place all spectators in the arena
session.spectators.forEach {
// config.spectatorSpawn.spawn(world, it)
}
// Spawn everyone as a spectator so they can explore the map.
for (player in session!!.allPlayers) {
player.gameMode = GameMode.SPECTATOR
}
// Randomize spawns
session.initialSpawns.addAll(config.spawns.filter {
//TODO: make this filter only spawns that meet the intitial spawn condidtions. Or just replace this whole method.
return@filter true
}.keys)
session.initialSpawns.shuffle()
val spectatorSpawnRule = session!!.spawnRules["spectator"] ?: throw ArenaStartAbortedException("Missing spawn rule \"spectator\". This rule is required.")
// Spawn all the players
session.participants.forEachIndexed { index, player ->
// TODO: Freeze players in place (for in arena countdown) (if countdown is 0 dont freeze them)
config.spawns[session.initialSpawns[index % session.initialSpawns.size]]!!.spawn(world, player)
}
// TODO:
spectatorSpawnRule.spawnPlayers(session!!.allPlayers, world)
session!!.currentCountdown = AsyncCountdown(plugin, spectatorTime, "Starting in", alwaysAsActionBar = true) { //TODO: Make the subtitle configurable
for (participant in session!!.participants) {
if (config.features.contains(ArenaFeature.GAMEMODE_ADVENTURE))
participant.gameMode = GameMode.ADVENTURE
else
participant.gameMode = GameMode.SURVIVAL
}
val initialSpawnRule = session!!.spawnRules["initial"] ?: throw ArenaStartAbortedException("Missing spawn rule \"initial\". This rule is required.")
initialSpawnRule.spawnPlayers(session!!.participants, world)
// Mark the game as in progress. This makes the event listeners all trigger from this point onwards.
session!!.isInProgress = true
session!!.currentCountdown = AsyncCountdown(plugin, beforeStartTime, "Beginning in", alwaysAsTitle = true) { //TODO: Make the subtitle configurable
config.blockRemoveZone?.removeBlocks(world)
startTimers()
}.start(session!!.allPlayers)
}.start(session!!.allPlayers)
}).start(session!!.allPlayers)
}
fun reset() {
world.reset()
fun startTimers() {
session!!.startTime = System.currentTimeMillis()
if (config.endConditions.time != -1) {
session!!.timeLeft = config.endConditions.time
session!!.endTimeTask = object : BukkitRunnable() {
override fun run() {
arenaManager.end()
}
}.runTaskLater(plugin, config.endConditions.time * 20L)
}
session!!.timerTask = object : BukkitRunnable() {
override fun run() {
session!!.timeLeft--
updateActionbar()
}
}.runTaskTimer(plugin, 20, 20)
}
fun startBorders() {
config.borders.vertical.getOrNull(0)?.execute(MCBorder(world), plugin)
config.borders.top.getOrNull(0)?.execute(session!!.borderTop, plugin)
config.borders.bottom.getOrNull(0)?.execute(session!!.borderBottom, plugin)
if (hasFeature(ArenaFeature.HORIZONTAL_BORDERS)) {
session!!.borderTask = BorderRunnable(world.world, session!!).runTaskTimer(plugin, 0, 10)
}
}
fun updateActionbar() {
var actionBar = Component.empty()
if (hasFeature(ArenaFeature.SHOW_TIMER))
actionBar = actionBar.append(Component
.text("Time left: ${ArenaUtil.formatTime(session!!.timeLeft)}"))
val allPlayers = session!!.originalParticipants.union(session!!.spectators)
for (player in allPlayers) {
player.sendActionBar(actionBar)
}
}
fun end() {
for (player in session!!.originalParticipants) {
ArenaUtil.clearPlayer(player)
player.gameMode = GameMode.SURVIVAL
session!!.spectators.remove(player)
}
world.empty()
if (pointsSession != null)
pointsSession!!.end()
session?.timerTask?.cancel()
session?.endTimeTask?.cancel()
session?.currentCountdown?.cancel()
session?.borderTask?.cancel()
}
fun hasFeature(feature: ArenaFeature) = config.features.contains(feature)
fun sendMessage(component: Component) {
for (player in session!!.allPlayers) {
player.sendMessage(component)
}
}
fun makeSpectator(player: Player) {
Logger.trace(Arena::class, "makeSpectator(player: ${player.name})")
player.gameMode = GameMode.SPECTATOR
session!!.participants.remove(player)
session!!.spectators.add(player)
}
fun respawn(player: Player) {
val lastCheckpoint: ProcessedArenaCheckpoint? = session!!.lastCheckpoint[player.uniqueId]
val spawnRule = if (lastCheckpoint != null)
lastCheckpoint.spawnRule!!
else
session!!.spawnRules["initial"]!!
spawnRule.spawnPlayer(player, world)
}
private class BorderRunnable(
private val world: World,
private val session: ArenaSession
): BukkitRunnable() {
override fun run() {
RenderUtil.drawXZPlane(world,
-100, -100,
100, 100,
session.borderTop.height, Color.RED)
RenderUtil.drawXZPlane(world,
-100, -100,
100, 100,
session.borderBottom.height, Color.RED)
}
}
}

View File

@ -1,74 +1,85 @@
package nl.kallestruik.darena.arenas
import nl.kallestruik.darena.types.arena.ArenaLoadout
import nl.kallestruik.darena.types.arena.ArenaCheckpoint
import nl.kallestruik.darena.types.arena.ArenaPoints
import nl.kallestruik.darena.types.arena.ArenaSpawn
import nl.kallestruik.darena.types.arena.ArenaSpawnRule
import nl.kallestruik.darena.util.ConfigHelper
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import nl.kallestruik.darena.types.ArenaFeature
import nl.kallestruik.darena.types.arena.*
import nl.kallestruik.darena.util.Logger
import nl.kallestruik.dlib.config.annotations.Description
import java.io.File
import org.bukkit.configuration.InvalidConfigurationException
import org.bukkit.configuration.file.YamlConfiguration
import kotlin.collections.mutableMapOf
@Serializable
data class ArenaConfig(
@Description(["The name of the arena."])
var name: String,
var file: File,
@Description(["The spawns that the arena will use. These alone are not enough to make a player spawn (See spawnRules)."])
var spawns: MutableMap<String, ArenaSpawn> = mutableMapOf(),
@Description(["The loadouts for the arena. These determine the items a player spawns with and what potion effects they get."])
var loadouts: MutableMap<String, ArenaLoadout> = mutableMapOf(),
var checkpoints: MutableMap<String, ArenaCheckpoint> = mutableMapOf(),
@Description(["The checkpoints for the arena. Both normal checkpoints and finish points are defined here."])
var checkpoints: MutableList<ArenaCheckpoint> = mutableListOf(),
@Description([
"The points that can be obtained by doing certain actions.",
"These points are always awarded top down with the last repeating.",
"For example when you set kill to 10, 5, and 0, the first player to get a kill will receive 10 points, the second 5, and every player after that will receive 0."])
var points: ArenaPoints = ArenaPoints(),
var spawnRules: MutableMap<String, ArenaSpawnRule> = mutableMapOf()
@Description([
"The spawn rules used by the arena. These rules are used by many different components to link multiple spawns together with a set of items.",
"There are two special spawn rules that are required for the operation of the plugin. These are:",
"initial - Used by the plugin to do the initial round of spawning.",
"spectator - Used by the plugin to spawn the spectators."])
var spawnRules: MutableMap<String, ArenaSpawnRule> = mutableMapOf(),
@Description(["The list of features that are enabled in the arena."])
var features: MutableSet<ArenaFeature> = mutableSetOf(),
@Description(["" +
"The different borders used by the arena. These are defined using border script. Below a short explanation of the different keyword:",
"CENTER <x> <z> - Set the center of the border to x,z. (ONLY AVAILABLE FOR VERTICAL BORDERS)",
"SET <i> - On vertical borders set the with to i. On horizontal borders set the height to i.",
"TRANSITION <i> <t> - Same as SET <i>, but over t seconds.",
"WAIT <t> - Wait for t seconds."])
var borders: ArenaBorders = ArenaBorders(),
@SerialName("end-conditions")
@Description(["The end conditions of the arena."])
var endConditions: ArenaEndConditions = ArenaEndConditions(),
@SerialName("spawn-pools")
@Description(["The spawn pools that can be used by the arena. These are used to define groups of spawn rules that can be chosen at random by checkpoints."])
var spawnPools: MutableMap<String, ArenaSpawnPool> = mutableMapOf(),
@SerialName("break-allowed")
@Description(["The blocks that players are allowed to break in the arena. Unless specified otherwise all blocks can be broken."])
var breakAllowed: ArenaAllowList = ArenaAllowList(),
@SerialName("place-allowed")
@Description(["The blocks that players are allowed to place in the arena. Unless specified otherwise all blocks can be broken."])
var placeAllowed: ArenaAllowList = ArenaAllowList(),
@Description(["The different countdowns used by the arena."])
var countdown: ArenaCountdown = ArenaCountdown(),
@SerialName("block-remove-zone")
@Description(["A area of blocks to remove at the start of the game."])
var blockRemoveZone: ArenaBlockRemoveZone? = null,
) {
fun save(toString: Boolean = false): String? {
val config = YamlConfiguration()
config.set("name", name)
@Transient
lateinit var file: File
ConfigHelper.saveMap(ConfigHelper.getOrCreateSection(config, "spawns"), spawns)
ConfigHelper.saveMap(ConfigHelper.getOrCreateSection(config, "loadouts"), loadouts)
ConfigHelper.saveMap(ConfigHelper.getOrCreateSection(config, "checkpoints"), checkpoints)
points.save(ConfigHelper.getOrCreateSection(config, "points"))
ConfigHelper.saveMap(ConfigHelper.getOrCreateSection(config, "spawnRules"), spawnRules)
fun save(toString: Boolean = false): String? {
val json = Json {
prettyPrint = true
}.encodeToString(this)
if (toString) {
return config.saveToString()
return json
}
config.save(file)
file.writeText(json, Charsets.UTF_8)
return null
}
companion object {
@Throws(InvalidConfigurationException::class)
fun load(file: File): ArenaConfig {
val config = ConfigHelper.getOrCreateConfig(file, "template/arena.yml")
if (!config.contains("name")) {
throw InvalidConfigurationException("The arena configuration file '${file.name}' does not contain the required attribute 'name'")
}
Logger.trace(ArenaConfig::class, "load(file: ${file.path})")
val arenaConfig = ArenaConfig(config.getString("name")!!, file)
val config = Json.decodeFromString<ArenaConfig>(file.readText(Charsets.UTF_8))
config.file = file
if (config.contains("spawns")) {
arenaConfig.spawns = ConfigHelper.loadMap(config.getConfigurationSection("spawns")!!, ArenaSpawn)
}
if (config.contains("loadouts")) {
arenaConfig.loadouts = ConfigHelper.loadMap(config.getConfigurationSection("loadouts")!!, ArenaLoadout)
}
if (config.contains("checkpoints")) {
arenaConfig.checkpoints = ConfigHelper.loadMap(config.getConfigurationSection("checkpoints")!!, ArenaCheckpoint)
}
if (config.contains("points")) {
arenaConfig.points = ArenaPoints.load(config.getConfigurationSection("points")!!)
}
if (config.contains("spawnRules")) {
arenaConfig.spawnRules = ConfigHelper.loadMap(config.getConfigurationSection("spawnRules")!!, ArenaSpawnRule)
}
return arenaConfig
return config
}
}
}

View File

@ -1,13 +1,81 @@
package nl.kallestruik.darena.arenas
import nl.kallestruik.darena.types.arena.ArenaSpawn
import nl.kallestruik.darena.exceptions.ArenaStartAbortedException
import nl.kallestruik.darena.types.arena.*
import nl.kallestruik.darena.types.border.HorizontalBorder
import nl.kallestruik.darena.types.countdown.Countdown
import org.bukkit.entity.Player
import java.util.UUID
import org.bukkit.scheduler.BukkitTask
import java.util.*
data class ArenaSession(
val participants: MutableList<Player> = ArrayList(),
val spectators: MutableList<Player> = ArrayList(),
val completedObjective: MutableList<UUID> = ArrayList(),
val deaths: MutableList<UUID> = ArrayList(),
val initialSpawns: MutableList<String> = ArrayList(),
)
val originalParticipants: MutableList<Player> = mutableListOf(),
val participants: MutableList<Player> = mutableListOf(),
val spectators: MutableList<Player> = mutableListOf(),
val allPlayers: MutableList<Player> = mutableListOf(),
val completedObjective: MutableList<UUID> = mutableListOf(),
val deaths: MutableList<UUID> = mutableListOf(),
val spawnRules: MutableMap<String, ProcessedArenaSpawnRule> = mutableMapOf(),
val spawnPools: MutableMap<String, ProcessedArenaSpawnPool> = mutableMapOf(),
val checkpoints: MutableList<ProcessedArenaCheckpoint> = mutableListOf(),
val lastCheckpoint: MutableMap<UUID, ProcessedArenaCheckpoint> = mutableMapOf(),
val reachedCheckpoints: MutableMap<UUID, MutableSet<String>> = mutableMapOf(),
val lastDamage: MutableMap<UUID, UUID> = mutableMapOf(),
val borderTop: HorizontalBorder = HorizontalBorder(),
val borderBottom: HorizontalBorder = HorizontalBorder(),
val finishedSectionsByPlayer: MutableMap<UUID, Int> = mutableMapOf(),
val finishedPerSection: MutableMap<Int, Int> = mutableMapOf(),
var startTime: Long = 0,
var playersFinished: Int = 0,
var timeLeft: Int = -1,
var endTimeTask: BukkitTask? = null,
var timerTask: BukkitTask? = null,
var borderTask: BukkitTask? = null,
var isInProgress: Boolean = false,
var currentCountdown: Countdown? = null,
) {
fun processSpawnRules(spawns: Map<String, ArenaSpawn>, rules: Map<String, ArenaSpawnRule>, loadouts: Map<String, ArenaLoadout>) {
for (rule in rules) {
val list = mutableListOf<ArenaSpawn>()
for (spawn in rule.value.spawns) {
if (spawn in spawns)
list.add(spawns[spawn]!!)
else
throw ArenaStartAbortedException("Spawn $spawn is used in spawn rule ${rule.key} but is not defined. Please resolve this issue before trying to start this arena.")
}
list.shuffle()
if (!loadouts.containsKey(rule.value.loadout) && rule.value.loadout != "none")
throw ArenaStartAbortedException("Loadout ${rule.value.loadout} is used in spawn rule ${rule.key} but is not defined. Please resolve this issue before trying to start this arena.")
spawnRules[rule.key] = ProcessedArenaSpawnRule(rule.value, list, loadouts[rule.value.loadout])
}
}
fun processSpawnPools(pools: Map<String, ArenaSpawnPool>) {
for (pool in pools) {
val list = mutableListOf<ProcessedArenaSpawnRule>()
for (rule in pool.value.rules) {
if (rule in spawnRules)
list.add(spawnRules[rule]!!)
else
throw ArenaStartAbortedException("Spawn rule $rule is used in spawn pool ${pool.key} but is not defined. Please resolve this issue before trying to start this arena.")
}
list.shuffle()
spawnPools[pool.key] = ProcessedArenaSpawnPool(pool.value.label, pool.value.times, list)
}
}
fun processCheckpoints(checkpoints: List<ArenaCheckpoint>) {
for (checkpoint in checkpoints) {
val spawnRule = spawnRules[checkpoint.spawnRule]
val spawnPool = spawnPools[checkpoint.spawnPool]
this.checkpoints.add(ProcessedArenaCheckpoint(checkpoint, spawnRule, spawnPool))
}
}
}

View File

@ -0,0 +1,122 @@
package nl.kallestruik.darena.arenas
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.TextColor
import nl.kallestruik.darena.exceptions.TeamNotFoundException
import nl.kallestruik.darena.managers.PointsManager
import nl.kallestruik.darena.managers.TeamManager
import nl.kallestruik.darena.types.Team
import nl.kallestruik.darena.types.arena.ProcessedArenaCheckpoint
import nl.kallestruik.darena.types.arena.ProcessedArenaSpawnPool
import nl.kallestruik.darena.util.Logger
import org.bukkit.entity.Player
import java.util.*
import kotlin.math.min
class PointsSession(
private val teamManager: TeamManager,
private val pointsManager: PointsManager,
private val arena: Arena,
) {
private val checkpointCompletionCount: MutableMap<String, Int> = mutableMapOf()
private var kills: Int = 0
// Team -> {reason -> amount}
private val points: MutableMap<Team, MutableMap<String, Int>> = HashMap()
@Throws(TeamNotFoundException::class)
fun givePointsToPlayer(uuid: UUID, amount: Int, reason: String) {
Logger.trace(PointsSession::class, "givePointsToPlayer(uuid: $uuid, amount: $amount, reason: $reason)")
Logger.debug(PointsSession::class, "Giving $amount points to player with uuid $uuid for reason $reason")
val team = teamManager.getTeamForPlayer(uuid) ?: return
givePointsToTeam(team, amount, reason)
}
fun givePointsToTeam(team: Team, amount: Int, reason: String) {
Logger.trace(PointsSession::class, "givePointsToTeam(team: $team, amount: $amount, reason: $reason)")
Logger.debug(PointsSession::class, "Giving $amount points to team ${team.name} for reason $reason")
val pointsMap = points.getOrDefault(team, mutableMapOf())
pointsMap[reason] = (pointsMap[reason] ?: 0) + amount
points[team] = pointsMap
}
fun giveCheckpointPoints(player: Player, checkpoint: ProcessedArenaCheckpoint) {
Logger.debug(PointsSession::class, "Giving checkpoint points to player ${player.name} for reaching checkpoint ${checkpoint.label}")
val checkpointPointList = arena.config.points.checkpoints[checkpoint.label] ?: return
val checkpointPoints = checkpointPointList[min(checkpointCompletionCount[checkpoint.label] ?: 0, checkpointPointList.size - 1)]
givePointsToPlayer(player.uniqueId, checkpointPoints, "Reaching checkpoint") //TODO: Make these messages configurable
checkpointCompletionCount[checkpoint.label] = (checkpointCompletionCount[checkpoint.label] ?: 0) + 1
}
fun giveKillPoints(killer: Player) {
val killPointList = arena.config.points.kill
val killPoints = killPointList[min(kills, killPointList.size -1 )]
givePointsToPlayer(killer.uniqueId, killPoints, "Kill")//TODO: Make these messages configurable
kills++
}
fun giveLMSPoints() {
var lmsCounter = 0
// First award points to the players still alive
val lmsPoints = arena.config.points.lastManStanding
for (player in arena.session!!.participants) {
givePointsToPlayer(player.uniqueId, lmsPoints[min(lmsCounter, lmsPoints.size - 1)], "Placement") //TODO: Make these messages configurable
lmsCounter++
}
for (uuid in arena.session!!.deaths.reversed()) {
givePointsToPlayer(uuid, lmsPoints[min(lmsCounter, lmsPoints.size - 1)], "Placement") //TODO: Make these messages configurable
lmsCounter++
}
}
fun end() {
giveLMSPoints()
val map = mutableMapOf<Team, Pair<Int, Component>>()
for (entry in points) {
val team = entry.key;
var detailComponent = Component.text("${team.name} - Points\n")
var totalPoints = 0
for (reason in entry.value) {
if (reason.value <= 0)
continue
totalPoints += reason.value
detailComponent = detailComponent.append(Component.text("\n[+${reason.value}] ${reason.key}"))
}
if (totalPoints > 0)
map[team] = Pair(totalPoints, detailComponent)
}
val sortedMap = map.toList().sortedByDescending { (_, v) -> v.first }.toMap()
//TODO: Make these message configurable
arena.sendMessage(Component.text("============[ Summary ]============"))
for (entry in sortedMap) {
// Give points to the team.
pointsManager.givePointsToTeam(entry.key, entry.value.first)
arena.sendMessage(Component.empty()
.append(teamManager.getTeamComponent(entry.key))
.append(Component
.text(": "))
.append(Component
.text(pointsManager.getPointsForTeam(entry.key)))
.append(Component
.text(" [+${entry.value.first}]")
.hoverEvent(entry.value.second.asHoverEvent())
.color(TextColor.color(0x55FFFF)))
)
}
}
fun givePoolPoints(player: Player, spawnPool: ProcessedArenaSpawnPool, checkpointNumber: Int) {
Logger.debug(PointsSession::class, "Giving pool points to player ${player.name} for reaching checkpoint $checkpointNumber in the pool ${spawnPool.label}")
val poolPointList = (arena.config.points.pools[spawnPool.label] ?: return)[checkpointNumber - 1]
val poolPoints = poolPointList[min(arena.session!!.finishedPerSection[checkpointNumber] ?: 0, poolPointList.size - 1)]
givePointsToPlayer(player.uniqueId, poolPoints, "Reaching checkpoint") //TODO: Make these messages configurable
arena.session!!.finishedPerSection[checkpointNumber] = (arena.session!!.finishedPerSection[checkpointNumber] ?: 0) + 1
}
}

View File

@ -3,46 +3,68 @@ package nl.kallestruik.darena.arenas.world
import nl.kallestruik.darena.exceptions.ArenaWorldCreationException
import nl.kallestruik.darena.exceptions.ArenaWorldLoadException
import nl.kallestruik.darena.exceptions.ArenaWorldSaveException
import nl.kallestruik.darena.managers.ConfigManager
import org.bukkit.Bukkit
import org.bukkit.Location
import org.bukkit.World
import org.bukkit.WorldCreator
import org.bukkit.plugin.java.JavaPlugin
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.exists
class ArenaWorld(
val name: String,
val plugin: JavaPlugin
val configManager: ConfigManager,
val plugin: JavaPlugin,
) {
var world: World
lateinit var world: World
val worldFolder = File(Bukkit.getServer().worldContainer, name)
init {
load()
world = Bukkit.createWorld(ArenaWorldCreator(name))
createWorld()
world.isAutoSave = false
}
@Throws(ArenaWorldCreationException::class)
private fun createWorld() {
world = WorldCreator.name(name).generator(ArenaChunkGenerator()).createWorld()
?: throw ArenaWorldCreationException("Exception while creating bukkit world for arena \"$name\".")
}
@Throws(ArenaWorldLoadException::class)
fun reset() {
//TODO: Teleport everyone out.
empty()
Bukkit.unloadWorld(world, false)
load()
createWorld()
}
@Throws(ArenaWorldSaveException::class)
fun save() {
world.save()
try {
val savePath = Path.of(plugin.dataFolder.path, "worlds", name)
Files.walk(savePath).use { walk ->
walk.sorted(Comparator.reverseOrder()).forEach { path ->
Files.delete(path)
}
}
Files.walk(world.worldFolder.toPath()).use { walk ->
if (savePath.exists())
Files.walk(savePath).use { walk ->
walk.sorted(Comparator.reverseOrder()).forEach { path ->
Files.delete(path)
}
}
Files.walk(worldFolder.toPath()).use { walk ->
walk.forEach { source ->
val destination = Path.of(
savePath.toString(), source.toString()
.substring(world.worldFolder.path.length)
.substring(worldFolder.path.length)
)
// Don't copy uid.dat files because they stop bukkit from loading the world.
if (source.endsWith("uid.dat"))
return@forEach
Files.copy(source, destination)
}
@ -56,7 +78,10 @@ class ArenaWorld(
private fun load() {
try {
val loadPath = Path.of(plugin.dataFolder.path, "worlds", name)
Files.walk(world.worldFolder.toPath()).use { walk ->
if (!loadPath.exists())
return
Files.walk(worldFolder.toPath()).use { walk ->
walk.sorted(Comparator.reverseOrder()).forEach { path ->
Files.delete(path)
}
@ -65,9 +90,12 @@ class ArenaWorld(
Files.walk(loadPath).use { walk ->
walk.forEach { source ->
val destination = Path.of(
world.worldFolder.path, source.toString()
worldFolder.path, source.toString()
.substring(loadPath.toString().length)
)
// Don't copy uid.dat files because they stop bukkit from loading the world.
if (source.endsWith("uid.dat"))
return@forEach
Files.copy(source, destination)
}
@ -76,4 +104,11 @@ class ArenaWorld(
throw ArenaWorldLoadException("There was an issue load the world for arena \"$name\"!", e)
}
}
fun empty() {
for (player in world.players) {
player.spigot().respawn()
player.teleport(Location(Bukkit.getWorld(configManager.spawnWorldName), configManager.spawnPos.x + 0.5, configManager.spawnPos.y + 0.5, configManager.spawnPos.z + 0.5))
}
}
}

View File

@ -1,5 +0,0 @@
package nl.kallestruik.darena.arenas.world
data class ArenaWorldConfig(
val name: String,
)

View File

@ -1,16 +0,0 @@
package nl.kallestruik.darena.arenas.world
import org.bukkit.WorldCreator
import org.bukkit.generator.ChunkGenerator
class ArenaWorldCreator(
name: String
): WorldCreator(name) {
private var generator: ChunkGenerator = ArenaChunkGenerator()
override fun generator(): ChunkGenerator {
return generator
}
}

View File

@ -0,0 +1,354 @@
package nl.kallestruik.darena.commands
import co.aikar.commands.BaseCommand
import co.aikar.commands.CommandHelp
import co.aikar.commands.annotation.*
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.event.ClickEvent
import net.kyori.adventure.text.format.NamedTextColor
import net.kyori.adventure.text.format.TextColor
import nl.kallestruik.darena.arenas.Arena
import nl.kallestruik.darena.exceptions.ArenaCreationException
import nl.kallestruik.darena.exceptions.ArenaEditException
import nl.kallestruik.darena.exceptions.TeamExistsException
import nl.kallestruik.darena.managers.*
import nl.kallestruik.darena.types.EditSetting
import nl.kallestruik.darena.types.Reloadable
import nl.kallestruik.darena.types.Team
import nl.kallestruik.darena.util.Logger
import org.bukkit.ChatColor
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
@CommandAlias("darena|arena")
class CommandDArena(
private val arenaManager: ArenaManager,
private val editManager: EditManager,
private val pointsManager: PointsManager,
private val teamManager: TeamManager,
private val gameManager: GameManager,
): BaseCommand() {
@HelpCommand
fun doHelp(sender: CommandSender, help: CommandHelp) {
Logger.trace(CommandDArena::class, "doHelp(sender: $sender, help: $help)")
help.showHelp()
}
@Subcommand("start|s")
@Description("Start an arena")
@CommandPermission("darena.start")
fun onStart(sender: CommandSender, arena: Arena, @Default("-1") loadingTimeOverride: Int, @Default("-1") spectatorTimeOverride: Int, @Default("-1") beforeStartTimeOverride: Int) {
Logger.trace(CommandDArena::class, "onStart(sender: $sender)")
arenaManager.start(arena, loadingTimeOverride, spectatorTimeOverride, beforeStartTimeOverride)
}
@Subcommand("end")
@Description("End the current arena")
@CommandPermission("darena.end")
fun onEnd(sender: CommandSender) {
Logger.trace(CommandDArena::class, "onEnd(sender: $sender)")
arenaManager.end()
}
@Subcommand("list")
@Description("List all arenas")
@CommandPermission("darena.list")
fun onList(sender: CommandSender) {
Logger.trace(CommandDArena::class, "onList(sender: $sender)")
sender.sendMessage(Component.text("============[ Arenas ]============"))
for (gameEntry in gameManager.games) {
sender.sendMessage(Component
.text("${gameEntry.key}:")
.hoverEvent(Component.text(gameEntry.value.description))
)
for (arena in gameEntry.value.arenas) {
sender.sendMessage(Component.empty()
.append(Component
.text(" ${arena.config.name} "))
.append(Component
.text("[start]")
.clickEvent(ClickEvent.runCommand("/darena start ${arena.config.name}"))
.color(NamedTextColor.GREEN))
.append(Component
.text(" "))
.append(Component
.text("[edit]")
.clickEvent(ClickEvent.runCommand("/darena edit load ${arena.config.name}"))
.color(NamedTextColor.AQUA))
)
}
}
}
@Subcommand("leaderboard|top")
@CommandAlias("leaderboard")
@Description("Show the points leaderboard")
@CommandPermission("darena.leaderboard")
fun onLeaderboard(sender: CommandSender) {
Logger.trace(CommandDArena::class, "onLeaderboard(sender: $sender)")
pointsManager.sendLeaderboard(sender)
}
@Subcommand("reload")
@Description("Reload the config")
@CommandPermission("darena.reload")
fun onReload(sender: CommandSender, reloadable: Reloadable) {
Logger.trace(CommandDArena::class, "onReload(sender: $sender, reloadable: $reloadable)")
reloadable.reload()
sender.sendMessage("${ChatColor.YELLOW}[?] Successfully reloaded")
}
@Subcommand("new")
@Description("Create a new arena")
@CommandPermission("darena.new")
fun onNew(sender: CommandSender, name: String) {
Logger.trace(CommandDArena::class, "onNew(sender: $sender, name: $name)")
try {
arenaManager.createArena(name)
sender.sendMessage("${ChatColor.YELLOW}[?] Arena $name created successfully. You can edit it with /darena edit load $name")
} catch (e: ArenaCreationException) {
sender.sendMessage("${ChatColor.RED}[!] ${e.message}}")
}
}
@Subcommand("edit")
inner class EditCommand: BaseCommand() {
@Subcommand("load")
@Description("Load and teleport you to the world of an arena for editing.")
@CommandPermission("darena.edit.load")
fun onLoad(player: Player, arena: Arena) {
Logger.trace(EditCommand::class, "onLoad(player: $player, arena: ${arena.config.name})")
try {
editManager.load(arena)
editManager.tp(player)
} catch (e: ArenaEditException) {
player.sendMessage("${ChatColor.RED}[!] ${e.message}")
}
}
@Subcommand("save")
@Description("Save the world currently being edited.")
@CommandPermission("darena.edit.load")
fun onSave(sender: CommandSender) {
Logger.trace(EditCommand::class, "onSave(sender: $sender)")
try {
editManager.save()
} catch (e: ArenaEditException) {
sender.sendMessage("${ChatColor.RED}[!] ${e.message}")
}
}
@Subcommand("tp")
@Description("Teleport into the world currently being edited.")
@CommandPermission("darena.edit.tp")
fun onTp(player: Player) {
Logger.trace(EditCommand::class, "onTp(player: $player)")
try {
editManager.tp(player)
} catch (e: ArenaEditException) {
player.sendMessage("${ChatColor.RED}[!] ${e.message}")
}
}
@Subcommand("finish")
@Description("Save and stop editing the world currently being edited.")
@CommandPermission("darena.edit.finish")
fun onFinish(sender: CommandSender) {
Logger.trace(EditCommand::class, "onFinish(sender: $sender)")
try {
editManager.finish()
} catch (e: ArenaEditException) {
sender.sendMessage("${ChatColor.RED}[!] ${e.message}")
}
}
@Subcommand("config")
@Description("Load and teleport you to the world of an arena for editing.")
@CommandPermission("darena.edit.load")
fun onConfig(player: Player, path: String) {
Logger.trace(EditCommand::class, "onConfig(player: $player, path: $path)")
player.sendMessage("This feature is currently disabled.")
// try {
// val info = editManager.getConfigValue(path)
// player.sendMessage("------------------")
// player.sendMessage("Name: ${info.name}")
// player.sendMessage("Value: ${info.value}")
// player.sendMessage("Description: ${info.description}")
// player.sendMessage("------------------")
// } catch (e: ArenaEditException) {
// player.sendMessage("${ChatColor.RED}[!] ${e.message}")
// }
}
@Subcommand("visualize")
inner class VisualizeCommand: BaseCommand() {
@Subcommand("spawns")
@Description("Toggle the visibility of all spawns")
@CommandPermission("darena.edit.visualize.spawns")
fun onSpawns(player: Player) {
editManager.toggleSpawnVisibility()
}
@Subcommand("checkpoints")
@Description("Toggle the visibility of all checkpoints")
@CommandPermission("darena.edit.visualize.checkpoints")
fun onCheckpoints(player: Player) {
editManager.toggleCheckpointVisibility()
}
}
@Subcommand("toggle")
inner class ToggleCommand: BaseCommand() {
@Subcommand("spawnplacing")
@Description("Toogle placing spawns with armor stands")
@CommandPermission("darena.edit.toggle.spawnplacing")
fun onSpawnPlacing(player: Player) {
val newValue = editManager.toggleFeature(player, EditSetting.SPAWN_PLACING)
player.sendMessage("Spawn placing is now: $newValue")
}
}
}
@Subcommand("team")
inner class TeamCommand: BaseCommand() {
@Subcommand("create")
@Description("Create a team.")
@CommandPermission("darena.team.create")
fun onCreate(sender: CommandSender, teamName: String) {
Logger.trace(TeamCommand::class, "onCreate(sender: $sender, teamName: $teamName)")
try {
teamManager.createTeam(teamName)
sender.sendMessage("${ChatColor.YELLOW}[?] Team $teamName was successfully created.")
} catch (e: TeamExistsException) {
sender.sendMessage("${ChatColor.RED}[!] ${e.message}")
}
}
@Subcommand("join|add-player")
@Description("Add a player to a team.")
@CommandPermission("darena.team.join")
@CommandCompletion("@team @players")
fun onAddPlayer(sender: CommandSender, team: Team, player: Player) {
Logger.trace(TeamCommand::class, "onAddPlayer(sender: $sender, team: $team, player: $player)")
if (teamManager.getTeamForPlayer(player.uniqueId) != null) {
sender.sendMessage("${ChatColor.RED}[!] Player is already in a team!")
return
}
teamManager.addPlayerToTeam(team, player.uniqueId)
sender.sendMessage("${ChatColor.YELLOW}[?] Player was successfully added to team.")
}
@Subcommand("leave|remove-player")
@Description("Remove a player from a team.")
@CommandPermission("darena.team.leave")
@CommandCompletion("@players")
fun onRemovePlayer(sender: CommandSender, player: Player) {
Logger.trace(TeamCommand::class, "onRemovePlayer(sender: $sender, player: $player)")
if (teamManager.getTeamForPlayer(player.uniqueId) == null) {
sender.sendMessage("${ChatColor.RED}[!] Player is not in a team!")
return
}
teamManager.removePlayerFromTeam(player.uniqueId)
sender.sendMessage("${ChatColor.YELLOW}[?] Player was successfully removed from their team.")
}
@Subcommand("remove")
@Description("Remove a team.")
@CommandPermission("darena.team.remove")
fun onRemove(sender: CommandSender, team: Team) {
Logger.trace(TeamCommand::class, "onRemove(sender: $sender, team: $team)")
teamManager.removeTeam(team)
sender.sendMessage("${ChatColor.YELLOW}[?] Team ${team.name} was successfully removed.")
}
@Subcommand("remove-all")
@Description("Remove all teams.")
@CommandPermission("darena.team.remove-all")
fun onRemoveAll(sender: CommandSender) {
Logger.trace(TeamCommand::class, "onRemoveAll(sender: $sender)")
val count = teamManager.removeAllTeams()
pointsManager.clear()
sender.sendMessage("${ChatColor.YELLOW}[?] Removed $count teams.")
}
@Subcommand("auto")
@Description("Remove all current teams and enable/disable automatic team mode.")
@CommandPermission("darena.team.auto")
fun onAuto(sender: CommandSender, enable: Boolean) {
Logger.trace(TeamCommand::class, "onAuto(sender: $sender, enable: $enable)")
teamManager.autoTeams = enable
teamManager.removeAllTeams()
if (enable)
teamManager.autoTeam()
sender.sendMessage("${ChatColor.YELLOW}[?] Automatic team creation is now ${if (enable) ("${ChatColor.GREEN}enabled") else "${ChatColor.RED}disabled"}")
}
@Subcommand("option")
@Description("Change team options.")
@CommandPermission("darena.team.option")
fun onOption(sender: CommandSender, team: Team, @Values("@teamOptions") option: String, value: String) {
Logger.trace(TeamCommand::class, "onOption(sender: $sender, option: $option, value: $value)")
try {
when (option.lowercase()) {
"color" -> team.color = TextColor.fromCSSHexString(value)!!
"prefix" -> team.prefix = value.replace("\"", "")
else -> sender.sendMessage("${ChatColor.RED}[!] Option $option does not exist!")
}
} catch (e: Exception) {
sender.sendMessage("${ChatColor.RED}[!] Something went wrong while trying to update the value of $option!")
}
}
@Subcommand("list")
@Description("Get a list of all teams and their members.")
@CommandPermission("darena.team.list")
fun onList(sender: CommandSender) {
Logger.trace(TeamCommand::class, "onList(sender: $sender)")
//TODO: Make these configurable
sender.sendMessage("============[ Teams ]============")
for (teamName in teamManager.getAllTeamNames()) {
val team = teamManager.getTeamByName(teamName)
sender.sendMessage(teamManager.getTeamComponent(team))
}
}
}
@Subcommand("points")
inner class PointsCommand: BaseCommand() {
@Subcommand("add")
@Description("Add points to a team.")
@CommandPermission("darena.points.add")
fun onAdd(sender: CommandSender, team: Team, amount: Int) {
Logger.trace(PointsCommand::class, "onAdd(sender: $sender, team: ${team.name}, amount: $amount)")
pointsManager.givePointsToTeam(team, amount)
sender.sendMessage("${ChatColor.YELLOW}[?] Added $amount points to the score of ${team.name}")
}
@Subcommand("remove")
@Description("Remove points from a team.")
@CommandPermission("darena.points.remove")
fun onRemove(sender: CommandSender, team: Team, amount: Int) {
Logger.trace(PointsCommand::class, "onRemove(sender: $sender, team: ${team.name}, amount: $amount)")
pointsManager.givePointsToTeam(team, -amount)
sender.sendMessage("${ChatColor.YELLOW}[?] Removed $amount points from the score of ${team.name}")
}
@Subcommand("reset")
@Description("Reset everyones score.")
@CommandPermission("darena.points.reset")
fun onReset(sender: CommandSender) {
Logger.trace(PointsCommand::class, "onReset(sender: $sender)")
pointsManager.clear()
sender.sendMessage("${ChatColor.YELLOW}[?] Reset everyone's score")
}
}
}

View File

@ -0,0 +1,26 @@
package nl.kallestruik.darena.events
import nl.kallestruik.darena.arenas.Arena
import nl.kallestruik.darena.types.arena.ProcessedArenaCheckpoint
import org.bukkit.entity.Player
import org.bukkit.event.HandlerList
import org.bukkit.event.player.PlayerEvent
class CheckpointReachedEvent(
player: Player,
val arena: Arena,
val checkpoint: ProcessedArenaCheckpoint
): PlayerEvent(player) {
override fun getHandlers(): HandlerList {
return HANDLERS_LIST;
}
companion object {
private val HANDLERS_LIST = HandlerList()
@JvmStatic
fun getHandlerList(): HandlerList {
return HANDLERS_LIST
}
}
}

View File

@ -0,0 +1,6 @@
package nl.kallestruik.darena.exceptions
class ArenaCreationException(
message: String? = null,
cause: Throwable? = null
): Exception(message, cause)

View File

@ -0,0 +1,6 @@
package nl.kallestruik.darena.exceptions
class ArenaEditException(
message: String? = null,
cause: Throwable? = null
): Exception(message, cause)

View File

@ -0,0 +1,6 @@
package nl.kallestruik.darena.exceptions
class ArenaEndAbortedException(
message: String? = null,
cause: Throwable? = null
): Exception(message, cause)

View File

@ -0,0 +1,6 @@
package nl.kallestruik.darena.exceptions
class ArenaStartAbortedException(
message: String? = null,
cause: Throwable? = null
): Exception(message, cause)

View File

@ -0,0 +1,6 @@
package nl.kallestruik.darena.exceptions
class PotionEffectNotFoundException(
message: String? = null,
cause: Throwable? = null
): Exception(message, cause)

View File

@ -0,0 +1,21 @@
package nl.kallestruik.darena.listeners
import nl.kallestruik.darena.managers.ArenaManager
import nl.kallestruik.darena.types.ArenaFeature
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.block.BlockBreakEvent
class BlockBreakListener(
val arenaManager: ArenaManager
): Listener {
@EventHandler
fun onBlockBreak(event: BlockBreakEvent) {
if (!arenaManager.shouldProcessEvent(ArenaFeature.NO_TILE_DROPS, event.player)) {
return
}
event.isCancelled = true
}
}

View File

@ -0,0 +1,24 @@
package nl.kallestruik.darena.listeners
import nl.kallestruik.darena.managers.ArenaManager
import nl.kallestruik.darena.types.ArenaFeature
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerMoveEvent
class BorderListener(
private val arenaManager: ArenaManager
): Listener {
@EventHandler
fun onMove(event: PlayerMoveEvent) {
if (!arenaManager.shouldProcessEvent(ArenaFeature.HORIZONTAL_BORDERS, event.player))
return
if (event.to.y < arenaManager.currentArena!!.session!!.borderBottom.height)
event.player.health = 0.0
if (event.to.y > arenaManager.currentArena!!.session!!.borderTop.height)
event.player.health = 0.0
}
}

View File

@ -0,0 +1,33 @@
package nl.kallestruik.darena.listeners
import nl.kallestruik.darena.managers.ArenaManager
import nl.kallestruik.darena.types.ArenaFeature
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.block.BlockBreakEvent
import org.bukkit.event.block.BlockPlaceEvent
class BuildRestrictionListener(
private val arenaManager: ArenaManager,
): Listener {
@EventHandler
fun onBlockPlace(event: BlockPlaceEvent) {
if (!arenaManager.shouldProcessEvent(ArenaFeature.RESTRICT_BUILDING, event.player))
return
if (!arenaManager.currentArena!!.config.placeAllowed.isAllowed(event.blockPlaced.type)) {
event.isCancelled = true;
}
}
@EventHandler
fun onBlockBreak(event: BlockBreakEvent) {
if (!arenaManager.shouldProcessEvent(ArenaFeature.RESTRICT_BUILDING, event.player))
return
if (!arenaManager.currentArena!!.config.breakAllowed.isAllowed(event.block.type)) {
event.isCancelled = true;
}
}
}

View File

@ -0,0 +1,52 @@
package nl.kallestruik.darena.listeners
import nl.kallestruik.darena.events.CheckpointReachedEvent
import nl.kallestruik.darena.managers.ArenaManager
import nl.kallestruik.darena.types.ArenaFeature
import nl.kallestruik.darena.util.ArenaUtil
import org.bukkit.Bukkit
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerMoveEvent
class CheckpointListener(
private val arenaManager: ArenaManager,
): Listener {
@EventHandler
fun onMove(event: PlayerMoveEvent) {
if (!arenaManager.shouldProcessEvent(ArenaFeature.CHECKPOINTS, event.player))
return
val arena = arenaManager.currentArena!!
val uuid = event.player.uniqueId
val reachedCheckpoints = arena.session!!.reachedCheckpoints[uuid] ?: mutableSetOf()
for (checkpoint in arena.session!!.checkpoints) {
if (reachedCheckpoints.contains(checkpoint.label))
continue
if (!checkpoint.isInside(event.to))
continue
reachedCheckpoints.add(checkpoint.label)
arena.session!!.lastCheckpoint[uuid] = checkpoint
arena.session!!.reachedCheckpoints[uuid] = reachedCheckpoints
arena.pointsSession!!.giveCheckpointPoints(event.player, checkpoint)
// Call event to let other handlers do further actions with it.
Bukkit.getPluginManager().callEvent(CheckpointReachedEvent(event.player, arena, checkpoint))
if (checkpoint.movePlayer) {
checkpoint.resolveSpawnRule()
if (checkpoint.spawnRule != null)
checkpoint.spawnRule!!.spawnPlayer(event.player, arena.world)
}
if (checkpoint.isFinish)
ArenaUtil.finishPlayer(event.player, arena, arenaManager)
break
}
}
}

View File

@ -0,0 +1,26 @@
package nl.kallestruik.darena.listeners
import net.kyori.adventure.text.Component
import nl.kallestruik.darena.events.CheckpointReachedEvent
import nl.kallestruik.darena.managers.ArenaManager
import nl.kallestruik.darena.types.ArenaFeature
import nl.kallestruik.darena.util.ArenaUtil
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
class CheckpointMessageListener(
val arenaManager: ArenaManager
): Listener {
@EventHandler
fun onCheckpointReached(event: CheckpointReachedEvent) {
if (!arenaManager.shouldProcessEvent(ArenaFeature.CHECKPOINT_MESSAGE, event.player)) {
return
}
val timeSinceStart = System.currentTimeMillis() - event.arena.session!!.startTime
//TODO: Make this message configurable.
event.arena.sendMessage(Component.text("${event.player.name} has reached a checkpoint! [${ArenaUtil.formatTimeMillis(timeSinceStart)}]"))
}
}

View File

@ -0,0 +1,24 @@
package nl.kallestruik.darena.listeners
import nl.kallestruik.darena.events.CheckpointReachedEvent
import nl.kallestruik.darena.managers.ArenaManager
import nl.kallestruik.darena.types.ArenaFeature
import org.bukkit.Sound
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
class CheckpointSoundListener(
val arenaManager: ArenaManager
): Listener {
@EventHandler
fun onCheckpointReached(event: CheckpointReachedEvent) {
if (!arenaManager.shouldProcessEvent(ArenaFeature.CHECKPOINT_SOUND, event.player)) {
return
}
event.player.let {
it.playSound(it.location, Sound.BLOCK_NOTE_BLOCK_BELL, 1.0F, 1.0F)
}
}
}

View File

@ -0,0 +1,168 @@
package nl.kallestruik.darena.listeners
import net.kyori.adventure.text.Component
import nl.kallestruik.darena.managers.EditManager
import nl.kallestruik.darena.types.EditSetting
import nl.kallestruik.darena.types.arena.ArenaSpawn
import nl.kallestruik.dlib.gui.chestGUI
import nl.kallestruik.dlib.gui.textInputDialog
import org.bukkit.Material
import org.bukkit.entity.ArmorStand
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.event.entity.EntityPlaceEvent
import org.bukkit.event.player.PlayerInteractAtEntityEvent
import org.bukkit.plugin.Plugin
class EditListener(
val editManager: EditManager,
val plugin: Plugin
): Listener {
@EventHandler
fun onEntityPlace(event: EntityPlaceEvent) {
if (!editManager.shouldProcessEvent(event.player, EditSetting.SPAWN_PLACING))
return
val entity = event.entity
if (entity !is ArmorStand)
return
val loc = entity.location
val spawn = ArenaSpawn(loc.x, loc.y, loc.z, loc.yaw, loc.pitch)
textInputDialog(event.player!!, plugin) {
prompt = Component.text("Spawn name?")
item {
material = Material.PAPER
amount = 1
}
onFinish { name ->
if (editManager.addSpawn(name, spawn)) {
event.player?.sendMessage("Added spawn $name")
} else {
event.player?.sendMessage("Adding spawn failed. Is the name already in use?")
}
event.entity.remove()
}
onCancel {
event.player?.sendMessage("Canceled adding spawn")
event.entity.remove()
}
}
}
@EventHandler
fun onEntityDamage(event: EntityDamageByEntityEvent) {
if (event.damager !is Player)
return
val player = event.damager as Player
if (!editManager.shouldProcessEvent(player, EditSetting.ALWAYS)) {
return
}
if (event.entity is ArmorStand) {
entityDamageArmorStand(player, event)
}
}
private fun entityDamageArmorStand(player: Player, event: EntityDamageByEntityEvent) {
if (!event.entity.hasMetadata("ArenaSpawn"))
return
// This cast should be safe since we should be the only ones editing this.
val spawnName = event.entity.getMetadata("ArenaSpawn")[0].value() as String
event.isCancelled = true
editManager.removeSpawnGUI(player, spawnName)
}
@EventHandler
fun onPlayerInteractAtEntity(event: PlayerInteractAtEntityEvent) {
if (!editManager.shouldProcessEvent(event.player, EditSetting.ALWAYS)) {
return
}
if (event.rightClicked is ArmorStand) {
playerInteractArmorstand(event.player, event)
}
}
private fun playerInteractArmorstand(player: Player, event: PlayerInteractAtEntityEvent) {
if (!event.rightClicked.hasMetadata("ArenaSpawn"))
return
// This cast should be safe since we should be the only ones editing this.
val spawnName = event.rightClicked.getMetadata("ArenaSpawn")[0].value() as String
val spawn = editManager.getSpawn(spawnName)
event.isCancelled = true
chestGUI(player, plugin) {
title = Component.text("Options for: $spawnName")
rows = 3
//TODO: Any more actions to add to a spawn?
// 0 1 2 3 4 5 6 7 8
// 0 b b b b I b b b b
// 1 b x R x x x D x b
// 2 b x x x x x x x b
// 3 b b b b b b b b b
border {
material = Material.LIGHT_BLUE_STAINED_GLASS_PANE
}
entry {
x = 4
y = 0
name = Component.text("Information")
material = Material.PAPER
lore(Component.empty())
lore(Component.text("x: ${spawn.x}"))
lore(Component.text("y: ${spawn.y}"))
lore(Component.text("z: ${spawn.z}"))
lore(Component.text("yaw: ${spawn.yaw}"))
lore(Component.text("pitch: ${spawn.pitch}"))
}
entry {
x = 2
y = 1
name = Component.text("Add to rule")
material = Material.LIME_DYE
onLeftClick {
editManager.addSpawnToRuleGUI(player, spawnName)
}
}
entry {
x = 6
y = 1
name = Component.text("Delete")
material = Material.BARRIER
onLeftClick {
editManager.removeSpawnGUI(player, spawnName)
}
}
}
}
}

View File

@ -0,0 +1,27 @@
package nl.kallestruik.darena.listeners
import nl.kallestruik.darena.managers.ArenaManager
import nl.kallestruik.darena.types.ArenaFeature
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.entity.EntityDamageEvent
class FallListener(
private val arenaManager: ArenaManager
): Listener {
@EventHandler
fun onDamage(event: EntityDamageEvent) {
if (event.entity !is Player)
return
if (!arenaManager.shouldProcessEvent(ArenaFeature.NO_FALL, event.entity as Player))
return
if (event.cause == EntityDamageEvent.DamageCause.FALL
|| event.cause == EntityDamageEvent.DamageCause.FLY_INTO_WALL
)
event.isCancelled = true
}
}

View File

@ -0,0 +1,80 @@
package nl.kallestruik.darena.listeners
import nl.kallestruik.darena.managers.ArenaManager
import nl.kallestruik.darena.managers.ConfigManager
import nl.kallestruik.darena.types.ArenaFeature
import org.bukkit.Material
import org.bukkit.entity.Fireball
import org.bukkit.entity.Player
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.entity.EntityExplodeEvent
import org.bukkit.event.player.PlayerInteractEvent
class FireballListener(
private val arenaManager: ArenaManager,
private val configManager: ConfigManager,
): Listener {
@EventHandler
fun onUse(event: PlayerInteractEvent) {
if (!arenaManager.shouldProcessEvent(ArenaFeature.ITEM_FIREBALL, event.player))
return
if (event.action != Action.RIGHT_CLICK_AIR && event.action != Action.RIGHT_CLICK_BLOCK)
return
if (!event.hasItem() || event.item!!.type != Material.FIRE_CHARGE)
return
if (event.player.getCooldown(Material.FIRE_CHARGE) > 0) return
val fireball = event.player.launchProjectile(Fireball::class.java)
fireball.velocity = fireball.velocity.multiply(3)
fireball.setIsIncendiary(false)
fireball.yield = 4f
event.player.setCooldown(Material.FIRE_CHARGE, configManager.cooldownFireball)
event.isCancelled = true
}
@EventHandler
fun onEntityExplode(event: EntityExplodeEvent) {
if (!arenaManager.shouldProcessEvent(ArenaFeature.ITEM_FIREBALL, null))
return
for (block in event.blockList()) {
block.type = Material.AIR
}
}
@EventHandler
fun onDamage(event: EntityDamageByEntityEvent) {
if (event.entity !is Player)
return
if (!arenaManager.shouldProcessEvent(ArenaFeature.ITEM_FIREBALL, event.entity as Player))
return
if (event.damager !is Fireball)
return
event.damage = 0.0
}
@EventHandler
fun onAttack(event: EntityDamageByEntityEvent) {
if (!arenaManager.shouldProcessEvent(ArenaFeature.ITEM_FIREBALL, null))
return
if (event.entity !is Fireball)
return
event.isCancelled = true
}
}

View File

@ -0,0 +1,50 @@
package nl.kallestruik.darena.listeners
import nl.kallestruik.darena.managers.ArenaManager
import nl.kallestruik.darena.types.ArenaFeature
import org.bukkit.Location
import org.bukkit.Material
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerFishEvent
class GrapplingHookListener(
val arenaManager: ArenaManager,
): Listener {
@EventHandler
fun onFish(event: PlayerFishEvent) {
if (!arenaManager.shouldProcessEvent(ArenaFeature.ITEM_GRAPPLING_HOOK, event.player)) {
return
}
if (!isNearBlock(event.hook.location))
return
val direction = event.hook.location.toVector().subtract(event.player.eyeLocation.toVector()).normalize()
event.player.velocity = event.player.velocity.add(direction.multiply(3))
}
fun isNearBlock(location: Location): Boolean {
if (location.subtract(0.0, 1.0, 0.0).block.type != Material.AIR)
return true
if (location.subtract(1.0, 0.0, 0.0).block.type != Material.AIR)
return true
if (location.subtract(0.0, 0.0, 1.0).block.type != Material.AIR)
return true
if (location.add(0.0, 1.0, 0.0).block.type != Material.AIR)
return true
if (location.add(1.0, 0.0, 0.0).block.type != Material.AIR)
return true
if (location.add(0.0, 0.0, 1.0).block.type != Material.AIR)
return true
return false
}
}

View File

@ -0,0 +1,99 @@
package nl.kallestruik.darena.listeners
import net.kyori.adventure.text.Component
import nl.kallestruik.darena.managers.ArenaManager
import nl.kallestruik.darena.types.ArenaFeature
import nl.kallestruik.darena.util.ArenaUtil
import org.bukkit.Bukkit
import org.bukkit.attribute.Attribute
import org.bukkit.entity.Entity
import org.bukkit.entity.Player
import org.bukkit.event.Cancellable
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.entity.EntityDamageByEntityEvent
import org.bukkit.event.entity.EntityDamageEvent
import org.bukkit.event.entity.PlayerDeathEvent
import org.bukkit.event.entity.ProjectileHitEvent
class KillListener(
private val arenaManager: ArenaManager
): Listener {
@EventHandler
fun onDeath(event: PlayerDeathEvent) {
if (!arenaManager.shouldProcessEvent(ArenaFeature.ALWAYS, event.entity))
return
event.isCancelled = true
event.entity.health = event.entity.getAttribute(Attribute.GENERIC_MAX_HEALTH)?.value ?: 20.0
arenaManager.currentArena!!.session!!.deaths.add(event.entity.uniqueId)
val killerUUID = arenaManager.currentArena!!.session!!.lastDamage[event.entity.uniqueId]
val killer = if (killerUUID != null)
Bukkit.getPlayer(killerUUID)
else
null
if (killer != null)
arenaManager.currentArena!!.pointsSession!!.giveKillPoints(killer)
if (arenaManager.shouldProcessEvent(ArenaFeature.DEATH_MESSAGE, event.entity)) {
if (killer == null)
arenaManager.currentArena!!.sendMessage(
Component.text("${event.entity.name} died")
)
else
arenaManager.currentArena!!.sendMessage(
Component.text("${event.entity.name} was killed by ${killer.name}")
)
}
if (arenaManager.shouldProcessEvent(ArenaFeature.RESPAWN, event.entity)) {
arenaManager.currentArena!!.respawn(event.entity)
} else {
if (arenaManager.shouldProcessEvent(ArenaFeature.ALWAYS, event.entity))
arenaManager.currentArena!!.makeSpectator(event.entity)
}
if (arenaManager.currentArena!!.session!!.participants.size <= arenaManager.currentArena!!.config.endConditions.playersLeft) {
arenaManager.end()
}
}
@EventHandler
fun onDamage(event: EntityDamageByEntityEvent) {
checkAndUpdateDamager(event.entity, event.damager, event)
}
@EventHandler
fun onProjectileHit(event: ProjectileHitEvent) {
checkAndUpdateDamager(event.hitEntity, event.entity, event)
}
private fun checkAndUpdateDamager(hit: Entity?, hitter: Entity, event: Cancellable) {
if (hit !is Player)
return
val damager = ArenaUtil.resolveDamager(hitter) ?: return
if (!arenaManager.shouldProcessEvent(ArenaFeature.PVP, hit)) {
if (arenaManager.shouldProcessEvent(ArenaFeature.ALWAYS, hit)) {
if (event is EntityDamageByEntityEvent)
if (event.cause != EntityDamageEvent.DamageCause.ENTITY_EXPLOSION
&& event.cause != EntityDamageEvent.DamageCause.BLOCK_EXPLOSION
) {
event.isCancelled = true
}
}
return
}
if (hit.uniqueId == damager.uniqueId)
return
arenaManager.currentArena!!.session!!.lastDamage[hit.uniqueId] = damager.uniqueId
}
}

View File

@ -0,0 +1,35 @@
package nl.kallestruik.darena.listeners
import nl.kallestruik.darena.events.CheckpointReachedEvent
import nl.kallestruik.darena.managers.ArenaManager
import nl.kallestruik.darena.util.ArenaUtil
import nl.kallestruik.darena.util.Logger
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
class SectionListener(
val arenaManager: ArenaManager,
): Listener {
@EventHandler
fun onCheckpointReached(event: CheckpointReachedEvent) {
val checkpoint = event.checkpoint
val arena = event.arena
val uuid = event.player.uniqueId
// Increment finished section counter for player.
arena.session!!.finishedSectionsByPlayer[uuid] = arena.session!!.finishedSectionsByPlayer.getOrDefault(uuid, 0) + 1
if (checkpoint.spawnPool != null) {
val finishedSectionsByPlayer = arena.session!!.finishedSectionsByPlayer[uuid]!!
arena.pointsSession!!.givePoolPoints(event.player, checkpoint.spawnPool, finishedSectionsByPlayer)
Logger.debug(CheckpointListener::class, "finished count: $finishedSectionsByPlayer")
Logger.debug(CheckpointListener::class, "Times: ${checkpoint.spawnPool.times}")
if (finishedSectionsByPlayer >= checkpoint.spawnPool.times) {
ArenaUtil.finishPlayer(event.player, arena, arenaManager)
return
}
}
}
}

View File

@ -0,0 +1,28 @@
package nl.kallestruik.darena.listeners
import nl.kallestruik.darena.managers.TeamManager
import nl.kallestruik.darena.util.Logger
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerJoinEvent
class TeamListener(
private val teamManager: TeamManager
): Listener {
@EventHandler
fun onPlayerJoined(event: PlayerJoinEvent) {
var team = teamManager.getTeamForPlayer(event.player.uniqueId)
if (team != null) {
team.mcTeam.addEntry(event.player.name)
return
}
if (!teamManager.autoTeams)
return
Logger.info(TeamListener::class, "Creating automatic team for player: ${event.player.name}")
team = teamManager.createTeam(event.player.name)
teamManager.addPlayerToTeam(team, event.player.uniqueId)
}
}

View File

@ -1,27 +1,144 @@
package nl.kallestruik.darena.managers
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import nl.kallestruik.darena.arenas.Arena
import nl.kallestruik.darena.arenas.ArenaConfig
import nl.kallestruik.darena.util.ArenaUtil
import nl.kallestruik.darena.exceptions.ArenaCreationException
import nl.kallestruik.darena.exceptions.ArenaEndAbortedException
import nl.kallestruik.darena.exceptions.ArenaStartAbortedException
import nl.kallestruik.darena.types.ArenaFeature
import nl.kallestruik.darena.types.Reloadable
import nl.kallestruik.darena.util.Logger
import nl.kallestruik.dlib.config.ConfigHelper.toConfigSchema
import org.bukkit.entity.Player
import org.bukkit.plugin.java.JavaPlugin
import java.io.File
import java.nio.file.Files
class ArenaManager(
private val arenaUtil: ArenaUtil,
private val teamManager: TeamManager,
private val pointsManager: PointsManager,
private val configManager: ConfigManager,
private val plugin: JavaPlugin,
) {
private val arenas: MutableList<Arena> = ArrayList()
private val arenaFolder: File,
schemaFile: File,
): Reloadable {
lateinit var editManager: EditManager;
private val arenas: MutableMap<String, Arena> = mutableMapOf()
var currentArena: Arena? = null
fun loadArenas(arenaFolder: File) {
override fun reload() {
arenas.clear()
loadArenas()
}
init {
schemaFile.parentFile.mkdirs()
schemaFile.writeText(Json.encodeToString(ArenaConfig.serializer().descriptor.toConfigSchema(listOf())))
}
fun loadArenas() {
Logger.trace(ArenaManager::class, "loadArenas()")
arenaFolder.mkdirs()
Files.walk(arenaFolder.toPath()).use { walk ->
walk.forEach { path ->
arenas.add(Arena(
ArenaConfig.load(path.toFile()),
arenaUtil,
plugin)
if (Files.isDirectory(path))
return@forEach
if (path.toString().endsWith(".yml")) {
Logger.info(ArenaManager::class, "Skipping legacy config at $path")
return@forEach
}
Logger.info(ArenaManager::class, "Loading arena from: $path")
val config = ArenaConfig.load(path.toFile())
arenas[config.name] = Arena(
config,
plugin,
configManager,
this,
teamManager,
pointsManager,
)
}
}
}
@Throws(ArenaStartAbortedException::class)
fun start(arena: Arena, loadingTimeOverride: Int, spectatorTimeOverride: Int, beforeStartTimeOverride: Int) {
Logger.trace(ArenaManager::class, "start(arena: ${arena.config.name}, loadingTimeOverride: $loadingTimeOverride, spectatorTimeOverride: $spectatorTimeOverride, beforeStartTimeOverride: $beforeStartTimeOverride)")
if (currentArena != null)
throw ArenaStartAbortedException("There is currently another arena in progress. Please end that one first.")
if (editManager.isEditing())
throw ArenaStartAbortedException("There is currently an arena being edited. Please finish that first.")
// Try to start the arena. If something goes wrong clean it up and throw the exception again so the user can be informed.
arena.start(loadingTimeOverride, spectatorTimeOverride, beforeStartTimeOverride)
currentArena = arena
}
@Throws(ArenaEndAbortedException::class)
fun end() {
Logger.trace(ArenaManager::class, "end()")
if (currentArena == null)
throw ArenaEndAbortedException("There is currently no arena in progress.")
currentArena!!.end()
currentArena = null
}
@Throws(ArenaCreationException::class)
fun createArena(name: String) {
if (arenas.contains(name))
throw ArenaCreationException("Arena with name $name already exists!")
val config = ArenaConfig(name = name)
config.file = File(arenaFolder, "$name.json")
arenas[config.name] = Arena(
config,
plugin,
configManager,
this,
teamManager,
pointsManager,
)
config.save(false)
}
fun saveConfig(arena: Arena) {
arena.config.save(false)
}
fun getArena(name: String): Arena? {
Logger.trace(ArenaManager::class, "getArena(name: $name)")
return arenas[name]
}
fun getAllArenaNames(): Collection<String> {
Logger.trace(ArenaManager::class, "getAllArenaNames()")
return arenas.keys
}
fun shouldProcessEvent(feature: ArenaFeature, player: Player?): Boolean {
if (currentArena == null)
return false
if (!currentArena!!.session!!.isInProgress)
return false
if (feature != ArenaFeature.ALWAYS && !currentArena!!.hasFeature(feature))
return false
if ((player != null && !currentArena!!.session!!.participants.contains(player)))
return false
return true
}
}

View File

@ -1,12 +1,32 @@
package nl.kallestruik.darena.managers
import nl.kallestruik.darena.types.Reloadable
import nl.kallestruik.darena.types.arena.ArenaLocation
import nl.kallestruik.darena.util.ConfigHelper
import java.io.File
class ConfigManager(
val configFile: File
) {
val configFile: File,
): Reloadable {
var spawnPos: ArenaLocation = ArenaLocation(0, 100, 0)
var spawnWorldName = "world"
var cooldownFireball = 30
override fun reload() {
load()
}
fun load() {
//TODO: Load config
val config = ConfigHelper.getOrCreateConfig(configFile, "template/config.yml")
if (config.isConfigurationSection("spawn-pos"))
spawnPos = ArenaLocation.load(config.getConfigurationSection("spawn-pos")!!)
if (config.contains("spawn-world"))
spawnWorldName = config.getString("spawn-world")!!
if (config.contains("cooldown-fireball"))
cooldownFireball = config.getInt("cooldown-fireball")
}
}

View File

@ -0,0 +1,311 @@
package nl.kallestruik.darena.managers
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import nl.kallestruik.darena.arenas.Arena
import nl.kallestruik.darena.arenas.world.ArenaWorld
import nl.kallestruik.darena.exceptions.ArenaEditException
import nl.kallestruik.darena.types.EditSetting
import nl.kallestruik.darena.types.EditVisualization
import nl.kallestruik.darena.types.arena.ArenaSpawn
import nl.kallestruik.darena.types.arena.ArenaSpawnRule
import nl.kallestruik.darena.util.Logger
import nl.kallestruik.darena.util.RenderUtil
import nl.kallestruik.dlib.config.ConfigEntryInfo
import nl.kallestruik.dlib.config.ConfigHelper
import nl.kallestruik.dlib.gui.confirmationDialog
import nl.kallestruik.dlib.gui.textInputDialog
import org.bukkit.Color
import org.bukkit.GameMode
import org.bukkit.Location
import org.bukkit.Material
import org.bukkit.entity.EntityType
import org.bukkit.entity.Player
import org.bukkit.metadata.FixedMetadataValue
import org.bukkit.plugin.Plugin
import org.bukkit.scheduler.BukkitRunnable
import org.bukkit.scheduler.BukkitTask
import java.util.*
class EditManager(
private val arenaManager: ArenaManager,
private val plugin: Plugin
) {
private var currentArena: Arena? = null
private var currentWorld: ArenaWorld? = null
private val currentSettings: MutableMap<UUID, MutableMap<EditSetting, Boolean>> = mutableMapOf()
private val currentVisualizations: MutableMap<EditVisualization, Boolean> = mutableMapOf()
private var checkpointVisualizationTask: BukkitTask? = null
@Throws(ArenaEditException::class)
fun load(arena: Arena) {
Logger.trace(EditManager::class, "load(arena: ${arena.config.name})")
if (isEditing())
throw ArenaEditException("Cant start editing a world when another one is already loaded!")
if (arenaManager.currentArena != null)
throw ArenaEditException("Cant start editing a world when there is an arena in progress!")
arena.world.reset()
currentArena = arena
currentWorld = arena.world
}
@Throws(ArenaEditException::class)
fun save() {
Logger.trace(EditManager::class, "save()")
if (!isEditing())
throw ArenaEditException("Cant save a world if you aren't editing one!")
if (isVisualizationEnabled(EditVisualization.SPAWNS)) {
removeSpawnVisualizers()
}
currentWorld!!.save()
saveConfig()
if (isVisualizationEnabled(EditVisualization.SPAWNS)) {
addSpawnVisualizers()
}
}
@Throws(ArenaEditException::class)
fun tp(player: Player) {
Logger.trace(EditManager::class, "tp(player: ${player.name})")
if (!isEditing())
throw ArenaEditException("Cant teleport to a world if none is being edited!")
player.teleport(Location(currentWorld!!.world, 0.5, 100.0, 0.5))
player.gameMode = GameMode.CREATIVE
}
fun finish() {
Logger.trace(EditManager::class, "finish()")
if (!isEditing())
throw ArenaEditException("Cant stop editing what is not being edited!")
save()
currentWorld!!.empty()
currentArena = null
currentWorld = null
currentSettings.clear()
checkpointVisualizationTask?.cancel()
}
fun isEditing(): Boolean {
return currentWorld != null
}
// fun getConfigValue(path: String): ConfigEntryInfo {
// Logger.trace(EditManager::class, "getConfigValue(path: $path)")
//
// if (!isEditing())
// throw ArenaEditException("You arent editing anything!")
//
// return ConfigHelper.getInfoByPath(path, currentArena!!.config)
// }
fun toggleFeature(player: Player, setting: EditSetting): Boolean {
val settingsMap = currentSettings.getOrPut(player.uniqueId, ::mutableMapOf)
val newValue = !(settingsMap[setting] ?: false)
settingsMap[setting] = newValue
return newValue
}
fun saveConfig() {
if (!isEditing()) {
throw ArenaEditException("You cant save something that has not been changed!")
}
arenaManager.saveConfig(currentArena!!)
}
fun shouldProcessEvent(player: Player?, setting: EditSetting): Boolean {
if (currentArena == null)
return false
if (!isEditing())
return false
if (player == null)
return false
if (setting != EditSetting.ALWAYS && !playerHasFeatureEnabled(player, setting))
return false
return true
}
private fun playerHasFeatureEnabled(player: Player, setting: EditSetting): Boolean {
return currentSettings[player.uniqueId]?.get(setting) ?: false
}
fun addSpawn(name: String, spawn: ArenaSpawn): Boolean {
Logger.trace(EditManager::class, "finishSpawn(name: $name, spawn: $spawn)")
if (!isEditing())
return false
if (name in currentArena!!.config.spawns)
return false
currentArena!!.config.spawns[name] = spawn
refreshSpawnVisualizer()
return true
}
fun isVisualizationEnabled(visualization: EditVisualization): Boolean {
return currentVisualizations[visualization] ?: false
}
fun toggleSpawnVisibility() {
if (!isEditing()) {
throw ArenaEditException("You cant visualize what isn't there!")
}
val newValue = !isVisualizationEnabled(EditVisualization.SPAWNS)
currentVisualizations[EditVisualization.SPAWNS] = newValue
if (newValue) {
addSpawnVisualizers()
} else {
removeSpawnVisualizers()
}
}
private fun addSpawnVisualizers() {
val world = currentWorld!!.world
for (entry in currentArena!!.config.spawns) {
val entity = world.spawnEntity(entry.value.toLocation(world), EntityType.ARMOR_STAND)
entity.setGravity(false)
entity.isCustomNameVisible = true
entity.customName(Component.text("SPAWN: ${entry.key}"))
entity.setMetadata("ArenaSpawn", FixedMetadataValue(plugin, entry.key))
}
}
private fun removeSpawnVisualizers() {
for (entity in currentWorld!!.world.entities) {
if (!entity.hasMetadata("ArenaSpawn"))
continue
entity.remove()
}
}
fun refreshSpawnVisualizer() {
if (!isEditing())
return
if (!isVisualizationEnabled(EditVisualization.SPAWNS))
return
removeSpawnVisualizers()
addSpawnVisualizers()
}
fun removeSpawn(spawnName: String) {
if (!isEditing()) {
throw ArenaEditException("Cant remove a spawn if you arent editing an arena!")
}
currentArena!!.config.spawns.remove(spawnName)
refreshSpawnVisualizer()
}
fun toggleCheckpointVisibility() {
if (!isEditing()) {
throw ArenaEditException("You cant visualize what isn't there!")
}
val newValue = !isVisualizationEnabled(EditVisualization.CHECKPOINTS)
currentVisualizations[EditVisualization.CHECKPOINTS] = newValue
if (newValue) {
checkpointVisualizationTask = CheckpointVisualizationRunnable(currentArena!!).runTaskTimer(plugin, 0, 5)
} else {
checkpointVisualizationTask?.cancel()
}
}
fun addSpawnToRuleGUI(player: Player, spawnName: String) {
if (!isEditing())
throw ArenaEditException("You arent editing an arena!")
if (spawnName !in currentArena!!.config.spawns)
throw ArenaEditException("That spawn does not exist!")
textInputDialog(player, plugin) {
prompt = Component.text("Rule name")
item {
material = Material.LIME_DYE
}
onCancel {
player.sendMessage(Component.text("Canceled adding $spawnName to a rule."))
}
onFinish { name ->
val rule: ArenaSpawnRule
if (name in currentArena!!.config.spawnRules) {
rule = currentArena!!.config.spawnRules[name]!!
player.sendMessage(Component.text("Added $spawnName to rule $name."))
} else {
rule = ArenaSpawnRule()
player.sendMessage(Component.text("Created new rule $name and added $spawnName to it."))
}
rule.spawns.add(spawnName)
currentArena!!.config.spawnRules[name] = rule
}
}
}
fun getSpawn(spawnName: String): ArenaSpawn {
if (!isEditing())
throw ArenaEditException("You cant get a spawn when you arent editing an arena!")
return currentArena!!.config.spawns[spawnName]
?: throw ArenaEditException("That spawn does not exist!")
}
fun removeSpawnGUI(player: Player, spawnName: String) {
confirmationDialog(player, plugin) {
title = Component.text("Remove spawn?")
description = Component
.text("Are you sure that you want to remove this spawn?")
.color(NamedTextColor.RED)
onConfirm {
removeSpawn(spawnName)
refreshSpawnVisualizer()
player.sendMessage(Component.text("Spawn $spawnName removed!"))
}
}
}
private class CheckpointVisualizationRunnable(
val arena: Arena,
): BukkitRunnable() {
override fun run() {
for (checkpoint in arena.config.checkpoints) {
val lower = checkpoint.lower
val upper = checkpoint.upper
RenderUtil.drawAABB(
arena.world.world,
lower.x, lower.y, lower.z,
upper.x, upper.y, upper.z,
Color.AQUA
)
}
}
}
}

View File

@ -0,0 +1,41 @@
package nl.kallestruik.darena.managers
import nl.kallestruik.darena.types.Game
import nl.kallestruik.darena.types.Reloadable
import nl.kallestruik.darena.util.ConfigHelper
import nl.kallestruik.darena.util.Logger
import java.io.File
class GameManager(
val arenaManager: ArenaManager,
val gameFile: File,
): Reloadable {
val games = mutableMapOf<String,Game>()
fun load() {
val config = ConfigHelper.getOrCreateConfig(gameFile, "template/games.yml")
for (gameName in config.getKeys(false)) {
val game = Game(gameName)
for (arenaName in config.getStringList("$gameName.arenas")) {
val arena = arenaManager.getArena(arenaName)
if (arena == null) {
Logger.warn(GameManager::class, "Arena with name $arenaName is used in games.yml but not defined!")
continue
}
game.arenas.add(arena)
}
if (config.contains("$gameName.description"))
game.description = config.getString("$gameName.description")!!
games[gameName] = game
}
}
override fun reload() {
games.clear()
load()
}
}

View File

@ -1,10 +1,16 @@
package nl.kallestruik.darena.managers
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import nl.kallestruik.darena.exceptions.TeamNotFoundException
import nl.kallestruik.darena.types.Team
import nl.kallestruik.darena.util.ConfigHelper
import nl.kallestruik.darena.util.Logger
import org.bukkit.Bukkit
import org.bukkit.command.CommandSender
import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
import java.util.*
import kotlin.collections.HashMap
class PointsManager(
val teamManager: TeamManager,
@ -13,24 +19,59 @@ class PointsManager(
private val points: MutableMap<Team, Int> = HashMap()
fun load() {
//TODO: Load points from file.
val config = ConfigHelper.getOrCreateConfig(pointsFile, "template/points.yml")
if (config.isConfigurationSection("teams")) {
for (teamName in config.getConfigurationSection("teams")!!.getKeys(false)) {
try {
val team = teamManager.getTeamByName(teamName)
points[team] = config.getInt("teams.$teamName")
} catch (e: TeamNotFoundException) {
Logger.warn(PointsManager::class, "Team $teamName is present in points.yml but does not exist ignoring.")
}
}
}
updateTeamSuffixForAll()
}
fun save() {
val config = YamlConfiguration()
val teamsSection = config.createSection("teams")
for (entry in points) {
teamsSection.set(entry.key.name, entry.value)
}
config.save(pointsFile)
}
@Throws(TeamNotFoundException::class)
fun givePointsToPlayer(uuid: UUID, amount: Int, reason: String) {
val team = teamManager.getTeamForPlayer(uuid)
fun givePointsToPlayer(uuid: UUID, amount: Int) {
val team = teamManager.getTeamForPlayer(uuid) ?: throw TeamNotFoundException()
givePointsToTeam(team, amount, reason)
givePointsToTeam(team, amount)
}
fun givePointsToTeam(team: Team, amount: Int, reason: String) {
points[team] = (points[team]?: 0) + amount
//TODO: Send message with reason to team members (and everyone else?)
fun givePointsToTeam(team: Team, amount: Int) {
points[team] = (points[team] ?: 0) + amount
updateTeamSuffix(team)
}
fun updateTeamSuffix(team: Team) {
team.mcTeam.suffix(
Component
.text(" [${getPointsForTeam(team)}]")
.color(NamedTextColor.AQUA)
)
}
fun updateTeamSuffixForAll() {
for (team in teamManager.getAllTeams()) {
updateTeamSuffix(team)
}
}
@Throws(TeamNotFoundException::class)
fun getPointsForPlayer(uuid: UUID): Int {
val team = teamManager.getTeamForPlayer(uuid)
val team = teamManager.getTeamForPlayer(uuid) ?: throw TeamNotFoundException()
return getPointsForTeam(team)
}
@ -39,5 +80,31 @@ class PointsManager(
return points[team] ?: 0
}
fun sendLeaderboard(sender: CommandSender) {
val sortedMap = points.toList().sortedByDescending { (_, v) -> v }.toMap()
//TODO: Make these message configurable
sender.sendMessage(Component.text("============[ Leaderboard ]============"))
var i = 1
for (entry in sortedMap) {
var teamMembers: Component = Component.empty()
for (uuid in teamManager.getTeamMembers(entry.key)) {
val player = Bukkit.getPlayer(uuid)
teamMembers = teamMembers.append(Component.text("${player?.name}\n"))
}
sender.sendMessage(
Component.empty()
.append(Component.text("$i. "))
.append(teamManager.getTeamComponent(entry.key))
.append(Component.text(": "))
.append(Component.text(entry.value))
)
i++
}
}
fun clear() {
points.clear()
updateTeamSuffixForAll()
}
}

View File

@ -1,13 +1,16 @@
package nl.kallestruik.darena.managers
import net.kyori.adventure.text.format.TextColor
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.event.HoverEvent
import nl.kallestruik.darena.exceptions.TeamExistsException
import nl.kallestruik.darena.exceptions.TeamNotFoundException
import nl.kallestruik.darena.types.Team
import nl.kallestruik.darena.util.ConfigHelper
import nl.kallestruik.darena.util.Logger
import org.bukkit.Bukkit
import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
class TeamManager(
val teamsFile: File
@ -15,37 +18,91 @@ class TeamManager(
private val teams: MutableMap<String, Team> = HashMap()
private val teamMembers: MutableMap<Team, MutableList<UUID>> = HashMap()
private val teamForPlayer: MutableMap<UUID, Team> = HashMap()
var autoTeams: Boolean = true
fun load() {
//TODO: Load teams from file
// Make sure all lists are empty so we don't have leftovers.
teams.clear()
teamMembers.clear()
teamForPlayer.clear()
val config = ConfigHelper.getOrCreateConfig(teamsFile, "template/teams.yml")
autoTeams = config.getBoolean("auto-teams")
if (config.isConfigurationSection("teams")) {
for (teamName in config.getConfigurationSection("teams")!!.getKeys(false)) {
Logger.debug(TeamManager::class, "Loading team $teamName")
val team = Team.fromConfig(config.getConfigurationSection("teams.$teamName")!!)
teams[teamName] = team
}
}
if (config.contains("members")) {
for (memberString in config.getStringList("members")) {
val parts = memberString.split(":")
val uuid = UUID.fromString(parts[0])
val teamName = parts[1]
addPlayerToTeam(teamName, uuid)
}
}
}
fun save() {
val config = YamlConfiguration()
config.set("auto-teams", autoTeams)
val teamSection = config.createSection("teams")
for (team in teams.values) {
team.save(teamSection)
}
config.set("members", teamForPlayer.map {
"${it.key}:${it.value.name}"
})
config.save(teamsFile)
}
@Throws(TeamExistsException::class)
fun createTeam(teamName: String) {
fun createTeam(teamName: String): Team {
if (teams.containsKey(teamName))
throw TeamExistsException("The team $teamName already exists!")
teams[teamName] = Team(teamName)
val team = Team(teamName)
teams[teamName] = team
return team
}
@Throws(TeamNotFoundException::class)
fun setTeamColor(teamName: String, color: TextColor) {
val team = getTeamByName(teamName)
team.color = color
fun removeTeam(team: Team) {
teams.remove(team.name)
for (entry in teamForPlayer) {
if (entry.value == team) {
removePlayerFromTeam(entry.key)
}
}
team.mcTeam.unregister()
}
@Throws(TeamNotFoundException::class)
fun addPlayerToTeam(teamName: String, uuid: UUID) {
Logger.trace(TeamManager::class, "addPlayerToTeam(teamName: $teamName, uuid: $uuid)")
val team = getTeamByName(teamName)
addPlayerToTeam(team, uuid)
}
fun addPlayerToTeam(team: Team, uuid: UUID) {
Logger.trace(TeamManager::class, "addPlayerToTeam(team: ${team.name}, uuid: $uuid)")
teamMembers.getOrPut(team) { ArrayList() }.add(uuid)
teamForPlayer[uuid] = team
val player = Bukkit.getPlayer(uuid)
if (player != null)
team.mcTeam.addEntry(player.name)
}
@Throws(TeamNotFoundException::class)
fun getTeamForPlayer(uuid: UUID): Team {
return teamForPlayer[uuid] ?: throw TeamNotFoundException("Player with uuid $uuid is not part of a team!")
fun getTeamForPlayer(uuid: UUID): Team? {
return teamForPlayer[uuid]
}
@Throws(TeamNotFoundException::class)
@ -56,6 +113,51 @@ class TeamManager(
@Throws(TeamNotFoundException::class)
fun getTeamMembers(teamName: String): List<UUID> {
val team = getTeamByName(teamName)
return getTeamMembers(team)
}
fun getTeamMembers(team: Team): List<UUID> {
return teamMembers[team] ?: return emptyList()
}
fun removeAllTeams(): Int {
val count = teams.size
for (team in getAllTeams()) {
removeTeam(team)
}
return count
}
fun autoTeam() {
for (player in Bukkit.getOnlinePlayers()) {
val team = createTeam(player.name)
addPlayerToTeam(team, player.uniqueId)
}
}
fun removePlayerFromTeam(uuid: UUID) {
val team = getTeamForPlayer(uuid)
teamMembers[team]?.remove(uuid)
teamForPlayer.remove(uuid)
team?.mcTeam?.removeEntry(Bukkit.getPlayer(uuid)!!.name)
}
fun getAllTeamNames() = teams.keys
fun getAllTeams() = teams.values
fun getMemberTooltip(team: Team): HoverEvent<Component> {
var hoverComponent = Component.text("Members:")
getTeamMembers(team).forEach {
hoverComponent = hoverComponent.append(Component.text("\n${Bukkit.getOfflinePlayer(it).name}"))
}
return hoverComponent.asHoverEvent()
}
fun getTeamComponent(team: Team): Component {
return Component
.text(team.name)
.color(team.color)
.hoverEvent(getMemberTooltip(team))
}
}

View File

@ -0,0 +1,19 @@
package nl.kallestruik.darena.types
enum class ArenaFeature {
ALWAYS,
RESPAWN,
DEATH_MESSAGE,
CHECKPOINTS,
CHECKPOINT_SOUND,
CHECKPOINT_MESSAGE,
PVP,
NO_FALL,
SHOW_TIMER,
RESTRICT_BUILDING,
GAMEMODE_ADVENTURE,
HORIZONTAL_BORDERS,
NO_TILE_DROPS,
ITEM_FIREBALL,
ITEM_GRAPPLING_HOOK;
}

View File

@ -1,4 +0,0 @@
package nl.kallestruik.darena.types
interface ConfigListLoadable<T> {
fun loadFromList(key: String, value: Any): T

View File

@ -1,7 +0,0 @@
package nl.kallestruik.darena.types
interface ConfigListSaveable {
fun getKey(): String
fun getValue(): Any
}

View File

@ -1,6 +0,0 @@
package nl.kallestruik.darena.types
import org.bukkit.configuration.ConfigurationSection
interface ConfigLoadable<T> {
fun load(section: ConfigurationSection): T

View File

@ -1,7 +0,0 @@
package nl.kallestruik.darena.types
import org.bukkit.configuration.ConfigurationSection
interface ConfigSaveable {
fun save(section: ConfigurationSection)
}

View File

@ -0,0 +1,6 @@
package nl.kallestruik.darena.types
enum class EditSetting {
ALWAYS,
SPAWN_PLACING
}

View File

@ -0,0 +1,6 @@
package nl.kallestruik.darena.types
enum class EditVisualization {
SPAWNS,
CHECKPOINTS
}

View File

@ -0,0 +1,9 @@
package nl.kallestruik.darena.types
import nl.kallestruik.darena.arenas.Arena
data class Game(
var name: String,
val arenas: MutableList<Arena> = mutableListOf(),
var description: String = "",
)

View File

@ -0,0 +1,6 @@
package nl.kallestruik.darena.types
interface Reloadable {
fun reload()
}

View File

@ -1,8 +1,47 @@
package nl.kallestruik.darena.types
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.TextColor
import org.bukkit.Bukkit
import org.bukkit.configuration.ConfigurationSection
data class Team(
val name: String,
val name: String
) {
val mcTeam: org.bukkit.scoreboard.Team = Bukkit.getScoreboardManager().mainScoreboard.getTeam(name)
?: Bukkit.getScoreboardManager().mainScoreboard.registerNewTeam(name)
init {
mcTeam.setOption(org.bukkit.scoreboard.Team.Option.COLLISION_RULE, org.bukkit.scoreboard.Team.OptionStatus.NEVER)
}
var color: TextColor = TextColor.color(0xFFFFFF)
)
set(value) {
field = value
mcTeam.prefix(mcTeam.prefix().color(value))
}
var prefix: String = ""
set(value) {
field = value
mcTeam.prefix(Component.text("$value ").color(color))
}
fun save(section: ConfigurationSection) {
val teamSection = section.createSection(name)
teamSection.set("color", color.asHexString())
teamSection.set("prefix", prefix)
}
companion object {
fun fromConfig(section: ConfigurationSection): Team {
val team = Team(
section.name
)
team.color = TextColor.fromCSSHexString(section.getString("color")!!)!!
team.prefix = section.getString("prefix")!!
return team
}
}
}

View File

@ -0,0 +1,27 @@
package nl.kallestruik.darena.types.arena
import kotlinx.serialization.Serializable
import nl.kallestruik.darena.util.Logger
import nl.kallestruik.dlib.config.annotations.Description
import org.bukkit.Material
@Serializable
data class ArenaAllowList(
@Description([
"The list of rules. These rules support full regular expression. For example .*_wool will match all colors of wool."])
var ruleList: MutableList<ArenaAllowRule> = mutableListOf(),
) {
fun isAllowed(material: Material): Boolean {
Logger.trace(ArenaAllowList::class, "isAllowed(material: $material)")
for (rule in ruleList) {
Logger.debug(ArenaAllowList::class, "Checking rule: $rule")
return when (rule.match(material.name.lowercase())) {
AllowRuleResult.NO_MATCH -> continue
AllowRuleResult.ALLOWED -> true
AllowRuleResult.BLOCKED -> false
}
}
return true
}
}

View File

@ -0,0 +1,55 @@
package nl.kallestruik.darena.types.arena
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import nl.kallestruik.darena.util.Logger
import nl.kallestruik.dlib.config.annotations.DefaultString
@Serializable
data class ArenaAllowRule(
@Serializable(RegexSerializer::class)
var rule: Regex,
@DefaultString("BLOCK")
var type: Type,
) {
fun match(toMatch: String): AllowRuleResult {
Logger.trace(ArenaAllowRule::class, "match(toMatch: $toMatch)")
if (rule.matches(toMatch)) {
Logger.debug(ArenaAllowRule::class, "Rule matches")
return when (type) {
Type.ALLOW -> AllowRuleResult.ALLOWED
Type.BLOCK -> AllowRuleResult.BLOCKED
}
}
return AllowRuleResult.NO_MATCH
}
object RegexSerializer: KSerializer<Regex> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Regex", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Regex) {
encoder.encodeString(value.pattern)
}
override fun deserialize(decoder: Decoder): Regex {
return Regex(decoder.decodeString())
}
}
enum class Type {
ALLOW,
BLOCK;
}
}
enum class AllowRuleResult {
NO_MATCH,
ALLOWED,
BLOCKED,
}

View File

@ -0,0 +1,24 @@
package nl.kallestruik.darena.types.arena
import kotlinx.serialization.Serializable
import nl.kallestruik.darena.arenas.world.ArenaWorld
import nl.kallestruik.dlib.config.annotations.Description
import org.bukkit.Material
@Serializable
data class ArenaBlockRemoveZone(
@Description(["The lower corner of the area to remove."])
val lower: ArenaLocation,
@Description(["The upper corner of the area to remove."])
val upper: ArenaLocation,
) {
fun removeBlocks(world: ArenaWorld) {
for (x in lower.x.until(upper.x + 1)) {
for (y in lower.y.until(upper.y + 1)) {
for (z in lower.z.until(upper.z + 1)) {
world.world.getBlockAt(x, y, z).type = Material.AIR
}
}
}
}
}

View File

@ -0,0 +1,15 @@
package nl.kallestruik.darena.types.arena
import kotlinx.serialization.Serializable
import nl.kallestruik.darena.types.border.AbstractBorderNode
import nl.kallestruik.dlib.config.annotations.Description
@Serializable
data class ArenaBorders(
@Description(["The vertical border of the world. This is the normal minecraft border."])
val vertical: List<AbstractBorderNode> = listOf(),
@Description(["A horizontal border that kills everyone that is above it. This border is indicated using particle effects."])
val top: List<AbstractBorderNode> = listOf(),
@Description(["A horizontal border that kills everyone that is below it. This border is indicated using particle effects."])
val bottom: List<AbstractBorderNode> = listOf(),
)

View File

@ -1,35 +1,30 @@
package nl.kallestruik.darena.types.arena
import org.bukkit.configuration.ConfigurationSection
import javax.naming.ConfigurationException
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.types.ConfigLoadable
import nl.kallestruik.darena.util.ConfigHelper
import nl.kallestruik.darena.util.Logger
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import nl.kallestruik.dlib.config.annotations.DefaultBoolean
import nl.kallestruik.dlib.config.annotations.Description
@Serializable
data class ArenaCheckpoint(
@Description(["The name of the checkpoint."])
val label: String,
@Description(["The lower corner of the checkpoint area."])
val lower: ArenaLocation,
@Description(["The upper corner of the checkpoint area."])
val upper: ArenaLocation,
val spawn: String,
): ConfigSaveable {
override fun save(section: ConfigurationSection) {
Logger.trace(ArenaCheckpoint::class, "save(section: ${section.currentPath})")
section.set("label", label)
lower.save(ConfigHelper.getOrCreateSection(section, "lower"))
upper.save(ConfigHelper.getOrCreateSection(section, "upper"))
section.set("spawn", spawn)
}
companion object: ConfigLoadable<ArenaCheckpoint> {
override fun load(section: ConfigurationSection): ArenaCheckpoint {
Logger.trace(ArenaCheckpoint::class, "load(section: ${section.currentPath})")
return ArenaCheckpoint(
section.name,
ArenaLocation.load(section.getConfigurationSection("lower")!!),
ArenaLocation.load(section.getConfigurationSection("upper")!!),
section.getString("spawn") ?: throw ConfigurationException("Could not find required field spawn in '${section.currentPath}'")
)
}
}
}
@SerialName("spawn-rule")
@Description(["The spawn rule to use for this checkpoint when respawning or moving players."])
var spawnRule: String = "",
@SerialName("spawn-pool")
@Description(["The spawn pool to use for this checkpoint when respawning or moving players."])
val spawnPool: String = "",
@SerialName("move-player")
@Description(["Whether or not to teleport players to the spawn-rule/spawn-pool when they reach the checkpoint."])
@DefaultBoolean(false)
val movePlayer: Boolean = false,
@SerialName("is-finish")
@Description(["Whether or not this checkpoint counts as a finish. If it does players will be placed into spectator mode upon reaching it."])
@DefaultBoolean(false)
val isFinish: Boolean = false,
)

View File

@ -0,0 +1,26 @@
package nl.kallestruik.darena.types.arena
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import nl.kallestruik.dlib.config.annotations.DefaultInt
import nl.kallestruik.dlib.config.annotations.Description
import nl.kallestruik.dlib.config.annotations.MinInt
@Serializable
data class ArenaCountdown(
@SerialName("loading")
@Description(["The time to count down before teleport players into the map for spectating."])
@MinInt(0)
@DefaultInt(10)
var loadingTime: Int = 10,
@SerialName("spectator")
@Description(["The time players have in spectator mode before being moved to the staging area."])
@MinInt(0)
@DefaultInt(10)
var spectatorTime: Int = 10,
@SerialName("before-start")
@Description(["The time to count down in the staging area before removing the area defined in the block-remove-zone section."])
@MinInt(0)
@DefaultInt(10)
var beforeStartTime: Int = 10,
)

View File

@ -1,32 +1,28 @@
package nl.kallestruik.darena.types.arena
import org.bukkit.configuration.ConfigurationSection
import org.bukkit.configuration.InvalidConfigurationException
import nl.kallestruik.darena.types.ConfigListSaveable
import nl.kallestruik.darena.types.ConfigListLoadable
import kotlinx.serialization.Serializable
import nl.kallestruik.darena.util.Logger
import nl.kallestruik.dlib.config.annotations.DefaultInt
import nl.kallestruik.dlib.config.annotations.Description
import nl.kallestruik.dlib.config.annotations.MinInt
import org.bukkit.NamespacedKey
import org.bukkit.enchantments.Enchantment
import org.bukkit.inventory.ItemStack
@Serializable
data class ArenaEnchantment(
@Description(["The name of the enchantment.",
"", "Valid values: The id of the enchantment as shown in the /enchant command without the minecraft:",
"For example: sharpness, knockback, power"])
val name: String,
@Description(["The level of the enchantment. Allows over enchanting."])
@MinInt(1)
@DefaultInt(1)
val level: Int,
): ConfigListSaveable {
override fun getKey(): String {
Logger.trace(ArenaEnchantment::class, "getKey()")
return name
}
) {
fun addToItem(stack: ItemStack) {
Logger.trace(ArenaEnchantment::class, "addToItem(stack: $stack)")
stack.addUnsafeEnchantment(Enchantment.getByKey(NamespacedKey.minecraft(name.lowercase()))!!, level)
override fun getValue(): Int {
Logger.trace(ArenaEnchantment::class, "getValue()")
return level
}
companion object: ConfigListLoadable<ArenaEnchantment> {
override fun loadFromList(key: String, value: Any): ArenaEnchantment {
Logger.trace(ArenaEnchantment::class, "loadFromList(key: $key, value: $value)")
return ArenaEnchantment(
key,
value as Int
)
}
}
}

View File

@ -0,0 +1,25 @@
package nl.kallestruik.darena.types.arena
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import nl.kallestruik.dlib.config.annotations.DefaultInt
import nl.kallestruik.dlib.config.annotations.Description
import nl.kallestruik.dlib.config.annotations.MinInt
@Serializable
data class ArenaEndConditions(
@SerialName("players-left")
@Description(["End the game when there are this many players left alive. Set to -1 to disable."])
@MinInt(-1)
@DefaultInt(-1)
val playersLeft: Int = -1,
@Description(["End the game after this amount of seconds. Set to -1 to disable."])
@MinInt(-1)
@DefaultInt(-1)
val time: Int = -1,
@SerialName("players-finished")
@Description(["End the game after this amount of players reach a finish checkpoint. Set to -1 to disable."])
@MinInt(-1)
@DefaultInt(-1)
val playersFinished: Int = -1,
)

View File

@ -1,36 +1,70 @@
package nl.kallestruik.darena.types.arena
import nl.kallestruik.darena.util.ConfigHelper
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.types.ConfigLoadable
import com.destroystokyo.paper.Namespaced
import kotlinx.serialization.Serializable
import nl.kallestruik.darena.util.Logger
import nl.kallestruik.dlib.config.annotations.*
import org.bukkit.Material
import org.bukkit.configuration.ConfigurationSection
import org.bukkit.NamespacedKey
import org.bukkit.inventory.ItemStack
@Serializable
data class ArenaItem(
@Description(["The item type."])
@DefaultString("AIR")
val material: Material = Material.AIR,
@Description(["The amount."])
@MinInt(1)
@DefaultInt(1)
val amount: Int = 1,
@Description(["The enchantments that will be applied to the item."])
val enchantments: List<ArenaEnchantment> = listOf(),
@Description(["Whether the item takes durability damage or not."])
@DefaultBoolean(false)
val unbreakable: Boolean = false,
): ConfigSaveable {
override fun save(section: ConfigurationSection) {
section.set("material", material.toString())
section.set("amount", amount)
ConfigHelper.saveList(ConfigHelper.getOrCreateSection(section, "enchantments"), enchantments, ArenaEnchantment)
section.set("unbreakable", unbreakable)
}
companion object: ConfigLoadable<ArenaItem> {
override fun load(section: ConfigurationSection): ArenaItem {
val enchantments = section.getConfigurationSection("enchantments")?.let {
ArenaEnchantment.loadList(it)
} ?: listOf()
return ArenaItem(
ConfigHelper.matchMaterial(section.getString("material")!!),
section.getInt("amount", 1),
enchantments,
section.getBoolean("unbreakable", false)
)
@Description(["A list of blocks that this block can be placed on. Requires the feature GAMEMODE_ADVENTURE to work.",
"", "Valid values: all minecraft block ids as seen in game with F3+h on without the minecraft:",
"For example: stone, granite, andesite, diamond_block"])
val placeOn: Set<String> = mutableSetOf(),
@Description(["A list of blocks that this item can be break. Requires the feature GAMEMODE_ADVENTURE to work.",
"", "Valid values: all minecraft block ids as seen in game with F3+h on without the minecraft:",
"For example: stone, granite, andesite, diamond_block"])
val canBreak: Set<String> = mutableSetOf(),
) {
fun asItemStack(): ItemStack {
Logger.trace(ArenaItem::class, "asItemStack()")
val stack = ItemStack(material, amount)
for (enchantment in enchantments) {
enchantment.addToItem(stack)
}
val meta = stack.itemMeta
if (placeOn.isNotEmpty()) {
val placeableKeys = mutableSetOf<Namespaced>()
for (block in placeOn) {
//TODO: Make this give a nicer error if it fails.
placeableKeys.add(NamespacedKey.minecraft(block))
}
meta.setPlaceableKeys(placeableKeys)
}
if (canBreak.isNotEmpty()) {
val destroyableKeys = mutableSetOf<Namespaced>()
for (block in canBreak) {
destroyableKeys.add(NamespacedKey.minecraft(block))
}
meta.setDestroyableKeys(destroyableKeys)
}
if (unbreakable) {
meta.isUnbreakable = true
}
stack.itemMeta = meta
return stack
}
}

View File

@ -1,43 +1,48 @@
package nl.kallestruik.darena.types.arena
import org.bukkit.configuration.ConfigurationSection
import nl.kallestruik.darena.util.ConfigHelper
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.types.ConfigLoadable
import kotlinx.serialization.Serializable
import nl.kallestruik.dlib.config.annotations.AllowedKeys
import nl.kallestruik.dlib.config.annotations.AllowedKeysExclusive
import nl.kallestruik.dlib.config.annotations.Description
import org.bukkit.entity.Player
import org.bukkit.inventory.EquipmentSlot
@Serializable
data class ArenaLoadout(
val name: String,
val hotbar: List<ArenaItem>,
val armor: List<ArenaItem>,
val offhand: ArenaItem,
): ConfigSaveable {
override fun save(section: ConfigurationSection) {
ConfigHelper.saveList(ConfigHelper.getOrCreateSection(section, "hotbar"), hotbar)
ConfigHelper.saveList(ConfigHelper.getOrCreateSection(section, "armor"), armor)
offhand.save(ConfigHelper.getOrCreateSection(section, "offhand"))
}
@Description(["The items that will be in the players hotbar."])
@AllowedKeys(["0", "1", "2", "3", "4", "5", "6", "7", "8"])
val hotbar: MutableMap<String, ArenaItem> = mutableMapOf(),
@Description(["The items that will be in the players armor slots."])
@AllowedKeysExclusive(["helmet", "head"])
@AllowedKeysExclusive(["chestplate", "chest", "elytra"])
@AllowedKeysExclusive(["leggings", "legs", "pants"])
@AllowedKeysExclusive(["boots", "feet"])
val armor: MutableMap<String, ArenaItem> = mutableMapOf(),
@Description(["The item that will be in the players offhand."])
val offhand: ArenaItem? = null,
@Description(["The potion effects that will be applied to players upon spawning."])
val effects: MutableList<ArenaPotionEffect> = mutableListOf(),
) {
fun equip(player: Player) {
for (entry in hotbar) {
val slot = entry.key.toInt()
player.inventory.setItem(slot, entry.value.asItemStack())
}
companion object : ConfigLoadable<ArenaLoadout> {
override fun load(section: ConfigurationSection): ArenaLoadout {
for (entry in armor) {
when (entry.key) {
"helmet", "head" -> player.inventory.setItem(EquipmentSlot.HEAD, entry.value.asItemStack())
"chestplate", "chest", "elytra" -> player.inventory.setItem(EquipmentSlot.CHEST, entry.value.asItemStack())
"leggings", "legs", "pants" -> player.inventory.setItem(EquipmentSlot.LEGS, entry.value.asItemStack())
"boots", "feet" -> player.inventory.setItem(EquipmentSlot.FEET, entry.value.asItemStack())
}
}
val hotbar = section.getConfigurationSection("hotbar")?.let {
ArenaItem.loadList(it)
}?: listOf()
if (offhand != null)
player.inventory.setItemInOffHand(offhand.asItemStack())
val armor = section.getConfigurationSection("armor")?.let {
ArenaItem.loadList(it)
}?: listOf()
val offhand = section.getConfigurationSection("offhand")?.let {
ArenaItem.load(it)
}?: ArenaItem()
return ArenaLoadout(
section.name,
hotbar,
armor,
offhand,
)
for (effect in effects) {
effect.apply(player)
}
}
}

View File

@ -1,22 +1,24 @@
package nl.kallestruik.darena.types.arena
import kotlinx.serialization.Serializable
import nl.kallestruik.dlib.config.annotations.DefaultInt
import nl.kallestruik.dlib.config.annotations.Description
import org.bukkit.configuration.ConfigurationSection
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.types.ConfigLoadable
@Serializable
data class ArenaLocation(
@Description(["The x coordinate."])
@DefaultInt(0)
val x: Int,
@Description(["The y coordinate."])
@DefaultInt(0)
val y: Int,
@Description(["The z coordinate."])
@DefaultInt(0)
val z: Int
): ConfigSaveable {
override fun save(section: ConfigurationSection) {
section.set("x", x)
section.set("y", y)
section.set("z", z)
}
companion object: ConfigLoadable<ArenaLocation> {
override fun load(section: ConfigurationSection): ArenaLocation {
) {
companion object {
fun load(section: ConfigurationSection): ArenaLocation {
return ArenaLocation(
section.getInt("x", 0),
section.getInt("y", 0),

View File

@ -1,31 +1,23 @@
package nl.kallestruik.darena.types.arena
import org.bukkit.configuration.ConfigurationSection
import nl.kallestruik.darena.util.ConfigHelper
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import nl.kallestruik.dlib.config.annotations.Description
@Serializable
data class ArenaPoints(
@Description(["The points given to a player when they get a kill."])
val kill: List<Int> = listOf(0),
@SerialName("last-man-standing")
@Description(["The points given to a player for staying alive the longest."])
val lastManStanding: List<Int> = listOf(0),
val checkpoints: Map<String, List<Int>> = mapOf()
) {
fun save(section: ConfigurationSection) {
section.set("kill", ConfigHelper.intListToString(kill))
section.set("lastManStanding", ConfigHelper.intListToString(lastManStanding))
val checkpointSection = ConfigHelper.getOrCreateSection(section, "checkpoints")
for (entry in checkpoints) {
checkpointSection.set(entry.key, ConfigHelper.intListToString(entry.value))
}
}
companion object {
fun load(section: ConfigurationSection): ArenaPoints {
return ArenaPoints(
ConfigHelper.parseIntList(section.getString("kill")!!),
ConfigHelper.parseIntList(section.getString("lastManStanding")!!),
ConfigHelper.parseStringIntListMap(section.getConfigurationSection("checkpoints")!!)
)
}
}
}
@Description(["The points given to a player for reaching a checkpoint.",
"","Valid keys: the label of a checkpoint defined in the checkpoints section."])
val checkpoints: Map<String, List<Int>> = mapOf(),
@Description(["The points given to a player for reaching a checkpoint that is part of a spawn pool.",
"The first list represents the first checkpoint, the second the second checkpoint, etc",
"",
"Valid keys: the names of spawn pools as defined in the spawn-pools section.",
])
val pools: Map<String, List<List<Int>>> = mapOf(),
)

View File

@ -1,28 +1,30 @@
package nl.kallestruik.darena.types.arena
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.types.ConfigLoadable
import org.bukkit.configuration.ConfigurationSection
import kotlinx.serialization.Serializable
import nl.kallestruik.dlib.config.annotations.Description
import nl.kallestruik.dlib.config.annotations.MaxInt
import nl.kallestruik.dlib.config.annotations.MinInt
import org.bukkit.entity.Player
import org.bukkit.potion.PotionEffect
import org.bukkit.potion.PotionEffectType
class ArenaPotionEffect(
@Serializable
data class ArenaPotionEffect(
@Description(["The name of the potion effect.",
"", "Valid values: the name of the potion effect as seen in the /effect command without minecraft:",
"For example: invisibility, strength, haste"])
val name: String,
@Description(["The strength of the potion effect."])
@MinInt(0)
@MaxInt(255)
val strength: Int,
@Description(["The duration of the potion effect in ticks. (20 ticks = 1 second)"])
@MinInt(0)
val duration: Int,
@Description(["Whether the potion effect should be hidden from the player. This hides the particles and the icon."])
val hidden: Boolean
): ConfigSaveable {
override fun save(section: ConfigurationSection) {
section.set("strength", strength)
section.set("duration", duration)
section.set("hidden", hidden)
}
companion object: ConfigLoadable<ArenaPotionEffect> {
override fun load(section: ConfigurationSection): ArenaPotionEffect {
return ArenaPotionEffect(
section.name,
section.getInt("strength"),
section.getInt("duration"),
section.getBoolean("hidden"))
}
) {
fun apply(player: Player) {
player.addPotionEffect(PotionEffect(PotionEffectType.getByName(name)!!, duration, strength, !hidden, !hidden, !hidden))
}
}

View File

@ -1,46 +1,44 @@
package nl.kallestruik.darena.types.arena
import kotlinx.serialization.Serializable
import nl.kallestruik.darena.arenas.world.ArenaWorld
import nl.kallestruik.darena.util.ConfigHelper
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.types.ConfigLoadable
import nl.kallestruik.darena.util.Logger
import nl.kallestruik.dlib.config.annotations.DefaultFloat
import nl.kallestruik.dlib.config.annotations.Description
import nl.kallestruik.dlib.config.annotations.MaxFloat
import nl.kallestruik.dlib.config.annotations.MinFloat
import org.bukkit.Location
import org.bukkit.configuration.ConfigurationSection
import org.bukkit.World
import org.bukkit.entity.Player
@Serializable
data class ArenaSpawn(
val label: String,
@Description(["The x coordinate of the spawn point."])
@DefaultFloat(0.0)
val x: Double,
@Description(["The y coordinate of the spawn point."])
@DefaultFloat(0.0)
val y: Double,
@Description(["The z coordinate of the spawn point."])
@DefaultFloat(0.0)
val z: Double,
@Description(["The yaw of the spawn point."])
@MinFloat(-180.0)
@MaxFloat(180.0)
@DefaultFloat(0.0)
val yaw: Float,
val pitch: Float,
val loadout: String
): ConfigSaveable {
@Description(["The pitch of the spawn point."])
@MinFloat(-90.0)
@MaxFloat(90.0)
@DefaultFloat(0.0)
val pitch: Float
) {
fun spawn(world: ArenaWorld, player: Player) {
player.teleport(Location(world.world, x, y, z, yaw, pitch))
Logger.trace(ArenaSpawn::class, "spawn(world: ${world.name}, player: ${player.name})")
player.teleport(toLocation(world.world))
}
override fun save(section: ConfigurationSection) {
section.set("x", x)
section.set("y", y)
section.set("z", z)
section.set("yaw", yaw.toDouble())
section.set("pitch", pitch.toDouble())
section.set("loadout", loadout)
}
companion object: ConfigLoadable<ArenaSpawn> {
override fun load(section: ConfigurationSection): ArenaSpawn {
return ArenaSpawn(
section.name,
section.getDouble("x"),
section.getDouble("y"),
section.getDouble("z"),
section.getDouble("yaw").toFloat(),
section.getDouble("pitch").toFloat(),
section.getString("loadout")!!
)
}
fun toLocation(world: World): Location {
return Location(world, x, y, z, yaw, pitch)
}
}

View File

@ -0,0 +1,19 @@
package nl.kallestruik.darena.types.arena
import kotlinx.serialization.Serializable
import nl.kallestruik.dlib.config.annotations.DefaultInt
import nl.kallestruik.dlib.config.annotations.Description
import nl.kallestruik.dlib.config.annotations.MinInt
@Serializable
data class ArenaSpawnPool(
@Description(["The name of the spawn pool."])
val label: String,
@Description(["The amount of spawn rules that will be chosen before a player is considered finished."])
@MinInt(1)
@DefaultInt(-1)
val times: Int,
@Description(["The list of spawn rules that the pool can choose from.",
"", "Valid values: any spawn rule defined in the spawn-rules section."])
val rules: List<String>
)

View File

@ -1,27 +1,27 @@
package nl.kallestruik.darena.types.arena
import nl.kallestruik.darena.types.ConfigLoadable
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.util.ConfigHelper
import org.bukkit.configuration.ConfigurationSection
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import nl.kallestruik.dlib.config.annotations.DefaultBoolean
import nl.kallestruik.dlib.config.annotations.DefaultString
import nl.kallestruik.dlib.config.annotations.Description
@Serializable
class ArenaSpawnRule(
val reuseSpawns: Boolean,
val effects: Map<String, ArenaPotionEffect>,
val spawns: List<String>
): ConfigSaveable {
override fun save(section: ConfigurationSection) {
section.set("reuseSpawns", reuseSpawns)
ConfigHelper.saveMap(ConfigHelper.getOrCreateSection(section, "effects"), effects)
section.set("spawns", spawns)
}
companion object: ConfigLoadable<ArenaSpawnRule> {
override fun load(section: ConfigurationSection): ArenaSpawnRule {
return ArenaSpawnRule(
section.getBoolean("reuseSpawn", true),
ConfigHelper.loadMap(section.getConfigurationSection("effects")!!, ArenaPotionEffect),
section.getStringList("spawns"))
}
}
}
@SerialName("reuse-spawns")
@Description(["Whether or not to reuse the spawns for different players."])
@DefaultBoolean(true)
val reuseSpawns: Boolean = true,
@SerialName("reuse-individual")
@Description(["Whether or not to reuse the spawns for individual players.",
"When this is set a player will always use the same spawn after spawning there once."])
@DefaultBoolean(false)
val reuseIndividual: Boolean = false,
@Description(["The list of spawns that this spawn rule can pick from.",
"", "Valid values: any spawn name defined in the spawns section."])
val spawns: MutableList<String> = mutableListOf(),
@Description(["The loadout to use for players that are spawned using this spawn rule. Use none to not give them any items.",
"", "Valid values: any loadout defined in the loadouts section or none."])
@DefaultString("none")
val loadout: String = "none"
)

View File

@ -0,0 +1,29 @@
package nl.kallestruik.darena.types.arena
import org.bukkit.Location
data class ProcessedArenaCheckpoint(
val label: String,
val lower: ArenaLocation,
val upper: ArenaLocation,
var spawnRule: ProcessedArenaSpawnRule?,
val spawnPool: ProcessedArenaSpawnPool?,
val movePlayer: Boolean,
val isFinish: Boolean,
) {
constructor(checkpoint: ArenaCheckpoint, spawnRule: ProcessedArenaSpawnRule?, spawnPool: ProcessedArenaSpawnPool?)
: this(checkpoint.label, checkpoint.lower, checkpoint.upper, spawnRule, spawnPool, checkpoint.movePlayer, checkpoint.isFinish)
fun isInside(location: Location): Boolean {
return location.blockX <= upper.x && location.blockX >= lower.x
&& location.blockY <= upper.y && location.blockY >= lower.y
&& location.blockZ <= upper.z && location.blockZ >= lower.z
}
fun resolveSpawnRule() {
if (spawnRule != null)
return
spawnRule = spawnPool!!.nextRule()
}
}

View File

@ -0,0 +1,11 @@
package nl.kallestruik.darena.types.arena
data class ProcessedArenaSpawnPool(
val label: String,
val times: Int,
val rules: MutableList<ProcessedArenaSpawnRule>
) {
fun nextRule(): ProcessedArenaSpawnRule? {
return rules.removeFirstOrNull()
}
}

View File

@ -0,0 +1,55 @@
package nl.kallestruik.darena.types.arena
import nl.kallestruik.darena.arenas.world.ArenaWorld
import nl.kallestruik.darena.util.ArenaUtil
import nl.kallestruik.darena.util.Logger
import org.bukkit.entity.Player
import java.util.*
data class ProcessedArenaSpawnRule(
val reuseSpawns: Boolean,
val reuseIndividual: Boolean,
val spawns: List<ArenaSpawn>,
val loadout: ArenaLoadout?,
) {
var lastSpawnIndex = -1
private val lastForPlayer = mutableMapOf<UUID, Int>()
constructor(spawnRule: ArenaSpawnRule, spawns: List<ArenaSpawn>, loadout: ArenaLoadout?): this(spawnRule.reuseSpawns, spawnRule.reuseIndividual, spawns, loadout)
fun spawnPlayers(players: Collection<Player>, world: ArenaWorld) {
Logger.trace(ProcessedArenaSpawnRule::class, "spawnPlayers(players: $players, world: ${world.name})")
players.forEach() {player ->
spawnPlayer(player, world)
}
}
fun spawnPlayer(player: Player, world: ArenaWorld) {
if (player.health <= 0) {
player.spigot().respawn()
}
Logger.trace(ProcessedArenaSpawnRule::class, "spawnPlayer(player: ${player.name}, world: ${world.name})")
if (player.uniqueId in lastForPlayer) {
spawnAndEquip(player, world, spawns[lastForPlayer[player.uniqueId]!!])
return
}
if (!reuseSpawns && lastSpawnIndex + 1 >= spawns.size) {
Logger.warn(ProcessedArenaSpawnRule::class, "Cant spawn player \"${player.name}\" because there are no spots left and reuse-spawns is set to false.")
return
}
val nextSpawnIndex = (lastSpawnIndex + 1) % spawns.size
spawnAndEquip(player, world, spawns[nextSpawnIndex])
lastForPlayer[player.uniqueId] = nextSpawnIndex
lastSpawnIndex++
}
private fun spawnAndEquip(player: Player, world: ArenaWorld, spawn: ArenaSpawn) {
ArenaUtil.clearPlayer(player)
spawn.spawn(world, player)
loadout?.equip(player)
}
}

View File

@ -0,0 +1,8 @@
package nl.kallestruik.darena.types.border
abstract class AbstractBorder {
abstract fun size(size: Double, time: Long = 0)
abstract fun center(x: Double, y: Double)
}

View File

@ -0,0 +1,53 @@
package nl.kallestruik.darena.types.border
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.Transient
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import org.bukkit.plugin.java.JavaPlugin
@Serializable(AbstractBorderNode.BorderScriptSerializer::class)
abstract class AbstractBorderNode {
@Transient
var next: AbstractBorderNode? = null
abstract fun execute(border: AbstractBorder, plugin: JavaPlugin)
abstract fun toCommand(): String
protected fun runNext(border: AbstractBorder, plugin: JavaPlugin) {
next?.execute(border, plugin)
}
open fun cancel() {
next?.cancel()
}
object BorderScriptSerializer: KSerializer<AbstractBorderNode> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("BorderScriptNode", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: AbstractBorderNode) {
encoder.encodeString(value.toCommand())
}
override fun deserialize(decoder: Decoder): AbstractBorderNode {
return createNode(decoder.decodeString())
}
private fun createNode(command: String): AbstractBorderNode {
val parts = command.split(" ", limit = 2)
return when (parts[0].uppercase()) {
"CENTER" -> BorderNodeCenter.fromParameters(parts[1])
"SET" -> BorderNodeSet.fromParameters(parts[1])
"TRANSITION" -> BorderNodeTransition.fromParameters(parts[1])
"WAIT" -> BorderNodeWait.fromParameters(parts[1])
else -> throw SerializationException("${parts[0]} is not a valid border script keyword.")
}
}
}
}

View File

@ -0,0 +1,25 @@
package nl.kallestruik.darena.types.border
import org.bukkit.plugin.java.JavaPlugin
class BorderNodeCenter(
private val x: Double,
private val y: Double,
): AbstractBorderNode() {
override fun execute(border: AbstractBorder, plugin: JavaPlugin) {
border.center(x, y)
runNext(border, plugin)
}
override fun toCommand(): String {
return "CENTER $x $y"
}
companion object {
fun fromParameters(params: String): BorderNodeCenter {
val parts = params.split(" ")
return BorderNodeCenter(parts[0].toDouble(), parts[1].toDouble())
}
}
}

View File

@ -0,0 +1,23 @@
package nl.kallestruik.darena.types.border
import org.bukkit.plugin.java.JavaPlugin
class BorderNodeSet(
private val size: Double
): AbstractBorderNode() {
override fun execute(border: AbstractBorder, plugin: JavaPlugin) {
border.size(size)
runNext(border, plugin)
}
override fun toCommand(): String {
return "SET $size"
}
companion object {
fun fromParameters(params: String): BorderNodeSet {
return BorderNodeSet(params.toDouble())
}
}
}

View File

@ -0,0 +1,25 @@
package nl.kallestruik.darena.types.border
import org.bukkit.plugin.java.JavaPlugin
class BorderNodeTransition(
private val size: Double,
private val time: Long
): AbstractBorderNode() {
override fun execute(border: AbstractBorder, plugin: JavaPlugin) {
border.size(size, time)
runNext(border, plugin)
}
override fun toCommand(): String {
return "TRANSITION $size $time"
}
companion object {
fun fromParameters(params: String): BorderNodeTransition {
val parts = params.split(" ")
return BorderNodeTransition(parts[0].toDouble(), parts[1].toLong())
}
}
}

View File

@ -0,0 +1,33 @@
package nl.kallestruik.darena.types.border
import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.scheduler.BukkitRunnable
class BorderNodeWait(
private val time: Long
): AbstractBorderNode() {
private var runnable: BukkitRunnable? = null
override fun execute(border: AbstractBorder, plugin: JavaPlugin) {
object : BukkitRunnable() {
override fun run() {
runNext(border, plugin)
}
}.runTaskLater(plugin, time * 20)
}
override fun toCommand(): String {
return "WAIT $time"
}
override fun cancel() {
super.cancel()
runnable?.cancel()
}
companion object {
fun fromParameters(params: String): BorderNodeWait {
return BorderNodeWait(params.toLong())
}
}
}

View File

@ -0,0 +1,16 @@
package nl.kallestruik.darena.types.border
class HorizontalBorder: AbstractBorder() {
var height = 0
//TODO: Implement visible border in the form of a block being placed.
//TODO: Implement visible border in the form of particles if a player is close.
override fun size(size: Double, time: Long) {
height = size.toInt()
//TODO: Implement transition over time.
}
override fun center(x: Double, y: Double) {
//TODO: Maybe throw exception or something
}
}

View File

@ -0,0 +1,15 @@
package nl.kallestruik.darena.types.border
import nl.kallestruik.darena.arenas.world.ArenaWorld
class MCBorder(
private val world: ArenaWorld
): AbstractBorder() {
override fun size(size: Double, time: Long) {
world.world.worldBorder.setSize(size, time)
}
override fun center(x: Double, y: Double) {
world.world.worldBorder.setCenter(x, y)
}
}

View File

@ -0,0 +1,50 @@
package nl.kallestruik.darena.types.countdown
import nl.kallestruik.darena.util.CountdownHelper
import org.bukkit.Bukkit
import org.bukkit.entity.Player
import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.scheduler.BukkitRunnable
import org.bukkit.scheduler.BukkitTask
open class AsyncCountdown(
val plugin: JavaPlugin,
val time: Int,
val subtitle: String,
val alwaysAsTitle: Boolean = false,
val alwaysAsActionBar: Boolean = false,
val after: Runnable,
): Countdown {
var task: BukkitTask? = null
override fun start(players: Collection<Player>): Countdown {
var currentTime = time
task = object : BukkitRunnable() {
override fun run() {
if (currentTime > 0) {
if (!alwaysAsActionBar && (alwaysAsTitle || currentTime <= 5 || currentTime % 10 == 0)) {
CountdownHelper.sendTitleToAll(players, currentTime, subtitle)
CountdownHelper.sendActionBarToAll(players, currentTime, subtitle)
} else {
CountdownHelper.sendActionBarToAll(players, currentTime, subtitle)
}
currentTime--
} else {
CountdownHelper.clearTitleForAll(players)
CountdownHelper.clearActionBarForAll(players)
Bukkit.getScheduler().runTask(plugin, after)
this@AsyncCountdown.cancel()
}
}
}.runTaskTimerAsynchronously(plugin, 0, 20)
return this
}
override fun cancel() {
task?.cancel()
}
}

View File

@ -0,0 +1,22 @@
package nl.kallestruik.darena.types.countdown
import org.bukkit.Bukkit
import org.bukkit.entity.Player
import org.bukkit.plugin.java.JavaPlugin
class AsyncCountdownWithSyncTask(
plugin: JavaPlugin,
time: Int,
subtitle: String,
private val during: Runnable,
after: Runnable,
alwaysAsTitle: Boolean = false,
alwaysAsActionBar: Boolean = false,
): AsyncCountdown(plugin, time, subtitle, alwaysAsTitle, alwaysAsActionBar, after) {
override fun start(players: Collection<Player>): Countdown {
super.start(players)
Bukkit.getScheduler().runTask(plugin, during)
return this
}
}

View File

@ -0,0 +1,9 @@
package nl.kallestruik.darena.types.countdown
import org.bukkit.entity.Player
interface Countdown {
fun start(players: Collection<Player>): Countdown
fun cancel()
}

View File

@ -1,10 +1,14 @@
package nl.kallestruik.darena.util
import nl.kallestruik.darena.arenas.Arena
import nl.kallestruik.darena.managers.ArenaManager
import org.bukkit.Bukkit
import org.bukkit.GameMode
import org.bukkit.entity.Entity
import org.bukkit.entity.Player
import org.bukkit.entity.Projectile
class ArenaUtil {
object ArenaUtil {
fun getPossibleParticipants(): List<Player> {
Logger.trace(ArenaUtil::class, "getPossibleParticipants()")
@ -15,4 +19,60 @@ class ArenaUtil {
Logger.trace(ArenaUtil::class, "getPossibleSpectators()")
return Bukkit.getOnlinePlayers().filter { it.gameMode == GameMode.SPECTATOR }
}
fun clearPlayer(player: Player) {
player.inventory.close()
player.inventory.clear()
player.exp = 0F
player.level = 0
player.health = 20.0
player.saturation = 0F
player.foodLevel = 20
player.fireTicks = 0
player.fallDistance = 0F
for (effect in player.activePotionEffects) {
player.removePotionEffect(effect.type)
}
}
fun resolveDamager(damager: Entity): Player? {
if (damager is Player)
return damager
if (damager is Projectile && damager.shooter is Player)
return damager.shooter as Player
return null
}
fun formatTime(timeLeft: Int): String {
val minutes = (timeLeft / 60).toString().padStart(2, '0')
val seconds = (timeLeft % 60).toString().padStart(2, '0')
return "$minutes:$seconds"
}
fun formatTimeMillis(timeLeft: Long): String {
val minutes = (timeLeft / 1000 / 60).toString().padStart(2, '0')
val seconds = ((timeLeft / 1000) % 60).toString().padStart(2, '0')
val millis = (timeLeft % 1000).toString().padStart(3, '0')
return "$minutes:$seconds.$millis"
}
fun finishPlayer(player: Player, arena: Arena, arenaManager: ArenaManager) {
Logger.trace(ArenaManager::class, "finishPlayer(player: $player, arena: ${arena.config.name}, arenaManager: $arenaManager)")
arena.makeSpectator(player)
arena.session!!.playersFinished++
if (arena.config.endConditions.playersFinished != -1
&& arena.session!!.playersFinished >= arena.config.endConditions.playersFinished
) {
arenaManager.end()
return
}
if (arenaManager.currentArena!!.session!!.participants.size <= arenaManager.currentArena!!.config.endConditions.playersLeft) {
arenaManager.end()
return
}
}
}

View File

@ -1,14 +1,12 @@
package nl.kallestruik.darena.util
import nl.kallestruik.darena.exceptions.MaterialNotFoundException
import nl.kallestruik.darena.types.ConfigSaveable
import nl.kallestruik.darena.types.ConfigLoadable
import nl.kallestruik.darena.types.ConfigListSaveable
import nl.kallestruik.darena.types.ConfigListLoadable
import org.bukkit.Material
import org.bukkit.configuration.file.YamlConfiguration
import org.bukkit.configuration.ConfigurationSection
import java.io.*
import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
object ConfigHelper {
@ -54,40 +52,6 @@ object ConfigHelper {
return section.createSection(key)
}
fun <T: ConfigSaveable> saveMap(section: ConfigurationSection, map: Map<String, T>) {
Logger.trace(ConfigHelper::class, "saveMap(section: ${section.currentPath}, map: ${map.toString()})")
for (entry in map.entries) {
entry.value.save(ConfigHelper.getOrCreateSection(section, entry.key))
}
}
fun <T: ConfigListSaveable> saveList(section: ConfigurationSection, list: List<T>) {
Logger.trace(ConfigHelper::class, "saveList(section: ${section.currentPath}, list: ${list.toString()})")
for (item in list) {
section.set(item.getKey(), item.getValue())
}
}
fun <R, T: ConfigLoadable<R>> loadMap(section: ConfigurationSection, loader: T): MutableMap<String, R> {
Logger.trace(ConfigHelper::class, "loadMap(section: ${section.currentPath}, loader: ${loader::class.qualifiedName})")
val map = mutableMapOf<String, R>()
for (key in section.getKeys(false)) {
map.put(key, loader.load(section.getConfigurationSection(key)!!))
}
return map
}
fun <R, T: ConfigListLoadable<R>> loadList(section: ConfigurationSection, loader: T): MutableList<R> {
Logger.trace(ConfigHelper::class, "loadList(section: ${section.currentPath}, loader: ${loader::class.qualifiedName})")
val list = mutableListOf<R>()
for (key in section.getKeys(false)) {
loader.loadFromList(key, section.get(key)!!)
}
return list
}
@Throws(MaterialNotFoundException::class)
fun matchMaterial(material: String): Material {
Logger.trace(ConfigHelper::class, "matchMaterial(material: $material)")
@ -97,6 +61,7 @@ object ConfigHelper {
fun parseIntList(string: String): List<Int> {
Logger.trace(ConfigHelper::class, "parseIntList(string: $string)")
val list = mutableListOf<Int>()
for (key in string.split(" ")) {
list.add(key.toInt())
}
@ -113,14 +78,4 @@ object ConfigHelper {
return sb.toString().trim()
}
fun parseStringIntListMap(section: ConfigurationSection): Map<String, List<Int>> {
Logger.trace(ConfigHelper::class, "parseStringIntListMap(section: ${section.currentPath})")
val map = mutableMapOf<String, List<Int>>()
for (key in section.getKeys(true)) {
map[key] = parseIntList(section.getString(key)!!)
}
return map
}
}

View File

@ -0,0 +1,50 @@
package nl.kallestruik.darena.util
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.TextColor
import net.kyori.adventure.title.Title
import org.bukkit.Sound
import org.bukkit.entity.Player
import java.time.Duration
object CountdownHelper {
fun sendTitleToAll(players: Collection<Player>, timeLeft: Int, subtitle: String) {
for (player in players) {
player.playSound(player.location, Sound.BLOCK_NOTE_BLOCK_BIT, 1.0F, 0.5F)
player.showTitle(
Title.title(
Component.text(timeLeft)
.color(TextColor.color(0xFFAA00)),
Component.text("$subtitle..."),
Title.Times.of(Duration.ZERO, Duration.ofSeconds(3), Duration.ofSeconds(2))
))
}
}
fun sendActionBarToAll(players: Collection<Player>, timeLeft: Int, prefix: String) {
for (player in players) {
player.sendActionBar(
Component.text("$prefix: ")
.append(
Component
.text(timeLeft)
.color(TextColor.color(0xFFAA00))
)
)
}
}
fun clearTitleForAll(players: Collection<Player>) {
for (player in players) {
player.clearTitle()
}
}
fun clearActionBarForAll(players: Collection<Player>) {
for (player in players) {
player.sendActionBar(Component.empty())
}
}
}

View File

@ -3,8 +3,7 @@ package nl.kallestruik.darena.util
import kotlin.reflect.KClass
object Logger {
var level: LogLevel = LogLevel.INFO
var level: LogLevel = LogLevel.TRACE
fun trace(source: KClass<*>, message: String) {
if (level.shouldLog(LogLevel.TRACE))
@ -36,7 +35,7 @@ object Logger {
println("[CRITICAL] [${source.qualifiedName}] $message")
}
enum LogLevel(
enum class LogLevel(
val weight: Int
) {
TRACE(100),
@ -44,7 +43,7 @@ object Logger {
INFO(60),
WARN(40),
ERROR(20),
CRITICAL(0)
CRITICAL(0);
fun shouldLog(level: LogLevel): Boolean {
return level.weight <= weight

View File

@ -0,0 +1,228 @@
package nl.kallestruik.darena.util
import org.bukkit.Color
import org.bukkit.Particle
import org.bukkit.World
object RenderUtil {
private const val AABB_PARTICLES_PER_BLOCK = 2
private const val PLANE_PARTICLES_PER_BLOCK = 1
fun drawXZPlane(world: World,
lowerX: Int, lowerZ: Int,
upperX: Int, upperZ: Int,
y: Int, color: Color) {
for (x in upperX downTo lowerX) {
for (z in upperZ downTo lowerZ) {
world.spawnParticle(
Particle.REDSTONE,
x + 0.5,
y + 0.5,
z + 0.5,
PLANE_PARTICLES_PER_BLOCK,
0.5,
0.0,
0.5,
Particle.DustOptions(color, 1.0F)
)
}
}
}
fun drawAABB(world: World,
lowerX: Int, lowerY: Int, lowerZ: Int,
upperX: Int, upperY: Int, upperZ: Int,
color: Color) {
// h--g
// /| /|
// e--f | y z
// | d|-c | /
// |/ |/ | /
// a--b 0 ---> x
//
// a = lower
// g = upper
// AB
for (x in upperX downTo lowerX) {
world.spawnParticle(
Particle.REDSTONE,
x + 0.5,
lowerY.toDouble(),
lowerZ.toDouble(),
AABB_PARTICLES_PER_BLOCK,
0.5,
0.0,
0.0,
Particle.DustOptions(color, 1.0F)
)
}
// AD
for (z in upperZ downTo lowerZ) {
world.spawnParticle(
Particle.REDSTONE,
lowerX.toDouble(),
lowerY.toDouble(),
z + 0.5,
AABB_PARTICLES_PER_BLOCK,
0.0,
0.0,
0.5,
Particle.DustOptions(color, 1.0F)
)
}
// AE
for (y in upperY downTo lowerY) {
world.spawnParticle(
Particle.REDSTONE,
lowerX.toDouble(),
y + 0.5,
lowerZ.toDouble(),
AABB_PARTICLES_PER_BLOCK,
0.0,
0.5,
0.0,
Particle.DustOptions(color, 1.0F)
)
}
// GH
for (x in upperX downTo lowerX) {
world.spawnParticle(
Particle.REDSTONE,
x + 0.5,
upperY + 1.0,
upperZ + 1.0,
AABB_PARTICLES_PER_BLOCK,
0.5,
0.0,
0.0,
Particle.DustOptions(color, 1.0F)
)
}
// FG
for (z in upperZ downTo lowerZ) {
world.spawnParticle(
Particle.REDSTONE,
upperX + 1.0,
upperY + 1.0,
z + 0.5,
AABB_PARTICLES_PER_BLOCK,
0.0,
0.0,
0.5,
Particle.DustOptions(color, 1.0F)
)
}
// CG
for (y in upperY downTo lowerY) {
world.spawnParticle(
Particle.REDSTONE,
upperX + 1.0,
y + 0.5,
upperZ + 1.0,
AABB_PARTICLES_PER_BLOCK,
0.0,
0.5,
0.0,
Particle.DustOptions(color, 1.0F)
)
}
// BF
for (y in upperY downTo lowerY) {
world.spawnParticle(
Particle.REDSTONE,
upperX + 1.0,
y + 0.5,
lowerZ.toDouble(),
AABB_PARTICLES_PER_BLOCK,
0.0,
0.5,
0.0,
Particle.DustOptions(color, 1.0F)
)
}
// DH
for (y in upperY downTo lowerY) {
world.spawnParticle(
Particle.REDSTONE,
lowerX.toDouble(),
y + 0.5,
upperZ + 1.0,
AABB_PARTICLES_PER_BLOCK,
0.0,
0.5,
0.0,
Particle.DustOptions(color, 1.0F)
)
}
// AF
for (x in upperX downTo lowerX) {
world.spawnParticle(
Particle.REDSTONE,
x + 0.5,
upperY + 1.0,
lowerZ.toDouble(),
AABB_PARTICLES_PER_BLOCK,
0.5,
0.0,
0.0,
Particle.DustOptions(color, 1.0F)
)
}
// AH
for (z in upperZ downTo lowerZ) {
world.spawnParticle(
Particle.REDSTONE,
lowerX.toDouble(),
upperY + 1.0,
z + 0.5,
AABB_PARTICLES_PER_BLOCK,
0.0,
0.0,
0.5,
Particle.DustOptions(color, 1.0F)
)
}
// BC
for (z in upperZ downTo lowerZ) {
world.spawnParticle(
Particle.REDSTONE,
upperX + 1.0,
lowerY.toDouble(),
z + 0.5,
AABB_PARTICLES_PER_BLOCK,
0.0,
0.0,
0.5,
Particle.DustOptions(color, 1.0F)
)
}
// CD
for (x in upperX downTo lowerX) {
world.spawnParticle(
Particle.REDSTONE,
x + 0.5,
lowerY.toDouble(),
upperZ + 1.0,
AABB_PARTICLES_PER_BLOCK,
0.5,
0.0,
0.0,
Particle.DustOptions(color, 1.0F)
)
}
}
}

View File

@ -4,4 +4,57 @@ main: nl.kallestruik.darena.DArena
api-version: 1.16
depend:
- "DLib"
commands: []
commands:
darena:
leaderboard:
permissions:
darena.debug:
default: op
darena.start:
default: op
darena.end:
default: op
darena.list:
default: op
darena.leaderboard:
default: true
darena.reload:
default: op
darena.new:
default: op
darena.edit.load:
default: op
darena.edit.save:
default: op
darena.edit.tp:
default: op
darena.edit.finish:
default: op
darena.team.create:
default: op
darena.team.join:
default: op
darena.team.leave:
default: op
darena.team.remove:
default: op
darena.team.remove-all:
default: op
darena.team.auto:
default: op
darena.team.option:
default: op
darena.team.list:
default: op
darena.points.add:
default: op
darena.points.remove:
default: op
darena.points.reset:
default: op
darena.edit.toggle.spawnplacing:
default: op
darena.edit.visualize.spawns:
default: op
darena.edit.visualize.checkpoints:
default: op

View File

@ -35,14 +35,14 @@
#loadouts:
# default:
# hotbar:
# weapon:
# 0:
# material: iron_axe
# amount: 1
# enchantments:
# name: sharpness
# level: 5
# unbreakable: true
# food:
# 1:
# material: cooked_beef
# amount: 10
# armor:
@ -72,6 +72,7 @@
# y: 10
# z: 10
# spawnRule: "label1"
# move-player: false
# checkpoint2:
# lower:
# x: 20
@ -82,6 +83,7 @@
# y: 10
# z: 30
# spawnRule: "label2"
# move-player: false
# finish:
# lower:
# x: 40
@ -92,6 +94,7 @@
# y: 10
# z: 50
# spawnRule: "spectator"
# move-player: true
#points:
# kill: 10 5
# last-man-standing: 10 5 3 0
@ -99,6 +102,11 @@
# checkpoint1: 10
# checkpoint2: 5
# finish: 20 10 5
# pools:
# maps:
# - 10 5
# - 12 6
# - 14 7
#spawnRules:
# initial:
# reuse-spawns: true
@ -107,7 +115,7 @@
# strength: 1
# duration: 10
# hidden: false
# invisability:
# invisibility:
# strength: 1
# duration: 100000000
# hidden: true
@ -118,3 +126,62 @@
# reuse-spawns: true
# spawns:
# - "spectatorSpawn"
# map1:
# reuse-spawns: false
# reuse-individual: true
# spawns:
# - "exampleSpawn1"
# - "exampleSpawn2"
# - "exampleSpawn3"
# map2:
# reuse-spawns: false
# reuse-individual: true
# spawns:
# - "exampleSpawn1"
# - "exampleSpawn2"
# - "exampleSpawn3"
# map3:
# reuse-spawns: false
# reuse-individual: true
# spawns:
# - "exampleSpawn1"
# - "exampleSpawn2"
# - "exampleSpawn3"
# map4:
# reuse-spawns: false
# reuse-individual: true
# spawns:
# - "exampleSpawn1"
# - "exampleSpawn2"
# - "exampleSpawn3"
# map5:
# reuse-spawns: false
# reuse-individual: true
# spawns:
# - "exampleSpawn1"
# - "exampleSpawn2"
# - "exampleSpawn3"
#spawn-pools:
# maps:
# times: 3
# rules:
# - map1
# - map2
# - map3
# - map4
# - map5
#borders:
# vertical:
# - CENTER 0 0
# - SET 100.0
# - WAIT 400
# - TRANSITION 80.5 200
# top:
# - SET 256.0
# bottom:
# - SET 80.0
#end-conditions:
# players-left: 1
# time: 60
# players-finished: 5

View File

@ -0,0 +1,8 @@
spawn-pos:
x: 0
y: 100
z: 0
spawn-world: world
cooldown-fireball: 30

View File

View File

@ -0,0 +1 @@
auto-teams: true