tr2/game_flow: implement bonus level support

This implements bonus level handling the game flow for TR2, similar to
TR1.

Resolves #2668.
This commit is contained in:
lahm86 2025-03-22 10:07:50 +00:00
parent 9191c91407
commit 3d47493af6
11 changed files with 45 additions and 8 deletions

View file

@ -622,6 +622,7 @@
"STATS_ASSAULT_NO_TIMES_SET": "No Times Set",
"STATS_ASSAULT_TITLE": "BEST TIMES",
"STATS_BASIC_FMT": "%d",
"STATS_BONUS_STATISTICS": "Bonus Statistics",
"STATS_DETAIL_FMT": "%d of %d",
"STATS_DISTANCE_TRAVELLED": "Distance Travelled",
"STATS_FINAL_STATISTICS": "Final Statistics",

View file

@ -1,4 +1,5 @@
## [Unreleased](https://github.com/LostArtefacts/TRX/compare/tr2-0.10...develop) - ××××-××-××
- added the bonus level game flow type, which allows for levels to be unlocked if all main game secrets are found (#2668)
- fixed the final two levels not allowing for secrets to be counted in the statistics (#1582)
- removed the need to specify in the game flow levels that have no secrets (secrets will be automatically counted) (#1582)

View file

@ -182,6 +182,7 @@ game with new enhancements and features.
- added ability to skip FMVs with both the Action key
- added ability to skip end credits with the Action and Escape keys
- added the ability to specify per-level SFX files rather than enforcing the default (main.sfx) on all levels
- added the ability to define bonus levels in the game flow, which unlock when all main game secrets are found
- added -l/--level and -s/--save command line arguments
- expanded internal game memory limit from 7.5 MB to unlimited (within system memory cap)
- expanded maximum object textures from 2048 to unlimited (within game's overall memory cap)

View file

@ -52,3 +52,4 @@ CFG_INT32(g_Config, audio.sound_volume, 10)
CFG_INT32(g_Config, audio.music_volume, 10)
CFG_BOOL(g_Config, audio.enable_lara_mic, false)
CFG_ENUM(g_Config, audio.underwater_music_mode, UMM_FULL, UNDERWATER_MUSIC_MODE)
CFG_BOOL(g_Config, profile.bonus_level_unlock, false)

View file

@ -109,4 +109,8 @@ typedef struct {
int32_t scaler;
float sizer;
} rendering;
struct {
bool bonus_level_unlock;
} profile;
} CONFIG;

View file

@ -12,6 +12,7 @@
#include "game/stats.h"
#include "global/vars.h"
#include <libtrx/config.h>
#include <libtrx/debug.h>
static DECLARE_GF_EVENT_HANDLER(M_HandlePlayLevel);
@ -80,7 +81,7 @@ static DECLARE_GF_EVENT_HANDLER(M_HandlePlayLevel)
default:
Savegame_ApplyLogicToCurrentInfo(level);
if (level->type == GFL_NORMAL) {
if (level->type == GFL_NORMAL || level->type == GFL_BONUS) {
GF_InventoryModifier_Scan(level);
GF_InventoryModifier_Apply(level, GF_INV_REGULAR);
Stats_Reset();
@ -117,7 +118,7 @@ static DECLARE_GF_EVENT_HANDLER(M_HandlePlayLevel)
break;
default:
if (level->type == GFL_NORMAL) {
if (level->type == GFL_NORMAL || level->type == GFL_BONUS) {
GF_InventoryModifier_Scan(Game_GetCurrentLevel());
GF_InventoryModifier_Apply(Game_GetCurrentLevel(), GF_INV_REGULAR);
}
@ -141,6 +142,12 @@ static DECLARE_GF_EVENT_HANDLER(M_HandlePlayLevel)
gf_cmd = GF_RunGame(level, seq_ctx);
}
if (gf_cmd.action == GF_LEVEL_COMPLETE) {
// TODO: refactor, currently required to guarantee final statistics are
// accurate prior to jumping to a bonus level.
if (level->type == GFL_NORMAL || level->type == GFL_BONUS) {
START_INFO *const start = Savegame_GetCurrentInfo(level);
start->stats = g_SaveGame.current_stats;
}
gf_cmd.action = GF_NOOP;
}
return gf_cmd;
@ -170,6 +177,9 @@ static DECLARE_GF_EVENT_HANDLER(M_HandleLevelComplete)
START_INFO *const start = Savegame_GetCurrentInfo(current_level);
start->stats = g_SaveGame.current_stats;
start->available = 0;
g_Config.profile.bonus_level_unlock =
Stats_CheckAllSecretsCollected(GFL_NORMAL);
if (next_level != nullptr) {
Savegame_PersistGameToCurrentInfo(next_level);
g_SaveGame.current_level = next_level->num;
@ -177,6 +187,9 @@ static DECLARE_GF_EVENT_HANDLER(M_HandleLevelComplete)
if (next_level == nullptr) {
return (GF_COMMAND) { .action = GF_NOOP };
}
if (next_level->type == GFL_BONUS && !g_Config.profile.bonus_level_unlock) {
return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };
}
return (GF_COMMAND) {
.action = GF_START_GAME,
.param = next_level->num,

View file

@ -17,6 +17,7 @@ GS_DEFINE(OSD_SAVE_GAME, "Saved game to save slot %d")
GS_DEFINE(KEYMAP_USE_FLARE, "Flare")
GS_DEFINE(KEYMAP_CAMERA_RESET, "Camera Reset")
GS_DEFINE(STATS_TIME_TAKEN, "Time Taken")
GS_DEFINE(STATS_BONUS_STATISTICS, "Bonus Statistics")
GS_DEFINE(STATS_SECRETS, "Secrets Found")
GS_DEFINE(STATS_AMMO_USED, "Ammo Used")
GS_DEFINE(STATS_AMMO_HITS, "Hits")

View file

@ -817,7 +817,8 @@ void Lara_Initialise(const GF_LEVEL *const level)
g_Lara.right_arm.lock = 0;
g_Lara.creature = nullptr;
if (level->type == GFL_NORMAL && g_GF_LaraStartAnim) {
if ((level->type == GFL_NORMAL || level->type == GFL_BONUS)
&& g_GF_LaraStartAnim) {
g_Lara.water_status = LWS_ABOVE_WATER;
g_Lara.gun_status = LGS_HANDS_BUSY;
Item_SwitchToObjAnim(item, LA_EXTRA_BREATH, 0, O_LARA_EXTRA);

View file

@ -38,14 +38,14 @@ void Stats_UpdateTimer(void)
}
#endif
FINAL_STATS Stats_ComputeFinalStats(void)
FINAL_STATS Stats_ComputeFinalStats(GF_LEVEL_TYPE level_type)
{
FINAL_STATS result = {};
const GF_LEVEL_TABLE *const level_table = GF_GetLevelTable(GFLT_MAIN);
for (int32_t i = 0; i < level_table->count; i++) {
const GF_LEVEL *const level = &level_table->levels[i];
if (level->type == GFL_GYM) {
if (level->type != level_type) {
continue;
}
result.timer += g_SaveGame.start[i].stats.timer;
@ -109,3 +109,9 @@ int32_t Stats_GetSecrets(void)
{
return m_LevelSecrets;
}
bool Stats_CheckAllSecretsCollected(GF_LEVEL_TYPE level_type)
{
const FINAL_STATS stats = Stats_ComputeFinalStats(level_type);
return stats.found_secrets >= stats.total_secrets;
}

View file

@ -2,9 +2,12 @@
#include "global/types.h"
#include <libtrx/game/game_flow.h>
void Stats_StartTimer(void);
void Stats_UpdateTimer(void);
void Stats_Reset(void);
void Stats_CalculateStats(void);
int32_t Stats_GetSecrets(void);
FINAL_STATS Stats_ComputeFinalStats(void);
FINAL_STATS Stats_ComputeFinalStats(GF_LEVEL_TYPE level_type);
bool Stats_CheckAllSecretsCollected(GF_LEVEL_TYPE level_type);

View file

@ -38,6 +38,7 @@ typedef struct {
UI_STATS_DIALOG_ARGS args;
UI_WIDGET *requester;
int32_t listener;
GF_LEVEL_TYPE level_type;
} UI_STATS_DIALOG;
static void M_AddRow(
@ -168,7 +169,7 @@ static void M_AddLevelStatsRows(UI_STATS_DIALOG *const self)
static void M_AddFinalStatsRows(UI_STATS_DIALOG *const self)
{
const FINAL_STATS final_stats = Stats_ComputeFinalStats();
const FINAL_STATS final_stats = Stats_ComputeFinalStats(self->level_type);
const STATS_COMMON *stats = (STATS_COMMON *)&final_stats;
M_AddRowFromRole(self, M_ROW_TIMER, stats);
M_AddRowFromRole(self, M_ROW_ALL_SECRETS, stats);
@ -296,6 +297,7 @@ UI_WIDGET *UI_StatsDialog_Create(UI_STATS_DIALOG_ARGS args)
ASSERT(args.style == UI_STATS_DIALOG_STYLE_BORDERED);
self->args = args;
self->level_type = GF_GetLevel(GFLT_MAIN, self->args.level_num)->type;
self->requester = UI_Requester_Create((UI_REQUESTER_SETTINGS) {
.is_selectable = false,
.visible_rows = VISIBLE_ROWS,
@ -314,7 +316,10 @@ UI_WIDGET *UI_StatsDialog_Create(UI_STATS_DIALOG_ARGS args)
break;
case UI_STATS_DIALOG_MODE_FINAL:
UI_Requester_SetTitle(self->requester, GS(STATS_FINAL_STATISTICS));
const char *title = self->level_type == GFL_BONUS
? GS(STATS_BONUS_STATISTICS)
: GS(STATS_FINAL_STATISTICS);
UI_Requester_SetTitle(self->requester, title);
M_AddFinalStatsRows(self);
break;