diff --git a/meson.build b/meson.build index df8f093c9..397089b3e 100644 --- a/meson.build +++ b/meson.build @@ -253,6 +253,7 @@ sources = [ 'src/game/phase/phase_game.c', 'src/game/phase/phase_inventory.c', 'src/game/phase/phase_pause.c', + 'src/game/phase/phase_photo_mode.c', 'src/game/phase/phase_picture.c', 'src/game/phase/phase_stats.c', 'src/game/random.c', diff --git a/src/game/camera.c b/src/game/camera.c index 1a1722a5c..1e8f475c5 100644 --- a/src/game/camera.c +++ b/src/game/camera.c @@ -5,6 +5,7 @@ #include "game/items.h" #include "game/los.h" #include "game/music.h" +#include "game/phase/phase.h" #include "game/random.h" #include "game/room.h" #include "game/sound.h" @@ -12,6 +13,7 @@ #include "global/const.h" #include "global/vars.h" #include "math/math.h" +#include "math/math_misc.h" #include "math/matrix.h" #include @@ -20,11 +22,44 @@ #include #include +#define DEFAULT_DISTANCE (WALL_L * 3 / 2) +#define PHOTO_ROT_DISTANCE (STEP_L / 3) +#define PHOTO_AXIS_SHIFT (STEP_L * 3 / 4) +#define PHOTO_ROT_SHIFT (PHD_DEGREE * 4) +#define PHOTO_CLAMP (STEP_L + 50) +#define PHOTO_MAX_SPEED 100 +#define PHOTO_ROLL WALL_L +#define PHOTO_MAX_ROLL (14 * PHOTO_ROLL) + +#define CAM_SPEED_SHIFT(val) (((float)m_PhotoSpeed / PHOTO_MAX_SPEED) * val) +#define CAM_ROT_SHIFT (MAX(PHD_DEGREE, CAM_SPEED_SHIFT(PHOTO_ROT_SHIFT))) + +#define CAM_INPUT_SHIFT(neg, pos) \ + ((g_Input.neg ? -1 : 0) + (g_Input.pos ? 1 : 0)) \ + * CAM_SPEED_SHIFT(PHOTO_AXIS_SHIFT) + +#define SHIFT_POS(a, b) \ + do { \ + a.x += b.x; \ + a.y += b.y; \ + a.z += b.z; \ + } while (false) + // Camera speed option ranges from 1-10, so index 0 is unused. static double m_ManualCameraMultiplier[11] = { 1.0, .5, .625, .75, .875, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, }; +static bool m_PhotoMode = false; +static int32_t m_PhotoSpeed = 0; +static int16_t m_Roll = 0; +static CAMERA_INFO m_OldCamera = { 0 }; + +static void M_UpdatePhotoMode(void); +static void M_ExitPhotoMode(void); +static bool M_PhotoBadPosition(GAME_VECTOR pos, int32_t clamp); +static int32_t M_ClampPhotoY(GAME_VECTOR *pos); + static bool M_BadPosition(int32_t x, int32_t y, int32_t z, int16_t room_num); static int32_t M_ShiftClamp(GAME_VECTOR *pos, int32_t clamp); static void M_SmartShift( @@ -95,6 +130,36 @@ static int32_t M_ShiftClamp(GAME_VECTOR *pos, int32_t clamp) } } +static bool M_PhotoBadPosition(const GAME_VECTOR pos, const int32_t clamp) +{ + return M_BadPosition(pos.x, pos.y, pos.z - clamp, pos.room_num) + || M_BadPosition(pos.x - clamp, pos.y, pos.z, pos.room_num); +} + +static int32_t M_ClampPhotoY(GAME_VECTOR *const pos) +{ + const SECTOR_INFO *const sector = + Room_GetSector(pos->x, pos->y, pos->z, &pos->room_num); + + int32_t height = + Room_GetHeight(sector, pos->x, pos->y, pos->z) - PHOTO_CLAMP; + int32_t ceiling = + Room_GetCeiling(sector, pos->x, pos->y, pos->z) + PHOTO_CLAMP; + + if (height < ceiling) { + ceiling = (height + ceiling) >> 1; + height = ceiling; + } + + if (pos->y > height) { + return height - pos->y; + } else if (pos->y < ceiling) { + return ceiling - pos->y; + } + + return 0; +} + static void M_SmartShift( GAME_VECTOR *ideal, void (*shift)( @@ -454,7 +519,7 @@ void Camera_ResetPosition(void) g_Camera.pos.z = g_Camera.target.z - 100; g_Camera.pos.room_num = g_Camera.target.room_num; - g_Camera.target_distance = WALL_L * 3 / 2; + g_Camera.target_distance = DEFAULT_DISTANCE; g_Camera.item = NULL; g_Camera.type = CAM_CHASE; @@ -467,6 +532,7 @@ void Camera_ResetPosition(void) void Camera_Initialise(void) { + m_PhotoMode = false; Camera_ResetPosition(); Camera_Update(); } @@ -556,7 +622,7 @@ void Camera_Look(ITEM_INFO *item) item->rot.y + g_Lara.torso_rot.y + g_Lara.head_rot.y; g_Camera.target_elevation = item->rot.x + g_Lara.torso_rot.x + g_Lara.head_rot.x; - g_Camera.target_distance = WALL_L * 3 / 2; + g_Camera.target_distance = DEFAULT_DISTANCE; int32_t distance = g_Camera.target_distance * Math_Cos(g_Camera.target_elevation) @@ -616,8 +682,167 @@ void Camera_Fixed(void) } } +int32_t Camera_GetPhotoMaxSpeed(void) +{ + return PHOTO_MAX_SPEED; +} + +int32_t Camera_GetPhotoCurrentSpeed(void) +{ + return m_PhotoSpeed; +} + +static void M_UpdatePhotoMode(void) +{ + if (!m_PhotoMode) { + m_OldCamera = g_Camera; + m_PhotoMode = true; + } + + const bool shift_input = g_Input.photo_mode_up || g_Input.photo_mode_down + || g_Input.photo_mode_forward || g_Input.photo_mode_back + || g_Input.photo_mode_left || g_Input.photo_mode_right; + const bool rot_input = g_Input.left || g_Input.right || g_Input.forward + || g_Input.back || g_InputDB.roll; + const bool roll_input = g_Input.step_left || g_Input.step_right; + if (rot_input) { + g_Camera.target_distance = PHOTO_ROT_DISTANCE; + } else { + g_Camera.target_distance = DEFAULT_DISTANCE; + } + + if (g_InputDB.look) { + g_Camera = m_OldCamera; + m_PhotoSpeed = 0; + m_Roll = 0; + } else if (shift_input || (rot_input && !g_InputDB.roll) || roll_input) { + m_PhotoSpeed++; + } else { + m_PhotoSpeed -= 4; + } + CLAMP(m_PhotoSpeed, 0, PHOTO_MAX_SPEED); + + if (g_Input.step_left) { + m_Roll -= CAM_SPEED_SHIFT(PHOTO_ROLL); + } else if (g_Input.step_right) { + m_Roll += CAM_SPEED_SHIFT(PHOTO_ROLL); + } + CLAMP(m_Roll, -PHOTO_MAX_ROLL, PHOTO_MAX_ROLL); + + if (!shift_input && !rot_input) { + return; + } + + const GAME_VECTOR old_pos = g_Camera.pos; + const GAME_VECTOR old_target = g_Camera.target; + + PHD_ANGLE angles[2]; + Math_GetVectorAngles( + g_Camera.target.x - g_Camera.pos.x, g_Camera.target.y - g_Camera.pos.y, + g_Camera.target.z - g_Camera.pos.z, angles); + g_Camera.target_angle = angles[0]; + + XYZ_16 shift = { + .x = CAM_INPUT_SHIFT(photo_mode_left, photo_mode_right), + .y = -CAM_INPUT_SHIFT(photo_mode_down, photo_mode_up), + .z = CAM_INPUT_SHIFT(photo_mode_back, photo_mode_forward), + }; + + const DIRECTION direction = + (uint16_t)(g_Camera.target_angle + PHD_45) / PHD_90; + switch (direction) { + case DIR_EAST: { + int16_t temp; + SWAP(shift.x, shift.z, temp); + shift.z *= -1; + break; + } + case DIR_SOUTH: { + shift.z *= -1; + shift.x *= -1; + break; + } + case DIR_WEST: { + int16_t temp; + SWAP(shift.x, shift.z, temp); + shift.x *= -1; + break; + } + default: + break; + } + + SHIFT_POS(g_Camera.pos, shift); + SHIFT_POS(g_Camera.target, shift); + + if (g_InputDB.roll) { + g_Camera.target_angle += (int16_t)PHD_90; + } else if (g_Input.left) { + g_Camera.target_angle -= (int16_t)CAM_ROT_SHIFT; + } else if (g_Input.right) { + g_Camera.target_angle += (int16_t)CAM_ROT_SHIFT; + } + + if (g_Input.forward) { + g_Camera.target.y += CAM_SPEED_SHIFT(8); + } else if (g_Input.back) { + g_Camera.target.y -= CAM_SPEED_SHIFT(8); + } + + if (rot_input) { + const int32_t distance = + g_Camera.target_distance * Math_Cos(g_Camera.target_elevation) + >> W2V_SHIFT; + g_Camera.target_square = SQUARE(distance); + + const PHD_ANGLE angle = g_Camera.target_angle; + g_Camera.pos.x = + g_Camera.target.x - (distance * Math_Sin(angle) >> W2V_SHIFT); + g_Camera.pos.z = + g_Camera.target.z - (distance * Math_Cos(angle) >> W2V_SHIFT); + } + + LOS_Check(&g_Camera.target, &g_Camera.pos); + + // The extra test without a clamp is needed for the beginning of Folly, + // where the camera starts in a bad position. + if ((M_PhotoBadPosition(g_Camera.pos, PHOTO_CLAMP) + || M_PhotoBadPosition(g_Camera.target, PHOTO_CLAMP)) + && M_PhotoBadPosition(g_Camera.pos, 0)) { + g_Camera.pos.x = old_pos.x; + g_Camera.pos.z = old_pos.z; + g_Camera.target.x = old_target.x; + g_Camera.target.z = old_target.z; + } + + const int32_t y_shift = + MAX(M_ClampPhotoY(&g_Camera.pos), M_ClampPhotoY(&g_Camera.target)); + g_Camera.pos.y += y_shift; + g_Camera.target.y += y_shift; + + if (g_Camera.pos.y >= NO_BAD_POS || g_Camera.pos.y <= NO_BAD_NEG + || g_Camera.target.y >= NO_BAD_POS || g_Camera.target.y <= NO_BAD_NEG) { + g_Camera.pos.y = old_pos.y; + g_Camera.target.y = old_target.y; + } +} + +static void M_ExitPhotoMode(void) +{ + g_Camera = m_OldCamera; + m_Roll = 0; + m_PhotoMode = false; +} + void Camera_Update(void) { + if (Phase_Get() == PHASE_PHOTO_MODE) { + M_UpdatePhotoMode(); + return; + } else if (m_PhotoMode) { + M_ExitPhotoMode(); + } + if (g_Camera.type == CAM_CINEMATIC) { M_LoadCutsceneFrame(); return; @@ -761,7 +986,7 @@ void Camera_Update(void) g_Camera.item = NULL; g_Camera.target_angle = g_Camera.additional_angle; g_Camera.target_elevation = g_Camera.additional_elevation; - g_Camera.target_distance = WALL_L * 3 / 2; + g_Camera.target_distance = DEFAULT_DISTANCE; g_Camera.flags = 0; } @@ -877,5 +1102,6 @@ void Camera_Apply(void) g_Camera.interp.result.pos.x, g_Camera.interp.result.pos.y + g_Camera.interp.result.shift, g_Camera.interp.result.pos.z, g_Camera.interp.result.target.x, - g_Camera.interp.result.target.y, g_Camera.interp.result.target.z, 0); + g_Camera.interp.result.target.y, g_Camera.interp.result.target.z, + m_Roll); } diff --git a/src/game/camera.h b/src/game/camera.h index 7ecddb99a..2b594eb27 100644 --- a/src/game/camera.h +++ b/src/game/camera.h @@ -17,3 +17,5 @@ void Camera_OffsetReset(void); void Camera_RefreshFromTrigger(const TRIGGER *trigger); void Camera_MoveManual(void); void Camera_Apply(void); +int32_t Camera_GetPhotoMaxSpeed(void); +int32_t Camera_GetPhotoCurrentSpeed(void); diff --git a/src/game/phase/phase.c b/src/game/phase/phase.c index 5edce5f4c..2571560fc 100644 --- a/src/game/phase/phase.c +++ b/src/game/phase/phase.c @@ -8,6 +8,7 @@ #include "game/phase/phase_game.h" #include "game/phase/phase_inventory.h" #include "game/phase/phase_pause.h" +#include "game/phase/phase_photo_mode.h" #include "game/phase/phase_picture.h" #include "game/phase/phase_stats.h" #include "global/types.h" @@ -93,6 +94,10 @@ static void M_SetUnconditionally(const PHASE phase, void *arg) case PHASE_INVENTORY: m_Phaser = &g_InventoryPhaser; break; + + case PHASE_PHOTO_MODE: + m_Phaser = &g_PhotoModePhaser; + break; } if (m_Phaser && m_Phaser->start) { diff --git a/src/game/phase/phase.h b/src/game/phase/phase.h index e7d90b5ca..59e387f25 100644 --- a/src/game/phase/phase.h +++ b/src/game/phase/phase.h @@ -28,6 +28,7 @@ typedef enum PHASE { PHASE_PICTURE, PHASE_STATS, PHASE_INVENTORY, + PHASE_PHOTO_MODE, } PHASE; typedef struct PHASER { diff --git a/src/game/phase/phase_game.c b/src/game/phase/phase_game.c index fa61b40c3..9ace87f4b 100644 --- a/src/game/phase/phase_game.c +++ b/src/game/phase/phase_game.c @@ -106,6 +106,9 @@ static PHASE_CONTROL M_Control(int32_t nframes) if (!g_Lara.death_timer && g_InputDB.pause) { Phase_Set(PHASE_PAUSE, NULL); return (PHASE_CONTROL) { .end = false }; + } else if (g_InputDB.toggle_photo_mode) { + Phase_Set(PHASE_PHOTO_MODE, NULL); + return (PHASE_CONTROL) { .end = false }; } else { Item_Control(); Effect_Control(); diff --git a/src/game/phase/phase_photo_mode.c b/src/game/phase/phase_photo_mode.c new file mode 100644 index 000000000..e890cd969 --- /dev/null +++ b/src/game/phase/phase_photo_mode.c @@ -0,0 +1,192 @@ +#include "game/phase/phase_photo_mode.h" + +#include "game/camera.h" +#include "game/console.h" +#include "game/game.h" +#include "game/input.h" +#include "game/interpolation.h" +#include "game/music.h" +#include "game/overlay.h" +#include "game/shell.h" +#include "game/sound.h" +#include "game/text.h" +#include "game/viewport.h" +#include "global/vars.h" + +#include + +#define MIN_PHOTO_FOV 10 +#define MAX_PHOTO_FOV 140 +#define LABEL_COUNT 10 + +typedef enum { + PS_NONE, + PS_ACTIVE, + PS_COOLDOWN, +} PHOTO_STATUS; + +static int32_t m_OldFOV; +static int32_t m_CurrentFOV; + +static PHOTO_STATUS m_Status = PS_NONE; +static bool m_ShowHelp = false; +static TEXTSTRING *m_Labels[LABEL_COUNT]; +static BAR_INFO m_SpeedBar = { 0 }; + +static const char *const m_LabelStrs[LABEL_COUNT] = { + // clang-format off + "Photo Mode", + "QEWASD: Move camera", + "ARROWS: Rotate camera", + "STEP L/R: Roll camera", + "ROLL: Rotate 90 degrees", + "JUMP/WALK: Adjust FOV", + "LOOK: Reset camera", + "ENTER: Toggle help", + "ACTION: Take picture", + "F1/ESC: Exit", + // clang-format on +}; + +static void M_UpdateUI(void); +static void M_Start(void *arg); +static void M_End(void); +static PHASE_CONTROL M_Control(int32_t nframes); +static void M_Draw(void); +static void M_AdjustFOV(void); + +static void M_Start(void *arg) +{ + m_Status = PS_NONE; + g_OldInputDB = g_Input; + m_OldFOV = Viewport_GetFOV(); + m_CurrentFOV = g_Config.fov_value; + + Overlay_HideGameInfo(); + Music_Pause(); + Sound_PauseAll(); + + const int16_t x = 21; + int16_t y = 50; + m_Labels[0] = Text_Create(x, y, m_LabelStrs[0]); + y += 25; + + for (int32_t i = 1; i < LABEL_COUNT; i++) { + m_Labels[i] = Text_Create(x, y, m_LabelStrs[i]); + y += 20; + } + + m_SpeedBar.type = BT_PROGRESS; + m_SpeedBar.value = 0; + m_SpeedBar.max_value = Camera_GetPhotoMaxSpeed(); + m_SpeedBar.show = true; + m_SpeedBar.blink = false; + m_SpeedBar.timer = 0; + m_SpeedBar.color = g_Config.enemy_healthbar_color; + m_SpeedBar.location = BL_BOTTOM_CENTER; + + M_UpdateUI(); + if (!m_ShowHelp) { + Console_Log( + "Entering Photo Mode...\nPress ENTER for help\nPress F1 to exit"); + } +} + +static void M_UpdateUI(void) +{ + for (int32_t i = 0; i < LABEL_COUNT; i++) { + Text_Hide(m_Labels[i], !m_ShowHelp); + } +} + +static void M_End(void) +{ + g_Input = g_OldInputDB; + Viewport_SetFOV(m_OldFOV); + + Music_Unpause(); + Sound_UnpauseAll(); + + for (int32_t i = 0; i < LABEL_COUNT; i++) { + Text_Remove(m_Labels[i]); + m_Labels[i] = NULL; + } +} + +static PHASE_CONTROL M_Control(int32_t nframes) +{ + if (m_Status == PS_ACTIVE) { + Shell_MakeScreenshot(); + Sound_Effect(SFX_MENU_CHOOSE, NULL, SPM_ALWAYS); + m_Status = PS_COOLDOWN; + } else if (m_Status == PS_COOLDOWN) { + m_Status = PS_NONE; + } + + Input_Update(); + Shell_ProcessInput(); + + if (g_InputDB.toggle_photo_mode || g_InputDB.menu_back) { + Phase_Set(PHASE_GAME, NULL); + } else { + if (g_InputDB.menu_confirm && !g_InputDB.action) { + m_ShowHelp = !m_ShowHelp; + M_UpdateUI(); + } + + if (g_InputDB.action) { + m_Status = PS_ACTIVE; + } else { + M_AdjustFOV(); + Camera_Update(); + m_SpeedBar.value = Camera_GetPhotoCurrentSpeed(); + m_SpeedBar.show = m_SpeedBar.value > 0; + } + } + + return (PHASE_CONTROL) { .end = false }; +} + +static void M_AdjustFOV(void) +{ + if (g_InputDB.look) { + Viewport_SetFOV(m_OldFOV); + return; + } + + if (!(g_Input.jump ^ g_Input.slow)) { + return; + } + + if (g_Input.jump) { + m_CurrentFOV++; + } else { + m_CurrentFOV--; + } + CLAMP(m_CurrentFOV, MIN_PHOTO_FOV, MAX_PHOTO_FOV); + Viewport_SetFOV(m_CurrentFOV * PHD_DEGREE); +} + +static void M_Draw(void) +{ + Interpolation_Disable(); + Game_DrawScene(false); + Interpolation_Enable(); + + if (m_Status != PS_NONE) { + return; + } + + Text_Draw(); + if (m_SpeedBar.show) { + Overlay_BarDraw(&m_SpeedBar, RSR_BAR); + } +} + +PHASER g_PhotoModePhaser = { + .start = M_Start, + .end = M_End, + .control = M_Control, + .draw = M_Draw, + .wait = NULL, +}; diff --git a/src/game/phase/phase_photo_mode.h b/src/game/phase/phase_photo_mode.h new file mode 100644 index 000000000..78974a042 --- /dev/null +++ b/src/game/phase/phase_photo_mode.h @@ -0,0 +1,5 @@ +#pragma once + +#include "game/phase/phase.h" + +extern PHASER g_PhotoModePhaser; diff --git a/src/global/types.h b/src/global/types.h index c7841362d..42178cb7b 100644 --- a/src/global/types.h +++ b/src/global/types.h @@ -1673,6 +1673,13 @@ typedef union INPUT_STATE { uint64_t toggle_bilinear_filter : 1; uint64_t toggle_perspective_filter : 1; uint64_t toggle_fps_counter : 1; + uint64_t toggle_photo_mode : 1; + uint64_t photo_mode_down : 1; + uint64_t photo_mode_up : 1; + uint64_t photo_mode_forward : 1; + uint64_t photo_mode_back : 1; + uint64_t photo_mode_left : 1; + uint64_t photo_mode_right : 1; uint64_t menu_up : 1; uint64_t menu_down : 1; uint64_t menu_left : 1; diff --git a/src/specific/s_input.c b/src/specific/s_input.c index 38f2b59b8..2a5c1f4bc 100644 --- a/src/specific/s_input.c +++ b/src/specific/s_input.c @@ -981,6 +981,15 @@ INPUT_STATE S_Input_GetCurrentState( linput.toggle_fps_counter = M_Key(INPUT_ROLE_FPS, layout_num); linput.toggle_bilinear_filter = M_Key(INPUT_ROLE_BILINEAR, layout_num); linput.toggle_perspective_filter = KEY_DOWN(SDL_SCANCODE_F4); + + linput.toggle_photo_mode = KEY_DOWN(SDL_SCANCODE_F1); + linput.photo_mode_down = KEY_DOWN(SDL_SCANCODE_Q); + linput.photo_mode_up = KEY_DOWN(SDL_SCANCODE_E); + linput.photo_mode_forward = KEY_DOWN(SDL_SCANCODE_W); + linput.photo_mode_back = KEY_DOWN(SDL_SCANCODE_S); + linput.photo_mode_left = KEY_DOWN(SDL_SCANCODE_A); + linput.photo_mode_right = KEY_DOWN(SDL_SCANCODE_D); + // clang-format on if (m_Controller) {