Projectile physics test

This commit is contained in:
DBotThePony 2022-02-21 17:00:09 +07:00
parent 135671cc20
commit a3f4cf8338
Signed by: DBot
GPG Key ID: DCC23B5715498507
26 changed files with 1047 additions and 185 deletions

View File

@ -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")

View File

@ -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].
*/

View File

@ -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]
*/

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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> {

View File

@ -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()

View File

@ -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)
}
}

View 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)
}
}

View File

@ -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")
}
}

View File

@ -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)
}
}
}

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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

View 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)
}
}

View File

@ -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>()

View File

@ -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)

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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) {
}
}

View File

@ -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
}
}

View File

@ -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)
}
}