#include "framework.h" #include "Game/Lara/lara_tests.h" #include "Game/animation.h" #include "Game/collision/collide_room.h" #include "Game/collision/collide_item.h" #include "Game/control/control.h" #include "Game/control/los.h" #include "Game/items.h" #include "Game/Lara/lara.h" #include "Game/Lara/lara_climb.h" #include "Game/Lara/lara_collide.h" #include "Game/Lara/lara_flare.h" #include "Game/Lara/lara_helpers.h" #include "Game/Lara/lara_monkey.h" #include "Renderer/Renderer11.h" #include "Scripting/GameFlowScript.h" #include "Specific/input.h" #include "Specific/level.h" using namespace TEN::Renderer; using namespace TEN::Floordata; // ----------------------------- // TEST FUNCTIONS // For State Control & Collision // ----------------------------- // Test if a ledge in front of item is valid to climb. bool TestValidLedge(ITEM_INFO* item, COLL_INFO* coll, bool ignoreHeadroom, bool heightLimit) { // Determine probe base left/right points int xl = phd_sin(coll->NearestLedgeAngle - ANGLE(90)) * coll->Setup.Radius; int zl = phd_cos(coll->NearestLedgeAngle - ANGLE(90)) * coll->Setup.Radius; int xr = phd_sin(coll->NearestLedgeAngle + ANGLE(90)) * coll->Setup.Radius; int zr = phd_cos(coll->NearestLedgeAngle + ANGLE(90)) * coll->Setup.Radius; // Determine probe top point int y = item->pos.yPos - coll->Setup.Height; // Get frontal collision data auto frontLeft = GetCollisionResult(item->pos.xPos + xl, y, item->pos.zPos + zl, GetRoom(item->location, item->pos.xPos, y, item->pos.zPos).roomNumber); auto frontRight = GetCollisionResult(item->pos.xPos + xr, y, item->pos.zPos + zr, GetRoom(item->location, item->pos.xPos, y, item->pos.zPos).roomNumber); // If any of the frontal collision results intersects item bounds, return false, because there is material intersection. // This check helps to filter out cases when Lara is formally facing corner but ledge check returns true because probe distance is fixed. if (frontLeft.Position.Floor < (item->pos.yPos - CLICK(0.5f)) || frontRight.Position.Floor < (item->pos.yPos - CLICK(0.5f))) return false; if (frontLeft.Position.Ceiling > (item->pos.yPos - coll->Setup.Height) || frontRight.Position.Ceiling > (item->pos.yPos - coll->Setup.Height)) return false; //g_Renderer.addDebugSphere(Vector3(item->pos.xPos + xl, left, item->pos.zPos + zl), 64, Vector4::One, RENDERER_DEBUG_PAGE::LOGIC_STATS); //g_Renderer.addDebugSphere(Vector3(item->pos.xPos + xr, right, item->pos.zPos + zr), 64, Vector4::One, RENDERER_DEBUG_PAGE::LOGIC_STATS); // Determine ledge probe embed offset. // We use 0.2f radius extents here for two purposes. First - we can't guarantee that shifts weren't already applied // and misfire may occur. Second - it guarantees that Lara won't land on a very thin edge of diagonal geometry. int xf = phd_sin(coll->NearestLedgeAngle) * (coll->Setup.Radius * 1.2f); int zf = phd_cos(coll->NearestLedgeAngle) * (coll->Setup.Radius * 1.2f); // Get floor heights at both points auto left = GetCollisionResult(item->pos.xPos + xf + xl, y, item->pos.zPos + zf + zl, GetRoom(item->location, item->pos.xPos, y, item->pos.zPos).roomNumber).Position.Floor; auto right = GetCollisionResult(item->pos.xPos + xf + xr, y, item->pos.zPos + zf + zr, GetRoom(item->location, item->pos.xPos, y, item->pos.zPos).roomNumber).Position.Floor; // If specified, limit vertical search zone only to nearest height if (heightLimit && (abs(left - y) > CLICK(0.5f) || abs(right - y) > CLICK(0.5f))) return false; // Determine allowed slope difference for a given collision radius auto slopeDelta = ((float)STEPUP_HEIGHT / (float)WALL_SIZE) * (coll->Setup.Radius * 2); // Discard if there is a slope beyond tolerance delta if (abs(left - right) >= slopeDelta) return false; // Discard if ledge is not within distance threshold if (abs(coll->NearestLedgeDistance) > OFFSET_RADIUS(coll->Setup.Radius)) return false; // Discard if ledge is not within angle threshold if (!TestValidLedgeAngle(item, coll)) return false; if (!ignoreHeadroom) { auto headroom = (coll->Front.Floor + coll->Setup.Height) - coll->Middle.Ceiling; if (headroom < CLICK(1)) return false; } return true; } bool TestValidLedgeAngle(ITEM_INFO* item, COLL_INFO* coll) { return abs((short)(coll->NearestLedgeAngle - item->pos.yRot)) <= LARA_GRAB_THRESHOLD; } bool TestLaraKeepLow(ITEM_INFO* item, COLL_INFO* coll) { // TODO: Temporary. coll->Setup.Radius is currently only set to // LARA_RAD_CRAWL in the collision function, then reset by LaraAboveWater(). // For tests called in control functions, then, it will store the wrong radius. @Sezz 2021.11.05 auto radius = (item->activeState == LS_CROUCH_IDLE || item->activeState == LS_CROUCH_TURN_LEFT || item->activeState == LS_CROUCH_TURN_RIGHT) ? LARA_RAD : LARA_RAD_CRAWL; auto y = item->pos.yPos; auto probeFront = GetCollisionResult(item, item->pos.yRot, radius, -coll->Setup.Height); auto probeBack = GetCollisionResult(item, item->pos.yRot + ANGLE(180.0f), radius, -coll->Setup.Height); auto probeMiddle = GetCollisionResult(item); // TODO: Cannot use as a failsafe in standing states; bugged with slanted ceilings reaching the ground. // In common setups, Lara may embed on such ceilings, resulting in inappropriate crouch state dispatches. @Sezz 2021.10.15 if ((probeFront.Position.Ceiling - y) >= -LARA_HEIGHT || // Front is not a clamp. (probeBack.Position.Ceiling - y) >= -LARA_HEIGHT || // Back is not a clamp. (probeMiddle.Position.Ceiling - y) >= -LARA_HEIGHT) // Middle is not a clamp. { return true; } return false; } bool TestLaraSlide(ITEM_INFO* item, COLL_INFO* coll) { if (GetCollisionResult(item).Position.FloorSlope && !TestEnvironment(ENV_FLAG_SWAMP, item)) { return true; } return false; } bool TestLaraHangJumpUp(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; if (!(TrInput & IN_ACTION) || info->gunStatus != LG_HANDS_FREE || coll->HitStatic) return false; if (TestLaraMonkeyGrab(item, coll)) { SetAnimation(item, LA_JUMP_UP_TO_MONKEY); item->airborne = false; item->speed = 0; item->fallspeed = 0; item->pos.yPos += coll->Middle.Ceiling + (LARA_HEIGHT_MONKEY - coll->Setup.Height); info->gunStatus = LG_HANDS_BUSY; return true; } if (coll->Middle.Ceiling > -STEPUP_HEIGHT || coll->CollisionType != CT_FRONT) return false; int edge; auto edgeCatch = TestLaraEdgeCatch(item, coll, &edge); if (!edgeCatch) return false; bool ladder = TestLaraHangOnClimbWall(item, coll); if (!(ladder && edgeCatch) && !(TestValidLedge(item, coll, true, true) && edgeCatch > 0)) { return false; } SetAnimation(item, LA_REACH_TO_HANG, 12); auto bounds = GetBoundsAccurate(item); if (edgeCatch <= 0) item->pos.yPos = edge - bounds->Y1 + 4; else item->pos.yPos += coll->Front.Floor - bounds->Y1; if (ladder) SnapItemToGrid(item, coll); // HACK: until fragile ladder code is refactored, we must exactly snap to grid. else SnapItemToLedge(item, coll); item->airborne = false; item->speed = 0; item->fallspeed = 0; info->gunStatus = LG_HANDS_BUSY; info->torsoXrot = 0; info->torsoYrot = 0; info->torsoZrot = 0; return true; } bool TestLaraHangJump(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; if (!(TrInput & IN_ACTION) || info->gunStatus != LG_HANDS_FREE || coll->HitStatic) return false; if (TestLaraMonkeyGrab(item, coll)) { SetAnimation(item, LA_REACH_TO_MONKEY); ResetLaraFlex(item); item->airborne = false; item->speed = 0; item->fallspeed = 0; item->pos.yPos += coll->Middle.Ceiling + (LARA_HEIGHT_MONKEY - coll->Setup.Height); info->gunStatus = LG_HANDS_BUSY; return true; } if (coll->Middle.Ceiling > -STEPUP_HEIGHT || coll->Middle.Floor < 200 || coll->CollisionType != CT_FRONT) { return false; } int edge; auto edgeCatch = TestLaraEdgeCatch(item, coll, &edge); if (!edgeCatch) return false; bool ladder = TestLaraHangOnClimbWall(item, coll); if (!(ladder && edgeCatch) && !(TestValidLedge(item, coll, true, true) && edgeCatch > 0)) { return false; } if (TestHangSwingIn(item)) { SetAnimation(item, LA_REACH_TO_HANG_OSCILLATE); ResetLaraFlex(item); } else SetAnimation(item, LA_REACH_TO_HANG); auto bounds = GetBoundsAccurate(item); if (edgeCatch <= 0) { item->pos.yPos = edge - bounds->Y1 - 20; item->pos.yRot = coll->NearestLedgeAngle; } else item->pos.yPos += coll->Front.Floor - bounds->Y1 - 20; if (ladder) SnapItemToGrid(item, coll); // HACK: until fragile ladder code is refactored, we must exactly snap to grid. else SnapItemToLedge(item, coll, 0.2f); item->airborne = true; item->speed = 2; item->fallspeed = 1; info->gunStatus = LG_HANDS_BUSY; return true; } bool TestLaraHang(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; auto angle = info->moveAngle; auto climbShift = 0; if (info->moveAngle == (short)(item->pos.yRot - ANGLE(90.0f))) climbShift = -coll->Setup.Radius; else if (info->moveAngle == (short)(item->pos.yRot + ANGLE(90.0f))) climbShift = coll->Setup.Radius; // Temporarily move item a bit closer to the wall to get more precise coll results auto oldPos = item->pos; item->pos.xPos += phd_sin(item->pos.yRot) * coll->Setup.Radius * 0.5f; item->pos.zPos += phd_cos(item->pos.yRot) * coll->Setup.Radius * 0.5f; // Get height difference with side spaces (left or right, depending on movement direction) auto hdif = LaraFloorFront(item, info->moveAngle, coll->Setup.Radius * 1.4f); // Set stopped flag, if floor height is above footspace which is step size auto stopped = hdif < CLICK(0.5f); // Set stopped flag, if ceiling height is below headspace which is step size if (LaraCeilingFront(item, info->moveAngle, coll->Setup.Radius * 1.5f, 0) > -950) stopped = true; // Backup item pos to restore it after coll tests item->pos = oldPos; // Setup coll info info->moveAngle = item->pos.yRot; coll->Setup.LowerFloorBound = NO_LOWER_BOUND; coll->Setup.UpperFloorBound = -STEPUP_HEIGHT; coll->Setup.LowerCeilingBound = 0; coll->Setup.ForwardAngle = info->moveAngle; // When Lara is about to move, use larger embed offset for stabilizing diagonal shimmying) auto embedOffset = 4; if (TrInput & (IN_LEFT | IN_RIGHT)) embedOffset = 16; item->pos.xPos += phd_sin(item->pos.yRot) * embedOffset; item->pos.zPos += phd_cos(item->pos.yRot) * embedOffset; GetCollisionInfo(coll, item); bool result = false; if (info->climbStatus) // Ladder case { if (TrInput & IN_ACTION && item->hitPoints > 0) { info->moveAngle = angle; if (!TestLaraHangOnClimbWall(item, coll)) { if (item->animNumber != LA_LADDER_TO_HANG_RIGHT && item->animNumber != LA_LADDER_TO_HANG_LEFT) { LaraSnapToEdgeOfBlock(item, coll, GetQuadrant(item->pos.yRot)); item->pos.yPos = coll->Setup.OldPosition.y; SetAnimation(item, LA_REACH_TO_HANG, 21); } result = true; } else { if (item->animNumber == LA_REACH_TO_HANG && item->frameNumber == GetFrameNumber(item, 21) && TestLaraClimbStance(item, coll)) { item->targetState = LS_LADDER_IDLE; } } } else // Death or action release { SetAnimation(item, LA_FALL_START); item->pos.yPos += 256; item->airborne = true; item->speed = 2; item->fallspeed = 1; info->gunStatus = LG_HANDS_FREE; } } else // Normal case { if (TrInput & IN_ACTION && item->hitPoints > 0 && coll->Front.Floor <= 0) { if (stopped && hdif > 0 && climbShift != 0 && (climbShift > 0 == coll->MiddleLeft.Floor > coll->MiddleRight.Floor)) stopped = false; auto verticalShift = coll->Front.Floor - GetBoundsAccurate(item)->Y1; auto x = item->pos.xPos; auto z = item->pos.zPos; info->moveAngle = angle; if (climbShift != 0) { auto s = phd_sin(info->moveAngle); auto c = phd_cos(info->moveAngle); auto testShift = Vector2(s * climbShift, c * climbShift); x += testShift.x; z += testShift.y; } if ((256 << GetQuadrant(item->pos.yRot)) & GetClimbFlags(x, item->pos.yPos, z, item->roomNumber)) { if (!TestLaraHangOnClimbWall(item, coll)) verticalShift = 0; // Ignore vertical shift if ladder is encountered next block } else if (!TestValidLedge(item, coll, true)) { if ((climbShift < 0 && coll->FrontLeft.Floor != coll->Front.Floor) || (climbShift > 0 && coll->FrontRight.Floor != coll->Front.Floor)) { stopped = true; } } if (!stopped && coll->Middle.Ceiling < 0 && coll->CollisionType == CT_FRONT && !coll->HitStatic && abs(verticalShift) < SLOPE_DIFFERENCE && TestValidLedgeAngle(item, coll)) { if (item->speed != 0) SnapItemToLedge(item, coll); item->pos.yPos += verticalShift; } else { item->pos.xPos = coll->Setup.OldPosition.x; item->pos.yPos = coll->Setup.OldPosition.y; item->pos.zPos = coll->Setup.OldPosition.z; if (item->activeState == LS_SHIMMY_LEFT || item->activeState == LS_SHIMMY_RIGHT) { SetAnimation(item, LA_REACH_TO_HANG, 21); } result = true; } } else // Death, incorrect ledge or ACTION release { SetAnimation(item, LA_JUMP_UP, 9); item->pos.xPos += coll->Shift.x; item->pos.yPos += GetBoundsAccurate(item)->Y2 * 1.8f; item->pos.zPos += coll->Shift.z; item->airborne = true; item->speed = 2; item->fallspeed = 1; info->gunStatus = LG_HANDS_FREE; } } return result; } CornerTestResult TestItemAtNextCornerPosition(ITEM_INFO* item, COLL_INFO* coll, float angle, bool outer) { auto result = CornerTestResult(); // Determine real turning angle auto turnAngle = outer ? angle : -angle; // Backup previous position into array PHD_3DPOS pos[3] = { item->pos, item->pos, item->pos }; // Do a two-step rotation check. First step is real resulting position, and second step is probing // position. We need this because checking at exact ending position does not always return // correct results with nearest ledge angle. for (int i = 0; i < 2; i++) { // Determine collision box anchor point and rotate collision box around this anchor point. // Then determine new test position from centerpoint of new collision box position. // Push back item a bit to compensate for possible edge ledge cases pos[i].xPos -= round((coll->Setup.Radius * (outer ? -0.2f : 0.2f)) * phd_sin(pos[i].yRot)); pos[i].zPos -= round((coll->Setup.Radius * (outer ? -0.2f : 0.2f)) * phd_cos(pos[i].yRot)); // Move item at the distance of full collision diameter plus half-radius margin to movement direction pos[i].xPos += round((coll->Setup.Radius * (i == 0 ? 2.0f : 2.5f)) * phd_sin(Lara.moveAngle)); pos[i].zPos += round((coll->Setup.Radius * (i == 0 ? 2.0f : 2.5f)) * phd_cos(Lara.moveAngle)); // Determine anchor point auto cX = pos[i].xPos + round(coll->Setup.Radius * phd_sin(pos[i].yRot)); auto cZ = pos[i].zPos + round(coll->Setup.Radius * phd_cos(pos[i].yRot)); cX += (coll->Setup.Radius * phd_sin(pos[i].yRot + ANGLE(90.0f * -std::copysign(1.0f, angle)))); cZ += (coll->Setup.Radius * phd_cos(pos[i].yRot + ANGLE(90.0f * -std::copysign(1.0f, angle)))); // Determine distance from anchor point to new item position auto dist = Vector2(pos[i].xPos, pos[i].zPos) - Vector2(cX, cZ); auto s = phd_sin(ANGLE(turnAngle)); auto c = phd_cos(ANGLE(turnAngle)); // Shift item to a new anchor point pos[i].xPos = dist.x * c - dist.y * s + cX; pos[i].zPos = dist.x * s + dist.y * c + cZ; // Virtually rotate item to new angle short newAngle = pos[i].yRot - ANGLE(turnAngle); pos[i].yRot = newAngle; // Snap to nearest ledge, if any. item->pos = pos[i]; SnapItemToLedge(item, coll, item->pos.yRot); // Copy resulting position to an array and restore original item position. pos[i] = item->pos; item->pos = pos[2]; if (i == 1) // Both passes finished, construct the result. { result.RealPositionResult = pos[0]; result.ProbeResult = pos[1]; result.Success = newAngle == pos[i].yRot; } } return result; } CORNER_RESULT TestLaraHangCorner(ITEM_INFO* item, COLL_INFO* coll, float testAngle) { LaraInfo*& info = item->data; // Lara isn't in stop state yet, bypass test if (item->animNumber != LA_REACH_TO_HANG) return CORNER_RESULT::NONE; // Static is in the way, bypass test if (coll->HitStatic) return CORNER_RESULT::NONE; // INNER CORNER TESTS // Backup old Lara position and frontal collision auto oldPos = item->pos; auto oldMoveAngle = Lara.moveAngle; auto cornerResult = TestItemAtNextCornerPosition(item, coll, testAngle, false); // Do further testing only if test angle is equal to resulting edge angle if (cornerResult.Success) { // Get bounding box height for further ledge height calculations auto bounds = GetBoundsAccurate(item); // Store next position item->pos = cornerResult.RealPositionResult; info->nextCornerPos.xPos = item->pos.xPos; info->nextCornerPos.yPos = LaraCollisionAboveFront(item, item->pos.yRot, coll->Setup.Radius * 2, abs(bounds->Y1) + LARA_HEADROOM).Position.Floor + abs(bounds->Y1); info->nextCornerPos.zPos = item->pos.zPos; info->nextCornerPos.yRot = item->pos.yRot; info->moveAngle = item->pos.yRot; item->pos = cornerResult.ProbeResult; auto result = TestLaraValidHangPos(item, coll); // Restore original item positions item->pos = oldPos; info->moveAngle = oldMoveAngle; if (result) return CORNER_RESULT::INNER; if (info->climbStatus) { auto& angleSet = testAngle > 0 ? LeftExtRightIntTab : LeftIntRightExtTab; if (GetClimbFlags(Lara.nextCornerPos.xPos, item->pos.yPos, Lara.nextCornerPos.zPos, item->roomNumber) & (short)angleSet[GetQuadrant(item->pos.yRot)]) { Lara.nextCornerPos.yPos = item->pos.yPos; // Restore original Y pos for ladder tests because we don't snap to ledge height in such case. return CORNER_RESULT::INNER; } } } // Restore original item positions item->pos = oldPos; info->moveAngle = oldMoveAngle; // OUTER CORNER TESTS // Test if there's a material obstacles blocking outer corner pathway if ((LaraFloorFront(item, item->pos.yRot + ANGLE(testAngle), coll->Setup.Radius + CLICK(1)) < 0) || (LaraCeilingFront(item, item->pos.yRot + ANGLE(testAngle), coll->Setup.Radius + CLICK(1), coll->Setup.Height) > 0)) return CORNER_RESULT::NONE; // Last chance for possible diagonal vs. non-diagonal cases: ray test if (! LaraPositionOnLOS(item, item->pos.yRot + ANGLE(testAngle), coll->Setup.Radius + CLICK(1))) return CORNER_RESULT::NONE; cornerResult = TestItemAtNextCornerPosition(item, coll, testAngle, true); // Additional test if there's a material obstacles blocking outer corner pathway if ((LaraFloorFront(item, item->pos.yRot, 0) < 0) || (LaraCeilingFront(item, item->pos.yRot, 0, coll->Setup.Height) > 0)) cornerResult.Success = false; // Do further testing only if test angle is equal to resulting edge angle if (cornerResult.Success) { // Get bounding box height for further ledge height calculations auto bounds = GetBoundsAccurate(item); // Store next position item->pos = cornerResult.RealPositionResult; info->nextCornerPos.xPos = item->pos.xPos; info->nextCornerPos.yPos = LaraCollisionAboveFront(item, item->pos.yRot, coll->Setup.Radius * 2, abs(bounds->Y1) + LARA_HEADROOM).Position.Floor + abs(bounds->Y1); info->nextCornerPos.zPos = item->pos.zPos; info->nextCornerPos.yRot = item->pos.yRot; info->moveAngle = item->pos.yRot; item->pos = cornerResult.ProbeResult; auto result = TestLaraValidHangPos(item, coll); // Restore original item positions item->pos = oldPos; info->moveAngle = oldMoveAngle; if (result) return CORNER_RESULT::OUTER; if (info->climbStatus) { auto& angleSet = testAngle > 0 ? LeftIntRightExtTab : LeftExtRightIntTab; if (GetClimbFlags(Lara.nextCornerPos.xPos, item->pos.yPos, Lara.nextCornerPos.zPos, item->roomNumber) & (short)angleSet[GetQuadrant(item->pos.yRot)]) { Lara.nextCornerPos.yPos = item->pos.yPos; // Restore original Y pos for ladder tests because we don't snap to ledge height in such case. return CORNER_RESULT::OUTER; } } } // Restore original item positions item->pos = oldPos; info->moveAngle = oldMoveAngle; return CORNER_RESULT::NONE; } bool TestLaraValidHangPos(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; // Get incoming ledge height and own Lara's upper bound. // First one will be negative while first one is positive. // Difference between two indicates difference in height between ledges. auto frontFloor = LaraCollisionAboveFront(item, info->moveAngle, coll->Setup.Radius + CLICK(0.5f), LARA_HEIGHT).Position.Floor; auto laraUpperBound = item->pos.yPos - coll->Setup.Height; // If difference is above 1/2 click, return false (ledge is out of reach). if (abs(frontFloor - laraUpperBound) > CLICK(0.5f)) return false; // Embed Lara into wall to make collision test succeed item->pos.xPos += phd_sin(item->pos.yRot) * 8; item->pos.zPos += phd_cos(item->pos.yRot) * 8; // Setup new GCI call info->moveAngle = item->pos.yRot; coll->Setup.LowerFloorBound = NO_LOWER_BOUND; coll->Setup.UpperFloorBound = -CLICK(2); coll->Setup.LowerCeilingBound = 0; coll->Setup.Mode = COLL_PROBE_MODE::FREE_FLAT; coll->Setup.ForwardAngle = info->moveAngle; GetCollisionInfo(coll, item); // Filter out narrow ceiling spaces, no collision cases and statics in front. if (coll->Middle.Ceiling >= 0 || coll->CollisionType != CT_FRONT || coll->HitStatic) return false; // Finally, do ordinary ledge checks (slope difference etc.) return TestValidLedge(item, coll); } bool TestLaraClimbStance(ITEM_INFO* item, COLL_INFO* coll) { int shiftRight, shiftLeft; if (LaraTestClimbPos(item, coll->Setup.Radius, coll->Setup.Radius + CLICK(0.5f), -700, CLICK(2), &shiftRight) != 1) return false; if (LaraTestClimbPos(item, coll->Setup.Radius, -(coll->Setup.Radius + CLICK(0.5f)), -700, CLICK(2), &shiftLeft) != 1) return false; if (shiftRight) { if (shiftLeft) { if (shiftRight < 0 != shiftLeft < 0) return false; if ((shiftRight < 0 && shiftLeft < shiftRight) || (shiftRight > 0 && shiftLeft > shiftRight)) { item->pos.yPos += shiftLeft; return true; } } item->pos.yPos += shiftRight; } else if (shiftLeft) item->pos.yPos += shiftLeft; return true; } bool TestLaraHangOnClimbWall(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; int shift, result; if (!info->climbStatus) return false; if (item->fallspeed < 0) return false; // HACK: Climb wall tests are highly fragile and depend on quadrant shifts. // Until climb wall tests are fully refactored, we need to recalculate COLL_INFO. auto coll2 = *coll; coll2.Setup.Mode = COLL_PROBE_MODE::QUADRANTS; GetCollisionInfo(&coll2, item); switch (GetQuadrant(item->pos.yRot)) { case NORTH: case SOUTH: item->pos.zPos += coll2.Shift.z; break; case EAST: case WEST: item->pos.xPos += coll2.Shift.x; break; default: break; } auto bounds = GetBoundsAccurate(item); if (info->moveAngle != item->pos.yRot) { short l = LaraCeilingFront(item, item->pos.yRot, 0, 0); short r = LaraCeilingFront(item, info->moveAngle, CLICK(0.5f), 0); if (abs(l - r) > SLOPE_DIFFERENCE) return false; } if (LaraTestClimbPos(item, LARA_RAD, LARA_RAD, bounds->Y1, bounds->Y2 - bounds->Y1, &shift) && LaraTestClimbPos(item, LARA_RAD, -LARA_RAD, bounds->Y1, bounds->Y2 - bounds->Y1, &shift)) { result = LaraTestClimbPos(item, LARA_RAD, 0, bounds->Y1, bounds->Y2 - bounds->Y1, &shift); if (result) { if (result != 1) item->pos.yPos += shift; return true; } } return false; } int TestLaraEdgeCatch(ITEM_INFO* item, COLL_INFO* coll, int* edge) { BOUNDING_BOX* bounds = GetBoundsAccurate(item); int hdif = coll->Front.Floor - bounds->Y1; if (hdif < 0 == hdif + item->fallspeed < 0) { hdif = item->pos.yPos + bounds->Y1; if ((hdif + item->fallspeed & 0xFFFFFF00) != (hdif & 0xFFFFFF00)) { if (item->fallspeed > 0) *edge = (hdif + item->fallspeed) & 0xFFFFFF00; else *edge = hdif & 0xFFFFFF00; return -1; } return 0; } if (!TestValidLedge(item, coll, true)) return 0; return 1; } bool TestHangSwingIn(ITEM_INFO* item) { LaraInfo*& info = item->data; int y = item->pos.yPos; auto probe = GetCollisionResult(item, item->pos.yRot, CLICK(0.5f)); if ((probe.Position.Floor - y) > 0 && (probe.Position.Ceiling - y) < -400 && probe.Position.Floor != NO_HEIGHT) { return true; } return false; } bool TestLaraHangSideways(ITEM_INFO* item, COLL_INFO* coll, short angle) { LaraInfo*& info = item->data; auto oldPos = item->pos; info->moveAngle = item->pos.yRot + angle; static constexpr auto sidewayTestDistance = 16; item->pos.xPos += phd_sin(info->moveAngle) * sidewayTestDistance; item->pos.zPos += phd_cos(info->moveAngle) * sidewayTestDistance; coll->Setup.OldPosition.y = item->pos.yPos; bool res = TestLaraHang(item, coll); item->pos = oldPos; return !res; } bool TestLaraFacingCorner(ITEM_INFO* item, short angle, int dist) { short angle1 = angle + ANGLE(15.0f); short angle2 = angle - ANGLE(15.0f); auto start = GAME_VECTOR(item->pos.xPos, item->pos.yPos - STEPUP_HEIGHT, item->pos.zPos, item->roomNumber); auto end1 = GAME_VECTOR(item->pos.xPos + dist * phd_sin(angle1), item->pos.yPos - STEPUP_HEIGHT, item->pos.zPos + dist * phd_cos(angle1), item->roomNumber); auto end2 = GAME_VECTOR(item->pos.xPos + dist * phd_sin(angle2), item->pos.yPos - STEPUP_HEIGHT, item->pos.zPos + dist * phd_cos(angle2), item->roomNumber); bool result1 = LOS(&start, &end1); bool result2 = LOS(&start, &end2); return (!result1 && !result2); } bool LaraPositionOnLOS(ITEM_INFO* item, short ang, int dist) { auto start1 = GAME_VECTOR(item->pos.xPos, item->pos.yPos - LARA_HEADROOM, item->pos.zPos, item->roomNumber); auto start2 = GAME_VECTOR(item->pos.xPos, item->pos.yPos - LARA_HEIGHT + LARA_HEADROOM, item->pos.zPos, item->roomNumber); auto end1 = GAME_VECTOR(item->pos.xPos + dist * phd_sin(ang), item->pos.yPos - LARA_HEADROOM, item->pos.zPos + dist * phd_cos(ang), item->roomNumber); auto end2 = GAME_VECTOR(item->pos.xPos + dist * phd_sin(ang), item->pos.yPos - LARA_HEIGHT + LARA_HEADROOM, item->pos.zPos + dist * phd_cos(ang), item->roomNumber); auto result1 = LOS(&start1, &end1); auto result2 = LOS(&start2, &end2); return (!result1 && !result2); } int LaraFloorFront(ITEM_INFO* item, short ang, int dist) { return LaraCollisionFront(item, ang, dist).Position.Floor; } COLL_RESULT LaraCollisionFront(ITEM_INFO* item, short ang, int dist) { auto probe = GetCollisionResult(item, ang, dist, -LARA_HEIGHT); if (probe.Position.Floor != NO_HEIGHT) probe.Position.Floor -= item->pos.yPos; return probe; } COLL_RESULT LaraCollisionAboveFront(ITEM_INFO* item, short ang, int dist, int h) { int x = item->pos.xPos + dist * phd_sin(ang); int y = item->pos.yPos - h; int z = item->pos.zPos + dist * phd_cos(ang); return GetCollisionResult(x, y, z, GetCollisionResult(item->pos.xPos, y, item->pos.zPos, item->roomNumber).RoomNumber); } int LaraCeilingFront(ITEM_INFO* item, short ang, int dist, int h) { return LaraCeilingCollisionFront(item, ang, dist, h).Position.Ceiling; } COLL_RESULT LaraCeilingCollisionFront(ITEM_INFO* item, short ang, int dist, int h) { auto probe = GetCollisionResult(item, ang, dist, -h); if (probe.Position.Ceiling != NO_HEIGHT) probe.Position.Ceiling += h - item->pos.yPos; return probe; } bool TestLaraFall(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; if (coll->Middle.Floor <= STEPUP_HEIGHT || info->waterStatus == LW_WADE) // TODO: This causes a legacy floor snap bug when Lara wades off a ledge into a dry room. @Sezz 2021.09.26 { return false; } return true; } bool TestLaraMonkeyGrab(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; if (info->canMonkeySwing && abs(coll->Middle.Ceiling) <= CLICK(0.5f) && (coll->Middle.Ceiling >= 0 || coll->CollisionType == CT_TOP || coll->CollisionType == CT_TOP_FRONT) && abs(coll->Middle.Ceiling + coll->Middle.Floor + coll->Setup.Height) > LARA_HEIGHT_MONKEY) { return true; } return false; } bool TestLaraMonkeyFall(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; int y = item->pos.yPos - LARA_HEIGHT_MONKEY; auto probe = GetCollisionResult(item); if (!info->canMonkeySwing || // No monkey sector. (probe.Position.Ceiling - y) > CLICK(1.25f) || // Outside lower bound. (probe.Position.Ceiling - y) < -CLICK(1.25f) || // Outside upper bound. probe.Position.CeilingSlope || // Is ceiling slope. probe.Position.Ceiling == NO_HEIGHT) { return true; } return false; } bool TestLaraLand(ITEM_INFO* item, COLL_INFO* coll) { int heightFromFloor = GetCollisionResult(item).Position.Floor - item->pos.yPos; if (item->airborne && item->fallspeed >= 0 && (heightFromFloor <= std::min(item->fallspeed, STEPUP_HEIGHT) || TestEnvironment(ENV_FLAG_SWAMP, item))) { return true; } return false; } bool TestLaraWaterStepOut(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; if (coll->CollisionType == CT_FRONT || coll->Middle.FloorSlope || coll->Middle.Floor >= 0) { return false; } if (coll->Middle.Floor >= -CLICK(0.5f)) SetAnimation(item, LA_STAND_IDLE); else { SetAnimation(item, LA_ONWATER_TO_WADE_1CLICK); item->targetState = LS_IDLE; } item->pos.yPos += coll->Middle.Floor + CLICK(2.75f) - 9; UpdateItemRoom(item, -(STEPUP_HEIGHT - 3)); item->pos.zRot = 0; item->pos.xRot = 0; item->airborne = false; item->speed = 0; item->fallspeed = 0; info->waterStatus = LW_WADE; return true; } bool TestLaraWaterClimbOut(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; if (coll->CollisionType != CT_FRONT || !(TrInput & IN_ACTION)) return false; if (info->gunStatus && (info->gunStatus != LG_READY || info->gunType != WEAPON_FLARE)) { return false; } if (coll->Middle.Ceiling > -STEPUP_HEIGHT) return false; int frontFloor = coll->Front.Floor + LARA_HEIGHT_SURFSWIM; if (frontFloor <= -CLICK(2) || frontFloor > CLICK(1.25f) - 4) { return false; } if (!TestValidLedge(item, coll)) return false; auto surface = LaraCollisionAboveFront(item, coll->Setup.ForwardAngle, CLICK(2), CLICK(1)); auto headroom = surface.Position.Floor - surface.Position.Ceiling; if (frontFloor <= -CLICK(1)) { if (headroom < LARA_HEIGHT) { if (g_GameFlow->Animations.CrawlExtended) SetAnimation(item, LA_ONWATER_TO_CROUCH_1CLICK); else return false; } else SetAnimation(item, LA_ONWATER_TO_STAND_1CLICK); } else if (frontFloor > CLICK(0.5f)) { if (headroom < LARA_HEIGHT) { if (g_GameFlow->Animations.CrawlExtended) SetAnimation(item, LA_ONWATER_TO_CROUCH_M1CLICK); else return false; } else SetAnimation(item, LA_ONWATER_TO_STAND_M1CLICK); } else { if (headroom < LARA_HEIGHT) { if (g_GameFlow->Animations.CrawlExtended) SetAnimation(item, LA_ONWATER_TO_CROUCH_0CLICK); else return false; } else SetAnimation(item, LA_ONWATER_TO_STAND_0CLICK); } UpdateItemRoom(item, -LARA_HEIGHT / 2); SnapItemToLedge(item, coll, 1.7f); item->pos.yPos += frontFloor - 5; item->activeState = LS_ONWATER_EXIT; item->airborne = false; item->speed = 0; item->fallspeed = 0; info->gunStatus = LG_HANDS_BUSY; info->waterStatus = LW_ABOVE_WATER; return true; } bool TestLaraLadderClimbOut(ITEM_INFO* item, COLL_INFO* coll) // NEW function for water to ladder move { LaraInfo*& info = item->data; if (!(TrInput & IN_ACTION) || !info->climbStatus || coll->CollisionType != CT_FRONT) { return false; } if (info->gunStatus && (info->gunStatus != LG_READY || info->gunType != WEAPON_FLARE)) { return false; } if (!TestLaraClimbStance(item, coll)) return false; short rot = item->pos.yRot; if (rot >= -ANGLE(35.0f) && rot <= ANGLE(35.0f)) rot = 0; else if (rot >= ANGLE(55.0f) && rot <= ANGLE(125.0f)) rot = ANGLE(90.0f); else if (rot >= ANGLE(145.0f) || rot <= -ANGLE(145.0f)) rot = ANGLE(180.0f); else if (rot >= -ANGLE(125.0f) && rot <= -ANGLE(55.0f)) rot = -ANGLE(90.0f); if (rot & 0x3FFF) return false; switch ((unsigned short)rot / ANGLE(90.0f)) { case NORTH: item->pos.zPos = (item->pos.zPos | (WALL_SIZE - 1)) - LARA_RAD - 1; break; case EAST: item->pos.xPos = (item->pos.xPos | (WALL_SIZE - 1)) - LARA_RAD - 1; break; case SOUTH: item->pos.zPos = (item->pos.zPos & -WALL_SIZE) + LARA_RAD + 1; break; case WEST: item->pos.xPos = (item->pos.xPos & -WALL_SIZE) + LARA_RAD + 1; break; } SetAnimation(item, LA_ONWATER_IDLE); item->targetState = LS_LADDER_IDLE; AnimateLara(item); item->pos.yRot = rot; item->pos.yPos -= 10;//otherwise she falls back into the water item->pos.zRot = 0; item->pos.xRot = 0; item->airborne = false; item->speed = 0; item->fallspeed = 0; info->gunStatus = LG_HANDS_BUSY; info->waterStatus = LW_ABOVE_WATER; return true; } void TestLaraWaterDepth(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; short roomNum = item->roomNumber; FLOOR_INFO* floor = GetFloor(item->pos.xPos, item->pos.yPos, item->pos.zPos, &roomNum); int waterDepth = GetWaterDepth(item->pos.xPos, item->pos.yPos, item->pos.zPos, roomNum); if (waterDepth == NO_HEIGHT) { item->fallspeed = 0; item->pos.xPos = coll->Setup.OldPosition.x; item->pos.yPos = coll->Setup.OldPosition.y; item->pos.zPos = coll->Setup.OldPosition.z; } // Height check was at CLICK(2) before but changed to this // because now Lara surfaces on a head level, not mid-body level. else if (waterDepth <= LARA_HEIGHT - LARA_HEADROOM / 2) { SetAnimation(item, LA_UNDERWATER_TO_STAND); item->targetState = LS_IDLE; item->pos.zRot = 0; item->pos.xRot = 0; item->speed = 0; item->fallspeed = 0; item->airborne = false; item->pos.yPos = GetFloorHeight(floor, item->pos.xPos, item->pos.yPos, item->pos.zPos); info->waterStatus = LW_WADE; } } #ifndef NEW_TIGHTROPE void GetTighRopeFallOff(int regularity) { if (LaraItem->hitPoints <= 0 || LaraItem->hitStatus) SetAnimation(LaraItem, LA_TIGHTROPE_FALL_LEFT); if (!info->tightRopeFall && !(GetRandomControl() & regularity)) info->tightRopeFall = 2 - ((GetRandomControl() & 0xF) != 0); } #endif bool IsStandingWeapon(LARA_WEAPON_TYPE gunType) { if (gunType == LARA_WEAPON_TYPE::WEAPON_SHOTGUN || gunType == LARA_WEAPON_TYPE::WEAPON_HK || gunType == LARA_WEAPON_TYPE::WEAPON_CROSSBOW || gunType == LARA_WEAPON_TYPE::WEAPON_TORCH || gunType == LARA_WEAPON_TYPE::WEAPON_GRENADE_LAUNCHER || gunType == LARA_WEAPON_TYPE::WEAPON_HARPOON_GUN || gunType == LARA_WEAPON_TYPE::WEAPON_ROCKET_LAUNCHER || gunType == LARA_WEAPON_TYPE::WEAPON_SNOWMOBILE) { return true; } return false; } bool IsJumpState(LARA_STATE state) { if (state == LS_JUMP_FORWARD || state == LS_JUMP_BACK || state == LS_JUMP_LEFT || state == LS_JUMP_RIGHT || state == LS_JUMP_UP || state == LS_FALL_BACK || state == LS_REACH || state == LS_SWAN_DIVE || state == LS_FREEFALL_DIVE || state == LS_FREEFALL) { return true; } return false; } bool IsRunJumpQueueableState(LARA_STATE state) { if (state == LS_RUN_FORWARD || state == LS_STEP_UP || state == LS_STEP_DOWN) { return true; } return false; } bool IsRunJumpCountableState(LARA_STATE state) { if (state == LS_RUN_FORWARD || state == LS_WALK_FORWARD || state == LS_JUMP_FORWARD || state == LS_SPRINT || state == LS_SPRINT_DIVE) { return true; } return false; } bool IsVaultState(LARA_STATE state) { if (state == LS_VAULT || state == LS_VAULT_2_STEPS || state == LS_VAULT_3_STEPS || state == LS_VAULT_1_STEP_CROUCH || state == LS_VAULT_2_STEPS_CROUCH || state == LS_VAULT_3_STEPS_CROUCH || state == LS_AUTO_JUMP) { return true; } return false; } bool TestLaraSplat(ITEM_INFO* item, int dist, int height, int side) { auto start = GAME_VECTOR( item->pos.xPos + (phd_cos(item->pos.yRot) * side), item->pos.yPos + height, item->pos.zPos + (phd_sin(item->pos.yRot) * -side), item->roomNumber); auto end = GAME_VECTOR( item->pos.xPos + (phd_sin(item->pos.yRot) * dist) + (phd_cos(item->pos.yRot) * side), item->pos.yPos + height, item->pos.zPos + (phd_cos(item->pos.yRot) * dist) + (phd_sin(item->pos.yRot) * -side), item->roomNumber); return !LOS(&start, &end); } bool TestLaraPose(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; if (!TestEnvironment(ENV_FLAG_SWAMP, item) && info->gunStatus == LG_HANDS_FREE && // Hands are free. !(TrInput & (IN_FLARE | IN_DRAW)) && // Avoid unsightly concurrent actions. (info->gunType != WEAPON_FLARE || info->flareAge > 0) && // Flare is not being handled. TODO: Will she pose with weapons drawn? info->Vehicle == NO_ITEM) // Not in a vehicle. { return true; } return false; } bool TestLaraStep(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; if (abs(coll->Middle.Floor) > 0 && (coll->Middle.Floor <= STEPUP_HEIGHT || // Within lower floor bound... info->waterStatus == LW_WADE) && // OR Lara is wading. coll->Middle.Floor >= -STEPUP_HEIGHT && // Within upper floor bound. coll->Middle.Floor != NO_HEIGHT) { return true; } return false; } bool TestLaraStepUp(ITEM_INFO* item, COLL_INFO* coll) { if (coll->Middle.Floor < -CLICK(0.5f) && // Within lower floor bound. coll->Middle.Floor >= -STEPUP_HEIGHT) // Within upper floor bound. { return true; } return false; } bool TestLaraStepDown(ITEM_INFO* item, COLL_INFO* coll) { if (coll->Middle.Floor <= STEPUP_HEIGHT && // Within lower floor bound. coll->Middle.Floor > CLICK(0.5f)) // Within upper floor bound. { return true; } return false; } bool TestLaraMonkeyStep(ITEM_INFO* item, COLL_INFO* coll) { int y = item->pos.yPos - LARA_HEIGHT_MONKEY; auto probe = GetCollisionResult(item); if ((probe.Position.Ceiling - y) <= CLICK(1.25f) && // Within lower ceiling bound. (probe.Position.Ceiling - y) >= -CLICK(1.25f) && // Within upper ceiling bound. probe.Position.Ceiling != NO_HEIGHT) { return true; } return false; } // TODO: This function and its clone TestLaraCrawlMoveTolerance() should become obsolete with more accurate and accessible collision detection in the future. // For now, it supercedes old probes and is used alongside COLL_INFO. @Sezz 2021.10.24 bool TestLaraMoveTolerance(ITEM_INFO* item, COLL_INFO* coll, MoveTestSetup testSetup) { int y = item->pos.yPos; int dist = OFFSET_RADIUS(coll->Setup.Radius); auto probe = GetCollisionResult(item, testSetup.Angle, dist, -coll->Setup.Height); bool isSlopeDown = testSetup.CheckSlopeDown ? (probe.Position.FloorSlope && probe.Position.Floor > y) : false; bool isSlopeUp = testSetup.CheckSlopeUp ? (probe.Position.FloorSlope && probe.Position.Floor < y) : false; bool isDeath = testSetup.CheckDeath ? probe.Block->Flags.Death : false; auto start1 = GAME_VECTOR(item->pos.xPos, y + testSetup.UpperFloorBound - 1, item->pos.zPos, item->roomNumber); auto end1 = GAME_VECTOR(probe.Coordinates.x, y + testSetup.UpperFloorBound - 1, probe.Coordinates.z, item->roomNumber); auto start2 = GAME_VECTOR(item->pos.xPos, y - coll->Setup.Height + 1, item->pos.zPos, item->roomNumber); auto end2 = GAME_VECTOR(probe.Coordinates.x, probe.Coordinates.y + 1, probe.Coordinates.z, item->roomNumber); // Conduct "ray" test at upper floor bound. if (!LOS(&start1, &end1)) return false; // Conduct "ray" test at lowest ceiling bound. if (!LOS(&start2, &end2)) return false; // Assess move feasibility to location ahead. if ((probe.Position.Floor - y) <= testSetup.LowerFloorBound && // Within lower floor bound. (probe.Position.Floor - y) >= testSetup.UpperFloorBound && // Within upper floor bound. (probe.Position.Ceiling - y) < -coll->Setup.Height && // Within lowest ceiling bound. abs(probe.Position.Ceiling - probe.Position.Floor) > coll->Setup.Height && // Space is not a clamp. !isSlopeDown && !isSlopeUp && !isDeath && // No slope or death sector (if applicable). probe.Position.Floor != NO_HEIGHT) { return true; } return false; } bool TestLaraRunForward(ITEM_INFO* item, COLL_INFO* coll) { // Using Lower/UpperFloorBound defined in run state collision function. MoveTestSetup testSetup { item->pos.yRot, NO_LOWER_BOUND, -STEPUP_HEIGHT, false, true, false }; return TestLaraMoveTolerance(item, coll, testSetup); } bool TestLaraWalkForward(ITEM_INFO* item, COLL_INFO* coll) { // Using Lower/UpperFloorBound defined in walk state collision function. MoveTestSetup testSetup { item->pos.yRot, STEPUP_HEIGHT, -STEPUP_HEIGHT }; return TestLaraMoveTolerance(item, coll, testSetup); } bool TestLaraWalkBack(ITEM_INFO* item, COLL_INFO* coll) { // Using Lower/UpperFloorBound defined in walk back state collision function. MoveTestSetup testSetup { item->pos.yRot + ANGLE(180.0f), STEPUP_HEIGHT, -STEPUP_HEIGHT }; return TestLaraMoveTolerance(item, coll, testSetup); } bool TestLaraRunBack(ITEM_INFO* item, COLL_INFO* coll) { // Using Lower/UpperFloorBound defined in hop back state collision function. MoveTestSetup testSetup { item->pos.yRot + ANGLE(180.0f), NO_LOWER_BOUND, -STEPUP_HEIGHT, false, false, false }; return TestLaraMoveTolerance(item, coll, testSetup); } bool TestLaraStepLeft(ITEM_INFO* item, COLL_INFO* coll) { // Using Lower/UpperFloorBound defined in step left state collision function. MoveTestSetup testSetup { item->pos.yRot - ANGLE(90.0f), CLICK(0.8f), -CLICK(0.8f) }; return TestLaraMoveTolerance(item, coll, testSetup); } bool TestLaraStepRight(ITEM_INFO* item, COLL_INFO* coll) { // Using Lower/UpperFloorBound defined in step right state collision function. MoveTestSetup testSetup { item->pos.yRot + ANGLE(90.0f), CLICK(0.8f), -CLICK(0.8f) }; return TestLaraMoveTolerance(item, coll, testSetup); } bool TestLaraWadeForwardSwamp(ITEM_INFO* item, COLL_INFO* coll) { // Using Lower/UpperFloorBound defined in wade forward state collision function. MoveTestSetup testSetup { item->pos.yRot, NO_LOWER_BOUND, -STEPUP_HEIGHT, false, false, false }; return TestLaraMoveTolerance(item, coll, testSetup); } bool TestLaraWalkBackSwamp(ITEM_INFO* item, COLL_INFO* coll) { // Using UpperFloorBound defined in walk back state collision function. MoveTestSetup testSetup { item->pos.yRot + ANGLE(180.0f), NO_LOWER_BOUND, -STEPUP_HEIGHT, false, false, false }; return TestLaraMoveTolerance(item, coll, testSetup); } bool TestLaraStepLeftSwamp(ITEM_INFO* item, COLL_INFO* coll) { // Using UpperFloorBound defined in step left state collision function. MoveTestSetup testSetup { item->pos.yRot - ANGLE(90.0f), NO_LOWER_BOUND, -CLICK(0.8f), false, false, false }; return TestLaraMoveTolerance(item, coll, testSetup); } bool TestLaraStepRightSwamp(ITEM_INFO* item, COLL_INFO* coll) { // Using UpperFloorBound defined in step right state collision function. MoveTestSetup testSetup { item->pos.yRot + ANGLE(90.0f), NO_LOWER_BOUND, -CLICK(0.8f), false, false, false }; return TestLaraMoveTolerance(item, coll, testSetup); } // HACK: coll->Setup.Radius and coll->Setup.Height are only set // in COLLISION functions, then reset by LaraAboveWater() to defaults. // This means they will store the wrong values for tests called in crawl CONTROL functions. // When states become objects, collision setup should occur at the beginning of each state, eliminating the need // for this clone function. @Sezz 2021.12.05 bool TestLaraCrawlMoveTolerance(ITEM_INFO* item, COLL_INFO* coll, MoveTestSetup testSetup) { int y = item->pos.yPos; int dist = OFFSET_RADIUS(LARA_RAD_CRAWL); auto probe = GetCollisionResult(item, testSetup.Angle, dist, -LARA_HEIGHT_CRAWL); bool isSlopeDown = testSetup.CheckSlopeDown ? (probe.Position.FloorSlope && probe.Position.Floor > y) : false; bool isSlopeUp = testSetup.CheckSlopeUp ? (probe.Position.FloorSlope && probe.Position.Floor < y) : false; bool isDeath = testSetup.CheckDeath ? probe.Block->Flags.Death : false; auto start1 = GAME_VECTOR(item->pos.xPos, y + testSetup.UpperFloorBound - 1, item->pos.zPos, item->roomNumber); auto end1 = GAME_VECTOR(probe.Coordinates.x, y + testSetup.UpperFloorBound - 1, probe.Coordinates.z, item->roomNumber); auto start2 = GAME_VECTOR(item->pos.xPos, y - LARA_HEIGHT_CRAWL + 1, item->pos.zPos, item->roomNumber); auto end2 = GAME_VECTOR(probe.Coordinates.x, probe.Coordinates.y + 1, probe.Coordinates.z, item->roomNumber); // Conduct "ray" test at upper floor bound. if (!LOS(&start1, &end1)) return false; // Conduct "ray" test at lowest ceiling bound. if (!LOS(&start2, &end2)) return false; // Assess move feasibility to location ahead. if ((probe.Position.Floor - y) <= testSetup.LowerFloorBound && // Within lower floor bound. (probe.Position.Floor - y) >= testSetup.UpperFloorBound && // Within upper floor bound. (probe.Position.Ceiling - y) < -LARA_HEIGHT_CRAWL && // Within lowest ceiling bound. abs(probe.Position.Ceiling - probe.Position.Floor) > LARA_HEIGHT_CRAWL && // Space is not a clamp. !isSlopeDown && !isSlopeUp && !isDeath && // No slope or death sector (if applicable). probe.Position.Floor != NO_HEIGHT) { return true; } return false; } bool TestLaraCrawlForward(ITEM_INFO* item, COLL_INFO* coll) { // Using Lower/UpperFloorBound defined in crawl state collision functions. MoveTestSetup testSetup { item->pos.yRot, CLICK(1) - 1, -(CLICK(1) - 1) }; return TestLaraCrawlMoveTolerance(item, coll, testSetup); } bool TestLaraCrawlBack(ITEM_INFO* item, COLL_INFO* coll) { // Using Lower/UpperFloorBound defined in crawl state collision functions. MoveTestSetup testSetup { item->pos.yRot + ANGLE(180.0f), CLICK(1) - 1, -(CLICK(1) - 1) }; return TestLaraCrawlMoveTolerance(item, coll, testSetup); } bool TestLaraCrouchToCrawl(ITEM_INFO* item) { LaraInfo*& info = item->data; if (info->gunStatus == LG_HANDS_FREE && // Hands are free. !(TrInput & (IN_FLARE | IN_DRAW)) && // Avoid unsightly concurrent actions. (info->gunType != WEAPON_FLARE || info->flareAge > 0)) // Not handling flare. { return true; } return false; } bool TestLaraCrouchRoll(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; int y = item->pos.yPos; int dist = CLICK(3); auto probe = GetCollisionResult(item, item->pos.yRot, dist, -LARA_HEIGHT_CRAWL); if ((probe.Position.Floor - y) <= (CLICK(1) - 1) && // Within lower floor bound. (probe.Position.Floor - y) >= -(CLICK(1) - 1) && // Within upper floor bound. (probe.Position.Ceiling - y) < -LARA_HEIGHT_CRAWL && // Within lowest ceiling bound. !probe.Position.FloorSlope && // Not a slope. info->waterSurfaceDist >= -CLICK(1) && // Water depth is optically permissive. !(TrInput & (IN_FLARE | IN_DRAW)) && // Avoid unsightly concurrent actions. (info->gunType != WEAPON_FLARE || info->flareAge > 0)) // Not handling flare. { return true; } return false; } bool TestLaraMonkeyMoveTolerance(ITEM_INFO* item, COLL_INFO* coll, MonkeyMoveTestSetup testSetup) { int y = item->pos.yPos - LARA_HEIGHT_MONKEY; int dist = OFFSET_RADIUS(coll->Setup.Radius); auto probe = GetCollisionResult(item, testSetup.Angle, dist); auto start1 = GAME_VECTOR(item->pos.xPos, y + testSetup.LowerCeilingBound + 1, item->pos.zPos, item->roomNumber); auto end1 = GAME_VECTOR(probe.Coordinates.x, probe.Coordinates.y - LARA_HEIGHT_MONKEY + testSetup.LowerCeilingBound + 1, probe.Coordinates.z, item->roomNumber); auto start2 = GAME_VECTOR(item->pos.xPos, y + LARA_HEIGHT_MONKEY - 1, item->pos.zPos, item->roomNumber); auto end2 = GAME_VECTOR(probe.Coordinates.x, probe.Coordinates.y - 1, probe.Coordinates.z, item->roomNumber); // Conduct "ray" test at lower ceiling bound. if (!LOS(&start1, &end1)) return false; // Conduct "ray" test at lowest floor bound. if (!LOS(&start2, &end2)) return false; // Assess move feasibility to location ahead. if (probe.BottomBlock->Flags.Monkeyswing && // Is monkey sector. (probe.Position.Floor - y) > LARA_HEIGHT_MONKEY && // Within highest floor bound. (probe.Position.Ceiling - y) <= testSetup.LowerCeilingBound && // Within lower ceiling bound. (probe.Position.Ceiling - y) >= testSetup.UpperCeilingBound && // Within upper ceiling bound. abs(probe.Position.Ceiling - probe.Position.Floor) > LARA_HEIGHT_MONKEY && // Space is not a clamp. !probe.Position.CeilingSlope && // No ceiling slope. probe.Position.Ceiling != NO_HEIGHT) { return true; } return false; } bool TestLaraMonkeyForward(ITEM_INFO* item, COLL_INFO* coll) { // Using Lower/UpperCeilingBound defined in monkey forward collision function. MonkeyMoveTestSetup testSetup { item->pos.yRot, CLICK(1.25f), -CLICK(1.25f) }; return TestLaraMonkeyMoveTolerance(item, coll, testSetup); } bool TestLaraMonkeyBack(ITEM_INFO* item, COLL_INFO* coll) { // Using Lower/UpperCeilingBound defined in monkey back collision function. MonkeyMoveTestSetup testSetup { item->pos.yRot + ANGLE(180.0f), CLICK(1.25f), -CLICK(1.25f) }; return TestLaraMonkeyMoveTolerance(item, coll, testSetup); } bool TestLaraMonkeyShimmyLeft(ITEM_INFO* item, COLL_INFO* coll) { // Using Lower/UpperCeilingBound defined in monkey shimmy left collision function. MonkeyMoveTestSetup testSetup { item->pos.yRot - ANGLE(90.0f), CLICK(0.5f), -CLICK(0.5f) }; return TestLaraMonkeyMoveTolerance(item, coll, testSetup); } bool TestLaraMonkeyShimmyRight(ITEM_INFO* item, COLL_INFO* coll) { // Using Lower/UpperCeilingBound defined in monkey shimmy right collision function. MonkeyMoveTestSetup testSetup { item->pos.yRot + ANGLE(90.0f), CLICK(0.5f), -CLICK(0.5f) }; return TestLaraMonkeyMoveTolerance(item, coll, testSetup); } VaultTestResult TestLaraVaultTolerance(ITEM_INFO* item, COLL_INFO* coll, VaultTestSetup testSetup) { LaraInfo*& info = item->data; int y = item->pos.yPos; int dist = OFFSET_RADIUS(coll->Setup.Radius); auto probeFront = GetCollisionResult(item, coll->NearestLedgeAngle, dist, -coll->Setup.Height); auto probeMiddle = GetCollisionResult(item); bool swampTooDeep = testSetup.CheckSwampDepth ? (TestEnvironment(ENV_FLAG_SWAMP, item) && info->waterSurfaceDist < -CLICK(3)) : TestEnvironment(ENV_FLAG_SWAMP, item); // "Floor" ahead may be formed by ceiling; raise y position of probe point to find potential vault candidate location. int yOffset = testSetup.LowerCeilingBound; while (((probeFront.Position.Ceiling - y) > -coll->Setup.Height || // Ceiling is below Lara's height... abs(probeFront.Position.Ceiling - probeFront.Position.Floor) <= testSetup.ClampMin || // OR clamp is too small abs(probeFront.Position.Ceiling - probeFront.Position.Floor) > testSetup.ClampMax) && // OR clamp is too large (future-proofing; not possible right now). yOffset > (testSetup.UpperCeilingBound - coll->Setup.Height)) // Offset is not too high. { probeFront = GetCollisionResult(item, coll->NearestLedgeAngle, dist, yOffset); yOffset -= std::max(CLICK(0.5f), testSetup.ClampMin); } // Assess vault candidate location. if ((probeFront.Position.Floor - y) < testSetup.LowerCeilingBound && // Within lower floor bound. (probeFront.Position.Floor - y) >= testSetup.UpperCeilingBound && // Within upper floor bound. abs(probeFront.Position.Ceiling - probeFront.Position.Floor) > testSetup.ClampMin && // Within clamp min. abs(probeFront.Position.Ceiling - probeFront.Position.Floor) <= testSetup.ClampMax && // Within clamp max. abs(probeMiddle.Position.Ceiling - probeFront.Position.Floor) >= testSetup.GapMin && // Gap is optically permissive. !swampTooDeep && // Swamp depth is permissive. probeFront.Position.Floor != NO_HEIGHT) { return VaultTestResult { true, probeFront.Position.Floor, (LARA_STATE)-1 }; } return VaultTestResult { false, NO_HEIGHT, (LARA_STATE)-1 }; } VaultTestResult TestLaraVault2Steps(ITEM_INFO* item, COLL_INFO* coll) { // Floor range: (-STEPUP_HEIGHT, -CLICK(2.5f)] // Clamp range: (-LARA_HEIGHT, -MAX_HEIGHT] VaultTestSetup testSetup { -STEPUP_HEIGHT, -CLICK(2.5f), LARA_HEIGHT, -MAX_HEIGHT, CLICK(1) }; return TestLaraVaultTolerance(item, coll, testSetup); } VaultTestResult TestLaraVault3Steps(ITEM_INFO* item, COLL_INFO* coll) { // Floor range: (-CLICK(2.5f), -CLICK(3.5f)] // Clamp range: (-LARA_HEIGHT, -MAX_HEIGHT] VaultTestSetup testSetup { -CLICK(2.5f), -CLICK(3.5f), LARA_HEIGHT, -MAX_HEIGHT, CLICK(1), }; return TestLaraVaultTolerance(item, coll, testSetup); } VaultTestResult TestLaraVault1StepToCrouch(ITEM_INFO* item, COLL_INFO* coll) { // Floor range: (0, -STEPUP_HEIGHT] // Clamp range: (-LARA_HEIGHT_CRAWL, -LARA_HEIGHT] VaultTestSetup testSetup { 0, -STEPUP_HEIGHT, LARA_HEIGHT_CRAWL, LARA_HEIGHT, CLICK(1), }; return TestLaraVaultTolerance(item, coll, testSetup); } VaultTestResult TestLaraVault2StepsToCrouch(ITEM_INFO* item, COLL_INFO* coll) { // Floor range: (-STEPUP_HEIGHT, -CLICK(2.5f)] // Clamp range: (-LARA_HEIGHT_CRAWL, -LARA_HEIGHT] VaultTestSetup testSetup { -STEPUP_HEIGHT, -CLICK(2.5f), LARA_HEIGHT_CRAWL, LARA_HEIGHT, CLICK(1), }; return TestLaraVaultTolerance(item, coll, testSetup); } VaultTestResult TestLaraVault3StepsToCrouch(ITEM_INFO* item, COLL_INFO* coll) { // Floor range: (-CLICK(2.5f), -CLICK(3.5f)] // Clamp range: (-LARA_HEIGHT_CRAWL, -LARA_HEIGHT] VaultTestSetup testSetup { -CLICK(2.5f), -CLICK(3.5f), LARA_HEIGHT_CRAWL, LARA_HEIGHT, CLICK(1), }; return TestLaraVaultTolerance(item, coll, testSetup); } VaultTestResult TestLaraVaultAutoJump(ITEM_INFO* item, COLL_INFO* coll) { // Floor range: (-CLICK(3.5f), -CLICK(7.5f)] // Clamp range: (-CLICK(0.1f), -MAX_HEIGHT] VaultTestSetup testSetup { -CLICK(3.5f), -CLICK(7.5f), CLICK(0.1f)/* TODO: Is this enough hand room?*/,-MAX_HEIGHT, CLICK(0.1f), false }; return TestLaraVaultTolerance(item, coll, testSetup); } VaultTestResult TestLaraLadderAutoJump(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; int y = item->pos.yPos; int dist = OFFSET_RADIUS(coll->Setup.Radius); auto probeFront = GetCollisionResult(item, coll->NearestLedgeAngle, dist, -coll->Setup.Height); auto probeMiddle = GetCollisionResult(item); if (TestValidLedgeAngle(item, coll) && // Appropriate angle difference from ladder. !TestEnvironment(ENV_FLAG_SWAMP, item) && // No swamp. info->climbStatus && // Ladder sector flag set. (probeMiddle.Position.Ceiling - y) <= -CLICK(6.5f) && // Within lowest middle ceiling bound. (Synced with TestLaraLadderMount()) coll->NearestLedgeDistance <= coll->Setup.Radius) // Appropriate distance from wall (tentative). { return VaultTestResult{ true, probeMiddle.Position.Ceiling, (LARA_STATE)-1 }; } return VaultTestResult{ false, NO_HEIGHT, (LARA_STATE)-1 }; } VaultTestResult TestLaraLadderMount(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; int y = item->pos.yPos; int dist = OFFSET_RADIUS(coll->Setup.Radius); auto probeFront = GetCollisionResult(item, coll->NearestLedgeAngle, dist, -coll->Setup.Height); auto probeMiddle = GetCollisionResult(item); if (TestValidLedgeAngle(item, coll) && info->climbStatus && // Ladder sector flag set. (probeMiddle.Position.Ceiling - y) <= -CLICK(4.5f) && // Within lower middle ceiling bound. (probeMiddle.Position.Floor - y) > -CLICK(6.5f) && // Within upper middle floor bound. (Synced with TestLaraAutoJump()) (probeFront.Position.Ceiling - y) <= -CLICK(4.5f) && // Within lowest front ceiling bound. coll->NearestLedgeDistance <= coll->Setup.Radius) // Appropriate distance from wall. { return VaultTestResult{ true, NO_HEIGHT, (LARA_STATE)-1 }; } return VaultTestResult{ false, NO_HEIGHT, (LARA_STATE)-1 }; } VaultTestResult TestLaraMonkeyAutoJump(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; int y = item->pos.yPos; auto probe = GetCollisionResult(item); if (!TestEnvironment(ENV_FLAG_SWAMP, item) && // No swamp. info->canMonkeySwing && // Monkey swing sector flag set. (probe.Position.Ceiling - y) < -LARA_HEIGHT_MONKEY && // Within lower ceiling bound. (probe.Position.Ceiling - y) >= -CLICK(7)) // Within upper ceiling bound. { return VaultTestResult{ true, probe.Position.Ceiling, (LARA_STATE)-1 }; } return VaultTestResult{ false, NO_HEIGHT, (LARA_STATE)-1 }; } VaultTestResult TestLaraVault(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; // This check is redundant, but provides for a slight optimisation. if (!(TrInput & IN_ACTION) || info->gunStatus != LG_HANDS_FREE) return VaultTestResult{ false, NO_HEIGHT, (LARA_STATE)-1 }; if (TestEnvironment(ENV_FLAG_SWAMP, item) && info->waterSurfaceDist < -CLICK(3)) return VaultTestResult{ false, NO_HEIGHT, (LARA_STATE)-1 }; VaultTestResult vaultResult; // Attempt ledge vault. if (TestValidLedge(item, coll)) { // Vault to crouch up one step. vaultResult = TestLaraVault1StepToCrouch(item, coll); if (vaultResult.Success) { vaultResult.TargetState = LS_VAULT_1_STEP_CROUCH; vaultResult.Height += CLICK(1); return vaultResult; } // Vault to stand up two steps. vaultResult = TestLaraVault2Steps(item, coll); if (vaultResult.Success) { vaultResult.TargetState = LS_VAULT_2_STEPS; vaultResult.Height += CLICK(2); return vaultResult; } // Vault to crouch up two steps. vaultResult = TestLaraVault2StepsToCrouch(item, coll); if (vaultResult.Success && g_GameFlow->Animations.CrawlExtended) { vaultResult.TargetState = LS_VAULT_2_STEPS_CROUCH; vaultResult.Height += CLICK(2); return vaultResult; } // Vault to stand up three steps. vaultResult = TestLaraVault3Steps(item, coll); if (vaultResult.Success) { vaultResult.TargetState = LS_VAULT_3_STEPS; vaultResult.Height += CLICK(3); return vaultResult; } // Vault to crouch up three steps. vaultResult = TestLaraVault3StepsToCrouch(item, coll); if (vaultResult.Success && g_GameFlow->Animations.CrawlExtended) { vaultResult.TargetState = LS_VAULT_3_STEPS_CROUCH; vaultResult.Height += CLICK(3); return vaultResult; } // Auto jump to ledge. vaultResult = TestLaraVaultAutoJump(item, coll); if (vaultResult.Success) { vaultResult.TargetState = LS_AUTO_JUMP; return vaultResult; } } // TODO: Move ladder checks here when ladders are less prone to breaking. // In this case, they fail due to a reliance on ShiftItem(). @Sezz 2021.02.05 // TODO: calcJumpVelocity not getting set? // Auto jump to monkey swing. vaultResult = TestLaraMonkeyAutoJump(item, coll); if (vaultResult.Success && g_GameFlow->Animations.MonkeyAutoJump) { vaultResult.TargetState = LS_AUTO_JUMP; return vaultResult; } return VaultTestResult{ false, NO_HEIGHT, (LARA_STATE)-1 }; } // Temporary solution to ladder mounts until ladders stop breaking whenever you try to do anything with them. @Sezz 2022.02.05 bool TestAndDoLaraLadderClimb(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; if (!(TrInput & IN_ACTION) || info->gunStatus != LG_HANDS_FREE) return false; if (TestEnvironment(ENV_FLAG_SWAMP, item) && info->waterSurfaceDist < -CLICK(3)) return false; // Auto jump to ladder. auto vaultResult = TestLaraLadderAutoJump(item, coll); if (vaultResult.Success) { info->calcJumpVelocity = -3 - sqrt(-9600 - 12 * std::max((vaultResult.Height - item->pos.yPos + CLICK(0.2f)), -CLICK(7.1f))); item->animNumber = LA_STAND_SOLID; item->frameNumber = GetFrameNumber(item, 0); item->targetState = LS_JUMP_UP; item->activeState = LS_IDLE; info->gunStatus = LG_HANDS_BUSY; info->turnRate = 0; ShiftItem(item, coll); SnapItemToGrid(item, coll); // HACK: until fragile ladder code is refactored, we must exactly snap to grid. AnimateLara(item); return true; } // Mount ladder. vaultResult = TestLaraLadderMount(item, coll); if (vaultResult.Success && TestLaraClimbStance(item, coll)) { item->animNumber = LA_STAND_SOLID; item->frameNumber = GetFrameNumber(item, 0); item->targetState = LS_LADDER_IDLE; item->activeState = LS_IDLE; info->gunStatus = LG_HANDS_BUSY; info->turnRate = 0; ShiftItem(item, coll); SnapItemToGrid(item, coll); // HACK: until fragile ladder code is refactored, we must exactly snap to grid. AnimateLara(item); return true; } } bool TestLaraCrawlVaultTolerance(ITEM_INFO* item, COLL_INFO* coll, CrawlVaultTestSetup testSetup) { int y = item->pos.yPos; auto probeA = GetCollisionResult(item, item->pos.yRot, testSetup.CrossDist, -LARA_HEIGHT_CRAWL); // Crossing. auto probeB = GetCollisionResult(item, item->pos.yRot, testSetup.DestDist, -LARA_HEIGHT_CRAWL); // Approximate destination. auto probeMiddle = GetCollisionResult(item); bool isSlope = testSetup.CheckSlope ? probeB.Position.FloorSlope : false; bool isDeath = testSetup.CheckDeath ? probeB.Block->Flags.Death : false; if ((probeA.Position.Floor - y) <= testSetup.LowerFloorBound && // Within lower floor bound. (probeA.Position.Floor - y) >= testSetup.UpperFloorBound && // Within upper floor bound. abs(probeA.Position.Ceiling - probeA.Position.Floor) > testSetup.ClampMin && // Crossing clamp limit. abs(probeB.Position.Ceiling - probeB.Position.Floor) > testSetup.ClampMin && // Destination clamp limit. abs(probeMiddle.Position.Ceiling - probeA.Position.Floor) >= testSetup.GapMin && // Gap is optically permissive (going up). abs(probeA.Position.Ceiling - probeMiddle.Position.Floor) >= testSetup.GapMin && // Gap is optically permissive (going down). abs(probeA.Position.Floor - probeB.Position.Floor) <= testSetup.MaxProbeHeightDif && // Crossing/destination floor height difference suggests continuous crawl surface. (probeA.Position.Ceiling - y) < -testSetup.GapMin && // Ceiling height is permissive. !isSlope && !isDeath && // No slope or death sector. probeA.Position.Floor != NO_HEIGHT && probeB.Position.Floor != NO_HEIGHT) { return true; } return false; } bool TestLaraCrawlUpStep(ITEM_INFO* item, COLL_INFO* coll) { // Floor range: [-CLICK(1), -STEPUP_HEIGHT] CrawlVaultTestSetup testSetup { -CLICK(1), -STEPUP_HEIGHT, LARA_HEIGHT_CRAWL, CLICK(0.6f), CLICK(1.2f), CLICK(2), CLICK(1) - 1 }; return TestLaraCrawlVaultTolerance(item, coll, testSetup); } bool TestLaraCrawlDownStep(ITEM_INFO* item, COLL_INFO* coll) { // Floor range: [STEPUP_HEIGHT, CLICK(1)] CrawlVaultTestSetup testSetup { STEPUP_HEIGHT, CLICK(1), LARA_HEIGHT_CRAWL, CLICK(0.6f), CLICK(1.2f), CLICK(2), CLICK(1) - 1 }; return TestLaraCrawlVaultTolerance(item, coll, testSetup); } bool TestLaraCrawlExitDownStep(ITEM_INFO* item, COLL_INFO* coll) { // Floor range: [STEPUP_HEIGHT, CLICK(1)] CrawlVaultTestSetup testSetup { STEPUP_HEIGHT, CLICK(1), LARA_HEIGHT, CLICK(1.25f), CLICK(1.2f), CLICK(1.5f), -MAX_HEIGHT, false }; return TestLaraCrawlVaultTolerance(item, coll, testSetup); } bool TestLaraCrawlExitJump(ITEM_INFO* item, COLL_INFO* coll) { // Floor range: [NO_LOWER_BOUND, STEPUP_HEIGHT) CrawlVaultTestSetup testSetup { NO_LOWER_BOUND, STEPUP_HEIGHT + 1, LARA_HEIGHT, CLICK(1.25f), CLICK(1.2f), CLICK(1.5f), NO_LOWER_BOUND, false }; return TestLaraCrawlVaultTolerance(item, coll, testSetup); } bool TestLaraCrawlToHang(ITEM_INFO* item, COLL_INFO* coll) { int y = item->pos.yPos; int dist = CLICK(1.2f); auto probe = GetCollisionResult(item, item->pos.yRot + ANGLE(180.0f), dist, -LARA_HEIGHT_CRAWL); bool objectCollided = TestLaraObjectCollision(item, item->pos.yRot + ANGLE(180.0f), CLICK(1.2f), -LARA_HEIGHT_CRAWL); if (!objectCollided && // No obstruction. (probe.Position.Floor - y) >= LARA_HEIGHT_STRETCH && // Highest floor bound. (probe.Position.Ceiling - y) <= -CLICK(0.75f) && // Gap is optically permissive. probe.Position.Floor != NO_HEIGHT) { return true; } return false; } CrawlVaultTestResult TestLaraCrawlVault(ITEM_INFO* item, COLL_INFO* coll) { // This check is redundant, but provides for a slight optimisation. if (!(TrInput & (IN_ACTION | IN_JUMP))) return CrawlVaultTestResult{ false, (LARA_STATE)-1 }; if (TestLaraCrawlExitDownStep(item, coll)) { if (TrInput & IN_CROUCH && TestLaraCrawlDownStep(item, coll)) return CrawlVaultTestResult{ true, LS_CRAWL_STEP_DOWN }; else [[likely]] return CrawlVaultTestResult{ true, LS_CRAWL_EXIT_STEP_DOWN }; } if (TestLaraCrawlExitJump(item, coll)) { if (TrInput & IN_WALK) return CrawlVaultTestResult{ true, LS_CRAWL_EXIT_FLIP }; else [[likely]] return CrawlVaultTestResult{ true, LS_CRAWL_EXIT_JUMP }; } if (TestLaraCrawlUpStep(item, coll)) return CrawlVaultTestResult{ true, LS_CRAWL_STEP_UP }; if (TestLaraCrawlDownStep(item, coll)) return CrawlVaultTestResult{ true, LS_CRAWL_STEP_DOWN }; return CrawlVaultTestResult{ false, (LARA_STATE)-1 }; } bool TestLaraJumpTolerance(ITEM_INFO* item, COLL_INFO* coll, JumpTestSetup testSetup) { LaraInfo*& info = item->data; int y = item->pos.yPos; auto probe = GetCollisionResult(item, testSetup.Angle, testSetup.Dist, -coll->Setup.Height); bool isWading = testSetup.CheckWadeStatus ? (info->waterStatus == LW_WADE) : false; if (!TestLaraFacingCorner(item, testSetup.Angle, testSetup.Dist) && // Avoid jumping through corners. (probe.Position.Floor - y) >= -STEPUP_HEIGHT && // Within highest floor bound. ((probe.Position.Ceiling - y) < -(coll->Setup.Height + (LARA_HEADROOM * 0.8f)) || // Within lowest ceiling bound... ((probe.Position.Ceiling - y) < -coll->Setup.Height && // OR ceiling is level with Lara's head (probe.Position.Floor - y) >= CLICK(0.5f))) && // AND there is a drop below. !isWading && // Not wading in water (if applicable). !TestEnvironment(ENV_FLAG_SWAMP, item) && // No swamp. probe.Position.Floor != NO_HEIGHT) { return true; } return false; } bool TestLaraRunJumpForward(ITEM_INFO* item, COLL_INFO* coll) { JumpTestSetup testSetup { item->pos.yRot, CLICK(1.5f) }; return TestLaraJumpTolerance(item, coll, testSetup); } bool TestLaraJumpForward(ITEM_INFO* item, COLL_INFO* coll) { JumpTestSetup testSetup { item->pos.yRot }; return TestLaraJumpTolerance(item, coll, testSetup); } bool TestLaraJumpBack(ITEM_INFO* item, COLL_INFO* coll) { JumpTestSetup testSetup { item->pos.yRot + ANGLE(180.0f) }; return TestLaraJumpTolerance(item, coll, testSetup); } bool TestLaraJumpLeft(ITEM_INFO* item, COLL_INFO* coll) { JumpTestSetup testSetup { item->pos.yRot - ANGLE(90.0f) }; return TestLaraJumpTolerance(item, coll, testSetup); } bool TestLaraJumpRight(ITEM_INFO* item, COLL_INFO* coll) { JumpTestSetup testSetup { item->pos.yRot + ANGLE(90.0f) }; return TestLaraJumpTolerance(item, coll, testSetup); } bool TestLaraJumpUp(ITEM_INFO* item, COLL_INFO* coll) { JumpTestSetup testSetup { 0, 0, false }; return TestLaraJumpTolerance(item, coll, testSetup); } bool TestLaraPoleCollision(ITEM_INFO* item, COLL_INFO* coll, bool up, float offset) { static constexpr auto poleProbeCollRadius = 16.0f; bool atLeastOnePoleCollided = false; if (GetCollidedObjects(item, WALL_SIZE, true, CollidedItems, nullptr, 0) && CollidedItems[0]) { auto laraBox = TO_DX_BBOX(item->pos, GetBoundsAccurate(item)); // HACK: because Core implemented upward pole movement as SetPosition command, we can't precisely // check her position. So we add a fixed height offset. auto sphere = BoundingSphere(laraBox.Center + Vector3(0, (laraBox.Extents.y + poleProbeCollRadius + offset) * (up ? -1 : 1), 0), poleProbeCollRadius); //g_Renderer.addDebugSphere(sphere.Center, 16.0f, Vector4(1, 0, 0, 1), RENDERER_DEBUG_PAGE::LOGIC_STATS); int i = 0; while (CollidedItems[i] != NULL) { auto& obj = CollidedItems[i]; i++; if (obj->objectNumber != ID_POLEROPE) continue; auto poleBox = TO_DX_BBOX(obj->pos, GetBoundsAccurate(obj)); poleBox.Extents = poleBox.Extents + Vector3(coll->Setup.Radius, 0, coll->Setup.Radius); //g_Renderer.addDebugBox(poleBox, Vector4(0, 0, 1, 1), RENDERER_DEBUG_PAGE::LOGIC_STATS); if (poleBox.Intersects(sphere)) { atLeastOnePoleCollided = true; break; } } } return atLeastOnePoleCollided; } bool TestLaraPoleUp(ITEM_INFO* item, COLL_INFO* coll) { if (!TestLaraPoleCollision(item, coll, true, CLICK(1))) return false; return (coll->Middle.Ceiling < -CLICK(1)); } bool TestLaraPoleDown(ITEM_INFO* item, COLL_INFO* coll) { if (!TestLaraPoleCollision(item, coll, false)) return false; return (coll->Middle.Floor > 0); }