Подгрузка описания прожектайлов и их тест рендер
This commit is contained in:
parent
4fc51530f7
commit
ad8910d098
@ -31,11 +31,12 @@ tasks.compileKotlin {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("stdlib"))
|
implementation(kotlin("stdlib"))
|
||||||
|
implementation(kotlin("reflect"))
|
||||||
|
|
||||||
implementation("org.apache.logging.log4j:log4j-api:2.17.1")
|
implementation("org.apache.logging.log4j:log4j-api:2.17.1")
|
||||||
implementation("org.apache.logging.log4j:log4j-core:2.17.1")
|
implementation("org.apache.logging.log4j:log4j-core:2.17.1")
|
||||||
|
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
|
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
|
||||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
||||||
|
|
||||||
implementation("com.google.code.gson:gson:2.8.9")
|
implementation("com.google.code.gson:gson:2.8.9")
|
||||||
|
@ -3,16 +3,15 @@ package ru.dbotthepony.kstarbound
|
|||||||
import org.apache.logging.log4j.LogManager
|
import org.apache.logging.log4j.LogManager
|
||||||
import org.lwjgl.Version
|
import org.lwjgl.Version
|
||||||
import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose
|
import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose
|
||||||
import ru.dbotthepony.kstarbound.api.PhysicalFS
|
|
||||||
import ru.dbotthepony.kstarbound.client.StarboundClient
|
import ru.dbotthepony.kstarbound.client.StarboundClient
|
||||||
import ru.dbotthepony.kstarbound.defs.TileDefinition
|
import ru.dbotthepony.kstarbound.defs.TileDefinition
|
||||||
import ru.dbotthepony.kstarbound.io.StarboundPak
|
|
||||||
import ru.dbotthepony.kstarbound.math.Vector2d
|
import ru.dbotthepony.kstarbound.math.Vector2d
|
||||||
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
|
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
|
||||||
import ru.dbotthepony.kstarbound.world.Chunk
|
import ru.dbotthepony.kstarbound.world.Chunk
|
||||||
import ru.dbotthepony.kstarbound.world.ChunkPos
|
import ru.dbotthepony.kstarbound.world.ChunkPos
|
||||||
import ru.dbotthepony.kstarbound.world.entities.Move
|
import ru.dbotthepony.kstarbound.world.entities.Move
|
||||||
import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
|
import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
|
||||||
|
import ru.dbotthepony.kstarbound.world.entities.Projectile
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
private val LOGGER = LogManager.getLogger()
|
private val LOGGER = LogManager.getLogger()
|
||||||
@ -20,6 +19,14 @@ private val LOGGER = LogManager.getLogger()
|
|||||||
fun main() {
|
fun main() {
|
||||||
LOGGER.info("Running LWJGL ${Version.getVersion()}")
|
LOGGER.info("Running LWJGL ${Version.getVersion()}")
|
||||||
|
|
||||||
|
if (true) {
|
||||||
|
//val pak = StarboundPak(File("J:\\SteamLibrary\\steamapps\\common\\Starbound\\assets\\packed.pak"))
|
||||||
|
//val json = JsonParser.parseReader(pak.getReader("/projectiles/traps/lowgravboostergas/lowgravboostergas.projectile"))
|
||||||
|
//val obj = Gson().fromJson(json, ProjectileDefinitionBuilder::class.java)
|
||||||
|
//println(obj.build())
|
||||||
|
//return
|
||||||
|
}
|
||||||
|
|
||||||
val client = StarboundClient()
|
val client = StarboundClient()
|
||||||
|
|
||||||
//Starbound.addFilePath(File("./unpacked_assets/"))
|
//Starbound.addFilePath(File("./unpacked_assets/"))
|
||||||
@ -108,7 +115,13 @@ fun main() {
|
|||||||
chunkA!!.foreground[rand.nextInt(0, CHUNK_SIZE_FF), rand.nextInt(0, CHUNK_SIZE_FF)] = tile
|
chunkA!!.foreground[rand.nextInt(0, CHUNK_SIZE_FF), rand.nextInt(0, CHUNK_SIZE_FF)] = tile
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
ent.dropToFloor()
|
ent.movement.dropToFloor()
|
||||||
|
|
||||||
|
for ((i, proj) in Starbound.projectilesAccess.values.withIndex()) {
|
||||||
|
val projEnt = Projectile(client.world!!, proj)
|
||||||
|
projEnt.pos = Vector2d(i * 2.0, 10.0)
|
||||||
|
projEnt.spawn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//val rand = Random()
|
//val rand = Random()
|
||||||
@ -117,7 +130,7 @@ fun main() {
|
|||||||
|
|
||||||
client.onDrawGUI {
|
client.onDrawGUI {
|
||||||
client.gl.font.render("${ent.pos}", y = 100f, scale = 0.25f)
|
client.gl.font.render("${ent.pos}", y = 100f, scale = 0.25f)
|
||||||
client.gl.font.render("${ent.velocity}", y = 120f, scale = 0.25f)
|
client.gl.font.render("${ent.movement.velocity}", y = 120f, scale = 0.25f)
|
||||||
}
|
}
|
||||||
|
|
||||||
client.onPreDrawWorld {
|
client.onPreDrawWorld {
|
||||||
@ -140,17 +153,17 @@ fun main() {
|
|||||||
//ent.velocity += client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1
|
//ent.velocity += client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1
|
||||||
|
|
||||||
if (client.input.KEY_LEFT_DOWN) {
|
if (client.input.KEY_LEFT_DOWN) {
|
||||||
ent.moveDirection = Move.MOVE_LEFT
|
ent.movement.moveDirection = Move.MOVE_LEFT
|
||||||
} else if (client.input.KEY_RIGHT_DOWN) {
|
} else if (client.input.KEY_RIGHT_DOWN) {
|
||||||
ent.moveDirection = Move.MOVE_RIGHT
|
ent.movement.moveDirection = Move.MOVE_RIGHT
|
||||||
} else {
|
} else {
|
||||||
ent.moveDirection = Move.STAND_STILL
|
ent.movement.moveDirection = Move.STAND_STILL
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client.input.KEY_SPACE_PRESSED && ent.onGround) {
|
if (client.input.KEY_SPACE_PRESSED && ent.movement.onGround) {
|
||||||
ent.requestJump()
|
ent.movement.requestJump()
|
||||||
} else if (client.input.KEY_SPACE_RELEASED) {
|
} else if (client.input.KEY_SPACE_RELEASED) {
|
||||||
ent.recallJump()
|
ent.movement.recallJump()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client.input.KEY_ESCAPE_PRESSED) {
|
if (client.input.KEY_ESCAPE_PRESSED) {
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
package ru.dbotthepony.kstarbound
|
package ru.dbotthepony.kstarbound
|
||||||
|
|
||||||
import com.google.gson.JsonElement
|
import com.google.gson.*
|
||||||
import com.google.gson.JsonObject
|
import org.apache.logging.log4j.LogManager
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import ru.dbotthepony.kstarbound.api.IVFS
|
import ru.dbotthepony.kstarbound.api.IVFS
|
||||||
import ru.dbotthepony.kstarbound.api.PhysicalFS
|
import ru.dbotthepony.kstarbound.api.PhysicalFS
|
||||||
import ru.dbotthepony.kstarbound.defs.TileDefinition
|
import ru.dbotthepony.kstarbound.api.getPathFolder
|
||||||
import ru.dbotthepony.kstarbound.defs.TileDefinitionBuilder
|
import ru.dbotthepony.kstarbound.defs.*
|
||||||
|
import ru.dbotthepony.kstarbound.defs.projectile.ConfigurableProjectile
|
||||||
|
import ru.dbotthepony.kstarbound.defs.projectile.ConfiguredProjectile
|
||||||
|
import ru.dbotthepony.kstarbound.defs.projectile.ProjectilePhysics
|
||||||
import ru.dbotthepony.kstarbound.io.StarboundPak
|
import ru.dbotthepony.kstarbound.io.StarboundPak
|
||||||
import ru.dbotthepony.kstarbound.world.World
|
import ru.dbotthepony.kstarbound.math.*
|
||||||
|
import ru.dbotthepony.kstarbound.util.Color
|
||||||
|
import ru.dbotthepony.kstarbound.util.ColorTypeAdapter
|
||||||
|
import ru.dbotthepony.kstarbound.util.CustomEnumTypeAdapter
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import java.text.DateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.collections.HashMap
|
import kotlin.collections.HashMap
|
||||||
@ -22,10 +28,31 @@ const val PIXELS_IN_STARBOUND_UNIT = 8.0
|
|||||||
const val PIXELS_IN_STARBOUND_UNITf = 8.0f
|
const val PIXELS_IN_STARBOUND_UNITf = 8.0f
|
||||||
|
|
||||||
class TileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause)
|
class TileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause)
|
||||||
|
class ProjectileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause)
|
||||||
|
|
||||||
object Starbound : IVFS {
|
object Starbound : IVFS {
|
||||||
|
private val LOGGER = LogManager.getLogger()
|
||||||
private val tiles = HashMap<String, TileDefinition>()
|
private val tiles = HashMap<String, TileDefinition>()
|
||||||
val tilesAccess = object : Map<String, TileDefinition> by tiles {}
|
private val projectiles = HashMap<String, ConfiguredProjectile>()
|
||||||
|
val tilesAccess = Collections.unmodifiableMap(tiles)
|
||||||
|
val projectilesAccess = Collections.unmodifiableMap(projectiles)
|
||||||
|
|
||||||
|
val gson = GsonBuilder()
|
||||||
|
.enableComplexMapKeySerialization()
|
||||||
|
.serializeNulls()
|
||||||
|
.setDateFormat(DateFormat.LONG)
|
||||||
|
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
|
||||||
|
.setPrettyPrinting()
|
||||||
|
.registerTypeAdapter(Color::class.java, ColorTypeAdapter.nullSafe())
|
||||||
|
.registerTypeAdapter(ProjectilePhysics::class.java, CustomEnumTypeAdapter(ProjectilePhysics.values()).nullSafe())
|
||||||
|
.registerTypeAdapter(AABB::class.java, AABBTypeAdapter)
|
||||||
|
.registerTypeAdapter(AABBi::class.java, AABBiTypeAdapter)
|
||||||
|
.registerTypeAdapter(Vector2d::class.java, Vector2dTypeAdapter)
|
||||||
|
.registerTypeAdapter(Vector2f::class.java, Vector2fTypeAdapter)
|
||||||
|
.registerTypeAdapter(Vector2i::class.java, Vector2iTypeAdapter)
|
||||||
|
.registerTypeAdapter(Poly::class.java, PolyTypeAdapter)
|
||||||
|
.registerTypeAdapter(ConfigurableProjectile::class.java, ConfigurableProjectile.ADAPTER)
|
||||||
|
.create()
|
||||||
|
|
||||||
var initializing = false
|
var initializing = false
|
||||||
private set
|
private set
|
||||||
@ -87,17 +114,36 @@ object Starbound : IVFS {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(false, false, "Loading materials...")
|
run {
|
||||||
|
val localTime = System.currentTimeMillis()
|
||||||
|
|
||||||
loadTileMaterials {
|
callback(false, false, "Loading materials...")
|
||||||
if (terminateLoading) {
|
|
||||||
throw InterruptedException("Game is terminating")
|
loadTileMaterials {
|
||||||
|
if (terminateLoading) {
|
||||||
|
throw InterruptedException("Game is terminating")
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(false, true, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(false, true, it)
|
callback(false, true, "Loaded materials in ${System.currentTimeMillis() - localTime}ms")
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(false, true, "Loaded materials")
|
run {
|
||||||
|
val localTime = System.currentTimeMillis()
|
||||||
|
callback(false, false, "Loading projectiles...")
|
||||||
|
|
||||||
|
loadProjectiles {
|
||||||
|
if (terminateLoading) {
|
||||||
|
throw InterruptedException("Game is terminating")
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(false, true, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(false, true, "Loaded Projectiles in ${System.currentTimeMillis() - localTime}ms")
|
||||||
|
}
|
||||||
|
|
||||||
initializing = false
|
initializing = false
|
||||||
initialized = true
|
initialized = true
|
||||||
@ -135,6 +181,16 @@ object Starbound : IVFS {
|
|||||||
return listing
|
return listing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun listDirectories(path: String): Collection<String> {
|
||||||
|
val listing = mutableListOf<String>()
|
||||||
|
|
||||||
|
for (fs in fileSystems) {
|
||||||
|
listing.addAll(fs.listDirectories(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
return listing
|
||||||
|
}
|
||||||
|
|
||||||
fun onInitialize(callback: () -> Unit) {
|
fun onInitialize(callback: () -> Unit) {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
callback()
|
callback()
|
||||||
@ -155,7 +211,7 @@ object Starbound : IVFS {
|
|||||||
|
|
||||||
private fun loadTileMaterials(callback: (String) -> Unit) {
|
private fun loadTileMaterials(callback: (String) -> Unit) {
|
||||||
for (fs in fileSystems) {
|
for (fs in fileSystems) {
|
||||||
for (listedFile in fs.listFiles("tiles/materials")) {
|
for (listedFile in fs.listAllFiles("tiles/materials")) {
|
||||||
if (listedFile.endsWith(".material")) {
|
if (listedFile.endsWith(".material")) {
|
||||||
try {
|
try {
|
||||||
callback("Loading $listedFile")
|
callback("Loading $listedFile")
|
||||||
@ -171,4 +227,23 @@ object Starbound : IVFS {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadProjectiles(callback: (String) -> Unit) {
|
||||||
|
for (fs in fileSystems) {
|
||||||
|
for (listedFile in fs.listAllFiles("projectiles")) {
|
||||||
|
if (listedFile.endsWith(".projectile")) {
|
||||||
|
try {
|
||||||
|
callback("Loading $listedFile")
|
||||||
|
|
||||||
|
val def = gson.fromJson(getReader(listedFile), ConfigurableProjectile::class.java).configure(getPathFolder(listedFile))
|
||||||
|
check(projectiles[def.projectileName] == null) { "Already has projectile with ID ${def.projectileName} loaded!" }
|
||||||
|
projectiles[def.projectileName] = def
|
||||||
|
} catch(err: Throwable) {
|
||||||
|
//throw ProjectileDefLoadingException("Loading projectile file $listedFile", err)
|
||||||
|
LOGGER.error("Loading projectile file $listedFile", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,29 @@ import java.nio.ByteBuffer
|
|||||||
interface IVFS {
|
interface IVFS {
|
||||||
fun pathExists(path: String): Boolean
|
fun pathExists(path: String): Boolean
|
||||||
|
|
||||||
|
fun pathExistsOrElse(path: String, orElse: String): String {
|
||||||
|
if (pathExists(path))
|
||||||
|
return path
|
||||||
|
|
||||||
|
return orElse
|
||||||
|
}
|
||||||
|
|
||||||
|
fun firstExisting(vararg pathList: String): String {
|
||||||
|
for (path in pathList)
|
||||||
|
if (pathExists(path))
|
||||||
|
return path
|
||||||
|
|
||||||
|
throw FileNotFoundException("Unable to find any of files specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun firstExistingOrNull(vararg pathList: String): String? {
|
||||||
|
for (path in pathList)
|
||||||
|
if (pathExists(path))
|
||||||
|
return path
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
fun read(path: String): ByteBuffer {
|
fun read(path: String): ByteBuffer {
|
||||||
return readOrNull(path) ?: throw FileNotFoundException("No such file $path")
|
return readOrNull(path) ?: throw FileNotFoundException("No such file $path")
|
||||||
}
|
}
|
||||||
@ -21,6 +44,35 @@ interface IVFS {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun listFiles(path: String): Collection<String>
|
fun listFiles(path: String): Collection<String>
|
||||||
|
fun listDirectories(path: String): Collection<String>
|
||||||
|
|
||||||
|
fun listFilesAndDirectories(path: String): Collection<String> {
|
||||||
|
val a = listFiles(path)
|
||||||
|
val b = listDirectories(path)
|
||||||
|
|
||||||
|
return ArrayList<String>(a.size + b.size).also { it.addAll(a); it.addAll(b) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listAllFiles(path: String): Collection<String> {
|
||||||
|
val lists = mutableListOf<Collection<String>>()
|
||||||
|
|
||||||
|
lists.add(listFiles(path))
|
||||||
|
|
||||||
|
for (dir in listDirectories(path)) {
|
||||||
|
lists.add(listAllFiles(dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
// flatten медленный
|
||||||
|
// return lists.flatten()
|
||||||
|
|
||||||
|
var size = 0
|
||||||
|
|
||||||
|
for (list in lists) {
|
||||||
|
size += list.size
|
||||||
|
}
|
||||||
|
|
||||||
|
return ArrayList<String>(size).also { lists.forEach(it::addAll) }
|
||||||
|
}
|
||||||
|
|
||||||
fun readDirect(path: String): ByteBuffer {
|
fun readDirect(path: String): ByteBuffer {
|
||||||
val read = read(path)
|
val read = read(path)
|
||||||
@ -39,6 +91,10 @@ interface IVFS {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPathFolder(path: String): String {
|
||||||
|
return path.substring(0, path.lastIndexOf('/'))
|
||||||
|
}
|
||||||
|
|
||||||
class PhysicalFS(root: File) : IVFS {
|
class PhysicalFS(root: File) : IVFS {
|
||||||
val root: File = root.absoluteFile
|
val root: File = root.absoluteFile
|
||||||
|
|
||||||
@ -71,8 +127,20 @@ class PhysicalFS(root: File) : IVFS {
|
|||||||
if (path.contains("..")) {
|
if (path.contains("..")) {
|
||||||
return listOf()
|
return listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
val fpath = File(root.absolutePath, path)
|
val fpath = File(root.absolutePath, path)
|
||||||
return fpath.listFiles()?.map {
|
return fpath.listFiles()?.filter { it.isFile }?.map {
|
||||||
|
it.path.replace('\\', '/').substring(root.path.length)
|
||||||
|
} ?: return listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun listDirectories(path: String): Collection<String> {
|
||||||
|
if (path.contains("..")) {
|
||||||
|
return listOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
val fpath = File(root.absolutePath, path)
|
||||||
|
return fpath.listFiles()?.filter { it.isDirectory }?.map {
|
||||||
it.path.replace('\\', '/').substring(root.path.length)
|
it.path.replace('\\', '/').substring(root.path.length)
|
||||||
} ?: return listOf()
|
} ?: return listOf()
|
||||||
}
|
}
|
||||||
|
@ -266,6 +266,9 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
|
|||||||
layerQueue.add(baked::renderStacked to zLevel)
|
layerQueue.add(baked::renderStacked to zLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//println("${entityRenderers.size} at $pos")
|
||||||
|
//println("${entities.size} at $pos")
|
||||||
|
|
||||||
for (renderer in entityRenderers.values) {
|
for (renderer in entityRenderers.values) {
|
||||||
layerQueue.add(lambda@{ it: Matrix4fStack ->
|
layerQueue.add(lambda@{ it: Matrix4fStack ->
|
||||||
val relative = renderer.renderPos - posVector2d
|
val relative = renderer.renderPos - posVector2d
|
||||||
@ -284,7 +287,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
|
|||||||
private val entityRenderers = HashMap<Entity, EntityRenderer>()
|
private val entityRenderers = HashMap<Entity, EntityRenderer>()
|
||||||
|
|
||||||
override fun onEntityAdded(entity: Entity) {
|
override fun onEntityAdded(entity: Entity) {
|
||||||
entityRenderers[entity] = EntityRenderer(state, entity, this)
|
entityRenderers[entity] = EntityRenderer.getRender(state, entity, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEntityTransferedToThis(entity: Entity, otherChunk: ClientChunk) {
|
override fun onEntityTransferedToThis(entity: Entity, otherChunk: ClientChunk) {
|
||||||
|
@ -9,7 +9,7 @@ import org.lwjgl.opengl.GL46.*
|
|||||||
// GL_STACK_UNDERFLOW
|
// GL_STACK_UNDERFLOW
|
||||||
// GL_OUT_OF_MEMORY
|
// GL_OUT_OF_MEMORY
|
||||||
|
|
||||||
sealed class OpenGLError(message: String, val statusCode: Int) : Throwable(message)
|
sealed class OpenGLError(message: String, val statusCode: Int) : RuntimeException(message)
|
||||||
|
|
||||||
class OpenGLUnknownError(statusCode: Int, message: String = "Unknown OpenGL error occured: $statusCode") : OpenGLError(message, statusCode)
|
class OpenGLUnknownError(statusCode: Int, message: String = "Unknown OpenGL error occured: $statusCode") : OpenGLError(message, statusCode)
|
||||||
|
|
||||||
|
@ -253,11 +253,30 @@ class GLStateTracker {
|
|||||||
private val named2DTextures = HashMap<String, GLTexture2D>()
|
private val named2DTextures = HashMap<String, GLTexture2D>()
|
||||||
|
|
||||||
fun loadNamedTexture(path: String, memoryFormat: Int = GL_RGBA, fileFormat: Int = GL_RGBA): GLTexture2D {
|
fun loadNamedTexture(path: String, memoryFormat: Int = GL_RGBA, fileFormat: Int = GL_RGBA): GLTexture2D {
|
||||||
if (!Starbound.pathExists(path)) {
|
return named2DTextures.computeIfAbsent(path) {
|
||||||
throw FileNotFoundException("Unable to locate $path")
|
if (!Starbound.pathExists(path)) {
|
||||||
|
throw FileNotFoundException("Unable to locate $path")
|
||||||
|
}
|
||||||
|
|
||||||
|
return@computeIfAbsent newTexture(path).upload(Starbound.readDirect(path), memoryFormat, fileFormat).generateMips()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var loadedEmptyTexture = false
|
||||||
|
private val missingTexturePath = "/assetmissing.png"
|
||||||
|
|
||||||
|
fun loadNamedTextureSafe(path: String, memoryFormat: Int = GL_RGBA, fileFormat: Int = GL_RGBA): GLTexture2D {
|
||||||
|
if (!loadedEmptyTexture) {
|
||||||
|
loadedEmptyTexture = true
|
||||||
|
named2DTextures[missingTexturePath] = newTexture(missingTexturePath).upload(Starbound.readDirect(missingTexturePath), memoryFormat, fileFormat).generateMips()
|
||||||
}
|
}
|
||||||
|
|
||||||
return named2DTextures.computeIfAbsent(path) {
|
return named2DTextures.computeIfAbsent(path) {
|
||||||
|
if (!Starbound.pathExists(path)) {
|
||||||
|
LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath)
|
||||||
|
return@computeIfAbsent named2DTextures[missingTexturePath]!!
|
||||||
|
}
|
||||||
|
|
||||||
return@computeIfAbsent newTexture(path).upload(Starbound.readDirect(path), memoryFormat, fileFormat).generateMips()
|
return@computeIfAbsent newTexture(path).upload(Starbound.readDirect(path), memoryFormat, fileFormat).generateMips()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -359,6 +378,16 @@ class GLStateTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val flat2DTexturedQuads = object : GLStreamBuilderList {
|
||||||
|
override val small by lazy {
|
||||||
|
return@lazy StreamVertexBuilder(GLFlatAttributeList.VERTEX_TEXTURE, VertexType.QUADS, 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val statefulSmall by lazy {
|
||||||
|
return@lazy StatefulStreamVertexBuilder(this@GLStateTracker, small)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val flat2DQuadLines = object : GLStreamBuilderList {
|
val flat2DQuadLines = object : GLStreamBuilderList {
|
||||||
override val small by lazy {
|
override val small by lazy {
|
||||||
return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS_AS_LINES, 1024)
|
return@lazy StreamVertexBuilder(GLFlatAttributeList.VEC2F, VertexType.QUADS_AS_LINES, 1024)
|
||||||
|
@ -45,6 +45,22 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
|
|||||||
var uploaded = false
|
var uploaded = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
val aspectRatioWH: Float get() {
|
||||||
|
if (height == 0) {
|
||||||
|
return 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
return width.toFloat() / height.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
val aspectRatioHW: Float get() {
|
||||||
|
if (width == 0) {
|
||||||
|
return 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
return height.toFloat() / width.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
private var mipsWarning = 2
|
private var mipsWarning = 2
|
||||||
|
|
||||||
var textureMinFilter by GLTexturePropertyTracker(GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
var textureMinFilter by GLTexturePropertyTracker(GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
package ru.dbotthepony.kstarbound.client.render
|
package ru.dbotthepony.kstarbound.client.render
|
||||||
|
|
||||||
|
import org.lwjgl.opengl.GL46.*
|
||||||
import ru.dbotthepony.kstarbound.client.ClientChunk
|
import ru.dbotthepony.kstarbound.client.ClientChunk
|
||||||
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
|
import ru.dbotthepony.kstarbound.client.gl.GLStateTracker
|
||||||
|
import ru.dbotthepony.kstarbound.client.gl.VertexTransformers
|
||||||
import ru.dbotthepony.kstarbound.math.Matrix4fStack
|
import ru.dbotthepony.kstarbound.math.Matrix4fStack
|
||||||
import ru.dbotthepony.kstarbound.math.Vector2d
|
import ru.dbotthepony.kstarbound.math.Vector2d
|
||||||
import ru.dbotthepony.kstarbound.world.entities.Entity
|
import ru.dbotthepony.kstarbound.world.entities.Entity
|
||||||
|
import ru.dbotthepony.kstarbound.world.entities.Projectile
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,7 +24,7 @@ open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open va
|
|||||||
|
|
||||||
open fun renderDebug() {
|
open fun renderDebug() {
|
||||||
if (chunk?.world?.client?.settings?.debugCollisions == true) {
|
if (chunk?.world?.client?.settings?.debugCollisions == true) {
|
||||||
state.quadWireframe(entity.worldaabb)
|
state.quadWireframe(entity.movement.worldAABB)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,4 +33,46 @@ open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open va
|
|||||||
override fun close() {
|
override fun close() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun getRender(state: GLStateTracker, entity: Entity, chunk: ClientChunk? = null): EntityRenderer {
|
||||||
|
return when (entity) {
|
||||||
|
is Projectile -> ProjectileRenderer(state, entity, chunk)
|
||||||
|
else -> EntityRenderer(state, entity, chunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class ProjectileRenderer(state: GLStateTracker, entity: Projectile, chunk: ClientChunk?) : EntityRenderer(state, entity, chunk) {
|
||||||
|
private val def = entity.def
|
||||||
|
private val texture = state.loadNamedTextureSafe(def.image.texture)
|
||||||
|
private val animator = FrameSetAnimator(def.image, def.animationCycle, true)
|
||||||
|
|
||||||
|
init {
|
||||||
|
texture.textureMagFilter = GL_NEAREST
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun render(stack: Matrix4fStack) {
|
||||||
|
state.shaderVertexTexture.use()
|
||||||
|
state.shaderVertexTexture.transform.set(stack.last)
|
||||||
|
state.activeTexture = 0
|
||||||
|
state.shaderVertexTexture["_texture"] = 0
|
||||||
|
texture.bind()
|
||||||
|
|
||||||
|
animator.advance()
|
||||||
|
|
||||||
|
val stateful = state.flat2DTexturedQuads.statefulSmall
|
||||||
|
val builder = stateful.builder
|
||||||
|
|
||||||
|
builder.begin()
|
||||||
|
|
||||||
|
val (u0, v0) = texture.pixelToUV(def.image.frames[animator.frame].texturePosition)
|
||||||
|
val (u1, v1) = texture.pixelToUV(def.image.frames[animator.frame].textureEndPosition)
|
||||||
|
|
||||||
|
builder.quadZ(0f, 0f, 1f, def.image.frames[animator.frame].aspectRatioHW, 5f, VertexTransformers.uv(u0, v0, u1, v1))
|
||||||
|
|
||||||
|
stateful.upload()
|
||||||
|
stateful.draw()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
package ru.dbotthepony.kstarbound.client.render
|
||||||
|
|
||||||
|
import org.lwjgl.glfw.GLFW.glfwGetTime
|
||||||
|
import ru.dbotthepony.kstarbound.defs.FrameSet
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Анимирует заданный FrameSet
|
||||||
|
*/
|
||||||
|
class FrameSetAnimator(
|
||||||
|
val set: FrameSet,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сколько времени занимает один кадр
|
||||||
|
*/
|
||||||
|
var animationCycle: Double,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Зациклить ли анимацию
|
||||||
|
*/
|
||||||
|
var animationLoops: Boolean,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Последний кадр анимации
|
||||||
|
*/
|
||||||
|
var lastFrame = set.frameCount - 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Первый кадр анимации
|
||||||
|
*/
|
||||||
|
var firstFrame = 0
|
||||||
|
|
||||||
|
var frame = 0
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает разницу между последним и первым кадром анимации
|
||||||
|
*/
|
||||||
|
val frameDiff get() = lastFrame - firstFrame
|
||||||
|
|
||||||
|
private val initial = glfwGetTime()
|
||||||
|
private var lastRender = initial
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сколько времени прошло с момента последнего кадра
|
||||||
|
*/
|
||||||
|
val delta get() = glfwGetTime() - lastRender
|
||||||
|
|
||||||
|
private var counter = 0.0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет glfw таймер и продвигает фрейм анимации
|
||||||
|
*/
|
||||||
|
fun advance() {
|
||||||
|
if (frameDiff == 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (frame + frameDiff >= lastFrame && !animationLoops) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
counter += delta / animationCycle
|
||||||
|
lastRender = glfwGetTime()
|
||||||
|
|
||||||
|
if (counter >= 1.0) {
|
||||||
|
frame = (frame + counter.toInt()) % frameDiff
|
||||||
|
counter %= 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/main/kotlin/ru/dbotthepony/kstarbound/defs/Animation.kt
Normal file
14
src/main/kotlin/ru/dbotthepony/kstarbound/defs/Animation.kt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package ru.dbotthepony.kstarbound.defs
|
||||||
|
|
||||||
|
import ru.dbotthepony.kstarbound.math.Vector2i
|
||||||
|
|
||||||
|
class AnimationDefinitionBuilder {
|
||||||
|
var frames: String? = null
|
||||||
|
var variants: Int? = null
|
||||||
|
var frameNumber: Int? = null
|
||||||
|
var animationCycle: Double? = null
|
||||||
|
var offset: Vector2i? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnimationDefinition {
|
||||||
|
}
|
@ -0,0 +1,269 @@
|
|||||||
|
package ru.dbotthepony.kstarbound.defs
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList
|
||||||
|
import com.google.common.collect.ImmutableMap
|
||||||
|
import com.google.gson.*
|
||||||
|
import com.google.gson.internal.bind.TypeAdapters
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import com.google.gson.stream.JsonToken
|
||||||
|
import com.google.gson.stream.JsonWriter
|
||||||
|
import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap
|
||||||
|
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
|
||||||
|
import it.unimi.dsi.fastutil.objects.ObjectArrayList
|
||||||
|
import it.unimi.dsi.fastutil.objects.ObjectArraySet
|
||||||
|
import org.apache.logging.log4j.LogManager
|
||||||
|
import ru.dbotthepony.kstarbound.Starbound
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.KMutableProperty1
|
||||||
|
import kotlin.reflect.KType
|
||||||
|
import kotlin.reflect.full.isSuperclassOf
|
||||||
|
|
||||||
|
private fun flattenJsonPrimitive(input: JsonPrimitive): Any {
|
||||||
|
if (input.isNumber) {
|
||||||
|
return input.asNumber
|
||||||
|
} else if (input.isString) {
|
||||||
|
return input.asString.intern()
|
||||||
|
} else {
|
||||||
|
return input.asBoolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun flattenJsonArray(input: JsonArray): ArrayList<Any> {
|
||||||
|
val flattened = ArrayList<Any>(input.size())
|
||||||
|
|
||||||
|
for (v in input) {
|
||||||
|
when (v) {
|
||||||
|
is JsonObject -> flattened.add(flattenJsonObject(v))
|
||||||
|
is JsonArray -> flattened.add(flattenJsonArray(v))
|
||||||
|
is JsonPrimitive -> flattened.add(flattenJsonPrimitive(v))
|
||||||
|
// is JsonNull -> baked.add(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return flattened
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun flattenJsonObject(input: JsonObject): Object2ObjectArrayMap<String, Any> {
|
||||||
|
val flattened = Object2ObjectArrayMap<String, Any>()
|
||||||
|
|
||||||
|
for ((k, v) in input.entrySet()) {
|
||||||
|
when (v) {
|
||||||
|
is JsonObject -> flattened[k] = flattenJsonObject(v)
|
||||||
|
is JsonArray -> flattened[k] = flattenJsonArray(v)
|
||||||
|
is JsonPrimitive -> flattened[k] = flattenJsonPrimitive(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return flattened
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flattenJsonElement(input: JsonElement): Any? {
|
||||||
|
return when (input) {
|
||||||
|
is JsonObject -> flattenJsonObject(input)
|
||||||
|
is JsonArray -> flattenJsonArray(input)
|
||||||
|
is JsonPrimitive -> flattenJsonPrimitive(input)
|
||||||
|
is JsonNull -> null
|
||||||
|
else -> throw IllegalArgumentException("Unknown argument $input")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов
|
||||||
|
*/
|
||||||
|
fun enrollList(input: List<Any>): ImmutableList<Any> {
|
||||||
|
val builder = ImmutableList.builder<Any>()
|
||||||
|
|
||||||
|
for (v in input) {
|
||||||
|
when (v) {
|
||||||
|
is Map<*, *> -> builder.add(enrollMap(v as Map<String, Any>))
|
||||||
|
is List<*> -> builder.add(enrollList(v as List<Any>))
|
||||||
|
else -> builder.add((v as? String)?.intern() ?: v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает глубокую неизменяемую копию [input] примитивов/List'ов/Map'ов
|
||||||
|
*/
|
||||||
|
fun enrollMap(input: Map<String, Any>): ImmutableMap<String, Any> {
|
||||||
|
val builder = ImmutableMap.builder<String, Any>()
|
||||||
|
|
||||||
|
for ((k, v) in input) {
|
||||||
|
when (v) {
|
||||||
|
is Map<*, *> -> builder.put(k.intern(), enrollMap(v as Map<String, Any>))
|
||||||
|
is List<*> -> builder.put(k.intern(), enrollList(v as List<Any>))
|
||||||
|
else -> builder.put(k.intern(), (v as? String)?.intern() ?: v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает глубокую изменяемую копию [input] примитивов/List'ов/Map'ов
|
||||||
|
*/
|
||||||
|
fun flattenList(input: List<Any>): ArrayList<Any> {
|
||||||
|
val list = ArrayList<Any>(input.size)
|
||||||
|
|
||||||
|
for (v in input) {
|
||||||
|
when (v) {
|
||||||
|
is Map<*, *> -> list.add(flattenMap(v as Map<String, Any>))
|
||||||
|
is List<*> -> list.add(flattenList(v as List<Any>))
|
||||||
|
else -> list.add(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flattenMap(input: Map<String, Any>): Object2ObjectArrayMap<String, Any> {
|
||||||
|
val map = Object2ObjectArrayMap<String, Any>()
|
||||||
|
|
||||||
|
for ((k, v) in input) {
|
||||||
|
when (v) {
|
||||||
|
is Map<*, *> -> map[k] = flattenMap(v as Map<String, Any>)
|
||||||
|
is List<*> -> map[k] = flattenList(v as List<Any>)
|
||||||
|
else -> map[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Базовый класс описания прототипа игрового объекта
|
||||||
|
*
|
||||||
|
* Должен иметь все (или больше) поля объекта, который он будет создавать
|
||||||
|
*
|
||||||
|
* Поля должны иметь базовые ограничения (т.е. ограничения, которые применимы для всех конфигураций прототипа).
|
||||||
|
* Если границы поля зависят от других полей, то проверка такого поля должна осуществляться уже при самой
|
||||||
|
* сборке прототипа.
|
||||||
|
*/
|
||||||
|
abstract class ConfigurableDefinition<Configurable : ConfigurableDefinition<Configurable, Configured>, Configured : ConfiguredDefinition<Configured, Configurable>> {
|
||||||
|
val json = Object2ObjectArrayMap<String, Any>()
|
||||||
|
fun enroll() = enrollMap(json)
|
||||||
|
abstract fun configure(directory: String = ""): Configured
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Базовый класс описанного прототипа игрового объекта
|
||||||
|
*
|
||||||
|
* Должен иметь все поля объекта, которые будут использоваться движком напрямую
|
||||||
|
*
|
||||||
|
* Создается соответствующим [ConfigurableDefinition], который проверил уже все поля
|
||||||
|
* на их правильность.
|
||||||
|
*/
|
||||||
|
abstract class ConfiguredDefinition<Configured : ConfiguredDefinition<Configured, Configurator>, Configurator : ConfigurableDefinition<Configurator, Configured>>(
|
||||||
|
val json: ImmutableMap<String, Any>
|
||||||
|
) {
|
||||||
|
open fun getParameter(key: String): Any? = json[key]
|
||||||
|
fun flatten() = flattenMap(json)
|
||||||
|
abstract fun reconfigure(): Configurator
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConfigurableTypeAdapter<T : ConfigurableDefinition<*, *>>(val factory: () -> T, vararg fields: KMutableProperty1<T, *>) : TypeAdapter<T>() {
|
||||||
|
private val mappedFields = Object2ObjectArrayMap<String, KMutableProperty1<T, in Any?>>()
|
||||||
|
// потому что returnType медленный
|
||||||
|
private val mappedFieldsReturnTypes = Object2ObjectArrayMap<String, KType>()
|
||||||
|
private val loggedMisses = ObjectArraySet<String>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
for (field in fields) {
|
||||||
|
// потому что в котлине нет понятия KProperty который не имеет getter'а, только setter
|
||||||
|
require(mappedFields.put(field.name, field as KMutableProperty1<T, in Any?>) == null) { "${field.name} is defined twice" }
|
||||||
|
mappedFieldsReturnTypes[field.name] = field.returnType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val fields: Array<KMutableProperty1<T, in Any?>> get() {
|
||||||
|
val iterator = mappedFields.values.iterator()
|
||||||
|
return Array(mappedFields.size) { iterator.next() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(writer: JsonWriter, value: T) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(reader: JsonReader): T? {
|
||||||
|
if (reader.peek() == JsonToken.NULL) {
|
||||||
|
reader.nextNull()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
val instance = factory.invoke()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
val name = reader.nextName()
|
||||||
|
val field = mappedFields[name]
|
||||||
|
|
||||||
|
if (field != null) {
|
||||||
|
try {
|
||||||
|
val peek = reader.peek()
|
||||||
|
val expectedType = mappedFieldsReturnTypes[name]!!
|
||||||
|
|
||||||
|
if (!expectedType.isMarkedNullable && peek == JsonToken.NULL) {
|
||||||
|
throw IllegalArgumentException("Property ${field.name} of ${instance::class.qualifiedName} does not accept nulls")
|
||||||
|
} else if (peek == JsonToken.NULL) {
|
||||||
|
field.set(instance, null)
|
||||||
|
reader.nextNull()
|
||||||
|
} else {
|
||||||
|
val classifier = expectedType.classifier
|
||||||
|
|
||||||
|
if (classifier is KClass<*>) {
|
||||||
|
if (classifier.isSuperclassOf(Float::class)) {
|
||||||
|
val read = reader.nextDouble()
|
||||||
|
instance.json[name] = read
|
||||||
|
field.set(instance, read.toFloat())
|
||||||
|
} else if (classifier.isSuperclassOf(Double::class)) {
|
||||||
|
val read = reader.nextDouble()
|
||||||
|
instance.json[name] = read
|
||||||
|
field.set(instance, read)
|
||||||
|
} else if (classifier.isSuperclassOf(Int::class)) {
|
||||||
|
val read = reader.nextInt()
|
||||||
|
instance.json[name] = read
|
||||||
|
field.set(instance, read)
|
||||||
|
} else if (classifier.isSuperclassOf(Long::class)) {
|
||||||
|
val read = reader.nextLong()
|
||||||
|
instance.json[name] = read
|
||||||
|
field.set(instance, read)
|
||||||
|
} else if (classifier.isSuperclassOf(String::class)) {
|
||||||
|
val read = reader.nextString()
|
||||||
|
instance.json[name] = read
|
||||||
|
field.set(instance, read)
|
||||||
|
} else if (classifier.isSuperclassOf(Boolean::class)) {
|
||||||
|
val read = reader.nextBoolean()
|
||||||
|
instance.json[name] = read
|
||||||
|
field.set(instance, read)
|
||||||
|
} else {
|
||||||
|
val readElement = TypeAdapters.JSON_ELEMENT.read(reader)
|
||||||
|
instance.json[name] = flattenJsonElement(readElement)
|
||||||
|
field.set(instance, Starbound.gson.fromJson(readElement, classifier.java))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw IllegalStateException("Expected ${field.name} classifier to be KClass, got $classifier")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(err: Throwable) {
|
||||||
|
throw JsonSyntaxException("Reading property ${field.name} of ${instance::class.qualifiedName} near ${reader.path}", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
instance.json[name] = TypeAdapters.JSON_ELEMENT.read(reader)
|
||||||
|
|
||||||
|
if (!loggedMisses.contains(name)) {
|
||||||
|
loggedMisses.add(name)
|
||||||
|
LOGGER.warn("{} has no property for storing {}, this value will be visible to Lua scripts only", instance::class.qualifiedName, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endObject()
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val LOGGER = LogManager.getLogger(ConfigurableTypeAdapter::class.java)
|
||||||
|
}
|
||||||
|
}
|
355
src/main/kotlin/ru/dbotthepony/kstarbound/defs/FrameGrid.kt
Normal file
355
src/main/kotlin/ru/dbotthepony/kstarbound/defs/FrameGrid.kt
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
package ru.dbotthepony.kstarbound.defs
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList
|
||||||
|
import com.google.gson.*
|
||||||
|
import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap
|
||||||
|
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
|
||||||
|
import org.apache.logging.log4j.LogManager
|
||||||
|
import ru.dbotthepony.kstarbound.Starbound
|
||||||
|
import ru.dbotthepony.kstarbound.math.Vector2i
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
|
class MalformedFrameGridException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause)
|
||||||
|
|
||||||
|
data class Frame(
|
||||||
|
val texture: String,
|
||||||
|
val texturePosition: Vector2i,
|
||||||
|
val textureSize: Vector2i,
|
||||||
|
) {
|
||||||
|
val textureEndPosition = texturePosition + textureSize
|
||||||
|
val width get() = textureSize.x
|
||||||
|
val height get() = textureSize.y
|
||||||
|
|
||||||
|
val aspectRatioWH: Float get() {
|
||||||
|
if (height == 0) {
|
||||||
|
return 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
return width.toFloat() / height.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
val aspectRatioHW: Float get() {
|
||||||
|
if (width == 0) {
|
||||||
|
return 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
return height.toFloat() / width.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class FrameSet(
|
||||||
|
val texture: String,
|
||||||
|
val name: String,
|
||||||
|
val frames: List<Frame>,
|
||||||
|
) {
|
||||||
|
val frameCount get() = frames.size
|
||||||
|
fun frame(num: Int) = frames[num]
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FrameSetBuilder(val name: String) {
|
||||||
|
val frames = Object2ObjectArrayMap<String, Frame>()
|
||||||
|
|
||||||
|
fun build(texture: String): FrameSet {
|
||||||
|
val list = ImmutableList.builder<Frame>()
|
||||||
|
val rebuild = Int2ObjectArrayMap<Frame>()
|
||||||
|
|
||||||
|
for ((k, v) in frames) {
|
||||||
|
val int = k.toIntOrNull()
|
||||||
|
|
||||||
|
if (int != null) {
|
||||||
|
rebuild[int] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rebuild.size == 0) {
|
||||||
|
throw IllegalStateException("Frame Set $name is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in 0 until rebuild.size) {
|
||||||
|
list.add(rebuild[i] ?: throw IllegalStateException("Frame Set $name has gap at $i"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return FrameSet(
|
||||||
|
texture = texture,
|
||||||
|
name = name,
|
||||||
|
frames = list.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IFrameGrid {
|
||||||
|
val texture: String
|
||||||
|
val size: Vector2i
|
||||||
|
val dimensions: Vector2i
|
||||||
|
val frames: List<FrameSet>
|
||||||
|
val frameCount get() = frames.size
|
||||||
|
val isResolved: Boolean
|
||||||
|
|
||||||
|
val textureSize get() = size * dimensions
|
||||||
|
|
||||||
|
fun resolve(determinedSize: Vector2i)
|
||||||
|
|
||||||
|
operator fun get(index: Int) = frames[index]
|
||||||
|
|
||||||
|
operator fun get(index: String): FrameSet {
|
||||||
|
for (frame in frames) {
|
||||||
|
if (index == frame.name) {
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw IndexOutOfBoundsException("No such frame strip with name $index")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun splitName(textureName: String, input: String, warn: Boolean, lazy: () -> String): Pair<String, String> {
|
||||||
|
val split = input.split(':')
|
||||||
|
val frameName: String
|
||||||
|
val setName: String
|
||||||
|
|
||||||
|
when (split.size) {
|
||||||
|
1 -> {
|
||||||
|
setName = "root"
|
||||||
|
frameName = split[0]
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
setName = split[0]
|
||||||
|
frameName = split[1]
|
||||||
|
}
|
||||||
|
else -> throw IllegalArgumentException("${lazy.invoke()}: Malformed frame name $input")
|
||||||
|
}
|
||||||
|
|
||||||
|
val frameNumber = frameName.toIntOrNull()
|
||||||
|
|
||||||
|
if (frameNumber == null) {
|
||||||
|
if (warn)
|
||||||
|
LOGGER.warn("{}: Frame {} will be discarded after frame grid is built, because it is not an integer", textureName, frameName)
|
||||||
|
} else {
|
||||||
|
require(frameNumber >= 0) { "${lazy.invoke()}: Frame number of $frameNumber does not make any sense" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return setName to frameName
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateFakeNames(dimensions: Vector2i): JsonArray {
|
||||||
|
return JsonArray(dimensions.y).also {
|
||||||
|
var stripElem = 0
|
||||||
|
|
||||||
|
for (stripNum in 0 until dimensions.y) {
|
||||||
|
val strip = JsonArray(dimensions.x)
|
||||||
|
|
||||||
|
for (i in 0 until dimensions.x) {
|
||||||
|
strip.add(stripElem.toString())
|
||||||
|
stripElem++
|
||||||
|
}
|
||||||
|
|
||||||
|
it.add(strip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Так как объект довольно сложный для автоматической десериализации через gson
|
||||||
|
* поэтому вот так
|
||||||
|
*/
|
||||||
|
fun fromJson(input: JsonObject, texture: String): IFrameGrid {
|
||||||
|
val frameGrid: JsonObject
|
||||||
|
val aliases: JsonObject
|
||||||
|
val texturePath = "$texture.png".intern()
|
||||||
|
|
||||||
|
if (input["frameGrid"] is JsonObject) {
|
||||||
|
frameGrid = input["frameGrid"] as JsonObject
|
||||||
|
} else {
|
||||||
|
frameGrid = input
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input["aliases"] is JsonObject) {
|
||||||
|
aliases = input["aliases"] as JsonObject
|
||||||
|
} else {
|
||||||
|
aliases = JsonObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
val size = Starbound.gson.fromJson(frameGrid["size"], Vector2i::class.java) ?: throw IllegalArgumentException("Size is missing")
|
||||||
|
val dimensions = Starbound.gson.fromJson(frameGrid["dimensions"], Vector2i::class.java) ?: throw IllegalArgumentException("Dimensions are missing")
|
||||||
|
|
||||||
|
require(size.x > 0) { "Invalid texture width of ${size.x}" }
|
||||||
|
require(size.y > 0) { "Invalid texture height of ${size.y}" }
|
||||||
|
|
||||||
|
require(dimensions.x > 0) { "Invalid texture frame count of ${dimensions.x}" }
|
||||||
|
require(dimensions.y > 0) { "Invalid texture stripe count of ${dimensions.y}" }
|
||||||
|
|
||||||
|
val names = frameGrid["names"] as? JsonArray ?: generateFakeNames(dimensions)
|
||||||
|
|
||||||
|
if (names.size() != dimensions.y) {
|
||||||
|
LOGGER.warn("{} inconsistency: it has Y frame span of {}, but {} name strips are defined", texture, dimensions.y, names.size())
|
||||||
|
}
|
||||||
|
|
||||||
|
val frameSets = Object2ObjectArrayMap<String, FrameSetBuilder>()
|
||||||
|
|
||||||
|
for (yPosition in 0 until names.size()) {
|
||||||
|
val list = names[yPosition] as? JsonArray ?: throw IllegalArgumentException("names->$yPosition is not an array")
|
||||||
|
|
||||||
|
if (list.size() != dimensions.x) {
|
||||||
|
LOGGER.warn("{} inconsistency: it has X frame span of {}, but strip at {} has {} names defined", texture, dimensions.x, yPosition, list.size())
|
||||||
|
}
|
||||||
|
|
||||||
|
for (xPosition in 0 until list.size()) {
|
||||||
|
val fullName = list[xPosition]
|
||||||
|
|
||||||
|
if (fullName is JsonNull) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fullName as? JsonPrimitive ?: throw IllegalArgumentException("names->$yPosition->$xPosition: Illegal value $fullName")
|
||||||
|
|
||||||
|
val (setName, frameNumber) = splitName(texture, fullName.asString, true) { "names->$yPosition->$xPosition" }
|
||||||
|
val frameSet = frameSets.computeIfAbsent(setName, ::FrameSetBuilder)
|
||||||
|
|
||||||
|
frameSet.frames[frameNumber] = Frame(
|
||||||
|
texture = texturePath,
|
||||||
|
textureSize = size,
|
||||||
|
texturePosition = Vector2i(x = size.x * xPosition, y = size.y * yPosition))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((newName, originalName) in aliases.entrySet()) {
|
||||||
|
originalName as? JsonPrimitive ?: throw IllegalArgumentException("aliases->$newName: Illegal value $originalName")
|
||||||
|
|
||||||
|
val (oSetName, oFrameNumber) = splitName(texture, originalName.asString, false) { "alias->$newName" }
|
||||||
|
val (nSetName, nFrameNumber) = splitName(texture, newName, true) { "alias->$newName" }
|
||||||
|
|
||||||
|
val oFrameSet = frameSets.computeIfAbsent(oSetName, ::FrameSetBuilder)
|
||||||
|
val nFrameSet = frameSets.computeIfAbsent(nSetName, ::FrameSetBuilder)
|
||||||
|
|
||||||
|
nFrameSet.frames[nFrameNumber] = requireNotNull(oFrameSet.frames[oFrameNumber]) { "alias->$newName points to nothing" }
|
||||||
|
}
|
||||||
|
|
||||||
|
val frameSetList = ImmutableList.builder<FrameSet>()
|
||||||
|
|
||||||
|
for (frameSet in frameSets.values) {
|
||||||
|
frameSetList.add(frameSet.build(texturePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResolvedFrameGrid(texturePath, size, dimensions, frameSetList.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun singleFrame(texturePath: String, size: Vector2i = Vector2i.ZERO): IFrameGrid {
|
||||||
|
val frame = Frame(
|
||||||
|
texture = texturePath,
|
||||||
|
textureSize = size,
|
||||||
|
texturePosition = Vector2i.ZERO)
|
||||||
|
|
||||||
|
return ResolvedFrameGrid(
|
||||||
|
texture = texturePath,
|
||||||
|
size = size,
|
||||||
|
dimensions = Vector2i.ONE_ONE,
|
||||||
|
frames = listOf(FrameSet(
|
||||||
|
name = "root",
|
||||||
|
frames = listOf(frame),
|
||||||
|
texture = texturePath))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cache = HashMap<String, IFrameGrid>()
|
||||||
|
|
||||||
|
fun loadCached(path: String, weak: Boolean = false, weakSize: Vector2i = Vector2i.ZERO): IFrameGrid {
|
||||||
|
if (path[0] != '/')
|
||||||
|
throw IllegalArgumentException("Path must be absolute")
|
||||||
|
|
||||||
|
val splitPath = path.split('/').toMutableList()
|
||||||
|
val last = splitPath.last()
|
||||||
|
splitPath.removeLast()
|
||||||
|
val splitLast = last.split('.')
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (splitLast.size == 1) {
|
||||||
|
// имя уже абсолютное
|
||||||
|
return cache.computeIfAbsent(path) {
|
||||||
|
val frames = Starbound.firstExistingOrNull("$path.frames", "${splitPath.joinToString("/")}/default.frames")
|
||||||
|
|
||||||
|
if (weak && frames == null) {
|
||||||
|
LOGGER.warn("Expected animated texture at {}, but .frames metafile is missing.", path)
|
||||||
|
|
||||||
|
return@computeIfAbsent singleFrame("$path.png", weakSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
return@computeIfAbsent fromJson(Starbound.loadJson(frames ?: throw FileNotFoundException("Unable to find .frames meta for $path")) as JsonObject, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newPath = "${splitPath.joinToString("/")}/${splitLast[0]}"
|
||||||
|
|
||||||
|
return cache.computeIfAbsent(newPath) {
|
||||||
|
val frames = Starbound.firstExistingOrNull("$newPath.frames", "${splitPath.joinToString("/")}/default.frames")
|
||||||
|
|
||||||
|
if (weak && frames == null) {
|
||||||
|
LOGGER.warn("Expected animated texture at {}, but .frames metafile is missing.", newPath)
|
||||||
|
|
||||||
|
return@computeIfAbsent singleFrame("$newPath.png", weakSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
return@computeIfAbsent fromJson(Starbound.loadJson(frames ?: throw FileNotFoundException("Unable to find .frames meta for $path")) as JsonObject, newPath)
|
||||||
|
}
|
||||||
|
} catch (err: Throwable) {
|
||||||
|
throw MalformedFrameGridException("Reading animated texture definition $path", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadFrameStrip(path: String, weak: Boolean = false, weakSize: Vector2i = Vector2i.ZERO): FrameSet {
|
||||||
|
if (path[0] != '/')
|
||||||
|
throw IllegalArgumentException("Path must be absolute")
|
||||||
|
|
||||||
|
val split = path.split(':')
|
||||||
|
|
||||||
|
if (split.size == 1) {
|
||||||
|
// мы хотим получить главный кадр, который является анонимным
|
||||||
|
val load = loadCached(path, weak, weakSize)
|
||||||
|
check(load.frameCount == 1) { "$path has ${load.frameCount} frame strips, but we want exactly one!" }
|
||||||
|
return load[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
val load = loadCached(split[0], weak, weakSize)
|
||||||
|
return load[split[1]]
|
||||||
|
}
|
||||||
|
|
||||||
|
private val LOGGER = LogManager.getLogger()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ResolvedFrameGrid(
|
||||||
|
override val texture: String,
|
||||||
|
override val size: Vector2i,
|
||||||
|
override val dimensions: Vector2i,
|
||||||
|
|
||||||
|
override val frames: List<FrameSet>
|
||||||
|
) : IFrameGrid {
|
||||||
|
override val isResolved = true
|
||||||
|
|
||||||
|
override fun resolve(determinedSize: Vector2i) {
|
||||||
|
require(determinedSize.x > 0) { "Invalid image width ${determinedSize.x}" }
|
||||||
|
require(determinedSize.y > 0) { "Invalid image height ${determinedSize.y}" }
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LazyFrameGrid(
|
||||||
|
override val texture: String,
|
||||||
|
override val size: Vector2i,
|
||||||
|
override val dimensions: Vector2i,
|
||||||
|
) : IFrameGrid {
|
||||||
|
private var _frames: List<FrameSet>? = null
|
||||||
|
|
||||||
|
override val frames: List<FrameSet>
|
||||||
|
get() = _frames ?: throw IllegalStateException("Call resolve() first")
|
||||||
|
|
||||||
|
override var isResolved = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun resolve(determinedSize: Vector2i) {
|
||||||
|
if (_frames != null)
|
||||||
|
return
|
||||||
|
|
||||||
|
check(dimensions == determinedSize) { "$texture was expected to have dimensions of $dimensions, $determinedSize given" }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package ru.dbotthepony.kstarbound.defs
|
||||||
|
|
||||||
|
fun ensureAbsolutePath(path: String, parent: String): String {
|
||||||
|
if (path[0] == '/')
|
||||||
|
return path
|
||||||
|
|
||||||
|
return "$parent/$path"
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package ru.dbotthepony.kstarbound.defs
|
||||||
|
|
||||||
|
class ParticleDefinitionBuilder {
|
||||||
|
var kind: String? = null
|
||||||
|
var animation: String? = null
|
||||||
|
var size: Double? = null
|
||||||
|
var timeToLive: Double? = null
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
package ru.dbotthepony.kstarbound.defs.projectile
|
||||||
|
|
||||||
|
import ru.dbotthepony.kstarbound.defs.ConfigurableDefinition
|
||||||
|
import ru.dbotthepony.kstarbound.defs.ConfigurableTypeAdapter
|
||||||
|
import ru.dbotthepony.kstarbound.defs.IFrameGrid
|
||||||
|
import ru.dbotthepony.kstarbound.defs.ensureAbsolutePath
|
||||||
|
import ru.dbotthepony.kstarbound.util.Color
|
||||||
|
|
||||||
|
class ConfigurableProjectile : ConfigurableDefinition<ConfigurableProjectile, ConfiguredProjectile>() {
|
||||||
|
var projectileName: String? = null
|
||||||
|
var physics: ProjectilePhysics = ProjectilePhysics.DEFAULT
|
||||||
|
var damageKindImage: String? = null
|
||||||
|
var damageType: String? = null
|
||||||
|
var damageKind: String? = null
|
||||||
|
|
||||||
|
var pointLight: Boolean = false
|
||||||
|
var lightColor: Color? = null
|
||||||
|
|
||||||
|
var onlyHitTerrain: Boolean = false
|
||||||
|
var orientationLocked: Boolean = false
|
||||||
|
|
||||||
|
var image: String? = null
|
||||||
|
|
||||||
|
var timeToLive: Double = Double.POSITIVE_INFINITY
|
||||||
|
var animationCycle: Double = Double.POSITIVE_INFINITY
|
||||||
|
var bounces: Int = -1
|
||||||
|
var frameNumber: Int = 1
|
||||||
|
|
||||||
|
var scripts: Array<String> = Array(0) { "" }
|
||||||
|
|
||||||
|
var hydrophobic: Boolean = false
|
||||||
|
|
||||||
|
override fun configure(directory: String): ConfiguredProjectile {
|
||||||
|
return ConfiguredProjectile(
|
||||||
|
json = enroll(),
|
||||||
|
projectileName = checkNotNull(projectileName) { "projectileName is null" },
|
||||||
|
physics = physics,
|
||||||
|
damageKindImage = damageKindImage,
|
||||||
|
damageType = damageType,
|
||||||
|
damageKind = damageKind,
|
||||||
|
pointLight = pointLight,
|
||||||
|
lightColor = lightColor,
|
||||||
|
onlyHitTerrain = onlyHitTerrain,
|
||||||
|
orientationLocked = orientationLocked,
|
||||||
|
image = IFrameGrid.loadFrameStrip(ensureAbsolutePath(requireNotNull(image) { "image is null" }, directory), weak = true),
|
||||||
|
timeToLive = timeToLive,
|
||||||
|
animationCycle = animationCycle,
|
||||||
|
bounces = bounces,
|
||||||
|
frameNumber = frameNumber,
|
||||||
|
scripts = scripts,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val ADAPTER = ConfigurableTypeAdapter(
|
||||||
|
::ConfigurableProjectile,
|
||||||
|
ConfigurableProjectile::projectileName,
|
||||||
|
ConfigurableProjectile::physics,
|
||||||
|
ConfigurableProjectile::damageKindImage,
|
||||||
|
ConfigurableProjectile::damageType,
|
||||||
|
ConfigurableProjectile::damageKind,
|
||||||
|
ConfigurableProjectile::pointLight,
|
||||||
|
ConfigurableProjectile::lightColor,
|
||||||
|
ConfigurableProjectile::onlyHitTerrain,
|
||||||
|
ConfigurableProjectile::orientationLocked,
|
||||||
|
ConfigurableProjectile::image,
|
||||||
|
ConfigurableProjectile::timeToLive,
|
||||||
|
ConfigurableProjectile::animationCycle,
|
||||||
|
ConfigurableProjectile::bounces,
|
||||||
|
ConfigurableProjectile::frameNumber,
|
||||||
|
ConfigurableProjectile::scripts,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package ru.dbotthepony.kstarbound.defs.projectile
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap
|
||||||
|
import ru.dbotthepony.kstarbound.defs.ConfiguredDefinition
|
||||||
|
import ru.dbotthepony.kstarbound.defs.FrameSet
|
||||||
|
import ru.dbotthepony.kstarbound.util.Color
|
||||||
|
|
||||||
|
class ConfiguredProjectile(
|
||||||
|
json: ImmutableMap<String, Any>,
|
||||||
|
val projectileName: String,
|
||||||
|
val physics: ProjectilePhysics,
|
||||||
|
val damageKindImage: String?,
|
||||||
|
val damageType: String?,
|
||||||
|
val damageKind: String?,
|
||||||
|
val pointLight: Boolean,
|
||||||
|
val lightColor: Color?,
|
||||||
|
val onlyHitTerrain: Boolean,
|
||||||
|
val orientationLocked: Boolean,
|
||||||
|
val image: FrameSet,
|
||||||
|
val timeToLive: Double,
|
||||||
|
val animationCycle: Double,
|
||||||
|
val bounces: Int,
|
||||||
|
val frameNumber: Int,
|
||||||
|
val scripts: Array<String>,
|
||||||
|
) : ConfiguredDefinition<ConfiguredProjectile, ConfigurableProjectile>(json) {
|
||||||
|
override fun reconfigure(): ConfigurableProjectile {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "ConfiguredProjectile($projectileName)"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
package ru.dbotthepony.kstarbound.defs.projectile
|
||||||
|
|
||||||
|
import com.google.gson.stream.JsonWriter
|
||||||
|
import ru.dbotthepony.kstarbound.util.IStringSerializable
|
||||||
|
|
||||||
|
enum class ProjectilePhysics(private vararg val aliases: String) : IStringSerializable {
|
||||||
|
GAS,
|
||||||
|
LASER,
|
||||||
|
BOOMERANG,
|
||||||
|
DEFAULT,
|
||||||
|
BULLET,
|
||||||
|
STICKY_BULLET("STICKYBULLET"),
|
||||||
|
ARROW,
|
||||||
|
UNDERWATER_ARROW("UNDERWATERARROW"),
|
||||||
|
UNDERWATER_ARROW_NO_STICKY("UNDERWATERARROWNOSTICKY"),
|
||||||
|
ROCKET,
|
||||||
|
GRAVITY_BULLET("GRAVITYBULLET"),
|
||||||
|
FLAME,
|
||||||
|
ARROW_NO_STICKY("ARROWNOSTICKY"),
|
||||||
|
|
||||||
|
SQUIRT,
|
||||||
|
FLYBUG,
|
||||||
|
ROLLER,
|
||||||
|
BOWLDER,
|
||||||
|
SMOOTH_ROLLING_BOULDER("SMOOTHROLLINGBOULDER"),
|
||||||
|
ROLLING_BOULDER("ROLLINGBOULDER"),
|
||||||
|
|
||||||
|
DRAGON_BONE("DRAGONBONE"),
|
||||||
|
DRAGON_HEAD("DRAGONHEAD"),
|
||||||
|
|
||||||
|
STICKY,
|
||||||
|
BOWLING_BALL("BOWLINGBALL"),
|
||||||
|
PAPER_PLANE("PAPERPLANE"),
|
||||||
|
BOULDER,
|
||||||
|
|
||||||
|
STATUS_POD("STATUSPOD"),
|
||||||
|
|
||||||
|
// ???
|
||||||
|
ILLUSION,
|
||||||
|
ILLUSION_ROCKET("ROCKETILLUSION"),
|
||||||
|
|
||||||
|
// ?????????????
|
||||||
|
FRIENDLY_BUBBLE("FRIENDLYBUBBLE"),
|
||||||
|
|
||||||
|
STICKY_HEAVY_GAS("STICKYHEAVYGAS"),
|
||||||
|
HEAVY_GAS("HEAVYGAS"),
|
||||||
|
BOUNCY_GAS("BOUNCYGAS"),
|
||||||
|
FIREBALL,
|
||||||
|
SLIDER,
|
||||||
|
GOOP,
|
||||||
|
HOVER,
|
||||||
|
|
||||||
|
BONE_THORN("BONETHORN"),
|
||||||
|
|
||||||
|
BIG_BUBBLE("BIGBUBBLE"),
|
||||||
|
FIREWORK_FALL("FIREWORKFALL"),
|
||||||
|
LIGHTNING_BOLT("LIGHTNINGBOLT"),
|
||||||
|
SIMPLE_ARC("SIMPLEARC"),
|
||||||
|
LOW_GRAVITY_ARC("LOWGRAVARC"),
|
||||||
|
|
||||||
|
SPIKE_BALL("SPIKEBALL"),
|
||||||
|
SHRAPNEL,
|
||||||
|
|
||||||
|
// что
|
||||||
|
WEATHER,
|
||||||
|
|
||||||
|
FIRE_SPREAD("FIRESPREAD"),
|
||||||
|
|
||||||
|
GRAPPLE_HOOK("GRAPPLEHOOK"),
|
||||||
|
BALLISTIC_GRAPPLE_HOOK("BALLISTICGRAPPLEHOOK"),
|
||||||
|
|
||||||
|
FLOATY_STICKY_BOMB("FLOATYSTICKYBOMB"),
|
||||||
|
STICKY_BOMB("STICKYBOMB"),
|
||||||
|
BOUNCY,
|
||||||
|
GRAVITY_BOMB("GRAVITYBOMB"),
|
||||||
|
DISC,
|
||||||
|
HEAVY_BOUNCER("HEAVYBOUNCER"),
|
||||||
|
|
||||||
|
WALL_STICKY("WALLSTICKY"),
|
||||||
|
FISHING_LURE_SINKING("FISHINGLURESINKING"),
|
||||||
|
FISHING_LURE("FISHINGLURE"),
|
||||||
|
RAIN("RAIN"),
|
||||||
|
|
||||||
|
PET_BALL("PETBALL"),
|
||||||
|
BOUNCY_BALL("BOUNCYBALL"),
|
||||||
|
BEACH_BALL("BEACHBALL"),
|
||||||
|
NOVELTY_BANANA("NOVELTYBANANA"),
|
||||||
|
|
||||||
|
SPACE_MINE("SPACEMINE"),
|
||||||
|
MECH_BATTERY("MECHBATTERY"),
|
||||||
|
|
||||||
|
GRENADE,
|
||||||
|
GRENADE_LARGE("LARGEGRENADE"),
|
||||||
|
GRENADE_Z_BOMB("GRENADEZBOMB"),
|
||||||
|
GRENADE_STICKY("STICKYGRENADE"),
|
||||||
|
GRENADE_SUPER_GRAVITY("SUPERHIGHGRAVGRENADE"),
|
||||||
|
GRENADE_HIGH_GRAVITY_V("VHIGHGRAVGRENADE"),
|
||||||
|
GRENADE_HIGH_GRAVITY("HIGHGRAVGRENADE"),
|
||||||
|
GRENADE_LOW_BOUNCE("GRENADELOWBOUNCE"),
|
||||||
|
GRENADE_NO_BOUNCE("GRENADENOBOUNCE");
|
||||||
|
|
||||||
|
override fun match(name: String): Boolean {
|
||||||
|
for (alias in aliases)
|
||||||
|
if (name == alias)
|
||||||
|
return true
|
||||||
|
|
||||||
|
return name == this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun write(out: JsonWriter) {
|
||||||
|
out.value(this.name)
|
||||||
|
}
|
||||||
|
}
|
@ -70,13 +70,12 @@ class StarboundPakFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class StarboundPakDirectory(val name: String, val parent: StarboundPakDirectory? = null) {
|
class StarboundPakDirectory(val name: String, val parent: StarboundPakDirectory? = null) {
|
||||||
private val files = HashMap<String, StarboundPakFile>()
|
val files = HashMap<String, StarboundPakFile>()
|
||||||
private val directories = HashMap<String, StarboundPakDirectory>()
|
val directories = HashMap<String, StarboundPakDirectory>()
|
||||||
|
|
||||||
fun resolve(path: Array<String>, level: Int = 0): StarboundPakDirectory {
|
fun resolve(path: Array<String>, level: Int = 0): StarboundPakDirectory {
|
||||||
if (path.size == level) {
|
if (path.size == level)
|
||||||
return this
|
return this
|
||||||
}
|
|
||||||
|
|
||||||
if (level == 0 && path[0] == "" && name == "/")
|
if (level == 0 && path[0] == "" && name == "/")
|
||||||
return resolve(path, 1)
|
return resolve(path, 1)
|
||||||
@ -94,6 +93,7 @@ class StarboundPakDirectory(val name: String, val parent: StarboundPakDirectory?
|
|||||||
fun getDirectory(name: String) = directories[name]
|
fun getDirectory(name: String) = directories[name]
|
||||||
|
|
||||||
fun listFiles(): Collection<StarboundPakFile> = Collections.unmodifiableCollection(files.values)
|
fun listFiles(): Collection<StarboundPakFile> = Collections.unmodifiableCollection(files.values)
|
||||||
|
fun listDirectories(): Collection<StarboundPakDirectory> = Collections.unmodifiableCollection(directories.values)
|
||||||
|
|
||||||
fun writeFile(file: StarboundPakFile) {
|
fun writeFile(file: StarboundPakFile) {
|
||||||
files[file.name.split('/').last()] = file
|
files[file.name.split('/').last()] = file
|
||||||
@ -105,9 +105,9 @@ class StarboundPakDirectory(val name: String, val parent: StarboundPakDirectory?
|
|||||||
|
|
||||||
while (getParent != null) {
|
while (getParent != null) {
|
||||||
if (getParent.parent != null) {
|
if (getParent.parent != null) {
|
||||||
build = "${getParent.name}/$name"
|
build = "${getParent.name}/$build"
|
||||||
} else {
|
} else {
|
||||||
build = "/$name"
|
build = "/$build"
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,6 +196,10 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String)
|
|||||||
return root.resolve(path.split("/").toTypedArray()).listFiles().map { it.name }
|
return root.resolve(path.split("/").toTypedArray()).listFiles().map { it.name }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun listDirectories(path: String): Collection<String> {
|
||||||
|
return root.resolve(path.split("/").toTypedArray()).listDirectories().map { it.fullName() }
|
||||||
|
}
|
||||||
|
|
||||||
override fun pathExists(path: String): Boolean {
|
override fun pathExists(path: String): Boolean {
|
||||||
return indexNodes.containsKey(path)
|
return indexNodes.containsKey(path)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package ru.dbotthepony.kstarbound.math
|
package ru.dbotthepony.kstarbound.math
|
||||||
|
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import com.google.gson.stream.JsonWriter
|
||||||
import ru.dbotthepony.kstarbound.api.IStruct2d
|
import ru.dbotthepony.kstarbound.api.IStruct2d
|
||||||
import ru.dbotthepony.kstarbound.world.ChunkPos
|
import ru.dbotthepony.kstarbound.world.ChunkPos
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
@ -33,8 +36,8 @@ data class SweepResult(
|
|||||||
*/
|
*/
|
||||||
data class AABB(val mins: Vector2d, val maxs: Vector2d) {
|
data class AABB(val mins: Vector2d, val maxs: Vector2d) {
|
||||||
init {
|
init {
|
||||||
require(mins.x < maxs.x) { "mins.x ${mins.x} is more or equal to maxs.x ${maxs.x}" }
|
require(mins.x <= maxs.x) { "mins.x ${mins.x} is more than maxs.x ${maxs.x}" }
|
||||||
require(mins.y < maxs.y) { "mins.y ${mins.y} is more or equal to maxs.y ${maxs.y}" }
|
require(mins.y <= maxs.y) { "mins.y ${mins.y} is more than maxs.y ${maxs.y}" }
|
||||||
}
|
}
|
||||||
|
|
||||||
operator fun plus(other: AABB) = AABB(mins + other.mins, maxs + other.maxs)
|
operator fun plus(other: AABB) = AABB(mins + other.mins, maxs + other.maxs)
|
||||||
@ -367,6 +370,31 @@ data class AABB(val mins: Vector2d, val maxs: Vector2d) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object AABBTypeAdapter : TypeAdapter<AABB>() {
|
||||||
|
override fun write(out: JsonWriter, value: AABB) {
|
||||||
|
`out`.beginArray()
|
||||||
|
Vector2dTypeAdapter.write(out, value.mins)
|
||||||
|
Vector2dTypeAdapter.write(out, value.maxs)
|
||||||
|
`out`.endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(`in`: JsonReader): AABB {
|
||||||
|
val (x1, x2) = Vector2dTypeAdapter.read(`in`)
|
||||||
|
val (y1, y2) = Vector2dTypeAdapter.read(`in`)
|
||||||
|
|
||||||
|
val xMins = x1.coerceAtMost(x2)
|
||||||
|
val xMaxs = x1.coerceAtLeast(x2)
|
||||||
|
|
||||||
|
val yMins = y1.coerceAtMost(y2)
|
||||||
|
val yMaxs = y1.coerceAtLeast(y2)
|
||||||
|
|
||||||
|
return AABB(
|
||||||
|
Vector2d(xMins, yMins),
|
||||||
|
Vector2d(xMaxs, yMaxs),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class AABBi(val mins: Vector2i, val maxs: Vector2i) {
|
data class AABBi(val mins: Vector2i, val maxs: Vector2i) {
|
||||||
init {
|
init {
|
||||||
require(mins.x <= maxs.x) { "mins.x ${mins.x} is more than maxs.x ${maxs.x}" }
|
require(mins.x <= maxs.x) { "mins.x ${mins.x} is more than maxs.x ${maxs.x}" }
|
||||||
@ -499,3 +527,29 @@ data class AABBi(val mins: Vector2i, val maxs: Vector2i) {
|
|||||||
val vectors: kotlin.collections.Iterator<Vector2i> get() = Iterator(::Vector2i)
|
val vectors: kotlin.collections.Iterator<Vector2i> get() = Iterator(::Vector2i)
|
||||||
val chunkPositions: kotlin.collections.Iterator<ChunkPos> get() = Iterator(::ChunkPos)
|
val chunkPositions: kotlin.collections.Iterator<ChunkPos> get() = Iterator(::ChunkPos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object AABBiTypeAdapter : TypeAdapter<AABBi>() {
|
||||||
|
override fun write(out: JsonWriter, value: AABBi) {
|
||||||
|
`out`.beginArray()
|
||||||
|
Vector2iTypeAdapter.write(out, value.mins)
|
||||||
|
Vector2iTypeAdapter.write(out, value.maxs)
|
||||||
|
`out`.endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(`in`: JsonReader): AABBi {
|
||||||
|
val (x1, x2) = Vector2iTypeAdapter.read(`in`)
|
||||||
|
val (y1, y2) = Vector2iTypeAdapter.read(`in`)
|
||||||
|
|
||||||
|
val xMins = x1.coerceAtMost(x2)
|
||||||
|
val xMaxs = x1.coerceAtLeast(x2)
|
||||||
|
|
||||||
|
val yMins = y1.coerceAtMost(y2)
|
||||||
|
val yMaxs = y1.coerceAtLeast(y2)
|
||||||
|
|
||||||
|
return AABBi(
|
||||||
|
Vector2i(xMins, yMins),
|
||||||
|
Vector2i(xMaxs, yMaxs),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
41
src/main/kotlin/ru/dbotthepony/kstarbound/math/Poly.kt
Normal file
41
src/main/kotlin/ru/dbotthepony/kstarbound/math/Poly.kt
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package ru.dbotthepony.kstarbound.math
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import com.google.gson.stream.JsonToken
|
||||||
|
import com.google.gson.stream.JsonWriter
|
||||||
|
|
||||||
|
class Poly(vararg points: Vector2d) {
|
||||||
|
val points: List<Vector2d> = ImmutableList.copyOf(points)
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Poly($points)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object PolyTypeAdapter : TypeAdapter<Poly>() {
|
||||||
|
override fun write(out: JsonWriter, value: Poly) {
|
||||||
|
`out`.beginArray()
|
||||||
|
|
||||||
|
for (point in value.points) {
|
||||||
|
Vector2dTypeAdapter.write(out, point)
|
||||||
|
}
|
||||||
|
|
||||||
|
`out`.endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(`in`: JsonReader): Poly {
|
||||||
|
`in`.beginArray()
|
||||||
|
|
||||||
|
val points = mutableListOf<Vector2d>()
|
||||||
|
|
||||||
|
while (`in`.peek() == JsonToken.BEGIN_ARRAY) {
|
||||||
|
points.add(Vector2dTypeAdapter.read(`in`))
|
||||||
|
}
|
||||||
|
|
||||||
|
`in`.endArray()
|
||||||
|
|
||||||
|
return Poly(*points.toTypedArray())
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
package ru.dbotthepony.kstarbound.math
|
package ru.dbotthepony.kstarbound.math
|
||||||
|
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import com.google.gson.stream.JsonWriter
|
||||||
import ru.dbotthepony.kstarbound.api.*
|
import ru.dbotthepony.kstarbound.api.*
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
@ -131,6 +134,27 @@ data class Vector2i(override val x: Int = 0, override val y: Int = 0) : IVector2
|
|||||||
val RIGHT = Vector2i().right()
|
val RIGHT = Vector2i().right()
|
||||||
val UP = Vector2i().up()
|
val UP = Vector2i().up()
|
||||||
val DOWN = Vector2i().down()
|
val DOWN = Vector2i().down()
|
||||||
|
val ONE_ONE = Vector2i(1, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Vector2iTypeAdapter : TypeAdapter<Vector2i>() {
|
||||||
|
override fun write(out: JsonWriter, value: Vector2i) {
|
||||||
|
`out`.beginArray()
|
||||||
|
`out`.value(value.x)
|
||||||
|
`out`.value(value.y)
|
||||||
|
`out`.endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(`in`: JsonReader): Vector2i {
|
||||||
|
`in`.beginArray()
|
||||||
|
|
||||||
|
val x = `in`.nextInt()
|
||||||
|
val y = `in`.nextInt()
|
||||||
|
|
||||||
|
`in`.endArray()
|
||||||
|
|
||||||
|
return Vector2i(x, y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,6 +272,26 @@ data class Vector2f(override val x: Float = 0f, override val y: Float = 0f) : IV
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object Vector2fTypeAdapter : TypeAdapter<Vector2f>() {
|
||||||
|
override fun write(out: JsonWriter, value: Vector2f) {
|
||||||
|
`out`.beginArray()
|
||||||
|
`out`.value(value.x)
|
||||||
|
`out`.value(value.y)
|
||||||
|
`out`.endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(`in`: JsonReader): Vector2f {
|
||||||
|
`in`.beginArray()
|
||||||
|
|
||||||
|
val x = `in`.nextDouble().toFloat()
|
||||||
|
val y = `in`.nextDouble().toFloat()
|
||||||
|
|
||||||
|
`in`.endArray()
|
||||||
|
|
||||||
|
return Vector2f(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class MutableVector2f(override var x: Float = 0f, override var y: Float = 0f) : IVector2f<MutableVector2f>() {
|
data class MutableVector2f(override var x: Float = 0f, override var y: Float = 0f) : IVector2f<MutableVector2f>() {
|
||||||
override fun make(x: Float, y: Float): MutableVector2f {
|
override fun make(x: Float, y: Float): MutableVector2f {
|
||||||
this.x = x
|
this.x = x
|
||||||
@ -367,6 +411,26 @@ data class Vector2d(override val x: Double = 0.0, override val y: Double = 0.0)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object Vector2dTypeAdapter : TypeAdapter<Vector2d>() {
|
||||||
|
override fun write(out: JsonWriter, value: Vector2d) {
|
||||||
|
`out`.beginArray()
|
||||||
|
`out`.value(value.x)
|
||||||
|
`out`.value(value.y)
|
||||||
|
`out`.endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(`in`: JsonReader): Vector2d {
|
||||||
|
`in`.beginArray()
|
||||||
|
|
||||||
|
val x = `in`.nextDouble()
|
||||||
|
val y = `in`.nextDouble()
|
||||||
|
|
||||||
|
`in`.endArray()
|
||||||
|
|
||||||
|
return Vector2d(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class MutableVector2d(override var x: Double = 0.0, override var y: Double = 0.0) : IVector2d<MutableVector2d>() {
|
data class MutableVector2d(override var x: Double = 0.0, override var y: Double = 0.0) : IVector2d<MutableVector2d>() {
|
||||||
override fun make(x: Double, y: Double): MutableVector2d {
|
override fun make(x: Double, y: Double): MutableVector2d {
|
||||||
this.x = x
|
this.x = x
|
||||||
|
@ -1,12 +1,22 @@
|
|||||||
package ru.dbotthepony.kstarbound.util
|
package ru.dbotthepony.kstarbound.util
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList
|
import com.google.common.collect.ImmutableList
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.*
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import com.google.gson.stream.JsonToken
|
||||||
|
import com.google.gson.stream.JsonWriter
|
||||||
import ru.dbotthepony.kstarbound.api.IStruct4f
|
import ru.dbotthepony.kstarbound.api.IStruct4f
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
data class Color(val red: Float, val green: Float, val blue: Float, val alpha: Float = 1f) : IStruct4f {
|
data class Color(val red: Float, val green: Float, val blue: Float, val alpha: Float = 1f) : IStruct4f {
|
||||||
constructor(input: JsonArray) : this(input[0].asFloat / 255f, input[1].asFloat / 255f, input[2].asFloat / 255f, if (input.size() >= 4) input[3].asFloat / 255f else 1f)
|
constructor(input: JsonArray) : this(input[0].asFloat / 255f, input[1].asFloat / 255f, input[2].asFloat / 255f, if (input.size() >= 4) input[3].asFloat / 255f else 1f)
|
||||||
|
|
||||||
|
constructor(input: Long) : this(
|
||||||
|
((input ushr 16) and 0xFFL).toFloat() / 255f,
|
||||||
|
((input ushr 8) and 0xFFL).toFloat() / 255f,
|
||||||
|
(input and 0xFFL).toFloat() / 255f,
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val WHITE = Color(1f, 1f, 1f)
|
val WHITE = Color(1f, 1f, 1f)
|
||||||
|
|
||||||
@ -16,6 +26,12 @@ data class Color(val red: Float, val green: Float, val blue: Float, val alpha: F
|
|||||||
|
|
||||||
val SLATE_GREY = Color(0.2f, 0.2f, 0.2f)
|
val SLATE_GREY = Color(0.2f, 0.2f, 0.2f)
|
||||||
|
|
||||||
|
val PRE_DEFINED_MAP = mapOf(
|
||||||
|
"red" to RED,
|
||||||
|
"green" to GREEN,
|
||||||
|
"blue" to BLUE,
|
||||||
|
)
|
||||||
|
|
||||||
val SHADES_OF_GRAY = ArrayList<Color>().let {
|
val SHADES_OF_GRAY = ArrayList<Color>().let {
|
||||||
for (i in 0 .. 256) {
|
for (i in 0 .. 256) {
|
||||||
it.add(Color(i / 256f, i / 256f, i / 256f))
|
it.add(Color(i / 256f, i / 256f, i / 256f))
|
||||||
@ -25,3 +41,91 @@ data class Color(val red: Float, val green: Float, val blue: Float, val alpha: F
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object ColorTypeAdapter : TypeAdapter<Color>() {
|
||||||
|
override fun write(out: JsonWriter, value: Color) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(`in`: JsonReader): Color {
|
||||||
|
when (val type = `in`.peek()) {
|
||||||
|
JsonToken.BEGIN_ARRAY -> {
|
||||||
|
`in`.beginArray()
|
||||||
|
val red = `in`.nextDouble()
|
||||||
|
val green = `in`.nextDouble()
|
||||||
|
val blue = `in`.nextDouble()
|
||||||
|
|
||||||
|
if (red % 1.0 == 0.0 && green % 1.0 == 0.0 && blue % 1.0 == 0.0) {
|
||||||
|
val alpha = `in`.peek().let { if (it == JsonToken.END_ARRAY) 255.0 else `in`.nextDouble() }
|
||||||
|
`in`.endArray()
|
||||||
|
|
||||||
|
return Color(
|
||||||
|
red.toFloat() / 255f,
|
||||||
|
green.toFloat() / 255f,
|
||||||
|
blue.toFloat() / 255f,
|
||||||
|
alpha.toFloat() / 255f,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val alpha = `in`.peek().let { if (it == JsonToken.END_ARRAY) 1.0 else `in`.nextDouble() }
|
||||||
|
`in`.endArray()
|
||||||
|
|
||||||
|
return Color(
|
||||||
|
red.toFloat(),
|
||||||
|
green.toFloat(),
|
||||||
|
blue.toFloat(),
|
||||||
|
alpha.toFloat(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonToken.BEGIN_OBJECT -> {
|
||||||
|
`in`.beginObject()
|
||||||
|
|
||||||
|
val keyed = mutableMapOf<String, Double>()
|
||||||
|
|
||||||
|
while (`in`.peek() != JsonToken.END_OBJECT) {
|
||||||
|
keyed[`in`.nextName()] = `in`.nextDouble()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyed.isEmpty())
|
||||||
|
throw IllegalArgumentException("Object is empty")
|
||||||
|
|
||||||
|
var values = 0
|
||||||
|
|
||||||
|
val red = keyed["red"]?.also { values++ } ?: keyed["r"]?.also { values++ } ?: 255.0
|
||||||
|
val green = keyed["green"]?.also { values++ } ?: keyed["g"]?.also { values++ } ?: 255.0
|
||||||
|
val blue = keyed["blue"]?.also { values++ } ?: keyed["b"]?.also { values++ } ?: 255.0
|
||||||
|
val alpha = keyed["alpha"]?.also { values++ } ?: keyed["a"]?.also { values++ } ?: 255.0
|
||||||
|
`in`.endObject()
|
||||||
|
|
||||||
|
if (values == 0) {
|
||||||
|
throw IllegalArgumentException("Object is not a color")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (red % 1.0 == 0.0 && green % 1.0 == 0.0 && blue % 1.0 == 0.0 && alpha % 1.0 == 0.0) {
|
||||||
|
return Color(
|
||||||
|
red.toFloat() / 255f,
|
||||||
|
green.toFloat() / 255f,
|
||||||
|
blue.toFloat() / 255f,
|
||||||
|
alpha.toFloat() / 255f,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return Color(
|
||||||
|
red.toFloat(),
|
||||||
|
green.toFloat(),
|
||||||
|
blue.toFloat(),
|
||||||
|
alpha.toFloat(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonToken.NUMBER -> return Color(`in`.nextLong())
|
||||||
|
JsonToken.STRING -> {
|
||||||
|
val name = `in`.nextString()
|
||||||
|
return Color.PRE_DEFINED_MAP[name] ?: throw IllegalArgumentException("Unknown pre defined color name $name")
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw IllegalArgumentException("Expected array, object or number; got $type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
package ru.dbotthepony.kstarbound.util
|
||||||
|
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import com.google.gson.stream.JsonWriter
|
||||||
|
|
||||||
|
interface IStringSerializable {
|
||||||
|
fun match(name: String): Boolean
|
||||||
|
fun write(out: JsonWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomEnumTypeAdapter<T : Enum<T>>(private val clazz: Array<T>) : TypeAdapter<T>() {
|
||||||
|
override fun write(out: JsonWriter, value: T) {
|
||||||
|
if (value is IStringSerializable)
|
||||||
|
value.write(out)
|
||||||
|
else
|
||||||
|
out.value(value.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun read(`in`: JsonReader): T {
|
||||||
|
val str = `in`.nextString().uppercase()
|
||||||
|
|
||||||
|
for (value in clazz) {
|
||||||
|
if (value is IStringSerializable && value.match(str) || value.name == str) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw IllegalArgumentException("${clazz[0]::class.java.name} does not have value for $str")
|
||||||
|
}
|
||||||
|
}
|
@ -492,6 +492,10 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
|
|||||||
onEntityRemoved(entity)
|
onEntityRemoved(entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Chunk(pos=$pos, entityCount=${entities.size}, world=$world)"
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY = object : IMutableTileChunk {
|
val EMPTY = object : IMutableTileChunk {
|
||||||
override val pos = ChunkPos(0, 0)
|
override val pos = ChunkPos(0, 0)
|
||||||
|
@ -5,6 +5,7 @@ import ru.dbotthepony.kstarbound.math.AABB
|
|||||||
import ru.dbotthepony.kstarbound.math.Vector2d
|
import ru.dbotthepony.kstarbound.math.Vector2d
|
||||||
import ru.dbotthepony.kstarbound.util.Color
|
import ru.dbotthepony.kstarbound.util.Color
|
||||||
import ru.dbotthepony.kstarbound.world.World
|
import ru.dbotthepony.kstarbound.world.World
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
enum class Move {
|
enum class Move {
|
||||||
STAND_STILL,
|
STAND_STILL,
|
||||||
@ -12,43 +13,34 @@ enum class Move {
|
|||||||
MOVE_RIGHT
|
MOVE_RIGHT
|
||||||
}
|
}
|
||||||
|
|
||||||
open class AliveEntity(world: World<*, *>) : Entity(world) {
|
interface IWalkableEntity : IEntity {
|
||||||
open var maxHealth = 10.0
|
/**
|
||||||
open var health = 10.0
|
* AABB сущности, которая стоит
|
||||||
open val moveDirection = Move.STAND_STILL
|
*/
|
||||||
override val collisionResolution = CollisionResolution.SLIDE
|
val standingAABB: AABB
|
||||||
|
|
||||||
open val aabbDucked get() = aabb
|
|
||||||
|
|
||||||
override val currentaabb: AABB get() {
|
|
||||||
if (isDucked) {
|
|
||||||
return aabbDucked
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.currentaabb
|
|
||||||
}
|
|
||||||
|
|
||||||
var wantsToDuck = false
|
|
||||||
var isDucked = false
|
|
||||||
protected set
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Максимальная скорость передвижения этого существа в Starbound Units/секунда
|
* AABB сущности, которая присела
|
||||||
|
*/
|
||||||
|
val duckingAABB: AABB
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Максимальная скорость передвижения этого AliveMovementController в Starbound Units/секунда
|
||||||
*
|
*
|
||||||
* Скорость передвижения: Это скорость вдоль земли (или в воздухе, если парит) при ходьбе.
|
* Скорость передвижения: Это скорость вдоль земли (или в воздухе, если парит) при ходьбе.
|
||||||
*
|
*
|
||||||
* Если вектор скорости вдоль поверхности (или в воздухе, если парит) больше заданного значения,
|
* Если вектор скорости вдоль поверхности (или в воздухе, если парит) больше заданного значения,
|
||||||
* то сущность быстро тормозит (учитывая силу трения)
|
* то сущность быстро тормозит (учитывая силу трения)
|
||||||
*/
|
*/
|
||||||
open val topSpeed = 20.0
|
val topSpeed: Double
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Скорость ускорения сущности в Starbound Units/секунда^2
|
* Скорость ускорения сущности в Starbound Units/секунда^2
|
||||||
*
|
*
|
||||||
* Если сущность хочет двигаться вправо или влево (а также вверх или вниз, если парит),
|
* Если сущность хочет двигаться вправо или влево,
|
||||||
* то она разгоняется с данной скоростью.
|
* то она разгоняется с данной скоростью.
|
||||||
*/
|
*/
|
||||||
open val moveSpeed = 64.0
|
val moveSpeed: Double
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* То, как сущность может влиять на свою скорость в Starbound Units/секунда^2
|
* То, как сущность может влиять на свою скорость в Starbound Units/секунда^2
|
||||||
@ -56,33 +48,83 @@ open class AliveEntity(world: World<*, *>) : Entity(world) {
|
|||||||
*
|
*
|
||||||
* Позволяет в т.ч. игрокам изменять свою траекторию полёта в стиле Quake.
|
* Позволяет в т.ч. игрокам изменять свою траекторию полёта в стиле Quake.
|
||||||
*/
|
*/
|
||||||
open val freeFallMoveSpeed = 8.0
|
val freeFallMoveSpeed: Double
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Сила", с которой сущность останавливается, если не хочет двигаться.
|
* "Сила", с которой сущность останавливается, если не хочет двигаться.
|
||||||
*
|
*
|
||||||
* Зависит от текущего трения, так как технически является множителем трения поверхности,
|
* Зависит от текущего трения, так как технически является множителем трения поверхности,
|
||||||
* на которой стоит сущность. Если сущность парит, то сила трения является константой и не зависит от её окружения.
|
* на которой стоит сущность.
|
||||||
*/
|
*/
|
||||||
open val brakeForce = 32.0
|
val brakeForce: Double
|
||||||
|
|
||||||
/**
|
|
||||||
* Импульс прыжка данной сущности. Если сущность парит, то данное значение не несёт никакой
|
|
||||||
* полезной нагрузки.
|
|
||||||
*/
|
|
||||||
open val jumpForce = 20.0
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Высота шага данной сущности. Данное значение отвечает за то, на сколько блоков
|
* Высота шага данной сущности. Данное значение отвечает за то, на сколько блоков
|
||||||
* сможет подняться сущность просто двигаясь в одном направлении без необходимости прыгнуть.
|
* сможет подняться сущность просто двигаясь в одном направлении без необходимости прыгнуть.
|
||||||
*/
|
*/
|
||||||
open val stepSize = 1.1
|
val jumpForce: Double
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Импульс прыжка данной сущности. Если сущность парит, то данное значение не несёт никакой
|
||||||
|
* полезной нагрузки.
|
||||||
|
*/
|
||||||
|
val stepSize: Double
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Базовый абстрактный класс, реализующий сущность, которая ходит по земле
|
||||||
|
*/
|
||||||
|
abstract class WalkableMovementController<T : IWalkableEntity>(entity: T) : MovementController<T>(entity) {
|
||||||
|
protected abstract val moveDirection: Move
|
||||||
|
override val collisionResolution = CollisionResolution.SLIDE
|
||||||
|
|
||||||
|
var wantsToDuck = false
|
||||||
|
var isDucked = false
|
||||||
|
protected set
|
||||||
|
|
||||||
|
override val currentAABB: AABB get() {
|
||||||
|
if (isDucked) {
|
||||||
|
return entity.duckingAABB
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity.standingAABB
|
||||||
|
}
|
||||||
|
|
||||||
override fun thinkPhysics(delta: Double) {
|
override fun thinkPhysics(delta: Double) {
|
||||||
super.thinkPhysics(delta)
|
super.thinkPhysics(delta)
|
||||||
thinkMovement(delta)
|
thinkMovement(delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Смотрим [IWalkableEntity.topSpeed]
|
||||||
|
*/
|
||||||
|
open val topSpeed by entity::topSpeed
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Смотрим [IWalkableEntity.moveSpeed]
|
||||||
|
*/
|
||||||
|
open val moveSpeed by entity::moveSpeed
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Смотрим [IWalkableEntity.freeFallMoveSpeed]
|
||||||
|
*/
|
||||||
|
open val freeFallMoveSpeed by entity::freeFallMoveSpeed
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Смотрим [IWalkableEntity.brakeForce]
|
||||||
|
*/
|
||||||
|
open val brakeForce by entity::brakeForce
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Смотрим [IWalkableEntity.stepSize]
|
||||||
|
*/
|
||||||
|
open val stepSize by entity::stepSize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Смотрим [IWalkableEntity.jumpForce]
|
||||||
|
*/
|
||||||
|
open val jumpForce by entity::jumpForce
|
||||||
|
|
||||||
protected var jumpRequested = false
|
protected var jumpRequested = false
|
||||||
protected var nextJump = 0.0
|
protected var nextJump = 0.0
|
||||||
|
|
||||||
@ -101,14 +143,13 @@ open class AliveEntity(world: World<*, *>) : Entity(world) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun thinkMovement(delta: Double) {
|
protected open fun thinkMovement(delta: Double) {
|
||||||
if (onGround || !affectedByGravity) {
|
if (onGround || !affectedByGravity) {
|
||||||
var add: Vector2d
|
var add = Vector2d.ZERO
|
||||||
|
|
||||||
if (isDucked) {
|
if (isDucked) {
|
||||||
thinkFriction(delta * brakeForce)
|
thinkFriction(delta * brakeForce)
|
||||||
add = Vector2d.ZERO
|
} else if (velocity.y.absoluteValue < 1 && !jumpRequested) {
|
||||||
} else {
|
|
||||||
when (moveDirection) {
|
when (moveDirection) {
|
||||||
Move.STAND_STILL -> {
|
Move.STAND_STILL -> {
|
||||||
thinkFriction(delta * brakeForce)
|
thinkFriction(delta * brakeForce)
|
||||||
@ -147,7 +188,7 @@ open class AliveEntity(world: World<*, *>) : Entity(world) {
|
|||||||
|
|
||||||
if (world is ClientWorld && world.client.settings.debugCollisions) {
|
if (world is ClientWorld && world.client.settings.debugCollisions) {
|
||||||
world.client.onPostDrawWorldOnce {
|
world.client.onPostDrawWorldOnce {
|
||||||
world.client.gl.quadWireframe(worldaabb + velocity * delta * 4.0 + sweep.hitPosition, Color.RED)
|
world.client.gl.quadWireframe(worldAABB + velocity * delta * 4.0 + sweep.hitPosition, Color.RED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -165,14 +206,14 @@ open class AliveEntity(world: World<*, *>) : Entity(world) {
|
|||||||
|
|
||||||
if (world is ClientWorld && world.client.settings.debugCollisions) {
|
if (world is ClientWorld && world.client.settings.debugCollisions) {
|
||||||
world.client.onPostDrawWorldOnce {
|
world.client.onPostDrawWorldOnce {
|
||||||
world.client.gl.quadWireframe(worldaabb + sweep.hitPosition + sweep2.hitPosition, Color.GREEN)
|
world.client.gl.quadWireframe(worldAABB + sweep.hitPosition + sweep2.hitPosition, Color.GREEN)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (world is ClientWorld && world.client.settings.debugCollisions) {
|
if (world is ClientWorld && world.client.settings.debugCollisions) {
|
||||||
world.client.onPostDrawWorldOnce {
|
world.client.onPostDrawWorldOnce {
|
||||||
world.client.gl.quadWireframe(worldaabb + sweep.hitPosition, Color.BLUE)
|
world.client.gl.quadWireframe(worldAABB + sweep.hitPosition, Color.BLUE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -210,9 +251,31 @@ open class AliveEntity(world: World<*, *>) : Entity(world) {
|
|||||||
if (wantsToDuck && onGround) {
|
if (wantsToDuck && onGround) {
|
||||||
isDucked = true
|
isDucked = true
|
||||||
} else if (isDucked) {
|
} else if (isDucked) {
|
||||||
if (world.isSpaceEmptyFromTiles(aabb + pos)) {
|
if (world.isSpaceEmptyFromTiles(entity.standingAABB + pos)) {
|
||||||
isDucked = false
|
isDucked = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract class AliveEntity(world: World<*, *>) : Entity(world) {
|
||||||
|
open var maxHealth = 10.0
|
||||||
|
open var health = 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class AliveWalkingEntity(world: World<*, *>) : AliveEntity(world), IWalkableEntity {
|
||||||
|
abstract override val movement: WalkableMovementController<*>
|
||||||
|
|
||||||
|
override val topSpeed = 20.0
|
||||||
|
override val moveSpeed = 64.0
|
||||||
|
override val freeFallMoveSpeed = 8.0
|
||||||
|
override val brakeForce = 32.0
|
||||||
|
override val jumpForce = 20.0
|
||||||
|
override val stepSize = 1.1
|
||||||
|
|
||||||
|
open var wantsToDuck
|
||||||
|
get() = movement.wantsToDuck
|
||||||
|
set(value) { movement.wantsToDuck = value }
|
||||||
|
|
||||||
|
open val isDucked get() = movement.isDucked
|
||||||
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package ru.dbotthepony.kstarbound.world.entities
|
package ru.dbotthepony.kstarbound.world.entities
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager
|
import org.apache.logging.log4j.LogManager
|
||||||
import ru.dbotthepony.kstarbound.client.render.EntityRenderer
|
|
||||||
import ru.dbotthepony.kstarbound.math.AABB
|
import ru.dbotthepony.kstarbound.math.AABB
|
||||||
import ru.dbotthepony.kstarbound.math.Vector2d
|
import ru.dbotthepony.kstarbound.math.Vector2d
|
||||||
import ru.dbotthepony.kstarbound.math.lerp
|
import ru.dbotthepony.kstarbound.math.lerp
|
||||||
@ -9,18 +8,29 @@ import ru.dbotthepony.kstarbound.world.Chunk
|
|||||||
import ru.dbotthepony.kstarbound.world.ChunkPos
|
import ru.dbotthepony.kstarbound.world.ChunkPos
|
||||||
import ru.dbotthepony.kstarbound.world.World
|
import ru.dbotthepony.kstarbound.world.World
|
||||||
|
|
||||||
enum class CollisionResolution {
|
/**
|
||||||
STOP,
|
* Интерфейс служит лишь для убирания жёсткой зависимости от класса Entity
|
||||||
BOUNCE,
|
*/
|
||||||
PUSH,
|
interface IEntity {
|
||||||
SLIDE,
|
val world: World<*, *>
|
||||||
|
var chunk: Chunk<*, *>?
|
||||||
|
var pos: Vector2d
|
||||||
|
var rotation: Double
|
||||||
|
val movement: MovementController<*>
|
||||||
|
val isSpawned: Boolean
|
||||||
|
val isRemoved: Boolean
|
||||||
|
|
||||||
|
fun spawn()
|
||||||
|
fun remove()
|
||||||
|
fun think(delta: Double)
|
||||||
|
fun onTouchSurface(velocity: Vector2d, normal: Vector2d)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Определяет из себя сущность в мире, которая имеет позицию, скорость и коробку столкновений
|
* Определяет из себя сущность в мире, которая имеет позицию, скорость и коробку столкновений
|
||||||
*/
|
*/
|
||||||
open class Entity(val world: World<*, *>) {
|
abstract class Entity(override val world: World<*, *>) : IEntity {
|
||||||
var chunk: Chunk<*, *>? = null
|
override var chunk: Chunk<*, *>? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
if (!isSpawned) {
|
if (!isSpawned) {
|
||||||
throw IllegalStateException("Trying to set chunk this entity belong to before spawning in world")
|
throw IllegalStateException("Trying to set chunk this entity belong to before spawning in world")
|
||||||
@ -50,10 +60,7 @@ open class Entity(val world: World<*, *>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open val currentaabb: AABB get() = aabb
|
override var pos = Vector2d()
|
||||||
open val worldaabb: AABB get() = currentaabb + pos
|
|
||||||
|
|
||||||
var pos = Vector2d()
|
|
||||||
set(value) {
|
set(value) {
|
||||||
if (field == value)
|
if (field == value)
|
||||||
return
|
return
|
||||||
@ -71,13 +78,14 @@ open class Entity(val world: World<*, *>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var velocity = Vector2d()
|
override var rotation: Double = 0.0
|
||||||
var isSpawned = false
|
|
||||||
|
final override var isSpawned = false
|
||||||
private set
|
private set
|
||||||
var isRemoved = false
|
final override var isRemoved = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
fun spawn() {
|
override fun spawn() {
|
||||||
if (isSpawned)
|
if (isSpawned)
|
||||||
throw IllegalStateException("Already spawned")
|
throw IllegalStateException("Already spawned")
|
||||||
|
|
||||||
@ -90,7 +98,7 @@ open class Entity(val world: World<*, *>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove() {
|
override fun remove() {
|
||||||
if (isRemoved)
|
if (isRemoved)
|
||||||
throw IllegalStateException("Already removed")
|
throw IllegalStateException("Already removed")
|
||||||
|
|
||||||
@ -103,107 +111,24 @@ open class Entity(val world: World<*, *>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Касается ли сущность земли
|
* Контроллер перемещения данной сущности
|
||||||
*
|
|
||||||
* Данный флаг выставляется при обработке скорости, если данный флаг не будет выставлен
|
|
||||||
* правильно, то сущность будет иметь очень плохое движение в стороны
|
|
||||||
*
|
|
||||||
* Так же от него зависит то, может ли сущность двигаться, если она не парит
|
|
||||||
*
|
|
||||||
* Если сущность касается земли, то на неё не действует гравитация
|
|
||||||
*/
|
*/
|
||||||
var onGround = false
|
abstract override val movement: MovementController<*>
|
||||||
protected set(value) {
|
protected abstract fun thinkAI(delta: Double)
|
||||||
field = value
|
|
||||||
nextOnGroundUpdate = world.timer + 0.1
|
|
||||||
}
|
|
||||||
|
|
||||||
protected var nextOnGroundUpdate = 0.0
|
|
||||||
|
|
||||||
var groundNormal = Vector2d.ZERO
|
|
||||||
protected set
|
|
||||||
|
|
||||||
protected var isHuggingAWall = false
|
|
||||||
|
|
||||||
// наследуемые свойства
|
|
||||||
open val aabb = AABB.rectangle(Vector2d.ZERO, 0.9, 0.9)
|
|
||||||
open val affectedByGravity = true
|
|
||||||
open val collisionResolution = CollisionResolution.STOP
|
|
||||||
|
|
||||||
protected open fun onTouchGround(velocity: Vector2d, normal: Vector2d) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun sweepRelative(velocity: Vector2d, delta: Double, collisionResolution: CollisionResolution = this.collisionResolution) = world.sweep(worldaabb, velocity, collisionResolution, delta)
|
|
||||||
protected fun sweepAbsolute(from: Vector2d, velocity: Vector2d, delta: Double, collisionResolution: CollisionResolution = this.collisionResolution) = world.sweep(aabb + from, velocity, collisionResolution, delta)
|
|
||||||
protected fun isSpaceOpen(relative: Vector2d, delta: Double) = !sweepRelative(relative, delta).hitAnything
|
|
||||||
|
|
||||||
fun dropToFloor() {
|
|
||||||
val sweep = sweepRelative(Vector2d.DROP_TO_FLOOR, 1.0, CollisionResolution.STOP)
|
|
||||||
|
|
||||||
if (!sweep.hitAnything)
|
|
||||||
return
|
|
||||||
|
|
||||||
pos += sweep.hitPosition
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun propagateVelocity(delta: Double) {
|
|
||||||
if (velocity.length == 0.0)
|
|
||||||
return
|
|
||||||
|
|
||||||
val sweep = sweepRelative(velocity * delta, delta)
|
|
||||||
this.velocity = sweep.hitPosition / delta
|
|
||||||
this.pos += this.velocity * delta
|
|
||||||
|
|
||||||
if (nextOnGroundUpdate <= world.timer || !onGround) {
|
|
||||||
onGround = sweep.hitNormal.dotProduct(world.gravity.normalized) <= -0.98
|
|
||||||
groundNormal = sweep.hitNormal
|
|
||||||
|
|
||||||
if (!onGround) {
|
|
||||||
val sweepGround = sweepRelative(world.gravity * delta, delta)
|
|
||||||
onGround = sweepGround.hitAnything && sweepGround.hitNormal.dotProduct(world.gravity.normalized) <= -0.98
|
|
||||||
groundNormal = sweepGround.hitNormal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun thinkGravity(delta: Double) {
|
|
||||||
velocity += world.gravity * delta
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun thinkFriction(delta: Double) {
|
|
||||||
velocity *= Vector2d(lerp(delta, 1.0, 0.01), 1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun thinkPhysics(delta: Double) {
|
|
||||||
if (!onGround && affectedByGravity)
|
|
||||||
thinkGravity(delta)
|
|
||||||
|
|
||||||
propagateVelocity(delta)
|
|
||||||
|
|
||||||
if (affectedByGravity && onGround)
|
|
||||||
thinkFriction(delta)
|
|
||||||
|
|
||||||
//dropToFloor()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Заставляет сущность "думать".
|
* Заставляет сущность "думать".
|
||||||
*/
|
*/
|
||||||
fun think(delta: Double) {
|
final override fun think(delta: Double) {
|
||||||
if (!isSpawned) {
|
if (!isSpawned) {
|
||||||
throw IllegalStateException("Tried to think before spawning in world")
|
throw IllegalStateException("Tried to think before spawning in world")
|
||||||
}
|
}
|
||||||
|
|
||||||
thinkPhysics(delta)
|
movement.thinkPhysics(delta)
|
||||||
thinkAI(delta)
|
thinkAI(delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun thinkAI(delta: Double) {
|
override fun onTouchSurface(velocity: Vector2d, normal: Vector2d) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val LOGGER = LogManager.getLogger(Entity::class.java)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,141 @@
|
|||||||
|
package ru.dbotthepony.kstarbound.world.entities
|
||||||
|
|
||||||
|
import ru.dbotthepony.kstarbound.math.AABB
|
||||||
|
import ru.dbotthepony.kstarbound.math.Vector2d
|
||||||
|
import ru.dbotthepony.kstarbound.math.lerp
|
||||||
|
|
||||||
|
enum class CollisionResolution {
|
||||||
|
STOP,
|
||||||
|
BOUNCE,
|
||||||
|
PUSH,
|
||||||
|
SLIDE,
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class MovementController<T : IEntity>(val entity: T) {
|
||||||
|
val world = entity.world
|
||||||
|
var pos by entity::pos
|
||||||
|
var rotation by entity::rotation
|
||||||
|
|
||||||
|
open val mass = 1.0
|
||||||
|
|
||||||
|
// наследуемые свойства
|
||||||
|
open val affectedByGravity = true
|
||||||
|
open val collisionResolution = CollisionResolution.STOP
|
||||||
|
|
||||||
|
var velocity = Vector2d()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Касается ли AABB сущности земли
|
||||||
|
*
|
||||||
|
* Данный флаг выставляется при обработке скорости, если данный флаг не будет выставлен
|
||||||
|
* правильно, то сущность будет иметь очень плохое движение в стороны
|
||||||
|
*
|
||||||
|
* Так же от него зависит то, может ли сущность двигаться, если она не парит
|
||||||
|
*
|
||||||
|
* Если сущность касается земли, то на неё не действует гравитация
|
||||||
|
*/
|
||||||
|
var onGround = false
|
||||||
|
protected set(value) {
|
||||||
|
field = value
|
||||||
|
nextOnGroundUpdate = world.timer + 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Текущий AABB этого Movement Controller
|
||||||
|
*
|
||||||
|
* Это может быть как и статичное значение (для данного типа сущности), так и динамичное
|
||||||
|
* (к примеру, присевший игрок)
|
||||||
|
*
|
||||||
|
* Данное значение, хоть и является val, НЕ ЯВЛЯЕТСЯ КОНСТАНТОЙ!
|
||||||
|
*/
|
||||||
|
abstract val currentAABB: AABB
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Текущий AABB в отображении на мировые координаты
|
||||||
|
*/
|
||||||
|
val worldAABB: AABB get() = currentAABB + pos
|
||||||
|
|
||||||
|
protected var nextOnGroundUpdate = 0.0
|
||||||
|
|
||||||
|
protected fun sweepRelative(velocity: Vector2d, delta: Double, collisionResolution: CollisionResolution = this.collisionResolution) = world.sweep(worldAABB, velocity, collisionResolution, delta)
|
||||||
|
protected fun sweepAbsolute(from: Vector2d, velocity: Vector2d, delta: Double, collisionResolution: CollisionResolution = this.collisionResolution) = world.sweep(currentAABB + from, velocity, collisionResolution, delta)
|
||||||
|
protected fun isSpaceOpen(relative: Vector2d, delta: Double) = !sweepRelative(relative, delta).hitAnything
|
||||||
|
|
||||||
|
fun dropToFloor() {
|
||||||
|
val sweep = sweepRelative(Vector2d.DROP_TO_FLOOR, 1.0, CollisionResolution.STOP)
|
||||||
|
|
||||||
|
if (!sweep.hitAnything)
|
||||||
|
return
|
||||||
|
|
||||||
|
pos += sweep.hitPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
var groundNormal = Vector2d.ZERO
|
||||||
|
protected set
|
||||||
|
|
||||||
|
protected open fun propagateVelocity(delta: Double) {
|
||||||
|
if (velocity.length == 0.0)
|
||||||
|
return
|
||||||
|
|
||||||
|
val sweep = sweepRelative(velocity * delta, delta)
|
||||||
|
this.velocity = sweep.hitPosition / delta
|
||||||
|
this.pos += this.velocity * delta
|
||||||
|
|
||||||
|
if (nextOnGroundUpdate <= world.timer || !onGround) {
|
||||||
|
onGround = sweep.hitNormal.dotProduct(world.gravity.normalized) <= -0.98
|
||||||
|
groundNormal = sweep.hitNormal
|
||||||
|
|
||||||
|
if (!onGround) {
|
||||||
|
val sweepGround = sweepRelative(world.gravity * delta, delta)
|
||||||
|
onGround = sweepGround.hitAnything && sweepGround.hitNormal.dotProduct(world.gravity.normalized) <= -0.98
|
||||||
|
groundNormal = sweepGround.hitNormal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun thinkGravity(delta: Double) {
|
||||||
|
velocity += world.gravity * delta
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun thinkFriction(delta: Double) {
|
||||||
|
velocity *= Vector2d(lerp(delta, 1.0, 0.01), 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun thinkPhysics(delta: Double) {
|
||||||
|
if (!onGround && affectedByGravity)
|
||||||
|
thinkGravity(delta)
|
||||||
|
|
||||||
|
propagateVelocity(delta)
|
||||||
|
|
||||||
|
if (affectedByGravity && onGround)
|
||||||
|
thinkFriction(delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun onTouchSurface(velocity: Vector2d, normal: Vector2d) {
|
||||||
|
entity.onTouchSurface(velocity, normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MovementController который ничего не делает (прям совсем)
|
||||||
|
*/
|
||||||
|
class DummyMovementController(entity: Entity) : MovementController<Entity>(entity) {
|
||||||
|
override val currentAABB = DUMMY_AABB
|
||||||
|
override val affectedByGravity = false
|
||||||
|
|
||||||
|
override fun propagateVelocity(delta: Double) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun thinkGravity(delta: Double) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun thinkFriction(delta: Double) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DUMMY_AABB = AABB.rectangle(Vector2d.ZERO, 0.1, 0.1)
|
||||||
|
}
|
||||||
|
}
|
@ -4,11 +4,19 @@ import ru.dbotthepony.kstarbound.math.AABB
|
|||||||
import ru.dbotthepony.kstarbound.math.Vector2d
|
import ru.dbotthepony.kstarbound.math.Vector2d
|
||||||
import ru.dbotthepony.kstarbound.world.World
|
import ru.dbotthepony.kstarbound.world.World
|
||||||
|
|
||||||
|
class PlayerMovementController(entity: PlayerEntity) : WalkableMovementController<PlayerEntity>(entity) {
|
||||||
|
public override var moveDirection = Move.STAND_STILL
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Физический аватар игрока в мире
|
* Физический аватар игрока в мире
|
||||||
*/
|
*/
|
||||||
open class PlayerEntity(world: World<*, *>) : AliveEntity(world) {
|
open class PlayerEntity(world: World<*, *>) : AliveWalkingEntity(world) {
|
||||||
override val aabb = AABB.rectangle(Vector2d.ZERO, 1.8, 3.7)
|
override val standingAABB = AABB.rectangle(Vector2d.ZERO, 1.8, 3.7)
|
||||||
override val aabbDucked: AABB = AABB.rectangle(Vector2d.ZERO, 1.8, 1.8) + Vector2d(y = -0.9)
|
override val duckingAABB = AABB.rectangle(Vector2d.ZERO, 1.8, 1.8) + Vector2d(y = -0.9)
|
||||||
override var moveDirection = Move.STAND_STILL
|
override val movement = PlayerMovementController(this)
|
||||||
|
|
||||||
|
override fun thinkAI(delta: Double) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
package ru.dbotthepony.kstarbound.world.entities
|
||||||
|
|
||||||
|
import ru.dbotthepony.kstarbound.defs.projectile.ConfiguredProjectile
|
||||||
|
import ru.dbotthepony.kstarbound.world.World
|
||||||
|
|
||||||
|
class Projectile(world: World<*, *>, val def: ConfiguredProjectile) : Entity(world) {
|
||||||
|
override val movement: MovementController<*> = DummyMovementController(this)
|
||||||
|
|
||||||
|
override fun thinkAI(delta: Double) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user