ui2: port stats dialogs

This commit is contained in:
Marcin Kurczewski 2025-04-15 21:40:26 +02:00
parent 7bf7c2e6b5
commit dfa3797b87
21 changed files with 757 additions and 843 deletions

View file

@ -25,6 +25,7 @@
- fixed the sprite UVs to restore the right and bottom edge pixels (#2672, regression from 4.8)
- fixed sprites missing the fog effect (regression from 4.9)
- fixed the camera going out of bounds in 60fps near specific invalid floor data (known as no-space) (#2764, regression from 4.9)
- fixed wrong PS1-style title bar color for the end of the level stats dialog (regression from 4.9)
- improved bubble appearance (#2672)
- improved rendering performance
- improved pause exit dialog - it can now be canceled with escape

View file

@ -9,7 +9,7 @@
#include "game/interpolation.h"
#include "game/shell.h"
#include "game/text.h"
#include "game/ui/widgets/stats_dialog.h"
#include "game/ui2.h"
#include "memory.h"
typedef enum {
@ -24,7 +24,8 @@ typedef struct {
STATE state;
FADER back_fader;
FADER top_fader;
UI_WIDGET *ui;
bool ui_active;
UI2_STATS_DIALOG_STATE ui_state;
} M_PRIV;
static bool M_IsFading(M_PRIV *p);
@ -86,14 +87,19 @@ static PHASE_CONTROL M_Start(PHASE *const phase)
M_FadeIn(p);
}
p->ui = UI_StatsDialog_Create((UI_STATS_DIALOG_ARGS) {
.mode = p->args.show_final_stats ? UI_STATS_DIALOG_MODE_FINAL
: UI_STATS_DIALOG_MODE_LEVEL,
.style = p->args.use_bare_style ? UI_STATS_DIALOG_STYLE_BARE
: UI_STATS_DIALOG_STYLE_BORDERED,
.level_num = p->args.level_num != -1 ? p->args.level_num
: Game_GetCurrentLevel()->num,
});
p->ui_active = true;
UI2_StatsDialog_Init(
&p->ui_state,
(UI2_STATS_DIALOG_ARGS) {
.mode = p->args.show_final_stats ? UI2_STATS_DIALOG_MODE_FINAL
: UI2_STATS_DIALOG_MODE_LEVEL,
.style = p->args.use_bare_style
? UI2_STATS_DIALOG_STYLE_BARE
: UI2_STATS_DIALOG_STYLE_BORDERED,
.level_num = p->args.level_num != -1
? p->args.level_num
: Game_GetCurrentLevel()->num,
});
}
return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };
@ -102,10 +108,11 @@ static PHASE_CONTROL M_Start(PHASE *const phase)
static void M_End(PHASE *const phase)
{
M_PRIV *const p = phase->priv;
Output_UnloadBackground();
if (p->ui != nullptr) {
p->ui->free(p->ui);
if (p->ui_active) {
p->ui_active = false;
UI2_StatsDialog_Free(&p->ui_state);
}
Output_UnloadBackground();
}
static PHASE_CONTROL M_Control(PHASE *const phase, int32_t num_frames)
@ -143,9 +150,6 @@ static PHASE_CONTROL M_Control(PHASE *const phase, int32_t num_frames)
};
}
if (p->ui != nullptr) {
p->ui->control(p->ui);
}
return (PHASE_CONTROL) { .action = PHASE_ACTION_CONTINUE };
}
@ -162,13 +166,11 @@ static void M_Draw(PHASE *const phase)
}
Fader_Draw(&p->back_fader);
if (p->ui != nullptr) {
p->ui->draw(p->ui);
UI2_BeginFade(&p->top_fader, true);
if (p->ui_active) {
UI2_StatsDialog(&p->ui_state);
}
Text_Draw();
Output_DrawPolyList();
Fader_Draw(&p->top_fader);
UI2_EndFade();
}
PHASE *Phase_Stats_Create(const PHASE_STATS_ARGS args)

View file

@ -0,0 +1,18 @@
#include "game/ui2/dialogs/stats.h"
void UI2_StatsDialog_Init(
UI2_STATS_DIALOG_STATE *const s, const UI2_STATS_DIALOG_ARGS args)
{
UI2_Requester_Init(&s->assault_req, 7, 10, false);
s->args = args;
}
void UI2_StatsDialog_Free(UI2_STATS_DIALOG_STATE *const s)
{
UI2_Requester_Free(&s->assault_req);
}
int32_t UI2_StatsDialog_Control(UI2_STATS_DIALOG_STATE *const s)
{
return UI2_Requester_Control(&s->assault_req);
}

View file

@ -1,24 +0,0 @@
#pragma once
#include "./base.h"
typedef enum {
UI_STATS_DIALOG_MODE_LEVEL,
UI_STATS_DIALOG_MODE_FINAL,
#if TR_VERSION == 2
UI_STATS_DIALOG_MODE_ASSAULT_COURSE,
#endif
} UI_STATS_DIALOG_MODE;
typedef enum {
UI_STATS_DIALOG_STYLE_BARE,
UI_STATS_DIALOG_STYLE_BORDERED,
} UI_STATS_DIALOG_STYLE;
typedef struct {
UI_STATS_DIALOG_MODE mode;
UI_STATS_DIALOG_STYLE style;
int32_t level_num;
} UI_STATS_DIALOG_ARGS;
UI_WIDGET *UI_StatsDialog_Create(UI_STATS_DIALOG_ARGS args);

View file

@ -5,6 +5,7 @@
#include "./ui2/dialogs/new_game.h"
#include "./ui2/dialogs/pause.h"
#include "./ui2/dialogs/photo_mode.h"
#include "./ui2/dialogs/stats.h"
#include "./ui2/elements/anchor.h"
#include "./ui2/elements/fade.h"
#include "./ui2/elements/flash.h"

View file

@ -0,0 +1,35 @@
#pragma once
#include "../common.h"
#include "../elements/requester.h"
typedef enum {
UI2_STATS_DIALOG_MODE_LEVEL,
UI2_STATS_DIALOG_MODE_FINAL,
#if TR_VERSION == 2
UI2_STATS_DIALOG_MODE_ASSAULT_COURSE,
#endif
} UI2_STATS_DIALOG_MODE;
typedef enum {
UI2_STATS_DIALOG_STYLE_BARE,
UI2_STATS_DIALOG_STYLE_BORDERED,
} UI2_STATS_DIALOG_STYLE;
typedef struct {
UI2_STATS_DIALOG_MODE mode;
UI2_STATS_DIALOG_STYLE style;
int32_t level_num;
} UI2_STATS_DIALOG_ARGS;
typedef struct {
UI2_STATS_DIALOG_ARGS args;
UI2_REQUESTER_STATE assault_req;
} UI2_STATS_DIALOG_STATE;
void UI2_StatsDialog_Init(
UI2_STATS_DIALOG_STATE *s, UI2_STATS_DIALOG_ARGS args);
void UI2_StatsDialog_Free(UI2_STATS_DIALOG_STATE *s);
int32_t UI2_StatsDialog_Control(UI2_STATS_DIALOG_STATE *s);
extern void UI2_StatsDialog(UI2_STATS_DIALOG_STATE *s);

View file

