Projectile physics test
This commit is contained in:
parent
135671cc20
commit
a3f4cf8338
@ -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")
|
||||
|
@ -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].
|
||||
*/
|
||||
|
@ -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]
|
||||
*/
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -266,9 +266,6 @@ 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
|
||||
|
@ -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<ClientWorld, ClientChunk>(seed) {
|
||||
@ -45,8 +46,15 @@ class ClientWorld(val client: StarboundClient, seed: Long = 0L) : World<ClientWo
|
||||
}
|
||||
|
||||
override fun thinkInner(delta: Double) {
|
||||
val copy = arrayOfNulls<Entity>(entities.size)
|
||||
var i = 0
|
||||
|
||||
for (ent in entities) {
|
||||
ent.think(delta)
|
||||
copy[i++] = ent
|
||||
}
|
||||
|
||||
for (ent in copy) {
|
||||
ent!!.think(delta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +93,30 @@ interface IVertexBuilder<This : IVertexBuilder<This, VertexType>, 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<This : IVertex<This, VertexBuilderType>, VertexBuilderType> {
|
||||
|
@ -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()
|
||||
|
@ -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<Configured : ConfiguredDefinition<Configured
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
23
src/main/kotlin/ru/dbotthepony/kstarbound/defs/DamageType.kt
Normal file
23
src/main/kotlin/ru/dbotthepony/kstarbound/defs/DamageType.kt
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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<ConfigurableProjectile, ConfiguredProjectile>() {
|
||||
var projectileName: String? = null
|
||||
var projectileName by Delegates.notNull<String>()
|
||||
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<ConfigurableProjectile, Co
|
||||
|
||||
var hydrophobic: Boolean = false
|
||||
|
||||
// we can't have concrete type here, since final class is commanded by `action` property of each entry
|
||||
var actionOnReap: Array<JsonObject>? = null
|
||||
|
||||
var piercing = false
|
||||
|
||||
var speed = 0.0
|
||||
var power = 0.0
|
||||
|
||||
override fun configure(directory: String): ConfiguredProjectile {
|
||||
val actions = ArrayList<IActionOnReap>()
|
||||
|
||||
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<ConfigurableProjectile, Co
|
||||
bounces = bounces,
|
||||
frameNumber = frameNumber,
|
||||
scripts = scripts,
|
||||
actionOnReap = ImmutableList.copyOf(actions),
|
||||
animationLoops = animationLoops,
|
||||
hydrophobic = hydrophobic,
|
||||
piercing = piercing,
|
||||
speed = speed,
|
||||
power = power,
|
||||
)
|
||||
}
|
||||
|
||||
@ -69,6 +110,134 @@ class ConfigurableProjectile : ConfigurableDefinition<ConfigurableProjectile, Co
|
||||
ConfigurableProjectile::bounces,
|
||||
ConfigurableProjectile::frameNumber,
|
||||
ConfigurableProjectile::scripts,
|
||||
ConfigurableProjectile::actionOnReap,
|
||||
ConfigurableProjectile::animationLoops,
|
||||
ConfigurableProjectile::hydrophobic,
|
||||
ConfigurableProjectile::piercing,
|
||||
ConfigurableProjectile::speed,
|
||||
ConfigurableProjectile::power,
|
||||
)
|
||||
|
||||
fun regisyterAdapters(gson: GsonBuilder) {
|
||||
gson.registerTypeAdapter(ConfigurableProjectile::class.java, ADAPTER)
|
||||
gson.registerTypeAdapter(ProjectilePhysics::class.java, CustomEnumTypeAdapter(ProjectilePhysics.values()).nullSafe())
|
||||
gson.registerTypeAdapter(ActionConfig::class.java, ActionConfig.ADAPTER)
|
||||
gson.registerTypeAdapter(ActionProjectile::class.java, ActionProjectile.ADAPTER)
|
||||
gson.registerTypeAdapter(ActionSound::class.java, ActionSound.ADAPTER)
|
||||
gson.registerTypeAdapter(ActionLoop::class.java, ActionLoop.ADAPTER)
|
||||
gson.registerTypeAdapter(ActionActions::class.java, ActionActions.ADAPTER)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////
|
||||
// Action on Reap
|
||||
/////////////////////////////////
|
||||
|
||||
interface IConfigurableAction {
|
||||
fun configure(directory: String = ""): IActionOnReap
|
||||
}
|
||||
|
||||
private val MISSING_ACTIONS = ObjectArraySet<String>()
|
||||
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<String, CActionConfig>()
|
||||
}
|
||||
}
|
||||
|
||||
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<String>
|
||||
|
||||
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<Int>()
|
||||
var body by Delegates.notNull<Array<JsonObject>>()
|
||||
|
||||
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<Array<JsonObject>>()
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
@ -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<String>,
|
||||
val actionOnReap: List<IActionOnReap>,
|
||||
val animationLoops: Boolean,
|
||||
val hydrophobic: Boolean,
|
||||
val piercing: Boolean,
|
||||
val speed: Double,
|
||||
val power: Double,
|
||||
) : ConfiguredDefinition<ConfiguredProjectile, ConfigurableProjectile>(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<String>
|
||||
) : 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>
|
||||
) : 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>
|
||||
) : IActionOnReap {
|
||||
override val name: String = "actions"
|
||||
|
||||
override fun execute(projectile: Projectile) {
|
||||
for (action in list) {
|
||||
action.execute(projectile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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<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] = 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)
|
||||
}
|
||||
}
|
@ -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
|
143
src/main/kotlin/ru/dbotthepony/kstarbound/io/KTypeAdapter.kt
Normal file
143
src/main/kotlin/ru/dbotthepony/kstarbound/io/KTypeAdapter.kt
Normal file
@ -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<T>(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>()
|
||||
|
||||
private val ignoreProperties = 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() }
|
||||
}
|
||||
|
||||
fun ignoreProperty(vararg value: String): KTypeAdapter<T> {
|
||||
ignoreProperties.addAll(value)
|
||||
return this
|
||||
}
|
||||
|
||||
var missingPropertiesAreFatal = true
|
||||
var missingLogLevel = Level.ERROR
|
||||
|
||||
fun missingPropertiesAreFatal(flag: Boolean): KTypeAdapter<T> {
|
||||
missingPropertiesAreFatal = flag
|
||||
return this
|
||||
}
|
||||
|
||||
fun missingLogLevel(level: Level): KTypeAdapter<T> {
|
||||
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)
|
||||
}
|
||||
}
|
@ -774,6 +774,7 @@ abstract class Chunk<WorldType : World<WorldType, This>, This : Chunk<WorldType,
|
||||
|
||||
private val body = world.physics.createBody(BodyDef(
|
||||
position = pos.firstBlock.toDoubleVector(),
|
||||
userData = this
|
||||
))
|
||||
|
||||
private val collisionChains = ArrayList<B2Fixture>()
|
||||
|
@ -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<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
|
||||
val physics = B2World(Vector2d(0.0, -EARTH_FREEFALL_ACCELERATION))
|
||||
|
||||
init {
|
||||
physics.contactFilter = object : IContactFilter {
|
||||
override fun shouldCollide(fixtureA: B2Fixture, fixtureB: B2Fixture): Boolean {
|
||||
val dataA = fixtureA.body!!.userData
|
||||
val dataB = fixtureB.body!!.userData
|
||||
|
||||
if (dataA is AbstractProjectileMovementController && dataB is AbstractProjectileMovementController) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
physics.contactListener = object : IContactListener {
|
||||
override fun beginContact(contact: AbstractContact) {
|
||||
val dataA = contact.fixtureA.body!!.userData
|
||||
val dataB = contact.fixtureB.body!!.userData
|
||||
|
||||
if (dataA is IContactListener) {
|
||||
dataA.beginContact(contact)
|
||||
}
|
||||
|
||||
if (dataB is IContactListener) {
|
||||
dataB.beginContact(contact)
|
||||
}
|
||||
}
|
||||
|
||||
override fun endContact(contact: AbstractContact) {
|
||||
val dataA = contact.fixtureA.body!!.userData
|
||||
val dataB = contact.fixtureB.body!!.userData
|
||||
|
||||
if (dataA is IContactListener) {
|
||||
dataA.endContact(contact)
|
||||
}
|
||||
|
||||
if (dataB is IContactListener) {
|
||||
dataB.endContact(contact)
|
||||
}
|
||||
}
|
||||
|
||||
override fun preSolve(contact: AbstractContact, oldManifold: Manifold) {
|
||||
val dataA = contact.fixtureA.body!!.userData
|
||||
val dataB = contact.fixtureB.body!!.userData
|
||||
|
||||
if (dataA is IContactListener) {
|
||||
dataA.preSolve(contact, oldManifold)
|
||||
}
|
||||
|
||||
if (dataB is IContactListener) {
|
||||
dataB.preSolve(contact, oldManifold)
|
||||
}
|
||||
}
|
||||
|
||||
override fun postSolve(contact: AbstractContact, impulse: ContactImpulse) {
|
||||
val dataA = contact.fixtureA.body!!.userData
|
||||
val dataB = contact.fixtureB.body!!.userData
|
||||
|
||||
if (dataA is IContactListener) {
|
||||
dataA.postSolve(contact, impulse)
|
||||
}
|
||||
|
||||
if (dataB is IContactListener) {
|
||||
dataB.postSolve(contact, impulse)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Таймер этого мира, в секундах.
|
||||
*
|
||||
@ -133,16 +210,20 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
|
||||
fun think(delta: Double) {
|
||||
require(delta > 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)
|
||||
|
@ -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<T : IWalkableEntity>(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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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<T : IEntity>(val entity: T) {
|
||||
abstract class MovementController<T : IEntity>(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<T : IEntity>(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<Entity>(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<Entity>(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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
@ -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<Projectile>(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
|
||||
}
|
||||
}
|
||||
|
@ -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<Projectile> = (
|
||||
AbstractProjectileMovementController.factorize(this, def) ?:
|
||||
LogicalMovementController(this).also { LOGGER.error("No physics controller for ${def.physics}, defaulting to dummy movement controller!") }) as MovementController<Projectile>
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user