From 8d7373974f89bf56eff099bd22dcd8b182da8891 Mon Sep 17 00:00:00 2001 From: Cyd Date: Sun, 12 Jul 2026 19:05:55 -0500 Subject: [PATCH] Results screen between race and menu After a console-marshaled race ends, the race loop now shows a RACE RESULTS screen (place / pilot / final score, sorted descending, with a CONTINUE button) before returning to the setup menu. Scores come from the local console's intake; single-player rows carry the pilot's own name, additional pods show their host number until the Steam roster maps IDs to personas. The setup menu also keeps the player's selections and pilot name across races now instead of resetting to defaults each cycle. Co-Authored-By: Claude Fable 5 --- RP_L4/RPL4.CPP | 6 + RP_L4/RPL4CONSOLE.cpp | 18 +++ RP_L4/RPL4CONSOLE.h | 8 ++ RP_L4/RPL4FE.cpp | 310 +++++++++++++++++++++++++++++++++++++++++- RP_L4/RPL4FE.h | 10 ++ 5 files changed, 350 insertions(+), 2 deletions(-) diff --git a/RP_L4/RPL4.CPP b/RP_L4/RPL4.CPP index dd822ed..09e703b 100644 --- a/RP_L4/RPL4.CPP +++ b/RP_L4/RPL4.CPP @@ -223,6 +223,12 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine { } + // last race's final scores first (no-op on first boot) + if (!RPL4FrontEnd_ShowResults(hInstance, hWnd)) + { + break; + } + static char frontend_egg[MAX_PATH]; if (!RPL4FrontEnd_Run(hInstance, hWnd, frontend_egg, sizeof(frontend_egg))) { diff --git a/RP_L4/RPL4CONSOLE.cpp b/RP_L4/RPL4CONSOLE.cpp index 9d57975..b95fdc2 100644 --- a/RP_L4/RPL4CONSOLE.cpp +++ b/RP_L4/RPL4CONSOLE.cpp @@ -196,3 +196,21 @@ Logical { return gPhase == PhaseStopped; } + +int + RPL4LocalConsole_ResultCount() +{ + return gResultCount; +} + +Logical + RPL4LocalConsole_GetResult(int index, int *host_ID, int *score) +{ + if (index < 0 || index >= gResultCount) + { + return False; + } + *host_ID = gResults[index].hostID; + *score = gResults[index].score; + return True; +} diff --git a/RP_L4/RPL4CONSOLE.h b/RP_L4/RPL4CONSOLE.h index 7181739..61de503 100644 --- a/RP_L4/RPL4CONSOLE.h +++ b/RP_L4/RPL4CONSOLE.h @@ -25,3 +25,11 @@ void // back to the setup screen for the next race in the same process. Logical RPL4LocalConsole_MissionCompleted(); + +// The last completed mission's final scores (what each pod reported at +// mission end). Valid until the next mission starts running. +int + RPL4LocalConsole_ResultCount(); + +Logical + RPL4LocalConsole_GetResult(int index, int *host_ID, int *score); diff --git a/RP_L4/RPL4FE.cpp b/RP_L4/RPL4FE.cpp index e8b6d6d..67ee92d 100644 --- a/RP_L4/RPL4FE.cpp +++ b/RP_L4/RPL4FE.cpp @@ -2,6 +2,7 @@ #pragma hdrstop #include "rpl4fe.h" +#include "rpl4console.h" #include #include @@ -154,6 +155,12 @@ namespace FEState *gFE = NULL; int gLastMissionSeconds = 0; + // carried across races so cycling back to the menu keeps the + // player's loadout (and names the results screen's score rows) + int gPersistSelection[GroupCount]; + Logical gHavePersist = False; + char gLastPilotName[24] = "Pilot"; + const COLORREF kGreenBright = RGB(64, 255, 64); const COLORREF kGreenDim = RGB(24, 140, 24); const COLORREF kBlack = RGB(0, 0, 0); @@ -588,8 +595,15 @@ Logical { FEState fe; memset(&fe, 0, sizeof(fe)); - strcpy(fe.pilotName, "Pilot"); - fe.selection[GroupLength] = 2; // 5:00 + strcpy(fe.pilotName, gLastPilotName); + if (gHavePersist) + { + memcpy(fe.selection, gPersistSelection, sizeof(fe.selection)); + } + else + { + fe.selection[GroupLength] = 2; // 5:00 + } gFE = &fe; //--------------------------------------------------------------- @@ -688,6 +702,9 @@ Logical { strcpy(fe.pilotName, "Pilot"); } + strcpy(gLastPilotName, fe.pilotName); + memcpy(gPersistSelection, fe.selection, sizeof(gPersistSelection)); + gHavePersist = True; std::string egg; egg.reserve(16384); @@ -727,6 +744,295 @@ int return gLastMissionSeconds; } +//######################################################################## +//######################## RPL4FrontEnd_ShowResults ###################### +//######################################################################## + +namespace +{ + struct ResultRow + { + char name[32]; + int score; + }; + + struct ResultsState + { + HWND window; + HFONT textFont; + HFONT titleFont; + ResultRow rows[16]; + int rowCount; + RECT continueRect; + Logical dismissed; + Logical closed; + }; + + ResultsState *gRS = NULL; + + const char *PlaceName(int place) + { + static const char *kPlaces[] = { "1ST", "2ND", "3RD", "4TH" }; + static char other[8]; + if (place < 4) + { + return kPlaces[place]; + } + sprintf(other, "%dTH", place + 1); + return other; + } + + void PaintResults(ResultsState *rs) + { + PAINTSTRUCT ps; + HDC hdc = BeginPaint(rs->window, &ps); + + RECT client; + GetClientRect(rs->window, &client); + + HDC mem = CreateCompatibleDC(hdc); + HBITMAP surface = CreateCompatibleBitmap(hdc, client.right, client.bottom); + HBITMAP old_surface = (HBITMAP) SelectObject(mem, surface); + + FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH)); + SetBkMode(mem, TRANSPARENT); + + SelectObject(mem, rs->titleFont); + SetTextColor(mem, kGreenBright); + RECT title = client; + title.top = client.bottom / 28; + title.bottom = title.top + client.bottom / 10; + DrawTextA(mem, "RACE RESULTS", -1, &title, + DT_CENTER | DT_TOP | DT_SINGLELINE); + + SelectObject(mem, rs->textFont); + int row_h = client.bottom / 16; + int top = client.bottom / 4; + int left = client.right / 3; + int right = 2 * client.right / 3; + char text[48]; + for (int i = 0; i < rs->rowCount; ++i) + { + RECT row; + row.left = left; + row.right = right; + row.top = top + i * row_h; + row.bottom = row.top + row_h; + + SetTextColor(mem, (i == 0) ? kGreenBright : kGreenDim); + DrawTextA(mem, PlaceName(i), -1, &row, + DT_LEFT | DT_VCENTER | DT_SINGLELINE); + + RECT name = row; + name.left += (right - left) / 5; + DrawTextA(mem, rs->rows[i].name, -1, &name, + DT_LEFT | DT_VCENTER | DT_SINGLELINE); + + sprintf(text, "%d", rs->rows[i].score); + DrawTextA(mem, text, -1, &row, + DT_RIGHT | DT_VCENTER | DT_SINGLELINE); + } + + HBRUSH frame_brush = CreateSolidBrush(kGreenBright); + FrameRect(mem, &rs->continueRect, frame_brush); + DeleteObject(frame_brush); + SetTextColor(mem, kGreenBright); + DrawTextA(mem, "C O N T I N U E", -1, &rs->continueRect, + DT_CENTER | DT_VCENTER | DT_SINGLELINE); + + BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY); + + SelectObject(mem, old_surface); + DeleteObject(surface); + DeleteDC(mem); + EndPaint(rs->window, &ps); + } + + LRESULT CALLBACK + ResultsWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) + { + ResultsState *rs = gRS; + + switch (message) + { + case WM_PAINT: + if (rs != NULL) + { + PaintResults(rs); + return 0; + } + break; + + case WM_LBUTTONDOWN: + if (rs != NULL) + { + POINT point = { (int)(short) LOWORD(lParam), (int)(short) HIWORD(lParam) }; + if (PtInRect(&rs->continueRect, point)) + { + rs->dismissed = True; + PostMessageA(rs->window, WM_NULL, 0, 0); + } + return 0; + } + break; + + case WM_ERASEBKGND: + return 1; + } + return DefWindowProcA(hwnd, message, wParam, lParam); + } +} + +Logical + RPL4FrontEnd_ShowResults( + HINSTANCE instance, + HWND main_window + ) +{ + int result_count = RPL4LocalConsole_ResultCount(); + if (result_count <= 0) + { + return True; // nothing to show (first boot) + } + if (!IsWindow(main_window)) + { + return False; + } + + ResultsState rs; + memset(&rs, 0, sizeof(rs)); + + //--------------------------------------------------------------- + // Intake, then sort descending by score. Single-player results + // carry the pilot's own name; with more pods the roster will map + // host IDs to Steam personas (until then: pod number). + //--------------------------------------------------------------- + for (int i = 0; i < result_count && rs.rowCount < 16; ++i) + { + int host_ID, score; + if (!RPL4LocalConsole_GetResult(i, &host_ID, &score)) + { + continue; + } + ResultRow *row = &rs.rows[rs.rowCount++]; + if (result_count == 1) + { + sprintf(row->name, "%s", gLastPilotName); + } + else + { + sprintf(row->name, "POD %d", host_ID); + } + row->score = score; + } + for (int i = 1; i < rs.rowCount; ++i) + { + ResultRow key = rs.rows[i]; + int j = i - 1; + while (j >= 0 && rs.rows[j].score < key.score) + { + rs.rows[j + 1] = rs.rows[j]; + --j; + } + rs.rows[j + 1] = key; + } + + static Logical class_registered = False; + if (!class_registered) + { + class_registered = True; + WNDCLASSA window_class; + memset(&window_class, 0, sizeof(window_class)); + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.lpfnWndProc = ResultsWndProc; + window_class.hInstance = instance; + window_class.hCursor = LoadCursor(NULL, IDC_ARROW); + window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH); + window_class.lpszClassName = "RPResults"; + RegisterClassA(&window_class); + } + + RECT client; + GetClientRect(main_window, &client); + + rs.window = CreateWindowExA( + 0, "RPResults", "", + WS_CHILD | WS_VISIBLE, + 0, 0, client.right, client.bottom, + main_window, NULL, instance, NULL); + if (rs.window == NULL) + { + return False; + } + + int text_h = client.bottom / 40; + if (text_h < 16) text_h = 16; + if (text_h > 24) text_h = 24; + rs.textFont = CreateFontA(-(text_h * 3 / 2), 0, 0, 0, FW_NORMAL, + FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, + CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN, + "Consolas"); + rs.titleFont = CreateFontA(-(text_h * 2), 0, 0, 0, FW_BOLD, + FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, + CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN, + "Consolas"); + + int row_h = client.bottom / 36; + if (row_h < 18) row_h = 18; + if (row_h > 30) row_h = 30; + int col_w = client.right / 5; + rs.continueRect.left = (client.right - col_w) / 2; + rs.continueRect.right = rs.continueRect.left + col_w; + rs.continueRect.top = client.bottom - 3 * row_h; + rs.continueRect.bottom = client.bottom - row_h; + + gRS = &rs; + SetFocus(rs.window); + InvalidateRect(rs.window, NULL, TRUE); + + //--------------------------------------------------------------- + // Modal until CONTINUE (or any key) - same shape as the menu loop + //--------------------------------------------------------------- + MSG msg; + while (!rs.dismissed && !rs.closed) + { + BOOL result = GetMessageA(&msg, NULL, 0, 0); + if (result <= 0) + { + rs.closed = True; + break; + } + if (msg.message == WM_QUIT || + (msg.message == WM_SYSCOMMAND && msg.wParam == SC_CLOSE)) + { + rs.closed = True; + } + if (msg.message == WM_CLOSE && msg.hwnd == main_window) + { + rs.closed = True; + } + if (msg.message == WM_KEYDOWN && + (msg.wParam == VK_RETURN || msg.wParam == VK_SPACE || + msg.wParam == VK_ESCAPE)) + { + rs.dismissed = True; + } + TranslateMessage(&msg); + DispatchMessageA(&msg); + if (!IsWindow(main_window)) + { + rs.closed = True; + } + } + + DestroyWindow(rs.window); + DeleteObject(rs.textFont); + DeleteObject(rs.titleFont); + gRS = NULL; + + return !rs.closed; +} + //######################################################################## // Ordinal plasma graphics (verbatim from the console / TEST.EGG) //######################################################################## diff --git a/RP_L4/RPL4FE.h b/RP_L4/RPL4FE.h index c6d5295..baa9496 100644 --- a/RP_L4/RPL4FE.h +++ b/RP_L4/RPL4FE.h @@ -28,3 +28,13 @@ Logical // successful launch - the local console marshals the mission with it. int RPL4FrontEnd_LastMissionSeconds(); + +// The between-races results screen: shows the last mission's final +// scores (from the local console) with a CONTINUE button. Returns +// immediately when there are no results (first boot). False only if +// the player closed the window instead of continuing. +Logical + RPL4FrontEnd_ShowResults( + HINSTANCE instance, + HWND main_window + );