From 708141d4be1fde8b380bd2fac6c15beff02e5b41 Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Fri, 4 Oct 2024 21:10:40 +0100 Subject: [PATCH] camera: add photo mode This adds a photo mode phase and camera logic to handle movement. No UI is presented currently. --- docs/tr1/CHANGELOG.md | 1 + docs/tr1/README.md | 1 + src/libtrx/include/libtrx/game/math.h | 5 + src/tr1/game/camera.c | 244 +++++++++++++++++++++++++- src/tr1/game/phase/phase.c | 5 + src/tr1/game/phase/phase.h | 1 + src/tr1/game/phase/phase_game.c | 3 + src/tr1/game/phase/phase_photo_mode.c | 122 +++++++++++++ src/tr1/game/phase/phase_photo_mode.h | 5 + src/tr1/game/room.c | 24 +++ src/tr1/game/room.h | 1 + src/tr1/meson.build | 1 + 12 files changed, 409 insertions(+), 4 deletions(-) create mode 100644 src/tr1/game/phase/phase_photo_mode.c create mode 100644 src/tr1/game/phase/phase_photo_mode.h diff --git a/docs/tr1/CHANGELOG.md b/docs/tr1/CHANGELOG.md index 561fbb16b..5503420bd 100644 --- a/docs/tr1/CHANGELOG.md +++ b/docs/tr1/CHANGELOG.md @@ -1,4 +1,5 @@ ## [Unreleased](https://github.com/LostArtefacts/TRX/compare/stable...develop) - ××××-××-×× +- added a photo mode feature (#1669) - added `/sfx` command - added `/nextlevel` alias to `/endlevel` console command - added `/quit` alias to `/exit` console command diff --git a/docs/tr1/README.md b/docs/tr1/README.md index 8e1815693..b709423d6 100644 --- a/docs/tr1/README.md +++ b/docs/tr1/README.md @@ -399,6 +399,7 @@ Not all options are turned on by default. Refer to `TR1X_ConfigTool.exe` for det - added a flag indicating if new game plus is unlocked to the player config which allows the player to select new game plus or not when making a new game - added weapons to Lara's empty holsters on pickup - added options to quiet or mute music while underwater +- added a photo mode feature - changed weapon pickup behavior when unarmed to set any weapon as the default weapon, not just pistols - fixed keys and items not working when drawing guns immediately after using them - fixed counting the secret in The Great Pyramid diff --git a/src/libtrx/include/libtrx/game/math.h b/src/libtrx/include/libtrx/game/math.h index 634f1cd6d..79c51ad49 100644 --- a/src/libtrx/include/libtrx/game/math.h +++ b/src/libtrx/include/libtrx/game/math.h @@ -32,6 +32,11 @@ typedef struct __PACKING { XYZ_16 max; } BOUNDS_16; +typedef struct __PACKING { + XYZ_32 min; + XYZ_32 max; +} BOUNDS_32; + #elif TR_VERSION == 2 typedef struct __PACKING { int16_t min_x; diff --git a/src/tr1/game/camera.c b/src/tr1/game/camera.c index a12c64b3c..96c2ae39e 100644 --- a/src/tr1/game/camera.c +++ b/src/tr1/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,17 +22,50 @@ #include #include +#define DEFAULT_DISTANCE (WALL_L * 3 / 2) +#define PHOTO_ROT_SHIFT (PHD_DEGREE * 4) +#define PHOTO_MAX_PITCH_ROLL (PHD_90 - PHD_DEGREE) +#define PHOTO_MAX_SPEED 100 + +#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 SHIFT_X(distance, elevation, angle) \ + (((distance * Math_Cos(elevation)) >> W2V_SHIFT) * Math_Sin(angle) \ + >> W2V_SHIFT) +#define SHIFT_Z(distance, elevation, angle) \ + (((distance * Math_Cos(elevation)) >> W2V_SHIFT) * Math_Cos(angle) \ + >> W2V_SHIFT) +#define SHIFT_Y(dy, distance, elevation) \ + (dy * -(distance * Math_Sin(elevation) >> W2V_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 BOUNDS_32 m_WorldBounds = { 0 }; + static void M_Chase(const ITEM *item); static void M_Combat(const ITEM *item); static void M_Look(const ITEM *item); static void M_Fixed(void); static void M_LoadCutsceneFrame(void); +static void M_UpdatePhotoMode(void); +static void M_ExitPhotoMode(void); + static void M_OffsetAdditionalAngle(int16_t delta); static void M_OffsetAdditionalElevation(int16_t delta); static void M_OffsetReset(void); @@ -137,7 +172,7 @@ static void M_Look(const ITEM *const 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; const int32_t distance = g_Camera.target_distance * Math_Cos(g_Camera.target_elevation) @@ -225,6 +260,196 @@ static void M_LoadCutsceneFrame(void) Viewport_SetFOV(ref->fov); } +static void M_UpdatePhotoMode(void) +{ + if (!m_PhotoMode) { + m_OldCamera = g_Camera; + m_PhotoMode = true; + } + + const bool axis_shift_input = g_Input.camera_up || g_Input.camera_down; + const bool dir_shift_input = g_Input.camera_forward || g_Input.camera_back + || g_Input.camera_left || g_Input.camera_right; + const bool rot_input = + g_Input.left || g_Input.right || g_Input.forward || g_Input.back; + const bool rot_target_input = g_InputDB.roll; + const bool roll_input = g_Input.step_left || g_Input.step_right; + + 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]; + g_Camera.target_elevation = angles[1]; + g_Camera.target_distance = DEFAULT_DISTANCE; + g_Camera.target_square = SQUARE(g_Camera.target_distance); + + if (g_InputDB.look) { + g_Camera = m_OldCamera; + m_PhotoSpeed = 0; + m_Roll = 0; + } else if (axis_shift_input || dir_shift_input || rot_input || roll_input) { + m_PhotoSpeed++; + CLAMPG(m_PhotoSpeed, PHOTO_MAX_SPEED); + } else { + m_PhotoSpeed = 0; + } + + if (g_Input.step_left) { + m_Roll -= CAM_ROT_SHIFT; + } else if (g_Input.step_right) { + m_Roll += CAM_ROT_SHIFT; + } + CLAMP(m_Roll, -PHOTO_MAX_PITCH_ROLL, PHOTO_MAX_PITCH_ROLL); + + if (!axis_shift_input && !dir_shift_input && !rot_input + && !rot_target_input) { + return; + } + + if (axis_shift_input) { + const int32_t distance = CAM_SPEED_SHIFT((WALL_L * 5.0) / LOGIC_FPS); + XYZ_32 shift = { + .x = 0, + .y = 0, + .z = 0, + }; + if (g_Input.camera_up) { + shift.y = -distance; + } else if (g_Input.camera_down) { + shift.y = distance; + } + SHIFT_POS(g_Camera.pos, shift); + SHIFT_POS(g_Camera.target, shift); + } + + if (dir_shift_input) { + const int32_t distance = CAM_SPEED_SHIFT((WALL_L * 5.0) / LOGIC_FPS); + + PHD_ANGLE angle = g_Camera.target_angle; + PHD_ANGLE elevation = g_Camera.target_elevation; + + if (g_Input.camera_forward || g_Input.camera_back) { + int32_t dy = 0; + if (g_Input.camera_forward) { + dy = 1; + } else { + angle += PHD_180; + dy = -1; + } + + const XYZ_32 shift = { + .x = SHIFT_X(distance, elevation, angle), + .z = SHIFT_Z(distance, elevation, angle), + .y = SHIFT_Y(dy, distance, elevation), + }; + SHIFT_POS(g_Camera.pos, shift); + SHIFT_POS(g_Camera.target, shift); + + 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); + angle = angles[0]; + elevation = angles[1]; + } + + if (g_Input.camera_left || g_Input.camera_right) { + if (g_Input.camera_left) { + angle -= PHD_90; + } else { + angle += PHD_90; + } + + const XYZ_32 shift = { + .x = SHIFT_X(distance, elevation, angle), + .z = SHIFT_Z(distance, elevation, angle), + .y = 0, + }; + SHIFT_POS(g_Camera.pos, shift); + SHIFT_POS(g_Camera.target, shift); + } + } + + if (rot_input) { + if (g_Input.forward) { + g_Camera.target_elevation -= CAM_ROT_SHIFT; + } else if (g_Input.back) { + g_Camera.target_elevation += CAM_ROT_SHIFT; + } + CLAMP( + g_Camera.target_elevation, -PHOTO_MAX_PITCH_ROLL, + PHOTO_MAX_PITCH_ROLL); + + 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; + } + const PHD_ANGLE angle = g_Camera.target_angle; + const PHD_ANGLE elevation = g_Camera.target_elevation; + const int32_t distance = g_Camera.target_distance; + const XYZ_32 shift = { + .x = SHIFT_X(distance, elevation, angle), + .z = SHIFT_Z(distance, elevation, angle), + .y = SHIFT_Y(1, distance, elevation), + }; + g_Camera.target.x = g_Camera.pos.x + shift.x; + g_Camera.target.y = g_Camera.pos.y + shift.y; + g_Camera.target.z = g_Camera.pos.z + shift.z; + } + + if (rot_target_input) { + const PHD_ANGLE angle = g_Camera.target_angle + PHD_90; + const PHD_ANGLE elevation = g_Camera.target_elevation; + const int32_t distance = g_Camera.target_distance; + const XYZ_32 shift = { + .x = SHIFT_X(distance, elevation, angle), + .z = SHIFT_Z(distance, elevation, angle), + .y = SHIFT_Y(1, distance, elevation), + }; + g_Camera.pos.x = g_Camera.target.x - shift.x; + g_Camera.pos.y = g_Camera.target.y - shift.y; + g_Camera.pos.z = g_Camera.target.z - shift.z; + } + + // While the camera is free, we want to clamp to within overall world bounds + // to help counteract getting lost in the void. + const GAME_VECTOR cam_pos = g_Camera.pos; + CLAMP(g_Camera.pos.x, m_WorldBounds.min.x, m_WorldBounds.max.x); + CLAMP(g_Camera.pos.y, m_WorldBounds.min.y, m_WorldBounds.max.y); + CLAMP(g_Camera.pos.z, m_WorldBounds.min.z, m_WorldBounds.max.z); + + g_Camera.target.x += (g_Camera.pos.x - cam_pos.x); + g_Camera.target.y += (g_Camera.pos.y - cam_pos.y); + g_Camera.target.z += (g_Camera.pos.z - cam_pos.z); + + const int16_t pos_room_num = + Room_GetIndexFromPos(g_Camera.pos.x, g_Camera.pos.y, g_Camera.pos.z); + const int16_t tar_room_num = Room_GetIndexFromPos( + g_Camera.target.x, g_Camera.target.y, g_Camera.target.z); + + if (pos_room_num != NO_ROOM) { + g_Camera.pos.room_num = pos_room_num; + if (tar_room_num == NO_ROOM) { + g_Camera.target.room_num = pos_room_num; + } + } + if (tar_room_num != NO_ROOM) { + g_Camera.target.room_num = tar_room_num; + if (pos_room_num == NO_ROOM) { + g_Camera.pos.room_num = tar_room_num; + } + } +} + +static void M_ExitPhotoMode(void) +{ + g_Camera = m_OldCamera; + m_Roll = 0; + m_PhotoMode = false; +} + static void M_OffsetAdditionalAngle(const int16_t delta) { g_Camera.additional_angle += delta; @@ -621,6 +846,9 @@ static void M_Shift( void Camera_Initialise(void) { + m_WorldBounds = Room_GetWorldBounds(); + m_PhotoMode = false; + m_Roll = 0; Camera_ResetPosition(); Camera_Update(); } @@ -645,7 +873,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; @@ -658,6 +886,13 @@ void Camera_ResetPosition(void) 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; @@ -801,7 +1036,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; } @@ -892,5 +1127,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/tr1/game/phase/phase.c b/src/tr1/game/phase/phase.c index 5edce5f4c..2571560fc 100644 --- a/src/tr1/game/phase/phase.c +++ b/src/tr1/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/tr1/game/phase/phase.h b/src/tr1/game/phase/phase.h index 1e3910ab2..603a65160 100644 --- a/src/tr1/game/phase/phase.h +++ b/src/tr1/game/phase/phase.h @@ -28,6 +28,7 @@ typedef enum { PHASE_PICTURE, PHASE_STATS, PHASE_INVENTORY, + PHASE_PHOTO_MODE, } PHASE; typedef struct { diff --git a/src/tr1/game/phase/phase_game.c b/src/tr1/game/phase/phase_game.c index 435be5335..14a5c5fc8 100644 --- a/src/tr1/game/phase/phase_game.c +++ b/src/tr1/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/tr1/game/phase/phase_photo_mode.c b/src/tr1/game/phase/phase_photo_mode.c new file mode 100644 index 000000000..67695793b --- /dev/null +++ b/src/tr1/game/phase/phase_photo_mode.c @@ -0,0 +1,122 @@ +#include "game/phase/phase_photo_mode.h" + +#include "game/camera.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/viewport.h" + +#include + +#include + +#define MIN_PHOTO_FOV 10 +#define MAX_PHOTO_FOV 150 + +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 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 = m_OldFOV / PHD_DEGREE; + + Overlay_HideGameInfo(); + Music_Pause(); + Sound_PauseAll(); +} + +static void M_End(void) +{ + g_Input = g_OldInputDB; + Viewport_SetFOV(m_OldFOV); + + Music_Unpause(); + Sound_UnpauseAll(); +} + +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.action) { + m_Status = PS_ACTIVE; + } else { + M_AdjustFOV(); + Camera_Update(); + } + + return (PHASE_CONTROL) { .end = false }; +} + +static void M_AdjustFOV(void) +{ + if (g_InputDB.look) { + Viewport_SetFOV(m_OldFOV); + m_CurrentFOV = m_OldFOV / PHD_DEGREE; + return; + } + + if (g_InputDB.toggle_ui) { + // This needs to be re-applied as Config_Write() will have reset it. + Viewport_SetFOV(m_CurrentFOV * PHD_DEGREE); + } + + if (!g_Input.draw) { + return; + } + + if (g_Input.slow) { + 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(); +} + +PHASER g_PhotoModePhaser = { + .start = M_Start, + .end = M_End, + .control = M_Control, + .draw = M_Draw, + .wait = NULL, +}; diff --git a/src/tr1/game/phase/phase_photo_mode.h b/src/tr1/game/phase/phase_photo_mode.h new file mode 100644 index 000000000..78974a042 --- /dev/null +++ b/src/tr1/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/tr1/game/room.c b/src/tr1/game/room.c index 841ebcaff..3c7bf7373 100644 --- a/src/tr1/game/room.c +++ b/src/tr1/game/room.c @@ -527,6 +527,30 @@ int16_t Room_GetIndexFromPos(const int32_t x, const int32_t y, const int32_t z) return NO_ROOM; } +BOUNDS_32 Room_GetWorldBounds(void) +{ + BOUNDS_32 bounds = { + .min.x = 0x7FFFFFFF, + .min.z = 0x7FFFFFFF, + .max.x = 0, + .max.z = 0, + .min.y = MAX_HEIGHT, + .max.y = -MAX_HEIGHT, + }; + + for (int32_t i = 0; i < g_RoomCount; i++) { + const ROOM *const room = &g_RoomInfo[i]; + bounds.min.x = MIN(bounds.min.x, room->pos.x); + bounds.max.x = MAX(bounds.max.x, room->pos.x + room->size.x * WALL_L); + bounds.min.z = MIN(bounds.min.z, room->pos.z); + bounds.max.z = MAX(bounds.max.z, room->pos.z + room->size.z * WALL_L); + bounds.min.y = MIN(bounds.min.y, room->max_ceiling); + bounds.max.y = MAX(bounds.max.y, room->min_floor); + } + + return bounds; +} + void Room_AlterFloorHeight(ITEM *item, int32_t height) { if (!height) { diff --git a/src/tr1/game/room.h b/src/tr1/game/room.h index 14df7cf0b..89cc5bc21 100644 --- a/src/tr1/game/room.h +++ b/src/tr1/game/room.h @@ -24,6 +24,7 @@ int16_t Room_GetCeiling(const SECTOR *sector, int32_t x, int32_t y, int32_t z); int16_t Room_GetHeight(const SECTOR *sector, int32_t x, int32_t y, int32_t z); int16_t Room_GetWaterHeight(int32_t x, int32_t y, int32_t z, int16_t room_num); int16_t Room_GetIndexFromPos(int32_t x, int32_t y, int32_t z); +BOUNDS_32 Room_GetWorldBounds(void); void Room_AlterFloorHeight(ITEM *item, int32_t height); diff --git a/src/tr1/meson.build b/src/tr1/meson.build index bcaf80e9a..b30afba19 100644 --- a/src/tr1/meson.build +++ b/src/tr1/meson.build @@ -261,6 +261,7 @@ sources = [ 'game/phase/phase_game.c', 'game/phase/phase_inventory.c', 'game/phase/phase_pause.c', + 'game/phase/phase_photo_mode.c', 'game/phase/phase_picture.c', 'game/phase/phase_stats.c', 'game/random.c',