osb/source/game/StarActorMovementController.cpp

1474 lines
58 KiB
C++
Raw Normal View History

2023-06-20 04:33:09 +00:00
#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);
}
}