diff --git a/README.md b/README.md index e13b7fe..f665540 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ #

Volynov

-

A space artillery game designed for players that want to destroy each other and the planets around them. Fire missiles, bomb, high tech payloads, and sabotage equipment at the enemy and odge their attempts to do the same. All spaceships, planets and warheads are influenced by realistic physics to produce n-body orbits and collisions.

+

A space artillery game designed for players that want to destroy each other and the planets around them. Fire missiles, bombs, high tech payloads, and sabotage equipment at the enemy and dodge their attempts to do the same. All spaceships, planets and warheads are influenced by realistic physics to produce n-body orbits and collisions.


2020 03 01, First contact, again diff --git a/build.gradle.kts b/build.gradle.kts index 62ea56a..012e691 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,14 +4,8 @@ plugins { kotlin("jvm") version "1.3.41" } -kotlin { - experimental { - coroutines = org.jetbrains.kotlin.gradle.dsl.Coroutines.ENABLE - } -} - group = "blaarkies" -version = "0.0-SNAPSHOT" +version = "0.0.0" repositories { mavenCentral() @@ -21,13 +15,6 @@ val kotlinVersion = "1.3.10" val lwjglVersion = "3.2.3" val lwjglNatives = "natives-windows" -tasks.test { - useJUnitPlatform() - testLogging { - events("PASSED", "SKIPPED", "FAILED") - } -} - dependencies { implementation(kotlin("stdlib-jdk8")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3") @@ -39,49 +26,114 @@ dependencies { implementation(platform("org.lwjgl:lwjgl-bom:$lwjglVersion")) val lwjglList = listOf( arrayOf("lwjgl", true), -// arrayOf("lwjgl-assimp", true), -// arrayOf("lwjgl-bgfx", true), -// arrayOf("lwjgl-cuda", false), -// arrayOf("lwjgl-egl", false), + // arrayOf("lwjgl-assimp", true), + // arrayOf("lwjgl-bgfx", true), + // arrayOf("lwjgl-cuda", false), + // arrayOf("lwjgl-egl", false), arrayOf("lwjgl-glfw", true), arrayOf("lwjgl-jawt", false), -// arrayOf("lwjgl-jemalloc", true), -// arrayOf("lwjgl-libdivide", true), -// arrayOf("lwjgl-llvm", true), -// arrayOf("lwjgl-lmdb", true), -// arrayOf("lwjgl-lz4", true), -// arrayOf("lwjgl-meow", true), -// arrayOf("lwjgl-nanovg", true), -// arrayOf("lwjgl-nfd", true), -// arrayOf("lwjgl-nuklear", true), -// arrayOf("lwjgl-odbc", false), -// arrayOf("lwjgl-openal", true), -// arrayOf("lwjgl-opencl", false), + // arrayOf("lwjgl-jemalloc", true), + // arrayOf("lwjgl-libdivide", true), + // arrayOf("lwjgl-llvm", true), + // arrayOf("lwjgl-lmdb", true), + // arrayOf("lwjgl-lz4", true), + // arrayOf("lwjgl-meow", true), + // arrayOf("lwjgl-nanovg", true), + // arrayOf("lwjgl-nfd", true), + // arrayOf("lwjgl-nuklear", true), + // arrayOf("lwjgl-odbc", false), + // arrayOf("lwjgl-openal", true), + // arrayOf("lwjgl-opencl", false), arrayOf("lwjgl-opengl", true), -// arrayOf("lwjgl-opengles", true), -// arrayOf("lwjgl-openvr", true), -// arrayOf("lwjgl-opus", true), -// arrayOf("lwjgl-ovr", true), -// arrayOf("lwjgl-par", true), -// arrayOf("lwjgl-remotery", true), -// arrayOf("lwjgl-rpmalloc", true), -// arrayOf("lwjgl-shaderc", true), -// arrayOf("lwjgl-sse", true), + // arrayOf("lwjgl-opengles", true), + // arrayOf("lwjgl-openvr", true), + // arrayOf("lwjgl-opus", true), + // arrayOf("lwjgl-ovr", true), + // arrayOf("lwjgl-par", true), + // arrayOf("lwjgl-remotery", true), + // arrayOf("lwjgl-rpmalloc", true), + // arrayOf("lwjgl-shaderc", true), + // arrayOf("lwjgl-sse", true), arrayOf("lwjgl-stb", true) -// arrayOf("lwjgl-tinyexr", true), -// arrayOf("lwjgl-tinyfd", true), -// arrayOf("lwjgl-tootle", true), -// arrayOf("lwjgl-vma", true), -// arrayOf("lwjgl-vulkan", false), -// arrayOf("lwjgl-xxhash", true), -// arrayOf("lwjgl-yoga", true), -// arrayOf("lwjgl-zstd", true) + // arrayOf("lwjgl-tinyexr", true), + // arrayOf("lwjgl-tinyfd", true), + // arrayOf("lwjgl-tootle", true), + // arrayOf("lwjgl-vma", true), + // arrayOf("lwjgl-vulkan", false), + // arrayOf("lwjgl-xxhash", true), + // arrayOf("lwjgl-yoga", true), + // arrayOf("lwjgl-zstd", true) ) lwjglList.forEach { implementation("org.lwjgl", it[0].toString()) } lwjglList.filter { it[1] == true } .forEach { runtimeOnly("org.lwjgl", it[0].toString(), classifier = lwjglNatives) } } -tasks.withType { - kotlinOptions.jvmTarget = "11" +sourceSets { + main { + resources { + exclude("textures", "fonts") + } + } +} + +tasks { + + withType { + kotlinOptions.jvmTarget = "1.8" + } + + register("zipFolder") { + from("$buildDir/Volynov-$version") + destinationDirectory.set(File("$buildDir/")) + } + + task("renameFolder") { + mustRunAfter("createBat") + doLast { + file("$buildDir/libs").renameTo(file("$buildDir/Volynov-$version")) + } + } + + task("createBat") { + mustRunAfter("uberJar") + doLast { + File("$buildDir/libs/run.bat") + .writeText("""|chcp 65001 + |java -Dfile.encoding=UTF-8 -Dorg.lwjgl.util.Debug=true -jar ./volynov-$version-uber.jar + |pause + """.trimMargin()) + } + } + + register("copyTextures") { + from("$projectDir/src/main/resources") + exclude("shaders") + into("$buildDir/libs") + } + + register("uberJar") { + dependsOn(configurations.runtimeClasspath) + + archiveClassifier.set("uber") + manifest { attributes["Main-Class"] = "MainKt" } + + from(sourceSets.main.get().output) + from({ + configurations.runtimeClasspath.get() + .filter { it.name.endsWith("jar") } + .map { zipTree(it) } + }) + } + + task("package") { + group = "build" + dependsOn("clean", "uberJar", "copyTextures", "createBat", "renameFolder", "zipFolder") + } +} + +tasks.test { + group = "build" + useJUnitPlatform() + testLogging { events("PASSED", "SKIPPED", "FAILED") } } diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index d9a7f1b..f4ad33a 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -6,7 +6,7 @@ import kotlin.system.exitProcess fun main() = runBlocking { try { val gameLogic: IGameLogic = AppLogic() - val gameEngine = AppRunner("Volynov", 700, 700, true, gameLogic) + val gameEngine = AppRunner("Volynov", 1920, 1080, true, gameLogic) gameEngine.run() } catch (exception: Exception) { exception.printStackTrace() diff --git a/src/main/kotlin/app/AppLogic.kt b/src/main/kotlin/app/AppLogic.kt index 04a66e8..9a7db9c 100644 --- a/src/main/kotlin/app/AppLogic.kt +++ b/src/main/kotlin/app/AppLogic.kt @@ -13,16 +13,15 @@ class AppLogic : IGameLogic { private val renderer = Renderer() private val gameState = GameState() - private val textures = TextureHolder() - private val drawer = Drawer(renderer, textures) - private val gamePhaseHandler = GamePhaseHandler(gameState, drawer, textures) + private val drawer = Drawer(renderer) + private val gamePhaseHandler = GamePhaseHandler(gameState, drawer) private val inputHandler = InputHandler(gamePhaseHandler) @Throws(Exception::class) override fun init(window: Window) { gameState.init(window) renderer.init(gameState.camera) - textures.init() + drawer.init() gamePhaseHandler.init(window) inputHandler.init(window) } diff --git a/src/main/kotlin/display/Window.kt b/src/main/kotlin/display/Window.kt index f9eb144..62140ef 100644 --- a/src/main/kotlin/display/Window.kt +++ b/src/main/kotlin/display/Window.kt @@ -1,16 +1,21 @@ package display import display.events.MouseButtonEvent +import display.events.MouseScrollEvent import io.reactivex.subjects.PublishSubject import org.jbox2d.common.Vec2 import org.lwjgl.BufferUtils import org.lwjgl.glfw.GLFW +import org.lwjgl.glfw.GLFW.glfwGetKey +import org.lwjgl.glfw.GLFW.glfwGetKeyName import org.lwjgl.glfw.GLFWErrorCallback import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL11 import org.lwjgl.opengl.GL11.* import org.lwjgl.system.Callback import org.lwjgl.system.MemoryUtil +import utility.Common.makeVec2 +import java.nio.DoubleBuffer class Window(private val title: String, var width: Int, var height: Int, private var vSync: Boolean) { @@ -21,7 +26,8 @@ class Window(private val title: String, var width: Int, var height: Int, private val keyboardEvent = PublishSubject.create() val mouseButtonEvent = PublishSubject.create() val cursorPositionEvent = PublishSubject.create() - val mouseScrollEvent = PublishSubject.create() + val mouseScrollEvent = PublishSubject.create() + val textInputEvent = PublishSubject.create() fun init() { // Setup an error callback. The default implementation @@ -41,10 +47,11 @@ class Window(private val title: String, var width: Int, var height: Int, private GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, 4) // anti-aliasing // Create the window - windowHandle = GLFW.glfwCreateWindow(width, height, title, MemoryUtil.NULL, MemoryUtil.NULL) - if (windowHandle == MemoryUtil.NULL) { - throw RuntimeException("Failed to create the GLFW window") - } + windowHandle = GLFW.glfwCreateWindow(width, height, title, GLFW.glfwGetPrimaryMonitor(), MemoryUtil.NULL) + // windowHandle = GLFW.glfwCreateWindow(width, height, title, MemoryUtil.NULL, MemoryUtil.NULL) + // if (windowHandle == MemoryUtil.NULL) { + // throw RuntimeException("Failed to create the GLFW window") + // } // Setup resize callback GLFW.glfwSetFramebufferSizeCallback(windowHandle) { _, width, height -> this.width = width @@ -52,9 +59,10 @@ class Window(private val title: String, var width: Int, var height: Int, private } // Get the resolution of the primary monitor - val videoMode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor())!! + // val videoMode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor())!! // Center our window - GLFW.glfwSetWindowPos(windowHandle, (videoMode.width() - width) / 2, (videoMode.height() - height) / 2) + // GLFW.glfwSetWindowPos(windowHandle, (videoMode.width() - width) / 2, (videoMode.height() - height) / 2) + GLFW.glfwMakeContextCurrent(windowHandle) // Make the OpenGL context current if (isVSync()) { // Enable v-sync GLFW.glfwSwapInterval(1) @@ -67,7 +75,7 @@ class Window(private val title: String, var width: Int, var height: Int, private } private fun setupInputCallbacks() { - GLFW.glfwSetKeyCallback(windowHandle) { window, key, scancode, action, mods -> + GLFW.glfwSetKeyCallback(windowHandle) { _, key, scancode, action, mods -> if (action == GLFW.GLFW_PRESS) { when { key == GLFW.GLFW_KEY_F12 -> { @@ -82,20 +90,22 @@ class Window(private val title: String, var width: Int, var height: Int, private } }?.let { callbacks.add(it) } - GLFW.glfwSetMouseButtonCallback(windowHandle) { window, button, action, mods -> + GLFW.glfwSetMouseButtonCallback(windowHandle) { _, button, action, mods -> mouseButtonEvent.onNext(MouseButtonEvent(button, action, mods, getCursorPosition())) }?.let { callbacks.add(it) } - GLFW.glfwSetCursorPosCallback(windowHandle) { window, xPos, yPos -> - cursorPositionEvent.onNext(Vec2(xPos.toFloat(), yPos.toFloat())) + GLFW.glfwSetCursorPosCallback(windowHandle) { _, xPos, yPos -> + cursorPositionEvent.onNext(makeVec2(xPos, yPos)) }?.let { callbacks.add(it) } - GLFW.glfwSetScrollCallback(windowHandle) { window, xOffset, yOffset -> - mouseScrollEvent.onNext(Vec2(xOffset.toFloat(), yOffset.toFloat())) + GLFW.glfwSetScrollCallback(windowHandle) { _, xOffset, yOffset -> + mouseScrollEvent.onNext(MouseScrollEvent(makeVec2(xOffset, yOffset), getCursorPosition())) }?.let { callbacks.add(it) } -// glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); -// glfwSetCharCallback(window, character_callback); + GLFW.glfwSetCharCallback(windowHandle) { _, codepoint -> + textInputEvent.onNext(Character.toChars(codepoint)[0].toString()) + }?.let { callbacks.add(it) } + // glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); } fun setClearColor(r: Float, g: Float, b: Float, alpha: Float) { @@ -120,7 +130,7 @@ class Window(private val title: String, var width: Int, var height: Int, private val y = BufferUtils.createDoubleBuffer(1) GLFW.glfwGetCursorPos(windowHandle, x, y) - return Vec2(x.get().toFloat(), y.get().toFloat()) + return makeVec2(x, y) } fun exit() { diff --git a/src/main/kotlin/display/draw/Drawer.kt b/src/main/kotlin/display/draw/Drawer.kt index 1fa13d4..a2b492b 100644 --- a/src/main/kotlin/display/draw/Drawer.kt +++ b/src/main/kotlin/display/draw/Drawer.kt @@ -3,40 +3,55 @@ package display.draw import display.graphic.BasicShapes import display.graphic.Color import display.graphic.Renderer -import display.graphic.Texture +import display.graphic.SnipRegion +import display.text.TextJustify import engine.freeBody.FreeBody +import engine.freeBody.Particle +import engine.freeBody.Vehicle import engine.physics.CellLocation import engine.physics.GravityCell +import game.GamePlayer import org.jbox2d.common.Vec2 +import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.GL12 +import utility.Common.makeVec2 +import utility.Common.makeVec2Circle +import utility.Common.vectorUnit import java.util.* import kotlin.math.sqrt -class Drawer(val renderer: Renderer, val textures: TextureHolder) { +class Drawer(val renderer: Renderer) { - fun drawDebugForces(freeBody: FreeBody) { - val x = freeBody.worldBody.position.x - val y = freeBody.worldBody.position.y - val accelerationX = freeBody.worldBody.m_force.x - val accelerationY = freeBody.worldBody.m_force.y - - val multiplier = 2000f - val linePoints = listOf( - x, - y, - x + accelerationX * multiplier, - y + accelerationY * multiplier - ) - val triangleStripPoints = BasicShapes.getLineTriangleStrip(linePoints, 2f) - val arrowHeadPoints = BasicShapes.getArrowHeadPoints(linePoints) - val data = getColoredData( - triangleStripPoints + arrowHeadPoints, - Color(0f, 1f, 1f, 1f), Color(0f, 1f, 1f, 0.0f) - ).toFloatArray() + val textures = TextureHolder() - textures.white_pixel.bind() -// renderer.drawStrip(data) + fun init() { + textures.init() + } - renderer.drawText(freeBody.id, freeBody.worldBody.position, Vec2(1f, 1f), Color.WHITE) + fun drawDebugForces(freeBody: FreeBody) { + // val x = freeBody.worldBody.position.x + // val y = freeBody.worldBody.position.y + // val accelerationX = freeBody.worldBody.m_force.x + // val accelerationY = freeBody.worldBody.m_force.y + // + // val multiplier = 2000f + // val linePoints = listOf( + // x, + // y, + // x + accelerationX * multiplier, + // y + accelerationY * multiplier + // ) + // val triangleStripPoints = BasicShapes.getLineTriangleStrip(linePoints, .2f) + // val arrowHeadPoints = BasicShapes.getArrowHeadPoints(linePoints) + // val data = getColoredData( + // triangleStripPoints + arrowHeadPoints, + // Color(0f, 1f, 1f, 1f), Color(0f, 1f, 1f, 0.0f) + // ).toFloatArray() + + textures.getTexture(TextureEnum.white_pixel).bind() + // renderer.drawStrip(data) + + renderer.drawText(freeBody.id, freeBody.worldBody.position, vectorUnit, Color.WHITE, TextJustify.LEFT) } fun drawTrail(freeBody: FreeBody) { @@ -45,24 +60,28 @@ class Drawer(val renderer: Renderer, val textures: TextureHolder) { if (linePoints.size < 4) { return } - val data = getLine(linePoints, Color(0.4f, 0.7f, 1f, 0.5f), Color.TRANSPARENT, .1f, 0f) + val trailColor = when (freeBody) { + is Vehicle -> freeBody.textureConfig.color.setAlpha(.3f) + else -> Color(.4f, .7f, 1f, .5f) + } + val data = getLine(linePoints, trailColor, Color.TRANSPARENT, .1f, 0f) - textures.white_pixel.bind() + textures.getTexture(TextureEnum.white_pixel).bind() renderer.drawStrip(data) } fun drawFreeBody(freeBody: FreeBody) { - freeBody.textureConfig.texture.bind() + textures.getTexture(freeBody.textureConfig.texture).bind() renderer.drawShape( freeBody.textureConfig.gpuBufferData, freeBody.worldBody.position, freeBody.worldBody.angle, - Vec2(freeBody.radius, freeBody.radius) + vectorUnit.mul(freeBody.radius) ) } fun drawGravityCells(gravityMap: HashMap, resolution: Float) { - textures.white_pixel.bind() + textures.getTexture(TextureEnum.white_pixel).bind() val maxMass = gravityMap.maxBy { (_, cell) -> cell.totalMass }!!.value.totalMass val scale = 0.707106781f * resolution gravityMap.forEach { (key, cell) -> @@ -74,12 +93,12 @@ class Drawer(val renderer: Renderer, val textures: TextureHolder) { (it[0] / 2 - 0.5f), (it[1] / 2 - 0.5f) ) }.toFloatArray() - renderer.drawShape(data, Vec2(key.x * resolution, key.y * resolution), 0f, Vec2(scale, scale)) + renderer.drawShape(data, makeVec2(key.x, key.y).mul(resolution), 0f, makeVec2(scale)) } } - fun drawPicture(texture: Texture, scale: Vec2 = Vec2(1f, 1f), offset: Vec2 = Vec2()) { - texture.bind() + fun drawBackground(textureEnum: TextureEnum, scale: Vec2 = vectorUnit, offset: Vec2 = Vec2()) { + val texture = textures.getTexture(textureEnum).bind() val left = -texture.width / 2f val right = texture.width / 2f @@ -91,12 +110,69 @@ class Drawer(val renderer: Renderer, val textures: TextureHolder) { listOf( it[0], it[1], 0f, 1f, 1f, 1f, 1f, - (it[0] / 2 - 0.5f) * scale.x + offset.x, - (it[1] / 2 - 0.5f) * scale.y + offset.y + (it[0] / 2f - 0.5f) * scale.x + offset.x, + (it[1] / 2f - 0.5f) * scale.y + offset.y + ) + }.toFloatArray() + + renderer.drawShape(data, scale = vectorUnit.mul(45f)) + } + + fun drawIcon(textureEnum: TextureEnum, + scale: Vec2 = vectorUnit, + offset: Vec2 = Vec2(), + color: Color + ) { + val texture = textures.getTexture(textureEnum).bind() + + val left = -texture.width / 2f + val right = texture.width / 2f + val top = texture.height / 2f + val bottom = -texture.height / 2f + + val data = listOf(left, bottom, left, top, right, top, right, bottom).chunked(2) + .flatMap { + listOf( + it[0], it[1], 0f, + color.red, color.green, color.blue, color.alpha, + (it[0] / 2f - 0.5f), + (it[1] / 2f - 0.5f) ) }.toFloatArray() - renderer.drawShape(data, scale = Vec2(1f, 1f).mul(45f)) + renderer.drawShape(data, offset, 0f, scale, useCamera = false, + snipRegion = SnipRegion(offset.add(scale.negate()), scale.mul(2f))) + } + + fun drawPlayerAimingPointer(player: GamePlayer) { + val playerLocation = player.vehicle!!.worldBody.position + val angle = player.playerAim.angle + val aimLocation = makeVec2Circle(angle).mul(player.playerAim.power / 10f) + + val linePoints = listOf( + playerLocation.x, + playerLocation.y, + playerLocation.x + aimLocation.x, + playerLocation.y + aimLocation.y + ) + val triangleStripPoints = BasicShapes.getLineTriangleStrip(linePoints, .2f) + val arrowHeadPoints = BasicShapes.getArrowHeadPoints(linePoints, .5f) + val data = getColoredData( + triangleStripPoints + arrowHeadPoints, Color.RED.setAlpha(.5f), Color.RED.setAlpha(.1f) + ).toFloatArray() + + textures.getTexture(TextureEnum.white_pixel).bind() + renderer.drawStrip(data) + } + + fun drawParticle(particle: Particle) { + textures.getTexture(particle.textureConfig.texture).bind() + renderer.drawShape( + particle.textureConfig.gpuBufferData, + particle.worldBody.position, + particle.worldBody.angle, + vectorUnit.mul(particle.radius) + ) } companion object { @@ -118,7 +194,8 @@ class Drawer(val renderer: Renderer, val textures: TextureHolder) { chunk[0], chunk[1], 0f, /* pos*/ color.red, color.green, color.blue, color.alpha, /* color*/ 0f, 0f /* texture*/ - ) } + ) + } } fun getLine( diff --git a/src/main/kotlin/display/draw/TextureConfig.kt b/src/main/kotlin/display/draw/TextureConfig.kt index d8d3418..4d52a83 100644 --- a/src/main/kotlin/display/draw/TextureConfig.kt +++ b/src/main/kotlin/display/draw/TextureConfig.kt @@ -1,23 +1,29 @@ package display.draw -import Vector2f -import display.graphic.Texture +import display.graphic.Color +import org.jbox2d.common.Vec2 +import utility.Common.vectorUnit class TextureConfig( - val texture: Texture, val scale: Vector2f = Vector2f(1f, 1f), val offset: Vector2f = Vector2f(), - var chunkedVertices: List> = listOf(), var gpuBufferData: FloatArray = floatArrayOf() + var texture: TextureEnum, + val scale: Vec2 = vectorUnit, + val offset: Vec2 = Vec2(), + var chunkedVertices: List> = listOf(), + var gpuBufferData: FloatArray = floatArrayOf(), + val color: Color = Color.WHITE ) { - fun updateGpuBufferData() { + fun updateGpuBufferData(): TextureConfig { gpuBufferData = chunkedVertices.flatMap { val (x, y) = it listOf( x, y, 0f, - 1f, 1f, 1f, 1f, + color.red, color.green, color.blue, color.alpha, (x * .5f - 0.5f) * scale.x + offset.x, (y * .5f - 0.5f) * scale.y + offset.y ) }.toFloatArray() + return this } } diff --git a/src/main/kotlin/display/draw/TextureEnum.kt b/src/main/kotlin/display/draw/TextureEnum.kt new file mode 100644 index 0000000..a679399 --- /dev/null +++ b/src/main/kotlin/display/draw/TextureEnum.kt @@ -0,0 +1,11 @@ +package display.draw + +enum class TextureEnum { + marble_earth, + full_moon, + metal, + pavement, + white_pixel, + stars_2k, + icon_aim +} diff --git a/src/main/kotlin/display/draw/TextureHolder.kt b/src/main/kotlin/display/draw/TextureHolder.kt index 4b98944..86c3b8a 100644 --- a/src/main/kotlin/display/draw/TextureHolder.kt +++ b/src/main/kotlin/display/draw/TextureHolder.kt @@ -4,20 +4,22 @@ import display.graphic.Texture class TextureHolder { - lateinit var marble_earth: Texture - lateinit var full_moon: Texture - lateinit var metal: Texture - lateinit var pavement: Texture - lateinit var white_pixel: Texture - lateinit var stars_2k: Texture + private val textureHashMap = HashMap() fun init() { - marble_earth = Texture.loadTexture("src\\main\\resources\\textures\\marble_earth.png") - full_moon = Texture.loadTexture("src\\main\\resources\\textures\\full_moon.png") - metal = Texture.loadTexture("src\\main\\resources\\textures\\metal.png") - pavement = Texture.loadTexture("src\\main\\resources\\textures\\pavement.png") - white_pixel = Texture.loadTexture("src\\main\\resources\\textures\\white_pixel.png") - stars_2k = Texture.loadTexture("src\\main\\resources\\textures\\stars_2k.png") + val hMap = textureHashMap + hMap[TextureEnum.marble_earth] = Texture.loadTexture("./textures/marble_earth.png") + hMap[TextureEnum.full_moon] = Texture.loadTexture("./textures/full_moon.png") + hMap[TextureEnum.metal] = Texture.loadTexture("./textures/metal.png") + hMap[TextureEnum.pavement] = Texture.loadTexture("./textures/pavement.png") + hMap[TextureEnum.white_pixel] = Texture.loadTexture("./textures/white_pixel.png") + hMap[TextureEnum.stars_2k] = Texture.loadTexture("./textures/stars_2k.png") + hMap[TextureEnum.icon_aim] = Texture.loadTexture("./textures/icon_aim.png") + } + + fun getTexture(texture: TextureEnum): Texture = textureHashMap[texture].let { + checkNotNull(it) { "Cannot find texture $texture" } + it } } diff --git a/src/main/kotlin/display/events/MouseScrollEvent.kt b/src/main/kotlin/display/events/MouseScrollEvent.kt new file mode 100644 index 0000000..f949e5e --- /dev/null +++ b/src/main/kotlin/display/events/MouseScrollEvent.kt @@ -0,0 +1,8 @@ +package display.events + +import org.jbox2d.common.Vec2 + +class MouseScrollEvent( + val movement: Vec2, + val location: Vec2 +) diff --git a/src/main/kotlin/display/graphic/BasicShapes.kt b/src/main/kotlin/display/graphic/BasicShapes.kt index 3655aa1..1f18aaa 100644 --- a/src/main/kotlin/display/graphic/BasicShapes.kt +++ b/src/main/kotlin/display/graphic/BasicShapes.kt @@ -23,19 +23,31 @@ object BasicShapes { val square = polygon4.map { it * sqrt(2f) } - private fun getPolygonVertices(corners: Int): List = (0 until corners).flatMap { - val t = 2 * PI * (it / corners.toFloat()) + PI * .25 + val polygon4Spiked = getSpikedPolygon(8) + + val verticalLine = listOf(0f, 1f, 0f, -1f) + + private fun getPolygonVertices(corners: Int, rotate: Double = .25): List = (0 until corners).flatMap { + val t = 2 * PI * (it / corners.toFloat()) + PI * rotate listOf(cos(t).toFloat(), sin(t).toFloat()) } - fun getArrowHeadPoints(linePoints: List): List { + private fun getSpikedPolygon(corners: Int, smoothness: Float = .6f): List { + return getPolygonVertices(corners).chunked(2) + .withIndex() + .flatMap { (index, vertex) -> + val scale = (if (index.rem(2) == 0) 1f else smoothness) + listOf(vertex[0] * scale, vertex[1] * scale) + } + } + + fun getArrowHeadPoints(linePoints: List, headSize: Float = 1f): List { val (ax, ay, bx, by) = linePoints val normalY = bx - ax val normalX = -by + ay val magnitude = Director.getDistance(normalX, normalY) - val headSize = 5f val x = headSize * normalX / magnitude val y = headSize * normalY / magnitude return listOf( diff --git a/src/main/kotlin/display/graphic/Color.kt b/src/main/kotlin/display/graphic/Color.kt index 6e3ce0b..1900ecc 100644 --- a/src/main/kotlin/display/graphic/Color.kt +++ b/src/main/kotlin/display/graphic/Color.kt @@ -1,6 +1,18 @@ package display.graphic -class Color(val red: Float = 0f, val green: Float = 0f, val blue: Float = 0f, val alpha: Float = 0f) { +class Color { + + val red: Float + val green: Float + val blue: Float + val alpha: Float + + constructor(red: Float = 0f, green: Float = 0f, blue: Float = 0f, alpha: Float = 1f) { + this.red = getSafeValue(red) + this.green = getSafeValue(green) + this.blue = getSafeValue(blue) + this.alpha = getSafeValue(alpha) + } operator fun times(value: Float) = Color(this.red * value, this.green * value, this.blue * value, this.alpha * value) @@ -15,9 +27,18 @@ class Color(val red: Float = 0f, val green: Float = 0f, val blue: Float = 0f, va getIntToFloat(alpha) ) + constructor(hexValue: String) { + val (red, green, blue, alpha) = hexValue.dropWhile { it == '#' }.chunked(2) + this.red = getHexToFloat(red) + this.green = getHexToFloat(green) + this.blue = getHexToFloat(blue) + this.alpha = getHexToFloat(alpha) + } + fun setAlpha(newValue: Float): Color = Color(red, green, blue, newValue) companion object { + val WHITE = Color(1f, 1f, 1f, 1f) val BLACK = Color(0f, 0f, 0f, 1f) val RED = Color(1f, 0f, 0f, 1f) @@ -25,8 +46,47 @@ class Color(val red: Float = 0f, val green: Float = 0f, val blue: Float = 0f, va val BLUE = Color(0f, 0f, 1f, 1f) val TRANSPARENT = Color(0f, 0f, 0f, 0f) + val HEX = Color("#01FF6499") + val HSV = createFromHsv(.5f, 1f, .5f, 1f) + + val PALETTE8 = (0..8).map { createFromHsv(it / 7f, 1f, .5f) } + val PALETTE_TINT10 = + (0..8).map { createFromHsv(it / 7f, 1f, .8f) } + listOf(WHITE, createFromHsv(0f, 0f, .7f)) + private fun getSafeValue(value: Float) = value.coerceIn(0f, 1f) private fun getIntToFloat(value: Int) = getSafeValue(value / 255f) + + private fun getHexToFloat(value: String) = value.toShort(16).toFloat().let { getSafeValue(it / 255f) } + + fun createFromHsv(hue: Float = 0f, saturation: Float = 0f, light: Float = 0f, alpha: Float = 1f): Color { + val rgbList = hslToRgb(hue, saturation, light) + return Color(rgbList[0], rgbList[1], rgbList[2], alpha) + } + + private fun hslToRgb(hue: Float, saturation: Float, light: Float): List { + return if (saturation == 0f) { + listOf(light, light, light) + } else { + val q = if (light < .5f) light * (1 + saturation) else light + saturation - light * saturation + val p = 2 * light - q + listOf( + hueToRgb(p, q, hue + 1f / 3f), + hueToRgb(p, q, hue), + hueToRgb(p, q, hue - 1f / 3f) + ) + } + } + + private fun hueToRgb(p: Float, q: Float, t: Float): Float { + var t = t + if (t < 0f) t += 1f + if (t > 1f) t -= 1f + if (t < 1f / 6f) return p + (q - p) * 6f * t + if (t < 1f / 2f) return q + return if (t < 2f / 3f) p + (q - p) * (2f / 3f - t) * 6f else p + } + } + } diff --git a/src/main/kotlin/display/graphic/Renderer.kt b/src/main/kotlin/display/graphic/Renderer.kt index 9acbecc..8320a4c 100644 --- a/src/main/kotlin/display/graphic/Renderer.kt +++ b/src/main/kotlin/display/graphic/Renderer.kt @@ -1,8 +1,9 @@ package display.graphic import Matrix4f -import input.CameraView import display.text.Font +import display.text.TextJustify +import input.CameraView import org.jbox2d.common.Vec2 import org.lwjgl.opengl.GL11.* import org.lwjgl.opengl.GL15.GL_ARRAY_BUFFER @@ -11,6 +12,8 @@ import org.lwjgl.opengl.GL20.GL_FRAGMENT_SHADER import org.lwjgl.opengl.GL20.GL_VERTEX_SHADER import org.lwjgl.system.MemoryUtil import utility.Common +import utility.Common.getSafePath +import utility.Common.vectorUnit import java.awt.FontFormatException import java.io.FileInputStream import java.io.IOException @@ -20,7 +23,7 @@ import java.util.logging.Logger class Renderer { - var debugOffset: Vec2 = Vec2(1f, 1f) + var debugOffset: Vec2 = vectorUnit private lateinit var vao: VertexArrayObject private lateinit var vbo: VertexBufferObject @@ -40,7 +43,8 @@ class Renderer { glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) font = try { - Font(FileInputStream("src\\main\\resources\\fonts\\ALBMT___.TTF"), 80) + val fontPath = getSafePath("./fonts/ALBMT___.TTF") + Font(FileInputStream(fontPath), 80) } catch (ex: FontFormatException) { Logger.getLogger(Renderer::class.java.name).log(Level.CONFIG, null, ex) Font() @@ -80,25 +84,33 @@ class Renderer { numVertices = 0 } - fun drawText(text: CharSequence, offset: Vec2, scale: Vec2, color: Color, useCamera: Boolean = true) = - font.drawText(this, text, offset, scale, color, useCamera) + fun drawText(text: CharSequence, + offset: Vec2, + scale: Vec2, + color: Color, + justify: TextJustify = TextJustify.LEFT, + useCamera: Boolean = true, + snipRegion: SnipRegion? = null + ) = font.drawText(this, text, offset, scale, color, justify, useCamera, snipRegion) fun drawShape( data: FloatArray, offset: Vec2 = Vec2(), h: Float = 0f, - scale: Vec2 = Vec2(1f, 1f), - useCamera: Boolean = true - ) = drawEntity(data, offset, h, scale, GL_TRIANGLE_FAN, useCamera) + scale: Vec2 = vectorUnit, + useCamera: Boolean = true, + snipRegion: SnipRegion? = null + ) = drawEntity(data, offset, h, scale, GL_TRIANGLE_FAN, useCamera, snipRegion) fun drawStrip( data: FloatArray, offset: Vec2 = Vec2(), h: Float = 0f, - scale: Vec2 = Vec2(1f, 1f), - useCamera: Boolean = true + scale: Vec2 = vectorUnit, + useCamera: Boolean = true, + snipRegion: SnipRegion? = null ) = - drawEntity(data, offset, h, scale, GL_TRIANGLE_STRIP, useCamera) + drawEntity(data, offset, h, scale, GL_TRIANGLE_STRIP, useCamera, snipRegion) private fun drawEntity( data: FloatArray, @@ -106,7 +118,8 @@ class Renderer { h: Float, scale: Vec2, drawType: Int, - useCamera: Boolean + useCamera: Boolean, + snipRegion: SnipRegion? ) { begin() if (vertices.remaining() < data.size) { @@ -117,7 +130,7 @@ class Renderer { vertices.put(data) numVertices += data.size / vertexDimensionCount - setUniformInputs(offset, 0f, h, scale, useCamera) + setUniformInputs(offset, 0f, h, scale, useCamera, snipRegion) end(drawType) } @@ -155,11 +168,17 @@ class Renderer { offset: Vec2 = Vec2(), z: Float = 0f, h: Float = 0f, - scale: Vec2 = Vec2(1f, 1f), - useCamera: Boolean + scale: Vec2 = vectorUnit, + useCamera: Boolean, + snipRegion: SnipRegion? ) { -// val uniTex = program!!.getUniformLocation("texImage") -// program!!.setUniform(uniTex, 0) + // val uniTex = program!!.getUniformLocation("texImage") + // program!!.setUniform(uniTex, 0) + + val gameCamera = cameraView.getRenderCamera() + val guiCamera = Matrix4f() + glDisable(GL_SCISSOR_TEST) + val model = Matrix4f.translate(offset.x, offset.y, z) .multiply(Matrix4f.rotate(h * Common.radianToDegree, 0f, 0f, 1f)) @@ -167,11 +186,17 @@ class Renderer { val uniModel = program.getUniformLocation("model") program.setUniform(uniModel, model) - val zoomScale = 1f / cameraView.z val view = when (useCamera) { - true -> Matrix4f.scale(zoomScale, zoomScale, 1f) - .multiply(Matrix4f.translate(-cameraView.location.x, -cameraView.location.y, 0f)) - false -> Matrix4f() + true -> gameCamera + false -> { + if (snipRegion != null) { + glScissor(cameraView.windowWidth.div(2).toInt() + snipRegion.offset.x.toInt(), + cameraView.windowHeight.div(2).toInt() + snipRegion.offset.y.toInt(), + snipRegion.scale.x.toInt(), snipRegion.scale.y.toInt()) + glEnable(GL_SCISSOR_TEST) + } + guiCamera + } } val uniView = program.getUniformLocation("view") program.setUniform(uniView, view) @@ -202,3 +227,4 @@ class Renderer { program.pointVertexAttribute(texAttribute, 2, 9 * java.lang.Float.BYTES, 7 * java.lang.Float.BYTES) } } + diff --git a/src/main/kotlin/display/graphic/SnipRegion.kt b/src/main/kotlin/display/graphic/SnipRegion.kt new file mode 100644 index 0000000..77a7fb8 --- /dev/null +++ b/src/main/kotlin/display/graphic/SnipRegion.kt @@ -0,0 +1,5 @@ +package display.graphic + +import org.jbox2d.common.Vec2 + +class SnipRegion(val offset: Vec2, val scale: Vec2) diff --git a/src/main/kotlin/display/graphic/Texture.kt b/src/main/kotlin/display/graphic/Texture.kt index b09660c..db28d7f 100644 --- a/src/main/kotlin/display/graphic/Texture.kt +++ b/src/main/kotlin/display/graphic/Texture.kt @@ -4,6 +4,8 @@ import org.lwjgl.BufferUtils import org.lwjgl.opengl.GL11.* import org.lwjgl.stb.STBImage.* import org.lwjgl.system.MemoryStack +import utility.Common.getSafePath +import java.io.File import java.nio.ByteBuffer class Texture { @@ -62,7 +64,9 @@ class Texture { return texture } - fun loadTexture(path: String): Texture { + fun loadTexture(resourcePath: String): Texture { + val safePath = getSafePath(resourcePath) + var image = BufferUtils.createByteBuffer(0) var width = 0 var height = 0 @@ -72,14 +76,15 @@ class Texture { val comp = stack.mallocInt(1) stbi_set_flip_vertically_on_load(true) - image = stbi_load(path, w, h, comp, 0) - ?: throw RuntimeException("Failed to load a texture file!\n${stbi_failure_reason()}") + image = stbi_load(safePath, w, h, comp, 0) + ?: throw RuntimeException("Cannot load texture file at $safePath \n${stbi_failure_reason()}") width = w.get() height = h.get() } return createTexture(width, height, image) } + } } diff --git a/src/main/kotlin/display/gui/GuiButton.kt b/src/main/kotlin/display/gui/GuiButton.kt index 7a1c241..6b97b32 100644 --- a/src/main/kotlin/display/gui/GuiButton.kt +++ b/src/main/kotlin/display/gui/GuiButton.kt @@ -1,9 +1,13 @@ package display.gui import display.draw.Drawer +import display.draw.TextureEnum import display.graphic.BasicShapes import display.graphic.Color +import display.graphic.SnipRegion +import display.text.TextJustify import org.jbox2d.common.Vec2 +import utility.Common.vectorUnit class GuiButton( drawer: Drawer, @@ -12,8 +16,9 @@ class GuiButton( title: String, textSize: Float = .2f, color: Color = Color.WHITE.setAlpha(.7f), - private val onClick: () -> Unit = {} -) : GuiElement(drawer, offset, scale, title, textSize, color) { + private val onClick: () -> Unit = {}, + updateCallback: (GuiElement) -> Unit = {} +) : GuiElement(drawer, offset, scale, title, textSize, color, updateCallback) { private var buttonOutline: FloatArray private var buttonBackground: FloatArray @@ -29,27 +34,29 @@ class GuiButton( calculateElementRegion(this) } - override fun render() { - drawer.textures.white_pixel.bind() + override fun render(snipRegion: SnipRegion?) { + drawer.textures.getTexture(TextureEnum.white_pixel).bind() when (currentPhase) { - GuiElementPhases.HOVERED -> drawer.renderer.drawShape(buttonBackground, offset, useCamera = false) + GuiElementPhases.HOVERED -> drawer.renderer.drawShape(buttonBackground, offset, useCamera = false, + snipRegion = snipRegion) } when (currentPhase) { GuiElementPhases.CLICKED -> drawer.renderer.drawStrip( buttonOutline, offset.add(Vec2(0f, -2f)), - useCamera = false + useCamera = false, + snipRegion = snipRegion ) - else -> drawer.renderer.drawStrip(buttonOutline, offset, useCamera = false) + else -> drawer.renderer.drawStrip(buttonOutline, offset, useCamera = false, snipRegion = snipRegion) } - GuiElement.drawLabel(drawer, this) - super.render() + super.render(snipRegion) + drawLabel(drawer, this, TextJustify.CENTER, snipRegion) } - override fun handleClick(location: Vec2) { + override fun handleLeftClick(location: Vec2) { when { isHover(location) -> { currentPhase = GuiElementPhases.CLICKED @@ -59,6 +66,4 @@ class GuiButton( } } - - } diff --git a/src/main/kotlin/display/gui/GuiController.kt b/src/main/kotlin/display/gui/GuiController.kt index eee14a1..9e1edd6 100644 --- a/src/main/kotlin/display/gui/GuiController.kt +++ b/src/main/kotlin/display/gui/GuiController.kt @@ -1,27 +1,50 @@ package display.gui import display.draw.Drawer +import display.draw.TextureEnum import display.graphic.Color +import display.text.TextJustify import game.GamePlayer import org.jbox2d.common.Vec2 +import utility.Common.roundFloat +import utility.Common.vectorUnit +import kotlin.math.roundToInt class GuiController(private val drawer: Drawer) { private val elements = mutableListOf() - fun render() = elements.forEach { it.render() } + fun render() = elements.forEach { it.render(null) } + + fun update() = elements.forEach { it.update() } fun clear() = elements.clear() fun checkHover(location: Vec2) = elements.forEach { it.handleHover(location) } - fun checkLeftClick(location: Vec2) = elements.toList().forEach { it.handleClick(location) } + fun checkLeftClick(location: Vec2) = elements.toList().forEach { it.handleLeftClick(location) } + + fun checkLeftClickDrag(location: Vec2, movement: Vec2) = + elements.forEach { it.handleLeftClickDrag(location, movement) } + + fun checkScroll(movement: Vec2, location: Vec2) = elements.forEach { it.handleScroll(location, movement) } + + fun checkAddTextInput(text: String) = elements.filterIsInstance().forEach { it.handleAddTextInput(text) } + + fun checkRemoveTextInput() = elements.filterIsInstance().forEach { it.handleRemoveTextInput() } + + fun stopTextInput() = elements.filterIsInstance().forEach { it.stopTextInput() } + + fun textInputIsBusy(): Boolean = elements.filterIsInstance().toList().any { it.textInputIsBusy } + + fun locationIsGui(location: Vec2): Boolean = elements.any { it.isHover(location) } fun createMainMenu( onClickNewGame: () -> Unit, onClickSettings: () -> Unit, onClickQuit: () -> Unit ) { + clear() val buttonScale = Vec2(200f, 44f) val menuButtons = listOf( GuiButton(drawer, scale = buttonScale, title = "New Game", textSize = .27f, onClick = onClickNewGame), @@ -32,11 +55,10 @@ class GuiController(private val drawer: Drawer) { setElementsInRows(menuButtons, 40f, false) menuButtons.forEach { it.addOffset(Vec2(0f, 150f)) } - elements.add(GuiLabel(drawer, Vec2(-10f, 250f), "Volynov", .6f)) + elements.add(GuiLabel(drawer, Vec2(-10f, 250f), TextJustify.CENTER, "Volynov", .6f)) elements.addAll(menuButtons) } - fun createMainMenuSelectPlayers( onClickStart: () -> Unit, onClickCancel: () -> Unit, @@ -44,7 +66,8 @@ class GuiController(private val drawer: Drawer) { onRemovePlayer: () -> Unit, playerList: MutableList ) { - elements.add(GuiLabel(drawer, Vec2(-10f, 250f), "Select Players", .2f)) + clear() + elements.add(GuiLabel(drawer, Vec2(0f, 250f), TextJustify.CENTER, "Select Players", .2f)) updateMainMenuSelectPlayers(playerList, onAddPlayer, onRemovePlayer) @@ -82,13 +105,18 @@ class GuiController(private val drawer: Drawer) { ) val addRemoveButtonsList = listOf(addPlayerButton, removePlayerButton) setElementsInRows(addRemoveButtonsList) - val addRemoveContainer = GuiWindow( + val addRemoveContainer = GuiPanel( drawer, scale = playerButtonSize, color = Color.TRANSPARENT, draggable = false, childElements = addRemoveButtonsList.toMutableList() ) - val playerButtons = players - .map { GuiButton(drawer, scale = playerButtonSize, title = "P${it.name}", textSize = .3f) } + val playerButtons = players.withIndex() + .map { (index, player) -> + val playerName = if (player.name.length == 1) "" else player.name + GuiInput(drawer, scale = Vec2(75f, 50f), placeholder = "Player ${index + 1}", + onChange = { text -> player.name = text }) + .setTextValue(playerName) + } .let { listOf(addRemoveContainer) + it } setElementsInColumns(playerButtons, 40f) when (indexOfPlayersButtons) { @@ -100,56 +128,128 @@ class GuiController(private val drawer: Drawer) { } fun createPlayersPickShields(player: GamePlayer, onClickShield: (player: GamePlayer) -> Unit) { - val shieldPickerWindow = - GuiWindow( - drawer, Vec2(200f, -200f), Vec2(150f, 150f), title = "Player ${player.name} to pick a shield", + clear() + val shieldPickerPanel = + GuiPanel( + drawer, Vec2(710f, -340f), Vec2(250f, 200f), title = "${player.name} to pick a shield", draggable = true ) - shieldPickerWindow.addChild( + shieldPickerPanel.addChild( GuiButton(drawer, scale = Vec2(100f, 25f), title = "Pick one", onClick = { onClickShield(player) }) ) - elements.add(shieldPickerWindow) + elements.add(shieldPickerPanel) } - fun createPlayerCommandPanel(player: GamePlayer, onClickFire: (player: GamePlayer) -> Unit) { - val commandPanelWindow = - GuiWindow( - drawer, Vec2(200f, -200f), Vec2(150f, 150f), title = "Player ${player.name}", - draggable = true + fun createPlayerCommandPanel( + player: GamePlayer, + onClickAim: (player: GamePlayer) -> Unit, + onClickPower: (player: GamePlayer) -> Unit, + onClickFire: (player: GamePlayer) -> Unit + ) { + clear() + val commandPanel = GuiPanel( + drawer, Vec2(710f, -340f), Vec2(250f, 200f), + title = player.name, draggable = true + ) + val weaponsList = GuiScroll(drawer, Vec2(50f, -50f), Vec2(100f, 100f)).addChildren( + (1..5).map { + GuiButton(drawer, scale = Vec2(100f, 25f), title = "Boom $it", textSize = .15f, + onClick = { println("clicked [Boom $it]") }) + } + ) + commandPanel.addChildren( + listOf( + GuiButton(drawer, Vec2(-200f, 0f), Vec2(50f, 25f), title = "Aim", + onClick = { onClickAim(player) }), + GuiButton(drawer, Vec2(-200f, -50f), Vec2(50f, 25f), title = "Power", + onClick = { onClickPower(player) }), + GuiButton(drawer, Vec2(-200f, -100f), Vec2(50f, 25f), title = "Fire", + onClick = { onClickFire(player) }), + + GuiLabel(drawer, Vec2(-210f, 110f), justify = TextJustify.LEFT, + title = getPlayerAimAngleDisplay(player), + textSize = .15f, + updateCallback = { it.title = getPlayerAimAngleDisplay(player) }), + GuiLabel(drawer, Vec2(-210f, 80f), justify = TextJustify.LEFT, title = getPlayerAimPowerDisplay(player), + textSize = .15f, + updateCallback = { it.title = getPlayerAimPowerDisplay(player) }), + + weaponsList, + + GuiIcon(drawer, Vec2(-230f, 110f), vectorUnit.mul(20f), texture = TextureEnum.icon_aim) + ) + ) + elements.add(commandPanel) + } + + private fun getPlayerAimPowerDisplay(player: GamePlayer): String = + player.playerAim.power.let { displayNumber(it, 2) + "%" } + + private fun getPlayerAimAngleDisplay(player: GamePlayer): String = + player.playerAim.getDegreesAngle().let { displayNumber(it, 2) + "ยบ" } + + fun createRoundLeaderboard(players: MutableList, onClickNextRound: () -> Unit) { + clear() + val leaderBoardPanel = GuiPanel(drawer, Vec2(), Vec2(200f, 300f), "Leaderboard", draggable = false) + val playerLines = listOf(GuiLabel( + drawer, + Vec2(-50f, 100f), + justify = TextJustify.LEFT, + title = "Player Score".padStart(10, ' '), + textSize = .2f + )) + players.sortedByDescending { it.score }.map { + GuiLabel( + drawer, + Vec2(-50f, 100f), + justify = TextJustify.LEFT, + title = "${it.name.padEnd(20, ' ')}${it.score.roundToInt()}".padStart(10, ' '), + textSize = .2f + ) + } + setElementsInRows(playerLines, 10f) + + leaderBoardPanel.addChildren(playerLines) + leaderBoardPanel.addChild( + GuiButton( + drawer, Vec2(0f, -250f), Vec2(100f, 25f), "Next Match", + onClick = onClickNextRound ) - commandPanelWindow.addChild( - GuiButton(drawer, scale = Vec2(100f, 25f), title = "Fire all guns!", onClick = { onClickFire(player) }) ) - elements.add(commandPanelWindow) + elements.add(leaderBoardPanel) } - private fun setElementsInColumns(elements: List, gap: Float = 0f, centered: Boolean = true) { - val totalWidth = elements.map { it.scale.x * 2f }.sum() + gap * (elements.size - 1) - val columnSize = totalWidth / elements.size + private fun displayNumber(value: Float, decimals: Int): String = roundFloat(value, decimals).toString() + + companion object { + + fun setElementsInColumns(elements: List, gap: Float = 0f, centered: Boolean = true) { + val totalWidth = elements.map { it.scale.x * 2f }.sum() + gap * (elements.size - 1) + val columnSize = totalWidth / elements.size - elements.withIndex() - .forEach { (index, element) -> - val newXOffset = when (centered) { - true -> columnSize * (index - (elements.size - 1) * .5f) - else -> columnSize * index + columnSize * .5f + elements.withIndex() + .forEach { (index, element) -> + val newXOffset = when (centered) { + true -> columnSize * (index - (elements.size - 1) * .5f) + else -> columnSize * index + columnSize * .5f + } + element.addOffset(Vec2(newXOffset, element.offset.y)) } - element.addOffset(Vec2(newXOffset, element.offset.y)) - } - } + } - private fun setElementsInRows(elements: List, gap: Float = 0f, centered: Boolean = true) { - val totalHeight = elements.map { it.scale.y * 2f }.sum() + gap * (elements.size - 1) - val rowSize = totalHeight / elements.size - - elements.withIndex() - .forEach { (index, element) -> - val newYOffset = when (centered) { - true -> rowSize * (index - (elements.size - 1) * .5f) - else -> rowSize * index + rowSize * .5f - } * -1f - element.addOffset(Vec2(element.offset.x, newYOffset)) - } - } + fun setElementsInRows(elements: List, gap: Float = 0f, centered: Boolean = true) { + val totalHeight = elements.map { it.scale.y * 2f }.sum() + gap * (elements.size - 1) + val rowSize = totalHeight / elements.size + + elements.withIndex() + .forEach { (index, element) -> + val newYOffset = when (centered) { + true -> rowSize * (index - (elements.size - 1) * .5f) + else -> rowSize * index + rowSize * .5f + } * -1f + element.addOffset(Vec2(element.offset.x, newYOffset)) + } + } + } } diff --git a/src/main/kotlin/display/gui/GuiElement.kt b/src/main/kotlin/display/gui/GuiElement.kt index 5e6e38b..c8c2a39 100644 --- a/src/main/kotlin/display/gui/GuiElement.kt +++ b/src/main/kotlin/display/gui/GuiElement.kt @@ -2,16 +2,19 @@ package display.gui import display.draw.Drawer import display.graphic.Color +import display.graphic.SnipRegion +import display.text.TextJustify import org.jbox2d.common.Vec2 -import utility.Common +import utility.Common.vectorUnit open class GuiElement( protected val drawer: Drawer, override var offset: Vec2, - override val scale: Vec2 = Vec2(1f, 1f), - override val title: String, + override val scale: Vec2 = vectorUnit, + override var title: String, override val textSize: Float, override val color: Color, + override val updateCallback: (GuiElement) -> Unit, override var id: GuiElementIdentifierType = GuiElementIdentifierType.DEFAULT ) : GuiElementInterface { @@ -20,55 +23,55 @@ open class GuiElement( protected var topRight: Vec2 = Vec2() protected var bottomLeft: Vec2 = Vec2() - override fun render() { - } + override fun render(snipRegion: SnipRegion?) = Unit - override fun addOffset(newOffset: Vec2) { - GuiElement.addOffset(this, newOffset) - } + override fun update() = updateCallback(this) - override fun updateOffset(newOffset: Vec2) { - GuiElement.updateOffset(this, newOffset) - } + override fun addOffset(newOffset: Vec2) = addOffset(this, newOffset) - override fun handleHover(location: Vec2) { - when { - isHover(location) -> currentPhase = GuiElementPhases.HOVERED - else -> currentPhase = GuiElementPhases.IDLE - } - } + override fun updateOffset(newOffset: Vec2) = updateOffset(this, newOffset) - override fun handleClick(location: Vec2) { + override fun handleHover(location: Vec2) = when { + isHover(location) -> currentPhase = GuiElementPhases.HOVERED + else -> currentPhase = GuiElementPhases.IDLE } - protected fun isHover(location: Vec2): Boolean { - return location.x > bottomLeft.x + override fun handleLeftClick(location: Vec2) = Unit + + override fun handleLeftClickDrag(location: Vec2, movement: Vec2) = Unit + + override fun handleScroll(location: Vec2, movement: Vec2) = Unit + + fun isHover(location: Vec2): Boolean = + location.x > bottomLeft.x && location.x < topRight.x && location.y > bottomLeft.y && location.y < topRight.y - } companion object { - fun drawLabel(drawer: Drawer, element: GuiElementInterface) { - drawer.renderer.drawText( - element.title, element.offset, - Common.vectorUnit.mul(element.textSize), - element.color, false - ) - } + fun drawLabel(drawer: Drawer, + element: GuiElementInterface, + justify: TextJustify = TextJustify.CENTER, + snipRegion: SnipRegion? + ) = drawer.renderer.drawText( + element.title, + element.offset, + vectorUnit.mul(element.textSize), + element.color, + justify, + false, + snipRegion) fun calculateElementRegion(element: GuiElement) { element.bottomLeft = element.offset.add(element.scale.negate()) element.topRight = element.offset.add(element.scale) } - fun addOffset(element: GuiElement, newOffset: Vec2) { - updateOffset(element, element.offset.add(newOffset)) - } + fun addOffset(element: GuiElement, newOffset: Vec2) = updateOffset(element, element.offset.add(newOffset)) fun updateOffset(element: GuiElement, newOffset: Vec2) { - element.offset = newOffset + element.offset.set(newOffset) calculateElementRegion(element) } diff --git a/src/main/kotlin/display/gui/GuiElementInterface.kt b/src/main/kotlin/display/gui/GuiElementInterface.kt index 8c4f7a5..f915b45 100644 --- a/src/main/kotlin/display/gui/GuiElementInterface.kt +++ b/src/main/kotlin/display/gui/GuiElementInterface.kt @@ -1,6 +1,7 @@ package display.gui import display.graphic.Color +import display.graphic.SnipRegion import org.jbox2d.common.Vec2 interface GuiElementInterface { @@ -10,11 +11,15 @@ interface GuiElementInterface { val title: String val textSize: Float val color: Color + val updateCallback: (GuiElement) -> Unit var id: GuiElementIdentifierType - fun render() + fun render(snipRegion: SnipRegion?) + fun update() + fun handleHover(location: Vec2) + fun handleLeftClick(location: Vec2) + fun handleLeftClickDrag(location: Vec2, movement: Vec2) + fun handleScroll(location: Vec2, movement: Vec2) fun addOffset(newOffset: Vec2) fun updateOffset(newOffset: Vec2) - fun handleHover(location: Vec2) - fun handleClick(location: Vec2) } diff --git a/src/main/kotlin/display/gui/GuiElementPhases.kt b/src/main/kotlin/display/gui/GuiElementPhases.kt index d2cbae5..5cccbb9 100644 --- a/src/main/kotlin/display/gui/GuiElementPhases.kt +++ b/src/main/kotlin/display/gui/GuiElementPhases.kt @@ -3,6 +3,7 @@ package display.gui enum class GuiElementPhases { IDLE, HOVERED, - CLICKED + CLICKED, + INPUT } diff --git a/src/main/kotlin/display/gui/GuiIcon.kt b/src/main/kotlin/display/gui/GuiIcon.kt new file mode 100644 index 0000000..e9581d5 --- /dev/null +++ b/src/main/kotlin/display/gui/GuiIcon.kt @@ -0,0 +1,34 @@ +package display.gui + +import display.draw.Drawer +import display.draw.TextureEnum +import display.graphic.Color +import display.graphic.SnipRegion +import org.jbox2d.common.Vec2 +import utility.Common.vectorUnit + +class GuiIcon( + drawer: Drawer, + offset: Vec2 = Vec2(), + scale: Vec2 = vectorUnit, + title: String = "", + textSize: Float = 0f, + color: Color = Color.WHITE.setAlpha(.7f), + val texture: TextureEnum = TextureEnum.white_pixel +) : GuiElement( + drawer, + offset, + scale, + title, + textSize, + color, + {} +) { + + override fun render(snipRegion: SnipRegion?) { + super.render(snipRegion) + + drawer.drawIcon(texture, scale, offset, color) + } + +} diff --git a/src/main/kotlin/display/gui/GuiInput.kt b/src/main/kotlin/display/gui/GuiInput.kt new file mode 100644 index 0000000..43761d7 --- /dev/null +++ b/src/main/kotlin/display/gui/GuiInput.kt @@ -0,0 +1,122 @@ +package display.gui + +import display.draw.Drawer +import display.draw.TextureEnum +import display.graphic.BasicShapes +import display.graphic.Color +import display.graphic.SnipRegion +import display.text.TextJustify +import org.jbox2d.common.Vec2 +import utility.Common.makeVec2 +import utility.Common.vectorUnit +import utility.toList + +class GuiInput( + drawer: Drawer, + offset: Vec2 = Vec2(), + scale: Vec2 = Vec2(200f, 50f), + placeholder: String, + textSize: Float = .15f, + color: Color = Color.WHITE.setAlpha(.7f), + private val onClick: () -> Unit = {}, + updateCallback: (GuiElement) -> Unit = {}, + val onChange: (String) -> Unit +) : GuiElement(drawer, offset, scale, placeholder, textSize, color, updateCallback) { + + private val blinkRate = 400 + private var cursorLine: FloatArray + private var buttonOutline: FloatArray + private var buttonBackground: FloatArray + private var backgroundColor = color.setAlpha(.1f) + + private var inputText = "" + private val paddedScale = Vec2(scale.x - 8f, 20f) + val textInputIsBusy + get() = currentPhase == GuiElementPhases.INPUT + + init { + val verticalCursorLinePoint = BasicShapes.verticalLine.chunked(2) + .flatMap { + val location = makeVec2(it[0] * paddedScale.x, it[1] * paddedScale.y) + .also { vec -> vec.x -= paddedScale.x } + location.toList() + } + cursorLine = Drawer.getLine(verticalCursorLinePoint, color, startWidth = 1.2f) + + val linePoints = BasicShapes.square.chunked(2) + .flatMap { listOf(it[0] * scale.x, it[1] * scale.y) } + buttonOutline = Drawer.getLine(linePoints, color, startWidth = 1f, wrapAround = true) + buttonBackground = Drawer.getColoredData(linePoints, backgroundColor).toFloatArray() + + calculateElementRegion(this) + } + + override fun render(snipRegion: SnipRegion?) { + drawer.textures.getTexture(TextureEnum.white_pixel).bind() + + when (currentPhase) { + GuiElementPhases.HOVERED -> drawer.renderer.drawShape(buttonBackground, offset, useCamera = false, + snipRegion = snipRegion) + GuiElementPhases.INPUT -> { + drawer.renderer.drawShape(buttonBackground, offset, useCamera = false, snipRegion = snipRegion) + + if (System.currentTimeMillis().rem(blinkRate * 2) < blinkRate) { + drawer.renderer.drawStrip(cursorLine, offset, useCamera = false, snipRegion = snipRegion) + } + } + } + + drawer.renderer.drawStrip(buttonOutline, offset, useCamera = false) + + val paddedOffset = offset.clone().also { it.x -= paddedScale.x } + when (inputText.length) { + 0 -> drawer.renderer.drawText(title, paddedOffset, vectorUnit.mul(textSize), + color.setAlpha(.4f), TextJustify.LEFT, false, snipRegion) + else -> drawer.renderer.drawText(inputText, paddedOffset, vectorUnit.mul(textSize), + color, TextJustify.LEFT, false, snipRegion) + } + super.render(snipRegion) + } + + override fun handleHover(location: Vec2) { + if (textInputIsBusy) return + super.handleHover(location) + } + + override fun handleLeftClick(location: Vec2) { + when { + isHover(location) -> { + currentPhase = GuiElementPhases.INPUT + onClick() + } + else -> currentPhase = GuiElementPhases.IDLE + } + } + + fun handleAddTextInput(text: String) { + if (textInputIsBusy) { + inputText += text + onChange(inputText) + } + } + + fun handleRemoveTextInput() { + if (textInputIsBusy) { + inputText = inputText.dropLast(1) + onChange(inputText) + } + } + + fun stopTextInput() { + if (textInputIsBusy) { + currentPhase = GuiElementPhases.IDLE + } + } + + fun setTextValue(text: String): GuiInput { + inputText = text + onChange(inputText) + return this + } + +} diff --git a/src/main/kotlin/display/gui/GuiLabel.kt b/src/main/kotlin/display/gui/GuiLabel.kt index caf9728..1aee370 100644 --- a/src/main/kotlin/display/gui/GuiLabel.kt +++ b/src/main/kotlin/display/gui/GuiLabel.kt @@ -2,20 +2,31 @@ package display.gui import display.draw.Drawer import display.graphic.Color +import display.graphic.SnipRegion +import display.text.TextJustify import org.jbox2d.common.Vec2 class GuiLabel( drawer: Drawer, offset: Vec2 = Vec2(), + val justify: TextJustify = TextJustify.LEFT, title: String, textSize: Float = 0f, - color: Color = Color.WHITE.setAlpha(.7f) -) : GuiElement(drawer, offset, title = title, textSize = textSize, color = color) { + color: Color = Color.WHITE.setAlpha(.7f), + updateCallback: (GuiElement) -> Unit = {} +) : GuiElement( + drawer, + offset, + Vec2(title.length * 16f, textSize * 100f), + title, + textSize, + color, + updateCallback +) { - override fun render() { - super.render() - - GuiElement.drawLabel(drawer, this) + override fun render(snipRegion: SnipRegion?) { + super.render(snipRegion) + drawLabel(drawer, this, justify, snipRegion) } } diff --git a/src/main/kotlin/display/gui/GuiPanel.kt b/src/main/kotlin/display/gui/GuiPanel.kt new file mode 100644 index 0000000..fc6e81b --- /dev/null +++ b/src/main/kotlin/display/gui/GuiPanel.kt @@ -0,0 +1,104 @@ +package display.gui + +import display.draw.Drawer +import display.draw.TextureEnum +import display.graphic.BasicShapes +import display.graphic.Color +import display.graphic.SnipRegion +import display.text.TextJustify +import org.jbox2d.common.Vec2 +import utility.Common + +class GuiPanel( + drawer: Drawer, + offset: Vec2 = Vec2(), + scale: Vec2 = Vec2(100f, 100f), + title: String = "", + textSize: Float = .3f, + color: Color = Color.BLACK.setAlpha(.5f), + private val childElements: MutableList = mutableListOf(), + private val draggable: Boolean = true, + updateCallback: (GuiElement) -> Unit = {} +) : GuiElement(drawer, offset, scale, title, textSize, color, updateCallback) { + + private val childElementOffsets = HashMap() + + init { + childElementOffsets.putAll(childElements.map { Pair(it, it.offset.clone()) }) + calculateElementRegion(this) + } + + override fun render(snipRegion: SnipRegion?) { + drawer.textures.getTexture(TextureEnum.white_pixel).bind() + drawer.renderer.drawShape(BasicShapes.square + .let { Drawer.getColoredData(it, color) } + .toFloatArray(), + offset, 0f, scale, useCamera = false, snipRegion = snipRegion + ) + + drawer.renderer.drawText( + title, + offset.add(Vec2(0f, scale.y - 25f)), + Common.vectorUnit.mul(.15f), + Color.WHITE, + TextJustify.CENTER, + false, + snipRegion + ) + + childElements.forEach { it.render(snipRegion) } + + super.render(snipRegion) + } + + override fun update() = childElements.forEach { it.update() } + + override fun addOffset(newOffset: Vec2) { + addOffset(this, newOffset) + calculateNewOffsets() + } + + override fun handleHover(location: Vec2) { + if (isHover(location)) { + super.handleHover(location) + childElements.forEach { it.handleHover(location) } + } + } + + override fun handleLeftClick(location: Vec2) { + if (isHover(location)) { + super.handleLeftClick(location) + childElements.forEach { it.handleLeftClick(location) } + } + } + + override fun handleLeftClickDrag(location: Vec2, movement: Vec2) { + if (draggable && isHover(location)) { // TODO: use custom isHover() to only allow a small region as drag handle + childElements.forEach { it.handleLeftClickDrag(location, movement) } + super.handleLeftClickDrag(location, movement) + addOffset(movement) + } + } + + override fun handleScroll(location: Vec2, movement: Vec2) { + if (isHover(location)) { + super.handleScroll(location, movement) + childElements.forEach { it.handleScroll(location, movement) } + } + } + + private fun calculateNewOffsets() = childElements.forEach { it.updateOffset(childElementOffsets[it]!!.add(offset)) } + + fun addChildren(elements: List) { + childElements.addAll(elements) + childElementOffsets.putAll(elements.map { Pair(it, it.offset.clone()) }) + calculateNewOffsets() + } + + fun addChild(element: GuiElement) { + childElements.add(element) + childElementOffsets[element] = element.offset.clone() + calculateNewOffsets() + } + +} diff --git a/src/main/kotlin/display/gui/GuiScroll.kt b/src/main/kotlin/display/gui/GuiScroll.kt new file mode 100644 index 0000000..4811a7f --- /dev/null +++ b/src/main/kotlin/display/gui/GuiScroll.kt @@ -0,0 +1,130 @@ +package display.gui + +import display.draw.Drawer +import display.draw.TextureEnum +import display.graphic.BasicShapes +import display.graphic.Color +import display.graphic.SnipRegion +import display.gui.GuiController.Companion.setElementsInRows +import org.jbox2d.common.Vec2 + +class GuiScroll( + drawer: Drawer, + offset: Vec2 = Vec2(), + scale: Vec2 = Vec2(100f, 100f), + color: Color = Color.WHITE.setAlpha(.5f), + private val childElements: MutableList = mutableListOf() +) : GuiElement(drawer, offset, scale, "", 0f, color, {}) { + + private var scrollOutline: FloatArray + private val childElementOffsets = HashMap() + + private var scrollBarPosition = 0f + private var scrollBarMin: Float = 0f + private var scrollBarMax: Float = 0f + + init { + val linePoints = BasicShapes.square + .chunked(2) + .flatMap { listOf(it[0] * scale.x, it[1] * scale.y) } + scrollOutline = Drawer.getLine(linePoints, color, startWidth = 1f, wrapAround = true) + + childElementOffsets.putAll(childElements.map { Pair(it, it.offset.clone()) }) + calculateElementRegion(this) + calculateNewOffsets() + } + + override fun render(snipRegion: SnipRegion?) { + // TODO: handle nested snipRegions, if this element is inside parent scroll + drawer.textures.getTexture(TextureEnum.white_pixel).bind() + drawer.renderer.drawStrip(scrollOutline, offset, useCamera = false, snipRegion = snipRegion) + super.render(snipRegion) + + childElements.filter { + it.offset.add(it.scale.negate()).y < offset.add(scale).y + && it.offset.add(it.scale).y > offset.add(scale.negate()).y + } + .forEach { it.render(SnipRegion(offset.add(scale.negate()), scale.mul(2f))) } + } + + override fun update() = childElements.forEach { it.update() } + + override fun addOffset(newOffset: Vec2) { + addOffset(this, newOffset) + calculateNewOffsets() + } + + override fun updateOffset(newOffset: Vec2) { + super.updateOffset(newOffset) + calculateNewOffsets() + } + + override fun handleHover(location: Vec2) { + if (isHover(location)) { + super.handleHover(location) + childElements.forEach { it.handleHover(location) } + } + } + + override fun handleLeftClick(location: Vec2) { + if (isHover(location)) { + super.handleLeftClick(location) + childElements.forEach { it.handleLeftClick(location) } + } + } + + override fun handleLeftClickDrag(location: Vec2, movement: Vec2) { + if (isHover(location)) { + addScrollBarPosition(movement.y * -.1f) + calculateNewOffsets() + super.handleLeftClickDrag(location, movement) + childElements.forEach { it.handleLeftClickDrag(location, movement) } + } + } + + override fun handleScroll(location: Vec2, movement: Vec2) { + if (isHover(location)) { + addScrollBarPosition(movement.y) + calculateNewOffsets() + } + } + + private fun addScrollBarPosition(movement: Float) { + scrollBarPosition = (scrollBarPosition + movement * 10f).coerceIn(scrollBarMin, scrollBarMax) + } + + private fun updateScrollBarRange() { + scrollBarMin = childElements.minBy { it.offset.y } + .let { childElementOffsets[it]!!.y + scale.y * 2 - it!!.scale.y } + scrollBarMax = 0f + } + + private fun calculateNewOffsets() { + childElements.forEach { + it.updateOffset(childElementOffsets[it]!! + .add(offset) + .add(Vec2(0f, scale.y - scrollBarPosition))) + } + } + + fun addChildren(elements: List): GuiScroll { + childElements.addAll(elements) + setElementsInRows(childElements, centered = false) + + childElementOffsets.putAll(elements.map { Pair(it, it.offset.clone()) }) + calculateNewOffsets() + updateScrollBarRange() + return this + } + + fun addChild(element: GuiElement): GuiScroll { + childElements.add(element) + setElementsInRows(childElements) + + childElementOffsets[element] = element.offset.clone() + calculateNewOffsets() + updateScrollBarRange() + return this + } + +} diff --git a/src/main/kotlin/display/gui/GuiWindow.kt b/src/main/kotlin/display/gui/GuiWindow.kt deleted file mode 100644 index 62805c7..0000000 --- a/src/main/kotlin/display/gui/GuiWindow.kt +++ /dev/null @@ -1,76 +0,0 @@ -package display.gui - -import display.draw.Drawer -import display.graphic.BasicShapes -import display.graphic.Color -import org.jbox2d.common.Vec2 -import utility.Common - -class GuiWindow( - drawer: Drawer, - offset: Vec2 = Vec2(), - scale: Vec2 = Vec2(100f, 100f), - title: String = " ", - textSize: Float = .3f, - color: Color = Color.BLACK.setAlpha(.5f), - private val childElements: MutableList = mutableListOf(), - private val draggable: Boolean = true -) : GuiElement(drawer, offset, scale, title, textSize, color) { - - private val childElementOffsets = HashMap() - - init { - childElementOffsets.putAll(childElements.map { Pair(it, it.offset.clone()) }) - } - - override fun render() { - drawer.textures.white_pixel.bind() - drawer.renderer.drawShape(BasicShapes.square - .let { Drawer.getColoredData(it, color) } - .toFloatArray(), - offset, 0f, scale, useCamera = false - ) - - drawer.renderer.drawText( - title, - offset.add(Vec2(0f, scale.y - 25f)), - Common.vectorUnit.mul(.15f), - Color.WHITE, - false - ) - - childElements.forEach { it.render() } - - super.render() - } - - override fun addOffset(newOffset: Vec2) { - GuiElement.addOffset(this, newOffset) - update() - } - - override fun handleHover(location: Vec2) { - childElements.forEach { it.handleHover(location) } - } - - override fun handleClick(location: Vec2) { - childElements.forEach { it.handleClick(location) } - } - - fun update() { - childElements.forEach { it.updateOffset(childElementOffsets[it]!!.add(offset)) } - } - - fun addChildren(elements: List) { - childElements.addAll(elements) - childElementOffsets.putAll(elements.map { Pair(it, it.offset.clone()) }) - update() - } - - fun addChild(element: GuiElement) { - childElements.add(element) - childElementOffsets[element] = element.offset.clone() - update() - } - -} diff --git a/src/main/kotlin/display/text/Font.kt b/src/main/kotlin/display/text/Font.kt index 46203bb..b08bc29 100644 --- a/src/main/kotlin/display/text/Font.kt +++ b/src/main/kotlin/display/text/Font.kt @@ -1,9 +1,6 @@ package display.text -import display.graphic.BasicShapes -import display.graphic.Color -import display.graphic.Renderer -import display.graphic.Texture +import display.graphic.* import org.jbox2d.common.Vec2 import org.lwjgl.system.MemoryUtil import java.awt.Font @@ -13,10 +10,10 @@ import java.awt.geom.AffineTransform import java.awt.image.AffineTransformOp import java.awt.image.BufferedImage import java.io.InputStream +import java.lang.NullPointerException import kotlin.math.hypot import java.awt.Color as AwtColor - class Font constructor(font: Font = Font(MONOSPACED, BOLD, 32), antiAlias: Boolean = true) { private val glyphs: MutableMap @@ -185,10 +182,12 @@ class Font constructor(font: Font = Font(MONOSPACED, BOLD, 32), antiAlias: Boole offset: Vec2, scale: Vec2, color: Color, - useCamera: Boolean + justify: TextJustify, + useCamera: Boolean, + snipRegion: SnipRegion? ) { - drawLetters(fontBitMapShadow, offset, scale, renderer, text, Color.BLACK, useCamera) - drawLetters(fontBitMap, offset, scale, renderer, text, color, useCamera) + drawLetters(fontBitMapShadow, offset, scale, renderer, text, Color.BLACK, justify, useCamera, snipRegion) + drawLetters(fontBitMap, offset, scale, renderer, text, color, justify, useCamera, snipRegion) } private fun drawLetters( @@ -198,19 +197,31 @@ class Font constructor(font: Font = Font(MONOSPACED, BOLD, 32), antiAlias: Boole renderer: Renderer, text: CharSequence, color: Color, - useCamera: Boolean + justify: TextJustify, + useCamera: Boolean, + snipRegion: SnipRegion? ) { - val glyphs = text.map { glyphs[it]!! } - val centerText = Vec2(-getTextTotalWidth(glyphs, scale) * .75f, 0f) + val glyphs = text.mapNotNull { + try { + glyphs[it] + } catch (e: NullPointerException) { + throw Exception("Could not find text character in font", e) + } + } + val justifyment = when (justify) { + TextJustify.LEFT -> Vec2() + TextJustify.CENTER -> Vec2(-getTextTotalWidth(glyphs, scale), 0f) + TextJustify.RIGHT -> Vec2() + } - var x = -glyphs[0].width * scale.x + var x = 0f//glyphs[0].width * scale.x var y = 0f glyphs.forEach { x += it.width * scale.x drawTextPosition( - texture, renderer, Vec2(x, y).add(offset).add(centerText), - scale, it, color, useCamera + texture, renderer, Vec2(x, y).add(offset).add(justifyment), + scale, it, color, useCamera, snipRegion ) x += it.width * scale.x } @@ -223,10 +234,13 @@ class Font constructor(font: Font = Font(MONOSPACED, BOLD, 32), antiAlias: Boole scale: Vec2, glyph: Glyph, color: Color, - useCamera: Boolean + useCamera: Boolean, + snipRegion: SnipRegion? ) { - val glyphScale = Vec2(glyph.width / texture.width.toFloat(), glyph.height / texture.height.toFloat()) - val glyphOffset = Vec2((glyph.x + glyph.width) / texture.width.toFloat(), glyph.y / texture.height.toFloat()) + val textureWidth = texture.width.toFloat() + val textureHeight = texture.height.toFloat() + val glyphScale = Vec2(glyph.width / textureWidth, glyph.height / textureHeight) + val glyphOffset = Vec2((glyph.x + glyph.width) / textureWidth, glyph.y / textureHeight) val debug = renderer.debugOffset val data = BasicShapes.square @@ -242,10 +256,10 @@ class Font constructor(font: Font = Font(MONOSPACED, BOLD, 32), antiAlias: Boole }.toFloatArray() texture.bind() - renderer.drawShape(data, offset, 0f, scale, useCamera) + renderer.drawShape(data, offset, 0f, scale, useCamera, snipRegion) -// textures.white_pixel.bind() -// renderer.drawShape(data, offset, 0f, scale, useCamera) + // textures.white_pixel.bind() + // renderer.drawShape(data, offset, 0f, scale, useCamera) } fun dispose() { diff --git a/src/main/kotlin/display/text/TextJustify.kt b/src/main/kotlin/display/text/TextJustify.kt new file mode 100644 index 0000000..f310af0 --- /dev/null +++ b/src/main/kotlin/display/text/TextJustify.kt @@ -0,0 +1,7 @@ +package display.text + +enum class TextJustify { + LEFT, + CENTER, + RIGHT +} diff --git a/src/main/kotlin/engine/FreeBodyCallback.kt b/src/main/kotlin/engine/FreeBodyCallback.kt new file mode 100644 index 0000000..5d6239b --- /dev/null +++ b/src/main/kotlin/engine/FreeBodyCallback.kt @@ -0,0 +1,6 @@ +package engine + +import engine.freeBody.FreeBody +import org.jbox2d.dynamics.Body + +class FreeBodyCallback(val freeBody: FreeBody, val callback: (FreeBody, Body) -> Unit) diff --git a/src/main/kotlin/engine/GameState.kt b/src/main/kotlin/engine/GameState.kt index 5dcc430..d79974a 100644 --- a/src/main/kotlin/engine/GameState.kt +++ b/src/main/kotlin/engine/GameState.kt @@ -2,16 +2,26 @@ package engine import input.CameraView import display.Window -import engine.freeBody.Planet -import engine.freeBody.Vehicle +import display.draw.TextureConfig +import display.draw.TextureEnum +import display.graphic.Color +import engine.freeBody.* +import engine.motion.Director import engine.motion.Motion import engine.physics.CellLocation import engine.physics.Gravity import engine.physics.GravityCell import game.GamePlayer -import game.GamePlayerTypes +import org.jbox2d.callbacks.ContactImpulse +import org.jbox2d.callbacks.ContactListener +import org.jbox2d.collision.Manifold import org.jbox2d.common.Vec2 +import org.jbox2d.dynamics.Body import org.jbox2d.dynamics.World +import org.jbox2d.dynamics.contacts.Contact +import org.jbox2d.dynamics.contacts.ContactEdge +import utility.Common +import utility.Common.makeVec2Circle class GameState { @@ -21,26 +31,30 @@ class GameState { lateinit var camera: CameraView var world = World(Vec2()) - var vehicles = mutableListOf() - var planets = mutableListOf() + val vehicles = mutableListOf() + val planets = mutableListOf() + val warheads = mutableListOf() + val particles = mutableListOf() -// private var worlds = mutableListOf() -// private var asteroids = mutableListOf() -// private var stars = mutableListOf() -// private var warheads = mutableListOf() + // private var asteroids = mutableListOf() + // private var stars = mutableListOf() - val tickables - get() = vehicles + planets + val gravityBodies + get() = vehicles + planets + warheads + + val trailerBodies + get() = vehicles + warheads var gravityMap = HashMap() var resolution = 0f + val activeCallbacks = mutableListOf<() -> Unit>() fun init(window: Window) { camera = CameraView(window) } private fun tickGravityChanges() { - Gravity.addGravityForces(tickables) + Gravity.addGravityForces(gravityBodies) .let { (gravityMap, resolution) -> this.gravityMap = gravityMap this.resolution = resolution @@ -54,8 +68,64 @@ class GameState { ) { world.step(timeStep, velocityIterations, positionIterations) + activeCallbacks.forEach { it() } + activeCallbacks.clear() + tickGravityChanges() - Motion.addNewTrailers(tickables.filter { it.radius > .5f }) + Motion.addNewTrailers(trailerBodies) + + tickWarheads() + tickParticles(timeStep) + } + + private fun tickParticles(timeStep: Float) { + particles.toList().forEach { + it.worldBody.position.addLocal(it.worldBody.linearVelocity.mul(timeStep)) + if (it.ageTime > 1000f) { + particles.remove(it) + return@forEach + } + + val scale = Common.getTimingFunctionEaseOut(it.ageTime / 1000f) + it.radius = it.fullRadius * scale + } + } + + private fun tickWarheads() { + warheads.toList() + .filter { it.ageTime > it.selfDestructTime } + .forEach { detonateWarhead(it) } + } + + private fun detonateWarhead(warhead: Warhead, body: Body? = null) { + val particle = warhead.createParticles(particles, world, body ?: warhead.worldBody) + + checkToDamageVehicles(particle, warhead) + + world.destroyBody(warhead.worldBody) + warheads.remove(warhead) + } + + private fun checkToDamageVehicles(particle: Particle, warhead: Warhead) { + vehicles.toList().map { + Pair(it, (Director.getDistance(it.worldBody, particle.worldBody) + - it.radius - warhead.radius).coerceAtLeast(0f)) + } + .filter { (_, distance) -> distance < particle.radius } + .forEach { (vehicle, distance) -> + val damageUnit = (1f - distance / particle.radius).coerceAtLeast(0f) + .let { Common.getTimingFunctionEaseIn(it) } + val totalDamage = damageUnit * warhead.damage + vehicle.hitPoints -= totalDamage + warhead.firedBy.scoreDamage(warhead, totalDamage, vehicle) + + if (vehicle.hitPoints <= 0) { + warhead.firedBy.scoreKill(vehicle) + + world.destroyBody(vehicle.worldBody) + vehicles.remove(vehicle) + } + } } fun reset() { @@ -64,6 +134,87 @@ class GameState { world = World(Vec2()) vehicles.clear() planets.clear() + + world.setContactListener(MyContactListener(this)) + } + + fun fireWarhead(player: GamePlayer, warheadType: String = "will make this some class later"): Warhead { + checkNotNull(player.vehicle) { "Player does not have a vehicle." } + val vehicle = player.vehicle!! + val angle = player.playerAim.angle + val power = player.playerAim.power * .15f + val originLocation = vehicle.worldBody.position + val originVelocity = vehicle.worldBody.linearVelocity + + val warheadRadius = .2f + val minimumSafeDistance = 1.5f * vehicle.radius + val angleVector = makeVec2Circle(angle) + + val warheadLocation = angleVector.mul(minimumSafeDistance).add(originLocation) + val warheadVelocity = angleVector.mul(power).add(originVelocity) + + val warheadMass = .1f + // val body = player.vehicle!!.worldBody + // body.applyLinearImpulse(angleVector.mul(power).negate(), body.localCenter) + // TODO: recoil causes excessive spin + + return Warhead.create( + world, player, warheadLocation.x, warheadLocation.y, angle, + warheadVelocity.x, warheadVelocity.y, 0f, + warheadMass, warheadRadius, + textureConfig = TextureConfig(TextureEnum.metal, color = Color.createFromHsv(0f, 1f, .3f, 1f)), + onWarheadCollision = { self, body -> detonateWarhead(self as Warhead, body) } + ) + .also { warheads.add(it) } + + } + + companion object { + + fun getContactBodies(contactEdge: ContactEdge): Sequence = sequence { + var currentContact = contactEdge + yield(currentContact.other) + + while (currentContact.next != null) { + yield(currentContact.other) + currentContact = currentContact.next + } + } + + fun getContactEdges(contactEdge: ContactEdge): Sequence = sequence { + var currentContact = contactEdge + yield(currentContact.contact) + + while (currentContact.next != null) { + yield(currentContact.contact) + currentContact = currentContact.next + } + } + + } +} + +class MyContactListener(val gameState: GameState) : ContactListener { + + override fun beginContact(contact: Contact) { + val bodies = listOf(contact.fixtureA, contact.fixtureB).map { it.body } + bodies.mapNotNull { it.userData } + .map { it as FreeBodyCallback } + .filter { it.freeBody is Warhead } + .forEach { warhead -> + val otherBody = bodies.find { body -> body != warhead.freeBody.worldBody }!! + gameState.activeCallbacks.add { warhead.callback(warhead.freeBody, otherBody) } + } + + } + + override fun endContact(contact: Contact) { + } + + override fun preSolve(contact: Contact, oldManifold: Manifold) { + } + + override fun postSolve(contact: Contact, impulse: ContactImpulse) { } } diff --git a/src/main/kotlin/engine/freeBody/FreeBody.kt b/src/main/kotlin/engine/freeBody/FreeBody.kt index bf98569..1523ea3 100644 --- a/src/main/kotlin/engine/freeBody/FreeBody.kt +++ b/src/main/kotlin/engine/freeBody/FreeBody.kt @@ -11,7 +11,6 @@ import kotlin.math.pow open class FreeBody( val id: String, var motion: Motion, - var shapeBox: Shape, var worldBody: Body, var radius: Float, val textureConfig: TextureConfig @@ -28,28 +27,29 @@ open class FreeBody( world: World, bodyDef: BodyDef ): Body { - val fixtureDef = FixtureDef() - fixtureDef.shape = shapeBox - fixtureDef.density = mass / (PI.toFloat() * radius.pow(2f)) - fixtureDef.friction = friction - fixtureDef.restitution = restitution - - val worldBody = world.createBody(bodyDef) - worldBody.createFixture(fixtureDef) - - return worldBody + val fixtureDef = FixtureDef().also { + it.shape = shapeBox + it.density = mass / (PI.toFloat() * radius.pow(2f)) + it.friction = friction + it.restitution = restitution + } + + return world.createBody(bodyDef) + .also { it.createFixture(fixtureDef) } } - fun createBodyDef(bodyType: BodyType, - x: Float, y: Float, h: Float, - dx: Float, dy: Float, dh: Float): BodyDef { - val bodyDef = BodyDef() - bodyDef.type = bodyType - bodyDef.position.set(x, y) - bodyDef.angle = h - bodyDef.linearVelocity = Vec2(dx, dy) - bodyDef.angularVelocity = dh - return bodyDef + fun createBodyDef( + bodyType: BodyType, + x: Float, y: Float, h: Float, + dx: Float, dy: Float, dh: Float + ): BodyDef { + return BodyDef().also { + it.type = bodyType + it.position.set(x, y) + it.angle = h + it.linearVelocity = Vec2(dx, dy) + it.angularVelocity = dh + } } } diff --git a/src/main/kotlin/engine/freeBody/Particle.kt b/src/main/kotlin/engine/freeBody/Particle.kt new file mode 100644 index 0000000..6986af9 --- /dev/null +++ b/src/main/kotlin/engine/freeBody/Particle.kt @@ -0,0 +1,23 @@ +package engine.freeBody + +import display.draw.TextureConfig +import org.jbox2d.dynamics.Body + +class Particle( + val id: String, + val worldBody: Body, + var radius: Float, + val textureConfig: TextureConfig +) { + + var fullRadius: Float = radius + + private val currentTime + get() = System.currentTimeMillis() + + val ageTime + get() = (currentTime - createdAt) + + private val createdAt = currentTime + +} diff --git a/src/main/kotlin/engine/freeBody/Planet.kt b/src/main/kotlin/engine/freeBody/Planet.kt index e6ae3b4..c0adaf7 100644 --- a/src/main/kotlin/engine/freeBody/Planet.kt +++ b/src/main/kotlin/engine/freeBody/Planet.kt @@ -3,17 +3,15 @@ package engine.freeBody import display.draw.TextureConfig import engine.motion.Motion import org.jbox2d.collision.shapes.CircleShape -import org.jbox2d.collision.shapes.Shape import org.jbox2d.dynamics.* class Planet( id: String, motion: Motion, - shapeBox: Shape, worldBody: Body, radius: Float, textureConfig: TextureConfig -) : FreeBody(id, motion, shapeBox, worldBody, radius, textureConfig) { +) : FreeBody(id, motion, worldBody, radius, textureConfig) { companion object { @@ -38,7 +36,7 @@ class Planet( val bodyDef = createBodyDef(BodyType.DYNAMIC, x, y, h, dx, dy, dh) val worldBody = createWorldBody(shapeBox, mass, radius, friction, restitution, world, bodyDef) - return Planet(id, Motion(), shapeBox, worldBody, radius, textureConfig) + return Planet(id, Motion(), worldBody, radius, textureConfig) } } diff --git a/src/main/kotlin/engine/freeBody/Vehicle.kt b/src/main/kotlin/engine/freeBody/Vehicle.kt index 7ad5168..234ced1 100644 --- a/src/main/kotlin/engine/freeBody/Vehicle.kt +++ b/src/main/kotlin/engine/freeBody/Vehicle.kt @@ -6,20 +6,22 @@ import engine.motion.Motion import engine.shields.VehicleShield import game.GamePlayer import org.jbox2d.collision.shapes.PolygonShape -import org.jbox2d.collision.shapes.Shape import org.jbox2d.common.Vec2 import org.jbox2d.dynamics.* +import utility.Common.makeVec2 +import kotlin.math.PI +import kotlin.math.pow class Vehicle( id: String, motion: Motion, - shapeBox: Shape, worldBody: Body, radius: Float, textureConfig: TextureConfig -) : FreeBody(id, motion, shapeBox, worldBody, radius, textureConfig) { +) : FreeBody(id, motion, worldBody, radius, textureConfig) { var shield: VehicleShield? = null + var hitPoints: Float = 100f companion object { @@ -38,20 +40,36 @@ class Vehicle( friction: Float = .6f, textureConfig: TextureConfig ): Vehicle { - val shapeBox = PolygonShape() - val vertices = BasicShapes.polygon4.chunked(2).map { Vec2(it[0] * radius, it[1] * radius) }.toTypedArray() - shapeBox.set(vertices, vertices.size) - + val fullShape = BasicShapes.polygon4Spiked.chunked(2) val bodyDef = createBodyDef(BodyType.DYNAMIC, x, y, h, dx, dy, dh) - val worldBody = createWorldBody(shapeBox, mass, radius, friction, restitution, world, bodyDef) - textureConfig.chunkedVertices = - shapeBox.vertices.flatMap { listOf(it.x / radius, it.y / radius) }.chunked(2) - - return Vehicle(player.name, Motion(), shapeBox, worldBody, radius, textureConfig) - .let { - player.vehicle = it - it + val worldBody = world.createBody(bodyDef) + + (listOf(fullShape.last()) + fullShape + listOf(fullShape.first())) + .map { listOf(it[0] * radius, it[1] * radius) } + .windowed(3, 2) + .map { (a, b, c) -> + val shapeBox = PolygonShape() + val vertices = listOf( + makeVec2(a), + makeVec2(b), + makeVec2(c), + Vec2() + ) + .toTypedArray() + shapeBox.set(vertices, vertices.size) + FixtureDef().also { + it.shape = shapeBox + it.density = mass / (PI.toFloat() * radius.pow(2f) * (fullShape.size * .5f)) + it.friction = friction + it.restitution = restitution + } } + .forEach { worldBody.createFixture(it) } + + textureConfig.chunkedVertices = listOf(listOf(0f, 0f)) + fullShape + listOf(fullShape.first()) + + return Vehicle(player.name, Motion(), worldBody, radius, textureConfig) + .also { player.vehicle = it } } } diff --git a/src/main/kotlin/engine/freeBody/Warhead.kt b/src/main/kotlin/engine/freeBody/Warhead.kt new file mode 100644 index 0000000..87f0f5a --- /dev/null +++ b/src/main/kotlin/engine/freeBody/Warhead.kt @@ -0,0 +1,100 @@ +package engine.freeBody + +import display.draw.TextureConfig +import display.draw.TextureEnum +import display.graphic.BasicShapes +import engine.FreeBodyCallback +import engine.motion.Motion +import game.GamePlayer +import org.jbox2d.collision.shapes.CircleShape +import org.jbox2d.collision.shapes.PolygonShape +import org.jbox2d.common.Vec2 +import org.jbox2d.dynamics.Body +import org.jbox2d.dynamics.BodyType +import org.jbox2d.dynamics.World + +class Warhead( + id: String, + val firedBy: GamePlayer, + motion: Motion, + worldBody: Body, + radius: Float, + textureConfig: TextureConfig +) : FreeBody(id, motion, worldBody, radius, textureConfig) { + + private val currentTime + get() = System.currentTimeMillis() + + val ageTime + get() = (currentTime - createdAt) + + private val createdAt = currentTime + val selfDestructTime = 45000f + // TODO: player in current aiming phase could just wait out this time if they wanted to + // also influences score + + val damage = 100f + + fun createParticles( + particles: MutableList, + world: World, + impacted: Body + ): Particle { + val shapeBox = CircleShape() + shapeBox.radius = 2f + + val location = worldBody.position + val velocity = impacted.linearVelocity + val bodyDef = createBodyDef(BodyType.STATIC, location.x, location.y, 0f, velocity.x, velocity.y, 0f) + val worldBody = world.createBody(bodyDef) + // createWorldBody(shapeBox, 0f, radius, 0f, 0f, world, bodyDef) + + val textureConfig = TextureConfig(TextureEnum.white_pixel, chunkedVertices = BasicShapes.polygon30.chunked(2)) + .updateGpuBufferData() + return Particle(id, worldBody, shapeBox.radius, textureConfig) + .also { particles.add(it) } + + } + + companion object { + + fun create( + world: World, + firedBy: GamePlayer, + x: Float, + y: Float, + h: Float, + dx: Float, + dy: Float, + dh: Float, + mass: Float, + radius: Float = .7F, + restitution: Float = .3f, + friction: Float = .6f, + textureConfig: TextureConfig, + onWarheadCollision: (FreeBody, Body) -> Unit + ): Warhead { + val shapeBox = PolygonShape() + val vertices = BasicShapes.polygon4.chunked(2) + .map { Vec2(it[0] * radius * 2f, it[1] * radius) } + .toTypedArray() + shapeBox.set(vertices, vertices.size) + + val bodyDef = createBodyDef(BodyType.DYNAMIC, x, y, h, dx, dy, dh) + val worldBody = createWorldBody(shapeBox, mass, radius, friction, restitution, world, bodyDef) + worldBody.isBullet = true + + textureConfig.chunkedVertices = + shapeBox.vertices.map { listOf(it.x / radius, it.y / radius) } + + return Warhead("1", firedBy, Motion(), worldBody, radius, textureConfig) + .also { + it.textureConfig.updateGpuBufferData() + firedBy.warheads.add(it) + worldBody.userData = FreeBodyCallback(it, onWarheadCollision) + } + } + + } + +} diff --git a/src/main/kotlin/game/GamePhaseHandler.kt b/src/main/kotlin/game/GamePhaseHandler.kt index caa5f1d..f05c5d0 100644 --- a/src/main/kotlin/game/GamePhaseHandler.kt +++ b/src/main/kotlin/game/GamePhaseHandler.kt @@ -1,21 +1,25 @@ package game -import display.draw.Drawer -import display.draw.TextureHolder import display.KeyboardEvent import display.Window +import display.draw.Drawer +import display.draw.TextureEnum import display.events.MouseButtonEvent +import display.events.MouseScrollEvent import display.graphic.Color import display.gui.GuiController +import display.text.TextJustify import engine.GameState +import engine.GameState.Companion.getContactBodies +import engine.motion.Director import engine.shields.VehicleShield import org.jbox2d.common.Vec2 +import utility.Common.getTimingFunctionEaseIn import utility.Common.getTimingFunctionEaseOut -import utility.Common.getTimingFunctionSineEaseIn import utility.Common.vectorUnit import kotlin.math.roundToInt -class GamePhaseHandler(private val gameState: GameState, val drawer: Drawer, val textures: TextureHolder) { +class GamePhaseHandler(private val gameState: GameState, val drawer: Drawer) { private val timeStep = 1f / 60f private val velocityIterations = 8 @@ -36,31 +40,30 @@ class GamePhaseHandler(private val gameState: GameState, val drawer: Drawer, val private var lastPhaseTimestamp = currentTime private val guiController = GuiController(drawer) + private val textInputIsBusy + get() = guiController.textInputIsBusy() private lateinit var exitCall: () -> Unit fun init(window: Window) { exitCall = { window.exit() } + when (0) { + 0 -> setupMainMenu() + 1 -> setupMainMenuSelectPlayers() + 2 -> { - setupMainMenu() - -// gameState.gamePlayers.add(GamePlayer("Bob")) -// setupStartGame() - } - - fun dragMouseRightClick(movement: Vec2) { - camera.moveLocation(movement.mulLocal(-camera.z)) - } + currentPhase = GamePhases.PLAYERS_PICK_SHIELDS + isTransitioning = false + gameState.reset() + gameState.gamePlayers.addAll((1..3).map { GamePlayer("Player $it") }) + MapGenerator.populateNewGameMap(gameState) - fun scrollCamera(movement: Float) { - camera.moveZoom(movement * -.001f) - } + gameState.gamePlayers.forEach { it.vehicle?.shield = VehicleShield() } + gameState.playerOnTurn = gameState.gamePlayers.first() - fun pauseGame(event: KeyboardEvent) { - when (currentPhase) { - GamePhases.PAUSE -> currentPhase = GamePhases.PLAY - GamePhases.PLAY -> currentPhase = GamePhases.PAUSE + setupNextPlayersTurn() + } + else -> throw Throwable("Enter a debug step number to start game") } - startTransition() } private fun startTransition() { @@ -68,26 +71,92 @@ class GamePhaseHandler(private val gameState: GameState, val drawer: Drawer, val isTransitioning = true } + private fun startNewPhase(newPhase: GamePhases) { + currentPhase = newPhase + startTransition() + } + fun update() { camera.update() val cp = currentPhase when { - cp == GamePhases.PAUSE && isTransitioning -> tickGamePausing(pauseDownDuration, GamePhases.PAUSE) + cp == GamePhases.PAUSE && isTransitioning -> tickGamePausing() cp == GamePhases.PAUSE -> return - cp == GamePhases.PLAY && isTransitioning -> tickGameUnpausing(pauseDownDuration, GamePhases.PLAY) + cp == GamePhases.PLAY && isTransitioning -> tickGameUnpausing() cp == GamePhases.MAIN_MENU -> return cp == GamePhases.MAIN_MENU_SELECT_PLAYERS -> return - cp == GamePhases.NEW_GAME_INTRO && isTransitioning -> tickGameUnpausing( - pauseDownDuration, GamePhases.NEW_GAME_INTRO - ) + cp == GamePhases.NEW_GAME_INTRO && isTransitioning -> tickGameUnpausing() cp == GamePhases.NEW_GAME_INTRO -> handleIntro() cp == GamePhases.PLAYERS_PICK_SHIELDS && isTransitioning -> { - if (elapsedTime < pauseDownDuration) isTransitioning = false + if (elapsedTime > pauseTime) isTransitioning = false } cp == GamePhases.PLAYERS_PICK_SHIELDS -> return cp == GamePhases.PLAYERS_TURN -> return + cp == GamePhases.PLAYERS_TURN_FIRED && isTransitioning -> tickGameUnpausing(quickStartTime) + cp == GamePhases.PLAYERS_TURN_FIRED -> handlePlayerShot() + cp == GamePhases.PLAYERS_TURN_FIRED_ENDS_EARLY -> handlePlayerShotEndsEarly() + cp == GamePhases.PLAYERS_TURN_AIMING -> return + cp == GamePhases.PLAYERS_TURN_POWERING -> return + cp == GamePhases.END_ROUND && isTransitioning -> tickGamePausing(outroDuration, endSpeed = .1f) + cp == GamePhases.END_ROUND -> gameState.tickClock(timeStep * .1f, velocityIterations, positionIterations) + + else -> gameState.tickClock(timeStep, velocityIterations, positionIterations) + } + } + + fun render() { + when (currentPhase) { + GamePhases.MAIN_MENU -> guiController.render() + GamePhases.MAIN_MENU_SELECT_PLAYERS -> guiController.render() + GamePhases.PLAYERS_PICK_SHIELDS -> drawWorldAndGui() + GamePhases.PLAYERS_TURN -> drawWorldAndGui() + GamePhases.PLAYERS_TURN_AIMING -> { + drawWorldAndGui() + drawer.drawPlayerAimingPointer(gameState.playerOnTurn!!) + } + GamePhases.PLAYERS_TURN_POWERING -> { + drawWorldAndGui() + drawer.drawPlayerAimingPointer(gameState.playerOnTurn!!) + } + GamePhases.END_ROUND -> drawWorldAndGui() + else -> drawPlayPhase() + } + + drawer.renderer.drawText( + "Animating: ${isTransitioning.toString().padEnd(5, ' ')} ${currentPhase.name}", + Vec2(120f - camera.windowWidth * .5f, -10f + camera.windowHeight * .5f), + vectorUnit.mul(0.1f), Color.GREEN, TextJustify.LEFT, false + ) + + drawer.renderer.drawText( + "${elapsedTime.div(100f).roundToInt().div(10f)} seconds", + Vec2(40f - camera.windowWidth * .5f, -30f + camera.windowHeight * .5f), + vectorUnit.mul(0.1f), Color.GREEN, TextJustify.LEFT, false + ) + } + + private fun handlePlayerShotEndsEarly() { + when { + isTransitioning -> tickGamePausing() + else -> setupNextPlayersTurn() + } + } + private fun handlePlayerShot() { + val roundEndsEarly = (gameState.warheads.none() + && gameState.particles.none() + && gameState.vehicles + .all { + it.worldBody.contactList != null + && getContactBodies(it.worldBody.contactList).any { other -> other.mass > 50f } + }) + when { + roundEndsEarly -> if (!checkStateEndOfRound()) startNewPhase(GamePhases.PLAYERS_TURN_FIRED_ENDS_EARLY) + elapsedTime > maxTurnDuration -> setupNextPlayersTurn() + elapsedTime > (maxTurnDuration - pauseTime) -> tickGamePausing( + pauseTime, calculatedElapsedTime = (elapsedTime - maxTurnDuration + pauseTime) + ) else -> gameState.tickClock(timeStep, velocityIterations, positionIterations) } } @@ -96,7 +165,7 @@ class GamePhaseHandler(private val gameState: GameState, val drawer: Drawer, val when { elapsedTime > introDuration -> playerSelectsShield() elapsedTime > (introDuration - introStartSlowdown) -> tickGamePausing( - introStartSlowdown, GamePhases.PLAYERS_PICK_SHIELDS, (elapsedTime - introDuration + introStartSlowdown) + introStartSlowdown, calculatedElapsedTime = (elapsedTime - introDuration + introStartSlowdown) ) else -> gameState.tickClock(timeStep, velocityIterations, positionIterations) } @@ -106,97 +175,101 @@ class GamePhaseHandler(private val gameState: GameState, val drawer: Drawer, val player?.vehicle?.shield = VehicleShield() if (gameState.gamePlayers.all { it.vehicle?.shield != null }) { - setupPlayersTurn() + setupNextPlayersTurn() return } - guiController.clear() + setNextPlayerOnTurn() setupPlayersPickShields() currentPhase = GamePhases.PLAYERS_PICK_SHIELDS - startTransition() + // startTransition() } - private fun setupPlayersTurn() { - guiController.clear() + private fun setupNextPlayersTurn() { + if (checkStateEndOfRound()) { + return + } + + gameState.gamePlayers + .joinToString { "${it.name} HP:${it.vehicle!!.hitPoints.toInt()}; " } + .also { println(it) } + setNextPlayerOnTurn() setupPlayerCommandPanel() - currentPhase = GamePhases.PLAYERS_TURN - startTransition() + startNewPhase(GamePhases.PLAYERS_TURN) + } + + private fun checkStateEndOfRound(): Boolean { + val vehiclesDestroyed = gameState.gamePlayers.count { it.vehicle!!.hitPoints > 0 } < 2 + if (vehiclesDestroyed) { + startNewPhase(GamePhases.END_ROUND) + guiController.createRoundLeaderboard(gameState.gamePlayers, + onClickNextRound = { setupMainMenuSelectPlayers() }) + return true + } + return false } private fun setupPlayerCommandPanel() { - guiController.clear() guiController.createPlayerCommandPanel( player = gameState.playerOnTurn!!, + onClickAim = { startNewPhase(GamePhases.PLAYERS_TURN_AIMING) }, + onClickPower = { startNewPhase(GamePhases.PLAYERS_TURN_POWERING) }, onClickFire = { player -> playerFires(player) } ) } private fun playerFires(player: GamePlayer) { - println("Player ${player.name} fired a gun!") + // check() {} player has enough funds && in stable position to fire large warheads + + val firedWarhead = gameState.fireWarhead(player, "boom small") + camera.trackFreeBody(firedWarhead, 200f) + + startNewPhase(GamePhases.PLAYERS_TURN_FIRED) } private fun setNextPlayerOnTurn() { - check(gameState.playerOnTurn != null) { "Cannot play a game with no players." } + checkNotNull(gameState.playerOnTurn) { "No player is on turn." } val playerOnTurn = gameState.playerOnTurn!! - val players = gameState.gamePlayers + val players = gameState.gamePlayers.filter { it.vehicle!!.hitPoints > 0 } gameState.playerOnTurn = players[(players.indexOf(playerOnTurn) + 1).rem(players.size)] camera.trackFreeBody(gameState.playerOnTurn!!.vehicle!!) } private fun setupPlayersPickShields() { - guiController.clear() guiController.createPlayersPickShields( onClickShield = { player -> playerSelectsShield(player) }, player = gameState.playerOnTurn!! ) } - fun render() { - when (currentPhase) { - GamePhases.MAIN_MENU -> guiController.render() - GamePhases.MAIN_MENU_SELECT_PLAYERS -> guiController.render() - GamePhases.PLAYERS_PICK_SHIELDS -> { - drawPlayPhase() - guiController.render() - } - GamePhases.PLAYERS_TURN -> { - drawPlayPhase() - guiController.render() - } - else -> drawPlayPhase() - } - - drawer.renderer.drawText( - "Animating: ${isTransitioning.toString().padEnd(5, ' ')} ${currentPhase.name}", - Vec2(120f - camera.windowWidth * .5f, -10f + camera.windowHeight * .5f), - vectorUnit.mul(0.1f), Color.GREEN, false - ) - - drawer.renderer.drawText( - "${elapsedTime.div(100f).roundToInt().div(10f)} seconds", - Vec2(40f - camera.windowWidth * .5f, -30f + camera.windowHeight * .5f), - vectorUnit.mul(0.1f), Color.GREEN, false - ) + private fun drawWorldAndGui() { + drawPlayPhase() + guiController.render() } private fun drawPlayPhase() { - drawer.drawPicture(textures.stars_2k) + drawer.drawBackground(TextureEnum.stars_2k) - val allFreeBodies = gameState.tickables + val allFreeBodies = gameState.gravityBodies allFreeBodies.forEach { drawer.drawTrail(it) } + gameState.particles.forEach { drawer.drawParticle(it) } allFreeBodies.forEach { drawer.drawFreeBody(it) } // allFreeBodies.forEach { drawDebugForces(it) } // drawer.drawGravityCells(gameState.gravityMap, gameState.resolution) } - private fun tickGameUnpausing(duration: Float = pauseDownDuration, endPhase: GamePhases) { - val interpolateStep = elapsedTime / duration + private fun tickGameUnpausing( + duration: Float = pauseTime, + endPhase: GamePhases? = null, + calculatedElapsedTime: Float? = null + ) { + val interpolateStep = (calculatedElapsedTime ?: elapsedTime.toFloat()) / duration if (interpolateStep >= 1f) { - currentPhase = endPhase + currentPhase = endPhase ?: currentPhase isTransitioning = false } else { val timeFunctionStep = getTimingFunctionEaseOut(interpolateStep) @@ -205,24 +278,52 @@ class GamePhaseHandler(private val gameState: GameState, val drawer: Drawer, val } private fun tickGamePausing( - duration: Float = pauseDownDuration, - endPhase: GamePhases, - calculatedElapsedTime: Float? = null + duration: Float = pauseTime, + endPhase: GamePhases? = null, + calculatedElapsedTime: Float? = null, + endSpeed: Float = 0f ) { val interpolateStep = (calculatedElapsedTime ?: elapsedTime.toFloat()) / duration if (interpolateStep >= 1f) { - currentPhase = endPhase + currentPhase = endPhase ?: currentPhase isTransitioning = false } else { - val timeFunctionStep = getTimingFunctionSineEaseIn(1f - interpolateStep) + val timeFunctionStep = getTimingFunctionEaseIn(1f - interpolateStep) * (1f - endSpeed) + endSpeed gameState.tickClock(timeStep * timeFunctionStep, velocityIterations, positionIterations) + // println("${roundFloat(interpolateStep, 2).toString().padEnd(4, '0')} <> " + roundFloat(timeFunctionStep, 2).toString().padEnd(4, '0')) + } + } + + fun dragMouseRightClick(movement: Vec2) { + camera.moveLocation(movement.mulLocal(-camera.z)) + } + + fun dragMouseLeftClick(location: Vec2, movement: Vec2) { + guiController.checkLeftClickDrag(getScreenLocation(location), movement) + } + + fun scrollMouse(event: MouseScrollEvent) { + val screenLocation = getScreenLocation(event.location) + if (guiController.locationIsGui(screenLocation)) { + guiController.checkScroll(event.movement, screenLocation) + } else { + camera.moveZoom(event.movement.y * -.001f) } } - fun doubleLeftClick(location: Vec2, click: MouseButtonEvent) { + fun pauseGame(event: KeyboardEvent) { + currentPhase = when (currentPhase) { + GamePhases.PAUSE -> GamePhases.PLAY + GamePhases.PLAY -> GamePhases.PAUSE + else -> GamePhases.PAUSE + } + startTransition() + } + + fun doubleLeftClick(location: Vec2) { val transformedLocation = getScreenLocation(location).mul(camera.z).add(camera.location) - val clickedBody = gameState.tickables.find { + val clickedBody = gameState.gravityBodies.find { it.worldBody.position .add(transformedLocation.mul(-1f)) .length() <= it.radius @@ -241,34 +342,80 @@ class GamePhaseHandler(private val gameState: GameState, val drawer: Drawer, val } fun moveMouse(location: Vec2) { - if (mouseElementPhases.any { currentPhase == it }) { - guiController.checkHover(getScreenLocation(location)) + when { + mouseElementPhases.any { currentPhase == it } -> guiController.checkHover(getScreenLocation(location)) + currentPhase == GamePhases.PLAYERS_TURN_AIMING -> { + val (playerOnTurn, transformedLocation, playerLocation) = getPlayerAndMouseLocations(location) + val aimDirection = Director.getDirection( + transformedLocation.x, transformedLocation.y, playerLocation.x, playerLocation.y + ) + playerOnTurn.playerAim.angle = aimDirection + guiController.update() + } + currentPhase == GamePhases.PLAYERS_TURN_POWERING -> { + val (playerOnTurn, transformedLocation, playerLocation) = getPlayerAndMouseLocations(location) + val distance = Director.getDistance( + transformedLocation.x, transformedLocation.y, playerLocation.x, playerLocation.y + ) + playerOnTurn.playerAim.power = (distance - 1f) * 10f + guiController.update() + } } } + private fun getPlayerAndMouseLocations(location: Vec2): Triple { + checkNotNull(gameState.playerOnTurn) { "No player is on turn." } + val playerOnTurn = gameState.playerOnTurn!! + val transformedLocation = getScreenLocation(location).mul(camera.z).add(camera.location) + val playerLocation = playerOnTurn.vehicle!!.worldBody.position + return Triple(playerOnTurn, transformedLocation, playerLocation) + } + fun leftClickMouse(event: MouseButtonEvent) { - if (mouseElementPhases.any { currentPhase == it }) { - guiController.checkLeftClick(getScreenLocation(event.location)) + when { + mouseElementPhases.any { currentPhase == it } -> guiController.checkLeftClick( + getScreenLocation(event.location)) + currentPhase == GamePhases.PLAYERS_TURN_AIMING -> currentPhase = GamePhases.PLAYERS_TURN + currentPhase == GamePhases.PLAYERS_TURN_POWERING -> currentPhase = GamePhases.PLAYERS_TURN } } private fun getScreenLocation(location: Vec2): Vec2 = - location.add(Vec2(-camera.windowWidth * .5f, -camera.windowHeight * .5f)) - .let { - it.y *= -1f - it - } + location.add(Vec2(-camera.windowWidth, -camera.windowHeight).mul(.5f)) + .also { it.y *= -1f } fun keyPressEscape(event: KeyboardEvent) { + if (textInputIsBusy) { + guiController.stopTextInput() + return + } when (currentPhase) { GamePhases.MAIN_MENU -> exitCall() else -> setupMainMenu() } } + fun keyPressBackspace(event: KeyboardEvent) { + if (textInputIsBusy) { + guiController.checkRemoveTextInput() + } + } + + fun keyPressEnter(event: KeyboardEvent) { + if (textInputIsBusy) { + guiController.stopTextInput() + } + } + + fun inputText(text: String) { + if (textInputIsBusy) { + guiController.checkAddTextInput(text) + } + } + private fun setupMainMenu() { currentPhase = GamePhases.MAIN_MENU - guiController.clear() + gameState.reset() guiController.createMainMenu( onClickNewGame = { setupMainMenuSelectPlayers() }, onClickSettings = {}, @@ -279,7 +426,6 @@ class GamePhaseHandler(private val gameState: GameState, val drawer: Drawer, val private fun setupMainMenuSelectPlayers() { currentPhase = GamePhases.MAIN_MENU_SELECT_PLAYERS gameState.reset() - guiController.clear() guiController.createMainMenuSelectPlayers( onClickStart = { setupStartGame() }, onClickCancel = { setupMainMenu() }, @@ -302,28 +448,33 @@ class GamePhaseHandler(private val gameState: GameState, val drawer: Drawer, val } private fun setupStartGame() { - currentPhase = GamePhases.NEW_GAME_INTRO - startTransition() + gameState.gamePlayers.withIndex() + .filter { (_, player) -> player.name.length <= 1 } + .forEach { (index, player) -> player.name = "Player ${index + 1}" } + guiController.clear() + startNewPhase(GamePhases.NEW_GAME_INTRO) - MapGenerator.populateNewGameMap(gameState, textures) + MapGenerator.populateNewGameMap(gameState) - if (gameState.gamePlayers.size > 0) { - val startingPlayer = gameState.gamePlayers.random() - gameState.playerOnTurn = startingPlayer - } + check(gameState.gamePlayers.size > 1) { "Cannot play a game with less than 2 players." } + gameState.playerOnTurn = gameState.gamePlayers.random() } companion object { - private const val pauseDownDuration = 1000f - private const val introDuration = 5000f + private const val pauseTime = 1000f + private const val introDuration = 3500f private const val introStartSlowdown = 2000f - private const val maxPlayDuration = 30000f + private const val maxTurnDuration = 20000f + private const val quickStartTime = 300f + private const val outroDuration = 5000f + private val mouseElementPhases = listOf( GamePhases.MAIN_MENU, GamePhases.MAIN_MENU_SELECT_PLAYERS, GamePhases.PLAYERS_PICK_SHIELDS, - GamePhases.PLAYERS_TURN + GamePhases.PLAYERS_TURN, + GamePhases.END_ROUND ) } diff --git a/src/main/kotlin/game/GamePhases.kt b/src/main/kotlin/game/GamePhases.kt index e5a7360..7024f65 100644 --- a/src/main/kotlin/game/GamePhases.kt +++ b/src/main/kotlin/game/GamePhases.kt @@ -8,6 +8,11 @@ enum class GamePhases { NEW_GAME_INTRO, PLAYERS_PICK_SHIELDS, NONE, - PLAYERS_TURN + PLAYERS_TURN, + PLAYERS_TURN_FIRED, + PLAYERS_TURN_FIRED_ENDS_EARLY, + PLAYERS_TURN_AIMING, + PLAYERS_TURN_POWERING, + END_ROUND } diff --git a/src/main/kotlin/game/GamePlayer.kt b/src/main/kotlin/game/GamePlayer.kt index 384a2c4..16c2b66 100644 --- a/src/main/kotlin/game/GamePlayer.kt +++ b/src/main/kotlin/game/GamePlayer.kt @@ -1,9 +1,40 @@ package game import engine.freeBody.Vehicle +import engine.freeBody.Warhead +import utility.Common.getTimingFunctionEaseIn class GamePlayer( - val name: String, + var name: String, val type: GamePlayerTypes = GamePlayerTypes.HUMAN, - var vehicle: Vehicle? = null -) + var vehicle: Vehicle? = null, + val playerAim: PlayerAim = PlayerAim(), + var score: Float = 0f +) { + + fun scoreDamage(warhead: Warhead, totalDamage: Float, vehicle: Vehicle) { + val selfHarm = when (vehicle) { + this.vehicle -> -.5f + else -> 1f + } + val noAgeBonusTime = 2000f + val age = warhead.ageTime + .minus(noAgeBonusTime) + .coerceAtLeast(0f) + .div(warhead.selfDestructTime - noAgeBonusTime) + .let { getTimingFunctionEaseIn(it) * 5 + 1f } + + score += selfHarm * totalDamage * age + } + + fun scoreKill(vehicle: Vehicle) { + val selfHarm = when (vehicle) { + this.vehicle -> -.5f + else -> 1f + } + score += selfHarm * 2f + } + + val warheads = mutableListOf() + +} diff --git a/src/main/kotlin/game/MapGenerator.kt b/src/main/kotlin/game/MapGenerator.kt index 06d2565..a8db1de 100644 --- a/src/main/kotlin/game/MapGenerator.kt +++ b/src/main/kotlin/game/MapGenerator.kt @@ -1,86 +1,90 @@ package game -import Vector2f import display.draw.TextureConfig -import display.draw.TextureHolder +import display.draw.TextureEnum import display.graphic.BasicShapes +import display.graphic.Color import engine.GameState import engine.freeBody.Planet import engine.freeBody.Vehicle import engine.motion.Director +import org.jbox2d.common.Vec2 import org.jbox2d.dynamics.World +import utility.Common.vectorUnit import kotlin.math.PI import kotlin.math.cos import kotlin.math.sin object MapGenerator { - fun populateNewGameMap(gameState: GameState, textures: TextureHolder) { + fun populateNewGameMap(gameState: GameState) { val terra = Planet.create( - gameState.world, "terra", 0f, 0f, 0f, 0f, 0f, .1f, 1800f, 4.5f, .3f, - textureConfig = TextureConfig(textures.marble_earth, chunkedVertices = BasicShapes.polygon30.chunked(2)) + gameState.world, "terra", 0f, 0f, 0f, 0f, 0f, .1f, 1600f, 4.5f, .3f, + textureConfig = TextureConfig(TextureEnum.marble_earth, chunkedVertices = BasicShapes.polygon30.chunked(2)) ) val luna = Planet.create( - gameState.world, "luna", -20f, 0f, 0f, 0f, 4.4f, -.4f, 100f, 1.25f, .5f, - textureConfig = TextureConfig(textures.full_moon, chunkedVertices = BasicShapes.polygon30.chunked(2)) + gameState.world, "luna", -25f, 0f, 0f, 0f, 4.8f, -.4f, 100f, 1.25f, .5f, + textureConfig = TextureConfig(TextureEnum.full_moon, chunkedVertices = BasicShapes.polygon30.chunked(2)) ) + val colorStartIndex = Math.random().times(11).toInt() val vehicles = gameState.gamePlayers.withIndex().map { (index, player) -> Vehicle.create( - gameState.world, player, -30f * index + 15f * gameState.gamePlayers.size, + gameState.world, player, -10f * index + .5f * gameState.gamePlayers.size, 10f, index * 1f, 0f, 0f, 1f, 3f, radius = .75f, - textureConfig = TextureConfig(textures.metal, Vector2f(.7f, .7f), Vector2f(0f, 0f)) + textureConfig = TextureConfig(TextureEnum.metal, vectorUnit.mul(.7f), + color = Color.PALETTE_TINT10[((colorStartIndex + index) * 2).rem(11)]) ) } gameState.vehicles.addAll(vehicles) gameState.planets.addAll(listOf(terra, luna)) - gameState.planets.addAll(createPlanets(gameState.world, 5, textures)) + gameState.planets.addAll(createAsteroids(gameState.world, 15)) - gameState.tickables.forEach { it.textureConfig.updateGpuBufferData() } + gameState.gravityBodies.forEach { it.textureConfig.updateGpuBufferData() } } - fun populateTestMap(gameState: GameState, textures: TextureHolder) { + fun populateTestMap(gameState: GameState) { val terra = Planet.create( gameState.world, "terra", 0f, 0f, 0f, 0f, 0f, .1f, 1800f, 4.5f, .3f, textureConfig = TextureConfig( - textures.marble_earth, - Vector2f(1f, 1f), - Vector2f(0f, 0f), + TextureEnum.marble_earth, + vectorUnit, + Vec2(), BasicShapes.polygon30.chunked(2) ) ) val luna = Planet.create( gameState.world, "luna", -20f, 0f, 0f, 0f, 4.4f, -.4f, 100f, 1.25f, .5f, textureConfig = TextureConfig( - textures.full_moon, - Vector2f(1f, 1f), - Vector2f(0f, 0f), + TextureEnum.full_moon, + vectorUnit.mul(.7f), + Vec2(), BasicShapes.polygon30.chunked(2) ) ) val alice = Vehicle.create( gameState.world, GamePlayer("alice"), -30f, 5f, 0f, -2f, 2.7f, 1f, 3f, radius = .75f, - textureConfig = TextureConfig(textures.metal, Vector2f(.7f, .7f), Vector2f(0f, 0f), listOf()) + textureConfig = TextureConfig(TextureEnum.metal, vectorUnit.mul(.7f)) ) val bob = Vehicle.create( gameState.world, GamePlayer("bob"), 25f, 0f, 0f, 2f, -3f, 0f, 3f, radius = .75f, - textureConfig = TextureConfig(textures.metal, Vector2f(.7f, .7f), Vector2f(0f, 0f), listOf()) + textureConfig = TextureConfig(TextureEnum.metal, vectorUnit.mul(.7f)) ) gameState.vehicles.addAll(listOf(alice, bob)) gameState.planets.addAll(listOf(terra, luna)) - gameState.planets.addAll(createPlanets(gameState.world, 20, textures)) + gameState.planets.addAll(createAsteroids(gameState.world, 20)) - gameState.tickables.forEach { it.textureConfig.updateGpuBufferData() } + gameState.gravityBodies.forEach { it.textureConfig.updateGpuBufferData() } } - private fun createPlanets(world: World, count: Int, textures: TextureHolder): List { + private fun createAsteroids(world: World, count: Int): List { return (1..count) .withIndex() .map { (i, _) -> val ratio = (2 * PI * 0.07 * i).toFloat() - val radius = 10f + val radius = 35f floatArrayOf( (i * .04f + radius) * cos(ratio), (i * .04f + radius) * sin(ratio), @@ -89,7 +93,7 @@ object MapGenerator { } .map { val direction = Director.getDirection(-it[0], -it[1]) + PI * .5f - val speed = 8f + val speed = 4.7f Planet.create( world, "${it[2].toInt()}", it[0], it[1], 0f, cos(direction).toFloat() * speed, @@ -97,9 +101,9 @@ object MapGenerator { .5f, 0.3f * it[2].rem(6f), .2f + it[2].rem(6f) * .05f, friction = .6f, textureConfig = TextureConfig( - textures.pavement, - Vector2f(.3f, .3f), - Vector2f(it[0].rem(20f) / 10 - 1, it[1].rem(20f) / 10 - 1), + TextureEnum.pavement, + vectorUnit.mul(.3f), + Vec2(it[0].rem(20f) / 10 - 1, it[1].rem(20f) / 10 - 1), BasicShapes.polygon9.chunked(2) ) ) diff --git a/src/main/kotlin/game/PlayerAim.kt b/src/main/kotlin/game/PlayerAim.kt new file mode 100644 index 0000000..d133228 --- /dev/null +++ b/src/main/kotlin/game/PlayerAim.kt @@ -0,0 +1,16 @@ +package game + +import utility.Common +import utility.Common.Pi2 +import utility.Common.radianToDegree + +class PlayerAim(var angle: Float = 0f, power: Float = 100f) { + + var power = power + set(value) { + field = value.coerceIn(0f, 100f) + } + + fun getDegreesAngle() = (angle + Pi2) % Pi2 * radianToDegree + +} diff --git a/src/main/kotlin/input/CameraView.kt b/src/main/kotlin/input/CameraView.kt index 2e3a1c9..5217306 100644 --- a/src/main/kotlin/input/CameraView.kt +++ b/src/main/kotlin/input/CameraView.kt @@ -1,5 +1,6 @@ package input +import Matrix4f import display.Window import engine.freeBody.FreeBody import org.jbox2d.common.Vec2 @@ -19,7 +20,7 @@ class CameraView(private val window: Window) { var currentPhase = CameraPhases.STATIC var lastStaticLocation = location private var lastPhaseTimestamp = System.currentTimeMillis() - private val transitionDuration = 1000f + private var transitionDuration = 1000f private lateinit var trackFreeBody: FreeBody @@ -47,12 +48,13 @@ class CameraView(private val window: Window) { location = position } - fun trackFreeBody(newFreeBody: FreeBody) { + fun trackFreeBody(newFreeBody: FreeBody, transitionTime: Float = 1000f) { currentPhase = CameraPhases.TRANSITION_TO_TARGET lastPhaseTimestamp = System.currentTimeMillis() trackFreeBody = newFreeBody lastStaticLocation = location + transitionDuration = transitionTime } fun moveLocation(movement: Vec2) { @@ -68,6 +70,12 @@ class CameraView(private val window: Window) { z = .05f } + fun getRenderCamera(): Matrix4f { + val zoomScale = 1f / z + return Matrix4f.scale(zoomScale, zoomScale, 1f) + .multiply(Matrix4f.translate(-location.x, -location.y, 0f)) + } + } enum class CameraPhases { diff --git a/src/main/kotlin/input/InputHandler.kt b/src/main/kotlin/input/InputHandler.kt index 12523f5..b1fd179 100644 --- a/src/main/kotlin/input/InputHandler.kt +++ b/src/main/kotlin/input/InputHandler.kt @@ -2,6 +2,7 @@ package input import display.events.MouseButtonEvent import display.Window +import display.events.MouseScrollEvent import game.GamePhaseHandler import io.reactivex.subjects.PublishSubject import org.jbox2d.common.Vec2 @@ -12,12 +13,19 @@ class InputHandler(private val gamePhaseHandler: GamePhaseHandler) { private val unsubscribe = PublishSubject.create() fun init(window: Window) { - setupDragRightClick(window) + setupDragClick(window) setupDoubleLeftClick(window) setupMouseScroll(window) setupKeyboard(window) setupMouseMove(window) setupMouseClicks(window) + setupTextInput(window) + } + + private fun setupTextInput(window: Window) { + window.textInputEvent.takeUntil(unsubscribe).subscribe { + gamePhaseHandler.inputText(it) + } } private fun setupMouseClicks(window: Window) { @@ -41,10 +49,12 @@ class InputHandler(private val gamePhaseHandler: GamePhaseHandler) { when (it.action) { GLFW.GLFW_PRESS -> { when (it.key) { - GLFW.GLFW_KEY_SPACE -> gamePhaseHandler.pauseGame(it) + // GLFW.GLFW_KEY_SPACE -> gamePhaseHandler.pauseGame(it) GLFW.GLFW_KEY_LEFT -> gamePhaseHandler.keyPressArrowLeft(it) GLFW.GLFW_KEY_RIGHT -> gamePhaseHandler.keyPressArrowRight(it) GLFW.GLFW_KEY_ESCAPE -> gamePhaseHandler.keyPressEscape(it) + GLFW.GLFW_KEY_BACKSPACE -> gamePhaseHandler.keyPressBackspace(it) + GLFW.GLFW_KEY_ENTER -> gamePhaseHandler.keyPressEnter(it) } } } @@ -53,13 +63,7 @@ class InputHandler(private val gamePhaseHandler: GamePhaseHandler) { private fun setupMouseScroll(window: Window) { window.mouseScrollEvent.takeUntil(unsubscribe) - .subscribe { gamePhaseHandler.scrollCamera(it.y) } - } - - private fun setupDragRightClick(window: Window) { - val mouseButtonRelease = PublishSubject.create() - window.mouseButtonEvent.takeUntil(unsubscribe) - .subscribe { click -> dragRightClick(click, window, mouseButtonRelease) } + .subscribe { gamePhaseHandler.scrollMouse(it) } } private fun setupDoubleLeftClick(window: Window) { @@ -73,27 +77,56 @@ class InputHandler(private val gamePhaseHandler: GamePhaseHandler) { } .filter { (isDoubleClick, _) -> isDoubleClick } .takeUntil(unsubscribe) - .subscribe { (_, click) -> gamePhaseHandler.doubleLeftClick(window.getCursorPosition(), click) } + .subscribe { (_, click) -> gamePhaseHandler.doubleLeftClick(window.getCursorPosition()) } } - private fun dragRightClick(click: MouseButtonEvent, window: Window, mouseButtonRelease: PublishSubject) { - if (click.button == GLFW.GLFW_MOUSE_BUTTON_RIGHT && click.action == GLFW.GLFW_PRESS) { + private fun setupDragClick(window: Window) { + val mouseButtonLeftRelease = PublishSubject.create() + val mouseButtonRightRelease = PublishSubject.create() + window.mouseButtonEvent.takeUntil(unsubscribe) + .subscribe { click -> dragClick(click, window, mouseButtonLeftRelease, mouseButtonRightRelease) } + } - var startLocation: Vec2? = null - window.cursorPositionEvent.takeUntil(mouseButtonRelease).subscribe { location -> - location.x *= -1f + private fun dragClick(click: MouseButtonEvent, + window: Window, + mouseButtonLeftRelease: PublishSubject, + mouseButtonRightRelease: PublishSubject + ) { + if (click.button == GLFW.GLFW_MOUSE_BUTTON_LEFT) { + when (click.action) { + GLFW.GLFW_PRESS -> { + window.cursorPositionEvent.takeUntil(mouseButtonLeftRelease) + .subscribe { + handleMouseMovement(click.location, it) { movement -> + gamePhaseHandler.dragMouseLeftClick(click.location, movement) + } + } + } + GLFW.GLFW_RELEASE -> mouseButtonLeftRelease.onNext(true) + } + } - if (startLocation != null) { - val movement = startLocation!!.add(location.mul(-1f)) - gamePhaseHandler.dragMouseRightClick(movement) + if (click.button == GLFW.GLFW_MOUSE_BUTTON_RIGHT) { + when (click.action) { + GLFW.GLFW_PRESS -> { + window.cursorPositionEvent.takeUntil(mouseButtonRightRelease) + .subscribe { + handleMouseMovement(click.location, it) { movement -> + gamePhaseHandler.dragMouseRightClick(movement) + } + } } - startLocation = location + GLFW.GLFW_RELEASE -> mouseButtonRightRelease.onNext(true) } - } else if (click.button == GLFW.GLFW_MOUSE_BUTTON_RIGHT && click.action == GLFW.GLFW_RELEASE) { - mouseButtonRelease.onNext(true) } } + private fun handleMouseMovement(startLocation: Vec2, location: Vec2, callback: (Vec2) -> Unit) { + val movement = startLocation.add(location.mul(-1f)).also { it.x *= -1f } + callback(movement) + startLocation.set(location) + } + fun dispose() { unsubscribe.onNext(true) } diff --git a/src/main/kotlin/utility/Common.kt b/src/main/kotlin/utility/Common.kt index 82be51f..47dc789 100644 --- a/src/main/kotlin/utility/Common.kt +++ b/src/main/kotlin/utility/Common.kt @@ -1,6 +1,9 @@ package utility +import org.jbox2d.common.MathUtils import org.jbox2d.common.Vec2 +import java.io.File +import java.nio.DoubleBuffer import java.util.* import kotlin.math.* @@ -17,6 +20,14 @@ object Common { return result } + fun getSafePath(resourcePath: String): String { + val alternativePath = "src/main/resources/${resourcePath.substring(1)}" + return when { + File(alternativePath).exists() -> alternativePath + else -> resourcePath + } + } + fun joinLists(aList: List, bList: List): Sequence> = sequence { aList.forEach { aItem -> bList.forEach { bItem -> @@ -35,21 +46,34 @@ object Common { fun roundFloat(value: Float, decimals: Int = 2): Float { val multiplier = 10f.pow(decimals) - return (value * multiplier).roundToInt() / multiplier + return value.times(multiplier).roundToInt().div(multiplier) } + const val Pi2 = 2f * PI.toFloat() + val vectorUnit = Vec2(1f, 1f) + fun makeVec2(list: List): Vec2 = Vec2(list[0], list[1]) + + fun makeVec2(x: Number, y: Number): Vec2 = Vec2(x.toFloat(), y.toFloat()) + + fun makeVec2(x: DoubleBuffer, y: DoubleBuffer): Vec2 = makeVec2(x.get(), y.get()) + + fun makeVec2(duplicate: Number): Vec2 = makeVec2(duplicate, duplicate) + + fun makeVec2Circle(angle: Float): Vec2 = Vec2(cos(angle), sin(angle)) + val radianToDegree = Math.toDegrees(1.0).toFloat() fun getTimingFunctionEaseOut(interpolateStep: Float) = getTimingFunctionFullSine(sqrt(interpolateStep)) - fun getTimingFunctionSineEaseIn(interpolateStep: Float) = 1f - getTimingFunctionEaseOut(1f - interpolateStep) + fun getTimingFunctionEaseIn(interpolateStep: Float) = 1f - getTimingFunctionEaseOut(1f - interpolateStep) fun getTimingFunctionFullSine(interpolateStep: Float) = (sin(interpolateStep * PI - PI * .5) * .5 + .5).toFloat() fun getTimingFunctionSigmoid(interpolateStep: Float, centerGradient: Float = 1f) = - (1f / (1f + exp((-(interpolateStep - .5f) * 10f)) * centerGradient)) - + (1f / (1f + exp((-(interpolateStep - .5f) * 10f)) * centerGradient)) * 1.023f - 0.0022f } + +fun Vec2.toList(): List = listOf(this.x, this.y) diff --git a/src/main/resources/textures/icon_aim.png b/src/main/resources/textures/icon_aim.png new file mode 100644 index 0000000..895e8b7 Binary files /dev/null and b/src/main/resources/textures/icon_aim.png differ diff --git a/src/test/kotlin/engine/motion/DirectorTest.kt b/src/test/kotlin/engine/motion/DirectorTest.kt index be0c1ed..bc797ff 100644 --- a/src/test/kotlin/engine/motion/DirectorTest.kt +++ b/src/test/kotlin/engine/motion/DirectorTest.kt @@ -3,6 +3,7 @@ package engine.motion import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import utility.Common +import utility.Common.roundFloat import kotlin.math.PI internal class DirectorTest { @@ -10,41 +11,45 @@ internal class DirectorTest { @Test fun distance() { val rightPointed = Director.getDistance(0f, 0f, 1f, 1f) - assertEquals(1.414f, Common.roundFloat(rightPointed, 3)) + assertEquals(1.414f, roundFloat(rightPointed, 3)) val leftPointed = Director.getDistance(0f, 0f, -1f, -1f) - assertEquals(1.414f, Common.roundFloat(leftPointed, 3)) + assertEquals(1.414f, roundFloat(leftPointed, 3)) } @Test fun direction() { // Test direction from client, to server val pointed0 = Director.getDirection(0f, 0f, -1f, 0f) - assertEquals(0f, pointed0, "0") + assertRoundEquals(0, pointed0, "0") val pointed45 = Director.getDirection(0f, 0f, -1f, -1f) - assertEquals(PI / 4, pointed45, "45") + assertRoundEquals(PI / 4, pointed45, "45") val pointed90 = Director.getDirection(0f, 0f, 0f, -1f) - assertEquals(PI / 2, pointed90, "90") + assertRoundEquals(PI / 2, pointed90, "90") val pointed135 = Director.getDirection(0f, 0f, 1f, -1f) - assertEquals(PI * 3 / 4, pointed135, "135") + assertRoundEquals(PI * 3 / 4, pointed135, "135") val pointed180 = Director.getDirection(0f, 0f, 1f, 0f) - assertEquals(PI, pointed180, "180") + assertRoundEquals(PI, pointed180, "180") val pointed225 = Director.getDirection(0f, 0f, 1f, 1f) - assertEquals(-PI * 3 / 4, pointed225, "225") + assertRoundEquals(-PI * 3 / 4, pointed225, "225") val pointed270 = Director.getDirection(0f, 0f, 0f, 1f) - assertEquals(-PI / 2, pointed270, "270") + assertRoundEquals(-PI / 2, pointed270, "270") val pointed315 = Director.getDirection(0f, 0f, -1f, 1f) - assertEquals(-PI / 4, pointed315, "315") + assertRoundEquals(-PI / 4, pointed315, "315") val vector = Director.getDirection(1f, 0f) - assertEquals(0f, vector, "vector") + assertRoundEquals(0, vector, "vector") + } + + private fun assertRoundEquals(expected: Number, actual: Float, message: String) { + assertEquals(roundFloat(expected.toFloat(), 2), roundFloat(actual, 2), message) } } diff --git a/src/test/kotlin/engine/physics/GravityTest.kt b/src/test/kotlin/engine/physics/GravityTest.kt index b51d40a..43db042 100644 --- a/src/test/kotlin/engine/physics/GravityTest.kt +++ b/src/test/kotlin/engine/physics/GravityTest.kt @@ -1,7 +1,7 @@ package engine.physics import display.draw.TextureConfig -import display.graphic.Texture +import display.draw.TextureEnum import engine.freeBody.Planet import org.jbox2d.common.Vec2 import org.jbox2d.dynamics.World @@ -29,17 +29,17 @@ internal class GravityTest { assertTrue(forceUpLeft.y > 0) } - @Test - fun in_binary_system_the_massive_body_moves_less() { - } +// @Test +// fun in_binary_system_the_massive_body_moves_less() { +// } private fun getGravityForceBetweenPlanetSatellite(sx: Float = 0f, sy: Float = 0f): Vec2 { - val world = World(Vec2(0f, 0f)) + val world = World(Vec2()) val terra = Planet.create(world, "terra", sx, sy, 0f, 0f, 0f, 0f, 100f, 10f, - textureConfig = TextureConfig(Texture())) + textureConfig = TextureConfig(TextureEnum.white_pixel)) val luna = Planet.create(world, "luna", 0f, 0f, 0f, 0f, 0f, 0f, 100f, 10f, - textureConfig = TextureConfig(Texture())) + textureConfig = TextureConfig(TextureEnum.white_pixel)) return Gravity.gravitationalForce(luna, terra) } diff --git a/src/test/kotlin/utility/CommonTest.kt b/src/test/kotlin/utility/CommonTest.kt new file mode 100644 index 0000000..a19db5a --- /dev/null +++ b/src/test/kotlin/utility/CommonTest.kt @@ -0,0 +1,45 @@ +package utility + +import org.jbox2d.common.Vec2 +import org.junit.jupiter.api.Test + +import utility.Common.getTimingFunctionEaseIn +import utility.Common.getTimingFunctionEaseOut +import utility.Common.getTimingFunctionFullSine +import utility.Common.getTimingFunctionSigmoid +import utility.Common.joinLists +import utility.Common.roundFloat + +internal class CommonTest { + + fun testest() { + + val data = getChartedData { + getTimingFunctionEaseIn(1f - it) + } + println(data) + } + + private fun getChartedData(timingFunction: (Float) -> Float): String { + val resolution = 50 + val values = (0 until resolution).map { it / resolution.minus(1).toFloat() } + .map { timingFunction(it) } + .map { roundFloat(it, 2) } + + val visualGraph = HashMap, Float>() + values.withIndex().forEach { (index, y) -> + visualGraph[Pair(index, (y * (resolution)).toInt())] = y + } + + val intRange = (0 until resolution).toList() + val result = joinLists(intRange, intRange) + .sortedBy { (x, y) -> -y } + .sortedBy { (x, y) -> x } + .map { (x, y) -> visualGraph[Pair(y, x)] } + .map { if (it != null) "x" else "." } + .chunked(resolution) + .map { it.joinToString("") } + .joinToString("\n") + return result + } +}