diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt index 0d867990..ba5284c4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt @@ -1,12 +1,11 @@ package ru.dbotthepony.kstarbound import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.ClientConfigParameters import ru.dbotthepony.kstarbound.defs.MovementParameters -import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.util.AssetPathStack import java.util.concurrent.ExecutorService -import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinTask import java.util.concurrent.Future import kotlin.reflect.KMutableProperty0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index f34d5f93..128aac68 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -11,7 +11,9 @@ import ru.dbotthepony.kstarbound.player.Avatar import ru.dbotthepony.kstarbound.world.entities.ItemEntity import ru.dbotthepony.kstarbound.json.VersionedJson import ru.dbotthepony.kstarbound.io.readVarInt +import ru.dbotthepony.kstarbound.json.BinaryJsonReader import ru.dbotthepony.kstarbound.util.AssetPathStack +import ru.dbotthepony.kstarbound.world.Direction import ru.dbotthepony.kstarbound.world.api.MutableCell import ru.dbotthepony.kstarbound.world.entities.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.WorldObject @@ -43,10 +45,12 @@ fun main() { val db = BTreeDB(File("F:\\SteamLibrary\\steamapps\\common\\Starbound - Unstable\\storage\\universe\\389760395_938904237_-238610574_5.world")) //val db = BTreeDB(File("world.world")) - //val meta = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(db.read(byteArrayOf(0, 0, 0, 0, 0))), Inflater()))) + val meta = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(db.read(byteArrayOf(0, 0, 0, 0, 0))), Inflater()))) - //println(meta.readInt()) - //println(meta.readInt()) + println(meta.readInt()) + println(meta.readInt()) + + println(VersionedJson(meta)) val client = StarboundClient() @@ -125,14 +129,14 @@ fun main() { val rand = Random() - for (i in 0 until 128) { + for (i in 0 until 0) { val item = ItemEntity(client.world!!, Registries.items.keys.values.random().value) item.position = Vector2d(225.0 - i, 785.0) item.spawn() - item.velocity = Vector2d(rand.nextDouble() * 32.0 - 16.0, rand.nextDouble() * 32.0 - 16.0) + item.movement.velocity = Vector2d(rand.nextDouble() * 32.0 - 16.0, rand.nextDouble() * 32.0 - 16.0) - item.mailbox.scheduleAtFixedRate({ item.velocity += Vector2d(rand.nextDouble() * 32.0 - 16.0, rand.nextDouble() * 32.0 - 16.0) }, 1000 + rand.nextLong(-100, 100), 1000 + rand.nextLong(-100, 100), TimeUnit.MILLISECONDS) + item.mailbox.scheduleAtFixedRate({ item.movement.velocity += Vector2d(rand.nextDouble() * 32.0 - 16.0, rand.nextDouble() * 32.0 - 16.0) }, 1000 + rand.nextLong(-100, 100), 1000 + rand.nextLong(-100, 100), TimeUnit.MILLISECONDS) //item.movement.applyVelocity(Vector2d(rand.nextDouble() * 1000.0 - 500.0, rand.nextDouble() * 1000.0 - 500.0)) } @@ -189,10 +193,9 @@ fun main() { if (ply != null) { client.camera.pos = ply!!.position - ply!!.velocity += Vector2d( - (if (client.input.KEY_A_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_D_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0), - (if (client.input.KEY_W_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) + (if (client.input.KEY_S_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / client.settings.zoom else 0.0) - ) * 8.0 + ply!!.movement.controlMove = if (client.input.KEY_A_DOWN) Direction.LEFT else if (client.input.KEY_D_DOWN) Direction.RIGHT else null + ply!!.movement.controlJump = client.input.KEY_SPACE_DOWN + ply!!.movement.controlRun = !client.input.KEY_LEFT_SHIFT_DOWN } if (client.input.KEY_ESCAPE_PRESSED) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 2c8d83d3..12441ba3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -80,6 +80,7 @@ import kotlin.random.Random object Starbound : ISBFileLocator { const val TICK_TIME_ADVANCE = 0.01666666666666664 const val TICK_TIME_ADVANCE_NANOS = 16_666_666L + const val DEDUP_CELL_STATES = true val thread = Thread(::universeThread, "Starbound Universe") val mailbox = MailboxExecutorService(thread) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index d6fcd528..d44b07bf 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -271,12 +271,12 @@ class StarboundClient : Closeable { val pHeight = stack.mallocInt(1) val pChannels = stack.mallocInt(1) - val readFromDisk = readInternalBytes("starbound.png") + val readFromDisk = readInternalBytes("starbound_icon.png") val buff = ByteBuffer.allocateDirect(readFromDisk.size) buff.put(readFromDisk) buff.position(0) - val data = STBImage.stbi_load_from_memory(buff, pWidth, pHeight, pChannels, 4) ?: throw IllegalStateException("Unable to decode starbound.png") + val data = STBImage.stbi_load_from_memory(buff, pWidth, pHeight, pChannels, 4) ?: throw IllegalStateException("Unable to decode starbound_icon.png") val img = GLFWImage.malloc() img.set(pWidth[0], pHeight[0], data) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ActorMovementModifiers.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ActorMovementModifiers.kt new file mode 100644 index 00000000..6d088a8c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ActorMovementModifiers.kt @@ -0,0 +1,30 @@ +package ru.dbotthepony.kstarbound.defs + +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +@JsonFactory +data class ActorMovementModifiers( + val groundMovementModifier: Double = 0.0, + val liquidMovementModifier: Double = 0.0, + val speedModifier: Double = 0.0, + val airJumpModifier: Double = 0.0, + val liquidJumpModifier: Double = 0.0, + val runningSuppressed: Boolean = false, + val jumpingSuppressed: Boolean = false, + val movementSuppressed: Boolean = false, + val facingSuppressed: Boolean = false, +) { + fun combine(other: ActorMovementModifiers): ActorMovementModifiers { + return ActorMovementModifiers( + groundMovementModifier = groundMovementModifier * other.groundMovementModifier, + liquidMovementModifier = liquidMovementModifier * other.liquidMovementModifier, + speedModifier = speedModifier * other.speedModifier, + airJumpModifier = airJumpModifier * other.airJumpModifier, + liquidJumpModifier = liquidJumpModifier * other.liquidJumpModifier, + runningSuppressed = runningSuppressed || other.runningSuppressed, + jumpingSuppressed = jumpingSuppressed || other.jumpingSuppressed, + movementSuppressed = movementSuppressed || other.movementSuppressed, + facingSuppressed = facingSuppressed || other.facingSuppressed, + ) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ActorMovementParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ActorMovementParameters.kt deleted file mode 100644 index e601725b..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ActorMovementParameters.kt +++ /dev/null @@ -1,66 +0,0 @@ -package ru.dbotthepony.kstarbound.defs - -import ru.dbotthepony.kstarbound.json.builder.JsonAlias -import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.builder.JsonFlat -import ru.dbotthepony.kstarbound.world.physics.Poly - -@JsonFactory -data class ActorMovementParameters( - @JsonFlat - val base: BaseMovementParameters.Impl = BaseMovementParameters.Impl(), - - @JsonAlias("collisionPoly") - val standingPoly: Poly? = null, - @JsonAlias("collisionPoly") - val crouchingPoly: Poly? = null, - - val walkSpeed: Double? = null, - val runSpeed: Double? = null, - val flySpeed: Double? = null, - - val minimumLiquidPercentage: Double? = null, - val liquidImpedance: Double? = null, - val normalGroundFriction: Double? = null, - val ambulatingGroundFriction: Double? = null, - val groundForce: Double? = null, - val airForce: Double? = null, - val liquidForce: Double? = null, - - val airJumpProfile: JumpProfile = JumpProfile(), - val liquidJumpProfile: JumpProfile = JumpProfile(), - - val fallStatusSpeedMin: Double? = null, - val fallThroughSustainFrames: Int? = null, - - val groundMovementMinimumSustain: Double? = null, - val groundMovementMaximumSustain: Double? = null, - val groundMovementCheckDistance: Double? = null, - - val pathExploreRate: Double? = null, -) : BaseMovementParameters by base { - fun merge(other: ActorMovementParameters): ActorMovementParameters { - return ActorMovementParameters( - base = base.merge(other.base), - standingPoly = standingPoly ?: other.standingPoly, - crouchingPoly = crouchingPoly ?: other.crouchingPoly, - walkSpeed = walkSpeed ?: other.walkSpeed, - runSpeed = runSpeed ?: other.runSpeed, - flySpeed = flySpeed ?: other.flySpeed, - minimumLiquidPercentage = minimumLiquidPercentage ?: other.minimumLiquidPercentage, - liquidImpedance = liquidImpedance ?: other.liquidImpedance, - normalGroundFriction = normalGroundFriction ?: other.normalGroundFriction, - ambulatingGroundFriction = ambulatingGroundFriction ?: other.ambulatingGroundFriction, - groundForce = groundForce ?: other.groundForce, - airForce = airForce ?: other.airForce, - liquidForce = liquidForce ?: other.liquidForce, - airJumpProfile = airJumpProfile.merge(other.airJumpProfile), - liquidJumpProfile = liquidJumpProfile.merge(other.liquidJumpProfile), - fallStatusSpeedMin = fallStatusSpeedMin ?: other.fallStatusSpeedMin, - fallThroughSustainFrames = fallThroughSustainFrames ?: other.fallThroughSustainFrames, - groundMovementMinimumSustain = groundMovementMinimumSustain ?: other.groundMovementMinimumSustain, - groundMovementMaximumSustain = groundMovementMaximumSustain ?: other.groundMovementMaximumSustain, - groundMovementCheckDistance = groundMovementCheckDistance ?: other.groundMovementCheckDistance, - ) - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/BaseMovementParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/BaseMovementParameters.kt deleted file mode 100644 index 4620374a..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/BaseMovementParameters.kt +++ /dev/null @@ -1,102 +0,0 @@ -package ru.dbotthepony.kstarbound.defs - -import com.google.common.collect.ImmutableSet -import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.builder.JsonImplementation -import ru.dbotthepony.kstarbound.util.KOptional - -val BaseMovementParameters.ignorePlatformCollision get() = if (this is MovementParameters) this.ignorePlatformCollision ?: false else false -val BaseMovementParameters.restDuration get() = if (this is MovementParameters) this.restDuration ?: 0 else 0 - -@JsonImplementation(BaseMovementParameters.Impl::class) -sealed interface BaseMovementParameters { - val mass: Double? - val gravityMultiplier: Double? - val liquidBuoyancy: Double? - val airBuoyancy: Double? - val bounceFactor: Double? - - // If set to true, during an update that has more than one internal movement - // step, the movement will stop on the first bounce. - val stopOnFirstBounce: Boolean? - - // Cheat when sliding on the ground, by trying to correct upwards before - // other directions (within a set limit). Allows smooth sliding along - // horizontal ground without losing horizontal speed. - val enableSurfaceSlopeCorrection: Boolean? - val slopeSlidingFactor: Double? - - // ignored - val maxMovementPerStep: Double? - val maximumCorrection: Double? - val speedLimit: Double? - - val stickyCollision: Boolean? - val stickyForce: Double? - - val airFriction: Double? - val liquidFriction: Double? - val groundFriction: Double? - - val collisionEnabled: Boolean? - val frictionEnabled: Boolean? - val gravityEnabled: Boolean? - - val maximumPlatformCorrection: Double? - val maximumPlatformCorrectionVelocityFactor: Double? - - val physicsEffectCategories: ImmutableSet? - - @JsonFactory - data class Impl( - override val mass: Double? = null, - override val gravityMultiplier: Double? = null, - override val liquidBuoyancy: Double? = null, - override val airBuoyancy: Double? = null, - override val bounceFactor: Double? = null, - override val stopOnFirstBounce: Boolean? = null, - override val enableSurfaceSlopeCorrection: Boolean? = null, - override val slopeSlidingFactor: Double? = null, - override val maxMovementPerStep: Double? = null, - override val maximumCorrection: Double? = null, - override val speedLimit: Double? = null, - override val stickyCollision: Boolean? = null, - override val stickyForce: Double? = null, - override val airFriction: Double? = null, - override val liquidFriction: Double? = null, - override val groundFriction: Double? = null, - override val collisionEnabled: Boolean? = null, - override val frictionEnabled: Boolean? = null, - override val gravityEnabled: Boolean? = null, - override val maximumPlatformCorrection: Double? = null, - override val maximumPlatformCorrectionVelocityFactor: Double? = null, - override val physicsEffectCategories: ImmutableSet? = null, - ) : BaseMovementParameters { - fun merge(other: Impl): Impl { - return Impl( - mass = mass ?: other.mass, - gravityMultiplier = gravityMultiplier ?: other.gravityMultiplier, - liquidBuoyancy = liquidBuoyancy ?: other.liquidBuoyancy, - airBuoyancy = airBuoyancy ?: other.airBuoyancy, - bounceFactor = bounceFactor ?: other.bounceFactor, - stopOnFirstBounce = stopOnFirstBounce ?: other.stopOnFirstBounce, - enableSurfaceSlopeCorrection = enableSurfaceSlopeCorrection ?: other.enableSurfaceSlopeCorrection, - slopeSlidingFactor = slopeSlidingFactor ?: other.slopeSlidingFactor, - maxMovementPerStep = maxMovementPerStep ?: other.maxMovementPerStep, - maximumCorrection = maximumCorrection ?: other.maximumCorrection, - speedLimit = speedLimit ?: other.speedLimit, - stickyCollision = stickyCollision ?: other.stickyCollision, - stickyForce = stickyForce ?: other.stickyForce, - airFriction = airFriction ?: other.airFriction, - liquidFriction = liquidFriction ?: other.liquidFriction, - groundFriction = groundFriction ?: other.groundFriction, - collisionEnabled = collisionEnabled ?: other.collisionEnabled, - frictionEnabled = frictionEnabled ?: other.frictionEnabled, - gravityEnabled = gravityEnabled ?: other.gravityEnabled, - maximumPlatformCorrection = maximumPlatformCorrection ?: other.maximumPlatformCorrection, - maximumPlatformCorrectionVelocityFactor = maximumPlatformCorrectionVelocityFactor ?: other.maximumPlatformCorrectionVelocityFactor, - physicsEffectCategories = physicsEffectCategories ?: other.physicsEffectCategories, - ) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JumpProfile.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JumpProfile.kt index 7042a6ae..ef6cde41 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JumpProfile.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JumpProfile.kt @@ -5,27 +5,31 @@ import ru.dbotthepony.kstarbound.util.KOptional @JsonFactory data class JumpProfile( - val jumpSpeed: KOptional = KOptional.empty(), - val jumpControlForce: KOptional = KOptional.empty(), - val jumpInitialPercentage: KOptional = KOptional.empty(), - val jumpHoldTime: KOptional = KOptional.empty(), - val jumpTotalHoldTime: KOptional = KOptional.empty(), - val multiJump: KOptional = KOptional.empty(), - val reJumpDelay: KOptional = KOptional.empty(), - val autoJump: KOptional = KOptional.empty(), - val collisionCancelled: KOptional = KOptional.empty(), + val jumpSpeed: Double? = null, + val jumpControlForce: Double? = null, + val jumpInitialPercentage: Double? = null, + val jumpHoldTime: Double? = null, + val jumpTotalHoldTime: Double? = null, + val multiJump: Boolean? = null, + val reJumpDelay: Double? = null, + val autoJump: Boolean? = null, + val collisionCancelled: Boolean? = null, ) { fun merge(other: JumpProfile): JumpProfile { return JumpProfile( - jumpSpeed = jumpSpeed.or(other.jumpSpeed), - jumpControlForce = jumpControlForce.or(other.jumpControlForce), - jumpInitialPercentage = jumpInitialPercentage.or(other.jumpInitialPercentage), - jumpHoldTime = jumpHoldTime.or(other.jumpHoldTime), - jumpTotalHoldTime = jumpTotalHoldTime.or(other.jumpTotalHoldTime), - multiJump = multiJump.or(other.multiJump), - reJumpDelay = reJumpDelay.or(other.reJumpDelay), - autoJump = autoJump.or(other.autoJump), - collisionCancelled = collisionCancelled.or(other.collisionCancelled), + jumpSpeed = other.jumpSpeed ?: jumpSpeed, + jumpControlForce = other.jumpControlForce ?: jumpControlForce, + jumpInitialPercentage = other.jumpInitialPercentage ?: jumpInitialPercentage, + jumpHoldTime = other.jumpHoldTime ?: jumpHoldTime, + jumpTotalHoldTime = other.jumpTotalHoldTime ?: jumpTotalHoldTime, + multiJump = other.multiJump ?: multiJump, + reJumpDelay = other.reJumpDelay ?: reJumpDelay, + autoJump = other.autoJump ?: autoJump, + collisionCancelled = other.collisionCancelled ?: collisionCancelled, ) } + + companion object { + val EMPTY = JumpProfile() + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MovementParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MovementParameters.kt index f50e3682..ac49e454 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MovementParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MovementParameters.kt @@ -1,27 +1,218 @@ package ru.dbotthepony.kstarbound.defs +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableSet +import ru.dbotthepony.kstarbound.json.builder.JsonAlias import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.builder.JsonFlat -import ru.dbotthepony.kstarbound.util.KOptional +import ru.dbotthepony.kstarbound.json.builder.JsonImplementation +import ru.dbotthepony.kstarbound.util.Either import ru.dbotthepony.kstarbound.world.physics.Poly +sealed class BaseMovementParameters { + abstract val mass: Double? + abstract val gravityMultiplier: Double? + abstract val liquidBuoyancy: Double? + abstract val airBuoyancy: Double? + abstract val bounceFactor: Double? + + // If set to true, during an update that has more than one internal movement + // step, the movement will stop on the first bounce. + abstract val stopOnFirstBounce: Boolean? + + // Cheat when sliding on the ground, by trying to correct upwards before + // other directions (within a set limit). Allows smooth sliding along + // horizontal ground without losing horizontal speed. + abstract val enableSurfaceSlopeCorrection: Boolean? + abstract val slopeSlidingFactor: Double? + + abstract val maxMovementPerStep: Double? + abstract val maximumCorrection: Double? + abstract val speedLimit: Double? + + abstract val stickyCollision: Boolean? + abstract val stickyForce: Double? + + abstract val airFriction: Double? + abstract val liquidFriction: Double? + abstract val groundFriction: Double? + + abstract val collisionEnabled: Boolean? + abstract val frictionEnabled: Boolean? + abstract val gravityEnabled: Boolean? + + abstract val maximumPlatformCorrection: Double? + abstract val maximumPlatformCorrectionVelocityFactor: Double? + + abstract val physicsEffectCategories: ImmutableSet? +} + @JsonFactory data class MovementParameters( - @JsonFlat - val base: BaseMovementParameters.Impl = BaseMovementParameters.Impl(), + override val mass: Double? = null, + override val gravityMultiplier: Double? = null, + override val liquidBuoyancy: Double? = null, + override val airBuoyancy: Double? = null, + override val bounceFactor: Double? = null, + override val stopOnFirstBounce: Boolean? = null, + override val enableSurfaceSlopeCorrection: Boolean? = null, + override val slopeSlidingFactor: Double? = null, + override val maxMovementPerStep: Double? = null, + override val maximumCorrection: Double? = null, + override val speedLimit: Double? = null, + override val stickyCollision: Boolean? = null, + override val stickyForce: Double? = null, + override val airFriction: Double? = null, + override val liquidFriction: Double? = null, + override val groundFriction: Double? = null, + override val collisionEnabled: Boolean? = null, + override val frictionEnabled: Boolean? = null, + override val gravityEnabled: Boolean? = null, + override val maximumPlatformCorrection: Double? = null, + override val maximumPlatformCorrectionVelocityFactor: Double? = null, + override val physicsEffectCategories: ImmutableSet? = null, val discontinuityThreshold: Float? = null, - val collisionPoly: Poly? = null, + val collisionPoly: Either>? = null, val ignorePlatformCollision: Boolean? = null, val restDuration: Int? = null, -) : BaseMovementParameters by base { +) : BaseMovementParameters() { fun merge(other: MovementParameters): MovementParameters { return MovementParameters( - base = base.merge(other.base), - discontinuityThreshold = discontinuityThreshold ?: other.discontinuityThreshold, - collisionPoly = collisionPoly ?: other.collisionPoly, - ignorePlatformCollision = ignorePlatformCollision ?: other.ignorePlatformCollision, - restDuration = restDuration ?: other.restDuration, + mass = other.mass ?: mass, + gravityMultiplier = other.gravityMultiplier ?: gravityMultiplier, + liquidBuoyancy = other.liquidBuoyancy ?: liquidBuoyancy, + airBuoyancy = other.airBuoyancy ?: airBuoyancy, + bounceFactor = other.bounceFactor ?: bounceFactor, + stopOnFirstBounce = other.stopOnFirstBounce ?: stopOnFirstBounce, + enableSurfaceSlopeCorrection = other.enableSurfaceSlopeCorrection ?: enableSurfaceSlopeCorrection, + slopeSlidingFactor = other.slopeSlidingFactor ?: slopeSlidingFactor, + maxMovementPerStep = other.maxMovementPerStep ?: maxMovementPerStep, + maximumCorrection = other.maximumCorrection ?: maximumCorrection, + speedLimit = other.speedLimit ?: speedLimit, + stickyCollision = other.stickyCollision ?: stickyCollision, + stickyForce = other.stickyForce ?: stickyForce, + airFriction = other.airFriction ?: airFriction, + liquidFriction = other.liquidFriction ?: liquidFriction, + groundFriction = other.groundFriction ?: groundFriction, + collisionEnabled = other.collisionEnabled ?: collisionEnabled, + frictionEnabled = other.frictionEnabled ?: frictionEnabled, + gravityEnabled = other.gravityEnabled ?: gravityEnabled, + maximumPlatformCorrection = other.maximumPlatformCorrection ?: maximumPlatformCorrection, + maximumPlatformCorrectionVelocityFactor = other.maximumPlatformCorrectionVelocityFactor ?: maximumPlatformCorrectionVelocityFactor, + physicsEffectCategories = other.physicsEffectCategories ?: physicsEffectCategories, + discontinuityThreshold = other.discontinuityThreshold ?: discontinuityThreshold, + collisionPoly = other.collisionPoly ?: collisionPoly, + ignorePlatformCollision = other.ignorePlatformCollision ?: ignorePlatformCollision, + restDuration = other.restDuration ?: restDuration, ) } + + companion object { + val EMPTY = MovementParameters() + } +} + +@JsonFactory +data class ActorMovementParameters( + override val mass: Double? = null, + override val gravityMultiplier: Double? = null, + override val liquidBuoyancy: Double? = null, + override val airBuoyancy: Double? = null, + override val bounceFactor: Double? = null, + override val stopOnFirstBounce: Boolean? = null, + override val enableSurfaceSlopeCorrection: Boolean? = null, + override val slopeSlidingFactor: Double? = null, + override val maxMovementPerStep: Double? = null, + override val maximumCorrection: Double? = null, + override val speedLimit: Double? = null, + override val stickyCollision: Boolean? = null, + override val stickyForce: Double? = null, + override val airFriction: Double? = null, + override val liquidFriction: Double? = null, + override val groundFriction: Double? = null, + override val collisionEnabled: Boolean? = null, + override val frictionEnabled: Boolean? = null, + override val gravityEnabled: Boolean? = null, + override val maximumPlatformCorrection: Double? = null, + override val maximumPlatformCorrectionVelocityFactor: Double? = null, + override val physicsEffectCategories: ImmutableSet? = null, + + @JsonAlias("collisionPoly") + val standingPoly: Either>? = null, + @JsonAlias("collisionPoly") + val crouchingPoly: Either>? = null, + + val walkSpeed: Double? = null, + val runSpeed: Double? = null, + val flySpeed: Double? = null, + + val minimumLiquidPercentage: Double? = null, + val liquidImpedance: Double? = null, + val normalGroundFriction: Double? = null, + val ambulatingGroundFriction: Double? = null, + val groundForce: Double? = null, + val airForce: Double? = null, + val liquidForce: Double? = null, + + val airJumpProfile: JumpProfile = JumpProfile.EMPTY, + val liquidJumpProfile: JumpProfile = JumpProfile.EMPTY, + + val fallStatusSpeedMin: Double? = null, + val fallThroughSustainFrames: Int? = null, + + val groundMovementMinimumSustain: Double? = null, + val groundMovementMaximumSustain: Double? = null, + val groundMovementCheckDistance: Double? = null, + + val pathExploreRate: Double? = null, +) : BaseMovementParameters() { + fun merge(other: ActorMovementParameters): ActorMovementParameters { + return ActorMovementParameters( + mass = other.mass ?: mass, + gravityMultiplier = other.gravityMultiplier ?: gravityMultiplier, + liquidBuoyancy = other.liquidBuoyancy ?: liquidBuoyancy, + airBuoyancy = other.airBuoyancy ?: airBuoyancy, + bounceFactor = other.bounceFactor ?: bounceFactor, + stopOnFirstBounce = other.stopOnFirstBounce ?: stopOnFirstBounce, + enableSurfaceSlopeCorrection = other.enableSurfaceSlopeCorrection ?: enableSurfaceSlopeCorrection, + slopeSlidingFactor = other.slopeSlidingFactor ?: slopeSlidingFactor, + maxMovementPerStep = other.maxMovementPerStep ?: maxMovementPerStep, + maximumCorrection = other.maximumCorrection ?: maximumCorrection, + speedLimit = other.speedLimit ?: speedLimit, + stickyCollision = other.stickyCollision ?: stickyCollision, + stickyForce = other.stickyForce ?: stickyForce, + airFriction = other.airFriction ?: airFriction, + liquidFriction = other.liquidFriction ?: liquidFriction, + groundFriction = other.groundFriction ?: groundFriction, + collisionEnabled = other.collisionEnabled ?: collisionEnabled, + frictionEnabled = other.frictionEnabled ?: frictionEnabled, + gravityEnabled = other.gravityEnabled ?: gravityEnabled, + maximumPlatformCorrection = other.maximumPlatformCorrection ?: maximumPlatformCorrection, + maximumPlatformCorrectionVelocityFactor = other.maximumPlatformCorrectionVelocityFactor ?: maximumPlatformCorrectionVelocityFactor, + physicsEffectCategories = other.physicsEffectCategories ?: physicsEffectCategories, + standingPoly = other.standingPoly ?: standingPoly, + crouchingPoly = other.crouchingPoly ?: crouchingPoly, + walkSpeed = other.walkSpeed ?: walkSpeed, + runSpeed = other.runSpeed ?: runSpeed, + flySpeed = other.flySpeed ?: flySpeed, + minimumLiquidPercentage = other.minimumLiquidPercentage ?: minimumLiquidPercentage, + liquidImpedance = other.liquidImpedance ?: liquidImpedance, + normalGroundFriction = other.normalGroundFriction ?: normalGroundFriction, + ambulatingGroundFriction = other.ambulatingGroundFriction ?: ambulatingGroundFriction, + groundForce = other.groundForce ?: groundForce, + airForce = other.airForce ?: airForce, + liquidForce = other.liquidForce ?: liquidForce, + airJumpProfile = airJumpProfile.merge(other.airJumpProfile), + liquidJumpProfile = liquidJumpProfile.merge(other.liquidJumpProfile), + fallStatusSpeedMin = other.fallStatusSpeedMin ?: fallStatusSpeedMin, + fallThroughSustainFrames = other.fallThroughSustainFrames ?: fallThroughSustainFrames, + groundMovementMinimumSustain = other.groundMovementMinimumSustain ?: groundMovementMinimumSustain, + groundMovementMaximumSustain = other.groundMovementMaximumSustain ?: groundMovementMaximumSustain, + groundMovementCheckDistance = other.groundMovementCheckDistance ?: groundMovementCheckDistance, + ) + } + + companion object { + val EMPTY = ActorMovementParameters() + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt index 33ad872f..aef681c5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt @@ -4,10 +4,10 @@ import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableSet import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.IScriptable import ru.dbotthepony.kstarbound.defs.IThingWithDescription -import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition import ru.dbotthepony.kstarbound.json.builder.JsonFactory diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerMovementModifiers.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/ActorMovementModifiers.kt similarity index 85% rename from src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerMovementModifiers.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/ActorMovementModifiers.kt index 8a0a5c6e..6284c9b8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerMovementModifiers.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/ActorMovementModifiers.kt @@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.defs.player import ru.dbotthepony.kstarbound.json.builder.JsonFactory @JsonFactory -class PlayerMovementModifiers( +class ActorMovementModifiers( val groundMovementModifier: Double = 1.0, val liquidMovementModifier: Double = 1.0, val speedModifier: Double = 1.0, @@ -14,8 +14,8 @@ class PlayerMovementModifiers( val facingSuppressed: Boolean = false, val movementSuppressed: Boolean = false, ) { - fun combine(other: PlayerMovementModifiers): PlayerMovementModifiers { - return PlayerMovementModifiers( + fun combine(other: ActorMovementModifiers): ActorMovementModifiers { + return ActorMovementModifiers( groundMovementModifier = groundMovementModifier * other.groundMovementModifier, liquidMovementModifier = liquidMovementModifier * other.liquidMovementModifier, speedModifier = speedModifier * other.speedModifier, @@ -27,4 +27,8 @@ class PlayerMovementModifiers( movementSuppressed = movementSuppressed || other.movementSuppressed, ) } + + companion object { + val EMPTY = ActorMovementModifiers() + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerDefinition.kt index 393e2932..35401737 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerDefinition.kt @@ -5,8 +5,8 @@ import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableSet import com.google.gson.JsonObject import ru.dbotthepony.kstarbound.Registry -import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.ActorMovementParameters +import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.Species import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/GameTimer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/GameTimer.kt new file mode 100644 index 00000000..b8cb64d9 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/GameTimer.kt @@ -0,0 +1,39 @@ +package ru.dbotthepony.kstarbound.util + +import ru.dbotthepony.kstarbound.Starbound + +class GameTimer(val time: Double = 0.0) { + var timer = time + private set + + fun reset() { + timer = time + } + + var hasFinished: Boolean + get() = timer <= 0.0 + set(value) { + if (value) + timer = 0.0 + else + timer = time + } + + val percent: Double + get() = if (time != 0.0) timer / time else 0.0 + + fun invert() { + timer = time - timer + } + + fun tick(delta: Double = Starbound.TICK_TIME_ADVANCE): Boolean { + timer = (timer - delta).coerceAtLeast(0.0) + return timer == 0.0 + } + + fun wrapTick(delta: Double = Starbound.TICK_TIME_ADVANCE): Boolean { + val result = tick(delta) + if (result) reset() + return result + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 19d82df0..8a1eacbd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -7,6 +7,7 @@ import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.util.MailboxExecutorService import ru.dbotthepony.kstarbound.util.ParallelPerform +import ru.dbotthepony.kstarbound.util.filterNotNull import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.TileView @@ -14,6 +15,7 @@ import ru.dbotthepony.kstarbound.world.entities.Entity import ru.dbotthepony.kstarbound.world.entities.WorldObject import ru.dbotthepony.kstarbound.world.physics.CollisionPoly import ru.dbotthepony.kstarbound.world.physics.CollisionType +import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares import ru.dbotthepony.kvector.api.IStruct2d @@ -24,7 +26,9 @@ import ru.dbotthepony.kvector.vector.Vector2d import ru.dbotthepony.kvector.vector.Vector2i import java.util.concurrent.ForkJoinPool import java.util.concurrent.locks.ReentrantLock +import java.util.function.Predicate import java.util.random.RandomGenerator +import java.util.stream.Stream import kotlin.concurrent.withLock abstract class World, ChunkType : Chunk>( @@ -184,7 +188,7 @@ abstract class World, ChunkType : Chunk, ChunkType : Chunk, ChunkType : Chunk): Stream { + return queryCollisions(with.aabb.enlarge(1.0, 1.0)).stream() + .filter(filter) + .map { with.intersect(it.poly) } + .filterNotNull() + } + + fun polyIntersects(with: Poly, filter: Predicate = Predicate { true }, tolerance: Double = 0.0): Boolean { + return collide(with, filter).anyMatch { it.penetration >= tolerance } + } + + fun polyIntersects(with: Poly, filter: Collection, tolerance: Double = 0.0): Boolean { + return polyIntersects(with, Predicate { it.type in filter }, tolerance) + } + fun gravityAt(pos: IStruct2i): Vector2d { return gravity } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt index 5ad2ea27..85138362 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt @@ -1,6 +1,7 @@ package ru.dbotthepony.kstarbound.world.api import com.github.benmanes.caffeine.cache.Interner +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.util.HashTableInterner import java.io.DataInputStream @@ -26,7 +27,7 @@ sealed class AbstractCell { } @JvmStatic - protected val POOL: Interner = HashTableInterner() + protected val POOL: Interner = if (Starbound.DEDUP_CELL_STATES) HashTableInterner() else Interner { it } val EMPTY: ImmutableCell = POOL.intern(ImmutableCell(AbstractTileState.EMPTY, AbstractTileState.EMPTY, AbstractLiquidState.EMPTY, 0, 0, 0, false)) val NULL: ImmutableCell = POOL.intern(ImmutableCell(AbstractTileState.NULL, AbstractTileState.NULL, AbstractLiquidState.EMPTY, 0, 0, 0, false)) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt index 89eabdd0..99982e58 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt @@ -1,7 +1,10 @@ package ru.dbotthepony.kstarbound.world.api +import com.github.benmanes.caffeine.cache.Interner import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition +import ru.dbotthepony.kstarbound.util.HashTableInterner import java.io.DataInputStream sealed class AbstractLiquidState { @@ -18,6 +21,9 @@ sealed class AbstractLiquidState { stream.skipNBytes(1 + 4 + 4 + 1) } - val EMPTY = ImmutableLiquidState() + @JvmStatic + protected val POOL: Interner = if (Starbound.DEDUP_CELL_STATES) HashTableInterner() else Interner { it } + + val EMPTY: ImmutableLiquidState = POOL.intern(ImmutableLiquidState()) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt index 83ae20b0..0f0992b2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt @@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world.api import com.github.benmanes.caffeine.cache.Interner import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier import ru.dbotthepony.kstarbound.defs.tile.TileDefinition @@ -24,7 +25,7 @@ sealed class AbstractTileState { } @JvmStatic - protected val POOL: Interner = HashTableInterner() + protected val POOL: Interner = if (Starbound.DEDUP_CELL_STATES) HashTableInterner() else Interner { it } val EMPTY: ImmutableTileState = POOL.intern(ImmutableTileState(BuiltinMetaMaterials.EMPTY)) val NULL: ImmutableTileState = POOL.intern(ImmutableTileState(BuiltinMetaMaterials.NULL)) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt index 0158b666..a274323c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableLiquidState.kt @@ -24,8 +24,6 @@ data class MutableLiquidState( } override fun immutable(): ImmutableLiquidState { - val result = ImmutableLiquidState(def, level, pressure, isInfinite) - if (result == EMPTY) return EMPTY - return result + return POOL.intern(ImmutableLiquidState(def, level, pressure, isInfinite)) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractActorMovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractActorMovementController.kt new file mode 100644 index 00000000..14709296 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractActorMovementController.kt @@ -0,0 +1,429 @@ +package ru.dbotthepony.kstarbound.world.entities + +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.ActorMovementParameters +import ru.dbotthepony.kstarbound.defs.JumpProfile +import ru.dbotthepony.kstarbound.defs.MovementParameters +import ru.dbotthepony.kstarbound.defs.player.ActorMovementModifiers +import ru.dbotthepony.kstarbound.util.GameTimer +import ru.dbotthepony.kstarbound.world.Direction +import ru.dbotthepony.kstarbound.world.physics.CollisionType +import ru.dbotthepony.kvector.vector.Vector2d +import kotlin.math.PI +import kotlin.math.absoluteValue +import kotlin.math.sign + +abstract class AbstractActorMovementController : AbstractMovementController() { + abstract var controlRun: Boolean + abstract var controlCrouch: Boolean + abstract var controlDown: Boolean + abstract var lastControlDown: Boolean + abstract var controlFly: Vector2d? + abstract var controlFace: Direction? + + abstract var isRunning: Boolean + protected set + abstract var isWalking: Boolean + protected set + abstract var isCrouching: Boolean + protected set + abstract var isFlying: Boolean + protected set + abstract var isFalling: Boolean + protected set + abstract var isJumping: Boolean + protected set + + abstract var canJump: Boolean + abstract var controlJump: Boolean + abstract var controlJumpAnyway: Boolean + + abstract var lastControlJump: Boolean + protected set + abstract var lastControlCrouch: Boolean + protected set + + abstract var isGroundMovement: Boolean + protected set + abstract var isLiquidMovement: Boolean + protected set + + abstract var controlRotationRate: Double + abstract var controlAcceleration: Vector2d + abstract var controlForce: Vector2d + + abstract var fallThroughSustain: Int + protected set + + // Target horizontal velocity for walking / running + abstract var targetHorizontalAmbulatingVelocity: Double + protected set + + abstract var controlPathMove: Pair? + abstract var pathMoveResult: Pair? + + abstract var controlMove: Direction? + + abstract var actorMovementParameters: ActorMovementParameters + abstract var movementModifiers: ActorMovementModifiers + + abstract var controlActorMovementParameters: ActorMovementParameters + abstract var controlMovementModifiers: ActorMovementModifiers + + abstract val approachVelocities: MutableList + abstract val approachVelocityAngles: MutableList + + abstract var movingDirection: Direction? + abstract var facingDirection: Direction? + + // this is set internally on each move step + final override var movementParameters: MovementParameters = MovementParameters.EMPTY + + abstract var anchorEntity: Entity? + + var pathController: PathController? = null + var groundMovementSustainTimer: GameTimer = GameTimer(0.0) + var reJumpTimer: GameTimer = GameTimer(0.0) + var jumpHoldTimer: GameTimer? = null + + data class ApproachVelocityCommand( + val target: Vector2d, + val maxControlForce: Double + ) + + data class ApproachVelocityAngleCommand( + val alongAngle: Double, + val targetVelocity: Double, + val maxControlForce: Double, + val positiveOnly: Boolean + ) + + fun calculateMovementParameters(base: ActorMovementParameters): MovementParameters { + val mass = base.mass + val gravityMultiplier = base.gravityMultiplier + + val liquidBuoyancy = base.liquidBuoyancy + val airBuoyancy = base.airBuoyancy + val bounceFactor = base.bounceFactor + val stopOnFirstBounce = base.stopOnFirstBounce + val enableSurfaceSlopeCorrection = base.enableSurfaceSlopeCorrection + val slopeSlidingFactor = base.slopeSlidingFactor + val maxMovementPerStep = base.maxMovementPerStep + + val collisionPoly = if (isCrouching) base.crouchingPoly else base.standingPoly + + val stickyCollision = base.stickyCollision + val stickyForce = base.stickyForce + + val airFriction = base.airFriction + val liquidFriction = base.liquidFriction + + // If we are traveling in the correct direction while in a movement mode that + // requires contact with the ground (ambulating i.e. walking or running), and + // not traveling faster than our target horizontal movement, then apply the + // special 'ambulatingGroundFriction'. + val relativeXVelocity = velocity.x - surfaceVelocity.x + val useAmbulatingGroundFriction = (isWalking || isRunning) && + targetHorizontalAmbulatingVelocity.sign == relativeXVelocity.sign && + relativeXVelocity.absoluteValue <= targetHorizontalAmbulatingVelocity.absoluteValue + + val groundFriction = if (useAmbulatingGroundFriction) base.ambulatingGroundFriction else base.normalGroundFriction + + val ignorePlatformCollision = fallThroughSustain > 0 || controlFly != null || controlDown + + val collisionEnabled = base.collisionEnabled + val frictionEnabled = base.frictionEnabled + val gravityEnabled = base.gravityEnabled + + val maximumPlatformCorrection = base.maximumPlatformCorrection + val maximumPlatformCorrectionVelocityFactor = base.maximumPlatformCorrectionVelocityFactor + + val physicsEffectCategories = base.physicsEffectCategories + + val maximumCorrection = base.maximumCorrection + val speedLimit = base.speedLimit + + return MovementParameters( + mass = mass, + gravityMultiplier = gravityMultiplier, + liquidBuoyancy = liquidBuoyancy, + airBuoyancy = airBuoyancy, + bounceFactor = bounceFactor, + stopOnFirstBounce = stopOnFirstBounce, + enableSurfaceSlopeCorrection = enableSurfaceSlopeCorrection, + slopeSlidingFactor = slopeSlidingFactor, + maxMovementPerStep = maxMovementPerStep, + maximumCorrection = maximumCorrection, + speedLimit = speedLimit, + stickyCollision = stickyCollision, + stickyForce = stickyForce, + airFriction = airFriction, + liquidFriction = liquidFriction, + groundFriction = groundFriction, + collisionEnabled = collisionEnabled, + frictionEnabled = frictionEnabled, + gravityEnabled = gravityEnabled, + maximumPlatformCorrection = maximumPlatformCorrection, + maximumPlatformCorrectionVelocityFactor = maximumPlatformCorrectionVelocityFactor, + physicsEffectCategories = physicsEffectCategories, + ignorePlatformCollision = ignorePlatformCollision, + collisionPoly = collisionPoly, + ) + } + + fun updateMovementParameters(base: ActorMovementParameters): MovementParameters { + val params = calculateMovementParameters(base) + movementParameters = params + return params + } + + open fun clearControls() { + controlRotationRate = 0.0 + controlAcceleration = Vector2d.ZERO + controlForce = Vector2d.ZERO + controlRun = false + controlCrouch = false + controlJump = false + controlJumpAnyway = false + controlFly = null + controlPathMove = null + controlActorMovementParameters = ActorMovementParameters.EMPTY + controlMovementModifiers = ActorMovementModifiers.EMPTY + } + + override fun move() { + // TODO: anchor entity + + if (anchorEntity?.isRemoved == true) + anchorEntity = null + + val anchorEntity = anchorEntity + + if (anchorEntity != null) { + controlRun = false + controlCrouch = false + isFlying = false + isFalling = false + canJump = false + controlJump = false + isGroundMovement = false + isLiquidMovement = false + + velocity = (anchorEntity.position - position) / Starbound.TICK_TIME_ADVANCE + super.move() + position = anchorEntity.position + } else { + val movementParameters = actorMovementParameters.merge(controlActorMovementParameters) + val movementModifiers = movementModifiers.combine(controlMovementModifiers) + + if (movementModifiers.movementSuppressed) { + controlMove = null + controlRun = false + controlCrouch = false + controlDown = false + controlJump = false + controlPathMove = null + } + + // controlling any other movement overrides the pathing + if (controlMove != null || controlCrouch || controlDown || controlJump || controlFly != null || approachVelocities.isNotEmpty() || approachVelocityAngles.isNotEmpty()) { + controlPathMove = null + } + + if (controlPathMove != null && pathMoveResult == null) { + if (appliedForceRegion) { + pathController?.reset() + } else if (!pathController!!.isPathfinding) { + // TODO: path move code + } else { + + } + } else { + pathController = null + } + + if (controlFly != null) + controlMove = null + + if ((controlDown && !lastControlDown) || controlFly != null) + fallThroughSustain = movementParameters.fallThroughSustainFrames ?: 0 + else if (fallThroughSustain > 0) + fallThroughSustain-- + + updateMovementParameters(movementParameters) + + targetHorizontalAmbulatingVelocity = 0.0 + + rotation = (rotation + controlRotationRate * Starbound.TICK_TIME_ADVANCE) % (PI * 2.0) + velocity += controlAcceleration * Starbound.TICK_TIME_ADVANCE + controlForce / mass * Starbound.TICK_TIME_ADVANCE + + approachVelocities.forEach { + approachVelocity(it.target, it.maxControlForce) + } + + approachVelocityAngles.forEach { + approachVelocityAlongAngle(it.alongAngle, it.targetVelocity, it.maxControlForce, it.positiveOnly) + } + + isLiquidMovement = liquidPercentage >= (actorMovementParameters.minimumLiquidPercentage ?: 0.0) + val liquidImpedance = liquidPercentage * (actorMovementParameters.liquidImpedance ?: 0.0) + + var updatedMovingDirection: Direction? = null + val isRunning = controlRun && !movementModifiers.runningSuppressed + + if (controlFly != null) { + var flyVelocity = controlFly!! + + if (flyVelocity.lengthSquared != 0.0) + flyVelocity = flyVelocity.unitVector * (actorMovementParameters.flySpeed ?: 0.0) + + if (isLiquidMovement) + approachVelocity(flyVelocity * (1.0 - liquidImpedance) * movementModifiers.speedModifier, (movementParameters.liquidForce ?: 0.0) * movementModifiers.liquidMovementModifier) + else + approachVelocity(flyVelocity * movementModifiers.speedModifier, movementParameters.airForce ?: 0.0) + + if (flyVelocity.x > 0.0) + updatedMovingDirection = Direction.RIGHT + else if (flyVelocity.x < 0.0) + updatedMovingDirection = Direction.LEFT + + groundMovementSustainTimer = GameTimer(0.0) + } else { + val jumpModifier: Double + val jumpProfile: JumpProfile + + if (isLiquidMovement) { + jumpModifier = movementModifiers.liquidJumpModifier + jumpProfile = movementParameters.liquidJumpProfile.copy(jumpSpeed = (movementParameters.liquidJumpProfile.jumpSpeed ?: 0.0) * (1.0 - liquidImpedance)) + } else { + jumpModifier = movementModifiers.airJumpModifier + jumpProfile = movementParameters.airJumpProfile + } + + var startJump = false + var holdJump = false + + // If we are on the ground, then reset the ground movement sustain timer + // to the maximum. If we are not on the ground or near the ground + // according to the nearGroundCheckDistance, and we are past the minimum + // sustain time, then go ahead and immediately clear the ground movement + // sustain timer. + val minGroundSustain = movementParameters.groundMovementMinimumSustain ?: 0.0 + val maxGroundSustain = movementParameters.groundMovementMaximumSustain ?: 0.0 + val groundCheckDistance = movementParameters.groundMovementCheckDistance ?: 0.0 + + groundMovementSustainTimer.tick() + + if (isOnGround) { + groundMovementSustainTimer = GameTimer(maxGroundSustain) + } else if (!groundMovementSustainTimer.hasFinished && groundCheckDistance > 0.0 && maxGroundSustain - groundMovementSustainTimer.timer > minGroundSustain) { + val collideAny = localHitboxes + .map { it + Vector2d(0.0, -groundCheckDistance) } + .anyMatch { + world.polyIntersects(it, { it.type >= CollisionType.PLATFORM }) + } + + if (collideAny) + groundMovementSustainTimer = GameTimer(0.0) + } + + val standingJumpable = !groundMovementSustainTimer.hasFinished + val controlJump = this.controlJump && (!movementModifiers.jumpingSuppressed || controlJumpAnyway) + + // We are doing a jump if m_reJumpTimer has run out and there has been a + // new m_controlJump command which was just recently triggered. If + // jumpProfile.autoJump is set, then we don't care whether it is a new + // m_controlJump command, m_controlJump can be held. + if (reJumpTimer.hasFinished && controlJump && (jumpProfile.autoJump == true || !lastControlJump)) { + if (standingJumpable || jumpProfile.multiJump == true || controlJumpAnyway) + startJump = true + } else if (isJumping && controlJump && jumpHoldTimer?.hasFinished != true) { + if (jumpProfile.collisionCancelled != true || collisionCorrection.y >= 0.0) + holdJump = true + } + + if (startJump) { + isJumping = true + reJumpTimer = GameTimer(jumpProfile.reJumpDelay ?: 0.0) + + jumpHoldTimer = if ((jumpProfile.jumpHoldTime ?: 0.0) >= 0.0) { + GameTimer(jumpProfile.jumpHoldTime ?: 0.0) + } else { + null + } + + velocity = velocity.copy(y = velocity.y + (jumpProfile.jumpSpeed ?: 0.0) * (jumpProfile.jumpInitialPercentage ?: 0.0) * jumpModifier) + groundMovementSustainTimer = GameTimer(0.0) + } else if (holdJump) { + reJumpTimer.tick() + jumpHoldTimer?.tick() + + approachYVelocity((jumpProfile.jumpSpeed ?: 0.0) * jumpModifier, (jumpProfile.jumpControlForce ?: 0.0) * jumpModifier) + } else { + isJumping = false + reJumpTimer.tick() + } + + if (controlMove == Direction.LEFT) { + updatedMovingDirection = Direction.LEFT + targetHorizontalAmbulatingVelocity = -1.0 * (if (isRunning) movementParameters.runSpeed ?: 0.0 else movementParameters.walkSpeed ?: 0.0) * movementModifiers.speedModifier + } else if (controlMove == Direction.RIGHT) { + updatedMovingDirection = Direction.RIGHT + targetHorizontalAmbulatingVelocity = 1.0 * (if (isRunning) movementParameters.runSpeed ?: 0.0 else movementParameters.walkSpeed ?: 0.0) * movementModifiers.speedModifier + } + + if (isLiquidMovement) + targetHorizontalAmbulatingVelocity *= 1.0 - liquidImpedance + + // don't ambulate if we're already moving faster than the target velocity in the direction of ambulation + val ambulationWouldAccelerate = (targetHorizontalAmbulatingVelocity + surfaceVelocity.x).absoluteValue > velocity.x.absoluteValue || + targetHorizontalAmbulatingVelocity < 0 != velocity.x < 0 + + if (targetHorizontalAmbulatingVelocity != 0.0 && ambulationWouldAccelerate) { + val ambulatingAccelerate = if (isOnGround) + (movementParameters.groundForce ?: 0.0) * movementModifiers.groundMovementModifier + else if (isLiquidMovement) + (movementParameters.liquidForce ?: 0.0) * movementModifiers.liquidMovementModifier + else + movementParameters.airForce ?: 0.0 + + approachXVelocity(targetHorizontalAmbulatingVelocity + surfaceVelocity.x, ambulatingAccelerate) + } + } + + movingDirection = updatedMovingDirection + + if (!movementModifiers.facingSuppressed) { + if (controlFace != null) + facingDirection = controlFace + else if (updatedMovingDirection != null) + facingDirection = updatedMovingDirection + else if (controlPathMove != null && pathController?.controlFace != null) + facingDirection = pathController?.controlFace + } + + isGroundMovement = !groundMovementSustainTimer.hasFinished + + if (isGroundMovement) { + this.isRunning = isRunning && controlMove != null + this.isWalking = !isRunning && controlMove != null + this.isCrouching = controlCrouch && controlMove != null + } + + isFlying = controlFly != null + isFalling = (velocity.y < (movementParameters.fallStatusSpeedMin ?: 0.0)) && !isGroundMovement + + super.move() + + lastControlDown = controlDown + lastControlJump = controlJump + + if (isLiquidMovement) + canJump = reJumpTimer.hasFinished && (!groundMovementSustainTimer.hasFinished || movementParameters.liquidJumpProfile.multiJump == true) + else + canJump = reJumpTimer.hasFinished && (!groundMovementSustainTimer.hasFinished || movementParameters.airJumpProfile.multiJump == true) + } + + clearControls() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractMovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractMovementController.kt new file mode 100644 index 00000000..76300ff4 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractMovementController.kt @@ -0,0 +1,492 @@ +package ru.dbotthepony.kstarbound.world.entities + +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.MovementParameters +import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kstarbound.world.physics.CollisionPoly +import ru.dbotthepony.kstarbound.world.physics.CollisionType +import ru.dbotthepony.kstarbound.world.physics.Poly +import ru.dbotthepony.kvector.util.linearInterpolation +import ru.dbotthepony.kvector.util2d.AABB +import ru.dbotthepony.kvector.vector.Vector2d +import ru.dbotthepony.kvector.vector.times +import java.util.stream.Stream +import kotlin.math.PI +import kotlin.math.absoluteValue +import kotlin.math.acos +import kotlin.math.cos +import kotlin.math.sin + +abstract class AbstractMovementController { + abstract val world: World<*, *> + + val localHitboxes: Stream + get() { return (movementParameters.collisionPoly?.map({ Stream.of(it) }, { it.stream() }) ?: return Stream.of()).map { it.rotate(rotation) + position } } + + abstract var position: Vector2d + + open fun shouldCollideWithType(type: CollisionType): Boolean { + return type !== CollisionType.NONE + } + + open fun shouldCollideWithBody(body: CollisionPoly): Boolean { + return shouldCollideWithType(body.type) + } + + // Movement variables + abstract var isOnGround: Boolean + protected set + abstract var isColliding: Boolean + protected set + abstract var isCollisionStuck: Boolean + protected set + abstract var isCollidingWithNull: Boolean + protected set + abstract var stickingDirection: Double? + protected set + abstract var surfaceSlope: Vector2d + protected set + abstract var surfaceVelocity: Vector2d + protected set + abstract var collisionCorrection: Vector2d + protected set + abstract var liquidPercentage: Double + protected set + + abstract var appliedForceRegion: Boolean + protected set + + abstract var isZeroGravity: Boolean + protected set + + abstract var movementParameters: MovementParameters + + abstract var rotation: Double + + open var gravityMultiplier = 1.0 + open var isGravityDisabled = false + + val mass: Double + get() = movementParameters.mass ?: 1.0 + + var velocity = Vector2d.ZERO + + fun determineGravity(): Vector2d { + if (isZeroGravity || isGravityDisabled) + return Vector2d.ZERO + + return world.gravityAt(position) + } + + open fun updateLiquidPercentage() { + + } + + open fun updateForceRegions() { + + } + + fun approachVelocity(targetVelocity: Vector2d, maxControlForce: Double) { + // Instead of applying the force directly, work backwards and figure out the + // maximum acceleration that could be achieved by the current control force, + // and maximize the change in velocity based on that. + + val diff = targetVelocity - velocity + val mag = diff.length + + if (mag == 0.0) return + + val maximumAcceleration = maxControlForce / mass * Starbound.TICK_TIME_ADVANCE + val clampedMag = mag.coerceIn(0.0, maximumAcceleration) + + velocity += diff * (clampedMag / mag) + } + + fun approachVelocityAlongAngle(angle: Double, targetVelocity: Double, maxControlForce: Double, positiveOnly: Boolean = false) { + // Same strategy as approachVelocity, work backwards to figure out the + // maximum acceleration and apply that. + + // Project the current velocity along the axis normal, the velocity + // difference is the difference between the targetVelocity and this + // projection. + + val axis = Vector2d(cos(angle), sin(angle)) + val velocityAlongAxis = velocity.dot(axis) + val diff = targetVelocity - velocityAlongAxis + + if (diff == 0.0 || positiveOnly && diff < 0.0) return + + val maximumAcceleration = maxControlForce / mass * Starbound.TICK_TIME_ADVANCE + val diffMag = diff.absoluteValue + val clampedMag = diffMag.coerceIn(0.0, maximumAcceleration) + + velocity += axis * diff * (clampedMag / diffMag) + } + + fun approachXVelocity(velocity: Double, maxControlForce: Double) { + approachVelocityAlongAngle(0.0, velocity, maxControlForce) + } + + fun approachYVelocity(velocity: Double, maxControlForce: Double) { + approachVelocityAlongAngle(PI / 2.0, velocity, maxControlForce) + } + + /** + * this function is executed in parallel + */ + // TODO: Ghost collisions occur, where objects trip on edges + open fun move() { + isZeroGravity = isGravityDisabled || gravityMultiplier == 0.0 || determineGravity().lengthSquared == 0.0 + + val movementParameters = movementParameters + + movementParameters.speedLimit?.let { + if (velocity.length > it) + velocity = velocity.unitVector * it + } + + // TODO: Here: moving platforms sticky code + + if (movementParameters.collisionPoly == null || !movementParameters.collisionPoly.map({ true }, { it.isNotEmpty() }) || movementParameters.collisionEnabled != true) { + position += velocity * Starbound.TICK_TIME_ADVANCE + surfaceSlope = Vector2d.POSITIVE_Y + surfaceVelocity = Vector2d.ZERO + isOnGround = false + stickingDirection = null + isColliding = false + isCollidingWithNull = false + isCollisionStuck = false + return + } + + var steps = 1 + + movementParameters.maxMovementPerStep?.let { + steps = (velocity.length * Starbound.TICK_TIME_ADVANCE / it).toInt() + 1 + } + + var relativeVelocity = velocity + surfaceSlope = Vector2d.POSITIVE_Y + // TODO: Here: moving platforms sticky code + + val dt = Starbound.TICK_TIME_ADVANCE / steps + + for (step in 0 until steps) { + val velocityMagnitude = relativeVelocity.length + val velocityDirection = relativeVelocity / velocityMagnitude + val movement = relativeVelocity * dt + + val ignorePlatforms = (movementParameters.ignorePlatformCollision ?: false) || relativeVelocity.y > 0.0 + val maximumCorrection = movementParameters.maximumCorrection ?: 0.0 + val maximumPlatformCorrection = (movementParameters.maximumPlatformCorrection ?: Double.POSITIVE_INFINITY) + + (movementParameters.maximumPlatformCorrectionVelocityFactor ?: 0.0) * velocityMagnitude + + val localHitboxes = localHitboxes.toList() + val aabb = localHitboxes.stream().map { it.aabb }.reduce(AABB::combine).get() + var queryBounds = aabb.enlarge(maximumCorrection, maximumCorrection) + queryBounds = queryBounds.combine(queryBounds + movement) + + val polies = world.queryCollisions(queryBounds).filter(this::shouldCollideWithBody) + + val results = ArrayList(localHitboxes.size) + + for (hitbox in localHitboxes) { + results.add(collisionSweep(hitbox, polies, movement, ignorePlatforms, (movementParameters.enableSurfaceSlopeCorrection ?: false) && !isZeroGravity, maximumCorrection, maximumPlatformCorrection, aabb.centre)) + } + + val result = results.minOrNull()!! + position += result.movement + + if (result.collisionType == CollisionType.NULL) { + isCollidingWithNull = true + break + } else { + isCollidingWithNull = false + } + + val correction = result.correction + val normCorrection = correction.unitVector + surfaceSlope = result.groundSlope + collisionCorrection = result.correction + isColliding = correction != Vector2d.ZERO || result.isStuck + isOnGround = !isZeroGravity && result.isOnGround + isCollisionStuck = result.isStuck + + // If we have collided, apply either sticky or normal (bouncing) collision physics + if (correction != Vector2d.ZERO) { + if (movementParameters.stickyCollision == true && result.collisionType !== CollisionType.SLIPPERY) { + // When sticking, cancel all velocity and apply stickyForce in the + // opposite of the direction of collision correction. + relativeVelocity = -normCorrection * (movementParameters.stickyForce ?: 0.0) / mass * dt + stickingDirection = -normCorrection.toAngle() + break + } else { + stickingDirection = null + val correctionDirection = correction.unitVector + + if (movementParameters.bounceFactor != null && movementParameters.bounceFactor != 0.0) { + val adjustment = correctionDirection * (velocityMagnitude * (correctionDirection * -velocityDirection)) + relativeVelocity += adjustment + movementParameters.bounceFactor * adjustment + + if (movementParameters.stopOnFirstBounce == true) { + // When bouncing, stop integrating at the moment of bounce. This + // prevents the frame of contact from being missed due to multiple + // iterations per frame. + break + } + } else { + // Only adjust the velocity to the extent that the collision was + // caused by the velocity in each axis, to eliminate collision + // induced velocity in a platformery way (each axis considered + // independently). + + if (relativeVelocity.x < 0.0 && correction.x > 0.0) + relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / Starbound.TICK_TIME_ADVANCE).coerceAtMost(0.0)) + else if (relativeVelocity.x > 0.0 && correction.x < 0.0) + relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / Starbound.TICK_TIME_ADVANCE).coerceAtLeast(0.0)) + + if (relativeVelocity.y < 0.0 && correction.y > 0.0) + relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / Starbound.TICK_TIME_ADVANCE).coerceAtMost(0.0)) + else if (relativeVelocity.y > 0.0 && correction.y < 0.0) + relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / Starbound.TICK_TIME_ADVANCE).coerceAtLeast(0.0)) + } + } + } + } + + var newVelocity = relativeVelocity + + updateLiquidPercentage() + + // TODO: sticky collision update + + // In order to make control work accurately, passive forces need to be + // applied to velocity *after* integrating. This prevents control from + // having to account for one timestep of passive forces in order to result + // in the correct controlled movement. + if (!isZeroGravity && stickingDirection == null) { + val buoyancy = (movementParameters.liquidBuoyancy ?: 0.0).coerceIn(0.0, 1.0) + liquidPercentage + (movementParameters.airBuoyancy ?: 0.0).coerceIn(0.0, 1.0) * (1.0 - liquidPercentage) + val gravity = determineGravity() * (movementParameters.gravityMultiplier ?: 1.0) * (1.0 - buoyancy) + var environmentVelocity = gravity * Starbound.TICK_TIME_ADVANCE + + if (isOnGround && (movementParameters.slopeSlidingFactor ?: 0.0) != 0.0 && surfaceSlope != Vector2d.ZERO) + environmentVelocity += -surfaceSlope * (surfaceSlope.x * surfaceSlope.y) * (movementParameters.slopeSlidingFactor ?: 0.0) + + newVelocity += environmentVelocity + } + + if (movementParameters.frictionEnabled == true) { + var refVel = Vector2d.ZERO + var friction = liquidPercentage * (movementParameters.liquidFriction ?: 0.0).coerceIn(0.0, 1.0) + (1.0 - liquidPercentage) * (movementParameters.airFriction ?: 0.0).coerceIn(0.0, 1.0) + + if (isOnGround) { + friction = friction.coerceAtLeast(movementParameters.groundFriction ?: 0.0) + refVel = surfaceVelocity + } + + // The equation for friction here is effectively: + // frictionForce = friction * (refVel - velocity) + // but it is applied here as a multiplicative factor from [0, 1] so it does + // not induce oscillation at very high friction and so it cannot be + // negative. + val frictionFactor = (friction / mass * Starbound.TICK_TIME_ADVANCE).coerceIn(0.0, 1.0) + newVelocity = linearInterpolation(frictionFactor, newVelocity, refVel) + } + + velocity = newVelocity + updateForceRegions() + } + + protected data class CollisionSeparation( + var correction: Vector2d = Vector2d.ZERO, + var solutionFound: Boolean = false, + var collisionType: CollisionType = CollisionType.NONE, + var axis: Vector2d = Vector2d.POSITIVE_Y, + var movingCollisionId: Int? = null, // TODO + ) + + protected data class CollisionResult( + val movement: Vector2d = Vector2d.ZERO, + val correction: Vector2d = Vector2d.ZERO, + var movingCollisionId: Int? = null, // TODO + var isStuck: Boolean = false, + var isOnGround: Boolean = false, + var groundSlope: Vector2d = Vector2d.ZERO, + var collisionType: CollisionType = CollisionType.NULL, + ) : Comparable { + override fun compareTo(other: CollisionResult): Int { + return movement.lengthSquared.compareTo(other.movement.lengthSquared) + } + } + + protected fun collisionSweep( + body: Poly, staticBodies: List, + movement: Vector2d, ignorePlatforms: Boolean, + slopeCorrection: Boolean, maximumCorrection: Double, + maximumPlatformCorrection: Double, sortCenter: Vector2d + ): CollisionResult { + val translatedBody = body + movement + var checkBody = translatedBody + var maxCollided = CollisionType.NONE + + var separation = CollisionSeparation() + var totalCorrection = Vector2d.ZERO + var movingCollisionId: Int? = null + + val sorted = staticBodies.stream() + .map { it to (it.poly.aabb.centre - sortCenter).lengthSquared } + .sorted { o1, o2 -> o1.second.compareTo(o2.second) } + .map { it.first } + .toList() + + if (slopeCorrection) { + // Starbound: First try separating with our ground sliding cheat. + separation = collisionSeparate(checkBody, sorted, ignorePlatforms, maximumPlatformCorrection, true, Entity.SEPARATION_TOLERANCE) + totalCorrection += separation.correction + checkBody += separation.correction + maxCollided = maxCollided.maxOf(separation.collisionType) + movingCollisionId = separation.movingCollisionId + + val upwardResult = movement + separation.correction + val upwardMagnitude = upwardResult.length + val upwardUnit = upwardResult / upwardMagnitude + // Starbound: Angle off of horizontal (minimum of either direction) + val horizontalAngle = acos(Vector2d.POSITIVE_X.dot(upwardUnit)).coerceAtMost(acos(Vector2d.NEGATIVE_X.dot(upwardUnit))) + + // Starbound: We need to make sure that even if we found a solution with the sliding + // Starbound: cheat, we are not beyond the angle and correction limits for the ground + // Starbound: cheat correction. + separation.solutionFound = separation.solutionFound && (upwardMagnitude < 0.2 || horizontalAngle < PI / 3.0) && totalCorrection.length <= maximumCorrection + + // KStarbound: if we got pushed into world geometry, then consider slide cheat didn't find a solution + if (separation.solutionFound) { + separation.solutionFound = staticBodies.all { it.poly.intersect(checkBody).let { it == null || it.penetration.absoluteValue <= Entity.SEPARATION_TOLERANCE } } + } + } + + if (!separation.solutionFound) { + checkBody = translatedBody + totalCorrection = Vector2d.ZERO + movingCollisionId = null + + for (i in 0 until Entity.SEPARATION_STEPS) { + separation = collisionSeparate(checkBody, sorted, ignorePlatforms, maximumPlatformCorrection, false, Entity.SEPARATION_TOLERANCE) + totalCorrection += separation.correction + checkBody += separation.correction + maxCollided = maxCollided.maxOf(separation.collisionType) + + if (totalCorrection.length >= maximumCorrection) { + separation.solutionFound = false + break + } else if (separation.solutionFound) { + break + } + } + } + + if (!separation.solutionFound && movement != Vector2d.ZERO) { + checkBody = body + totalCorrection = -movement + + for (i in 0 until Entity.SEPARATION_STEPS) { + separation = collisionSeparate(checkBody, sorted, true, maximumPlatformCorrection, false, Entity.SEPARATION_TOLERANCE) + totalCorrection += separation.correction + checkBody += separation.correction + maxCollided = maxCollided.maxOf(separation.collisionType) + + if (totalCorrection.length >= maximumCorrection) { + separation.solutionFound = false + break + } else if (separation.solutionFound) { + break + } + } + } + + if (separation.solutionFound) { + val result = CollisionResult( + movement = movement + totalCorrection, + correction = totalCorrection, + isStuck = false, + isOnGround = -totalCorrection.dot(determineGravity()) > Entity.SEPARATION_TOLERANCE, + movingCollisionId = movingCollisionId, + collisionType = maxCollided, + // groundSlope = Vector2d.POSITIVE_Y, + groundSlope = separation.axis + ) + + // TODO: what they wanted to achieve with this? + /*if (result.isOnGround) { + // If we are on the ground and need to find the ground slope, look for a + // vertex on the body being moved that is touching an edge of one of the + // collision polys. We only want a slope to be produced from an edge of + // colision geometry, not an edge of the colliding body. Pick the + // touching edge that is the most horizontally overlapped with the + // geometry, rather than off to the side. + var maxSideHorizontalOverlap = 0.0 + var touchingBounds = checkBody.aabb.enlarge(SEPARATION_TOLERANCE, SEPARATION_TOLERANCE) + + for (poly in staticBodies) { + for (edge in poly.poly.edges) { + + } + } + }*/ + + return result + } else { + return CollisionResult(Vector2d.ZERO, -movement, null, true, true, Vector2d.POSITIVE_Y, maxCollided) + } + } + + protected fun collisionSeparate( + poly: Poly, staticBodies: List, + ignorePlatforms: Boolean, maximumPlatformCorrection: Double, + upward: Boolean, separationTolerance: Double + ): CollisionSeparation { + val separation = CollisionSeparation() + var intersects = false + var correctedPoly = poly + + for (body in staticBodies) { + if (ignorePlatforms && body.type === CollisionType.PLATFORM) + continue + + var result = if (upward) + correctedPoly.intersect(body.poly, Vector2d.POSITIVE_Y, false) + else if (body.type == CollisionType.PLATFORM) + correctedPoly.intersect(body.poly, Vector2d.POSITIVE_Y, true) + else + correctedPoly.intersect(body.poly) + + if (body.type === CollisionType.PLATFORM && result != null && (result.penetration <= 0.0 || result.penetration > maximumPlatformCorrection)) + result = null + + if (result != null) { + intersects = true + correctedPoly += result.vector + separation.correction += result.vector + separation.collisionType = separation.collisionType.maxOf(body.type) + } + } + + separation.solutionFound = true + + if (intersects) { + for (body in staticBodies) { + if (body.type === CollisionType.PLATFORM) + continue + + val result = correctedPoly.intersect(body.poly) + + if (result != null && result.penetration > separationTolerance) { + separation.collisionType = separation.collisionType.maxOf(body.type) + separation.solutionFound = false + break + } + } + } + + return separation + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt deleted file mode 100644 index 71373a69..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt +++ /dev/null @@ -1,19 +0,0 @@ -package ru.dbotthepony.kstarbound.world.entities - -import ru.dbotthepony.kstarbound.world.World - -abstract class ActorEntity(world: World<*, *>) : Entity(world) { - var isWalking = false - var isRunning = false - var isCrouching = false - var isFlying = false - var isFalling = false - var canJump = false - var isJumping = false - var isGroundMovement = false - var isLiquidMovement = false - - override fun move() { - super.move() - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt index 3ebb0b82..11a7ce83 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Entity.kt @@ -1,27 +1,13 @@ package ru.dbotthepony.kstarbound.world.entities -import ru.dbotthepony.kstarbound.GlobalDefaults -import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.StarboundClient -import ru.dbotthepony.kstarbound.defs.BaseMovementParameters -import ru.dbotthepony.kstarbound.defs.ignorePlatformCollision -import ru.dbotthepony.kstarbound.defs.restDuration import ru.dbotthepony.kstarbound.util.MailboxExecutorService import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.World -import ru.dbotthepony.kstarbound.world.physics.CollisionPoly -import ru.dbotthepony.kstarbound.world.physics.CollisionType -import ru.dbotthepony.kstarbound.world.physics.Poly -import ru.dbotthepony.kvector.util.linearInterpolation import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.Vector2d -import ru.dbotthepony.kvector.vector.times -import java.util.EnumSet import kotlin.concurrent.withLock -import kotlin.math.PI -import kotlin.math.absoluteValue -import kotlin.math.acos abstract class Entity(val world: World<*, *>) { var chunk: Chunk<*, *>? = null @@ -63,7 +49,6 @@ abstract class Entity(val world: World<*, *>) { val old = field field = Vector2d(world.x.cell(value.x), world.y.cell(value.y)) - physicsSleepTicks = 0 if (isSpawned && !isRemoved) { val oldChunkPos = world.chunkFromCell(old) @@ -75,53 +60,10 @@ abstract class Entity(val world: World<*, *>) { } } - var velocity = Vector2d.ZERO - set(value) { - field = value - physicsSleepTicks = 0 - } + abstract val movement: AbstractMovementController - // Movement variables - var isZeroGravity = false - private set - - var isOnGround = false - private set - var isColliding = false - private set - var isCollisionStuck = false - private set - var isCollidingWithNull = false - private set - var stickingDirection: Double? = null - private set - var surfaceSlope = Vector2d.ZERO - private set - var surfaceVelocity = Vector2d.ZERO - private set - var collisionCorrection = Vector2d.ZERO - private set - var liquidPercentage = 0.0 - private set - - // Movement parameters - open val movementParameters: BaseMovementParameters = GlobalDefaults.movementParameters - - var gravityMultiplier = 1.0 - var isGravityDisabled = false - - var mass = 1.0 - set(value) { - require(value > 0.0) { "Invalid mass: $value" } - field = value - } - - var physicsSleepTicks = 0 val mailbox = MailboxExecutorService(world.mailbox.thread) - protected val hitboxes = ArrayList() - protected val collisionFilter: EnumSet = EnumSet.of(CollisionType.NONE) - /** * true - whitelist, false - blacklist */ @@ -186,419 +128,15 @@ abstract class Entity(val world: World<*, *>) { } - fun updateLiquidPercentage() { - - } - - fun updateForceRegions() { - - } - - fun determineGravity(): Vector2d { - if (isZeroGravity || isGravityDisabled) - return Vector2d.ZERO - - return world.gravityAt(position) - } - - /** - * this function is executed in parallel - */ - // TODO: Ghost collisions occur, where objects trip on edges - open fun move() { - isZeroGravity = isGravityDisabled || gravityMultiplier == 0.0 || determineGravity().lengthSquared == 0.0 - - if (!isZeroGravity) - velocity += world.gravity * Starbound.TICK_TIME_ADVANCE - - movementParameters.speedLimit?.let { - if (velocity.length > it) - velocity = velocity.unitVector * it - } - - // TODO: Here: moving platforms sticky code - - if (hitboxes.isEmpty() || movementParameters.collisionEnabled != true) { - position += velocity * Starbound.TICK_TIME_ADVANCE - surfaceSlope = Vector2d.POSITIVE_Y - surfaceVelocity = Vector2d.ZERO - isOnGround = false - stickingDirection = null - isColliding = false - isCollidingWithNull = false - isCollisionStuck = false - return - } - - var steps = 1 - - movementParameters.maxMovementPerStep?.let { - steps = (velocity.length * Starbound.TICK_TIME_ADVANCE / it).toInt() + 1 - } - - var relativeVelocity = if (physicsSleepTicks > 0) { - physicsSleepTicks-- - Vector2d.ZERO - } else { - velocity - } - - val originalMovement = relativeVelocity * Starbound.TICK_TIME_ADVANCE - surfaceSlope = Vector2d.POSITIVE_Y - // TODO: Here: moving platforms sticky code - - val dt = Starbound.TICK_TIME_ADVANCE / steps - - for (step in 0 until steps) { - val velocityMagnitude = relativeVelocity.length - val velocityDirection = relativeVelocity / velocityMagnitude - val movement = relativeVelocity * dt - - val ignorePlatforms = movementParameters.ignorePlatformCollision || relativeVelocity.y > 0.0 - val maximumCorrection = movementParameters.maximumCorrection ?: 0.0 - val maximumPlatformCorrection = (movementParameters.maximumPlatformCorrection ?: Double.POSITIVE_INFINITY) + - (movementParameters.maximumPlatformCorrectionVelocityFactor ?: 0.0) * velocityMagnitude - - val localHitboxes = hitboxes.map { it + position } - val aabb = localHitboxes.stream().map { it.aabb }.reduce(AABB::combine).get() - var queryBounds = aabb.enlarge(maximumCorrection, maximumCorrection) - queryBounds = queryBounds.combine(queryBounds + movement) - - val polies = world.queryCollisions(queryBounds).filter { - if (collisionFilterMode) - it.type in collisionFilter - else - it.type !in collisionFilter - } - - val results = ArrayList(localHitboxes.size) - - for (hitbox in localHitboxes) { - results.add(collisionSweep(hitbox, polies, movement, ignorePlatforms, (movementParameters.enableSurfaceSlopeCorrection ?: false) && !isZeroGravity, maximumCorrection, maximumPlatformCorrection, aabb.centre)) - } - - val result = results.minOrNull()!! - position += result.movement - - if (result.collisionType == CollisionType.NULL) { - isCollidingWithNull = true - break - } else { - isCollidingWithNull = false - } - - val correction = result.correction - val normCorrection = correction.unitVector - surfaceSlope = result.groundSlope - collisionCorrection = result.correction - isColliding = correction != Vector2d.ZERO || result.isStuck - isOnGround = !isZeroGravity && result.isOnGround - isCollisionStuck = result.isStuck - - // If we have collided, apply either sticky or normal (bouncing) collision physics - if (correction != Vector2d.ZERO) { - if (movementParameters.stickyCollision == true && result.collisionType !== CollisionType.SLIPPERY) { - // When sticking, cancel all velocity and apply stickyForce in the - // opposite of the direction of collision correction. - relativeVelocity = -normCorrection * (movementParameters.stickyForce ?: 0.0) / mass * dt - stickingDirection = -normCorrection.toAngle() - break - } else { - stickingDirection = null - val correctionDirection = correction.unitVector - - if (movementParameters.bounceFactor != null && movementParameters.bounceFactor != 0.0) { - val adjustment = correctionDirection * (velocityMagnitude * (correctionDirection * -velocityDirection)) - relativeVelocity += adjustment + movementParameters.bounceFactor!! * adjustment - - if (movementParameters.stopOnFirstBounce == true) { - // When bouncing, stop integrating at the moment of bounce. This - // prevents the frame of contact from being missed due to multiple - // iterations per frame. - break - } - } else { - // Only adjust the velocity to the extent that the collision was - // caused by the velocity in each axis, to eliminate collision - // induced velocity in a platformery way (each axis considered - // independently). - - if (relativeVelocity.x < 0.0 && correction.x > 0.0) - relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / Starbound.TICK_TIME_ADVANCE).coerceAtMost(0.0)) - else if (relativeVelocity.x > 0.0 && correction.x < 0.0) - relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / Starbound.TICK_TIME_ADVANCE).coerceAtLeast(0.0)) - - if (relativeVelocity.y < 0.0 && correction.y > 0.0) - relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / Starbound.TICK_TIME_ADVANCE).coerceAtMost(0.0)) - else if (relativeVelocity.y > 0.0 && correction.y < 0.0) - relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / Starbound.TICK_TIME_ADVANCE).coerceAtLeast(0.0)) - } - } - } - } - - var newVelocity = relativeVelocity - - updateLiquidPercentage() - - // TODO: sticky collision update - - // In order to make control work accurately, passive forces need to be - // applied to velocity *after* integrating. This prevents control from - // having to account for one timestep of passive forces in order to result - // in the correct controlled movement. - if (!isZeroGravity && stickingDirection == null) { - val buoyancy = (movementParameters.liquidBuoyancy ?: 0.0).coerceIn(0.0, 1.0) + liquidPercentage + (movementParameters.airBuoyancy ?: 0.0).coerceIn(0.0, 1.0) * (1.0 - liquidPercentage) - val gravity = determineGravity() * (movementParameters.gravityMultiplier ?: 1.0) * (1.0 - buoyancy) - var environmentVelocity = gravity * Starbound.TICK_TIME_ADVANCE - - if (isOnGround && (movementParameters.slopeSlidingFactor ?: 0.0) != 0.0 && surfaceSlope != Vector2d.ZERO) - environmentVelocity += surfaceSlope * (surfaceSlope.x * surfaceSlope.y) * (movementParameters.slopeSlidingFactor ?: 0.0) - - newVelocity += environmentVelocity - } - - // If original movement was entirely (almost) in the direction of gravity - // and was entirely (almost) cancelled by collision correction, put the - // entity into rest for restDuration - if ( - physicsSleepTicks == 0 && - originalMovement.dot(determineGravity()) in 0.99 .. 1.01 && - collisionCorrection.dot(determineGravity()) in -1.01 .. -0.99 - ) { - physicsSleepTicks = movementParameters.restDuration - } - - if (movementParameters.frictionEnabled == true) { - var refVel = Vector2d.ZERO - var friction = liquidPercentage * (movementParameters.liquidFriction ?: 0.0).coerceIn(0.0, 1.0) + (1.0 - liquidPercentage) * (movementParameters.airFriction ?: 0.0).coerceIn(0.0, 1.0) - - if (isOnGround) { - friction = friction.coerceAtLeast(movementParameters.groundFriction ?: 0.0) - refVel = surfaceVelocity - } - - // The equation for friction here is effectively: - // frictionForce = friction * (refVel - velocity) - // but it is applied here as a multiplicative factor from [0, 1] so it does - // not induce oscillation at very high friction and so it cannot be - // negative. - val frictionFactor = (friction / mass * Starbound.TICK_TIME_ADVANCE).coerceIn(0.0, 1.0) - newVelocity = linearInterpolation(frictionFactor, newVelocity, refVel) - } - - velocity = newVelocity - updateForceRegions() - } - - protected data class CollisionSeparation( - var correction: Vector2d = Vector2d.ZERO, - var solutionFound: Boolean = false, - var collisionType: CollisionType = CollisionType.NONE, - var axis: Vector2d = Vector2d.POSITIVE_Y, - var movingCollisionId: Int? = null, // TODO - ) - - protected data class CollisionResult( - val movement: Vector2d = Vector2d.ZERO, - val correction: Vector2d = Vector2d.ZERO, - var movingCollisionId: Int? = null, // TODO - var isStuck: Boolean = false, - var isOnGround: Boolean = false, - var groundSlope: Vector2d = Vector2d.ZERO, - var collisionType: CollisionType = CollisionType.NULL, - ) : Comparable { - override fun compareTo(other: CollisionResult): Int { - return movement.lengthSquared.compareTo(other.movement.lengthSquared) - } - } - - protected fun collisionSweep( - body: Poly, staticBodies: List, - movement: Vector2d, ignorePlatforms: Boolean, - slopeCorrection: Boolean, maximumCorrection: Double, - maximumPlatformCorrection: Double, sortCenter: Vector2d - ): CollisionResult { - val translatedBody = body + movement - var checkBody = translatedBody - var maxCollided = CollisionType.NONE - - var separation = CollisionSeparation() - var totalCorrection = Vector2d.ZERO - var movingCollisionId: Int? = null - - val sorted = staticBodies.stream() - .map { it to (it.poly.aabb.centre - sortCenter).lengthSquared } - .sorted { o1, o2 -> o1.second.compareTo(o2.second) } - .map { it.first } - .toList() - - if (slopeCorrection) { - // Starbound: First try separating with our ground sliding cheat. - separation = collisionSeparate(checkBody, sorted, ignorePlatforms, maximumPlatformCorrection, true, SEPARATION_TOLERANCE) - totalCorrection += separation.correction - checkBody += separation.correction - maxCollided = maxCollided.maxOf(separation.collisionType) - movingCollisionId = separation.movingCollisionId - - val upwardResult = movement + separation.correction - val upwardMagnitude = upwardResult.length - val upwardUnit = upwardResult / upwardMagnitude - // Starbound: Angle off of horizontal (minimum of either direction) - val horizontalAngle = acos(Vector2d.POSITIVE_X.dot(upwardUnit)).coerceAtMost(acos(Vector2d.NEGATIVE_X.dot(upwardUnit))) - - // Starbound: We need to make sure that even if we found a solution with the sliding - // Starbound: cheat, we are not beyond the angle and correction limits for the ground - // Starbound: cheat correction. - separation.solutionFound = separation.solutionFound && (upwardMagnitude < 0.2 || horizontalAngle < PI / 3.0) && totalCorrection.length <= maximumCorrection - - // KStarbound: if we got pushed into world geometry, then consider slide cheat didn't find a solution - if (separation.solutionFound) { - separation.solutionFound = staticBodies.all { it.poly.intersect(checkBody).let { it == null || it.penetration.absoluteValue <= SEPARATION_TOLERANCE } } - } - } - - if (!separation.solutionFound) { - checkBody = translatedBody - totalCorrection = Vector2d.ZERO - movingCollisionId = null - - for (i in 0 until SEPARATION_STEPS) { - separation = collisionSeparate(checkBody, sorted, ignorePlatforms, maximumPlatformCorrection, false, SEPARATION_TOLERANCE) - totalCorrection += separation.correction - checkBody += separation.correction - maxCollided = maxCollided.maxOf(separation.collisionType) - - if (totalCorrection.length >= maximumCorrection) { - separation.solutionFound = false - break - } else if (separation.solutionFound) { - break - } - } - } - - if (!separation.solutionFound && movement != Vector2d.ZERO) { - checkBody = body - totalCorrection = -movement - - for (i in 0 until SEPARATION_STEPS) { - separation = collisionSeparate(checkBody, sorted, true, maximumPlatformCorrection, false, SEPARATION_TOLERANCE) - totalCorrection += separation.correction - checkBody += separation.correction - maxCollided = maxCollided.maxOf(separation.collisionType) - - if (totalCorrection.length >= maximumCorrection) { - separation.solutionFound = false - break - } else if (separation.solutionFound) { - break - } - } - } - - if (separation.solutionFound) { - val result = CollisionResult( - movement = movement + totalCorrection, - correction = totalCorrection, - isStuck = false, - isOnGround = -totalCorrection.dot(determineGravity()) > SEPARATION_TOLERANCE, - movingCollisionId = movingCollisionId, - collisionType = maxCollided, - // groundSlope = Vector2d.POSITIVE_Y, - groundSlope = separation.axis - ) - - // TODO: what they wanted to achieve with this? - /*if (result.isOnGround) { - // If we are on the ground and need to find the ground slope, look for a - // vertex on the body being moved that is touching an edge of one of the - // collision polys. We only want a slope to be produced from an edge of - // colision geometry, not an edge of the colliding body. Pick the - // touching edge that is the most horizontally overlapped with the - // geometry, rather than off to the side. - var maxSideHorizontalOverlap = 0.0 - var touchingBounds = checkBody.aabb.enlarge(SEPARATION_TOLERANCE, SEPARATION_TOLERANCE) - - for (poly in staticBodies) { - for (edge in poly.poly.edges) { - - } - } - }*/ - - return result - } else { - return CollisionResult(Vector2d.ZERO, -movement, null, true, true, Vector2d.POSITIVE_Y, maxCollided) - } - } - - protected fun collisionSeparate( - poly: Poly, staticBodies: List, - ignorePlatforms: Boolean, maximumPlatformCorrection: Double, - upward: Boolean, separationTolerance: Double - ): CollisionSeparation { - val separation = CollisionSeparation() - var intersects = false - var correctedPoly = poly - - for (body in staticBodies) { - if (ignorePlatforms && body.type === CollisionType.PLATFORM) - continue - - var result = if (upward) - correctedPoly.intersect(body.poly, Vector2d.POSITIVE_Y, false) - else if (body.type == CollisionType.PLATFORM) - correctedPoly.intersect(body.poly, Vector2d.POSITIVE_Y, true) - else - correctedPoly.intersect(body.poly) - - if (body.type === CollisionType.PLATFORM && result != null && (result.penetration <= 0.0 || result.penetration > maximumPlatformCorrection)) - result = null - - if (result != null) { - intersects = true - correctedPoly += result.vector - separation.correction += result.vector - separation.collisionType = separation.collisionType.maxOf(body.type) - } - } - - separation.solutionFound = true - - if (intersects) { - for (body in staticBodies) { - if (body.type === CollisionType.PLATFORM) - continue - - val result = correctedPoly.intersect(body.poly) - - if (result != null && result.penetration > separationTolerance) { - separation.collisionType = separation.collisionType.maxOf(body.type) - separation.solutionFound = false - break - } - } - } - - return separation - } - - protected open fun onTouch(velocity: Vector2d, normal: Vector2d, poly: CollisionPoly) { - - } - open fun render(client: StarboundClient = StarboundClient.current()) { - hitboxes.forEach { (it + position).render(client) } + val hitboxes = movement.localHitboxes.toList() + if (hitboxes.isEmpty()) return + + hitboxes.forEach { it.render(client) } world.queryCollisions( - hitboxes.stream().map { (it + position).aabb }.reduce(AABB::combine).get().enlarge(2.0, 2.0) - ).filter { - if (collisionFilterMode) - it.type in collisionFilter - else - it.type !in collisionFilter - }.forEach { it.poly.render(client, BLOCK_COLLISION_COLOR) } + hitboxes.stream().map { it.aabb }.reduce(AABB::combine).get().enlarge(2.0, 2.0) + ).filter(movement::shouldCollideWithBody).forEach { it.poly.render(client, BLOCK_COLLISION_COLOR) } } open var maxHealth = 0.0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EntityActorMovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EntityActorMovementController.kt new file mode 100644 index 00000000..35dea239 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EntityActorMovementController.kt @@ -0,0 +1,69 @@ +package ru.dbotthepony.kstarbound.world.entities + +import ru.dbotthepony.kstarbound.GlobalDefaults +import ru.dbotthepony.kstarbound.defs.ActorMovementParameters +import ru.dbotthepony.kstarbound.defs.MovementParameters +import ru.dbotthepony.kstarbound.defs.player.ActorMovementModifiers +import ru.dbotthepony.kstarbound.world.Direction +import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kstarbound.world.physics.Poly +import ru.dbotthepony.kvector.vector.Vector2d + +class EntityActorMovementController(val entity: Entity) : AbstractActorMovementController() { + override val world: World<*, *> by entity::world + override var position: Vector2d by entity::position + override var actorMovementParameters: ActorMovementParameters = GlobalDefaults.actorMovementParameters + + override var lastControlDown: Boolean = false + override var fallThroughSustain: Int = 0 + override var targetHorizontalAmbulatingVelocity: Double = 0.0 + + override var controlRun: Boolean = false + override var controlCrouch: Boolean = false + override var controlDown: Boolean = false + override var controlFly: Vector2d? = null + override var isFlying: Boolean = false + override var isFalling: Boolean = false + override var canJump: Boolean = true + override var controlJump: Boolean = false + override var isGroundMovement: Boolean = false + override var isLiquidMovement: Boolean = false + override var controlPathMove: Pair? = null + override var pathMoveResult: Pair? = null + override var controlMove: Direction? = null + override var movementModifiers: ActorMovementModifiers = ActorMovementModifiers.EMPTY + + override var controlActorMovementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY + override var controlMovementModifiers: ActorMovementModifiers = ActorMovementModifiers.EMPTY + + override var isOnGround: Boolean = false + override var isColliding: Boolean = false + override var isCollisionStuck: Boolean = false + override var isCollidingWithNull: Boolean = false + override var stickingDirection: Double? = null + override var surfaceSlope: Vector2d = Vector2d.ZERO + override var surfaceVelocity: Vector2d = Vector2d.ZERO + override var collisionCorrection: Vector2d = Vector2d.ZERO + override var liquidPercentage: Double = 0.0 + override var appliedForceRegion: Boolean = false + override var isZeroGravity: Boolean = false + + override var controlRotationRate: Double = 0.0 + override var controlAcceleration: Vector2d = Vector2d.ZERO + override var controlForce: Vector2d = Vector2d.ZERO + override var rotation: Double = 0.0 + + override var lastControlCrouch: Boolean = false + override var controlFace: Direction? = null + override var isRunning: Boolean = false + override var isWalking: Boolean = false + override var isCrouching: Boolean = false + override var isJumping: Boolean = false + override var lastControlJump: Boolean = false + override var controlJumpAnyway: Boolean = false + override val approachVelocities: MutableList = ArrayList() + override val approachVelocityAngles: MutableList = ArrayList() + override var movingDirection: Direction? = null + override var facingDirection: Direction? = null + override var anchorEntity: Entity? = null +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EntityMovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EntityMovementController.kt new file mode 100644 index 00000000..f1a772ba --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EntityMovementController.kt @@ -0,0 +1,26 @@ +package ru.dbotthepony.kstarbound.world.entities + +import ru.dbotthepony.kstarbound.GlobalDefaults +import ru.dbotthepony.kstarbound.defs.MovementParameters +import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kstarbound.world.physics.Poly +import ru.dbotthepony.kvector.vector.Vector2d + +class EntityMovementController(val entity: Entity) : AbstractMovementController() { + override val world: World<*, *> by entity::world + override var position: Vector2d by entity::position + override var movementParameters: MovementParameters = GlobalDefaults.movementParameters + + override var isOnGround: Boolean = false + override var isColliding: Boolean = false + override var isCollisionStuck: Boolean = false + override var isCollidingWithNull: Boolean = false + override var stickingDirection: Double? = null + override var surfaceSlope: Vector2d = Vector2d.ZERO + override var surfaceVelocity: Vector2d = Vector2d.ZERO + override var collisionCorrection: Vector2d = Vector2d.ZERO + override var liquidPercentage: Double = 0.0 + override var appliedForceRegion: Boolean = false + override var isZeroGravity: Boolean = false + override var rotation: Double = 0.0 +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt index 1747d2c5..ed4e1828 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ItemEntity.kt @@ -1,13 +1,16 @@ package ru.dbotthepony.kstarbound.world.entities import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition +import ru.dbotthepony.kstarbound.util.Either import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.physics.Poly import ru.dbotthepony.kvector.util2d.AABB import ru.dbotthepony.kvector.vector.Vector2d class ItemEntity(world: World<*, *>, val def: IItemDefinition) : Entity(world) { + override val movement = EntityMovementController(this) + init { - hitboxes.add(Poly(AABB.rectangle(Vector2d.ZERO, 0.75, 0.75))) + movement.movementParameters = movement.movementParameters.copy(collisionPoly = Either.left(Poly(AABB.rectangle(Vector2d.ZERO, 0.75, 0.75)))) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PathController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PathController.kt new file mode 100644 index 00000000..c895bc6e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PathController.kt @@ -0,0 +1,24 @@ +package ru.dbotthepony.kstarbound.world.entities + +import ru.dbotthepony.kstarbound.world.Direction +import ru.dbotthepony.kstarbound.world.World +import ru.dbotthepony.kvector.vector.Vector2d + +class PathController(val world: World<*, *>, var edgeTimer: Double = 0.0) { + var startPosition: Vector2d? = null + private set + var endPosition: Vector2d? = null + private set + var controlFace: Direction? = null + private set + + fun reset() { + edgeTimer = 0.0 + startPosition = null + endPosition = null + controlFace = null + } + + val isPathfinding: Boolean + get() = TODO() +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt index 6a67a19c..832d2f84 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt @@ -1,18 +1,45 @@ package ru.dbotthepony.kstarbound.world.entities -import ru.dbotthepony.kstarbound.GlobalDefaults import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.ActorMovementParameters +import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.util.Either import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.physics.Poly class PlayerEntity(world: World<*, *>) : Entity(world) { - override val movementParameters: ActorMovementParameters = GlobalDefaults.actorMovementParameters + override val movement = EntityActorMovementController(this) init { - GlobalDefaults.actorMovementParameters.standingPoly?.let { - //hitboxes.add(it) - hitboxes.add(Starbound.gson.fromJson("""[ [-0.75, -2.0], [-0.35, -2.5], [0.35, -2.5], [0.75, -2.0], [0.75, 0.65], [0.35, 1.22], [-0.35, 1.22], [-0.75, 0.65] ]""", Poly::class.java)) - } + movement.actorMovementParameters = movement.actorMovementParameters.merge( + Starbound.gson.fromJson(""" + { + "standingPoly" : [ [-0.75, -2.0], [-0.35, -2.5], [0.35, -2.5], [0.75, -2.0], [0.75, 0.65], [0.35, 1.22], [-0.35, 1.22], [-0.75, 0.65] ], + "crouchingPoly" : [ [-0.75, -2.0], [-0.35, -2.5], [0.35, -2.5], [0.75, -2.0], [0.75, -1], [0.35, -0.5], [-0.35, -0.5], [-0.75, -1] ], + "mass" : 1.6, + + // should keep the player from teleporting through walls + "maximumCorrection" : 3, + "maxMovementPerStep" : 0.4, + + "liquidFriction" : 13.0, + "normalGroundFriction" : 35.0, + + "groundForce" : 250.0, + "airForce" : 50.0, + "liquidForce" : 80.0 + } + """.trimIndent(), ActorMovementParameters::class.java) + ).merge(Starbound.gson.fromJson(""" + { + "flySpeed" : 0, + "airFriction" : 0.5, + "airJumpProfile" : { + "jumpSpeed" : 23.0, + "jumpInitialPercentage" : 0.75, + "jumpHoldTime" : 0.2 + } + } + """.trimIndent(), ActorMovementParameters::class.java)) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt index 82c7f241..16ece1ee 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt @@ -159,6 +159,9 @@ class Poly private constructor(val edges: ImmutableList, val vertices: Imm } fun rotate(radians: Double): Poly { + if (radians == 0.0) + return this + val sin = sin(radians) val cos = cos(radians) diff --git a/src/main/resources/starbound.png b/src/main/resources/starbound.png index adceefc5..792e7694 100644 Binary files a/src/main/resources/starbound.png and b/src/main/resources/starbound.png differ diff --git a/src/main/resources/starbound.svg b/src/main/resources/starbound.svg new file mode 100644 index 00000000..87ea9605 --- /dev/null +++ b/src/main/resources/starbound.svg @@ -0,0 +1,11 @@ + +Made with Pixels to Svg https://codepen.io/shshaw/pen/XbxvNj + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/starbound.xcf b/src/main/resources/starbound.xcf new file mode 100644 index 00000000..97b12bf6 Binary files /dev/null and b/src/main/resources/starbound.xcf differ diff --git a/src/main/resources/starbound_icon.png b/src/main/resources/starbound_icon.png new file mode 100644 index 00000000..db183a94 Binary files /dev/null and b/src/main/resources/starbound_icon.png differ diff --git a/src/main/resources/starbound_icon.xcf b/src/main/resources/starbound_icon.xcf new file mode 100644 index 00000000..df5c9f73 Binary files /dev/null and b/src/main/resources/starbound_icon.xcf differ