tr2: make water color customizable like in TR1X

Tackles #1619. While does not offer a simple toggle for the PS1 style,
it is now possible to edit the game flow file by hand with those
settings.
This commit is contained in:
Marcin Kurczewski 2025-04-10 18:07:11 +02:00
parent 2aa0f2e161
commit 556702da1c
No known key found for this signature in database
GPG key ID: CC65E6FD28CAE42A
24 changed files with 197 additions and 95 deletions

View file

@ -476,6 +476,9 @@
"DETAIL_FOG_START": "Fog start",
"DETAIL_INTEGER_FMT": "%d",
"DETAIL_TITLE": "Graphic Options",
"DETAIL_WATER_COLOR_B": "Water color (B)",
"DETAIL_WATER_COLOR_G": "Water color (G)",
"DETAIL_WATER_COLOR_R": "Water color (R)",
"HEADING_GAME_OVER": "GAME OVER",
"HEADING_INVENTORY": "INVENTORY",
"HEADING_ITEMS": "ITEMS",

View file

@ -198,8 +198,8 @@ remains distinct for each game.
<td>
Water color (R, G, B). 1.0 means pass-through, 0.0 means no value at all.
<ul>
<li>[0.6, 0.7, 1.0] is the original DOS version filter.</li>
<li>[0.45, 1.0, 1.0] is the default TombATI filter.</li>
<li><code>[0.6, 0.7, 1.0]</code> is the original DOS version color.</li>
<li><code>[0.45, 1.0, 1.0]</code> is the default TombATI color.</li>
</ul>
</td>
</tr>
@ -331,12 +331,31 @@ remains distinct for each game.
<a name="draw-distance-max"></a>
<code>fog_end</code>
</td>
<td>Double<strong>*</strong></td>
<td>Double</td>
<td>
The distance (in tiles) at which objects and the world are clipped away.
The default value in OG TR2 is hardcoded to 20.
</td>
</tr>
<tr valign="top">
<td>
<a name="water-color"></a>
<code>water_color</code>
</td>
<td>Float array</td>
<td>
Water color (R, G, B). 1.0 means pass-through, 0.0 means no value at all.
<ul>
<li><code>[0.5, 0.75, 1.0]</code> is the default PC hardware renderer color.</li>
<li><code>[0.66, 0.66, 1.0]</code> is the default PC software renderer color.</li>
<li><code>[0.8, 1.0, 0.5]</code> is the PS1 color in the Venice, Bartoli's Hideout and Opera House levels.</li>
<li><code>[0.8, 1.0, 0.6]</code> is the PS1 color for the Temple of Xian level.</li>
<li><code>[0.8, 1.0, 0.8]</code> is the PS1 color for the Floating Islands and Dragon's Lair levels.</li>
<li><code>[0.7, 0.9, 0.9]</code> is the PS1 color for The Great Wall and Tibetan Foothills levels.</li>
<li><code>[0.5, 1.0, 1.0]</code> is the PS1 color for all other PS1 levels.</li>
</ul>
</td>
</tr>
</table>
## Game flow commands

View file

