From 4db69db0cd9c8651cc44acb0c276c3233ae80ece Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Fri, 1 Dec 2023 13:28:25 +0700 Subject: [PATCH] Regular movement code port from original engine --- build.gradle.kts | 2 +- .../dbotthepony/kstarbound/GlobalDefaults.kt | 2 +- .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 2 +- .../kstarbound/defs/BaseMovementParameters.kt | 137 +++--- .../kstarbound/defs/MovementParameters.kt | 16 +- .../defs/PlayerMovementParameters.kt | 66 +++ .../defs/monster/MonsterTypeDefinition.kt | 2 +- .../defs/player/PlayerDefinition.kt | 1 + .../defs/player/PlayerMovementParameters.kt | 69 --- .../ru/dbotthepony/kstarbound/world/World.kt | 8 + .../kstarbound/world/entities/Entity.kt | 450 ++++++++++++++++-- .../kstarbound/world/entities/PlayerEntity.kt | 12 +- .../kstarbound/world/physics/CollisionType.kt | 9 + .../kstarbound/world/physics/Poly.kt | 50 +- 14 files changed, 627 insertions(+), 199 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerMovementParameters.kt delete mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerMovementParameters.kt diff --git a/build.gradle.kts b/build.gradle.kts index 4f4bd742..669d4aa1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -82,7 +82,7 @@ dependencies { implementation("net.java.dev.jna:jna:5.13.0") implementation("com.github.jnr:jnr-ffi:2.2.13") - implementation("ru.dbotthepony:kvector:2.11.1") + implementation("ru.dbotthepony:kvector:2.12.0") implementation("com.github.ben-manes.caffeine:caffeine:3.1.5") implementation("org.classdump.luna:luna-all-shaded:0.4.1") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt index 57ed26a7..d10df8b5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt @@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.defs.ClientConfigParameters import ru.dbotthepony.kstarbound.defs.MovementParameters -import ru.dbotthepony.kstarbound.defs.player.PlayerMovementParameters +import ru.dbotthepony.kstarbound.defs.PlayerMovementParameters import ru.dbotthepony.kstarbound.util.AssetPathStack import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinTask diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 2351e9b5..079b786c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -129,7 +129,7 @@ fun main() { val rand = Random() - for (i in 0 until 0) { + for (i in 0 until 128) { val item = ItemEntity(client.world!!, Registries.items.keys.values.random().value) item.position = Vector2d(225.0 - i, 785.0) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/BaseMovementParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/BaseMovementParameters.kt index f6144b97..4620374a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/BaseMovementParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/BaseMovementParameters.kt @@ -5,94 +5,97 @@ 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) -interface BaseMovementParameters { - val mass: KOptional - val gravityMultiplier: KOptional - val liquidBuoyancy: KOptional - val airBuoyancy: KOptional - val bounceFactor: KOptional +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: KOptional + 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: KOptional - val slopeSlidingFactor: KOptional + val enableSurfaceSlopeCorrection: Boolean? + val slopeSlidingFactor: Double? // ignored - val maxMovementPerStep: KOptional - val maximumCorrection: KOptional - val speedLimit: KOptional + val maxMovementPerStep: Double? + val maximumCorrection: Double? + val speedLimit: Double? - val stickyCollision: KOptional - val stickyForce: KOptional + val stickyCollision: Boolean? + val stickyForce: Double? - val airFriction: KOptional - val liquidFriction: KOptional - val groundFriction: KOptional + val airFriction: Double? + val liquidFriction: Double? + val groundFriction: Double? - val collisionEnabled: KOptional - val frictionEnabled: KOptional - val gravityEnabled: KOptional + val collisionEnabled: Boolean? + val frictionEnabled: Boolean? + val gravityEnabled: Boolean? - val maximumPlatformCorrection: KOptional - val maximumPlatformCorrectionVelocityFactor: KOptional + val maximumPlatformCorrection: Double? + val maximumPlatformCorrectionVelocityFactor: Double? - val physicsEffectCategories: KOptional> + val physicsEffectCategories: ImmutableSet? @JsonFactory data class Impl( - override val mass: KOptional = KOptional.empty(), - override val gravityMultiplier: KOptional = KOptional.empty(), - override val liquidBuoyancy: KOptional = KOptional.empty(), - override val airBuoyancy: KOptional = KOptional.empty(), - override val bounceFactor: KOptional = KOptional.empty(), - override val stopOnFirstBounce: KOptional = KOptional.empty(), - override val enableSurfaceSlopeCorrection: KOptional = KOptional.empty(), - override val slopeSlidingFactor: KOptional = KOptional.empty(), - override val maxMovementPerStep: KOptional = KOptional.empty(), - override val maximumCorrection: KOptional = KOptional.empty(), - override val speedLimit: KOptional = KOptional.empty(), - override val stickyCollision: KOptional = KOptional.empty(), - override val stickyForce: KOptional = KOptional.empty(), - override val airFriction: KOptional = KOptional.empty(), - override val liquidFriction: KOptional = KOptional.empty(), - override val groundFriction: KOptional = KOptional.empty(), - override val collisionEnabled: KOptional = KOptional.empty(), - override val frictionEnabled: KOptional = KOptional.empty(), - override val gravityEnabled: KOptional = KOptional.empty(), - override val maximumPlatformCorrection: KOptional = KOptional.empty(), - override val maximumPlatformCorrectionVelocityFactor: KOptional = KOptional.empty(), - override val physicsEffectCategories: KOptional> = KOptional.empty(), + 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.or(other.mass), - gravityMultiplier = gravityMultiplier.or(other.gravityMultiplier), - liquidBuoyancy = liquidBuoyancy.or(other.liquidBuoyancy), - airBuoyancy = airBuoyancy.or(other.airBuoyancy), - bounceFactor = bounceFactor.or(other.bounceFactor), - stopOnFirstBounce = stopOnFirstBounce.or(other.stopOnFirstBounce), - enableSurfaceSlopeCorrection = enableSurfaceSlopeCorrection.or(other.enableSurfaceSlopeCorrection), - slopeSlidingFactor = slopeSlidingFactor.or(other.slopeSlidingFactor), - maxMovementPerStep = maxMovementPerStep.or(other.maxMovementPerStep), - maximumCorrection = maximumCorrection.or(other.maximumCorrection), - speedLimit = speedLimit.or(other.speedLimit), - stickyCollision = stickyCollision.or(other.stickyCollision), - stickyForce = stickyForce.or(other.stickyForce), - airFriction = airFriction.or(other.airFriction), - liquidFriction = liquidFriction.or(other.liquidFriction), - groundFriction = groundFriction.or(other.groundFriction), - collisionEnabled = collisionEnabled.or(other.collisionEnabled), - frictionEnabled = frictionEnabled.or(other.frictionEnabled), - gravityEnabled = gravityEnabled.or(other.gravityEnabled), - maximumPlatformCorrection = maximumPlatformCorrection.or(other.maximumPlatformCorrection), - maximumPlatformCorrectionVelocityFactor = maximumPlatformCorrectionVelocityFactor.or(other.maximumPlatformCorrectionVelocityFactor), - physicsEffectCategories = physicsEffectCategories.or(other.physicsEffectCategories), + 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, ) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MovementParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MovementParameters.kt index b0edcbf4..f50e3682 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MovementParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/MovementParameters.kt @@ -10,18 +10,18 @@ data class MovementParameters( @JsonFlat val base: BaseMovementParameters.Impl = BaseMovementParameters.Impl(), - val discontinuityThreshold: KOptional = KOptional.empty(), - val collisionPoly: KOptional = KOptional.empty(), - val ignorePlatformCollision: KOptional = KOptional.empty(), - val restDuration: KOptional = KOptional.empty(), + val discontinuityThreshold: Float? = null, + val collisionPoly: Poly? = null, + val ignorePlatformCollision: Boolean? = null, + val restDuration: Int? = null, ) : BaseMovementParameters by base { fun merge(other: MovementParameters): MovementParameters { return MovementParameters( base = base.merge(other.base), - discontinuityThreshold = discontinuityThreshold.or(other.discontinuityThreshold), - collisionPoly = collisionPoly.or(other.collisionPoly), - ignorePlatformCollision = ignorePlatformCollision.or(other.ignorePlatformCollision), - restDuration = restDuration.or(other.restDuration), + discontinuityThreshold = discontinuityThreshold ?: other.discontinuityThreshold, + collisionPoly = collisionPoly ?: other.collisionPoly, + ignorePlatformCollision = ignorePlatformCollision ?: other.ignorePlatformCollision, + restDuration = restDuration ?: other.restDuration, ) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerMovementParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerMovementParameters.kt new file mode 100644 index 00000000..5b5455ef --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerMovementParameters.kt @@ -0,0 +1,66 @@ +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 PlayerMovementParameters( + @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: PlayerMovementParameters): PlayerMovementParameters { + return PlayerMovementParameters( + 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/monster/MonsterTypeDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt index 61a8d267..faabdd36 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/monster/MonsterTypeDefinition.kt @@ -7,7 +7,7 @@ import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.IScriptable import ru.dbotthepony.kstarbound.defs.IThingWithDescription -import ru.dbotthepony.kstarbound.defs.player.PlayerMovementParameters +import ru.dbotthepony.kstarbound.defs.PlayerMovementParameters 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/PlayerDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerDefinition.kt index 4af53605..f564cf39 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerDefinition.kt @@ -6,6 +6,7 @@ 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.PlayerMovementParameters 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/defs/player/PlayerMovementParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerMovementParameters.kt deleted file mode 100644 index 6d2d9848..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/PlayerMovementParameters.kt +++ /dev/null @@ -1,69 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.player - -import ru.dbotthepony.kstarbound.defs.BaseMovementParameters -import ru.dbotthepony.kstarbound.defs.JumpProfile -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.world.physics.Poly - -@JsonFactory -data class PlayerMovementParameters( - @JsonFlat - val base: BaseMovementParameters.Impl = BaseMovementParameters.Impl(), - - @JsonAlias("collisionPoly") - val standingPoly: KOptional = KOptional.empty(), - @JsonAlias("collisionPoly") - val crouchingPoly: KOptional = KOptional.empty(), - - val walkSpeed: KOptional = KOptional.empty(), - val runSpeed: KOptional = KOptional.empty(), - val flySpeed: KOptional = KOptional.empty(), - - val minimumLiquidPercentage: KOptional = KOptional.empty(), - val liquidImpedance: KOptional = KOptional.empty(), - val normalGroundFriction: KOptional = KOptional.empty(), - val ambulatingGroundFriction: KOptional = KOptional.empty(), - val groundForce: KOptional = KOptional.empty(), - val airForce: KOptional = KOptional.empty(), - val liquidForce: KOptional = KOptional.empty(), - - val airJumpProfile: JumpProfile = JumpProfile(), - val liquidJumpProfile: JumpProfile = JumpProfile(), - - val fallStatusSpeedMin: KOptional = KOptional.empty(), - val fallThroughSustainFrames: KOptional = KOptional.empty(), - - val groundMovementMinimumSustain: KOptional = KOptional.empty(), - val groundMovementMaximumSustain: KOptional = KOptional.empty(), - val groundMovementCheckDistance: KOptional = KOptional.empty(), - - val pathExploreRate: KOptional = KOptional.empty(), -) : BaseMovementParameters by base { - fun merge(other: PlayerMovementParameters): PlayerMovementParameters { - return PlayerMovementParameters( - base = base.merge(other.base), - standingPoly = standingPoly.or(other.standingPoly), - crouchingPoly = crouchingPoly.or(other.crouchingPoly), - walkSpeed = walkSpeed.or(other.walkSpeed), - runSpeed = runSpeed.or(other.runSpeed), - flySpeed = flySpeed.or(other.flySpeed), - minimumLiquidPercentage = minimumLiquidPercentage.or(other.minimumLiquidPercentage), - liquidImpedance = liquidImpedance.or(other.liquidImpedance), - normalGroundFriction = normalGroundFriction.or(other.normalGroundFriction), - ambulatingGroundFriction = ambulatingGroundFriction.or(other.ambulatingGroundFriction), - groundForce = groundForce.or(other.groundForce), - airForce = airForce.or(other.airForce), - liquidForce = liquidForce.or(other.liquidForce), - airJumpProfile = airJumpProfile.merge(other.airJumpProfile), - liquidJumpProfile = liquidJumpProfile.merge(other.liquidJumpProfile), - fallStatusSpeedMin = fallStatusSpeedMin.or(other.fallStatusSpeedMin), - fallThroughSustainFrames = fallThroughSustainFrames.or(other.fallThroughSustainFrames), - groundMovementMinimumSustain = groundMovementMinimumSustain.or(other.groundMovementMinimumSustain), - groundMovementMaximumSustain = groundMovementMaximumSustain.or(other.groundMovementMaximumSustain), - groundMovementCheckDistance = groundMovementCheckDistance.or(other.groundMovementCheckDistance), - ) - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 64aab434..19d82df0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -246,4 +246,12 @@ abstract class World, ChunkType : Chunk) { var chunk: Chunk<*, *>? = null @@ -76,6 +81,41 @@ abstract class Entity(val world: World<*, *>) { physicsSleepTicks = 0 } + // 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) @@ -87,8 +127,6 @@ abstract class Entity(val world: World<*, *>) { */ protected var collisionFilterMode = false - open var movementParameters: BaseMovementParameters = GlobalDefaults.movementParameters - /** * Whenever is this entity spawned in world ([spawn] called). * Doesn't mean entity still exists in world, check it with [isRemoved] @@ -148,78 +186,402 @@ 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() { - if (physicsSleepTicks > PHYSICS_TICKS_UNTIL_SLEEP) return - var physicsSleepTicks = physicsSleepTicks - velocity += world.gravity * Starbound.TICK_TIME_ADVANCE + isZeroGravity = isGravityDisabled || gravityMultiplier == 0.0 || determineGravity().lengthSquared == 0.0 - movementParameters.speedLimit.ifPresent { - if (velocity.length > it) { + if (!isZeroGravity) + velocity += world.gravity * Starbound.TICK_TIME_ADVANCE + + movementParameters.speedLimit?.let { + if (velocity.length > it) velocity = velocity.unitVector * it - } } - if (hitboxes.isEmpty()) { + // 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 } - val steps = roundTowardsPositiveInfinity(velocity.length / 10.0 / hitboxes.stream().map { it.aabb }.reduce(AABB::combine).get().let { it.width.coerceAtLeast(it.height).coerceAtLeast(0.1) }) + 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) { - position += velocity * dt + val velocityMagnitude = relativeVelocity.length + val velocityDirection = relativeVelocity / velocityMagnitude + val movement = relativeVelocity * dt - for (i in 0 until 10) { - val localHitboxes = hitboxes.map { it + position } + 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 polies = world.queryCollisions( - localHitboxes.stream().map { it.aabb }.reduce(AABB::combine).get().enlarge(2.0, 2.0) - ).filter { - if (collisionFilterMode) - it.type in collisionFilter - else - it.type !in collisionFilter - } + 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) - if (polies.isEmpty()) break + val polies = world.queryCollisions(queryBounds).filter { + if (collisionFilterMode) + it.type in collisionFilter + else + it.type !in collisionFilter + } - val intersects = ArrayList>() + val results = ArrayList(localHitboxes.size) - localHitboxes.forEach { hitbox -> - polies.forEach { poly -> hitbox.intersect(poly.poly)?.let { intersects.add(it to poly) } } - } + for (hitbox in localHitboxes) { + results.add(collisionSweep(hitbox, polies, movement, ignorePlatforms, (movementParameters.enableSurfaceSlopeCorrection ?: false) && !isZeroGravity, maximumCorrection, maximumPlatformCorrection, aabb.centre)) + } - if (intersects.isEmpty()) { + 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 { - val (max, data) = intersects.maxByOrNull { it.first }!! - // resolve collision - position += max.axis * max.penetration - // collision response - val response = max.axis * velocity.dot(max.axis * (1.0 + data.bounceFactor) * (1.0 + movementParameters.bounceFactor.orElse(0.0))) - velocity -= response + stickingDirection = null + val correctionDirection = correction.unitVector - val gravityDot = world.gravity.unitVector.dot(max.axis) - // impulse? - velocity += data.velocity * gravityDot * dt - // friction - velocity *= 1.0 - gravityDot.absoluteValue * 0.08 + if (movementParameters.bounceFactor != null && movementParameters.bounceFactor != 0.0) { + val adjustment = correctionDirection * (velocityMagnitude * (correctionDirection * -velocityDirection)) + relativeVelocity += adjustment + movementParameters.bounceFactor!! * adjustment - onTouch(response, max.axis, data) + 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)) + } } } } - if (velocity.lengthSquared < 0.25) { - physicsSleepTicks++ + 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 } - this.physicsSleepTicks = physicsSleepTicks + // 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) { @@ -249,5 +611,7 @@ abstract class Entity(val world: World<*, *>) { companion object { const val PHYSICS_TICKS_UNTIL_SLEEP = 16 val BLOCK_COLLISION_COLOR = RGBAColor(65, 179, 217) + const val SEPARATION_STEPS = 3 + const val SEPARATION_TOLERANCE = 0.001 } } 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 03ddb93b..94750c87 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt @@ -2,18 +2,18 @@ package ru.dbotthepony.kstarbound.world.entities import ru.dbotthepony.kstarbound.GlobalDefaults import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.BaseMovementParameters +import ru.dbotthepony.kstarbound.defs.PlayerMovementParameters import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.physics.Poly class PlayerEntity(world: World<*, *>) : Entity(world) { - init { - movementParameters = GlobalDefaults.playerMovementParameters + override val movementParameters: PlayerMovementParameters = GlobalDefaults.playerMovementParameters - GlobalDefaults.playerMovementParameters.standingPoly.ifPresent { + init { + GlobalDefaults.playerMovementParameters.standingPoly?.let { //hitboxes.add(it) - hitboxes.add(Starbound.gson.fromJson("""[ [0.5625, 1.9375], [1.0625, 1.4375], [1.0625, -2.5625], [0.5625, -3.0625], [-0.5625, -3.0625], [-1.0625, -2.5625], [-1.0625, 1.4375], [-0.5625, 1.9375] ]""", Poly::class.java)) - }.ifNotPresent { - throw IllegalStateException("No player collision poly") + 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)) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt index 3e7206b7..d3c731f5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/CollisionType.kt @@ -10,4 +10,13 @@ enum class CollisionType(val isEmpty: Boolean) { DYNAMIC(false), SLIPPERY(false), BLOCK(false); + + fun maxOf(other: CollisionType): CollisionType { + if (this === NULL || other === NULL) + return NULL + else if (this > other) + return this + else + return other + } } 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 ee02bedf..3853bc30 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt @@ -7,7 +7,6 @@ import com.google.gson.TypeAdapterFactory import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter -import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import org.lwjgl.opengl.GL11.GL_LINES import ru.dbotthepony.kstarbound.client.StarboundClient @@ -21,6 +20,8 @@ import ru.dbotthepony.kvector.util2d.intersectSegments import ru.dbotthepony.kvector.vector.RGBAColor import ru.dbotthepony.kvector.vector.Vector2d import kotlin.math.absoluteValue +import kotlin.math.cos +import kotlin.math.sin private fun calculateEdges(points: List): Pair, ImmutableList> { require(points.size >= 2) { "Provided poly is invalid (only ${points.size} points are defined)" } @@ -51,6 +52,13 @@ private fun calculateEdges(points: List): Pair, val vertices: Imm return Poly(edges.build(), vertices.build()) } + fun rotate(radians: Double): Poly { + val sin = sin(radians) + val cos = cos(radians) + + val edges = ImmutableList.Builder() + val vertices = ImmutableList.Builder() + + for (edge in this.edges) { + edges.add(Edge(rotate(edge.p0, sin, cos), rotate(edge.p1, sin, cos), rotate(edge.normal, sin, cos))) + } + + for (vertex in this.vertices) { + vertices.add(rotate(vertex, sin, cos)) + } + + return Poly(edges.build(), vertices.build()) + } + // min / max fun project(normal: Vector2d): IStruct2d { var min = vertices.first().dot(normal) @@ -157,7 +183,10 @@ class Poly private constructor(val edges: ImmutableList, val vertices: Imm return Vector2d(min, max) } - fun intersect(other: Poly): Penetration? { + /** + * @param axis separate ONLY along specified axis, that said, if axis is positive Y and we have collision, and closest separation axis is to up right, we instead separate only up until we no longer collide. + */ + fun intersect(other: Poly, axis: Vector2d? = null, strictAxis: Boolean = false): Penetration? { if (!aabb.intersectWeak(other.aabb)) return null @@ -165,6 +194,10 @@ class Poly private constructor(val edges: ImmutableList, val vertices: Imm edges.forEach { normals.add(it.normal) } other.edges.forEach { normals.add(it.normal) } + if (axis != null) { + normals.removeIf { it.dot(axis) == 0.0 } + } + val intersections = ArrayList() for (normal in normals) { @@ -208,12 +241,25 @@ class Poly private constructor(val edges: ImmutableList, val vertices: Imm } } else if (projectOther.component1() in projectThis.component1() .. projectThis.component2()) { // other's min point is within this + // push to right intersections.add(Penetration(normal, projectOther.component1() - projectThis.component2())) } else { // other's max point in within this + // push to left intersections.add(Penetration(normal, projectOther.component2() - projectThis.component1())) } + if (axis != null) { + val last = intersections.removeLast() + + if (strictAxis) { + // TODO: NYI + intersections.add(Penetration(axis, last.penetration / axis.dot(last.axis))) + } else { + intersections.add(Penetration(axis, last.penetration / axis.dot(last.axis))) + } + } + if (intersections.last().penetration == 0.0) { return null }