From a3f4cf8338333694a9707c3eae52739c602cb959 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Mon, 21 Feb 2022 17:00:09 +0700 Subject: [PATCH] Projectile physics test --- build.gradle.kts | 4 +- .../kvector/vector/ndouble/Vector2d.kt | 32 +++ .../kvector/vector/nfloat/Vector2f.kt | 33 ++++ .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 22 ++- .../ru/dbotthepony/kstarbound/Starbound.kt | 14 +- .../kstarbound/client/ClientChunk.kt | 3 - .../kstarbound/client/ClientWorld.kt | 10 +- .../kstarbound/client/gl/VertexBuilder.kt | 24 +++ .../client/render/EntityRenderer.kt | 19 +- .../kstarbound/defs/ConfigurableDefinition.kt | 118 ----------- .../dbotthepony/kstarbound/defs/DamageType.kt | 23 +++ .../defs/projectile/Configurable.kt | 183 +++++++++++++++++- .../kstarbound/defs/projectile/Configured.kt | 98 +++++++++- .../defs/projectile/ProjectilePhysics.kt | 2 +- .../kstarbound/io/ConfigurableTypeAdapter.kt | 129 ++++++++++++ .../{util => io}/CustomEnumTypeAdapter.kt | 2 +- .../dbotthepony/kstarbound/io/KTypeAdapter.kt | 143 ++++++++++++++ .../ru/dbotthepony/kstarbound/world/Chunk.kt | 1 + .../ru/dbotthepony/kstarbound/world/World.kt | 99 +++++++++- .../kstarbound/world/entities/AliveEntity.kt | 7 +- .../kstarbound/world/entities/Entity.kt | 20 +- .../world/entities/MovementController.kt | 37 +++- .../kstarbound/world/entities/PlayerEntity.kt | 19 ++ .../kstarbound/world/entities/Projectile.kt | 12 -- .../world/entities/projectile/Physics.kt | 123 ++++++++++++ .../world/entities/projectile/Projectile.kt | 55 ++++++ 26 files changed, 1047 insertions(+), 185 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/DamageType.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/io/ConfigurableTypeAdapter.kt rename src/main/kotlin/ru/dbotthepony/kstarbound/{util => io}/CustomEnumTypeAdapter.kt (95%) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/io/KTypeAdapter.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Projectile.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/projectile/Physics.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/projectile/Projectile.kt diff --git a/build.gradle.kts b/build.gradle.kts index 8d53997f..0656940e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -58,8 +58,8 @@ sourceSets.test { } dependencies { - implementation(kotlin("stdlib")) - implementation(kotlin("reflect")) + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.10") + implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.10") implementation("org.apache.logging.log4j:log4j-api:2.17.1") implementation("org.apache.logging.log4j:log4j-core:2.17.1") diff --git a/src/kvector/kotlin/ru/dbotthepony/kvector/vector/ndouble/Vector2d.kt b/src/kvector/kotlin/ru/dbotthepony/kvector/vector/ndouble/Vector2d.kt index 5973184d..f30c01ac 100644 --- a/src/kvector/kotlin/ru/dbotthepony/kvector/vector/ndouble/Vector2d.kt +++ b/src/kvector/kotlin/ru/dbotthepony/kvector/vector/ndouble/Vector2d.kt @@ -6,6 +6,9 @@ package ru.dbotthepony.kvector.vector.ndouble import ru.dbotthepony.kvector.api.* import ru.dbotthepony.kvector.vector.nfloat.Vector2f import kotlin.math.absoluteValue +import kotlin.math.acos +import kotlin.math.cos +import kotlin.math.sin /** * 2D Vector, representing two-dimensional coordinates as [Double]s @@ -123,6 +126,35 @@ open class Vector2d( return Vector2d(x / other, y / other) } + /** + * Rotates this vector by given [angle] in radians + * + * @return rotated vector + */ + fun rotate(angle: Double): Vector2d { + val s = sin(angle) + val c = cos(angle) + + return Vector2d(x * c - s * y, s * x + c * y) + } + + /** + * Returns the angle in radians this unit vector points to, + * treating zero angle as [POSITIVE_X]. + * + * If this vector is not normalized (not a unit vector), + * behavior of this method is undefined. + */ + fun toAngle(): Double { + val dot = dot(POSITIVE_X) + + if (y > 0.0) { + return acos(dot) + } else { + return -acos(dot) + } + } + /** * Calculates vector vector * vector, returning result as [Double]. */ diff --git a/src/kvector/kotlin/ru/dbotthepony/kvector/vector/nfloat/Vector2f.kt b/src/kvector/kotlin/ru/dbotthepony/kvector/vector/nfloat/Vector2f.kt index cb92d2b0..28b47407 100644 --- a/src/kvector/kotlin/ru/dbotthepony/kvector/vector/nfloat/Vector2f.kt +++ b/src/kvector/kotlin/ru/dbotthepony/kvector/vector/nfloat/Vector2f.kt @@ -5,7 +5,11 @@ package ru.dbotthepony.kvector.vector.nfloat import ru.dbotthepony.kvector.api.* import ru.dbotthepony.kvector.vector.ndouble.Vector2d +import ru.dbotthepony.kvector.vector.ndouble.Vector2d.Companion.POSITIVE_X import kotlin.math.absoluteValue +import kotlin.math.acos +import kotlin.math.cos +import kotlin.math.sin /** * 2D Vector, representing two-dimensional coordinates as [Float]s @@ -156,6 +160,35 @@ open class Vector2f( return "[${x}f ${y}f]" } + /** + * Rotates this vector by given [angle] in radians + * + * @return rotated vector + */ + fun rotate(angle: Double): Vector2f { + val s = sin(angle).toFloat() + val c = cos(angle).toFloat() + + return Vector2f(x * c - s * y, s * x + c * y) + } + + /** + * Returns the angle in radians this unit vector points to, + * treating zero angle as [POSITIVE_X]. + * + * If this vector is not normalized (not a unit vector), + * behavior of this method is undefined. + */ + fun toAngle(): Float { + val dot = dot(POSITIVE_X) + + if (y > 0.0) { + return acos(dot) + } else { + return -acos(dot) + } + } + /** * Calculates vector vector * vector, returning result as [Double] */ diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 6b18cc94..f6dd9e46 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -4,18 +4,17 @@ import org.apache.logging.log4j.LogManager import org.lwjgl.Version import org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose import ru.dbotthepony.kbox2d.api.* -import ru.dbotthepony.kbox2d.collision.DistanceProxy -import ru.dbotthepony.kbox2d.collision.b2TimeOfImpact import ru.dbotthepony.kbox2d.collision.shapes.CircleShape import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.defs.TileDefinition +import ru.dbotthepony.kstarbound.defs.projectile.ProjectilePhysics 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 ru.dbotthepony.kstarbound.world.entities.projectile.Projectile import ru.dbotthepony.kvector.vector.ndouble.Vector2d import java.io.File import java.util.* @@ -125,10 +124,17 @@ fun main() { // ent.movement.dropToFloor() - for ((i, proj) in Starbound.projectilesAccess.values.withIndex()) { - val projEnt = Projectile(client.world!!, proj) - projEnt.position = Vector2d(i * 2.0, 10.0) - projEnt.spawn() + run { + var i = 0 + + for (proj in Starbound.projectilesAccess.values) { + if (proj.physics == ProjectilePhysics.BOUNCY) { + val projEnt = Projectile(client.world!!, proj) + projEnt.position = Vector2d(i * 2.0, 18.0) + projEnt.spawn() + i++ + } + } } run { @@ -161,7 +167,7 @@ fun main() { } } - ent.position += Vector2d(y = 36.0, x = -10.0) + ent.position += Vector2d(y = 14.0, x = -10.0) client.onDrawGUI { client.gl.font.render("${ent.position}", y = 100f, scale = 0.25f) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index b4f178cd..1816708c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -6,12 +6,9 @@ import ru.dbotthepony.kstarbound.api.IVFS import ru.dbotthepony.kstarbound.api.PhysicalFS 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.defs.projectile.* import ru.dbotthepony.kstarbound.io.* import ru.dbotthepony.kstarbound.math.* -import ru.dbotthepony.kstarbound.util.CustomEnumTypeAdapter import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABBi import ru.dbotthepony.kvector.vector.Color @@ -48,14 +45,19 @@ object Starbound : IVFS { .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY) .setPrettyPrinting() .registerTypeAdapter(Color::class.java, ColorTypeAdapter.nullSafe()) - .registerTypeAdapter(ProjectilePhysics::class.java, CustomEnumTypeAdapter(ProjectilePhysics.values()).nullSafe()) + + // math .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) + + .also(ConfigurableProjectile::regisyterAdapters) + + .registerTypeAdapter(DamageType::class.java, CustomEnumTypeAdapter(DamageType.values()).nullSafe()) + .create() var initializing = false diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt index 5268a223..3e4fb485 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientChunk.kt @@ -266,9 +266,6 @@ class ClientChunk(world: ClientWorld, pos: ChunkPos) : Chunk val relative = renderer.renderPos - posVector2d diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt index 1cedfdfd..de813094 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientWorld.kt @@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.client import ru.dbotthepony.kstarbound.client.render.renderLayeredList import ru.dbotthepony.kstarbound.math.encasingChunkPosAABB import ru.dbotthepony.kstarbound.world.* +import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kvector.util2d.AABB class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World(seed) { @@ -45,8 +46,15 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World(entities.size) + var i = 0 + for (ent in entities) { - ent.think(delta) + copy[i++] = ent + } + + for (ent in copy) { + ent!!.think(delta) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt index c6b01e6b..04ade50b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/VertexBuilder.kt @@ -93,6 +93,30 @@ interface IVertexBuilder, VertexType : I return this as This } + + fun quadRotatedZ( + x0: Float, + y0: Float, + x1: Float, + y1: Float, + z: Float, + x: Float, + y: Float, + angle: Double, + lambda: VertexTransformer = emptyTransform + ): This { + check(type.elements == 4) { "Currently building $type" } + + val s = sin(angle).toFloat() + val c = cos(angle).toFloat() + + lambda(vertex().pushVec3f(x + x0 * c - s * y0, y + s * x0 + c * y0, z), 0).end() + lambda(vertex().pushVec3f(x + x1 * c - s * y0, y + s * x1 + c * y0, z), 1).end() + lambda(vertex().pushVec3f(x + x0 * c - s * y1, y + s * x0 + c * y1, z), 2).end() + lambda(vertex().pushVec3f(x + x1 * c - s * y1, y + s * x1 + c * y1, z), 3).end() + + return this as This + } } interface IVertex, VertexBuilderType> { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt index bfb28741..b2f620af 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/EntityRenderer.kt @@ -1,15 +1,23 @@ package ru.dbotthepony.kstarbound.client.render import org.lwjgl.opengl.GL46.* +import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNIT +import ru.dbotthepony.kstarbound.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.client.ClientChunk import ru.dbotthepony.kstarbound.client.gl.GLStateTracker import ru.dbotthepony.kstarbound.client.gl.VertexTransformers import ru.dbotthepony.kstarbound.world.entities.Entity -import ru.dbotthepony.kstarbound.world.entities.Projectile +import ru.dbotthepony.kstarbound.world.entities.projectile.Projectile import ru.dbotthepony.kvector.matrix.Matrix4fStack import ru.dbotthepony.kvector.vector.ndouble.Vector2d import java.io.Closeable +/** + * Pseudo Z position for entities, for them to appear behind tile geometry, + * but in front of background walls geometry + */ +const val Z_LEVEL_ENTITIES = 30000 + /** * Базовый класс, отвечающий за отрисовку определённого ентити в мире * @@ -28,7 +36,7 @@ open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open va } } - open val layer: Int = 100 + open val layer: Int = Z_LEVEL_ENTITIES override fun close() { @@ -47,7 +55,7 @@ open class EntityRenderer(val state: GLStateTracker, val entity: Entity, open va 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) + private val animator = FrameSetAnimator(def.image, def.animationCycle, entity.def.animationLoops) init { texture.textureMagFilter = GL_NEAREST @@ -70,7 +78,10 @@ open class ProjectileRenderer(state: GLStateTracker, entity: Projectile, chunk: val (u0, v0) = texture.pixelToUV(animator.frameObj.texturePosition) val (u1, v1) = texture.pixelToUV(animator.frameObj.textureEndPosition) - builder.quadZ(0f, 0f, 1f, animator.frameObj.aspectRatioHW, 5f, VertexTransformers.uv(u0, v0, u1, v1)) + val width = (animator.frameObj.width / PIXELS_IN_STARBOUND_UNITf) / 2f + val height = (animator.frameObj.height / PIXELS_IN_STARBOUND_UNITf) / 2f + + builder.quadRotatedZ(-width, -height, width, height, 5f, 0f, 0f, entity.movement.angle, VertexTransformers.uv(u0, v0, u1, v1)) stateful.upload() stateful.draw() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ConfigurableDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ConfigurableDefinition.kt index 7d37ee6c..c3ea0ea5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ConfigurableDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ConfigurableDefinition.kt @@ -3,20 +3,7 @@ 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) { @@ -162,108 +149,3 @@ abstract class ConfiguredDefinition>(val factory: () -> T, vararg fields: KMutableProperty1) : TypeAdapter() { - private val mappedFields = Object2ObjectArrayMap>() - // потому что returnType медленный - private val mappedFieldsReturnTypes = Object2ObjectArrayMap() - private val loggedMisses = ObjectArraySet() - - init { - for (field in fields) { - // потому что в котлине нет понятия KProperty который не имеет getter'а, только setter - require(mappedFields.put(field.name, field as KMutableProperty1) == null) { "${field.name} is defined twice" } - mappedFieldsReturnTypes[field.name] = field.returnType - } - } - - val fields: Array> 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) - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/DamageType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/DamageType.kt new file mode 100644 index 00000000..4cc03398 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/DamageType.kt @@ -0,0 +1,23 @@ +package ru.dbotthepony.kstarbound.defs + +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kstarbound.io.IStringSerializable + +enum class DamageType(private vararg val aliases: String) : IStringSerializable { + NORMAL, + IGNORE_DEFENCE("IGNORESDEF", "IGNOREDEF"), + STATUS, + NO_DAMAGE("NODAMAGE"); + + 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) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt index e8207a0b..887fb058 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt @@ -1,19 +1,27 @@ 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 com.google.common.collect.ImmutableList +import com.google.gson.GsonBuilder +import com.google.gson.JsonObject +import it.unimi.dsi.fastutil.objects.ObjectArraySet +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.* +import ru.dbotthepony.kstarbound.io.ConfigurableTypeAdapter +import ru.dbotthepony.kstarbound.io.KTypeAdapter +import ru.dbotthepony.kstarbound.io.CustomEnumTypeAdapter import ru.dbotthepony.kvector.vector.Color +import kotlin.properties.Delegates class ConfigurableProjectile : ConfigurableDefinition() { - var projectileName: String? = null + var projectileName by Delegates.notNull() var physics: ProjectilePhysics = ProjectilePhysics.DEFAULT var damageKindImage: String? = null - var damageType: String? = null + var damageType = DamageType.NORMAL var damageKind: String? = null var pointLight: Boolean = false + var animationLoops: Boolean = true var lightColor: Color? = null var onlyHitTerrain: Boolean = false @@ -30,10 +38,37 @@ class ConfigurableProjectile : ConfigurableDefinition? = null + + var piercing = false + + var speed = 0.0 + var power = 0.0 + override fun configure(directory: String): ConfiguredProjectile { + val actions = ArrayList() + + if (actionOnReap != null) { + for (action in actionOnReap!!) { + val configurable = constructAction(action) + + if (configurable != null) { + actions.add(configurable.configure(directory)) + } + } + } + + if (timeToLive.isInfinite() && animationCycle.isFinite() && !animationLoops) { + timeToLive = animationCycle * (frameNumber - 1) + LOGGER.warn("{} has no time to live defined, assuming it live as long as its animation plays: {}", projectileName, timeToLive) + } + + check(timeToLive >= 0.0) { "Invalid time to live $timeToLive" } + return ConfiguredProjectile( json = enroll(), - projectileName = checkNotNull(projectileName) { "projectileName is null" }, + projectileName = projectileName, physics = physics, damageKindImage = damageKindImage, damageType = damageType, @@ -48,6 +83,12 @@ class ConfigurableProjectile : ConfigurableDefinition() +private val LOGGER = LogManager.getLogger() + +private fun constructAction(input: JsonObject): IConfigurableAction? { + return when (val elem = (input["action"] ?: throw IllegalArgumentException("Action has no, well, `action` key to specify whatever is it.")).asString) { + "config" -> Starbound.gson.fromJson(input, ActionConfig::class.java) + "projectile" -> Starbound.gson.fromJson(input, ActionProjectile::class.java) + "sound" -> Starbound.gson.fromJson(input, ActionSound::class.java) + "loop" -> Starbound.gson.fromJson(input, ActionLoop::class.java) + "actions" -> Starbound.gson.fromJson(input, ActionActions::class.java) + else -> { + if (!MISSING_ACTIONS.contains(elem)) { + MISSING_ACTIONS.add(elem) + LOGGER.error("No projectile action on reap handler is registered for '{}'!", elem) + } + + return null + } + } +} + +class ActionConfig : IConfigurableAction { + lateinit var file: String + + override fun configure(directory: String): IActionOnReap { + return cache.computeIfAbsent(ensureAbsolutePath(file, directory)) { + if (!Starbound.pathExists(it)) { + LOGGER.error("Config $it does not exist") + return@computeIfAbsent CActionConfig(file, null) + } + + return@computeIfAbsent CActionConfig(file, constructAction(Starbound.loadJson(it) as JsonObject)?.configure()) + } + } + + companion object { + val ADAPTER = KTypeAdapter(::ActionConfig, ActionConfig::file).ignoreProperty("action") + + private val cache = HashMap() + } +} + +class ActionProjectile : IConfigurableAction { + lateinit var type: String + var angle = 0.0 + var inheritDamageFactor = 1.0 + + override fun configure(directory: String): IActionOnReap { + return CActionProjectile(type, angle, inheritDamageFactor) + } + + companion object { + val ADAPTER = KTypeAdapter(::ActionProjectile, + ActionProjectile::type, + ActionProjectile::angle, + ActionProjectile::inheritDamageFactor, + ).ignoreProperty("action").missingPropertiesAreFatal(false) + } +} + +class ActionSound : IConfigurableAction { + lateinit var options: Array + + override fun configure(directory: String): IActionOnReap { + return CActionSound(ImmutableList.copyOf(options)) + } + + companion object { + val ADAPTER = KTypeAdapter(::ActionSound, + ActionSound::options, + ).ignoreProperty("action") + } +} + +class ActionLoop : IConfigurableAction { + var count by Delegates.notNull() + var body by Delegates.notNull>() + + override fun configure(directory: String): IActionOnReap { + return CActionLoop(count, ImmutableList.copyOf(body.mapNotNull { constructAction(it)?.configure() })) + } + + companion object { + val ADAPTER = KTypeAdapter(::ActionLoop, + ActionLoop::count, + ActionLoop::body, + ).ignoreProperty("action") + } +} + +class ActionActions : IConfigurableAction { + var list by Delegates.notNull>() + + override fun configure(directory: String): IActionOnReap { + return CActionActions(ImmutableList.copyOf(list.mapNotNull { constructAction(it)?.configure() })) + } + + companion object { + val ADAPTER = KTypeAdapter(::ActionActions, + ActionActions::list, + ).ignoreProperty("action") } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configured.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configured.kt index c57f58fb..88dffc14 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configured.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configured.kt @@ -1,8 +1,13 @@ package ru.dbotthepony.kstarbound.defs.projectile import com.google.common.collect.ImmutableMap +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.ConfiguredDefinition +import ru.dbotthepony.kstarbound.defs.DamageType import ru.dbotthepony.kstarbound.defs.FrameSet +import ru.dbotthepony.kstarbound.world.entities.projectile.AbstractProjectileMovementController +import ru.dbotthepony.kstarbound.world.entities.projectile.Projectile import ru.dbotthepony.kvector.vector.Color class ConfiguredProjectile( @@ -10,7 +15,7 @@ class ConfiguredProjectile( val projectileName: String, val physics: ProjectilePhysics, val damageKindImage: String?, - val damageType: String?, + val damageType: DamageType, val damageKind: String?, val pointLight: Boolean, val lightColor: Color?, @@ -22,6 +27,12 @@ class ConfiguredProjectile( val bounces: Int, val frameNumber: Int, val scripts: Array, + val actionOnReap: List, + val animationLoops: Boolean, + val hydrophobic: Boolean, + val piercing: Boolean, + val speed: Double, + val power: Double, ) : ConfiguredDefinition(json) { override fun reconfigure(): ConfigurableProjectile { TODO("Not yet implemented") @@ -31,3 +42,88 @@ class ConfiguredProjectile( return "ConfiguredProjectile($projectileName)" } } + +interface IActionOnReap { + val name: String + fun execute(projectile: Projectile) +} + +data class CActionConfig( + val file: String, + val delegate: IActionOnReap?, +) : IActionOnReap { + override val name: String = "config" + + override fun execute(projectile: Projectile) { + delegate?.execute(projectile) + } +} + +data class CActionProjectile( + val type: String, + val angle: Double, + val inheritDamageFactor: Double, +) : IActionOnReap { + override val name: String = "projectile" + + override fun execute(projectile: Projectile) { + val def = Starbound.projectilesAccess[type] + + if (def == null) { + LOGGER.error("Tried to create unknown projectile '{}' as result of reap of '{}'!", type, projectile.def.projectileName) + return + } + + val ent = Projectile(projectile.world, def) + ent.position = projectile.position + // ent.angle = projectile.angle + ent.angle = Math.toRadians(angle) + + if (ent.movement is AbstractProjectileMovementController) { + ent.movement.push() + } + + ent.spawn() + } + + companion object { + private val LOGGER = LogManager.getLogger(CActionProjectile::class.java) + } +} + +data class CActionSound( + val options: List +) : IActionOnReap { + override val name: String = "sound" + + override fun execute(projectile: Projectile) { + println("Play sound ${options.random()}!") + } +} + +data class CActionLoop( + val count: Int, + val body: List +) : IActionOnReap { + override val name: String = "loop" + + override fun execute(projectile: Projectile) { + for (i in 0 until count) { + for (action in body) { + action.execute(projectile) + } + } + } +} + +data class CActionActions( + val list: List +) : IActionOnReap { + override val name: String = "actions" + + override fun execute(projectile: Projectile) { + for (action in list) { + action.execute(projectile) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/ProjectilePhysics.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/ProjectilePhysics.kt index ecd55cfe..061556ed 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/ProjectilePhysics.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/ProjectilePhysics.kt @@ -1,7 +1,7 @@ package ru.dbotthepony.kstarbound.defs.projectile import com.google.gson.stream.JsonWriter -import ru.dbotthepony.kstarbound.util.IStringSerializable +import ru.dbotthepony.kstarbound.io.IStringSerializable enum class ProjectilePhysics(private vararg val aliases: String) : IStringSerializable { GAS, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/ConfigurableTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/ConfigurableTypeAdapter.kt new file mode 100644 index 00000000..c5573ba8 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/ConfigurableTypeAdapter.kt @@ -0,0 +1,129 @@ +package ru.dbotthepony.kstarbound.io + +import com.google.gson.JsonSyntaxException +import com.google.gson.TypeAdapter +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.Object2ObjectArrayMap +import it.unimi.dsi.fastutil.objects.ObjectArraySet +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.ConfigurableDefinition +import ru.dbotthepony.kstarbound.defs.flattenJsonElement +import kotlin.reflect.KClass +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.KType +import kotlin.reflect.full.isSuperclassOf + +/** + * Kotlin property aware adapter with arbitrary structure writer + */ +class ConfigurableTypeAdapter>(val factory: () -> T, vararg fields: KMutableProperty1) : TypeAdapter() { + private val mappedFields = Object2ObjectArrayMap>() + // потому что returnType медленный + private val mappedFieldsReturnTypes = Object2ObjectArrayMap() + private val loggedMisses = ObjectArraySet() + + init { + for (field in fields) { + // потому что в котлине нет понятия KProperty который не имеет getter'а, только setter + require(mappedFields.put(field.name, field as KMutableProperty1) == null) { "${field.name} is defined twice" } + mappedFieldsReturnTypes[field.name] = field.returnType + } + } + + val fields: Array> 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] = flattenJsonElement(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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/CustomEnumTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/CustomEnumTypeAdapter.kt similarity index 95% rename from src/main/kotlin/ru/dbotthepony/kstarbound/util/CustomEnumTypeAdapter.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/io/CustomEnumTypeAdapter.kt index ade9adae..db68b710 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/CustomEnumTypeAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/CustomEnumTypeAdapter.kt @@ -1,4 +1,4 @@ -package ru.dbotthepony.kstarbound.util +package ru.dbotthepony.kstarbound.io import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/KTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/KTypeAdapter.kt new file mode 100644 index 00000000..67c7e7ac --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/KTypeAdapter.kt @@ -0,0 +1,143 @@ +package ru.dbotthepony.kstarbound.io + +import com.google.gson.JsonSyntaxException +import com.google.gson.TypeAdapter +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.Object2ObjectArrayMap +import it.unimi.dsi.fastutil.objects.ObjectArraySet +import org.apache.logging.log4j.Level +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 + +/** + * Kotlin property aware adapter + */ +class KTypeAdapter(val factory: () -> T, vararg fields: KMutableProperty1) : TypeAdapter() { + private val mappedFields = Object2ObjectArrayMap>() + // потому что returnType медленный + private val mappedFieldsReturnTypes = Object2ObjectArrayMap() + private val loggedMisses = ObjectArraySet() + + private val ignoreProperties = ObjectArraySet() + + init { + for (field in fields) { + // потому что в котлине нет понятия KProperty который не имеет getter'а, только setter + require(mappedFields.put(field.name, field as KMutableProperty1) == null) { "${field.name} is defined twice" } + mappedFieldsReturnTypes[field.name] = field.returnType + } + } + + val fields: Array> get() { + val iterator = mappedFields.values.iterator() + return Array(mappedFields.size) { iterator.next() } + } + + fun ignoreProperty(vararg value: String): KTypeAdapter { + ignoreProperties.addAll(value) + return this + } + + var missingPropertiesAreFatal = true + var missingLogLevel = Level.ERROR + + fun missingPropertiesAreFatal(flag: Boolean): KTypeAdapter { + missingPropertiesAreFatal = flag + return this + } + + fun missingLogLevel(level: Level): KTypeAdapter { + missingLogLevel = level + return this + } + + 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() + field.set(instance, read.toFloat()) + } else if (classifier.isSuperclassOf(Double::class)) { + val read = reader.nextDouble() + field.set(instance, read) + } else if (classifier.isSuperclassOf(Int::class)) { + val read = reader.nextInt() + field.set(instance, read) + } else if (classifier.isSuperclassOf(Long::class)) { + val read = reader.nextLong() + field.set(instance, read) + } else if (classifier.isSuperclassOf(String::class)) { + val read = reader.nextString() + field.set(instance, read) + } else if (classifier.isSuperclassOf(Boolean::class)) { + val read = reader.nextBoolean() + field.set(instance, read) + } else { + val readElement = TypeAdapters.JSON_ELEMENT.read(reader) + 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 if (!ignoreProperties.contains(name) && missingPropertiesAreFatal) { + throw JsonSyntaxException("Property $name is not present in ${instance::class.qualifiedName}") + } else { + if (!ignoreProperties.contains(name) && !loggedMisses.contains(name)) { + LOGGER.log(missingLogLevel, "{} has no property for storing {}", instance::class.qualifiedName, name) + loggedMisses.add(name) + } + + reader.skipValue() + } + } + + reader.endObject() + return instance + } + + companion object { + private val LOGGER = LogManager.getLogger(ConfigurableTypeAdapter::class.java) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index 218b13ee..e985f3ec 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -774,6 +774,7 @@ abstract class Chunk, This : Chunk() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 88bbe6b8..eef0eed3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -1,11 +1,19 @@ package ru.dbotthepony.kstarbound.world +import ru.dbotthepony.kbox2d.api.ContactImpulse +import ru.dbotthepony.kbox2d.api.IContactFilter +import ru.dbotthepony.kbox2d.api.IContactListener +import ru.dbotthepony.kbox2d.api.Manifold +import ru.dbotthepony.kbox2d.dynamics.B2Fixture import ru.dbotthepony.kbox2d.dynamics.B2World +import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact import ru.dbotthepony.kstarbound.METRES_IN_STARBOUND_UNIT import ru.dbotthepony.kstarbound.defs.TileDefinition import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.world.entities.CollisionResolution import ru.dbotthepony.kstarbound.world.entities.Entity +import ru.dbotthepony.kstarbound.world.entities.MovementController +import ru.dbotthepony.kstarbound.world.entities.projectile.AbstractProjectileMovementController import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.util2d.AABBi import ru.dbotthepony.kvector.vector.ndouble.Vector2d @@ -109,6 +117,75 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk 0.0) { "Tried to update $this by $delta seconds" } - for (chunk in dirtyPhysicsChunks) { - chunk.bakeCollisions() + try { + for (chunk in dirtyPhysicsChunks) { + chunk.bakeCollisions() + } + + dirtyPhysicsChunks.clear() + + physics.step(delta, 6, 4) + + timer += delta + thinkInner(delta) + } catch(err: Throwable) { + throw RuntimeException("Ticking world $this", err) } - - dirtyPhysicsChunks.clear() - - physics.step(delta, 6, 4) - - timer += delta - thinkInner(delta) } protected abstract fun thinkInner(delta: Double) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt index f9e6f5f9..dd1e2a2b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AliveEntity.kt @@ -2,14 +2,11 @@ package ru.dbotthepony.kstarbound.world.entities import ru.dbotthepony.kbox2d.api.ContactEdge import ru.dbotthepony.kbox2d.api.FixtureDef -import ru.dbotthepony.kbox2d.api.b2_linearSlop import ru.dbotthepony.kbox2d.api.b2_polygonRadius import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape import ru.dbotthepony.kbox2d.dynamics.B2Fixture -import ru.dbotthepony.kstarbound.client.ClientWorld import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kvector.util2d.AABB -import ru.dbotthepony.kvector.vector.Color import ru.dbotthepony.kvector.vector.ndouble.Vector2d import ru.dbotthepony.kvector.vector.ndouble.times import kotlin.math.absoluteValue @@ -108,8 +105,8 @@ abstract class WalkableMovementController(entity: T) : Move open var isDucked = false protected set - override fun thinkPhysics(delta: Double) { - super.thinkPhysics(delta) + override fun think(delta: Double) { + super.think(delta) thinkMovement(delta) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt index 052f5fbd..b49ace9b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.world.entities +import ru.dbotthepony.kstarbound.defs.DamageType import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.World @@ -66,6 +67,14 @@ interface IEntity { fun remove() fun think(delta: Double) fun onTouchSurface(velocity: Vector2d, normal: Vector2d) + + fun dealDamage( + amount: Double, + kind: String, + type: DamageType, + ) { + // Do nothing by default + } } /** @@ -127,6 +136,13 @@ abstract class Entity(override val world: World<*, *>) : IEntity { } override var angle: Double = 0.0 + set(value) { + if (field == value) + return + + field = value + movement.notifyPositionChanged() + } final override var isSpawned = false private set @@ -156,6 +172,8 @@ abstract class Entity(override val world: World<*, *>) : IEntity { world.entities.remove(this) chunk?.removeEntity(this) } + + movement.destroy() } /** @@ -172,7 +190,7 @@ abstract class Entity(override val world: World<*, *>) : IEntity { throw IllegalStateException("Tried to think before spawning in world") } - movement.thinkPhysics(delta) + movement.think(delta) thinkAI(delta) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt index b778af59..8cc43ffe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt @@ -1,7 +1,7 @@ package ru.dbotthepony.kstarbound.world.entities -import ru.dbotthepony.kbox2d.api.BodyDef -import ru.dbotthepony.kbox2d.api.BodyType +import ru.dbotthepony.kbox2d.api.* +import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.ndouble.Vector2d @@ -12,16 +12,21 @@ enum class CollisionResolution { SLIDE, } -abstract class MovementController(val entity: T) { +abstract class MovementController(val entity: T) : IContactListener { val world = entity.world open var position by entity::position open var angle by entity::angle + open fun destroy() { + body.world.destroyBody(body) + } + protected val body by lazy { world.physics.createBody(BodyDef( position = position, angle = angle, - type = BodyType.DYNAMIC + type = BodyType.DYNAMIC, + userData = this )) } @@ -47,7 +52,11 @@ abstract class MovementController(val entity: T) { return body.worldSpaceAABB } - open fun thinkPhysics(delta: Double) { + /** + * This is called on each world step to update variables and account of changes of + * physics world and this physics body. + */ + open fun think(delta: Double) { mutePositionChanged = true position = body.position angle = body.angle @@ -79,7 +88,7 @@ class LogicalMovementController(entity: Entity) : MovementController(ent override val onGround: Boolean = false override val velocity: Vector2d = Vector2d.ZERO - override fun thinkPhysics(delta: Double) { + override fun think(delta: Double) { // no-op } @@ -87,6 +96,22 @@ class LogicalMovementController(entity: Entity) : MovementController(ent // no-op } + override fun beginContact(contact: AbstractContact) { + // no-op + } + + override fun endContact(contact: AbstractContact) { + // no-op + } + + override fun preSolve(contact: AbstractContact, oldManifold: Manifold) { + // no-op + } + + override fun postSolve(contact: AbstractContact, impulse: ContactImpulse) { + // no-op + } + companion object { private val DUMMY_AABB = AABB.rectangle(Vector2d.ZERO, 0.1, 0.1) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt index 8419ef6a..44b1e62f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt @@ -1,8 +1,11 @@ package ru.dbotthepony.kstarbound.world.entities +import ru.dbotthepony.kbox2d.api.ContactImpulse import ru.dbotthepony.kbox2d.api.FixtureDef +import ru.dbotthepony.kbox2d.api.Manifold import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape import ru.dbotthepony.kbox2d.dynamics.B2Fixture +import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.ndouble.Vector2d @@ -33,6 +36,22 @@ class PlayerMovementController(entity: PlayerEntity) : WalkableMovementControlle recreateSensors() } + override fun beginContact(contact: AbstractContact) { + + } + + override fun endContact(contact: AbstractContact) { + + } + + override fun preSolve(contact: AbstractContact, oldManifold: Manifold) { + + } + + override fun postSolve(contact: AbstractContact, impulse: ContactImpulse) { + + } + override fun canUnDuck(): Boolean { return world.isSpaceEmptyFromTiles(STANDING_AABB + position) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Projectile.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Projectile.kt deleted file mode 100644 index c58cb24a..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Projectile.kt +++ /dev/null @@ -1,12 +0,0 @@ -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<*> = LogicalMovementController(this) - - override fun thinkAI(delta: Double) { - - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/projectile/Physics.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/projectile/Physics.kt new file mode 100644 index 00000000..1f2b6f0c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/projectile/Physics.kt @@ -0,0 +1,123 @@ +package ru.dbotthepony.kstarbound.world.entities.projectile + +import ru.dbotthepony.kbox2d.api.ContactImpulse +import ru.dbotthepony.kbox2d.api.FixtureDef +import ru.dbotthepony.kbox2d.api.Manifold +import ru.dbotthepony.kbox2d.collision.shapes.PolygonShape +import ru.dbotthepony.kbox2d.dynamics.contact.AbstractContact +import ru.dbotthepony.kstarbound.defs.projectile.ConfiguredProjectile +import ru.dbotthepony.kstarbound.defs.projectile.ProjectilePhysics +import ru.dbotthepony.kstarbound.world.Chunk +import ru.dbotthepony.kstarbound.world.entities.AliveEntity +import ru.dbotthepony.kstarbound.world.entities.LogicalMovementController +import ru.dbotthepony.kstarbound.world.entities.MovementController +import ru.dbotthepony.kvector.vector.ndouble.Vector2d +import kotlin.math.PI + +abstract class AbstractProjectileMovementController(entity: Projectile, val def: ConfiguredProjectile) : MovementController(entity) { + var bounces = 0 + protected set + + override fun beginContact(contact: AbstractContact) { + val dataA = contact.fixtureA.body!!.userData + val dataB = contact.fixtureB.body!!.userData + + if (dataA is Chunk<*, *>.TileLayer || dataB is Chunk<*, *>.TileLayer) { + bounces++ + + if (def.bounces > 0 && bounces >= def.bounces) { + // We can't detonate inside physics simulation + entity.markForDetonation() + } + } else if (dataA is MovementController<*>) { + entity.collideWithEntity(dataA.entity) + } else if (dataB is MovementController<*>) { + entity.collideWithEntity(dataB.entity) + } + } + + override fun endContact(contact: AbstractContact) { + + } + + override fun preSolve(contact: AbstractContact, oldManifold: Manifold) { + + } + + override fun postSolve(contact: AbstractContact, impulse: ContactImpulse) { + + } + + /** + * Applies linear velocity along current facing angle scaled with [ConfiguredProjectile.speed] + */ + open fun push() { + body.linearVelocity += Vector2d.POSITIVE_Y.rotate(body.angle) * def.speed + } + + protected open fun updateAngle() { + body.setTransform(position, body.linearVelocity.normalized.toAngle()) + } + + override fun think(delta: Double) { + super.think(delta) + updateAngle() + } + + companion object { + fun factorize(entity: Projectile, def: ConfiguredProjectile): MovementController<*>? { + return when (def.physics) { + ProjectilePhysics.DEFAULT -> LogicalMovementController(entity) + ProjectilePhysics.BOUNCY -> BouncyPhysics(entity, def) + ProjectilePhysics.FLAME -> FlamePhysics(entity, def) + else -> null + } + } + } +} + +class BouncyPhysics(entity: Projectile, def: ConfiguredProjectile) : AbstractProjectileMovementController(entity, def) { + init { + body.createFixture(FixtureDef( + shape = PolygonShape().also { it.setAsBox(0.5, 0.2) }, + restitution = 0.9, + friction = 0.7, + density = 2.0, + )) + } +} + +class FlamePhysics(entity: Projectile, def: ConfiguredProjectile) : AbstractProjectileMovementController(entity, def) { + init { + body.createFixture(FixtureDef( + shape = PolygonShape().also { it.setAsBox(0.2, 0.2) }, + restitution = 0.0, + friction = 1.0, + density = 0.3, + )) + } + + private var touchedGround = false + private var fixedRotation = false + + override fun updateAngle() { + if (!fixedRotation && !touchedGround) + super.updateAngle() + } + + override fun think(delta: Double) { + super.think(delta) + + if (touchedGround && !fixedRotation) { + fixedRotation = true + body.setTransform(body.position, -PI / 2.0) + body.isFixedRotation = true + } + } + + override fun beginContact(contact: AbstractContact) { + super.beginContact(contact) + touchedGround = true + } +} + diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/projectile/Projectile.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/projectile/Projectile.kt new file mode 100644 index 00000000..d73f7baa --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/projectile/Projectile.kt @@ -0,0 +1,55 @@ +package ru.dbotthepony.kstarbound.world.entities.projectile + +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kstarbound.defs.projectile.ConfiguredProjectile +import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kstarbound.world.entities.Entity +import ru.dbotthepony.kstarbound.world.entities.IEntity +import ru.dbotthepony.kstarbound.world.entities.LogicalMovementController +import ru.dbotthepony.kstarbound.world.entities.MovementController + +class Projectile(world: World<*, *>, val def: ConfiguredProjectile) : Entity(world) { + override val movement: MovementController = ( + AbstractProjectileMovementController.factorize(this, def) ?: + LogicalMovementController(this).also { LOGGER.error("No physics controller for ${def.physics}, defaulting to dummy movement controller!") }) as MovementController + + private var timeToLive = def.timeToLive + private var markForDeath = false + + override fun thinkAI(delta: Double) { + timeToLive -= delta + + if (timeToLive <= 0.0 || markForDeath) { + detonate() + } + } + + fun markForDetonation() { + markForDeath = true + } + + fun collideWithEntity(other: IEntity) { + // Can't do anything if we are technically dead + if (markForDeath) + return + + if (!def.piercing) { + markForDeath = true + } + + if (def.damageKind != null) + other.dealDamage(def.power, def.damageKind, def.damageType) + } + + fun detonate() { + for (action in def.actionOnReap) { + action.execute(this) + } + + remove() + } + + companion object { + private val LOGGER = LogManager.getLogger(Projectile::class.java) + } +}