@ -2,6 +2,7 @@
- added support for The Golden Mask (#1621)
- added sunglasses for graphic options (#1615)
- added control over the fog distances for players and level builders (#1622)
- added control over the water color for players and level builders (#1619)
- added an installer for Windows (#2681)
- added the bonus level game flow type, which allows for levels to be unlocked if all main game secrets are found (#2668)
- added the ability for custom levels to have up to two of each secret type per level (#2674)

View file

@ -24,6 +24,7 @@ CFG_INT32(g_Config, visuals.fov, 80)
CFG_BOOL(g_Config, visuals.use_pcx_fov, true)
CFG_INT32(g_Config, visuals.fog_start, 22)
CFG_INT32(g_Config, visuals.fog_end, 30)
CFG_RGB888(g_Config, visuals.water_color, 127, 223, 255)
CFG_DOUBLE(g_Config, ui.text_scale, 1.0)
CFG_DOUBLE(g_Config, ui.bar_scale, 1.0)
CFG_BOOL(g_Config, ui.enable_photo_mode_ui, true)

View file

@ -101,6 +101,26 @@ static void M_LoadCommonSettings(
settings->fog_end.value = value;
}
}
{
JSON_ARRAY *const tmp_arr = JSON_ObjectGetArray(obj, "water_color");
if (tmp_arr != nullptr) {
const RGB_F color = {
JSON_ArrayGetDouble(tmp_arr, 0, JSON_INVALID_NUMBER),
JSON_ArrayGetDouble(tmp_arr, 1, JSON_INVALID_NUMBER),
JSON_ArrayGetDouble(tmp_arr, 2, JSON_INVALID_NUMBER),
};
if (color.r != JSON_INVALID_NUMBER && color.g != JSON_INVALID_NUMBER
&& color.b != JSON_INVALID_NUMBER) {
settings->water_color.is_present = true;
settings->water_color.value = (RGB_888) {
color.r * 255.0f,
color.g * 255.0f,
color.b * 255.0f,
};
}
}
}
}
static void M_LoadCommonRoot(JSON_OBJECT *const obj, GAME_FLOW *const gf)

View file

@ -99,26 +99,6 @@ static void M_LoadSettings(
JSON_OBJECT *const obj, GF_LEVEL_SETTINGS *const settings)
{
M_LoadCommonSettings(obj, settings);
{
JSON_ARRAY *const tmp_arr = JSON_ObjectGetArray(obj, "water_color");
if (tmp_arr != nullptr) {
const RGB_F color = {
JSON_ArrayGetDouble(tmp_arr, 0, JSON_INVALID_NUMBER),
JSON_ArrayGetDouble(tmp_arr, 1, JSON_INVALID_NUMBER),
JSON_ArrayGetDouble(tmp_arr, 2, JSON_INVALID_NUMBER),
};
if (color.r != JSON_INVALID_NUMBER && color.g != JSON_INVALID_NUMBER
&& color.b != JSON_INVALID_NUMBER) {
settings->water_color.is_present = true;
settings->water_color.value = (RGB_888) {
color.r * 255.0f,
color.g * 255.0f,
color.b * 255.0f,
};
}
}
}
}
static void M_LoadLevelGameSpecifics(

View file

@ -0,0 +1,31 @@
#include "game/level/settings.h"
#include "config.h"
#include "game/game_flow.h"
RGB_888 Level_GetWaterColor(void)
{
const GF_LEVEL *const level = GF_GetCurrentLevel();
if (level != nullptr && level->settings.water_color.is_present) {
return level->settings.water_color.value;
}
return g_Config.visuals.water_color;
}
float Level_GetFogStart(void)
{
const GF_LEVEL *const level = GF_GetCurrentLevel();
if (level != nullptr && level->settings.fog_start.is_present) {
return level->settings.fog_start.value;
}
return g_Config.visuals.fog_start;
}
float Level_GetFogEnd(void)
{
const GF_LEVEL *const level = GF_GetCurrentLevel();
if (level != nullptr && level->settings.fog_end.is_present) {
return level->settings.fog_end.value;
}
return g_Config.visuals.fog_end;
}

View file

@ -1,5 +1,6 @@
#pragma once
#include "../colors.h"
#include "../game/input.h"
#include "../game/sound/enum.h"
#include "../gfx/common.h"
@ -50,6 +51,8 @@ typedef struct {
bool fix_item_rots;
int32_t fov;
bool use_pcx_fov;
RGB_888 water_color;
int32_t fog_start;
int32_t fog_end;
} visuals;

View file

@ -82,12 +82,11 @@ typedef struct {
bool is_present;
float value;
} fog_start, fog_end;
#if TR_VERSION == 1
struct {
bool is_present;
RGB_888 value;
} water_color;
#elif TR_VERSION == 2
#if TR_VERSION == 2
char *sfx_path;
#endif
} GF_LEVEL_SETTINGS;

View file

@ -132,3 +132,6 @@ GS_DEFINE(DETAIL_FLOAT_FMT, "%.1f")
GS_DEFINE(DETAIL_TITLE, "Graphic Options")
GS_DEFINE(DETAIL_FOG_START, "Fog start")
GS_DEFINE(DETAIL_FOG_END, "Fog end")
GS_DEFINE(DETAIL_WATER_COLOR_R, "Water color (R)")
GS_DEFINE(DETAIL_WATER_COLOR_G, "Water color (G)")
GS_DEFINE(DETAIL_WATER_COLOR_B, "Water color (B)")

View file

@ -1,4 +1,5 @@
#pragma once
#include "level/common.h"
#include "level/settings.h"
#include "level/types.h"

View file

@ -0,0 +1,7 @@
#pragma once
#include "../../colors.h"
extern RGB_888 Level_GetWaterColor(void);
extern float Level_GetFogStart(void);
extern float Level_GetFogEnd(void);

View file

@ -163,6 +163,7 @@ sources = [
'game/lara/misc.c',
'game/level/common.c',
'game/level/faces.c',
'game/level/settings.c',
'game/math/trig.c',
'game/math/util.c',
'game/matrix.c',

View file

@ -8,9 +8,6 @@ GS_DEFINE(PASSPORT_MODE_NEW_GAME_PLUS, "New Game+")
GS_DEFINE(PASSPORT_MODE_NEW_GAME_JP, "Japanese NG")
GS_DEFINE(PASSPORT_MODE_NEW_GAME_JP_PLUS, "Japanese NG+")
GS_DEFINE(DETAIL_FPS, "FPS")
GS_DEFINE(DETAIL_WATER_COLOR_R, "Water color (R)")
GS_DEFINE(DETAIL_WATER_COLOR_G, "Water color (G)")
GS_DEFINE(DETAIL_WATER_COLOR_B, "Water color (B)")
GS_DEFINE(DETAIL_TRAPEZOID_FILTER, "Trapezoid filter")
GS_DEFINE(DETAIL_REFLECTIONS, "Reflections")
GS_DEFINE(DETAIL_BILINEAR, "Bilinear")

View file

@ -4,7 +4,6 @@
#include "game/carrier.h"
#include "game/effects.h"
#include "game/game.h"
#include "game/game_flow.h"
#include "game/inventory_ring/vars.h"
#include "game/items.h"
#include "game/lara/common.h"
@ -333,33 +332,6 @@ void Level_Load(const GF_LEVEL *const level)
Benchmark_End(&benchmark, nullptr);
}
RGB_888 Level_GetWaterColor(void)
{
const GF_LEVEL *const level = GF_GetCurrentLevel();
if (level != nullptr && level->settings.water_color.is_present) {
return level->settings.water_color.value;
}
return g_Config.visuals.water_color;
}
float Level_GetFogStart(void)
{
const GF_LEVEL *const level = GF_GetCurrentLevel();
if (level != nullptr && level->settings.fog_start.is_present) {
return level->settings.fog_start.value;
}
return g_Config.visuals.fog_start;
}
float Level_GetFogEnd(void)
{
const GF_LEVEL *const level = GF_GetCurrentLevel();
if (level != nullptr && level->settings.fog_end.is_present) {
return level->settings.fog_end.value;
}
return g_Config.visuals.fog_end;
}
void Level_Unload(void)
{
Output_ObserveLevelUnload();

View file

@ -2,11 +2,7 @@
#include "game/game_flow/types.h"
#include <libtrx/colors.h>
#include <libtrx/game/level.h>
bool Level_Initialise(const GF_LEVEL *level, GF_SEQUENCE_CONTEXT seq_ctx);
void Level_Load(const GF_LEVEL *level);
RGB_888 Level_GetWaterColor(void);
float Level_GetFogStart(void);
float Level_GetFogEnd(void);

View file

@ -387,7 +387,6 @@ void Output_SetWindowSize(int32_t width, int32_t height)
void Output_ApplyLevelSettings(void)
{
const RGB_888 color = Level_GetWaterColor();
Output_SetWaterColor(Level_GetWaterColor());
Output_SetFogStart(Level_GetFogStart() * WALL_L);
Output_SetFogEnd(Level_GetFogEnd() * WALL_L);

View file

@ -410,21 +410,3 @@ void Level_Init(void)
Benchmark_End(&benchmark, nullptr);
}
float Level_GetFogStart(void)
{
const GF_LEVEL *const level = GF_GetCurrentLevel();
if (level != nullptr && level->settings.fog_start.is_present) {
return level->settings.fog_start.value;
}
return g_Config.visuals.fog_start;
}
float Level_GetFogEnd(void)
{
const GF_LEVEL *const level = GF_GetCurrentLevel();
if (level != nullptr && level->settings.fog_end.is_present) {
return level->settings.fog_end.value;
}
return g_Config.visuals.fog_end;
}

View file

@ -2,12 +2,9 @@
#include "game/game_flow/types.h"
#include <libtrx/game/level/common.h>
#include <libtrx/game/level.h>
void Level_Init(void);
bool Level_Initialise(const GF_LEVEL *level, GF_SEQUENCE_CONTEXT seq_ctx);
bool Level_Load(const GF_LEVEL *level);
void Level_Unload(void);
float Level_GetFogStart(void);
float Level_GetFogEnd(void);

View file

@ -66,6 +66,7 @@ static BACKGROUND_TYPE m_BackgroundType = BK_TRANSPARENT;
static XYZ_32 m_LsVectorView = {};
static int32_t m_FogEnd = 0;
static RGB_F m_WaterColor = {};
static bool m_IsWaterEffect = false;
static bool m_IsWibbleEffect = false;
@ -411,6 +412,7 @@ static void M_CalcSkyboxLight(const OBJECT_MESH *const mesh)
void Output_ApplyLevelSettings(void)
{
Output_SetWaterColor(Level_GetWaterColor());
Output_SetFogStart(Level_GetFogStart() * WALL_L);
Output_SetFogEnd(Level_GetFogEnd() * WALL_L);
Viewport_Reset();
@ -940,6 +942,21 @@ int32_t Output_GetFarZ(void)
return Output_GetFogEnd() << W2V_SHIFT;
}
void Output_SetWaterColor(const RGB_888 color)
{
m_WaterColor.r = color.r / 255.0f;
m_WaterColor.g = color.g / 255.0f;
m_WaterColor.b = color.b / 255.0f;
}
RGB_F Output_GetTint(void)
{
if (Output_IsShadeEffect()) {
return m_WaterColor;
}
return (RGB_F) { 1.0f, 1.0f, 1.0f };
}
int32_t Output_GetFogEnd(void)
{
return m_FogEnd;

View file

@ -91,3 +91,6 @@ void Output_SetSunsetTimer(int32_t timer);
int32_t Output_GetNearZ(void);
int32_t Output_GetFarZ(void);
void Output_SetWaterColor(RGB_888 color);
RGB_F Output_GetTint(void);

View file

@ -130,13 +130,10 @@ static void M_ShadeColor(
GFX_3D_VERTEX *const target, uint32_t red, uint32_t green,
const uint32_t blue, const uint8_t alpha)
{
if (Output_IsShadeEffect()) {
red /= 2;
green = green * 7 / 8;
}
target->r = red;
target->g = green;
target->b = blue;
const RGB_F tint = Output_GetTint();
target->r = red * tint.r;
target->g = green * tint.g;
target->b = blue * tint.b;
target->a = alpha;
}

View file

@ -1439,8 +1439,11 @@ static void M_SetWet(RENDERER *const renderer, const bool is_wet)
{
M_PRIV *const priv = renderer->priv;
if (is_wet) {
const RGB_F tint = Output_GetTint();
GFX_2D_Renderer_SetTint(
priv->renderer_2d, (GFX_COLOR) { .r = 170, .g = 170, .b = 255 });
priv->renderer_2d,
(GFX_COLOR) {
.r = tint.r * 255, .g = tint.g * 255, .b = tint.b * 255 });
} else {
GFX_2D_Renderer_SetTint(
priv->renderer_2d, (GFX_COLOR) { .r = 255, .g = 255, .b = 255 });

View file

@ -1,6 +1,7 @@
#include "game/ui/widgets/graphics_dialog.h"
#include <libtrx/config.h>
#include <libtrx/debug.h>
#include <libtrx/game/game_string.h>
#include <libtrx/game/input.h>
#include <libtrx/game/text.h>
@ -19,6 +20,7 @@ typedef struct {
void *target;
int32_t min_value;
int32_t max_value;
int32_t misc;
} M_OPTION;
typedef struct {
@ -58,6 +60,7 @@ static M_OPTION m_Options[] = {
.min_value = 1,
.max_value = 100,
},
{
.option_type = COT_INT32,
.label_id = GS_ID(DETAIL_FOG_END),
@ -65,11 +68,40 @@ static M_OPTION m_Options[] = {
.min_value = 1,
.max_value = 100,
},
{
.option_type = COT_RGB888,
.label_id = GS_ID(DETAIL_WATER_COLOR_R),
.target = &g_Config.visuals.water_color,
.min_value = 0,
.max_value = 255,
.misc = 0,
},
{
.option_type = COT_RGB888,
.label_id = GS_ID(DETAIL_WATER_COLOR_G),
.target = &g_Config.visuals.water_color,
.min_value = 0,
.max_value = 255,
.misc = 1,
},
{
.option_type = COT_RGB888,
.label_id = GS_ID(DETAIL_WATER_COLOR_B),
.target = &g_Config.visuals.water_color,
.min_value = 0,
.max_value = 255,
.misc = 2,
},
{
.target = nullptr,
},
};
static uint8_t *M_GetColorComponent(const M_OPTION *option);
static void M_ClearRows(M_WIDGET *self);
static char *M_FormatRowValue(int32_t row_idx);
static bool M_CanChangeValue(int32_t row_idx, int32_t delta);
@ -90,6 +122,21 @@ static void M_Control(M_WIDGET *self);
static void M_Draw(M_WIDGET *self);
static void M_Free(M_WIDGET *self);
static uint8_t *M_GetColorComponent(const M_OPTION *const option)
{
RGB_888 *const color = option->target;
switch (option->misc) {
case 0:
return &color->r;
case 1:
return &color->g;
case 2:
return &color->b;
}
ASSERT_FAIL();
return nullptr;
}
static void M_ClearRows(M_WIDGET *const self)
{
for (int32_t i = 0; i < self->row_count; i++) {
@ -115,6 +162,11 @@ static char *M_FormatRowValue(const int32_t row_idx)
return String_Format(
GS(DETAIL_INTEGER_FMT), *(int32_t *)option->target);
break;
case COT_RGB888: {
const uint8_t *const component = M_GetColorComponent(option);
return String_Format("%d", *component);
break;
}
default:
break;
}
@ -132,6 +184,15 @@ static bool M_CanChangeValue(const int32_t row_idx, const int32_t delta)
return *(int32_t *)option->target < option->max_value;
}
break;
case COT_RGB888: {
const uint8_t *const component = M_GetColorComponent(option);
if (delta < 0) {
return *component > option->min_value;
} else if (delta > 0) {
return *component < option->max_value;
}
break;
}
default:
return false;
}
@ -160,6 +221,11 @@ static bool M_RequestChangeValue(
case COT_INT32:
*(int32_t *)option->target += delta;
break;
case COT_RGB888: {
uint8_t *const component = M_GetColorComponent(option);
*component += delta;
break;
}
default:
return false;
}
@ -197,13 +263,13 @@ static M_ROW *M_AddRow(
M_ROW *const row = &self->rows[self->row_count - 1];
row->stack =
UI_Stack_Create(UI_STACK_LAYOUT_HORIZONTAL, 200, UI_STACK_AUTO_SIZE);
UI_Stack_Create(UI_STACK_LAYOUT_HORIZONTAL, 220, UI_STACK_AUTO_SIZE);
UI_Stack_SetHAlign(row->stack, UI_STACK_H_ALIGN_DISTRIBUTE);
row->frame = UI_Frame_Create(row->stack, 0, 0);
UI_Frame_SetFrameVisible(row->frame, false);
row->title_label = UI_Label_Create(left_text, 120, UI_LABEL_AUTO_SIZE);
row->title_label = UI_Label_Create(left_text, 150, UI_LABEL_AUTO_SIZE);
UI_Stack_AddChild(row->stack, row->title_label);
row->arrow_left_label = UI_Label_Create(
@ -349,7 +415,6 @@ UI_WIDGET *UI_GraphicsDialog_Create(void)
.free = (UI_WIDGET_FREE)M_Free,
};
self->visible_rows = 2;
self->outer_stack = UI_Stack_Create(
UI_STACK_LAYOUT_VERTICAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE);
UI_Stack_SetHAlign(self->outer_stack, UI_STACK_H_ALIGN_CENTER);
@ -361,6 +426,11 @@ UI_WIDGET *UI_GraphicsDialog_Create(void)
self->listener = UI_Events_Subscribe(
"canvas_resize", nullptr, M_HandleCanvasResize, self);
self->visible_rows = 0;
for (int32_t i = 0; m_Options[i].target != nullptr; i++) {
self->visible_rows++;
}
for (int32_t i = 0; m_Options[i].target != nullptr; i++) {
char *value_text = M_FormatRowValue(i);
M_AddRow(