diff options
author | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
---|---|---|
committer | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
commit | 6352e8e3196f78388b6c771073f9e03eaa612673 (patch) | |
tree | e23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/StarActorMovementController.cpp | |
parent | 6741a057e5639280d85d0f88ba26f000baa58f61 (diff) |
everything everywhere
all at once
Diffstat (limited to 'source/game/StarActorMovementController.cpp')
-rw-r--r-- | source/game/StarActorMovementController.cpp | 1473 |
1 files changed, 1473 insertions, 0 deletions
diff --git a/source/game/StarActorMovementController.cpp b/source/game/StarActorMovementController.cpp new file mode 100644 index 0000000..915e7e0 --- /dev/null +++ b/source/game/StarActorMovementController.cpp @@ -0,0 +1,1473 @@ +#include "StarActorMovementController.hpp" +#include "StarDataStreamExtra.hpp" +#include "StarJsonExtra.hpp" +#include "StarRoot.hpp" +#include "StarAssets.hpp" +#include "StarPlatformerAStar.hpp" +#include "StarObject.hpp" + +namespace Star { + +ActorJumpProfile::ActorJumpProfile() {} + +ActorJumpProfile::ActorJumpProfile(Json const& config) { + jumpSpeed = config.optFloat("jumpSpeed"); + jumpControlForce = config.optFloat("jumpControlForce"); + jumpInitialPercentage = config.optFloat("jumpInitialPercentage"); + jumpHoldTime = config.optFloat("jumpHoldTime"); + jumpTotalHoldTime = config.optFloat("jumpTotalHoldTime"); + multiJump = config.optBool("multiJump"); + reJumpDelay = config.optFloat("reJumpDelay"); + autoJump = config.optBool("autoJump"); + collisionCancelled = config.optBool("collisionCancelled"); +} + +Json ActorJumpProfile::toJson() const { + return JsonObject{ + {"jumpSpeed", jsonFromMaybe(jumpSpeed)}, + {"jumpControlForce", jsonFromMaybe(jumpControlForce)}, + {"jumpInitialPercentage", jsonFromMaybe(jumpInitialPercentage)}, + {"jumpHoldTime", jsonFromMaybe(jumpHoldTime)}, + {"jumpTotalHoldTime", jsonFromMaybe(jumpTotalHoldTime)}, + {"multiJump", jsonFromMaybe(multiJump)}, + {"reJumpDelay", jsonFromMaybe(reJumpDelay)}, + {"autoJump", jsonFromMaybe(autoJump)}, + {"collisionCancelled", jsonFromMaybe(collisionCancelled)} + }; +} + +ActorJumpProfile ActorJumpProfile::merge(ActorJumpProfile const& rhs) const { + ActorJumpProfile merged; + + merged.jumpSpeed = rhs.jumpSpeed.orMaybe(jumpSpeed); + merged.jumpControlForce = rhs.jumpControlForce.orMaybe(jumpControlForce); + merged.jumpInitialPercentage = rhs.jumpInitialPercentage.orMaybe(jumpInitialPercentage); + merged.jumpHoldTime = rhs.jumpHoldTime.orMaybe(jumpHoldTime); + merged.jumpTotalHoldTime = rhs.jumpTotalHoldTime.orMaybe(jumpTotalHoldTime); + merged.multiJump = rhs.multiJump.orMaybe(multiJump); + merged.reJumpDelay = rhs.reJumpDelay.orMaybe(reJumpDelay); + merged.autoJump = rhs.autoJump.orMaybe(autoJump); + merged.collisionCancelled = rhs.collisionCancelled.orMaybe(collisionCancelled); + + return merged; +} + +DataStream& operator>>(DataStream& ds, ActorJumpProfile& movementParameters) { + ds.read(movementParameters.jumpSpeed); + ds.read(movementParameters.jumpControlForce); + ds.read(movementParameters.jumpInitialPercentage); + ds.read(movementParameters.jumpHoldTime); + ds.read(movementParameters.jumpTotalHoldTime); + ds.read(movementParameters.multiJump); + ds.read(movementParameters.reJumpDelay); + ds.read(movementParameters.autoJump); + ds.read(movementParameters.collisionCancelled); + + return ds; +} + +DataStream& operator<<(DataStream& ds, ActorJumpProfile const& movementParameters) { + ds.write(movementParameters.jumpSpeed); + ds.write(movementParameters.jumpControlForce); + ds.write(movementParameters.jumpInitialPercentage); + ds.write(movementParameters.jumpHoldTime); + ds.write(movementParameters.jumpTotalHoldTime); + ds.write(movementParameters.multiJump); + ds.write(movementParameters.reJumpDelay); + ds.write(movementParameters.autoJump); + ds.write(movementParameters.collisionCancelled); + + return ds; +} + +ActorMovementParameters ActorMovementParameters::sensibleDefaults() { + return ActorMovementParameters(Root::singleton().assets()->json("/default_actor_movement.config").toObject()); +} + +ActorMovementParameters::ActorMovementParameters(Json const& config) { + if (config.isNull()) + return; + + mass = config.optFloat("mass"); + gravityMultiplier = config.optFloat("gravityMultiplier"); + liquidBuoyancy = config.optFloat("liquidBuoyancy"); + airBuoyancy = config.optFloat("airBuoyancy"); + bounceFactor = config.optFloat("bounceFactor"); + stopOnFirstBounce = config.optBool("stopOnFirstBounce"); + enableSurfaceSlopeCorrection = config.optBool("enableSurfaceSlopeCorrection"); + slopeSlidingFactor = config.optFloat("slopeSlidingFactor"); + maxMovementPerStep = config.optFloat("maxMovementPerStep"); + maximumCorrection = config.optFloat("maximumCorrection"); + speedLimit = config.optFloat("speedLimit"); + + // "collisionPoly" is used as a synonym for setting both the standing and + // crouching polys. + + auto collisionPolyConfig = config.get("collisionPoly", {}); + auto standingPolyConfig = config.get("standingPoly", {}); + auto crouchingPolyConfig = config.get("crouchingPoly", {}); + + if (!standingPolyConfig.isNull()) + standingPoly = jsonToPolyF(standingPolyConfig); + else if (!collisionPolyConfig.isNull()) + standingPoly = jsonToPolyF(collisionPolyConfig); + + if (!crouchingPolyConfig.isNull()) + crouchingPoly = jsonToPolyF(crouchingPolyConfig); + else if (!collisionPolyConfig.isNull()) + crouchingPoly = jsonToPolyF(collisionPolyConfig); + + stickyCollision = config.optBool("stickyCollision"); + stickyForce = config.optFloat("stickyForce"); + + walkSpeed = config.optFloat("walkSpeed"); + runSpeed = config.optFloat("runSpeed"); + flySpeed = config.optFloat("flySpeed"); + airFriction = config.optFloat("airFriction"); + liquidFriction = config.optFloat("liquidFriction"); + minimumLiquidPercentage = config.optFloat("minimumLiquidPercentage"); + liquidImpedance = config.optFloat("liquidImpedance"); + normalGroundFriction = config.optFloat("normalGroundFriction"); + ambulatingGroundFriction = config.optFloat("ambulatingGroundFriction"); + groundForce = config.optFloat("groundForce"); + airForce = config.optFloat("airForce"); + liquidForce = config.optFloat("liquidForce"); + + airJumpProfile = config.opt("airJumpProfile").apply(construct<ActorJumpProfile>()).value(); + liquidJumpProfile = config.opt("liquidJumpProfile").apply(construct<ActorJumpProfile>()).value(); + + fallStatusSpeedMin = config.optFloat("fallStatusSpeedMin"); + fallThroughSustainFrames = config.optInt("fallThroughSustainFrames"); + maximumPlatformCorrection = config.optFloat("maximumPlatformCorrection"); + maximumPlatformCorrectionVelocityFactor = config.optFloat("maximumPlatformCorrectionVelocityFactor"); + + physicsEffectCategories = config.opt("physicsEffectCategories").apply(jsonToStringSet); + + groundMovementMinimumSustain = config.optFloat("groundMovementMinimumSustain"); + groundMovementMaximumSustain = config.optFloat("groundMovementMaximumSustain"); + groundMovementCheckDistance = config.optFloat("groundMovementCheckDistance"); + + collisionEnabled = config.optBool("collisionEnabled"); + frictionEnabled = config.optBool("frictionEnabled"); + gravityEnabled = config.optBool("gravityEnabled"); + + pathExploreRate = config.optFloat("pathExploreRate"); +} + +Json ActorMovementParameters::toJson() const { + return JsonObject{{"mass", jsonFromMaybe(mass)}, + {"gravityMultiplier", jsonFromMaybe(gravityMultiplier)}, + {"liquidBuoyancy", jsonFromMaybe(liquidBuoyancy)}, + {"airBuoyancy", jsonFromMaybe(airBuoyancy)}, + {"bounceFactor", jsonFromMaybe(bounceFactor)}, + {"stopOnFirstBounce", jsonFromMaybe(stopOnFirstBounce)}, + {"enableSurfaceSlopeCorrection", jsonFromMaybe(enableSurfaceSlopeCorrection)}, + {"slopeSlidingFactor", jsonFromMaybe(slopeSlidingFactor)}, + {"maxMovementPerStep", jsonFromMaybe(maxMovementPerStep)}, + {"maximumCorrection", jsonFromMaybe(maximumCorrection)}, + {"speedLimit", jsonFromMaybe(speedLimit)}, + {"standingPoly", jsonFromMaybe(standingPoly, jsonFromPolyF)}, + {"crouchingPoly", jsonFromMaybe(crouchingPoly, jsonFromPolyF)}, + {"stickyCollision", jsonFromMaybe(stickyCollision)}, + {"stickyForce", jsonFromMaybe(stickyForce)}, + {"walkSpeed", jsonFromMaybe(walkSpeed)}, + {"runSpeed", jsonFromMaybe(runSpeed)}, + {"flySpeed", jsonFromMaybe(flySpeed)}, + {"airFriction", jsonFromMaybe(airFriction)}, + {"liquidFriction", jsonFromMaybe(liquidFriction)}, + {"minimumLiquidPercentage", jsonFromMaybe(minimumLiquidPercentage)}, + {"liquidImpedance", jsonFromMaybe(liquidImpedance)}, + {"normalGroundFriction", jsonFromMaybe(normalGroundFriction)}, + {"ambulatingGroundFriction", jsonFromMaybe(ambulatingGroundFriction)}, + {"groundForce", jsonFromMaybe(groundForce)}, + {"airForce", jsonFromMaybe(airForce)}, + {"liquidForce", jsonFromMaybe(liquidForce)}, + {"airJumpProfile", airJumpProfile.toJson()}, + {"liquidJumpProfile", liquidJumpProfile.toJson()}, + {"fallStatusSpeedMin", jsonFromMaybe(fallStatusSpeedMin)}, + {"fallThroughSustainFrames", jsonFromMaybe(fallThroughSustainFrames)}, + {"maximumPlatformCorrection", jsonFromMaybe(maximumPlatformCorrection)}, + {"maximumPlatformCorrectionVelocityFactor", jsonFromMaybe(maximumPlatformCorrectionVelocityFactor)}, + {"physicsEffectCategories", jsonFromMaybe(physicsEffectCategories, jsonFromStringSet)}, + {"groundMovementMinimumSustain", jsonFromMaybe(groundMovementMinimumSustain)}, + {"groundMovementMaximumSustain", jsonFromMaybe(groundMovementMaximumSustain)}, + {"groundMovementCheckDistance", jsonFromMaybe(groundMovementCheckDistance)}, + {"collisionEnabled", jsonFromMaybe(collisionEnabled)}, + {"frictionEnabled", jsonFromMaybe(frictionEnabled)}, + {"gravityEnabled", jsonFromMaybe(gravityEnabled)}, + {"pathExploreRate", jsonFromMaybe(pathExploreRate)}}; +} + +ActorMovementParameters ActorMovementParameters::merge(ActorMovementParameters const& rhs) const { + ActorMovementParameters merged; + + merged.mass = rhs.mass.orMaybe(mass); + merged.gravityMultiplier = rhs.gravityMultiplier.orMaybe(gravityMultiplier); + merged.liquidBuoyancy = rhs.liquidBuoyancy.orMaybe(liquidBuoyancy); + merged.airBuoyancy = rhs.airBuoyancy.orMaybe(airBuoyancy); + merged.bounceFactor = rhs.bounceFactor.orMaybe(bounceFactor); + merged.stopOnFirstBounce = rhs.stopOnFirstBounce.orMaybe(stopOnFirstBounce); + merged.enableSurfaceSlopeCorrection = rhs.enableSurfaceSlopeCorrection.orMaybe(enableSurfaceSlopeCorrection); + merged.slopeSlidingFactor = rhs.slopeSlidingFactor.orMaybe(slopeSlidingFactor); + merged.maxMovementPerStep = rhs.maxMovementPerStep.orMaybe(maxMovementPerStep); + merged.maximumCorrection = rhs.maximumCorrection.orMaybe(maximumCorrection); + merged.speedLimit = rhs.speedLimit.orMaybe(speedLimit); + merged.standingPoly = rhs.standingPoly.orMaybe(standingPoly); + merged.crouchingPoly = rhs.crouchingPoly.orMaybe(crouchingPoly); + merged.stickyCollision = rhs.stickyCollision.orMaybe(stickyCollision); + merged.stickyForce = rhs.stickyForce.orMaybe(stickyForce); + merged.walkSpeed = rhs.walkSpeed.orMaybe(walkSpeed); + merged.runSpeed = rhs.runSpeed.orMaybe(runSpeed); + merged.flySpeed = rhs.flySpeed.orMaybe(flySpeed); + merged.airFriction = rhs.airFriction.orMaybe(airFriction); + merged.liquidFriction = rhs.liquidFriction.orMaybe(liquidFriction); + merged.minimumLiquidPercentage = rhs.minimumLiquidPercentage.orMaybe(minimumLiquidPercentage); + merged.liquidImpedance = rhs.liquidImpedance.orMaybe(liquidImpedance); + merged.normalGroundFriction = rhs.normalGroundFriction.orMaybe(normalGroundFriction); + merged.ambulatingGroundFriction = rhs.ambulatingGroundFriction.orMaybe(ambulatingGroundFriction); + merged.groundForce = rhs.groundForce.orMaybe(groundForce); + merged.airForce = rhs.airForce.orMaybe(airForce); + merged.liquidForce = rhs.liquidForce.orMaybe(liquidForce); + + merged.airJumpProfile = airJumpProfile.merge(rhs.airJumpProfile); + merged.liquidJumpProfile = liquidJumpProfile.merge(rhs.liquidJumpProfile); + + merged.fallStatusSpeedMin = rhs.fallStatusSpeedMin.orMaybe(fallStatusSpeedMin); + merged.fallThroughSustainFrames = rhs.fallThroughSustainFrames.orMaybe(fallThroughSustainFrames); + merged.maximumPlatformCorrection = rhs.maximumPlatformCorrection.orMaybe(maximumPlatformCorrection); + merged.maximumPlatformCorrectionVelocityFactor = rhs.maximumPlatformCorrectionVelocityFactor.orMaybe(maximumPlatformCorrectionVelocityFactor); + + merged.physicsEffectCategories = rhs.physicsEffectCategories.orMaybe(physicsEffectCategories); + + merged.groundMovementMinimumSustain = rhs.groundMovementMinimumSustain.orMaybe(groundMovementMinimumSustain); + merged.groundMovementMaximumSustain = rhs.groundMovementMaximumSustain.orMaybe(groundMovementMaximumSustain); + merged.groundMovementCheckDistance = rhs.groundMovementCheckDistance.orMaybe(groundMovementCheckDistance); + + merged.collisionEnabled = rhs.collisionEnabled.orMaybe(collisionEnabled); + merged.frictionEnabled = rhs.frictionEnabled.orMaybe(frictionEnabled); + merged.gravityEnabled = rhs.gravityEnabled.orMaybe(gravityEnabled); + + merged.pathExploreRate = rhs.pathExploreRate.orMaybe(pathExploreRate); + + return merged; +} + +DataStream& operator>>(DataStream& ds, ActorMovementParameters& movementParameters) { + ds.read(movementParameters.mass); + ds.read(movementParameters.gravityMultiplier); + ds.read(movementParameters.liquidBuoyancy); + ds.read(movementParameters.airBuoyancy); + ds.read(movementParameters.bounceFactor); + ds.read(movementParameters.stopOnFirstBounce); + ds.read(movementParameters.enableSurfaceSlopeCorrection); + ds.read(movementParameters.slopeSlidingFactor); + ds.read(movementParameters.maxMovementPerStep); + ds.read(movementParameters.maximumCorrection); + ds.read(movementParameters.speedLimit); + ds.read(movementParameters.standingPoly); + ds.read(movementParameters.crouchingPoly); + ds.read(movementParameters.stickyCollision); + ds.read(movementParameters.stickyForce); + ds.read(movementParameters.walkSpeed); + ds.read(movementParameters.runSpeed); + ds.read(movementParameters.flySpeed); + ds.read(movementParameters.airFriction); + ds.read(movementParameters.liquidFriction); + ds.read(movementParameters.minimumLiquidPercentage); + ds.read(movementParameters.liquidImpedance); + ds.read(movementParameters.normalGroundFriction); + ds.read(movementParameters.ambulatingGroundFriction); + ds.read(movementParameters.groundForce); + ds.read(movementParameters.airForce); + ds.read(movementParameters.liquidForce); + ds.read(movementParameters.airJumpProfile); + ds.read(movementParameters.liquidJumpProfile); + ds.read(movementParameters.fallStatusSpeedMin); + ds.read(movementParameters.fallThroughSustainFrames); + ds.read(movementParameters.maximumPlatformCorrection); + ds.read(movementParameters.maximumPlatformCorrectionVelocityFactor); + ds.read(movementParameters.physicsEffectCategories); + ds.read(movementParameters.collisionEnabled); + ds.read(movementParameters.frictionEnabled); + ds.read(movementParameters.gravityEnabled); + ds.read(movementParameters.pathExploreRate); + + return ds; +} + +DataStream& operator<<(DataStream& ds, ActorMovementParameters const& movementParameters) { + ds.write(movementParameters.mass); + ds.write(movementParameters.gravityMultiplier); + ds.write(movementParameters.liquidBuoyancy); + ds.write(movementParameters.airBuoyancy); + ds.write(movementParameters.bounceFactor); + ds.write(movementParameters.stopOnFirstBounce); + ds.write(movementParameters.enableSurfaceSlopeCorrection); + ds.write(movementParameters.slopeSlidingFactor); + ds.write(movementParameters.maxMovementPerStep); + ds.write(movementParameters.maximumCorrection); + ds.write(movementParameters.speedLimit); + ds.write(movementParameters.standingPoly); + ds.write(movementParameters.crouchingPoly); + ds.write(movementParameters.stickyCollision); + ds.write(movementParameters.stickyForce); + ds.write(movementParameters.walkSpeed); + ds.write(movementParameters.runSpeed); + ds.write(movementParameters.flySpeed); + ds.write(movementParameters.airFriction); + ds.write(movementParameters.liquidFriction); + ds.write(movementParameters.minimumLiquidPercentage); + ds.write(movementParameters.liquidImpedance); + ds.write(movementParameters.normalGroundFriction); + ds.write(movementParameters.ambulatingGroundFriction); + ds.write(movementParameters.groundForce); + ds.write(movementParameters.airForce); + ds.write(movementParameters.liquidForce); + ds.write(movementParameters.airJumpProfile); + ds.write(movementParameters.liquidJumpProfile); + ds.write(movementParameters.fallStatusSpeedMin); + ds.write(movementParameters.fallThroughSustainFrames); + ds.write(movementParameters.maximumPlatformCorrection); + ds.write(movementParameters.maximumPlatformCorrectionVelocityFactor); + ds.write(movementParameters.physicsEffectCategories); + ds.write(movementParameters.collisionEnabled); + ds.write(movementParameters.frictionEnabled); + ds.write(movementParameters.gravityEnabled); + ds.write(movementParameters.pathExploreRate); + + return ds; +} + +ActorMovementModifiers::ActorMovementModifiers(Json const& config) { + groundMovementModifier = 1.0f; + liquidMovementModifier = 1.0f; + speedModifier = 1.0f; + airJumpModifier = 1.0f; + liquidJumpModifier = 1.0f; + runningSuppressed = false; + jumpingSuppressed = false; + facingSuppressed = false; + movementSuppressed = false; + + if (!config.isNull()) { + groundMovementModifier = config.getFloat("groundMovementModifier", 1.0f); + liquidMovementModifier = config.getFloat("liquidMovementModifier", 1.0f); + speedModifier = config.getFloat("speedModifier", 1.0f); + airJumpModifier = config.getFloat("airJumpModifier", 1.0f); + liquidJumpModifier = config.getFloat("liquidJumpModifier", 1.0f); + runningSuppressed = config.getBool("runningSuppressed", false); + jumpingSuppressed = config.getBool("jumpingSuppressed", false); + facingSuppressed = config.getBool("facingSuppressed", false); + movementSuppressed = config.getBool("movementSuppressed", false); + } +} + +Json ActorMovementModifiers::toJson() const { + return JsonObject{ + {"groundMovementModifier", groundMovementModifier}, + {"liquidMovementModifier", liquidMovementModifier}, + {"speedModifier", speedModifier}, + {"airJumpModifier", airJumpModifier}, + {"liquidJumpModifier", liquidJumpModifier}, + {"runningSuppressed", runningSuppressed}, + {"jumpingSuppressed", jumpingSuppressed}, + {"facingSuppressed", facingSuppressed}, + {"movementSuppressed", movementSuppressed} + }; +} + +ActorMovementModifiers ActorMovementModifiers::combine(ActorMovementModifiers const& rhs) const { + ActorMovementModifiers res = *this; + + res.groundMovementModifier *= rhs.groundMovementModifier; + res.liquidMovementModifier *= rhs.liquidMovementModifier; + res.speedModifier *= rhs.speedModifier; + res.airJumpModifier *= rhs.airJumpModifier; + res.liquidJumpModifier *= rhs.liquidJumpModifier; + res.runningSuppressed = res.runningSuppressed || rhs.runningSuppressed; + res.jumpingSuppressed = res.jumpingSuppressed || rhs.jumpingSuppressed; + res.facingSuppressed = res.facingSuppressed || rhs.facingSuppressed; + res.movementSuppressed = res.movementSuppressed || rhs.movementSuppressed; + + return res; +} + +DataStream& operator>>(DataStream& ds, ActorMovementModifiers& movementModifiers) { + ds.read(movementModifiers.groundMovementModifier); + ds.read(movementModifiers.liquidMovementModifier); + ds.read(movementModifiers.speedModifier); + ds.read(movementModifiers.airJumpModifier); + ds.read(movementModifiers.liquidJumpModifier); + ds.read(movementModifiers.runningSuppressed); + ds.read(movementModifiers.jumpingSuppressed); + ds.read(movementModifiers.facingSuppressed); + ds.read(movementModifiers.movementSuppressed); + + return ds; +} + +DataStream& operator<<(DataStream& ds, ActorMovementModifiers const& movementModifiers) { + ds.write(movementModifiers.groundMovementModifier); + ds.write(movementModifiers.liquidMovementModifier); + ds.write(movementModifiers.speedModifier); + ds.write(movementModifiers.airJumpModifier); + ds.write(movementModifiers.liquidJumpModifier); + ds.write(movementModifiers.runningSuppressed); + ds.write(movementModifiers.jumpingSuppressed); + ds.write(movementModifiers.facingSuppressed); + ds.write(movementModifiers.movementSuppressed); + + return ds; +} + +ActorMovementController::ActorMovementController(ActorMovementParameters const& parameters) { + m_controlRotationRate = 0.0f; + m_controlRun = false; + m_controlCrouch = false; + m_controlDown = false; + m_controlJump = false; + m_controlJumpAnyway = false; + m_controlPathMove = {}; + + addNetElement(&m_walking); + addNetElement(&m_running); + addNetElement(&m_movingDirection); + addNetElement(&m_facingDirection); + addNetElement(&m_crouching); + addNetElement(&m_flying); + addNetElement(&m_falling); + addNetElement(&m_canJump); + addNetElement(&m_jumping); + addNetElement(&m_groundMovement); + addNetElement(&m_liquidMovement); + addNetElement(&m_anchorState); + + m_fallThroughSustain = 0; + m_lastControlJump = false; + m_lastControlDown = false; + m_targetHorizontalAmbulatingVelocity = 0.0f; + + resetBaseParameters(parameters); +} + +ActorMovementParameters const& ActorMovementController::baseParameters() const { + return m_baseParameters; +} + +void ActorMovementController::updateBaseParameters(ActorMovementParameters const& parameters) { + m_baseParameters = m_baseParameters.merge(parameters); + applyMCParameters(m_baseParameters); +} + +void ActorMovementController::resetBaseParameters(ActorMovementParameters const& parameters) { + m_baseParameters = ActorMovementParameters::sensibleDefaults().merge(parameters); + applyMCParameters(m_baseParameters); +} + +ActorMovementModifiers const& ActorMovementController::baseModifiers() const { + return m_baseModifiers; +} + +void ActorMovementController::updateBaseModifiers(ActorMovementModifiers const& modifiers) { + m_baseModifiers = m_baseModifiers.combine(modifiers); +} + +void ActorMovementController::resetBaseModifiers(ActorMovementModifiers const& modifiers) { + m_baseModifiers = modifiers; +} + +Json ActorMovementController::storeState() const { + return JsonObject{{"position", jsonFromVec2F(MovementController::position())}, + {"velocity", jsonFromVec2F(velocity())}, + {"rotation", MovementController::rotation()}, + {"movingDirection", DirectionNames.getRight(m_movingDirection.get())}, + {"facingDirection", DirectionNames.getRight(m_facingDirection.get())}, + {"crouching", m_crouching.get()}}; +} + +void ActorMovementController::loadState(Json const& state) { + setPosition(jsonToVec2F(state.get("position"))); + setVelocity(jsonToVec2F(state.get("velocity"))); + setRotation(state.getFloat("rotation")); + m_movingDirection.set(DirectionNames.getLeft(state.getString("movingDirection"))); + m_facingDirection.set(DirectionNames.getLeft(state.getString("facingDirection"))); + m_crouching.set(state.getBool("crouching")); +} + +void ActorMovementController::setAnchorState(EntityAnchorState anchorState) { + doSetAnchorState(anchorState); +} + +void ActorMovementController::resetAnchorState() { + doSetAnchorState({}); +} + +Maybe<EntityAnchorState> ActorMovementController::anchorState() const { + return m_anchorState.get(); +} + +EntityAnchorConstPtr ActorMovementController::entityAnchor() const { + return m_entityAnchor; +} + +Vec2F ActorMovementController::position() const { + if (m_entityAnchor) + return m_entityAnchor->position; + return MovementController::position(); +} + +float ActorMovementController::xPosition() const { + return position()[0]; +} + +float ActorMovementController::yPosition() const { + return position()[1]; +} + +float ActorMovementController::rotation() const { + if (m_entityAnchor) + return m_entityAnchor->angle; + return MovementController::rotation(); +} + +PolyF ActorMovementController::collisionBody() const { + auto collisionBody = MovementController::collisionPoly(); + collisionBody.rotate(rotation()); + collisionBody.translate(position()); + return collisionBody; +} + +RectF ActorMovementController::localBoundBox() const { + auto poly = MovementController::collisionPoly(); + poly.rotate(rotation()); + return poly.boundBox(); +} + +RectF ActorMovementController::collisionBoundBox() const { + return collisionBody().boundBox(); +} + +bool ActorMovementController::walking() const { + return m_walking.get(); +} + +bool ActorMovementController::running() const { + return m_running.get(); +} + +Direction ActorMovementController::movingDirection() const { + return m_movingDirection.get(); +} + +Direction ActorMovementController::facingDirection() const { + if (m_entityAnchor) + return m_entityAnchor->direction; + return m_facingDirection.get(); +} + +bool ActorMovementController::crouching() const { + return m_crouching.get(); +} + +bool ActorMovementController::flying() const { + return m_flying.get(); +} + +bool ActorMovementController::falling() const { + return m_falling.get(); +} + +bool ActorMovementController::canJump() const { + return m_canJump.get(); +} + +bool ActorMovementController::jumping() const { + return m_jumping.get(); +} + +bool ActorMovementController::groundMovement() const { + return m_groundMovement.get(); +} + +bool ActorMovementController::liquidMovement() const { + return m_liquidMovement.get(); +} + +bool ActorMovementController::pathfinding() const { + if (m_pathController) + return m_pathController->pathfinding(); + else + return false; +} + +void ActorMovementController::controlRotation(float rotationRate) { + m_controlRotationRate += rotationRate; +} + +void ActorMovementController::controlAcceleration(Vec2F const& acceleration) { + m_controlAcceleration += acceleration; +} + +void ActorMovementController::controlForce(Vec2F const& force) { + m_controlForce += force; +} + +void ActorMovementController::controlApproachVelocity(Vec2F const& targetVelocity, float maxControlForce) { + m_controlApproachVelocities.append({targetVelocity, maxControlForce}); +} + +void ActorMovementController::controlApproachVelocityAlongAngle( + float angle, float targetVelocity, float maxControlForce, bool positiveOnly) { + m_controlApproachVelocityAlongAngles.append({angle, targetVelocity, maxControlForce, positiveOnly}); +} + +void ActorMovementController::controlApproachXVelocity(float targetXVelocity, float maxControlForce) { + controlApproachVelocityAlongAngle(0, targetXVelocity, maxControlForce); +} + +void ActorMovementController::controlApproachYVelocity(float targetYVelocity, float maxControlForce) { + controlApproachVelocityAlongAngle(Constants::pi / 2, targetYVelocity, maxControlForce); +} + +void ActorMovementController::controlParameters(ActorMovementParameters const& parameters) { + m_controlParameters = m_controlParameters.merge(parameters); +} + +void ActorMovementController::controlModifiers(ActorMovementModifiers const& modifiers) { + m_controlModifiers = m_controlModifiers.combine(modifiers); +} + +void ActorMovementController::controlMove(Direction direction, bool run) { + m_controlMove = direction; + m_controlRun = run; +} + +void ActorMovementController::controlFace(Direction direction) { + m_controlFace = direction; +} + +void ActorMovementController::controlDown() { + m_controlDown = true; +} + +void ActorMovementController::controlCrouch() { + m_controlCrouch = true; +} + +void ActorMovementController::controlJump(bool jumpEvenIfUnable) { + m_controlJump = true; + m_controlJumpAnyway |= jumpEvenIfUnable; +} + +void ActorMovementController::controlFly(Vec2F const& velocity) { + m_controlFly = velocity; +} + +Maybe<pair<Vec2F, bool>> ActorMovementController::pathMove(Vec2F const& position, bool run, Maybe<PlatformerAStar::Parameters> const& parameters) { + if (!m_pathController) + m_pathController = make_shared<PathController>(world()); + + // set new parameters if they have changed + if (m_pathController->targetPosition().isNothing() || (parameters && m_pathController->parameters() != *parameters)) { + if (parameters) + m_pathController->setParameters(*parameters); + m_pathMoveResult = m_pathController->findPath(*this, position).apply([position](bool result) { return pair<Vec2F, bool>(position, result); }); + } + + // update target position if it has changed + if (m_pathController) { + m_pathController->findPath(*this, position); + } + + if (m_pathMoveResult.isValid()) { + // path controller failed or succeeded, return the result and reset the controller + m_pathController->reset(); + } + + return take(m_pathMoveResult); +} + +Maybe<pair<Vec2F, bool>> ActorMovementController::controlPathMove(Vec2F const& position, bool run, Maybe<PlatformerAStar::Parameters> const& parameters) { + auto result = pathMove(position, run, parameters); + + if (result.isNothing()) + m_controlPathMove = pair<Vec2F, bool>(position, run); + + return result; +} + +void ActorMovementController::clearControls() { + m_controlRotationRate = 0.0f; + m_controlAcceleration = Vec2F(); + m_controlForce = Vec2F(); + m_controlApproachVelocities.clear(); + m_controlApproachVelocityAlongAngles.clear(); + m_controlMove = {}; + m_controlFace = {}; + m_controlRun = false; + m_controlCrouch = false; + m_controlDown = false; + m_controlJump = false; + m_controlJumpAnyway = false; + m_controlFly = {}; + m_controlPathMove = {}; + m_controlParameters = ActorMovementParameters(); + m_controlModifiers = ActorMovementModifiers(); +} + +void ActorMovementController::tickMaster() { + EntityAnchorConstPtr newAnchor; + if (auto anchorState = m_anchorState.get()) { + if (auto anchorableEntity = as<AnchorableEntity>(world()->entity(anchorState->entityId))) + newAnchor = anchorableEntity->anchor(anchorState->positionIndex); + } + + if (newAnchor) + m_entityAnchor = newAnchor; + else + resetAnchorState(); + + if (m_entityAnchor) { + m_walking.set(false); + m_running.set(false); + m_crouching.set(false); + m_flying.set(false); + m_falling.set(false); + m_canJump.set(false); + m_jumping.set(false); + m_groundMovement.set(false); + m_liquidMovement.set(false); + + setVelocity((m_entityAnchor->position - MovementController::position()) / WorldTimestep); + MovementController::tickMaster(); + setPosition(m_entityAnchor->position); + } else { + auto activeParameters = m_baseParameters.merge(m_controlParameters); + auto activeModifiers = m_baseModifiers.combine(m_controlModifiers); + + if (activeModifiers.movementSuppressed) { + m_controlMove = {}; + m_controlRun = false; + m_controlCrouch = false; + m_controlDown = false; + m_controlJump = false; + m_controlFly = {}; + m_controlPathMove = {}; + } + + if (m_controlMove.isValid() + || m_controlCrouch + || m_controlDown + || m_controlJump + || m_controlFly.isValid() + || !m_controlApproachVelocities.empty() + || !m_controlApproachVelocityAlongAngles.empty()) { + // controlling any other movement overrides the pathing + m_controlPathMove.reset(); + } + + if (m_controlPathMove && m_pathMoveResult.isNothing()) { + if (appliedForceRegion()) { + m_pathController->reset(); + } else if (!m_pathController->pathfinding()) { + m_pathMoveResult = m_pathController->move(*this, activeParameters, activeModifiers, m_controlPathMove->second, WorldTimestep) + .apply([this](bool result) { return pair<Vec2F, bool>(m_controlPathMove->first, result); }); + + auto action = m_pathController->curAction(); + bool onGround = false; + if (auto a = action) { + using namespace PlatformerAStar; + m_walking.set(a == Action::Walk && !m_controlPathMove->second); + m_running.set(a == Action::Walk && m_controlPathMove->second); + m_flying.set(a == Action::Fly || a == Action::Swim); + m_falling.set((a == Action::Arc && yVelocity() < 0.0f) || a == Action::Drop); + m_jumping.set(a == Action::Arc && yVelocity() >= 0.0f); + + onGround = a == Action::Walk || a == Action::Drop || a == Action::Jump; + + if (a == Action::Land || a== Action::Jump) { + auto inLiquid = liquidPercentage() >= activeParameters.minimumLiquidPercentage.value(1.0); + m_liquidMovement.set(inLiquid); + m_groundMovement.set(!inLiquid); + onGround = !inLiquid && onGround; + } else { + m_liquidMovement.set(a == Action::Swim); + m_groundMovement.set(a != Action::Arc && a != Action::Swim); + } + } else { + m_walking.set(false); + m_running.set(false); + } + auto facing = m_controlFace.orMaybe(m_pathController->facing()).value(m_facingDirection.get()); + m_facingDirection.set(facing); + m_movingDirection.set(m_pathController->facing().value(m_facingDirection.get())); + + applyMCParameters(activeParameters); + + // MovementController still handles updating liquid percentage and updating force regions + updateLiquidPercentage(); + updateForceRegions(); + // onGround flag needs to be manually set, won't be set by MovementController::tickMaster + setOnGround(onGround); + clearControls(); + return; + } else { + m_pathMoveResult = m_pathController->findPath(*this, m_controlPathMove->first).apply([this](bool result) { + return pair<Vec2F, bool>(m_controlPathMove->first, result); + }); + } + } else { + m_pathController = {}; + } + + // Do some basic movement consistency checks. + if (m_controlFly) + m_controlMove = {}; + + if ((m_controlDown && !m_lastControlDown) || m_controlFly) + m_fallThroughSustain = *activeParameters.fallThroughSustainFrames; + else if (m_fallThroughSustain > 0) + --m_fallThroughSustain; + + applyMCParameters(activeParameters); + + m_targetHorizontalAmbulatingVelocity = 0.0f; + + rotate(m_controlRotationRate); + accelerate(m_controlAcceleration); + force(m_controlForce); + + for (auto const& approach : m_controlApproachVelocities) + approachVelocity(approach.targetVelocity * activeModifiers.speedModifier, approach.maxControlForce); + + for (auto const& approach : m_controlApproachVelocityAlongAngles) + approachVelocityAlongAngle(approach.alongAngle, approach.targetVelocity * activeModifiers.speedModifier, approach.maxControlForce, approach.positiveOnly); + + m_liquidMovement.set(liquidPercentage() >= *activeParameters.minimumLiquidPercentage); + float liquidImpedance = *activeParameters.liquidImpedance * liquidPercentage(); + + Maybe<Direction> updatedMovingDirection; + bool running = m_controlRun && !activeModifiers.runningSuppressed; + + if (m_controlFly) { + Vec2F flyVelocity = *m_controlFly; + if (flyVelocity.magnitudeSquared() != 0) + flyVelocity = flyVelocity.normalized() * *activeParameters.flySpeed; + + if (m_liquidMovement.get()) + approachVelocity(flyVelocity * (1.0f - liquidImpedance) * activeModifiers.speedModifier, *activeParameters.liquidForce * activeModifiers.liquidMovementModifier); + else + approachVelocity(flyVelocity * activeModifiers.speedModifier, *activeParameters.airForce); + + if (flyVelocity[0] > 0) + updatedMovingDirection = Direction::Right; + else if (flyVelocity[0] < 0) + updatedMovingDirection = Direction::Left; + + m_groundMovementSustainTimer = GameTimer(0); + + } else { + float jumpModifier; + ActorJumpProfile jumpProfile; + if (m_liquidMovement.get()) { + jumpModifier = activeModifiers.liquidJumpModifier; + jumpProfile = activeParameters.liquidJumpProfile; + *jumpProfile.jumpSpeed *= (1.0f - liquidImpedance); + } else { + jumpModifier = activeModifiers.airJumpModifier; + jumpProfile = activeParameters.airJumpProfile; + } + + bool startJump = false; + bool 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. + float minGroundSustain = *activeParameters.groundMovementMinimumSustain; + float maxGroundSustain = *activeParameters.groundMovementMaximumSustain; + float groundCheckDistance = *activeParameters.groundMovementCheckDistance; + m_groundMovementSustainTimer.tick(); + if (onGround()) { + m_groundMovementSustainTimer = GameTimer(maxGroundSustain); + } else if (!m_groundMovementSustainTimer.ready() && groundCheckDistance > 0.0f && maxGroundSustain - m_groundMovementSustainTimer.timer > minGroundSustain) { + PolyF collisionBody = MovementController::collisionBody(); + collisionBody.translate(Vec2F(0, -groundCheckDistance)); + if (!world()->polyCollision(collisionBody, {CollisionKind::Block, CollisionKind::Dynamic, CollisionKind::Platform, CollisionKind::Slippery})) + m_groundMovementSustainTimer = GameTimer(0); + } + + bool standingJumpable = !m_groundMovementSustainTimer.ready(); + bool controlJump = m_controlJump && (!activeModifiers.jumpingSuppressed || m_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 (m_reJumpTimer.ready() && controlJump && (*jumpProfile.autoJump || !m_lastControlJump)) { + if (standingJumpable || *jumpProfile.multiJump || m_controlJumpAnyway) + startJump = true; + } else if (m_jumping.get() && controlJump && (!m_jumpHoldTimer || !m_jumpHoldTimer->ready())) { + if (!*jumpProfile.collisionCancelled || collisionCorrection()[1] >= 0.0f) + holdJump = true; + } + + if (startJump) { + m_jumping.set(true); + + m_reJumpTimer = GameTimer(*jumpProfile.reJumpDelay); + if (*jumpProfile.jumpHoldTime >= 0.0f) + m_jumpHoldTimer = GameTimer(*jumpProfile.jumpHoldTime); + else + m_jumpHoldTimer = {}; + + setYVelocity(yVelocity() + *jumpProfile.jumpSpeed * *jumpProfile.jumpInitialPercentage * jumpModifier); + + m_groundMovementSustainTimer = GameTimer(0); + + } else if (holdJump) { + m_reJumpTimer.tick(); + if (m_jumpHoldTimer) + m_jumpHoldTimer->tick(); + + approachYVelocity(*jumpProfile.jumpSpeed * jumpModifier, *jumpProfile.jumpControlForce * jumpModifier); + + } else { + m_jumping.set(false); + m_reJumpTimer.tick(); + } + + if (m_controlMove == Direction::Left) { + updatedMovingDirection = Direction::Left; + m_targetHorizontalAmbulatingVelocity = + -1.0f * (running ? *activeParameters.runSpeed * activeModifiers.speedModifier + : *activeParameters.walkSpeed * activeModifiers.speedModifier); + } else if (m_controlMove == Direction::Right) { + updatedMovingDirection = Direction::Right; + m_targetHorizontalAmbulatingVelocity = + 1.0f * (running ? *activeParameters.runSpeed * activeModifiers.speedModifier + : *activeParameters.walkSpeed * activeModifiers.speedModifier); + } + + if (m_liquidMovement.get()) + m_targetHorizontalAmbulatingVelocity *= (1.0f - liquidImpedance); + + Vec2F surfaceVelocity = MovementController::surfaceVelocity(); + + // don't ambulate if we're already moving faster than the target velocity in the direction of ambulation + bool ambulationWouldAccelerate = abs(m_targetHorizontalAmbulatingVelocity + surfaceVelocity[0]) > abs(xVelocity()) + || (m_targetHorizontalAmbulatingVelocity < 0) != (xVelocity() < 0); + + if (m_targetHorizontalAmbulatingVelocity != 0.0f && ambulationWouldAccelerate) { + float ambulatingAccel; + if (onGround()) + ambulatingAccel = *activeParameters.groundForce * activeModifiers.groundMovementModifier; + else if (m_liquidMovement.get()) + ambulatingAccel = *activeParameters.liquidForce * activeModifiers.liquidMovementModifier; + else + ambulatingAccel = *activeParameters.airForce; + + approachXVelocity(m_targetHorizontalAmbulatingVelocity + surfaceVelocity[0], ambulatingAccel); + } + } + + if (updatedMovingDirection) + m_movingDirection.set(*updatedMovingDirection); + + if (!activeModifiers.facingSuppressed) { + if (m_controlFace) + m_facingDirection.set(*m_controlFace); + else if (updatedMovingDirection) + m_facingDirection.set(*updatedMovingDirection); + else if (m_controlPathMove && m_pathController && m_pathController->facing()) + m_facingDirection.set(*m_pathController->facing()); + } + + m_groundMovement.set(!m_groundMovementSustainTimer.ready()); + if (m_groundMovement.get()) { + m_running.set(running && m_controlMove); + m_walking.set(!running && m_controlMove); + m_crouching.set(m_controlCrouch && !m_controlMove); + } + m_flying.set((bool)m_controlFly); + + bool falling = (yVelocity() < *activeParameters.fallStatusSpeedMin) && !m_groundMovement.get(); + m_falling.set(falling); + + MovementController::tickMaster(); + + m_lastControlJump = m_controlJump; + m_lastControlDown = m_controlDown; + + if (m_liquidMovement.get()) + m_canJump.set(m_reJumpTimer.ready() && (!m_groundMovementSustainTimer.ready() || *activeParameters.liquidJumpProfile.multiJump)); + else + m_canJump.set(m_reJumpTimer.ready() && (!m_groundMovementSustainTimer.ready() || *activeParameters.airJumpProfile.multiJump)); + } + + clearControls(); +} + +void ActorMovementController::tickSlave() { + MovementController::tickSlave(); + + m_entityAnchor.reset(); + if (auto anchorState = m_anchorState.get()) { + if (auto anchorableEntity = as<AnchorableEntity>(world()->entity(anchorState->entityId))) + m_entityAnchor = anchorableEntity->anchor(anchorState->positionIndex); + } +} + +void ActorMovementController::applyMCParameters(ActorMovementParameters const& parameters) { + MovementParameters mcParameters; + + mcParameters.mass = parameters.mass; + + if (!onGround() && yVelocity() < 0) + mcParameters.gravityMultiplier = *parameters.gravityMultiplier; + else + mcParameters.gravityMultiplier = parameters.gravityMultiplier; + + mcParameters.liquidBuoyancy = parameters.liquidBuoyancy; + mcParameters.airBuoyancy = parameters.airBuoyancy; + mcParameters.bounceFactor = parameters.bounceFactor; + mcParameters.stopOnFirstBounce = parameters.stopOnFirstBounce; + mcParameters.enableSurfaceSlopeCorrection = parameters.enableSurfaceSlopeCorrection; + mcParameters.slopeSlidingFactor = parameters.slopeSlidingFactor; + mcParameters.maxMovementPerStep = parameters.maxMovementPerStep; + + if (m_crouching.get()) + mcParameters.collisionPoly = *parameters.crouchingPoly; + else + mcParameters.collisionPoly = *parameters.standingPoly; + + mcParameters.stickyCollision = parameters.stickyCollision; + mcParameters.stickyForce = parameters.stickyForce; + + mcParameters.airFriction = parameters.airFriction; + mcParameters.liquidFriction = parameters.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'. + float relativeXVelocity = xVelocity() - surfaceVelocity()[0]; + bool useAmbulatingGroundFriction = (m_walking.get() || m_running.get()) + && copysign(1, m_targetHorizontalAmbulatingVelocity) == copysign(1, relativeXVelocity) + && fabs(relativeXVelocity) <= fabs(m_targetHorizontalAmbulatingVelocity); + + if (useAmbulatingGroundFriction) + mcParameters.groundFriction = parameters.ambulatingGroundFriction; + else + mcParameters.groundFriction = parameters.normalGroundFriction; + + mcParameters.collisionEnabled = parameters.collisionEnabled; + mcParameters.frictionEnabled = parameters.frictionEnabled; + mcParameters.gravityEnabled = parameters.gravityEnabled; + + mcParameters.ignorePlatformCollision = m_fallThroughSustain > 0 || m_controlFly || m_controlDown; + mcParameters.maximumPlatformCorrection = parameters.maximumPlatformCorrection; + mcParameters.maximumPlatformCorrectionVelocityFactor = parameters.maximumPlatformCorrectionVelocityFactor; + + mcParameters.physicsEffectCategories = parameters.physicsEffectCategories; + + mcParameters.maximumCorrection = parameters.maximumCorrection; + mcParameters.speedLimit = parameters.speedLimit; + + MovementController::applyParameters(mcParameters); +} + +void ActorMovementController::doSetAnchorState(Maybe<EntityAnchorState> anchorState) { + EntityAnchorConstPtr entityAnchor; + if (anchorState) { + auto anchorableEntity = as<AnchorableEntity>(world()->entity(anchorState->entityId)); + if (!anchorableEntity) + throw ActorMovementControllerException::format("No such anchorable entity id %s in ActorMovementController::setAnchorState", anchorState->entityId); + entityAnchor = anchorableEntity->anchor(anchorState->positionIndex); + if (!entityAnchor) + throw ActorMovementControllerException::format("Anchor position %s is disabled ActorMovementController::setAnchorState", anchorState->positionIndex); + } + + if (!entityAnchor && m_entityAnchor && m_entityAnchor->exitBottomPosition) { + auto boundBox = MovementController::localBoundBox(); + Vec2F bottomMid = {boundBox.center()[0], boundBox.yMin()}; + setPosition(*m_entityAnchor->exitBottomPosition - bottomMid); + } + + m_anchorState.set(anchorState); + m_entityAnchor = move(entityAnchor); + + if (m_entityAnchor) + setPosition(m_entityAnchor->position); +} + + +PathController::PathController(World* world) + : m_world(world), m_edgeTimer(0.0) { } + +PlatformerAStar::Parameters const& PathController::parameters() { + return m_parameters; +} + +void PathController::setParameters(PlatformerAStar::Parameters const& parameters) { + m_parameters = parameters; +} + +void PathController::reset() { + m_startPosition = {}; + m_targetPosition = {}; + m_controlFace = {}; + m_pathFinder = {}; + m_path = {}; + m_edgeIndex = 0; + m_edgeTimer = 0.0; +} + +bool PathController::pathfinding() const { + return m_path.isNothing(); +} + +Maybe<Vec2F> PathController::targetPosition() const { + return m_targetPosition; +} + +Maybe<Direction> PathController::facing() const { + return m_controlFace; +} + +Maybe<PlatformerAStar::Action> PathController::curAction() const { + if (m_path && m_edgeIndex < m_path->size()) { + return m_path->at(m_edgeIndex).action; + } + return {}; +} + +Maybe<bool> PathController::findPath(ActorMovementController& movementController, Vec2F const& targetPosition) { + using namespace PlatformerAStar; + + // reached the end of the last path and we have a new target position to move toward + if (m_path && m_edgeIndex == m_path->size() && m_world->geometry().diff(*m_targetPosition, targetPosition).magnitude() > 0.001) { + reset(); + m_targetPosition = targetPosition; + } + + // starting a new path, or the target position moved by more than 2 blocks + if (!m_targetPosition || (!m_path && !m_pathFinder) || m_world->geometry().diff(*m_targetPosition, targetPosition).magnitude() > 2.0) { + auto grounded = movementController.onGround(); + if (m_path) { + // if already moving on a path, collision will be disabled and we can't use MovementController::onGround() to check for ground collision + auto const groundCollision = CollisionSet{ CollisionKind::Null, CollisionKind::Block, CollisionKind::Slippery, CollisionKind::Platform }; + grounded = onGround(movementController, movementController.position(), groundCollision); + } + if (*movementController.parameters().gravityEnabled && !grounded && !movementController.liquidMovement()) { + return {}; + } + m_startPosition = movementController.position(); + m_targetPosition = targetPosition; + m_pathFinder = make_shared<PathFinder>(m_world, movementController.position(), *m_targetPosition, movementController.baseParameters(), m_parameters); + } + + if (!m_pathFinder && m_path && m_edgeIndex == m_path->size()) + return true; // Reached goal + + if (m_pathFinder) { + auto explored = m_pathFinder->explore(movementController.baseParameters().pathExploreRate.value(100.0)); + if (explored) { + auto pathfinder = m_pathFinder; + m_pathFinder = {}; + + if (*explored) { + auto path = *pathfinder->result(); + + float newEdgeTimer = 0.0; + float newEdgeIndex = 0.0; + + // if we have a path already, see if our paths can be merged either by splicing or fast forwarding + bool merged = false; + if (m_path && !path.empty()) { + // try to fast forward on the new path + size_t i = 0; + // fast forward from current edge, or the last edge of the path + auto curEdgeIndex = min(m_edgeIndex, m_path->size() - 1); + auto& curEdge = m_path->at(curEdgeIndex); + for (auto& edge : path) { + if (curEdge.action == edge.action && curEdge.source.position == edge.source.position && edge.target.position == edge.target.position) { + // fast forward on the new path + newEdgeTimer = m_edgeTimer; + newEdgeIndex = i; + + merged = true; + break; + } + i++; + } + + if (!merged) { + // try to splice the new path onto the current path + auto& newPathStart = path.at(0); + for (size_t i = m_edgeIndex; i < m_path->size(); ++i) { + auto& edge = m_path->at(i); + if (edge.target.position == newPathStart.source.position) { + // splice the new path onto our current path up to this index + auto newPath = m_path->slice(0, i + 1); + newPath.appendAll(path); + path = newPath; + + newEdgeTimer = m_edgeTimer; + newEdgeIndex = m_edgeIndex; + + merged = true; + break; + } + i++; + } + } + } + + if (!merged && movementController.position() != *m_startPosition) { + // merging the paths failed, and the entity has moved from the path start position + // try to bridge the gap from the current position to the new path + auto bridgePathFinder = make_shared<PathFinder>(m_world, movementController.position(), *m_startPosition, movementController.baseParameters(), m_parameters); + auto explored = bridgePathFinder->explore(movementController.baseParameters().pathExploreRate.value(100.0)); + + if (explored && *explored) { + // concatenate the bridge path with the new path + auto newPath = *bridgePathFinder->result(); + newPath.appendAll(path); + path = newPath; + } else { + // if the gap isn't bridged in a single tick, reset and start over + reset(); + return {}; + } + } + + if(!path.empty() && !validateEdge(movementController, path[0])) { + // reset if the first edge is invalid + reset(); + return false; + } + + m_edgeTimer = newEdgeTimer; + m_edgeIndex = newEdgeIndex; + m_path = path; + if (m_path->empty()) + return true; + } else { + reset(); + return false; + } + } + } + return {}; +} + +Maybe<bool> PathController::move(ActorMovementController& movementController, ActorMovementParameters const& parameters, ActorMovementModifiers const& modifiers, bool run, float dt) { + using namespace PlatformerAStar; + + // pathfind to a new target position in the background while moving on the current path + if (m_pathFinder && m_targetPosition) + findPath(movementController, *m_targetPosition); + + if (!m_path) + return {}; + + m_controlFace = {}; + + while (m_edgeIndex < m_path->size()) { + auto& edge = m_path->at(m_edgeIndex); + Vec2F delta = m_world->geometry().diff(edge.target.position, edge.source.position); + + Vec2F sourceVelocity; + Vec2F targetVelocity; + switch (edge.action) { + case Action::Jump: + if (modifiers.jumpingSuppressed) { + reset(); + return {}; + } + break; + case Action::Arc: + sourceVelocity = edge.source.velocity.value(Vec2F()); + targetVelocity = edge.target.velocity.value(Vec2F()); + break; + case Action::Drop: + targetVelocity = edge.target.velocity.value(Vec2F()); + break; + case Action::Fly: + { + // accelerate along path using airForce + float angleFactor = movementController.velocity().normalized() * delta.normalized(); + float speedAlongAngle = angleFactor * movementController.velocity().magnitude(); + auto acc = parameters.airForce.value(0.0) / movementController.mass(); + sourceVelocity = delta.normalized() * fmin(parameters.flySpeed.value(0.0), speedAlongAngle + acc * WorldTimestep);; + targetVelocity = sourceVelocity; + } + break; + case Action::Swim: + sourceVelocity = targetVelocity = delta.normalized() * parameters.flySpeed.value(0.0f) * (1.0f - parameters.liquidImpedance.value(0.0f)); + break; + case Action::Walk: + sourceVelocity = delta.normalized() * (run ? parameters.runSpeed.value(0.0f) : parameters.walkSpeed.value(0.0f)); + sourceVelocity *= modifiers.speedModifier; + targetVelocity = sourceVelocity; + break; + default: {} + } + + Vec2F avgVelocity = (sourceVelocity + targetVelocity) / 2.0; + float avgSpeed = avgVelocity.magnitude(); + auto edgeTime = avgSpeed > 0.0f ? delta.magnitude() / avgSpeed : 0.2; + + auto edgeProgress = m_edgeTimer / edgeTime; + if (edgeProgress > 1.0f) { + m_edgeTimer -= edgeTime; + m_edgeIndex++; + if (m_edgeIndex < m_path->size()) { + if (!validateEdge(movementController, m_path->at(m_edgeIndex))) { + // Logger::info("Path invalidated on %s %s %s", ActionNames.getRight(nextEdge.action), nextEdge.source.position, nextEdge.target.position); + reset(); + return {}; + } + } + continue; + } + + auto curVelocity = sourceVelocity + ((targetVelocity - sourceVelocity) * edgeProgress); + movementController.setVelocity(curVelocity); + auto movement = (curVelocity + sourceVelocity) / 2.0 * m_edgeTimer; + movementController.setPosition(edge.source.position + movement); + + // Shows path and current step + // for (size_t i = 0; i < m_path->size(); i++) { + // auto debugEdge = m_path->get(i); + // SpatialLogger::logPoint("world", debugEdge.source.position, Color::Blue.toRgba()); + // SpatialLogger::logPoint("world", debugEdge.target.position, Color::Blue.toRgba()); + // SpatialLogger::logLine("world", debugEdge.source.position, debugEdge.target.position, Color::Blue.toRgba()); + + // if (i == m_edgeIndex) { + // Vec2F velocity = debugEdge.source.velocity.orMaybe(debugEdge.target.velocity).value({ 0.0, 0.0 }); + // SpatialLogger::logPoint("world", debugEdge.source.position, Color::Yellow.toRgba()); + // SpatialLogger::logLine("world", debugEdge.source.position, debugEdge.target.position, Color::Yellow.toRgba()); + // SpatialLogger::logText("world", strf("%s %s", ActionNames.getRight(debugEdge.action), curVelocity), debugEdge.source.position, Color::Yellow.toRgba()); + // } + // } + + if (auto direction = directionOf(delta[0])) + m_controlFace = direction; + + m_edgeTimer += dt; + return {}; + } + + if (auto lastEdge = m_path->maybeLast()) { + movementController.setPosition(lastEdge->target.position); + movementController.setVelocity(Vec2F()); + } + + // reached the end of the path, success unless we're also currently pathfinding to a new position + if (m_pathFinder) + return {}; + else + return true; +} + +bool PathController::validateEdge(ActorMovementController& movementController, PlatformerAStar::Edge const& edge) { + using namespace PlatformerAStar; + + auto const groundCollision = CollisionSet{ CollisionKind::Null, CollisionKind::Block, CollisionKind::Slippery, CollisionKind::Platform }; + auto const solidCollision = CollisionSet{ CollisionKind::Null, CollisionKind::Block, CollisionKind::Slippery }; + + auto const openDoors = [&](RectF const& bounds) { + auto objects = m_world->entityQuery(bounds, entityTypeFilter<Object>()); + auto opened = objects.filtered([&](EntityPtr const& e) -> bool { + if (auto object = as<Object>(e)) { + if (object->isMaster()) { + auto arg = m_world->luaRoot()->luaEngine().createString("closedDoor"); + auto res = object->callScript("hasCapability", LuaVariadic<LuaValue>{arg}); + if (res && res->is<LuaBoolean>() && res->get<LuaBoolean>()){ + m_world->sendEntityMessage(e->entityId(), "openDoor"); + return true; + } + } + } + return false; + }); + return !opened.empty(); + }; + + auto poly = movementController.collisionPoly(); + poly.translate(edge.target.position); + if (m_world->polyCollision(poly) || movingCollision(movementController, poly)) { + auto bounds = RectI::integral(poly.boundBox()); + // for (auto line : bounds.edges()) { + // SpatialLogger::logLine("world", Line2F(line), Color::Magenta.toRgba()); + // } + if (m_world->rectTileCollision(bounds) && !m_world->rectTileCollision(bounds, solidCollision)) { + if (!openDoors(poly.boundBox())) { + // SpatialLogger::logPoly("world", poly, Color::Yellow.toRgba()); + return false; + } + } else { + // SpatialLogger::logPoly("world", poly, Color::Red.toRgba()); + return false; + } + } + //SpatialLogger::logPoly("world", poly, Color::Blue.toRgba()); + + auto inLiquid = [&](Vec2F const& position) -> bool { + auto bounds = movementController.localBoundBox().translated(position); + auto liquidLevel = m_world->liquidLevel(bounds); + if (liquidLevel.level >= movementController.baseParameters().minimumLiquidPercentage.value(1.0)) { + // for (auto line : bounds.edges()) { + // SpatialLogger::logLine("world", line, Color::Blue.toRgba()); + // } + return true; + } else { + // for (auto line : bounds.edges()) { + // SpatialLogger::logLine("world", line, Color::Red.toRgba()); + // } + return false; + } + }; + + switch (edge.action) { + case Action::Walk: + return onGround(movementController, edge.source.position, groundCollision); + case Action::Swim: + return inLiquid(edge.target.position); + case Action::Land: + return onGround(movementController, edge.target.position, groundCollision) || inLiquid(edge.target.position); + case Action::Drop: + return onGround(movementController, edge.source.position, groundCollision) && !onGround(movementController, edge.source.position, solidCollision); + default: + return true; + } +} + +bool PathController::movingCollision(ActorMovementController& movementController, PolyF const& collisionPoly) { + bool collided = false; + movementController.forEachMovingCollision(collisionPoly.boundBox(), [&](MovingCollisionId id, PhysicsMovingCollision mc, PolyF poly, RectF bounds) { + if (poly.intersects(collisionPoly)) { + // set collided and stop iterating + collided = true; + return false; + } + return true; + }); + return collided; +} + +bool PathController::onGround(ActorMovementController const& movementController, Vec2F const& position, CollisionSet const& collisionSet) const { + auto bounds = RectI::integral(movementController.localBoundBox().translated(position)); + Vec2I min = Vec2I(bounds.xMin(), bounds.yMin() - 1); + Vec2I max = Vec2I(bounds.xMax(), bounds.yMin()); + // for (auto line : RectF(Vec2F(min), Vec2F(max)).edges()) { + // SpatialLogger::logLine("world", line, m_world->rectTileCollision(RectI(min, max), collisionSet) ? Color::Blue.toRgba() : Color::Red.toRgba()); + // } + return m_world->rectTileCollision(RectI(min, max), collisionSet); +} + +} |