@ -210,6 +210,7 @@ sources = [
'game/ui/widgets/window.c',
'game/ui2/common.c',
'game/ui2/dialogs/examine_item.c',
'game/ui2/dialogs/stats.c',
'game/ui2/dialogs/new_game.c',
'game/ui2/dialogs/pause.c',
'game/ui2/dialogs/photo_mode.c',

View file

@ -5,74 +5,80 @@
#include "game/game_string.h"
#include "game/input.h"
#include "game/text.h"
#include "game/ui/widgets/stats_dialog.h"
#include "global/vars.h"
#include <libtrx/config.h>
#include <libtrx/game/ui2.h>
#include <stdint.h>
#include <stdio.h>
static UI_WIDGET *m_Dialog = nullptr;
typedef struct {
struct {
bool is_ready;
UI2_STATS_DIALOG_STATE state;
} ui;
} M_PRIV;
static M_PRIV m_Priv = {};
static int16_t m_CompassNeedle = 0;
static int16_t m_CompassSpeed = 0;
static void M_Init(void);
static void M_Shutdown(void);
static void M_Init(M_PRIV *p);
static void M_Shutdown(M_PRIV *p);
static void M_Init(void)
static void M_Init(M_PRIV *const p)
{
m_Dialog = UI_StatsDialog_Create((UI_STATS_DIALOG_ARGS) {
.mode = UI_STATS_DIALOG_MODE_LEVEL,
.style = UI_STATS_DIALOG_STYLE_BORDERED,
.level_num = Game_GetCurrentLevel()->num,
});
p->ui.is_ready = true;
UI2_StatsDialog_Init(
&p->ui.state,
(UI2_STATS_DIALOG_ARGS) {
.mode = UI2_STATS_DIALOG_MODE_LEVEL,
.style = UI2_STATS_DIALOG_STYLE_BORDERED,
.level_num = Game_GetCurrentLevel()->num,
});
}
static void M_Shutdown(void)
static void M_Shutdown(M_PRIV *const p)
{
if (m_Dialog != nullptr) {
m_Dialog->free(m_Dialog);
m_Dialog = nullptr;
if (p->ui.is_ready) {
p->ui.is_ready = false;
UI2_StatsDialog_Free(&p->ui.state);
}
}
void Option_Compass_Control(INVENTORY_ITEM *const inv_item, const bool is_busy)
{
M_PRIV *const p = &m_Priv;
if (is_busy) {
return;
}
if (g_Config.gameplay.enable_compass_stats) {
char buf[100];
char time_buf[100];
if (m_Dialog == nullptr) {
M_Init();
}
if (m_Dialog != nullptr) {
m_Dialog->control(m_Dialog);
}
if (!p->ui.is_ready && g_Config.gameplay.enable_compass_stats) {
M_Init(p);
}
UI2_StatsDialog_Control(&p->ui.state);
if (g_InputDB.menu_confirm || g_InputDB.menu_back) {
M_Shutdown();
inv_item->goal_frame = inv_item->frames_total - 1;
M_Shutdown(p);
inv_item->anim_direction = 1;
inv_item->goal_frame = inv_item->frames_total - 1;
}
}
void Option_Compass_Draw(void)
{
if (m_Dialog != nullptr) {
m_Dialog->draw(m_Dialog);
M_PRIV *const p = &m_Priv;
if (p->ui.is_ready) {
UI2_StatsDialog(&p->ui.state);
}
}
void Option_Compass_Shutdown(void)
{
M_Shutdown();
M_PRIV *const p = &m_Priv;
M_Shutdown(p);
}
void Option_Compass_UpdateNeedle(const INVENTORY_ITEM *const inv_item)

View file

@ -1,387 +0,0 @@
#include "game/ui/widgets/stats_dialog.h"
#include "game/game_flow.h"
#include "game/game_string.h"
#include "game/input.h"
#include "game/savegame.h"
#include "game/stats.h"
#include "global/vars.h"
#include <libtrx/config.h>
#include <libtrx/game/ui/common.h>
#include <libtrx/game/ui/widgets/label.h>
#include <libtrx/game/ui/widgets/stack.h>
#include <libtrx/game/ui/widgets/window.h>
#include <libtrx/memory.h>
#include <stdio.h>
#include <string.h>
#define ROW_HEIGHT_BARE 25
#define ROW_HEIGHT_BORDERED 18
#define ROW_WIDTH_BORDERED 200
#define ROW_WIDTH_BORDERED_FULL 315
typedef enum {
M_ROW_KILLS,
M_ROW_PICKUPS,
M_ROW_SECRETS,
M_ROW_DEATHS,
M_ROW_TIMER,
M_ROW_AMMO,
M_ROW_MEDIPACKS_USED,
M_ROW_DISTANCE_TRAVELLED,
} M_ROW_ROLE;
typedef struct {
M_ROW_ROLE role;
UI_WIDGET *stack;
UI_WIDGET *key_label;
UI_WIDGET *value_label;
} M_ROW;
typedef struct {
UI_WIDGET_VTABLE vtable;
UI_STATS_DIALOG_ARGS args;
GF_LEVEL_TYPE level_type;
int32_t listener;
int32_t row_count;
UI_WIDGET *title;
UI_WIDGET *stack;
UI_WIDGET *window;
UI_WIDGET *root; // just a pointer to either stack or window
M_ROW *rows;
} UI_STATS_DIALOG;
static void M_FormatTime(char *out, int32_t total_frames);
static const char *M_GetDialogTitle(UI_STATS_DIALOG *self);
static void M_AddRow(
UI_STATS_DIALOG *self, M_ROW_ROLE role, const char *key, const char *value);
static void M_AddRowFromRole(
UI_STATS_DIALOG *self, M_ROW_ROLE role, const STATS_COMMON *stats);
static void M_AddCommonRows(UI_STATS_DIALOG *self, const STATS_COMMON *stats);
static void M_AddLevelStatsRows(UI_STATS_DIALOG *self);
static void M_AddFinalStatsRows(UI_STATS_DIALOG *self);
static void M_UpdateTimerRow(UI_STATS_DIALOG *self);
static void M_DoLayout(UI_STATS_DIALOG *self);
static void M_HandleLayoutUpdate(const EVENT *event, void *data);
static int32_t M_GetWidth(const UI_STATS_DIALOG *self);
static int32_t M_GetHeight(const UI_STATS_DIALOG *self);
static void M_SetPosition(UI_STATS_DIALOG *self, int32_t x, int32_t y);
static void M_Control(UI_STATS_DIALOG *self);
static void M_Draw(UI_STATS_DIALOG *self);
static void M_Free(UI_STATS_DIALOG *self);
static void M_FormatTime(char *const out, const int32_t total_frames)
{
const int32_t total_seconds = total_frames / LOGIC_FPS;
const int32_t hours = total_seconds / 3600;
const int32_t minutes = (total_seconds / 60) % 60;
const int32_t seconds = total_seconds % 60;
if (hours != 0) {
sprintf(out, "%d:%02d:%02d", hours, minutes, seconds);
} else {
sprintf(out, "%d:%02d", minutes, seconds);
}
}
static const char *M_GetDialogTitle(UI_STATS_DIALOG *const self)
{
switch (self->args.mode) {
case UI_STATS_DIALOG_MODE_LEVEL:
return GF_GetLevel(GFLT_MAIN, self->args.level_num)->title;
case UI_STATS_DIALOG_MODE_FINAL:
return self->level_type == GFL_BONUS ? GS(STATS_BONUS_STATISTICS)
: GS(STATS_FINAL_STATISTICS);
}
return nullptr;
}
static void M_AddRow(
UI_STATS_DIALOG *const self, const M_ROW_ROLE role, const char *const key,
const char *const value)
{
self->row_count++;
self->rows = Memory_Realloc(self->rows, sizeof(M_ROW) * self->row_count);
M_ROW *const row = &self->rows[self->row_count - 1];
row->role = role;
// create a stack
int32_t row_height;
if (self->args.style == UI_STATS_DIALOG_STYLE_BARE) {
row_height = ROW_HEIGHT_BARE;
row->stack = UI_Stack_Create(
UI_STACK_LAYOUT_HORIZONTAL, UI_STACK_AUTO_SIZE, row_height);
} else {
row_height = ROW_HEIGHT_BORDERED;
const int32_t row_width = g_Config.gameplay.stat_detail_mode == SDM_FULL
? ROW_WIDTH_BORDERED_FULL
: ROW_WIDTH_BORDERED;
row->stack =
UI_Stack_Create(UI_STACK_LAYOUT_HORIZONTAL, row_width, row_height);
UI_Stack_SetHAlign(row->stack, UI_STACK_H_ALIGN_DISTRIBUTE);
}
// create a key label; append space for the bare style
char key2[strlen(key) + 2];
sprintf(
key2, self->args.style == UI_STATS_DIALOG_STYLE_BARE ? "%s " : "%s",
key);
row->key_label = UI_Label_Create(key2, UI_LABEL_AUTO_SIZE, row_height);
// create a value label
row->value_label = UI_Label_Create(value, UI_LABEL_AUTO_SIZE, row_height);
UI_Stack_AddChild(row->stack, row->key_label);
UI_Stack_AddChild(row->stack, row->value_label);
UI_Stack_AddChild(self->stack, row->stack);
}
static void M_AddRowFromRole(
UI_STATS_DIALOG *const self, const M_ROW_ROLE role,
const STATS_COMMON *const stats)
{
char buf[50];
const char *const num_fmt =
g_Config.gameplay.stat_detail_mode == SDM_MINIMAL
? GS(STATS_BASIC_FMT)
: GS(STATS_DETAIL_FMT);
switch (role) {
case M_ROW_KILLS:
sprintf(buf, num_fmt, stats->kill_count, stats->max_kill_count);
M_AddRow(self, role, GS(STATS_KILLS), buf);
break;
case M_ROW_PICKUPS:
sprintf(buf, num_fmt, stats->pickup_count, stats->max_pickup_count);
M_AddRow(self, role, GS(STATS_PICKUPS), buf);
break;
case M_ROW_SECRETS:
sprintf(
buf, GS(STATS_DETAIL_FMT), stats->secret_count,
stats->max_secret_count);
M_AddRow(self, role, GS(STATS_SECRETS), buf);
break;
case M_ROW_DEATHS:
sprintf(buf, GS(STATS_BASIC_FMT), stats->death_count);
M_AddRow(self, role, GS(STATS_DEATHS), buf);
break;
case M_ROW_TIMER:
M_FormatTime(buf, stats->timer);
M_AddRow(self, role, GS(STATS_TIME_TAKEN), buf);
break;
case M_ROW_AMMO:
sprintf(buf, GS(PAGINATION_NAV), stats->ammo_hits, stats->ammo_used);
M_AddRow(self, role, GS(STATS_AMMO), buf);
break;
case M_ROW_MEDIPACKS_USED:
sprintf(buf, GS(DETAIL_FLOAT_FMT), stats->medipacks_used);
M_AddRow(self, role, GS(STATS_MEDIPACKS_USED), buf);
break;
case M_ROW_DISTANCE_TRAVELLED:
const int32_t distance_travelled = stats->distance_travelled / 445;
if (distance_travelled < 1000) {
sprintf(buf, "%dm", distance_travelled);
} else {
sprintf(
buf, "%d.%02dkm", distance_travelled / 1000,
(distance_travelled % 1000) / 10);
}
M_AddRow(self, role, GS(STATS_DISTANCE_TRAVELLED), buf);
break;
default:
break;
}
}
static void M_AddCommonRows(
UI_STATS_DIALOG *const self, const STATS_COMMON *const stats)
{
if (g_Config.gameplay.stat_detail_mode == SDM_MINIMAL) {
M_AddRowFromRole(self, M_ROW_KILLS, stats);
M_AddRowFromRole(self, M_ROW_PICKUPS, stats);
M_AddRowFromRole(self, M_ROW_SECRETS, stats);
M_AddRowFromRole(self, M_ROW_TIMER, stats);
} else {
M_AddRowFromRole(self, M_ROW_TIMER, stats);
M_AddRowFromRole(self, M_ROW_SECRETS, stats);
M_AddRowFromRole(self, M_ROW_PICKUPS, stats);
M_AddRowFromRole(self, M_ROW_KILLS, stats);
if (g_Config.gameplay.stat_detail_mode == SDM_FULL) {
M_AddRowFromRole(self, M_ROW_AMMO, stats);
M_AddRowFromRole(self, M_ROW_MEDIPACKS_USED, stats);
M_AddRowFromRole(self, M_ROW_DISTANCE_TRAVELLED, stats);
}
}
if (g_Config.gameplay.enable_deaths_counter && stats->death_count >= 0) {
// Always use sum of all levels for the deaths.
// Deaths get stored in the resume info for the level they happen
// on, so if the player dies in Vilcabamba and reloads Caves, they
// should still see an incremented death counter.
M_AddRowFromRole(self, M_ROW_DEATHS, stats);
}
}
static void M_AddLevelStatsRows(UI_STATS_DIALOG *const self)
{
const GF_LEVEL *const current_level =
GF_GetLevel(GFLT_MAIN, self->args.level_num);
const RESUME_INFO *const current_info =
Savegame_GetCurrentInfo(current_level);
const STATS_COMMON *const stats = (STATS_COMMON *)&current_info->stats;
M_AddCommonRows(self, stats);
}
static void M_AddFinalStatsRows(UI_STATS_DIALOG *const self)
{
FINAL_STATS final_stats;
Stats_ComputeFinal(self->level_type, &final_stats);
M_AddCommonRows(self, (STATS_COMMON *)&final_stats);
}
static void M_UpdateTimerRow(UI_STATS_DIALOG *const self)
{
if (self->args.mode != UI_STATS_DIALOG_MODE_LEVEL) {
return;
}
for (int32_t i = 0; i < self->row_count; i++) {
if (self->rows[i].role != M_ROW_TIMER) {
continue;
}
char buf[50];
const GF_LEVEL *const level =
GF_GetLevel(GFLT_MAIN, self->args.level_num);
const RESUME_INFO *const current_info = Savegame_GetCurrentInfo(level);
M_FormatTime(buf, current_info->stats.timer);
UI_Label_ChangeText(self->rows[i].value_label, buf);
return;
}
}
static void M_DoLayout(UI_STATS_DIALOG *const self)
{
M_SetPosition(
self, (UI_GetCanvasWidth() - M_GetWidth(self)) / 2,
(UI_GetCanvasHeight() - M_GetHeight(self)) / 2);
}
static void M_HandleLayoutUpdate(const EVENT *event, void *data)
{
UI_STATS_DIALOG *const self = (UI_STATS_DIALOG *)data;
M_DoLayout(self);
}
static int32_t M_GetWidth(const UI_STATS_DIALOG *const self)
{
return self->root->get_width(self->root);
}
static int32_t M_GetHeight(const UI_STATS_DIALOG *const self)
{
return self->root->get_height(self->root);
}
static void M_SetPosition(
UI_STATS_DIALOG *const self, const int32_t x, const int32_t y)
{
self->root->set_position(self->root, x, y);
}
static void M_Control(UI_STATS_DIALOG *const self)
{
if (self->root->control != nullptr) {
self->root->control(self->root);
}
M_UpdateTimerRow(self);
}
static void M_Draw(UI_STATS_DIALOG *const self)
{
if (self->root->draw != nullptr) {
self->root->draw(self->root);
}
}
static void M_Free(UI_STATS_DIALOG *const self)
{
if (self->title != nullptr) {
self->title->free(self->title);
}
for (int32_t i = 0; i < self->row_count; i++) {
self->rows[i].key_label->free(self->rows[i].key_label);
self->rows[i].value_label->free(self->rows[i].value_label);
self->rows[i].stack->free(self->rows[i].stack);
}
if (self->window != nullptr) {
self->window->free(self->window);
}
self->stack->free(self->stack);
UI_Events_Unsubscribe(self->listener);
Memory_Free(self);
}
UI_WIDGET *UI_StatsDialog_Create(const UI_STATS_DIALOG_ARGS args)
{
UI_STATS_DIALOG *const self = Memory_Alloc(sizeof(UI_STATS_DIALOG));
self->vtable = (UI_WIDGET_VTABLE) {
.get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth,
.get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight,
.set_position = (UI_WIDGET_SET_POSITION)M_SetPosition,
.control = (UI_WIDGET_CONTROL)M_Control,
.draw = (UI_WIDGET_DRAW)M_Draw,
.free = (UI_WIDGET_FREE)M_Free,
};
self->args = args;
self->level_type = GF_GetLevel(GFLT_MAIN, self->args.level_num)->type;
self->row_count = 0;
self->rows = nullptr;
self->stack = UI_Stack_Create(
UI_STACK_LAYOUT_VERTICAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE);
UI_Stack_SetHAlign(self->stack, UI_STACK_H_ALIGN_CENTER);
self->listener = UI_Events_Subscribe(
"layout_update", nullptr, M_HandleLayoutUpdate, self);
const char *title = M_GetDialogTitle(self);
switch (self->args.style) {
case UI_STATS_DIALOG_STYLE_BARE:
if (title != nullptr) {
self->title =
UI_Label_Create(title, UI_LABEL_AUTO_SIZE, ROW_HEIGHT_BARE);
UI_Stack_AddChild(self->stack, self->title);
}
self->root = self->stack;
break;
case UI_STATS_DIALOG_STYLE_BORDERED:
self->window = UI_Window_Create(self->stack, 8, 8, 8, 8);
UI_Window_SetTitle(self->window, title);
self->root = self->window;
break;
}
if (self->args.mode == UI_STATS_DIALOG_MODE_LEVEL) {
M_AddLevelStatsRows(self);
} else if (self->args.mode == UI_STATS_DIALOG_MODE_FINAL) {
M_AddFinalStatsRows(self);
}
M_DoLayout(self);
return (UI_WIDGET *)self;
}

View file

@ -1,3 +0,0 @@
#pragma once
#include <libtrx/game/ui/widgets/stats_dialog.h>

View file

@ -0,0 +1,269 @@
#include "game/ui2/dialogs/stats.h"
#include "game/game_flow.h"
#include "game/game_string.h"
#include "game/savegame.h"
#include "game/stats.h"
#include <libtrx/config.h>
#include <libtrx/game/ui2/common.h>
#include <libtrx/game/ui2/elements/anchor.h>
#include <libtrx/game/ui2/elements/frame.h>
#include <libtrx/game/ui2/elements/label.h>
#include <libtrx/game/ui2/elements/modal.h>
#include <libtrx/game/ui2/elements/stack.h>
#include <libtrx/game/ui2/elements/window.h>
#include <stdio.h>
#include <string.h>
typedef enum {
M_ROW_KILLS,
M_ROW_PICKUPS,
M_ROW_SECRETS,
M_ROW_DEATHS,
M_ROW_TIMER,
M_ROW_AMMO,
M_ROW_MEDIPACKS_USED,
M_ROW_DISTANCE_TRAVELLED,
} M_ROW_ROLE;
static void M_FormatTime(char *out, int32_t total_frames);
static void M_FormatDistance(char *const out, int32_t distance);
static void M_Row(
const UI2_STATS_DIALOG_STATE *s, const char *key, const char *value);
static void M_RowFromRole(
const UI2_STATS_DIALOG_STATE *s, M_ROW_ROLE role,
const STATS_COMMON *stats);
static void M_CommonRows(
const UI2_STATS_DIALOG_STATE *s, const STATS_COMMON *stats);
static void M_LevelStatsRows(const UI2_STATS_DIALOG_STATE *s);
static void M_FinalStatsRows(const UI2_STATS_DIALOG_STATE *s);
static const char *M_GetDialogTitle(const UI2_STATS_DIALOG_STATE *s);
static void M_BeginDialog(const UI2_STATS_DIALOG_STATE *s);
static void M_EndDialog(const UI2_STATS_DIALOG_STATE *s);
static void M_FormatTime(char *const out, const int32_t total_frames)
{
const int32_t total_seconds = total_frames / LOGIC_FPS;
const int32_t hours = total_seconds / 3600;
const int32_t minutes = (total_seconds / 60) % 60;
const int32_t seconds = total_seconds % 60;
if (hours != 0) {
sprintf(out, "%d:%02d:%02d", hours, minutes, seconds);
} else {
sprintf(out, "%d:%02d", minutes, seconds);
}
}
static void M_FormatDistance(char *const out, int32_t distance)
{
distance /= 445;
if (distance < 1000) {
sprintf(out, "%dm", distance);
} else {
sprintf(out, "%d.%02dkm", distance / 1000, (distance % 1000) / 10);
}
}
static void M_Row(
const UI2_STATS_DIALOG_STATE *const s, const char *const key,
const char *const value)
{
if (s->args.style == UI2_STATS_DIALOG_STYLE_BARE) {
UI2_BeginStack(UI2_STACK_HORIZONTAL);
UI2_Label(key);
UI2_Label(" ");
UI2_Label(value);
UI2_EndStack();
} else {
UI2_BeginStackEx((UI2_STACK_SETTINGS) {
.orientation = UI2_STACK_HORIZONTAL,
.spacing = { .h = 30.0f },
.align = { .h = UI2_STACK_H_ALIGN_DISTRIBUTE },
});
UI2_Label(key);
UI2_Label(value);
UI2_EndStack();
}
}
static void M_RowFromRole(
const UI2_STATS_DIALOG_STATE *const s, const M_ROW_ROLE role,
const STATS_COMMON *const stats)
{
char buf[50];
const char *const num_fmt =
g_Config.gameplay.stat_detail_mode == SDM_MINIMAL
? GS(STATS_BASIC_FMT)
: GS(STATS_DETAIL_FMT);
switch (role) {
case M_ROW_KILLS:
sprintf(buf, num_fmt, stats->kill_count, stats->max_kill_count);
M_Row(s, GS(STATS_KILLS), buf);
break;
case M_ROW_PICKUPS:
sprintf(buf, num_fmt, stats->pickup_count, stats->max_pickup_count);
M_Row(s, GS(STATS_PICKUPS), buf);
break;
case M_ROW_SECRETS:
sprintf(
buf, GS(STATS_DETAIL_FMT), stats->secret_count,
stats->max_secret_count);
M_Row(s, GS(STATS_SECRETS), buf);
break;
case M_ROW_DEATHS:
sprintf(buf, GS(STATS_BASIC_FMT), stats->death_count);
M_Row(s, GS(STATS_DEATHS), buf);
break;
case M_ROW_TIMER:
M_FormatTime(buf, stats->timer);
M_Row(s, GS(STATS_TIME_TAKEN), buf);
break;
case M_ROW_AMMO:
sprintf(buf, GS(PAGINATION_NAV), stats->ammo_hits, stats->ammo_used);
M_Row(s, GS(STATS_AMMO), buf);
break;
case M_ROW_MEDIPACKS_USED:
sprintf(buf, GS(DETAIL_FLOAT_FMT), stats->medipacks_used);
M_Row(s, GS(STATS_MEDIPACKS_USED), buf);
break;
case M_ROW_DISTANCE_TRAVELLED:
M_FormatDistance(buf, stats->distance_travelled);
M_Row(s, GS(STATS_DISTANCE_TRAVELLED), buf);
break;
default:
break;
}
}
static void M_CommonRows(
const UI2_STATS_DIALOG_STATE *const s, const STATS_COMMON *const stats)
{
if (g_Config.gameplay.stat_detail_mode == SDM_MINIMAL) {
M_RowFromRole(s, M_ROW_KILLS, stats);
M_RowFromRole(s, M_ROW_PICKUPS, stats);
M_RowFromRole(s, M_ROW_SECRETS, stats);
M_RowFromRole(s, M_ROW_TIMER, stats);
} else {
M_RowFromRole(s, M_ROW_TIMER, stats);
M_RowFromRole(s, M_ROW_SECRETS, stats);
M_RowFromRole(s, M_ROW_PICKUPS, stats);
M_RowFromRole(s, M_ROW_KILLS, stats);
if (g_Config.gameplay.stat_detail_mode == SDM_FULL) {
M_RowFromRole(s, M_ROW_AMMO, stats);
M_RowFromRole(s, M_ROW_MEDIPACKS_USED, stats);
M_RowFromRole(s, M_ROW_DISTANCE_TRAVELLED, stats);
}
}
if (g_Config.gameplay.enable_deaths_counter && stats->death_count >= 0) {
// Always use sum of all levels for the deaths.
// Deaths get stored in the resume info for the level they happen
// on, so if the player dies in Vilcabamba and reloads Caves, they
// should still see an incremented death counter.
M_RowFromRole(s, M_ROW_DEATHS, stats);
}
}
static void M_LevelStatsRows(const UI2_STATS_DIALOG_STATE *const s)
{
const GF_LEVEL *const current_level =
GF_GetLevel(GFLT_MAIN, s->args.level_num);
const RESUME_INFO *const current_info =
Savegame_GetCurrentInfo(current_level);
const STATS_COMMON *const stats = (STATS_COMMON *)&current_info->stats;
M_CommonRows(s, stats);
}
static void M_FinalStatsRows(const UI2_STATS_DIALOG_STATE *const s)
{
FINAL_STATS final_stats;
const GF_LEVEL_TYPE level_type =
GF_GetLevel(GFLT_MAIN, s->args.level_num)->type;
Stats_ComputeFinal(level_type, &final_stats);
M_CommonRows(s, (STATS_COMMON *)&final_stats);
}
static const char *M_GetDialogTitle(const UI2_STATS_DIALOG_STATE *const s)
{
switch (s->args.mode) {
case UI2_STATS_DIALOG_MODE_LEVEL:
return GF_GetLevel(GFLT_MAIN, s->args.level_num)->title;
case UI2_STATS_DIALOG_MODE_FINAL: {
const GF_LEVEL_TYPE level_type =
GF_GetLevel(GFLT_MAIN, s->args.level_num)->type;
if (level_type == GFL_BONUS) {
return GS(STATS_BONUS_STATISTICS);
}
return GS(STATS_FINAL_STATISTICS);
}
}
return nullptr;
}
static void M_BeginDialog(const UI2_STATS_DIALOG_STATE *const s)
{
const char *const title = M_GetDialogTitle(s);
UI2_BeginModal(0.5f, 0.5f);
if (s->args.style == UI2_STATS_DIALOG_STYLE_BARE) {
UI2_BeginStackEx((UI2_STACK_SETTINGS) {
.orientation = UI2_STACK_VERTICAL,
.spacing = { .v = 11.0f },
.align = { .h = UI2_STACK_H_ALIGN_CENTER },
});
if (title != nullptr) {
UI2_Label(title);
}
} else {
UI2_BeginWindow();
if (title != nullptr) {
UI2_WindowTitle(title);
}
UI2_BeginWindowBody();
UI2_BeginStackEx((UI2_STACK_SETTINGS) {
.orientation = UI2_STACK_VERTICAL,
.spacing = { .v = 4.0f },
.align = { .h = UI2_STACK_H_ALIGN_SPAN },
});
}
}
static void M_EndDialog(const UI2_STATS_DIALOG_STATE *const s)
{
if (s->args.style == UI2_STATS_DIALOG_STYLE_BARE) {
UI2_EndStack();
} else {
UI2_EndStack();
UI2_EndWindowBody();
UI2_EndWindow();
}
UI2_EndModal();
}
void UI2_StatsDialog(UI2_STATS_DIALOG_STATE *const s)
{
M_BeginDialog(s);
switch (s->args.mode) {
case UI2_STATS_DIALOG_MODE_LEVEL:
M_LevelStatsRows(s);
break;
case UI2_STATS_DIALOG_MODE_FINAL:
M_FinalStatsRows(s);
break;
}
M_EndDialog(s);
}

View file

@ -0,0 +1,3 @@
#pragma once
#include <libtrx/game/ui2/dialogs/stats.h>

View file

@ -263,7 +263,7 @@ sources = [
'game/stats/common.c',
'game/text.c',
'game/ui/common.c',
'game/ui/widgets/stats_dialog.c',
'game/ui2/dialogs/stats.c',
'game/viewport.c',
'global/enum_map.c',
'global/vars.c',

View file

@ -73,7 +73,7 @@ void Option_Draw(INVENTORY_ITEM *const item)
Option_Passport_Draw(item);
break;
case O_COMPASS_OPTION:
Option_Compass_Draw(item);
Option_Compass_Draw();
break;
case O_DETAIL_OPTION:
Option_Detail_Draw(item);

View file

@ -27,5 +27,5 @@ void Option_Controls_ShowControls(void);
void Option_Controls_UpdateText(void);
void Option_Compass_Control(INVENTORY_ITEM *item, bool is_busy);
void Option_Compass_Draw(INVENTORY_ITEM *item);
void Option_Compass_Draw(void);
void Option_Compass_Shutdown(void);

View file

@ -4,66 +4,74 @@
#include "game/requester.h"
#include "game/savegame.h"
#include "game/sound.h"
#include "game/ui/widgets/stats_dialog.h"
#include "global/vars.h"
#include <libtrx/game/ui2.h>
#include <stdio.h>
static UI_WIDGET *m_Dialog = nullptr;
typedef struct {
bool ui_active;
UI2_STATS_DIALOG_STATE ui_state;
} M_PRIV;
static void M_Init(void);
static M_PRIV m_Priv = {};
static void M_Init(void)
static void M_Init(M_PRIV *p);
static void M_Shutdown(M_PRIV *p);
static void M_Init(M_PRIV *const p)
{
m_Dialog = UI_StatsDialog_Create((UI_STATS_DIALOG_ARGS) {
.mode = Game_IsInGym() ? UI_STATS_DIALOG_MODE_ASSAULT_COURSE
: UI_STATS_DIALOG_MODE_LEVEL,
.level_num = Game_GetCurrentLevel()->num,
.style = UI_STATS_DIALOG_STYLE_BORDERED,
});
p->ui_active = true;
UI2_StatsDialog_Init(
&p->ui_state,
(UI2_STATS_DIALOG_ARGS) {
.mode = Game_IsInGym() ? UI2_STATS_DIALOG_MODE_ASSAULT_COURSE
: UI2_STATS_DIALOG_MODE_LEVEL,
.level_num = Game_GetCurrentLevel()->num,
.style = UI2_STATS_DIALOG_STYLE_BORDERED,
});
}
static void M_Shutdown(void)
static void M_Shutdown(M_PRIV *const p)
{
if (m_Dialog != nullptr) {
m_Dialog->free(m_Dialog);
m_Dialog = nullptr;
if (p->ui_active) {
p->ui_active = false;
UI2_StatsDialog_Free(&p->ui_state);
}
}
void Option_Compass_Control(INVENTORY_ITEM *const item, const bool is_busy)
void Option_Compass_Control(INVENTORY_ITEM *const inv_item, const bool is_busy)
{
M_PRIV *const p = &m_Priv;
if (is_busy) {
return;
}
char buffer[32];
const RESUME_INFO *const current_info =
Savegame_GetCurrentInfo(Game_GetCurrentLevel());
const int32_t sec = current_info->stats.timer / FRAMES_PER_SECOND;
sprintf(buffer, "%02d:%02d:%02d", sec / 3600, sec / 60 % 60, sec % 60);
if (m_Dialog == nullptr) {
M_Init();
if (!p->ui_active) {
M_Init(p);
}
m_Dialog->control(m_Dialog);
UI2_StatsDialog_Control(&p->ui_state);
if (g_InputDB.menu_confirm || g_InputDB.menu_back) {
item->anim_direction = 1;
item->goal_frame = item->frames_total - 1;
M_Shutdown(p);
inv_item->anim_direction = 1;
inv_item->goal_frame = inv_item->frames_total - 1;
}
Sound_Effect(SFX_MENU_STOPWATCH, 0, SPM_ALWAYS);
}
void Option_Compass_Draw(INVENTORY_ITEM *const item)
void Option_Compass_Draw(void)
{
if (m_Dialog != nullptr) {
m_Dialog->draw(m_Dialog);
M_PRIV *const p = &m_Priv;
if (p->ui_active) {
UI2_StatsDialog(&p->ui_state);
}
}
void Option_Compass_Shutdown(void)
{
M_Shutdown();
M_PRIV *const p = &m_Priv;
M_Shutdown(p);
}

View file

@ -1,340 +0,0 @@
#include "game/ui/widgets/stats_dialog.h"
#include "game/game.h"
#include "game/game_flow.h"
#include "game/game_string.h"
#include "game/gym.h"
#include "game/input.h"
#include "game/savegame.h"
#include "game/stats.h"
#include "global/vars.h"
#include <libtrx/debug.h>
#include <libtrx/game/ui/common.h>
#include <libtrx/game/ui/widgets/label.h>
#include <libtrx/game/ui/widgets/requester.h>
#include <libtrx/game/ui/widgets/stack.h>
#include <libtrx/memory.h>
#include <stdio.h>
#include <string.h>
#define VISIBLE_ROWS 7
#define ROW_HEIGHT 18
typedef enum {
M_ROW_GENERIC,
M_ROW_TIMER,
M_ROW_LEVEL_SECRETS,
M_ROW_ALL_SECRETS,
M_ROW_KILLS,
M_ROW_AMMO_USED,
M_ROW_AMMO_HITS,
M_ROW_MEDIPACKS,
M_ROW_DISTANCE_TRAVELED,
} M_ROW_ROLE;
typedef struct {
UI_WIDGET_VTABLE vtable;
UI_STATS_DIALOG_ARGS args;
UI_WIDGET *requester;
int32_t listener;
GF_LEVEL_TYPE level_type;
} UI_STATS_DIALOG;
static void M_AddRow(
UI_STATS_DIALOG *self, M_ROW_ROLE role, const char *left_text,
const char *right_text);
static void M_AddRowFromRole(
UI_STATS_DIALOG *self, M_ROW_ROLE role, const STATS_COMMON *stats);
static void M_AddLevelStatsRows(UI_STATS_DIALOG *self);
static void M_AddFinalStatsRows(UI_STATS_DIALOG *self);
static void M_AddAssaultCourseStatsRows(UI_STATS_DIALOG *self);
static void M_UpdateTimerRow(UI_STATS_DIALOG *self);
static void M_DoLayout(UI_STATS_DIALOG *self);
static void M_HandleLayoutUpdate(const EVENT *event, void *data);
static int32_t M_GetWidth(const UI_STATS_DIALOG *self);
static int32_t M_GetHeight(const UI_STATS_DIALOG *self);
static void M_SetPosition(UI_STATS_DIALOG *self, int32_t x, int32_t y);
static void M_Control(UI_STATS_DIALOG *self);
static void M_Draw(UI_STATS_DIALOG *self);
static void M_Free(UI_STATS_DIALOG *self);
static void M_AddRow(
UI_STATS_DIALOG *const self, const M_ROW_ROLE role,
const char *const left_text, const char *const right_text)
{
UI_Requester_AddRowLR(
self->requester, left_text, right_text, (void *)(intptr_t)role);
}
static void M_AddRowFromRole(
UI_STATS_DIALOG *const self, const M_ROW_ROLE role,
const STATS_COMMON *const stats)
{
char buf[64];
switch (role) {
case M_ROW_TIMER: {
const int32_t sec = stats->timer / FRAMES_PER_SECOND;
sprintf(
buf, "%02d:%02d:%02d", (sec / 60) / 60, (sec / 60) % 60, sec % 60);
M_AddRow(self, role, GS(STATS_TIME_TAKEN), buf);
break;
}
case M_ROW_LEVEL_SECRETS: {
char *ptr = buf;
int32_t num_secrets = 0;
const LEVEL_STATS *const level_stats = (LEVEL_STATS *)stats;
for (int32_t i = 1; i >= 0; i--) {
for (int32_t j = 0; j < 3; j++) {
const int32_t flag = 1 << (j + i * 3);
if ((level_stats->secret_flags & flag) != 0) {
sprintf(ptr, "\\{secret %d}", j + 1);
num_secrets++;
} else {
strcpy(ptr, " ");
}
ptr += strlen(ptr);
}
}
*ptr++ = '\0';
if (num_secrets == 0) {
strcpy(buf, GS(MISC_NONE));
}
M_AddRow(self, role, GS(STATS_SECRETS), buf);
break;
}
case M_ROW_ALL_SECRETS:
sprintf(
buf, GS(STATS_DETAIL_FMT), ((FINAL_STATS *)stats)->found_secrets,
((FINAL_STATS *)stats)->total_secrets);
M_AddRow(self, role, GS(STATS_SECRETS), buf);
break;
case M_ROW_KILLS:
sprintf(buf, GS(STATS_BASIC_FMT), stats->kill_count);
M_AddRow(self, role, GS(STATS_KILLS), buf);
break;
case M_ROW_AMMO_USED:
sprintf(buf, "%d", stats->ammo_used);
M_AddRow(self, role, GS(STATS_AMMO_USED), buf);
break;
case M_ROW_AMMO_HITS:
sprintf(buf, "%d", stats->ammo_hits);
M_AddRow(self, role, GS(STATS_AMMO_HITS), buf);
break;
case M_ROW_MEDIPACKS:
sprintf(buf, GS(DETAIL_FLOAT_FMT), stats->medipacks_used);
M_AddRow(self, role, GS(STATS_MEDIPACKS_USED), buf);
break;
case M_ROW_DISTANCE_TRAVELED:
const int32_t distance = stats->distance_travelled / 445;
if (distance < 1000) {
sprintf(buf, "%dm", distance);
} else {
sprintf(buf, "%d.%02dkm", distance / 1000, (distance % 1000) / 10);
}
M_AddRow(self, role, GS(STATS_DISTANCE_TRAVELLED), buf);
break;
default:
break;
}
}
static void M_AddLevelStatsRows(UI_STATS_DIALOG *const self)
{
const GF_LEVEL *const current_level =
GF_GetLevel(GFLT_MAIN, self->args.level_num);
const RESUME_INFO *const current_info =
Savegame_GetCurrentInfo(current_level);
const STATS_COMMON *const stats = (STATS_COMMON *)&current_info->stats;
M_AddRowFromRole(self, M_ROW_TIMER, stats);
if (stats->max_secret_count != 0) {
M_AddRowFromRole(self, M_ROW_LEVEL_SECRETS, stats);
}
M_AddRowFromRole(self, M_ROW_KILLS, stats);
M_AddRowFromRole(self, M_ROW_AMMO_USED, stats);
M_AddRowFromRole(self, M_ROW_AMMO_HITS, stats);
M_AddRowFromRole(self, M_ROW_MEDIPACKS, stats);
M_AddRowFromRole(self, M_ROW_DISTANCE_TRAVELED, stats);
}
static void M_AddFinalStatsRows(UI_STATS_DIALOG *const self)
{
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);
M_AddRowFromRole(self, M_ROW_KILLS, stats);
M_AddRowFromRole(self, M_ROW_AMMO_USED, stats);
M_AddRowFromRole(self, M_ROW_AMMO_HITS, stats);
M_AddRowFromRole(self, M_ROW_MEDIPACKS, stats);
M_AddRowFromRole(self, M_ROW_DISTANCE_TRAVELED, stats);
}
static void M_AddAssaultCourseStatsRows(UI_STATS_DIALOG *const self)
{
const ASSAULT_STATS stats = Gym_GetAssaultStats();
if (stats.best_time[0] == 0) {
M_AddRow(self, M_ROW_GENERIC, GS(STATS_ASSAULT_NO_TIMES_SET), nullptr);
return;
}
for (int32_t i = 0; i < 10; i++) {
char left_buf[32] = "";
char right_buf[32] = "";
if (stats.best_time[i] != 0) {
sprintf(
left_buf, "%2d: %s %d", i + 1, GS(STATS_ASSAULT_FINISH),
stats.best_finish[i]);
const int32_t sec = stats.best_time[i] / FRAMES_PER_SECOND;
sprintf(
right_buf, "%02d:%02d.%-2d", sec / 60, sec % 60,
stats.best_time[i] % FRAMES_PER_SECOND
/ (FRAMES_PER_SECOND / 10));
}
M_AddRow(self, M_ROW_GENERIC, left_buf, right_buf);
}
}
static void M_UpdateTimerRow(UI_STATS_DIALOG *const self)
{
if (self->args.mode != UI_STATS_DIALOG_MODE_LEVEL) {
return;
}
for (int32_t i = 0; i < UI_Requester_GetRowCount(self->requester); i++) {
if ((intptr_t)UI_Requester_GetRowUserData(self->requester, i)
!= M_ROW_TIMER) {
continue;
}
char buf[32];
const RESUME_INFO *const current_info =
Savegame_GetCurrentInfo(Game_GetCurrentLevel());
const int32_t sec = current_info->stats.timer / FRAMES_PER_SECOND;
sprintf(
buf, "%02d:%02d:%02d", (sec / 60) / 60, (sec / 60) % 60, sec % 60);
UI_Requester_ChangeRowLR(
self->requester, i, nullptr, buf, (void *)(intptr_t)M_ROW_TIMER);
return;
}
}
static void M_DoLayout(UI_STATS_DIALOG *const self)
{
M_SetPosition(
self, (UI_GetCanvasWidth() - M_GetWidth(self)) / 2,
(UI_GetCanvasHeight() - M_GetHeight(self)) - 50);
}
static void M_HandleLayoutUpdate(const EVENT *event, void *data)
{
UI_STATS_DIALOG *const self = (UI_STATS_DIALOG *)data;
M_DoLayout(self);
}
static int32_t M_GetWidth(const UI_STATS_DIALOG *const self)
{
return self->requester->get_width(self->requester);
}
static int32_t M_GetHeight(const UI_STATS_DIALOG *const self)
{
return self->requester->get_height(self->requester);
}
static void M_SetPosition(
UI_STATS_DIALOG *const self, const int32_t x, const int32_t y)
{
self->requester->set_position(self->requester, x, y);
}
static void M_Control(UI_STATS_DIALOG *const self)
{
if (self->requester->control != nullptr) {
self->requester->control(self->requester);
}
M_UpdateTimerRow(self);
}
static void M_Draw(UI_STATS_DIALOG *const self)
{
if (self->requester->draw != nullptr) {
self->requester->draw(self->requester);
}
}
static void M_Free(UI_STATS_DIALOG *const self)
{
self->requester->free(self->requester);
UI_Events_Unsubscribe(self->listener);
Memory_Free(self);
}
UI_WIDGET *UI_StatsDialog_Create(UI_STATS_DIALOG_ARGS args)
{
UI_STATS_DIALOG *const self = Memory_Alloc(sizeof(UI_STATS_DIALOG));
self->vtable = (UI_WIDGET_VTABLE) {
.get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth,
.get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight,
.set_position = (UI_WIDGET_SET_POSITION)M_SetPosition,
.control = (UI_WIDGET_CONTROL)M_Control,
.draw = (UI_WIDGET_DRAW)M_Draw,
.free = (UI_WIDGET_FREE)M_Free,
};
// TODO: add support for the bare style by merging TR1 and TR2 stats dialog
// implementations.
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,
.width = 290,
.row_height = ROW_HEIGHT,
});
self->listener = UI_Events_Subscribe(
"layout_update", nullptr, M_HandleLayoutUpdate, self);
switch (args.mode) {
case UI_STATS_DIALOG_MODE_LEVEL:
UI_Requester_SetTitle(
self->requester, GF_GetLevel(GFLT_MAIN, args.level_num)->title);
M_AddLevelStatsRows(self);
break;
case UI_STATS_DIALOG_MODE_FINAL:
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;
case UI_STATS_DIALOG_MODE_ASSAULT_COURSE:
UI_Requester_SetTitle(self->requester, GS(STATS_ASSAULT_TITLE));
M_AddAssaultCourseStatsRows(self);
break;
}
M_DoLayout(self);
return (UI_WIDGET *)self;
}

View file

@ -1,3 +0,0 @@
#pragma once
#include <libtrx/game/ui/widgets/stats_dialog.h>

View file

@ -0,0 +1,324 @@
#include "game/ui2/dialogs/stats.h"
#include "game/game_flow.h"
#include "game/game_string.h"
#include "game/gym.h"
#include "game/savegame.h"
#include "game/stats.h"
#include <libtrx/debug.h>
#include <libtrx/game/ui2/common.h>
#include <libtrx/game/ui2/elements/anchor.h>
#include <libtrx/game/ui2/elements/label.h>
#include <libtrx/game/ui2/elements/modal.h>
#include <libtrx/game/ui2/elements/pad.h>
#include <libtrx/game/ui2/elements/requester.h>
#include <libtrx/game/ui2/elements/spacer.h>
#include <libtrx/game/ui2/elements/stack.h>
#include <libtrx/game/ui2/elements/window.h>
#include <stdio.h>
#include <string.h>
typedef enum {
M_ROW_GENERIC,
M_ROW_TIMER,
M_ROW_LEVEL_SECRETS,
M_ROW_ALL_SECRETS,
M_ROW_KILLS,
M_ROW_AMMO_USED,
M_ROW_AMMO_HITS,
M_ROW_MEDIPACKS,
M_ROW_DISTANCE_TRAVELLED,
} M_ROW_ROLE;
static void M_FormatTime(char *out, int32_t total_frames);
static void M_FormatSecrets(char *out, const LEVEL_STATS *level_stats);
static void M_FormatDistance(char *const out, int32_t distance);
static void M_Row(
const UI2_STATS_DIALOG_STATE *s, const char *key, const char *value);
static void M_RowFromRole(
const UI2_STATS_DIALOG_STATE *s, M_ROW_ROLE role,
const STATS_COMMON *stats);
static void M_LevelStatsRows(const UI2_STATS_DIALOG_STATE *s);
static void M_FinalStatsRows(const UI2_STATS_DIALOG_STATE *s);
static void M_AssaultCourseStatsRows(UI2_STATS_DIALOG_STATE *s);
static const char *M_GetDialogTitle(const UI2_STATS_DIALOG_STATE *s);
static void M_BeginDialog(const UI2_STATS_DIALOG_STATE *s);
static void M_EndDialog(const UI2_STATS_DIALOG_STATE *s);
static void M_FormatTime(char *const out, const int32_t total_frames)
{
const int32_t total_seconds = total_frames / LOGIC_FPS;
const int32_t hours = total_seconds / 3600;
const int32_t minutes = (total_seconds / 60) % 60;
const int32_t seconds = total_seconds % 60;
sprintf(out, "%02d:%02d:%02d", hours, minutes, seconds);
}
static void M_FormatSecrets(
char *const out, const LEVEL_STATS *const level_stats)
{
char *ptr = out;
int32_t num_secrets = 0;
for (int32_t i = 1; i >= 0; i--) {
for (int32_t j = 0; j < 3; j++) {
const int32_t flag = 1 << (j + i * 3);
if ((level_stats->secret_flags & flag) != 0) {
sprintf(ptr, "\\{secret %d}", j + 1);
num_secrets++;
} else {
strcpy(ptr, " ");
}
ptr += strlen(ptr);
}
}
*ptr++ = '\0';
if (num_secrets == 0) {
strcpy(out, GS(MISC_NONE));
}
}
static void M_FormatDistance(char *const out, int32_t distance)
{
distance /= 445;
if (distance < 1000) {
sprintf(out, "%dm", distance);
} else {
sprintf(out, "%d.%02dkm", distance / 1000, (distance % 1000) / 10);
}
}
static void M_Row(
const UI2_STATS_DIALOG_STATE *const s, const char *const key,
const char *const value)
{
UI2_BeginStackEx((UI2_STACK_SETTINGS) {
.orientation = UI2_STACK_HORIZONTAL,
.spacing = { .h = TR_VERSION == 1 ? 30.0f : 90.0f },
.align = { .h = UI2_STACK_H_ALIGN_DISTRIBUTE },
});
UI2_Label(key);
UI2_Label(value);
UI2_EndStack();
}
static void M_RowFromRole(
const UI2_STATS_DIALOG_STATE *const s, const M_ROW_ROLE role,
const STATS_COMMON *const stats)
{
char buf[64];
switch (role) {
case M_ROW_TIMER: {
M_FormatTime(buf, stats->timer);
M_Row(s, GS(STATS_TIME_TAKEN), buf);
break;
}
case M_ROW_LEVEL_SECRETS: {
M_FormatSecrets(buf, (LEVEL_STATS *)stats);
M_Row(s, GS(STATS_SECRETS), buf);
break;
}
case M_ROW_ALL_SECRETS:
sprintf(
buf, GS(STATS_DETAIL_FMT), ((FINAL_STATS *)stats)->found_secrets,
((FINAL_STATS *)stats)->total_secrets);
M_Row(s, GS(STATS_SECRETS), buf);
break;
case M_ROW_KILLS:
sprintf(buf, GS(STATS_BASIC_FMT), stats->kill_count);
M_Row(s, GS(STATS_KILLS), buf);
break;
case M_ROW_AMMO_USED:
sprintf(buf, "%d", stats->ammo_used);
M_Row(s, GS(STATS_AMMO_USED), buf);
break;
case M_ROW_AMMO_HITS:
sprintf(buf, "%d", stats->ammo_hits);
M_Row(s, GS(STATS_AMMO_HITS), buf);
break;
case M_ROW_MEDIPACKS:
sprintf(buf, GS(DETAIL_FLOAT_FMT), stats->medipacks_used);
M_Row(s, GS(STATS_MEDIPACKS_USED), buf);
break;
case M_ROW_DISTANCE_TRAVELLED:
M_FormatDistance(buf, stats->distance_travelled);
M_Row(s, GS(STATS_DISTANCE_TRAVELLED), buf);
break;
default:
break;
}
}
static void M_LevelStatsRows(const UI2_STATS_DIALOG_STATE *const s)
{
const GF_LEVEL *const current_level =
GF_GetLevel(GFLT_MAIN, s->args.level_num);
const RESUME_INFO *const current_info =
Savegame_GetCurrentInfo(current_level);
const STATS_COMMON *const stats = (STATS_COMMON *)&current_info->stats;
M_RowFromRole(s, M_ROW_TIMER, stats);
if (stats->max_secret_count != 0) {
M_RowFromRole(s, M_ROW_LEVEL_SECRETS, stats);
}
M_RowFromRole(s, M_ROW_KILLS, stats);
M_RowFromRole(s, M_ROW_AMMO_USED, stats);
M_RowFromRole(s, M_ROW_AMMO_HITS, stats);
M_RowFromRole(s, M_ROW_MEDIPACKS, stats);
M_RowFromRole(s, M_ROW_DISTANCE_TRAVELLED, stats);
}
static void M_FinalStatsRows(const UI2_STATS_DIALOG_STATE *const s)
{
const GF_LEVEL_TYPE level_type =
GF_GetLevel(GFLT_MAIN, s->args.level_num)->type;
const FINAL_STATS final_stats = Stats_ComputeFinalStats(level_type);
const STATS_COMMON *const stats = (STATS_COMMON *)&final_stats;
M_RowFromRole(s, M_ROW_TIMER, stats);
M_RowFromRole(s, M_ROW_ALL_SECRETS, stats);
M_RowFromRole(s, M_ROW_KILLS, stats);
M_RowFromRole(s, M_ROW_AMMO_USED, stats);
M_RowFromRole(s, M_ROW_AMMO_HITS, stats);
M_RowFromRole(s, M_ROW_MEDIPACKS, stats);
M_RowFromRole(s, M_ROW_DISTANCE_TRAVELLED, stats);
}
static void M_AssaultCourseStatsRows(UI2_STATS_DIALOG_STATE *const s)
{
const ASSAULT_STATS stats = Gym_GetAssaultStats();
int32_t present = 0;
for (int i = 0; i < MAX_ASSAULT_TIMES; i++) {
if (stats.best_time[i] != 0) {
present++;
}
}
UI2_Requester_SetMaxRows(&s->assault_req, present);
int32_t visible = 0;
if (stats.best_time[0] == 0) {
UI2_BeginAnchor(0.5f, 0.5f);
UI2_Label(GS(STATS_ASSAULT_NO_TIMES_SET));
UI2_EndAnchor();
visible = 1;
} else {
int32_t first = UI2_Requester_GetFirstRow(&s->assault_req);
int32_t last = UI2_Requester_GetLastRow(&s->assault_req);
for (int32_t i = first; i < last; i++) {
char left_buf[32] = "";
char right_buf[32] = "";
ASSERT(stats.best_time[i] != 0);
sprintf(
left_buf, "%2d: %s %d", i + 1, GS(STATS_ASSAULT_FINISH),
stats.best_finish[i]);
const int32_t sec = stats.best_time[i] / FRAMES_PER_SECOND;
sprintf(
right_buf, "%02d:%02d.%-2d", sec / 60, sec % 60,
stats.best_time[i] % FRAMES_PER_SECOND
/ (FRAMES_PER_SECOND / 10));
M_Row(s, left_buf, right_buf);
visible++;
}
}
UI2_Spacer(
0.0f, TEXT_HEIGHT_FIXED * MAX(0, s->assault_req.vis_rows - visible));
}
static const char *M_GetDialogTitle(const UI2_STATS_DIALOG_STATE *const s)
{
switch (s->args.mode) {
case UI2_STATS_DIALOG_MODE_LEVEL:
return GF_GetLevel(GFLT_MAIN, s->args.level_num)->title;
case UI2_STATS_DIALOG_MODE_FINAL: {
const GF_LEVEL_TYPE level_type =
GF_GetLevel(GFLT_MAIN, s->args.level_num)->type;
const char *const title = level_type == GFL_BONUS
? GS(STATS_BONUS_STATISTICS)
: GS(STATS_FINAL_STATISTICS);
return title;
}
case UI2_STATS_DIALOG_MODE_ASSAULT_COURSE:
return GS(STATS_ASSAULT_TITLE);
}
return nullptr;
}
static void M_BeginDialog(const UI2_STATS_DIALOG_STATE *const s)
{
const char *const title = M_GetDialogTitle(s);
UI2_BeginModal(0.5f, 1.0f);
UI2_BeginPad(40.f, 40.0f);
if (s->args.style == UI2_STATS_DIALOG_STYLE_BARE) {
UI2_BeginStackEx((UI2_STACK_SETTINGS) {
.orientation = UI2_STACK_VERTICAL,
.spacing = { .v = 11.0f },
.align = { .h = UI2_STACK_H_ALIGN_CENTER },
});
if (title != nullptr) {
UI2_Label(title);
}
} else {
UI2_BeginWindow();
if (title != nullptr) {
UI2_WindowTitle(title);
}
UI2_BeginWindowBody();
UI2_BeginStackEx((UI2_STACK_SETTINGS) {
.orientation = UI2_STACK_VERTICAL,
.spacing = { .v = 3.0f },
.align = { .h = UI2_STACK_H_ALIGN_SPAN },
});
}
// ensure minimum dialog width
UI2_Spacer(290.0f, 0.0f);
}
static void M_EndDialog(const UI2_STATS_DIALOG_STATE *const s)
{
if (s->args.style == UI2_STATS_DIALOG_STYLE_BARE) {
UI2_EndStack();
} else {
UI2_EndStack();
UI2_EndWindowBody();
UI2_EndWindow();
}
UI2_EndPad();
UI2_EndModal();
}
void UI2_StatsDialog(UI2_STATS_DIALOG_STATE *const s)
{
// TODO: add support for the bare style by merging TR1 and TR2 stats dialog
// implementations.
ASSERT(s->args.style == UI2_STATS_DIALOG_STYLE_BORDERED);
M_BeginDialog(s);
switch (s->args.mode) {
case UI2_STATS_DIALOG_MODE_LEVEL:
M_LevelStatsRows(s);
break;
case UI2_STATS_DIALOG_MODE_FINAL:
M_FinalStatsRows(s);
break;
case UI2_STATS_DIALOG_MODE_ASSAULT_COURSE:
M_AssaultCourseStatsRows(s);
break;
}
M_EndDialog(s);
}

View file

@ -0,0 +1,3 @@
#pragma once
#include <libtrx/game/ui2/dialogs/stats.h>

View file

@ -278,7 +278,7 @@ sources = [
'game/ui/widgets/controls_layout_editor.c',
'game/ui/widgets/controls_layout_selector.c',
'game/ui/widgets/graphics_dialog.c',
'game/ui/widgets/stats_dialog.c',
'game/ui2/dialogs/stats.c',
'game/viewport.c',
'global/enum_map.c',
'global/vars.c',