#include "framework.h" #include "lara.h" #include "lara_tests.h" #include "input.h" #include "level.h" #include "animation.h" #include "lara_climb.h" #include "lara_monkey.h" #include "lara_collide.h" #include "lara_flare.h" #include "control/control.h" #include "control/los.h" #include "items.h" #include "Renderer11.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 point. // We use 1/3 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.3f); int zf = phd_cos(coll->NearestLedgeAngle) * (coll->Setup.Radius * 1.3f); // Determine probe left/right points int xl = xf + phd_sin(coll->NearestLedgeAngle - ANGLE(90)) * coll->Setup.Radius; int zl = zf + phd_cos(coll->NearestLedgeAngle - ANGLE(90)) * coll->Setup.Radius; int xr = xf + phd_sin(coll->NearestLedgeAngle + ANGLE(90)) * coll->Setup.Radius; int zr = zf + phd_cos(coll->NearestLedgeAngle + ANGLE(90)) * coll->Setup.Radius; // Determine probe top point int y = item->pos.yPos - coll->Setup.Height; // Get floor heights at both points auto left = GetCollisionResult(item->pos.xPos + xl, y, item->pos.zPos + zl, GetRoom(item->location, item->pos.xPos, y, item->pos.zPos).roomNumber).Position.Floor; auto right = GetCollisionResult(item->pos.xPos + xr, y, item->pos.zPos + zr, GetRoom(item->location, item->pos.xPos, y, item->pos.zPos).roomNumber).Position.Floor; //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 allowed slope difference for a given collision radius auto slopeDelta = ((float)STEPUP_HEIGHT / (float)WALL_SIZE) * (coll->Setup.Radius * 2); // If specified, limit vertical search zone only to nearest height if (heightLimit && (abs(left - y) > (STEP_SIZE / 2) || abs(right - y) > (STEP_SIZE / 2))) return false; // 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) > 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 < STEP_SIZE) return false; } return (coll->CollisionType == CT_FRONT); } bool TestValidLedgeAngle(ITEM_INFO* item, COLL_INFO* coll) { return abs((short) (coll->NearestLedgeAngle - item->pos.yRot)) <= LARA_GRAB_THRESHOLD; } bool TestLaraVault(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; if (!(TrInput & IN_ACTION) || info->gunStatus != LG_HANDS_FREE || (TestLaraSwamp(item) && info->waterSurfaceDist < -(WALL_SIZE - STEP_SIZE))) { return false; } // TODO: LUA info->NewAnims.CrawlExtended = true; info->NewAnims.MonkeyAutoJump = false; if (TestValidLedge(item, coll)) { bool success = false; // Vault to crouch up one step. if (coll->Front.Floor < 0 && // Lower floor bound. coll->Front.Floor > -STEPUP_HEIGHT && // Upper floor bound. info->NewAnims.CrawlExtended) { if (abs((coll->Front.Ceiling - coll->Setup.Height) - coll->Front.Floor) > LARA_HEIGHT_CRAWL) // Front clamp buffer. Presumably, nothing more is necessary, but tend to this in the future. @Sezz 2021.11.06 { item->animNumber = LA_VAULT_TO_CROUCH_1CLICK; item->currentAnimState = LS_GRABBING; item->frameNumber = GetFrameNumber(item, 0); item->goalAnimState = LS_CROUCH_IDLE; item->pos.yPos += coll->Front.Floor + STEP_SIZE; info->gunStatus = LG_HANDS_BUSY; success = true; } } // Vault up two steps. else if (coll->Front.Floor <= -STEPUP_HEIGHT && // Lower floor bound. coll->Front.Floor >= -(STEP_SIZE * 2 + STEP_SIZE / 2)) // Upper floor bound. { // Vault to stand up two steps. if (abs((coll->Front.Ceiling - coll->Setup.Height) - coll->Front.Floor) > LARA_HEIGHT/* && // Front clamp buffer. BUG: Turned away from the ledge and toward a section with a low ceiling, stand-to-crawl vault will be performed instead. @Sezz 2021.11.06 abs((coll->FrontLeft.Ceiling - coll->Setup.Height) - coll->FrontLeft.Floor) > LARA_HEIGHT && // Left clamp buffer. // TODO: Ceilings don't push, so these are unnecessary for now. @Sezz 2021.11.06 abs((coll->FrontRight.Ceiling - coll->Setup.Height) - coll->FrontRight.Floor) > LARA_HEIGHT*/) // Right clamp buffer. { item->animNumber = LA_VAULT_TO_STAND_2CLICK_START; item->currentAnimState = LS_GRABBING; item->frameNumber = GetFrameNumber(item, 0); item->goalAnimState = LS_IDLE; item->pos.yPos += coll->Front.Floor + (STEP_SIZE * 2); info->gunStatus = LG_HANDS_BUSY; success = true; } // Vault to crouch up two steps. else if (abs((coll->Front.Ceiling - coll->Setup.Height) - coll->Front.Floor) > LARA_HEIGHT_CRAWL && // Front clamp buffer. abs((coll->FrontLeft.Ceiling - coll->Setup.Height) - coll->FrontLeft.Floor) > LARA_HEIGHT_CRAWL && // Left clamp buffer. abs((coll->FrontRight.Ceiling - coll->Setup.Height) - coll->FrontRight.Floor) > LARA_HEIGHT_CRAWL && // Right clamp buffer. info->NewAnims.CrawlExtended) { item->animNumber = LA_VAULT_TO_CROUCH_2CLICK; item->frameNumber = GetFrameNumber(item, 0); item->currentAnimState = LS_GRABBING; item->goalAnimState = LS_CROUCH_IDLE; item->pos.yPos += coll->Front.Floor + (STEP_SIZE * 2); info->gunStatus = LG_HANDS_BUSY; success = true; } } // Vault up three steps. else if (coll->Front.Floor <= -(STEP_SIZE * 2 + STEP_SIZE / 2) && // Lower floor bound. coll->Front.Floor >= -(WALL_SIZE - STEP_SIZE / 2)) // Upper floor bound. { // Vault to stand up three steps. if (abs((coll->Front.Ceiling - coll->Setup.Height) - coll->Front.Floor) > LARA_HEIGHT/* && // Front clamp buffer. BUG: Turned away from the ledge and toward a section with a low ceiling, stand-to-crawl vault will be performed instead. @Sezz 2021.11.06 abs((coll->FrontLeft.Ceiling - coll->Setup.Height) - coll->FrontLeft.Floor) > LARA_HEIGHT && // Left clamp buffer. // TODO: Ceilings don't push, so these are unnecessary for now. @Sezz 2021.11.06 abs((coll->FrontRight.Ceiling - coll->Setup.Height) - coll->FrontRight.Floor) > LARA_HEIGHT*/) // Right clamp buffer. { item->animNumber = LA_VAULT_TO_STAND_3CLICK; item->currentAnimState = LS_GRABBING; item->frameNumber = GetFrameNumber(item, 0); item->goalAnimState = LS_IDLE; item->pos.yPos += coll->Front.Floor + (WALL_SIZE - STEP_SIZE); info->gunStatus = LG_HANDS_BUSY; success = true; } // Vault to crouch up three steps. else if (abs((coll->Front.Ceiling - coll->Setup.Height) - coll->Front.Floor) > LARA_HEIGHT_CRAWL && // Front clamp buffer. abs((coll->FrontLeft.Ceiling - coll->Setup.Height) - coll->FrontLeft.Floor) > LARA_HEIGHT_CRAWL && // Left clamp buffer. abs((coll->FrontRight.Ceiling - coll->Setup.Height) - coll->FrontRight.Floor) > LARA_HEIGHT_CRAWL && // Right clamp buffer. info->NewAnims.CrawlExtended) { item->animNumber = LA_VAULT_TO_CROUCH_3CLICK; item->frameNumber = GetFrameNumber(item, 0); item->currentAnimState = LS_GRABBING; item->goalAnimState = LS_CROUCH_IDLE; item->pos.yPos += coll->Front.Floor + (WALL_SIZE - STEP_SIZE); info->gunStatus = LG_HANDS_BUSY; success = true; } } // Auto jump. else if (coll->Front.Floor >= -(WALL_SIZE * 2 - STEP_SIZE / 2) && // Upper floor bound. coll->Front.Floor <= -(WALL_SIZE - STEP_SIZE / 2) && // Lower floor bound. !TestLaraSwamp(item)) { item->animNumber = LA_STAND_SOLID; item->frameNumber = GetFrameNumber(item, 0); item->goalAnimState = LS_JUMP_UP; item->currentAnimState = LS_IDLE; info->calcFallSpeed = -3 - sqrt(-9600 - 12 * coll->Front.Floor); AnimateLara(item); success = true; } if (success) { SnapItemToLedge(item, coll, 0.2f); return true; } } // Begin ladder climb. if (info->climbStatus) { if (coll->Front.Floor > -(WALL_SIZE * 2 - STEP_SIZE / 2) || // Upper front floor bound. coll->FrontLeft.Floor > -(WALL_SIZE * 2 - STEP_SIZE / 2) || // Upper left floor bound. coll->FrontRight.Floor > -(STEP_SIZE * 2) || // Upper right floor bound. coll->Middle.Ceiling > -(WALL_SIZE + STEP_SIZE / 2 + 6) || // Upper ceiling bound. info->waterStatus == LW_WADE) { if ((coll->Front.Floor < -WALL_SIZE || coll->Front.Ceiling >= (STEP_SIZE * 2 - 6)) && coll->Middle.Ceiling <= -(STEP_SIZE * 2 + 6)) { if (TestLaraClimbStance(item, coll)) { item->animNumber = LA_STAND_SOLID; item->frameNumber = GetFrameNumber(item, 0); item->goalAnimState = LS_LADDER_IDLE; item->currentAnimState = 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; } } return false; } // Auto jump to ladder. TODO: Check swamps. item->animNumber = LA_STAND_SOLID; item->frameNumber = GetFrameNumber(item, 0); item->goalAnimState = LS_JUMP_UP; item->currentAnimState = LS_IDLE; info->calcFallSpeed = -116; 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; } // Auto jump to monkey swing. if (info->canMonkeySwing && !TestLaraSwamp(item) && info->NewAnims.MonkeyAutoJump) { short roomNum = item->roomNumber; int ceiling = (GetCeiling(GetFloor(item->pos.xPos, item->pos.yPos, item->pos.zPos, &roomNum), item->pos.xPos, item->pos.yPos, item->pos.zPos))-(item->pos.yPos); if (ceiling > (WALL_SIZE * 2 - STEP_SIZE) || ceiling < -(WALL_SIZE * 2 - STEP_SIZE) || abs(ceiling) == (WALL_SIZE - STEP_SIZE)) { return false; } item->animNumber = LA_STAND_IDLE; item->frameNumber = GetFrameNumber(item, 0); item->goalAnimState = LS_JUMP_UP; item->currentAnimState = LS_TEST_1; return true; } return false; } bool TestLaraKeepCrouched(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->currentAnimState == LS_CROUCH_IDLE || item->currentAnimState == LS_CROUCH_TURN_LEFT || item->currentAnimState == LS_CROUCH_TURN_RIGHT) ? LARA_RAD : LARA_RAD_CRAWL; auto y = item->pos.yPos; auto probeBack = GetCollisionResult(item, coll->Setup.ForwardAngle + ANGLE(180.0f), radius, 0); // 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. // A buffer might help, but improved collision handling would presumably eliminate this issue entirely. @Sezz 2021.10.15 if ((coll->Middle.Ceiling - LARA_HEIGHT_CRAWL) >= -LARA_HEIGHT || // Middle is not a clamp. (coll->Front.Ceiling - LARA_HEIGHT_CRAWL) >= -LARA_HEIGHT || // Front is not a clamp. (probeBack.Position.Ceiling - y) >= -LARA_HEIGHT) // Back is not a clamp. { return true; } return false; } bool TestLaraSlide(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; if (TestLaraSwamp(item)) return false; static short oldAngle = 1; if (abs(coll->TiltX) <= 2 && abs(coll->TiltZ) <= 2) return false; short angle = ANGLE(0.0f); if (coll->TiltX > 2) angle = -ANGLE(90.0f); else if (coll->TiltX < -2) angle = ANGLE(90.0f); if (coll->TiltZ > 2 && coll->TiltZ > abs(coll->TiltX)) angle = ANGLE(180.0f); else if (coll->TiltZ < -2 && -coll->TiltZ > abs(coll->TiltX)) angle = ANGLE(0.0f); short delta = angle - item->pos.yRot; ShiftItem(item, coll); if (delta < -ANGLE(90.0f) || delta > ANGLE(90.0f)) { if (item->currentAnimState == LS_SLIDE_BACK && oldAngle == angle) return true; SetAnimation(item, LA_SLIDE_BACK_START); item->pos.yRot = angle + ANGLE(180.0f); } else { if (item->currentAnimState == LS_SLIDE_FORWARD && oldAngle == angle) return true; SetAnimation(item, LA_SLIDE_FORWARD); item->pos.yRot = angle; } info->moveAngle = angle; oldAngle = angle; return true; } bool TestLaraSwamp(ITEM_INFO* item) { return (g_Level.Rooms[item->roomNumber].flags & ENV_FLAG_SWAMP); } bool TestLaraWater(ITEM_INFO* item) { return (g_Level.Rooms[item->roomNumber].flags & ENV_FLAG_WATER); } SPLAT_COLL TestLaraWall(ITEM_INFO* item, int front, int right, int down) { int x = item->pos.xPos; int y = item->pos.yPos + down; int z = item->pos.zPos; short angle = GetQuadrant(item->pos.yRot); short roomNum = item->roomNumber; FLOOR_INFO* floor; int h, c; switch (angle) { case NORTH: x -= right; break; case EAST: z -= right; break; case SOUTH: x += right; break; case WEST: z += right; break; default: break; } GetFloor(x, y, z, &roomNum); switch (angle) { case NORTH: z += front; break; case EAST: x += front; break; case SOUTH: z -= front; break; case WEST: x -= front; break; default: break; } floor = GetFloor(x, y, z, &roomNum); h = GetFloorHeight(floor, x, y, z); c = GetCeiling(floor, x, y, z); if (h == NO_HEIGHT) return SPLAT_COLL::WALL; if (y >= h || y <= c) return SPLAT_COLL::STEP; return SPLAT_COLL::NONE; } 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 (info->canMonkeySwing && coll->CollisionType == CT_TOP) { SetAnimation(item, LA_JUMP_UP_TO_MONKEYSWING); item->gravityStatus = false; item->speed = 0; item->fallspeed = 0; info->gunStatus = LG_HANDS_BUSY; MonkeySwingSnap(item, coll); return true; } if ((coll->CollisionType != CT_FRONT) || (coll->Middle.Ceiling > -STEPUP_HEIGHT)) 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; auto angle = item->pos.yRot; if (TestHangSwingIn(item, angle)) { SetAnimation(item, LA_JUMP_UP_TO_MONKEYSWING); } else { SetAnimation(item, LA_REACH_TO_HANG, 12); if (TestHangFeet(item, angle)) item->goalAnimState = LS_HANG_FEET; } 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->gravityStatus = false; item->speed = 0; item->fallspeed = 0; info->gunStatus = LG_HANDS_BUSY; info->torsoYrot = 0; info->torsoXrot = 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 (info->canMonkeySwing && coll->CollisionType == CT_TOP) { info->headYrot = 0; info->headXrot = 0; info->torsoYrot = 0; info->torsoXrot = 0; info->gunStatus = LG_HANDS_BUSY; SetAnimation(item, LA_REACH_TO_MONKEYSWING); item->gravityStatus = false; item->speed = 0; item->fallspeed = 0; 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; auto angle = item->pos.yRot; info->NewAnims.OscillateHanging = true; if (TestHangSwingIn(item, angle)) { if (info->NewAnims.OscillateHanging) { info->headYrot = 0; info->headXrot = 0; info->torsoYrot = 0; info->torsoXrot = 0; SetAnimation(item, LA_REACH_TO_HANG_OSCILLATE); } else { info->headYrot = 0; info->headXrot = 0; info->torsoYrot = 0; info->torsoXrot = 0; SetAnimation(item, LA_REACH_TO_MONKEYSWING); } } else { SetAnimation(item, LA_REACH_TO_HANG); if (TestHangFeet(item, angle)) item->goalAnimState = LS_HANG_FEET; } 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->gravityStatus = 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 delta = 0; auto flag = false; auto angle = info->moveAngle; if (info->moveAngle == (short) (item->pos.yRot - ANGLE(90.0f))) delta = -coll->Setup.Radius; else if (info->moveAngle == (short) (item->pos.yRot + ANGLE(90.0f))) delta = coll->Setup.Radius; auto s = phd_sin(info->moveAngle); auto c = phd_cos(info->moveAngle); auto testShift = Vector2(s * delta, c * delta); 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; auto hdif = LaraFloorFront(item, info->moveAngle, coll->Setup.Radius * 1.5f); if (hdif < 200) flag = true; auto cdif = LaraCeilingFront(item, info->moveAngle, coll->Setup.Radius * 1.5f, 0); item->pos = oldPos; auto dir = GetQuadrant(item->pos.yRot); // 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; info->moveAngle = item->pos.yRot; coll->Setup.BadHeightDown = NO_BAD_POS; coll->Setup.BadHeightUp = -STEPUP_HEIGHT; coll->Setup.BadCeilingHeight = 0; coll->Setup.ForwardAngle = info->moveAngle; GetCollisionInfo(coll, item); bool result = false; if (info->climbStatus) { 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, dir); 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->goalAnimState = LS_LADDER_IDLE; } } } else { SetAnimation(item, LA_FALL_START); item->pos.yPos += 256; item->gravityStatus = true; item->speed = 2; item->fallspeed = 1; info->gunStatus = LG_HANDS_FREE; } } else { if (TrInput & IN_ACTION && item->hitPoints > 0 && coll->Front.Floor <= 0) { if (flag && hdif > 0 && delta > 0 == coll->MiddleLeft.Floor > coll->MiddleRight.Floor) flag = false; auto front = coll->Front.Floor; auto dfront = coll->Front.Floor - GetBoundsAccurate(item)->Y1; auto flag2 = false; auto x = item->pos.xPos; auto z = item->pos.zPos; if (delta != 0) { x += testShift.x; z += testShift.y; } info->moveAngle = angle; if (256 << dir & GetClimbFlags(x, item->pos.yPos, z, item->roomNumber)) { if (!TestLaraHangOnClimbWall(item, coll)) dfront = 0; } else if (!TestValidLedge(item, coll, true)) { if (delta < 0 && coll->FrontLeft.Floor != coll->Front.Floor || delta > 0 && coll->FrontRight.Floor != coll->Front.Floor) flag2 = true; } coll->Front.Floor = front; if (!flag2 && coll->Middle.Ceiling < 0 && coll->CollisionType == CT_FRONT && !flag && !coll->HitStatic && cdif <= -950 && abs(dfront) < SLOPE_DIFFERENCE && TestValidLedgeAngle(item, coll)) { if (item->speed != 0) SnapItemToLedge(item, coll); item->pos.yPos += dfront; } else { item->pos.xPos = coll->Setup.OldPosition.x; item->pos.yPos = coll->Setup.OldPosition.y; item->pos.zPos = coll->Setup.OldPosition.z; if (item->currentAnimState == LS_SHIMMY_LEFT || item->currentAnimState == LS_SHIMMY_RIGHT) { SetAnimation(item, LA_REACH_TO_HANG, 21); } else if (item->currentAnimState == LS_SHIMMY_FEET_LEFT || item->currentAnimState == LS_SHIMMY_FEET_RIGHT) { SetAnimation(item, LA_HANG_FEET_IDLE); } result = true; } } else { SetAnimation(item, LA_JUMP_UP, 9); item->pos.xPos += coll->Shift.x; item->pos.yPos += GetBoundsAccurate(item)->Y2 * 2.4f; item->pos.zPos += coll->Shift.z; item->gravityStatus = true; item->speed = 2; item->fallspeed = 1; info->gunStatus = LG_HANDS_FREE; } } 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 && item->animNumber != LA_HANG_FEET_IDLE) 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; int oldFrontFloor = coll->Front.Floor; // Quadrant is only used for ladder checks auto quadrant = GetQuadrant(item->pos.yRot); // Get bounding box height for further ledge height calculations auto bounds = GetBoundsAccurate(item); // Virtually rotate Lara 90 degrees to the right and snap to nearest ledge, if any. short newAngle = item->pos.yRot + ANGLE(testAngle); item->pos.yRot = newAngle; SnapItemToLedge(item, coll, item->pos.yRot); // Do further testing only if test angle is equal to resulting edge angle if (newAngle == item->pos.yRot) { // Push Lara further to the right to avoid false floor hits on the left side auto c = phd_cos(item->pos.yRot + ANGLE(testAngle)); auto s = phd_sin(item->pos.yRot + ANGLE(testAngle)); item->pos.xPos += s * coll->Setup.Radius / 2; item->pos.zPos += c * coll->Setup.Radius / 2; // Store next position info->nextCornerPos.x = item->pos.xPos; info->nextCornerPos.y = LaraCollisionAboveFront(item, item->pos.yRot, coll->Setup.Radius * 2, abs(bounds->Y1)).Position.Floor + abs(bounds->Y1); info->nextCornerPos.z = item->pos.zPos; info->moveAngle = item->pos.yRot; auto result = TestLaraValidHangPos(item, coll); // Restore original item positions item->pos = oldPos; info->moveAngle = oldMoveAngle; if (result && (abs(oldFrontFloor - coll->Front.Floor) <= SLOPE_DIFFERENCE)) return CORNER_RESULT::INNER; if (info->climbStatus) { auto angleSet = testAngle > 0 ? LeftExtRightIntTab : LeftIntRightExtTab; if (GetClimbFlags(item->pos.xPos, item->pos.yPos, item->pos.zPos, item->roomNumber) & (short)angleSet[quadrant]) return CORNER_RESULT::INNER; } } // Restore original item positions item->pos = oldPos; info->moveAngle = oldPos.yRot; // 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 + STEP_SIZE) < 0) return CORNER_RESULT::NONE; if (LaraCeilingFront(item, item->pos.yRot + ANGLE(testAngle), coll->Setup.Radius + STEP_SIZE, 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 + STEP_SIZE)) return CORNER_RESULT::NONE; // Push Lara diagonally to other side of corner at distance of 1/2 wall size auto c = phd_cos(item->pos.yRot + ANGLE(testAngle / 2)); auto s = phd_sin(item->pos.yRot + ANGLE(testAngle / 2)); item->pos.xPos += s * WALL_SIZE / 3; item->pos.zPos += c * WALL_SIZE / 3; // Virtually rotate Lara 90 degrees to the left and snap to nearest ledge, if any. newAngle = item->pos.yRot - ANGLE(testAngle); item->pos.yRot = newAngle; info->moveAngle = item->pos.yRot; SnapItemToLedge(item, coll, item->pos.yRot); // 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)) { // Restore original item positions item->pos = oldPos; return CORNER_RESULT::NONE; } // Do further testing only if test angle is equal to resulting edge angle if (newAngle == item->pos.yRot) { // Store next position info->nextCornerPos.x = item->pos.xPos; info->nextCornerPos.y = LaraCollisionAboveFront(item, item->pos.yRot, coll->Setup.Radius * 2, abs(bounds->Y1)).Position.Floor + abs(bounds->Y1); info->nextCornerPos.z = item->pos.zPos; info->moveAngle = item->pos.yRot; auto result = TestLaraValidHangPos(item, coll); // Restore original item positions item->pos = oldPos; info->moveAngle = oldMoveAngle; if (result && (abs(oldFrontFloor - coll->Front.Floor) <= SLOPE_DIFFERENCE)) return CORNER_RESULT::OUTER; if (info->climbStatus) { auto angleSet = testAngle > 0 ? LeftIntRightExtTab : LeftExtRightIntTab; if (GetClimbFlags(item->pos.xPos, item->pos.yPos, item->pos.zPos, item->roomNumber) & (short)angleSet[quadrant]) return CORNER_RESULT::OUTER; } } // Restore original item positions item->pos = oldPos; info->moveAngle = oldPos.yRot; 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 + STEP_SIZE / 2, 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) > STEP_SIZE / 2) 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.BadHeightDown = NO_BAD_POS; coll->Setup.BadHeightUp = -512; coll->Setup.BadCeilingHeight = 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 shift_r, shift_l; if (LaraTestClimbPos(item, coll->Setup.Radius, coll->Setup.Radius + 120, -700, (STEP_SIZE * 2), &shift_r) != 1) return false; if (LaraTestClimbPos(item, coll->Setup.Radius, -(coll->Setup.Radius + 120), -700, (STEP_SIZE * 2), &shift_l) != 1) return false; if (shift_r) { if (shift_l) { if (shift_r < 0 != shift_l < 0) return false; if ((shift_r < 0 && shift_l < shift_r) || (shift_r > 0 && shift_l > shift_r)) { item->pos.yPos += shift_l; return true; } } item->pos.yPos += shift_r; } else if (shift_l) { item->pos.yPos += shift_l; } return true; } bool TestLaraHangOnClimbWall(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; int shift, result; if (info->climbStatus == 0) 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, STEP_SIZE / 2, 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, short angle) { LaraInfo*& info = item->data; int x = item->pos.xPos; int y = item->pos.yPos; int z = item->pos.zPos; short roomNum = item->roomNumber; FLOOR_INFO* floor; int floorHeight, ceilingHeight; info->NewAnims.OscillateHanging = true; z += phd_cos(angle) * (STEP_SIZE / 2); x += phd_sin(angle) * (STEP_SIZE / 2); floor = GetFloor(x, y, z, &roomNum); floorHeight = GetFloorHeight(floor, x, y, z); ceilingHeight = GetCeiling(floor, x, y, z); if (floorHeight != NO_HEIGHT) { if (info->NewAnims.OscillateHanging) { if (floorHeight - y > 0 && ceilingHeight - y < -400) return true; } else { if (floorHeight - y > 0 && ceilingHeight - y < -400 && (y - 819 - ceilingHeight > -72)) return true; } } return false; } bool TestHangFeet(ITEM_INFO* item, short angle) { LaraInfo*& info = item->data; //TODO: LUA info->NewAnims.FeetHang = 0; if (info->climbStatus || !info->NewAnims.FeetHang) return false; int x = item->pos.xPos; int y = item->pos.yPos; int z = item->pos.zPos; short roomNum = item->roomNumber; z += phd_cos(angle) * (STEP_SIZE / 2); x += phd_sin(angle) * (STEP_SIZE / 2); auto floor = GetFloor(x, y, z, &roomNum); int floorHeight = GetFloorHeight(floor, x, y, z); int ceilingHeight = GetCeiling(floor, x, y, z); int m = ceilingHeight - y; int j = y - (STEP_SIZE / 2) - ceilingHeight; if (floorHeight != NO_HEIGHT) { if (floorHeight < y && m < -(STEP_SIZE / 2) && j > -(STEP_SIZE / 4 + STEP_SIZE / 32)) return true; } return false; } bool TestLaraHangSideways(ITEM_INFO* item, COLL_INFO* coll, short angle) { LaraInfo*& info = item->data; int oldx = item->pos.xPos; int oldz = item->pos.zPos; int x = item->pos.xPos; int z = item->pos.zPos; info->moveAngle = item->pos.yRot + angle; z += phd_cos(info->moveAngle) * 16; x += phd_sin(info->moveAngle) * 16; item->pos.xPos = x; item->pos.zPos = z; coll->Setup.OldPosition.y = item->pos.yPos; auto res = TestLaraHang(item, coll); item->pos.xPos = oldx; item->pos.zPos = oldz; info->moveAngle = item->pos.yRot + angle; return !res; } void SetCornerAnim(ITEM_INFO* item, COLL_INFO* coll, short rot, short flip) { LaraInfo*& info = item->data; if (item->hitPoints <= 0) { SetAnimation(item, LA_FALL_START); item->gravityStatus = true; item->speed = 2; item->pos.yPos += STEP_SIZE; item->fallspeed = 1; info->gunStatus = LG_HANDS_FREE; item->pos.yRot += rot / 2; } else if (flip) { if (info->isClimbing) { SetAnimation(item, LA_LADDER_IDLE); } else { SetAnimation(item, LA_REACH_TO_HANG, 21); } coll->Setup.OldPosition.x = item->pos.xPos = info->nextCornerPos.x; coll->Setup.OldPosition.y = item->pos.yPos = info->nextCornerPos.y; coll->Setup.OldPosition.z = item->pos.zPos = info->nextCornerPos.z; item->pos.yRot += rot; } } void SetCornerAnimFeet(ITEM_INFO* item, COLL_INFO* coll, short rot, short flip) { LaraInfo*& info = item->data; if (item->hitPoints <= 0) { SetAnimation(item, LA_FALL_START); item->gravityStatus = true; item->speed = 2; item->pos.yPos += STEP_SIZE; item->fallspeed = 1; info->gunStatus = LG_HANDS_FREE; item->pos.yRot += rot / 2; } else if (flip) { SetAnimation(item, LA_HANG_FEET_IDLE); coll->Setup.OldPosition.x = item->pos.xPos = info->nextCornerPos.x; coll->Setup.OldPosition.y = item->pos.yPos = info->nextCornerPos.y; coll->Setup.OldPosition.z = item->pos.zPos = info->nextCornerPos.z; item->pos.yRot += rot; } } bool TestLaraStandingJump(ITEM_INFO* item, COLL_INFO* coll, short angle) { auto y = item->pos.yPos; auto probe = GetCollisionResult(item, angle, STEP_SIZE, coll->Setup.Height); if (!TestLaraFacingCorner(item, angle, STEP_SIZE) && probe.Position.Floor - y >= -STEPUP_HEIGHT && // Highest floor bound. probe.Position.Ceiling - y < -(coll->Setup.Height + LARA_HEADROOM * 0.7f)) // Lowest ceiling bound. { return true; } return false; } bool TestLaraFacingCorner(ITEM_INFO* item, short angle, int dist) { auto angle1 = angle + ANGLE(15.0f); auto angle2 = angle - ANGLE(15.0f); auto vec1 = GAME_VECTOR(item->pos.xPos + dist * phd_sin(angle1), item->pos.yPos - STEPUP_HEIGHT, item->pos.zPos + dist * phd_cos(angle1), item->roomNumber); auto vec2 = GAME_VECTOR(item->pos.xPos + dist * phd_sin(angle2), item->pos.yPos - STEPUP_HEIGHT, item->pos.zPos + dist * phd_cos(angle2), item->roomNumber); auto pos = GAME_VECTOR(item->pos.xPos, item->pos.yPos - STEPUP_HEIGHT, item->pos.zPos, item->roomNumber); auto result1 = LOS(&pos, &vec1); auto result2 = LOS(&pos, &vec2); return ((result1 == 0) && (result2 == 0)); } bool LaraPositionOnLOS(ITEM_INFO* item, short ang, int dist) { auto pos1 = GAME_VECTOR(item->pos.xPos, item->pos.yPos - LARA_HEADROOM, item->pos.zPos, item->roomNumber); auto pos2 = GAME_VECTOR(item->pos.xPos, item->pos.yPos - LARA_HEIGHT + LARA_HEADROOM, item->pos.zPos, item->roomNumber); auto vec1 = GAME_VECTOR(item->pos.xPos + dist * phd_sin(ang), item->pos.yPos - LARA_HEADROOM, item->pos.zPos + dist * phd_cos(ang), item->roomNumber); auto vec2 = 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(&pos1, &vec1); auto result2 = LOS(&pos2, &vec2); return (result1 != 0 && result2 != 0); } 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; } // TODO: Gradually replace calls. bool LaraFallen(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; if (info->waterStatus == LW_WADE || coll->Middle.Floor <= STEPUP_HEIGHT) return false; SetAnimation(item, LA_FALL_START); item->fallspeed = 0; item->gravityStatus = true; return true; } bool LaraLandedBad(ITEM_INFO* item, COLL_INFO* coll) { int landspeed = item->fallspeed - 140; if (landspeed > 0) { if (landspeed <= 14) { item->hitPoints -= 1000 * SQUARE(landspeed) / 196; return item->hitPoints <= 0; } else { item->hitPoints = -1; return true; } } return false; } bool TestLaraWaterStepOut(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; if (coll->CollisionType == CT_FRONT || coll->Middle.Slope || coll->Middle.Floor >= 0) { return false; } if (coll->Middle.Floor >= -(STEP_SIZE / 2)) { SetAnimation(item, LA_STAND_IDLE); } else { SetAnimation(item, LA_ONWATER_TO_WADE_1CLICK); item->goalAnimState = LS_IDLE; } item->pos.yPos += coll->Middle.Floor + (STEP_SIZE * 2 + STEP_SIZE / 2 + STEP_SIZE / 4 - 9); UpdateItemRoom(item, -(STEPUP_HEIGHT - 3)); item->pos.zRot = 0; item->pos.xRot = 0; item->gravityStatus = 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; // TODO: LUA info->NewAnims.CrawlExtended = true; 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 <= -(STEP_SIZE * 2) || frontFloor > (STEP_SIZE + STEP_SIZE / 4 - 4)) { return false; } if (!TestValidLedge(item, coll)) return false; auto surface = LaraCollisionAboveFront(item, coll->Setup.ForwardAngle, (STEP_SIZE * 2), STEP_SIZE); auto headroom = surface.Position.Floor - surface.Position.Ceiling; if (frontFloor <= -STEP_SIZE) { if (headroom < LARA_HEIGHT) { if (info->NewAnims.CrawlExtended) SetAnimation(item, LA_ONWATER_TO_CROUCH_1CLICK); else return false; } else SetAnimation(item, LA_ONWATER_TO_STAND_1CLICK); } else if (frontFloor > (STEP_SIZE / 2)) { if (headroom < LARA_HEIGHT) { if (info->NewAnims.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 (info->NewAnims.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->currentAnimState = LS_ONWATER_EXIT; item->gravityStatus = 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->goalAnimState = 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->gravityStatus = 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 roomNumber = item->roomNumber; FLOOR_INFO* floor = GetFloor(item->pos.xPos, item->pos.yPos, item->pos.zPos, &roomNumber); int waterDepth = GetWaterDepth(item->pos.xPos, item->pos.yPos, item->pos.zPos, roomNumber); 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 STEP_SIZE * 2 before but changed to this // because now Lara surfaces on a head level, not mid-body level. if (waterDepth <= LARA_HEIGHT - LARA_HEADROOM / 2) { SetAnimation(item, LA_UNDERWATER_TO_STAND); item->goalAnimState = LS_IDLE; item->pos.zRot = 0; item->pos.xRot = 0; item->speed = 0; item->fallspeed = 0; item->gravityStatus = false; info->waterStatus = LW_WADE; item->pos.yPos = GetFloorHeight(floor, item->pos.xPos, item->pos.yPos, item->pos.zPos); } } #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 TestLaraPose(ITEM_INFO* item, COLL_INFO* coll) { LaraInfo*& info = item->data; if (!TestLaraSwamp(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(COLL_INFO* coll) { if (abs(coll->Middle.Floor) > 0 && //coll->Middle.Floor <= STEPUP_HEIGHT && // Lower floor bound. BUG: Wading in water over a pit, Lara will not descend. coll->Middle.Floor >= -STEPUP_HEIGHT && // Upper floor bound. coll->Middle.Floor != NO_HEIGHT) { return true; } return false; } bool TestLaraStepUp(ITEM_INFO* item, COLL_INFO* coll) { if (coll->Middle.Floor < -(STEP_SIZE / 2) && // Lower floor bound. coll->Middle.Floor >= -STEPUP_HEIGHT && // Upper floor bound. coll->Middle.Floor != NO_HEIGHT && item->currentAnimState != LS_WALK_BACK && item->currentAnimState != LS_RUN_BACK && item->currentAnimState != LS_CRAWL_IDLE && // Crawl step up handled differently. item->currentAnimState != LS_CRAWL_FORWARD) { return true; } return false; } bool TestLaraStepDown(ITEM_INFO* item, COLL_INFO* coll) { if (coll->Middle.Floor > (STEP_SIZE / 2) && // Upper floor bound. coll->Middle.Floor <= STEPUP_HEIGHT && // Lower floor bound. coll->Middle.Floor != NO_HEIGHT && item->currentAnimState != LS_RUN_FORWARD && // No step down anim exists for these states. item->currentAnimState != LS_SPRINT && item->currentAnimState != LS_WADE_FORWARD && item->currentAnimState != LS_RUN_BACK && item->currentAnimState != LS_CRAWL_IDLE && // Crawl step down handled differently. item->currentAnimState != LS_CRAWL_FORWARD) { return true; } return false; } // TODO: This function 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 TestLaraMove(ITEM_INFO* item, COLL_INFO* coll, short angle, int lowerBound, int upperBound, bool checkSlope, bool checkDeath) { // TODO: coll->Setup.Radius is currently only set to // LARA_RAD_CRAWL in the collision function, then reset by LaraAboveWater(). // For tests called in crawl control functions, then, it will store the wrong radius. // Function below (TestLaraMoveCrawl()) is a clone to account for this. @Sezz 2021.11.05 auto y = item->pos.yPos; auto probe = GetCollisionResult(item, angle, coll->Setup.Radius * sqrt(2) + 4, 0); // Offset required to account for gap between Lara and the wall. Results in slight overshoot, but avoids oscillation. auto noSlope = checkSlope ? !probe.Position.Slope : true; auto noDeath = checkDeath ? !probe.Block->Flags.Death : true; if ((probe.Position.Floor - y) <= lowerBound && // Lower floor bound. (probe.Position.Floor - y) >= upperBound && // Upper floor bound. (probe.Position.Ceiling - y) < -coll->Setup.Height && // Lowest ceiling bound. abs(probe.Position.Ceiling - probe.Position.Floor) > LARA_HEIGHT && // Space is not a clamp. noSlope && // Not a slope (if applicable). noDeath && // Not a death sector (if applicable). probe.Position.Floor != NO_HEIGHT) { return true; } return false; } bool TestLaraMoveCrawl(ITEM_INFO* item, COLL_INFO* coll, short angle, int lowerBound, int upperBound) { auto y = item->pos.yPos; auto probe = GetCollisionResult(item, angle, LARA_RAD_CRAWL * sqrt(2) + 4, 0); if ((probe.Position.Floor - y) <= lowerBound && // Lower floor bound. (probe.Position.Floor - y) >= upperBound && // Upper floor bound. (probe.Position.Ceiling - y) < -LARA_HEIGHT_CRAWL && // Lowest ceiling bound. abs(probe.Position.Ceiling - probe.Position.Floor) > LARA_HEIGHT_CRAWL && // Space is not a clamp. !probe.Position.Slope && // No slope. probe.Position.Floor != NO_HEIGHT) { return true; } return false; } bool TestLaraRunForward(ITEM_INFO* item, COLL_INFO* coll) { auto y = item->pos.yPos - coll->Setup.Height; auto probe = GetCollisionResult(item, coll->Setup.ForwardAngle, coll->Setup.Radius * sqrt(2) + 4, 0); // BUG: This interferes with the one-step stand-to-crouch vault where the ceiling is lower than Lara's height. if ((probe.Position.Ceiling - y) < 0) // Hack to ensure Lara can run off diagonal ledges. coll->Front.Floor often holds the wrong height because of quadrant-dependent wall pushing. @Sezz 2021.11.06 return true; return false; // TODO: TestLaraMove() call not useful yet; it can block Lara from climbing. @Sezz 2021.10.22 // Additionally, run forward test needs to incorporate a unique ceiling check (as above). @Sezz 2021.10.28 //return TestLaraMove(item, coll, item->pos.yRot, STEPUP_HEIGHT, -STEPUP_HEIGHT, false); // Using BadHeightUp/Down defined in walk and run state collision functions. } bool TestLaraWalkForward(ITEM_INFO* item, COLL_INFO* coll) { if (coll->CollisionType != CT_FRONT && coll->CollisionType != CT_TOP_FRONT) { return true; } return false; // TODO: Same issues as in TestLaraRunForward(). //return TestLaraMove(item, coll, item->pos.yRot, STEPUP_HEIGHT, -STEPUP_HEIGHT, true, true); // Using BadHeightUp/Down defined in walk and run state collision functions. } bool TestLaraWalkBack(ITEM_INFO* item, COLL_INFO* coll) { return TestLaraMove(item, coll, item->pos.yRot + ANGLE(180.0f), STEPUP_HEIGHT, -STEPUP_HEIGHT, true, true); // Using BadHeightUp/Down defined in walk back state collision function. } bool TestLaraHopBack(ITEM_INFO* item, COLL_INFO* coll) { return TestLaraMove(item, coll, item->pos.yRot + ANGLE(180.0f), NO_BAD_POS, -STEPUP_HEIGHT, false); // Using BadHeightUp/Down defined in hop back state collision function. } bool TestLaraStepLeft(ITEM_INFO* item, COLL_INFO* coll) { return TestLaraMove(item, coll, item->pos.yRot - ANGLE(90.0f), STEP_SIZE / 2, -STEP_SIZE / 2, true, true); // Using BadHeightUp/Down defined in step left state collision function. } bool TestLaraStepRight(ITEM_INFO* item, COLL_INFO* coll) { return TestLaraMove(item, coll, item->pos.yRot + ANGLE(90.0f), STEP_SIZE / 2, -STEP_SIZE / 2, true, true); // Using BadHeightUp/Down defined in step right state collision function. } bool TestLaraWalkBackSwamp(ITEM_INFO* item, COLL_INFO* coll) { return TestLaraMove(item, coll, item->pos.yRot + ANGLE(180.0f), NO_BAD_POS, -STEPUP_HEIGHT, false); // Using BadHeightUp defined in walk back state collision function. } bool TestLaraStepLeftSwamp(ITEM_INFO* item, COLL_INFO* coll) { return TestLaraMove(item, coll, item->pos.yRot - ANGLE(90.0f), NO_BAD_POS, -STEP_SIZE / 2, false); // Using BadHeightUp defined in step left state collision function. } bool TestLaraStepRightSwamp(ITEM_INFO* item, COLL_INFO* coll) { return TestLaraMove(item, coll, item->pos.yRot + ANGLE(90.0f), NO_BAD_POS, -STEP_SIZE / 2, false); // Using BadHeightUp defined in step right state collision function. } bool TestLaraCrawlForward(ITEM_INFO* item, COLL_INFO* coll) { return TestLaraMoveCrawl(item, coll, item->pos.yRot, STEP_SIZE - 1, -(STEP_SIZE - 1)); // Using BadHeightUp/Down defined in crawl state collision functions. } bool TestLaraCrawlBack(ITEM_INFO* item, COLL_INFO* coll) { return TestLaraMoveCrawl(item, coll, item->pos.yRot + ANGLE(180.0f), STEP_SIZE - 1, -(STEP_SIZE - 1)); // Using BadHeightUp/Down defined in crawl state collision functions. } 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; auto y = item->pos.yPos; auto probe = GetCollisionResult(item, item->pos.yRot, WALL_SIZE / 2, 0); if ((probe.Position.Floor - y) <= (STEP_SIZE - 1) && // Lower floor bound. (probe.Position.Floor - y) >= -(STEP_SIZE - 1) && // Upper floor bound. (probe.Position.Ceiling - y) < -LARA_HEIGHT_CRAWL && // Lowest ceiling bound. !probe.Position.Slope && // Not a slope. info->waterSurfaceDist >= -STEP_SIZE && // Water depth is optically feasible for action. !(TrInput & (IN_FLARE | IN_DRAW)) && // Avoid unsightly concurrent actions. (info->gunType != WEAPON_FLARE || info->flareAge > 0)) // Not handling flare. { return true; } return false; } bool TestLaraCrawlUpStep(ITEM_INFO* item, COLL_INFO* coll) { auto y = item->pos.yPos; auto probeA = GetCollisionResult(item, coll->Setup.ForwardAngle, STEP_SIZE, 0); // Crossing. auto probeB = GetCollisionResult(item, coll->Setup.ForwardAngle, STEP_SIZE * 2, 0); // Approximate destination. if ((probeA.Position.Floor - y) <= -STEP_SIZE && // Lower floor bound. Synced with crawl states' BadHeightUp. (probeA.Position.Floor - y) >= -STEPUP_HEIGHT && // Upper floor bound. ((coll->Middle.Ceiling - LARA_HEIGHT_CRAWL) - (probeA.Position.Floor - y)) <= -(STEP_SIZE / 2 + STEP_SIZE / 8) && // Gap is optically feasible for action. abs(probeA.Position.Ceiling - probeA.Position.Floor) > LARA_HEIGHT_CRAWL && // Crossing is not a clamp. abs(probeB.Position.Ceiling - probeB.Position.Floor) > LARA_HEIGHT_CRAWL && // Destination is not a clamp. !probeA.Position.Slope && // Crossing is not a slope. !probeB.Position.Slope && // Destination is not a slope. probeB.Position.Floor != NO_HEIGHT) // Sezz note: Might be preventing ascent/descent in that one edge case with narrow gap. { return true; } return false; } bool TestLaraCrawlDownStep(ITEM_INFO* item, COLL_INFO* coll) { auto y = item->pos.yPos; auto probeA = GetCollisionResult(item, coll->Setup.ForwardAngle, STEP_SIZE, 0); // Crossing. auto probeB = GetCollisionResult(item, coll->Setup.ForwardAngle, STEP_SIZE * 2, 0); // Approximate destination. if ((probeA.Position.Floor - y) <= STEPUP_HEIGHT && // Lower floor bound. Synced with crawl exit jump's highest floor bound. (probeA.Position.Floor - y) >= STEP_SIZE && // Upper floor bound. Synced with crawl states' BadHeightDown. (probeA.Position.Ceiling - y) <= -(STEP_SIZE / 2 + STEP_SIZE / 4) && // Gap is optically feasible for action. abs(probeA.Position.Ceiling - probeA.Position.Floor) > LARA_HEIGHT_CRAWL && // Crossing is not a clamp. abs(probeB.Position.Ceiling - probeB.Position.Floor) > LARA_HEIGHT_CRAWL && // Destination is not a clamp. !probeA.Position.Slope && // Crossing is not a slope. !probeB.Position.Slope && // Destination is not a slope. probeB.Position.Floor != NO_HEIGHT) { return true; } return false; } bool TestLaraCrawlExitDownStep(ITEM_INFO* item, COLL_INFO* coll) { auto y = item->pos.yPos; auto probeA = GetCollisionResult(item, coll->Setup.ForwardAngle, STEP_SIZE, 0); // Crossing. auto probeB = GetCollisionResult(item, coll->Setup.ForwardAngle, STEP_SIZE + STEP_SIZE / 2, 0); // Approximate destination. if ((probeA.Position.Floor - y) <= STEPUP_HEIGHT && // Lower floor bound. Synced with crawl exit jump's highest floor bound. (probeA.Position.Floor - y) >= STEP_SIZE && // Upper floor bound. Synced with crawl states' BadHeightDown. (probeA.Position.Ceiling - y) <= -(STEP_SIZE + STEP_SIZE / 4) && // Gap is optically feasible for action. abs(probeA.Position.Ceiling - probeA.Position.Floor) > LARA_HEIGHT && // Crossing is not a clamp. abs(probeB.Position.Ceiling - probeB.Position.Floor) > LARA_HEIGHT && // Destination is not a clamp. probeB.Position.Floor != NO_HEIGHT) { return true; } return false; } bool TestLaraCrawlExitJump(ITEM_INFO* item, COLL_INFO* coll) { auto y = item->pos.yPos; auto probeA = GetCollisionResult(item, coll->Setup.ForwardAngle, STEP_SIZE, 0); // Crossing. auto probeB = GetCollisionResult(item, coll->Setup.ForwardAngle, STEP_SIZE + STEP_SIZE / 2, 0); // Approximate destination. if ((probeA.Position.Floor - y) > STEPUP_HEIGHT && // Highest floor bound. Synced with crawl down step and crawl exit down step's lower floor bounds. (probeA.Position.Ceiling - y) <= -(STEP_SIZE + STEP_SIZE / 4) && // Gap is optically feasible for action. abs(probeA.Position.Ceiling - probeA.Position.Floor) > LARA_HEIGHT && // Crossing is not a clamp. abs(probeB.Position.Ceiling - probeB.Position.Floor) > LARA_HEIGHT && // Destination is not a clamp. probeB.Position.Floor != NO_HEIGHT) { return true; } return false; } bool TestLaraCrawlVault(ITEM_INFO* item, COLL_INFO* coll) { if (TestLaraCrawlExitJump(item, coll) || TestLaraCrawlExitDownStep(item, coll) || TestLaraCrawlUpStep(item, coll) || TestLaraCrawlDownStep(item, coll)) { return true; } return false; } bool TestLaraCrawlToHang(ITEM_INFO* item, COLL_INFO* coll) { auto y = item->pos.yPos; auto probe = GetCollisionResult(item, coll->Setup.ForwardAngle + ANGLE(180.0f), LARA_RAD_CRAWL * sqrt(2) + 4, 0); auto objectCollided = TestLaraObjectCollision(item, item->pos.yRot + ANGLE(180.0f), LARA_RAD_CRAWL * sqrt(2) + 4, 0); if (!objectCollided && // No obstruction. (probe.Position.Floor - y) >= LARA_HEIGHT_STRETCH && // Highest floor bound. (probe.Position.Ceiling - y) <= -(STEP_SIZE / 2 + STEP_SIZE / 4) && // Gap is optically feasible for action. probe.Position.Floor != NO_HEIGHT) { return true; } return false; } bool TestLaraPoleUp(ITEM_INFO* item, COLL_INFO* coll) { // TODO: Accuracy. return (coll->Middle.Ceiling < -STEP_SIZE); } bool TestLaraPoleDown(ITEM_INFO* item, COLL_INFO* coll) { return (coll->Middle.Floor > 0); }