Подгрузка описания прожектайлов и их тест рендер
This commit is contained in:
parent
4fc51530f7
commit
ad8910d098
@ -31,11 +31,12 @@ tasks.compileKotlin {
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(kotlin("reflect"))
|
||||
|
||||
implementation("org.apache.logging.log4j:log4j-api: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")
|
||||
|
||||
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.lwjgl.Version
|
||||
import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose
|
||||
import ru.dbotthepony.kstarbound.api.PhysicalFS
|
||||
import ru.dbotthepony.kstarbound.client.StarboundClient
|
||||
import ru.dbotthepony.kstarbound.defs.TileDefinition
|
||||
import ru.dbotthepony.kstarbound.io.StarboundPak
|
||||
import ru.dbotthepony.kstarbound.math.Vector2d
|
||||
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
|
||||
import ru.dbotthepony.kstarbound.world.Chunk
|
||||
import ru.dbotthepony.kstarbound.world.ChunkPos
|
||||
import ru.dbotthepony.kstarbound.world.entities.Move
|
||||
import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.Projectile
|
||||
import java.io.File
|
||||
|
||||
private val LOGGER = LogManager.getLogger()
|
||||
@ -20,6 +19,14 @@ private val LOGGER = LogManager.getLogger()
|
||||
fun main() {
|
||||
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()
|
||||
|
||||
//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
|
||||
}*/
|
||||
|
||||
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()
|
||||
@ -117,7 +130,7 @@ fun main() {
|
||||
|
||||
client.onDrawGUI {
|
||||
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 {
|
||||
@ -140,17 +153,17 @@ fun main() {
|
||||
//ent.velocity += client.camera.velocity.toDoubleVector() * client.frameRenderTime * 0.1
|
||||
|
||||
if (client.input.KEY_LEFT_DOWN) {
|
||||
ent.moveDirection = Move.MOVE_LEFT
|
||||
ent.movement.moveDirection = Move.MOVE_LEFT
|
||||
} else if (client.input.KEY_RIGHT_DOWN) {
|
||||
ent.moveDirection = Move.MOVE_RIGHT
|
||||
ent.movement.moveDirection = Move.MOVE_RIGHT
|
||||
} else {
|
||||
ent.moveDirection = Move.STAND_STILL
|
||||
ent.movement.moveDirection = Move.STAND_STILL
|
||||
}
|
||||
|
||||
if (client.input.KEY_SPACE_PRESSED && ent.onGround) {
|
||||
ent.requestJump()
|
||||
if (client.input.KEY_SPACE_PRESSED && ent.movement.onGround) {
|
||||
ent.movement.requestJump()
|
||||
} else if (client.input.KEY_SPACE_RELEASED) {
|
||||
ent.recallJump()
|
||||
ent.movement.recallJump()
|
||||
}
|
||||
|
||||
if (client.input.KEY_ESCAPE_PRESSED) {
|
||||
|
@ -1,16 +1,22 @@
|
||||
package ru.dbotthepony.kstarbound
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import com.google.gson.*
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kstarbound.api.IVFS
|
||||
import ru.dbotthepony.kstarbound.api.PhysicalFS
|
||||
import ru.dbotthepony.kstarbound.defs.TileDefinition
|
||||
import ru.dbotthepony.kstarbound.defs.TileDefinitionBuilder
|
||||
import ru.dbotthepony.kstarbound.api.getPathFolder
|
||||
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.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.nio.ByteBuffer
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.HashMap
|
||||
@ -22,10 +28,31 @@ const val PIXELS_IN_STARBOUND_UNIT = 8.0
|
||||
const val PIXELS_IN_STARBOUND_UNITf = 8.0f
|
||||
|
||||
class TileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause)
|
||||
class ProjectileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause)
|
||||
|
||||
object Starbound : IVFS {
|
||||
private val LOGGER = LogManager.getLogger()
|
||||
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
|
||||
private set
|
||||
@ -87,17 +114,36 @@ object Starbound : IVFS {
|
||||
}
|
||||
}
|
||||
|
||||
callback(false, false, "Loading materials...")
|
||||
run {
|
||||
val localTime = System.currentTimeMillis()
|
||||
|
||||
loadTileMaterials {
|
||||
if (terminateLoading) {
|
||||
throw InterruptedException("Game is terminating")
|
||||
callback(false, false, "Loading materials...")
|
||||
|
||||
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
|
||||
initialized = true
|
||||
@ -135,6 +181,16 @@ object Starbound : IVFS {
|
||||
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) {
|
||||
if (initialized) {
|
||||
callback()
|
||||
@ -155,7 +211,7 @@ object Starbound : IVFS {
|
||||
|
||||
private fun loadTileMaterials(callback: (String) -> Unit) {
|
||||
for (fs in fileSystems) {
|
||||
for (listedFile in fs.listFiles("tiles/materials")) {
|
||||
for (listedFile in fs.listAllFiles("tiles/materials")) {
|
||||
if (listedFile.endsWith(".material")) {
|
||||
try {
|
||||
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 {
|
||||
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 {
|
||||
return readOrNull(path) ?: throw FileNotFoundException("No such file $path")
|
||||
}
|
||||
@ -21,6 +44,35 @@ interface IVFS {
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
val root: File = root.absoluteFile
|
||||
|
||||
@ -71,8 +127,20 @@ class PhysicalFS(root: File) : IVFS {
|
||||
if (path.contains("..")) {
|
||||
return listOf()
|
||||
}
|
||||
|
||||
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)
|
||||
} ?: return listOf()
|
||||
}
|
||||
|
@ -266,6 +266,9 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
|
||||
layerQueue.add(baked::renderStacked to zLevel)
|
||||
}
|
||||
|
||||
//println("${entityRenderers.size} at $pos")
|
||||
//println("${entities.size} at $pos")
|
||||
|
||||
for (renderer in entityRenderers.values) {
|
||||
layerQueue.add(lambda@{ it: Matrix4fStack ->
|
||||
val relative = renderer.renderPos - posVector2d
|
||||
@ -284,7 +287,7 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk<ClientWorld, Client
|
||||
private val entityRenderers = HashMap<Entity, EntityRenderer>()
|
||||
|
||||
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) {
|
||||
|
@ -9,7 +9,7 @@ import org.lwjgl.opengl.GL46.*
|
||||
// GL_STACK_UNDERFLOW
|
||||
// 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)
|
||||
|
||||
|
@ -253,11 +253,30 @@ class GLStateTracker {
|
||||
private val named2DTextures = HashMap<String, GLTexture2D>()
|
||||
|
||||
fun loadNamedTexture(path: String, memoryFormat: Int = GL_RGBA, fileFormat: Int = GL_RGBA): GLTexture2D {
|
||||
if (!Starbound.pathExists(path)) {
|
||||
throw FileNotFoundException("Unable to locate $path")
|
||||
return named2DTextures.computeIfAbsent(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) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
override val small by lazy {
|
||||
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
|
||||
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
|
||||
|
||||
var textureMinFilter by GLTexturePropertyTracker(GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
||||
|
@ -1,10 +1,13 @@
|
||||
package ru.dbotthepony.kstarbound.client.render
|
||||
|
||||
import org.lwjgl.opengl.GL46.*
|
||||
import ru.dbotthepony.kstarbound.client.ClientChunk
|
||||
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.Vector2d
|
||||
import ru.dbotthepony.kstarbound.world.entities.Entity
|
||||
import ru.dbotthepony.kstarbound.world.entities.Projectile
|
||||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
@ -21,7 +24,7 @@ open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open va
|
||||
|
||||
open fun renderDebug() {
|
||||
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() {
|
||||
|
||||
}
|
||||
|
||||
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) {
|
||||
private val files = HashMap<String, StarboundPakFile>()
|
||||
private val directories = HashMap<String, StarboundPakDirectory>()
|
||||
val files = HashMap<String, StarboundPakFile>()
|
||||
val directories = HashMap<String, StarboundPakDirectory>()
|
||||
|
||||
fun resolve(path: Array<String>, level: Int = 0): StarboundPakDirectory {
|
||||
if (path.size == level) {
|
||||
if (path.size == level)
|
||||
return this
|
||||
}
|
||||
|
||||
if (level == 0 && path[0] == "" && name == "/")
|
||||
return resolve(path, 1)
|
||||
@ -94,6 +93,7 @@ class StarboundPakDirectory(val name: String, val parent: StarboundPakDirectory?
|
||||
fun getDirectory(name: String) = directories[name]
|
||||
|
||||
fun listFiles(): Collection<StarboundPakFile> = Collections.unmodifiableCollection(files.values)
|
||||
fun listDirectories(): Collection<StarboundPakDirectory> = Collections.unmodifiableCollection(directories.values)
|
||||
|
||||
fun writeFile(file: StarboundPakFile) {
|
||||
files[file.name.split('/').last()] = file
|
||||
@ -105,9 +105,9 @@ class StarboundPakDirectory(val name: String, val parent: StarboundPakDirectory?
|
||||
|
||||
while (getParent != null) {
|
||||
if (getParent.parent != null) {
|
||||
build = "${getParent.name}/$name"
|
||||
build = "${getParent.name}/$build"
|
||||
} else {
|
||||
build = "/$name"
|
||||
build = "/$build"
|
||||
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 }
|
||||
}
|
||||
|
||||
override fun listDirectories(path: String): Collection<String> {
|
||||
return root.resolve(path.split("/").toTypedArray()).listDirectories().map { it.fullName() }
|
||||
}
|
||||
|
||||
override fun pathExists(path: String): Boolean {
|
||||
return indexNodes.containsKey(path)
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
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.world.ChunkPos
|
||||
import kotlin.math.absoluteValue
|
||||
@ -33,8 +36,8 @@ data class SweepResult(
|
||||
*/
|
||||
data class AABB(val mins: Vector2d, val maxs: Vector2d) {
|
||||
init {
|
||||
require(mins.x < maxs.x) { "mins.x ${mins.x} is more or equal to maxs.x ${maxs.x}" }
|
||||
require(mins.y < maxs.y) { "mins.y ${mins.y} is more or equal to maxs.y ${maxs.y}" }
|
||||
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 than maxs.y ${maxs.y}" }
|
||||
}
|
||||
|
||||
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) {
|
||||
init {
|
||||
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 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
|
||||
|
||||
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 kotlin.math.cos
|
||||
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 UP = Vector2i().up()
|
||||
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>() {
|
||||
override fun make(x: Float, y: Float): MutableVector2f {
|
||||
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>() {
|
||||
override fun make(x: Double, y: Double): MutableVector2d {
|
||||
this.x = x
|
||||
|
@ -1,12 +1,22 @@
|
||||
package ru.dbotthepony.kstarbound.util
|
||||
|
||||
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 java.lang.reflect.Type
|
||||
|
||||
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: Long) : this(
|
||||
((input ushr 16) and 0xFFL).toFloat() / 255f,
|
||||
((input ushr 8) and 0xFFL).toFloat() / 255f,
|
||||
(input and 0xFFL).toFloat() / 255f,
|
||||
)
|
||||
|
||||
companion object {
|
||||
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 PRE_DEFINED_MAP = mapOf(
|
||||
"red" to RED,
|
||||
"green" to GREEN,
|
||||
"blue" to BLUE,
|
||||
)
|
||||
|
||||
val SHADES_OF_GRAY = ArrayList<Color>().let {
|
||||
for (i in 0 .. 256) {
|
||||
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)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Chunk(pos=$pos, entityCount=${entities.size}, world=$world)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
val EMPTY = object : IMutableTileChunk {
|
||||
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.util.Color
|
||||
import ru.dbotthepony.kstarbound.world.World
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
enum class Move {
|
||||
STAND_STILL,
|
||||
@ -12,43 +13,34 @@ enum class Move {
|
||||
MOVE_RIGHT
|
||||
}
|
||||
|
||||
open class AliveEntity(world: World<*, *>) : Entity(world) {
|
||||
open var maxHealth = 10.0
|
||||
open var health = 10.0
|
||||
open val moveDirection = Move.STAND_STILL
|
||||
override val collisionResolution = CollisionResolution.SLIDE
|
||||
|
||||
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
|
||||
interface IWalkableEntity : IEntity {
|
||||
/**
|
||||
* AABB сущности, которая стоит
|
||||
*/
|
||||
val standingAABB: AABB
|
||||
|
||||
/**
|
||||
* Максимальная скорость передвижения этого существа в Starbound Units/секунда
|
||||
* AABB сущности, которая присела
|
||||
*/
|
||||
val duckingAABB: AABB
|
||||
|
||||
/**
|
||||
* Максимальная скорость передвижения этого AliveMovementController в Starbound Units/секунда
|
||||
*
|
||||
* Скорость передвижения: Это скорость вдоль земли (или в воздухе, если парит) при ходьбе.
|
||||
*
|
||||
* Если вектор скорости вдоль поверхности (или в воздухе, если парит) больше заданного значения,
|
||||
* то сущность быстро тормозит (учитывая силу трения)
|
||||
*/
|
||||
open val topSpeed = 20.0
|
||||
val topSpeed: Double
|
||||
|
||||
/**
|
||||
* Скорость ускорения сущности в Starbound Units/секунда^2
|
||||
*
|
||||
* Если сущность хочет двигаться вправо или влево (а также вверх или вниз, если парит),
|
||||
* Если сущность хочет двигаться вправо или влево,
|
||||
* то она разгоняется с данной скоростью.
|
||||
*/
|
||||
open val moveSpeed = 64.0
|
||||
val moveSpeed: Double
|
||||
|
||||
/**
|
||||
* То, как сущность может влиять на свою скорость в Starbound Units/секунда^2
|
||||
@ -56,33 +48,83 @@ open class AliveEntity(world: World<*, *>) : Entity(world) {
|
||||
*
|
||||
* Позволяет в т.ч. игрокам изменять свою траекторию полёта в стиле Quake.
|
||||
*/
|
||||
open val freeFallMoveSpeed = 8.0
|
||||
val freeFallMoveSpeed: Double
|
||||
|
||||
/**
|
||||
* "Сила", с которой сущность останавливается, если не хочет двигаться.
|
||||
*
|
||||
* Зависит от текущего трения, так как технически является множителем трения поверхности,
|
||||
* на которой стоит сущность. Если сущность парит, то сила трения является константой и не зависит от её окружения.
|
||||
* на которой стоит сущность.
|
||||
*/
|
||||
open val brakeForce = 32.0
|
||||
|
||||
/**
|
||||
* Импульс прыжка данной сущности. Если сущность парит, то данное значение не несёт никакой
|
||||
* полезной нагрузки.
|
||||
*/
|
||||
open val jumpForce = 20.0
|
||||
val brakeForce: Double
|
||||
|
||||
/**
|
||||
* Высота шага данной сущности. Данное значение отвечает за то, на сколько блоков
|
||||
* сможет подняться сущность просто двигаясь в одном направлении без необходимости прыгнуть.
|
||||
*/
|
||||
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) {
|
||||
super.thinkPhysics(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 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) {
|
||||
var add: Vector2d
|
||||
var add = Vector2d.ZERO
|
||||
|
||||
if (isDucked) {
|
||||
thinkFriction(delta * brakeForce)
|
||||
add = Vector2d.ZERO
|
||||
} else {
|
||||
} else if (velocity.y.absoluteValue < 1 && !jumpRequested) {
|
||||
when (moveDirection) {
|
||||
Move.STAND_STILL -> {
|
||||
thinkFriction(delta * brakeForce)
|
||||
@ -147,7 +188,7 @@ open class AliveEntity(world: World<*, *>) : Entity(world) {
|
||||
|
||||
if (world is ClientWorld && world.client.settings.debugCollisions) {
|
||||
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 {
|
||||
@ -165,14 +206,14 @@ open class AliveEntity(world: World<*, *>) : Entity(world) {
|
||||
|
||||
if (world is ClientWorld && world.client.settings.debugCollisions) {
|
||||
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) {
|
||||
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) {
|
||||
isDucked = true
|
||||
} else if (isDucked) {
|
||||
if (world.isSpaceEmptyFromTiles(aabb + pos)) {
|
||||
if (world.isSpaceEmptyFromTiles(entity.standingAABB + pos)) {
|
||||
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
|
||||
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kstarbound.client.render.EntityRenderer
|
||||
import ru.dbotthepony.kstarbound.math.AABB
|
||||
import ru.dbotthepony.kstarbound.math.Vector2d
|
||||
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.World
|
||||
|
||||
enum class CollisionResolution {
|
||||
STOP,
|
||||
BOUNCE,
|
||||
PUSH,
|
||||
SLIDE,
|
||||
/**
|
||||
* Интерфейс служит лишь для убирания жёсткой зависимости от класса Entity
|
||||
*/
|
||||
interface IEntity {
|
||||
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<*, *>) {
|
||||
var chunk: Chunk<*, *>? = null
|
||||
abstract class Entity(override val world: World<*, *>) : IEntity {
|
||||
override var chunk: Chunk<*, *>? = null
|
||||
set(value) {
|
||||
if (!isSpawned) {
|
||||
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
|
||||
open val worldaabb: AABB get() = currentaabb + pos
|
||||
|
||||
var pos = Vector2d()
|
||||
override var pos = Vector2d()
|
||||
set(value) {
|
||||
if (field == value)
|
||||
return
|
||||
@ -71,13 +78,14 @@ open class Entity(val world: World<*, *>) {
|
||||
}
|
||||
}
|
||||
|
||||
var velocity = Vector2d()
|
||||
var isSpawned = false
|
||||
override var rotation: Double = 0.0
|
||||
|
||||
final override var isSpawned = false
|
||||
private set
|
||||
var isRemoved = false
|
||||
final override var isRemoved = false
|
||||
private set
|
||||
|
||||
fun spawn() {
|
||||
override fun spawn() {
|
||||
if (isSpawned)
|
||||
throw IllegalStateException("Already spawned")
|
||||
|
||||
@ -90,7 +98,7 @@ open class Entity(val world: World<*, *>) {
|
||||
}
|
||||
}
|
||||
|
||||
fun remove() {
|
||||
override fun remove() {
|
||||
if (isRemoved)
|
||||
throw IllegalStateException("Already removed")
|
||||
|
||||
@ -103,107 +111,24 @@ open class Entity(val world: World<*, *>) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Касается ли сущность земли
|
||||
*
|
||||
* Данный флаг выставляется при обработке скорости, если данный флаг не будет выставлен
|
||||
* правильно, то сущность будет иметь очень плохое движение в стороны
|
||||
*
|
||||
* Так же от него зависит то, может ли сущность двигаться, если она не парит
|
||||
*
|
||||
* Если сущность касается земли, то на неё не действует гравитация
|
||||
* Контроллер перемещения данной сущности
|
||||
*/
|
||||
var onGround = false
|
||||
protected set(value) {
|
||||
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()
|
||||
}
|
||||
abstract override val movement: MovementController<*>
|
||||
protected abstract fun thinkAI(delta: Double)
|
||||
|
||||
/**
|
||||
* Заставляет сущность "думать".
|
||||
*/
|
||||
fun think(delta: Double) {
|
||||
final override fun think(delta: Double) {
|
||||
if (!isSpawned) {
|
||||
throw IllegalStateException("Tried to think before spawning in world")
|
||||
}
|
||||
|
||||
thinkPhysics(delta)
|
||||
movement.thinkPhysics(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.world.World
|
||||
|
||||
class PlayerMovementController(entity: PlayerEntity) : WalkableMovementController<PlayerEntity>(entity) {
|
||||
public override var moveDirection = Move.STAND_STILL
|
||||
}
|
||||
|
||||
/**
|
||||
* Физический аватар игрока в мире
|
||||
*/
|
||||
open class PlayerEntity(world: World<*, *>) : AliveEntity(world) {
|
||||
override val aabb = 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 var moveDirection = Move.STAND_STILL
|
||||
open class PlayerEntity(world: World<*, *>) : AliveWalkingEntity(world) {
|
||||
override val standingAABB = AABB.rectangle(Vector2d.ZERO, 1.8, 3.7)
|
||||
override val duckingAABB = AABB.rectangle(Vector2d.ZERO, 1.8, 1.8) + Vector2d(y = -0.9)
|
||||
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