Front end: the miniconsole menu -- interactive race setup (Workstream B)
The in-game menu that replaces the operator console. A GDI-painted green-on-black terminal (RP412 RPL4FE pattern) in its own top-level window, shown in front-end mode (no -egg/-net) before the mission: - Catalogs as clickable columns: MAP (8), MECH (8), COLOR (8), TIME, WEATHER, LENGTH, plus a PILOT NAME edit box and a LAUNCH button. Selected item highlighted; click cycles selections. - LAUNCH fills a BTFeMission from the selections, writes the egg (BTFeMission_WriteEgg -- the existing builder), and points the standard -egg load path at it. Closing the menu quits the process. - btl4main.cpp: front-end mode runs the menu; menu-close exits. Verified end to end: no-args launch shows the menu; clicking LAUNCH builds frontend.egg from the chosen map/mech/color/time/length and the mission loads and runs (map=grass mech=bhk1 color=White time=day length=300 from the default selections). Remaining in Phase 5: the LocalConsole marshal (timed stop + results + single-binary menu<->mission loop). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -333,20 +333,292 @@ int BTFeMission_WriteEgg(const BTFeMission *mission, const char *path)
|
||||
return 1;
|
||||
}
|
||||
|
||||
//===========================================================================//
|
||||
// THE MINICONSOLE MENU -- the in-game race setup that replaces the operator
|
||||
// console. A GDI-painted green-on-black terminal (RP412 RPL4FE pattern): the
|
||||
// catalogs render as clickable columns, a pilot-name edit box, and a LAUNCH
|
||||
// button; clicking LAUNCH fills a BTFeMission from the selections and writes
|
||||
// the egg. Runs in its own top-level window before the mission (front-end
|
||||
// mode), so no D3D is up yet.
|
||||
//===========================================================================//
|
||||
namespace {
|
||||
|
||||
struct MenuEntry { const char *key; const char *name; };
|
||||
|
||||
const MenuEntry kMenuMaps[] = {
|
||||
{ "grass", "Grasslands" }, { "cavern", "Cavern" }, { "rav", "Ravine" },
|
||||
{ "polar3", "Polar III" }, { "polar4", "Polar IV" }, { "arena1", "Arena I" },
|
||||
{ "arena2", "Arena II" }, { "dbase", "Database" },
|
||||
};
|
||||
const MenuEntry kMenuMechs[] = {
|
||||
{ "bhk1", "Black Hawk" }, { "avatar", "Avatar" }, { "loki", "Loki" },
|
||||
{ "madcat", "Mad Cat" }, { "owens", "Owens" }, { "sunder", "Sunder" },
|
||||
{ "thor", "Thor" }, { "vulture","Vulture" },
|
||||
};
|
||||
const MenuEntry kMenuColors[] = {
|
||||
{ "White","White" }, { "Red","Red" }, { "Blue","Blue" }, { "Green","Green" },
|
||||
{ "Yellow","Yellow" },{ "Orange","Orange" },{ "Purple","Purple" },{ "Cyan","Cyan" },
|
||||
};
|
||||
const MenuEntry kMenuTimes[] = { { "day","Day" }, { "night","Night" } };
|
||||
const MenuEntry kMenuWeather[] = { { "clear","Clear" }, { "fog","Light Fog" }, { "soup","Heavy Fog" } };
|
||||
struct LenEntry { int seconds; const char *name; };
|
||||
const LenEntry kMenuLengths[] = { { 180,"3:00" }, { 300,"5:00" }, { 600,"10:00" }, { 900,"15:00" }, { 0,"Endless" } };
|
||||
|
||||
#define MENU_COUNT(t) ((int)(sizeof(t)/sizeof((t)[0])))
|
||||
|
||||
enum MenuGroup { GMap=0, GMech, GColor, GTime, GWeather, GLength, GLaunch, GCount };
|
||||
|
||||
const char *GroupTitle(int g) {
|
||||
switch (g) {
|
||||
case GMap: return "MAP"; case GMech: return "MECH"; case GColor: return "COLOR";
|
||||
case GTime: return "TIME"; case GWeather: return "WEATHER"; case GLength: return "LENGTH";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
int GroupSize(int g) {
|
||||
switch (g) {
|
||||
case GMap: return MENU_COUNT(kMenuMaps); case GMech: return MENU_COUNT(kMenuMechs);
|
||||
case GColor: return MENU_COUNT(kMenuColors); case GTime: return MENU_COUNT(kMenuTimes);
|
||||
case GWeather: return MENU_COUNT(kMenuWeather); case GLength: return MENU_COUNT(kMenuLengths);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const char *ItemName(int g, int i) {
|
||||
switch (g) {
|
||||
case GMap: return kMenuMaps[i].name; case GMech: return kMenuMechs[i].name;
|
||||
case GColor: return kMenuColors[i].name; case GTime: return kMenuTimes[i].name;
|
||||
case GWeather: return kMenuWeather[i].name; case GLength: return kMenuLengths[i].name;
|
||||
case GLaunch: return "L A U N C H";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
struct MenuItem { int group, index; RECT rect; };
|
||||
struct MenuState {
|
||||
int selection[GCount];
|
||||
MenuItem items[128];
|
||||
int itemCount;
|
||||
HWND window, nameEdit;
|
||||
HFONT textFont, titleFont;
|
||||
HBRUSH editBrush;
|
||||
int launched, closed;
|
||||
};
|
||||
MenuState *gMenu = NULL;
|
||||
|
||||
const COLORREF kGreenBright = RGB(64, 255, 64);
|
||||
const COLORREF kGreenDim = RGB(24, 140, 24);
|
||||
const COLORREF kBlack = RGB(0, 0, 0);
|
||||
|
||||
void AddGroupItems(MenuState *m, int group, int x, int *y, int row_h, int col_w) {
|
||||
int n = GroupSize(group);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
MenuItem *it = &m->items[m->itemCount++];
|
||||
it->group = group; it->index = i;
|
||||
it->rect.left = x; it->rect.top = *y;
|
||||
it->rect.right = x + col_w; it->rect.bottom = *y + row_h;
|
||||
*y += row_h;
|
||||
}
|
||||
*y += row_h; // gap + header room for the next group
|
||||
}
|
||||
|
||||
void LayoutMenu(MenuState *m, int cw, int ch) {
|
||||
m->itemCount = 0;
|
||||
int row_h = ch / 26; if (row_h < 20) row_h = 20; if (row_h > 34) row_h = 34;
|
||||
int col_w = cw / 5;
|
||||
int col1 = cw / 12;
|
||||
int col2 = col1 + col_w + cw / 24;
|
||||
int col3 = col2 + col_w + cw / 24;
|
||||
int top = ch / 7;
|
||||
|
||||
int y = top; AddGroupItems(m, GMap, col1, &y, row_h, col_w);
|
||||
AddGroupItems(m, GTime, col1, &y, row_h, col_w);
|
||||
AddGroupItems(m, GWeather, col1, &y, row_h, col_w);
|
||||
AddGroupItems(m, GLength, col1, &y, row_h, col_w);
|
||||
y = top; AddGroupItems(m, GMech, col2, &y, row_h, col_w);
|
||||
y = top + 2 * row_h; AddGroupItems(m, GColor, col3, &y, row_h, col_w);
|
||||
|
||||
MenuItem *launch = &m->items[m->itemCount++];
|
||||
launch->group = GLaunch; launch->index = 0;
|
||||
launch->rect.left = col3; launch->rect.top = ch - 3 * row_h;
|
||||
launch->rect.right = col3 + col_w; launch->rect.bottom = ch - row_h;
|
||||
|
||||
if (m->nameEdit != NULL)
|
||||
MoveWindow(m->nameEdit, col3, top - row_h / 4, col_w, row_h, TRUE);
|
||||
}
|
||||
|
||||
void PaintMenu(MenuState *m) {
|
||||
PAINTSTRUCT ps;
|
||||
HDC hdc = BeginPaint(m->window, &ps);
|
||||
RECT client; GetClientRect(m->window, &client);
|
||||
HDC mem = CreateCompatibleDC(hdc);
|
||||
HBITMAP surf = CreateCompatibleBitmap(hdc, client.right, client.bottom);
|
||||
HBITMAP oldSurf = (HBITMAP) SelectObject(mem, surf);
|
||||
FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH));
|
||||
SetBkMode(mem, TRANSPARENT);
|
||||
|
||||
SelectObject(mem, m->titleFont);
|
||||
SetTextColor(mem, kGreenBright);
|
||||
RECT title = client; title.top = client.bottom / 26; title.bottom = title.top + client.bottom / 9;
|
||||
DrawTextA(mem, "B A T T L E T E C H - MISSION SETUP", -1, &title, DT_CENTER | DT_TOP | DT_SINGLELINE);
|
||||
|
||||
SelectObject(mem, m->textFont);
|
||||
int prevGroup = -1;
|
||||
for (int i = 0; i < m->itemCount; ++i) {
|
||||
const MenuItem *it = &m->items[i];
|
||||
if (it->group != prevGroup && it->group < GLaunch) {
|
||||
prevGroup = it->group;
|
||||
RECT h = it->rect; h.top -= (it->rect.bottom - it->rect.top); h.bottom = it->rect.top;
|
||||
SetTextColor(mem, kGreenBright);
|
||||
DrawTextA(mem, GroupTitle(it->group), -1, &h, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
RECT row = it->rect;
|
||||
if (it->group == GLaunch) {
|
||||
HBRUSH b = CreateSolidBrush(kGreenBright); FrameRect(mem, &row, b); DeleteObject(b);
|
||||
SetTextColor(mem, kGreenBright);
|
||||
DrawTextA(mem, ItemName(it->group, it->index), -1, &row, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||
continue;
|
||||
}
|
||||
int selected = (m->selection[it->group] == it->index);
|
||||
if (selected) {
|
||||
HBRUSH b = CreateSolidBrush(kGreenDim); FillRect(mem, &row, b); DeleteObject(b);
|
||||
SetTextColor(mem, kGreenBright);
|
||||
} else SetTextColor(mem, kGreenDim);
|
||||
RECT t = row; t.left += 8;
|
||||
DrawTextA(mem, ItemName(it->group, it->index), -1, &t, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
// pilot-name header
|
||||
if (m->nameEdit != NULL) {
|
||||
RECT er; GetWindowRect(m->nameEdit, &er);
|
||||
POINT c = { er.left, er.top }; ScreenToClient(m->window, &c);
|
||||
RECT h; h.left = c.x; h.right = c.x + 300; h.bottom = c.y; h.top = c.y - 26;
|
||||
SetTextColor(mem, kGreenBright);
|
||||
DrawTextA(mem, "PILOT NAME", -1, &h, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);
|
||||
SelectObject(mem, oldSurf); DeleteObject(surf); DeleteDC(mem);
|
||||
EndPaint(m->window, &ps);
|
||||
}
|
||||
|
||||
void MenuClick(MenuState *m, int x, int y) {
|
||||
for (int i = 0; i < m->itemCount; ++i) {
|
||||
const MenuItem *it = &m->items[i];
|
||||
if (x >= it->rect.left && x < it->rect.right && y >= it->rect.top && y < it->rect.bottom) {
|
||||
if (it->group == GLaunch) {
|
||||
m->launched = 1; PostMessageA(m->window, WM_NULL, 0, 0);
|
||||
} else {
|
||||
m->selection[it->group] = it->index;
|
||||
InvalidateRect(m->window, NULL, FALSE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK MenuWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
MenuState *m = gMenu;
|
||||
switch (msg) {
|
||||
case WM_PAINT: if (m) { PaintMenu(m); return 0; } break;
|
||||
case WM_LBUTTONDOWN: if (m) { MenuClick(m, (int)(short)LOWORD(lp), (int)(short)HIWORD(lp)); return 0; } break;
|
||||
case WM_CTLCOLOREDIT:
|
||||
if (m) { SetTextColor((HDC)wp, kGreenBright); SetBkColor((HDC)wp, kBlack); return (LRESULT)m->editBrush; }
|
||||
break;
|
||||
case WM_CLOSE: if (m) m->closed = 1; return 0;
|
||||
case WM_DESTROY: return 0;
|
||||
}
|
||||
return DefWindowProcA(hwnd, msg, wp, lp);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
//---------------------------------------------------------------------------//
|
||||
// Front-end mode entry. Builds the default solo egg into frontend.egg and
|
||||
// points the engine at it via the standard -egg path. The interactive menu
|
||||
// (catalog selection on-screen) and the marshal are layered on top of this.
|
||||
// Run the menu; on LAUNCH build the egg + point the engine at it. Returns 1
|
||||
// if a mission was launched, 0 if the player closed the menu (quit).
|
||||
//---------------------------------------------------------------------------//
|
||||
int BTFrontEnd_Run()
|
||||
{
|
||||
HINSTANCE instance = GetModuleHandleA(NULL);
|
||||
|
||||
MenuState m; memset(&m, 0, sizeof(m));
|
||||
m.selection[GLength] = 1; // default 5:00
|
||||
gMenu = &m;
|
||||
|
||||
static int classReg = 0;
|
||||
if (!classReg) {
|
||||
classReg = 1;
|
||||
WNDCLASSA wc; memset(&wc, 0, sizeof(wc));
|
||||
wc.style = CS_HREDRAW | CS_VREDRAW;
|
||||
wc.lpfnWndProc = MenuWndProc;
|
||||
wc.hInstance = instance;
|
||||
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
|
||||
wc.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
|
||||
wc.lpszClassName = "BTFrontEnd";
|
||||
RegisterClassA(&wc);
|
||||
}
|
||||
|
||||
int W = 1000, H = 720;
|
||||
int sx = (GetSystemMetrics(SM_CXSCREEN) - W) / 2;
|
||||
int sy = (GetSystemMetrics(SM_CYSCREEN) - H) / 2;
|
||||
m.window = CreateWindowExA(0, "BTFrontEnd", "BattleTech",
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE, sx < 0 ? 0 : sx, sy < 0 ? 0 : sy, W, H,
|
||||
NULL, NULL, instance, NULL);
|
||||
if (m.window == NULL) { gMenu = NULL; return 0; }
|
||||
|
||||
RECT client; GetClientRect(m.window, &client);
|
||||
int text_h = client.bottom / 32; if (text_h < 16) text_h = 16; if (text_h > 26) text_h = 26;
|
||||
m.textFont = CreateFontA(-text_h, 0,0,0, FW_NORMAL, 0,0,0, ANSI_CHARSET,
|
||||
OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY, DEFAULT_PITCH | FF_MODERN, "Consolas");
|
||||
m.titleFont = CreateFontA(-(text_h * 2), 0,0,0, FW_BOLD, 0,0,0, ANSI_CHARSET,
|
||||
OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY, DEFAULT_PITCH | FF_MODERN, "Consolas");
|
||||
m.editBrush = CreateSolidBrush(kBlack);
|
||||
m.nameEdit = CreateWindowExA(0, "EDIT", "Pilot",
|
||||
WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, 0,0,10,10, m.window, NULL, instance, NULL);
|
||||
SendMessageA(m.nameEdit, WM_SETFONT, (WPARAM) m.textFont, TRUE);
|
||||
SendMessageA(m.nameEdit, EM_SETLIMITTEXT, 22, 0);
|
||||
|
||||
LayoutMenu(&m, client.right, client.bottom);
|
||||
InvalidateRect(m.window, NULL, TRUE);
|
||||
SetForegroundWindow(m.window);
|
||||
|
||||
MSG msg;
|
||||
while (!m.launched && !m.closed) {
|
||||
BOOL r = GetMessageA(&msg, NULL, 0, 0);
|
||||
if (r <= 0) { m.closed = 1; break; }
|
||||
if (msg.message == WM_QUIT) m.closed = 1;
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageA(&msg);
|
||||
}
|
||||
|
||||
// harvest the loadout
|
||||
char pilotName[24] = "Pilot";
|
||||
if (!m.closed) {
|
||||
GetWindowTextA(m.nameEdit, pilotName, sizeof(pilotName) - 1);
|
||||
if (pilotName[0] == '\0') strcpy(pilotName, "Pilot");
|
||||
}
|
||||
int launched = m.launched, closed = m.closed;
|
||||
int selMap = m.selection[GMap], selMech = m.selection[GMech], selColor = m.selection[GColor];
|
||||
int selTime = m.selection[GTime], selWx = m.selection[GWeather], selLen = m.selection[GLength];
|
||||
|
||||
DestroyWindow(m.window);
|
||||
DeleteObject(m.textFont); DeleteObject(m.titleFont); DeleteObject(m.editBrush);
|
||||
gMenu = NULL;
|
||||
|
||||
if (closed || !launched)
|
||||
return 0;
|
||||
|
||||
// Build the mission from the selections.
|
||||
BTFeMission mission;
|
||||
BTFeMission_Default(&mission);
|
||||
strcpy(mission.map, kMenuMaps[selMap].key);
|
||||
strcpy(mission.timeOfDay, kMenuTimes[selTime].key);
|
||||
strcpy(mission.weather, kMenuWeather[selWx].key);
|
||||
mission.lengthSeconds = kMenuLengths[selLen].seconds;
|
||||
strcpy(mission.pilots[0].name, pilotName);
|
||||
strcpy(mission.pilots[0].vehicle, kMenuMechs[selMech].key);
|
||||
strcpy(mission.pilots[0].color, kMenuColors[selColor].key);
|
||||
|
||||
const char *egg_path = "frontend.egg";
|
||||
if (!BTFeMission_WriteEgg(&mission, egg_path))
|
||||
return 0;
|
||||
|
||||
L4Application::SetEggNotationFileName(egg_path);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user