Martian Football: the second scenario is playable

The setup menu gains a SCENARIO group. Picking Football relayouts the
menu live: the track list swaps to the football-legal set (drops
Paingod Passage and the race build of Freezemoon Freeway, adds the
football build headmf - RPConfig per-scenario invalid lists), the
COLOR and BADGE columns become TEAM and POSITION, and the title
reads MARTIAN FOOTBALL.

The egg builder was restructured around one flat pilot table so both
scenarios share it. Football emits scenario=football, per-pilot team=
and position= entries, and the [teams] / [team::X] / [pilots::X] /
[teambitmap] blocks in RPFootballMission layout (team name bitmaps
generated by the same GDI plasma renderer as pilot names). Colors are
derived rather than chosen, as the arcade did it: the runner wears the
team runner color, crushers and blockers the team color. Multi-pod
games alternate pilots across two teams and give each team exactly one
runner, honoring the host own position pick.

Verified end to end: Football launches from the menu, the engine loads
the football mission (the cockpit shows the RUNNER - GO FOR POINTS
panel), the console marshals it to a scored finish, and the Death Race
path still passes its regression unchanged.

Known gap: lobby members cannot pick their own team or position yet -
the host pick seeds a deterministic assignment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-24 19:42:33 -05:00
co-authored by Claude Fable 5
parent 0b84b32486
commit 3194d3f973
4 changed files with 397 additions and 105 deletions
+305 -69
View File
@@ -35,6 +35,52 @@ namespace
{ "headoff", "Freezemoon's Freeway" },
};
// Football excludes pain/headoff and adds the football build of
// Freezemoon's Freeway (RPConfig.xml per-scenario invalid lists).
const CatalogEntry kFootballMaps[] =
{
{ "wise", "Wiseguy's Wake" },
{ "lyzlane", "Lyz's Lane" },
{ "yip", "Yip's Yahoorama" },
{ "blade", "Blade's Edge" },
{ "otto", "Berserker's Bahnzai" },
{ "frstrm", "Firestorm's Fury" },
{ "burnt", "Burnt Cookies" },
{ "brewers", "Brewer's Bane" },
{ "headmf", "Freezemoon's Freeway" },
};
const CatalogEntry kScenarios[] =
{
{ "race", "Death Race" },
{ "football", "Football" },
};
// Two-color teams: crushers/blockers wear the team color, the
// runner wears the runner color (that is how you spot the ball).
struct TeamEntry
{
const char *key;
const char *name;
const char *teamColor;
const char *runnerColor;
};
const TeamEntry kTeams[] =
{
{ "Red/Pink", "Red / Pink", "Red", "Pink" },
{ "Blue/Aqua", "Blue / Aqua", "Blue", "Aqua" },
{ "Green/Yellow", "Green / Yellow", "Green", "Yellow" },
{ "Black/Purple", "Black / Purple", "Black", "Purple" },
};
const CatalogEntry kPositions[] =
{
{ "runner", "Runner" },
{ "crusher", "Crusher" },
{ "blocker", "Blocker" },
};
const CatalogEntry kVehicles[] =
{
{ "quark", "Quark" },
@@ -125,12 +171,31 @@ namespace
GroupTime,
GroupWeather,
GroupLength,
GroupScenario,
GroupTeam, // football only
GroupPosition, // football only
GroupLaunch,
GroupSteamHost, // buttons, not selections (Steam builds only)
GroupSteamJoin,
GroupCount
};
Logical IsFootball(const int *selection)
{
return selection[GroupScenario] == 1;
}
const CatalogEntry *ActiveMaps(const int *selection, int *count)
{
if (IsFootball(selection))
{
*count = (int)(sizeof(kFootballMaps) / sizeof(kFootballMaps[0]));
return kFootballMaps;
}
*count = (int)(sizeof(kMaps) / sizeof(kMaps[0]));
return kMaps;
}
struct FEItem
{
int group;
@@ -311,8 +376,10 @@ namespace
int top = client_h / 7;
int y = top;
AddGroupItems(fe, GroupMap, FE_COUNT(kMaps), col1, &y, row_h, col_w);
int y_after_maps = y;
AddGroupItems(fe, GroupScenario, FE_COUNT(kScenarios), col1, &y, row_h, col_w);
int map_count;
ActiveMaps(fe->selection, &map_count);
AddGroupItems(fe, GroupMap, map_count, col1, &y, row_h, col_w);
AddGroupItems(fe, GroupTime, FE_COUNT(kTimes), col1, &y, row_h, col_w);
AddGroupItems(fe, GroupWeather, FE_COUNT(kWeather), col1, &y, row_h, col_w);
AddGroupItems(fe, GroupLength, FE_COUNT(kLengths), col1, &y, row_h, col_w);
@@ -321,8 +388,16 @@ namespace
AddGroupItems(fe, GroupVehicle, FE_COUNT(kVehicles), col2, &y, row_h, col_w);
y = top + 2 * row_h; // leave room for the name edit + header
AddGroupItems(fe, GroupColor, FE_COUNT(kColors), col3, &y, row_h, col_w);
AddGroupItems(fe, GroupBadge, FE_COUNT(kBadges), col3, &y, row_h, col_w);
if (IsFootball(fe->selection))
{
AddGroupItems(fe, GroupTeam, FE_COUNT(kTeams), col3, &y, row_h, col_w);
AddGroupItems(fe, GroupPosition, FE_COUNT(kPositions), col3, &y, row_h, col_w);
}
else
{
AddGroupItems(fe, GroupColor, FE_COUNT(kColors), col3, &y, row_h, col_w);
AddGroupItems(fe, GroupBadge, FE_COUNT(kBadges), col3, &y, row_h, col_w);
}
// launch button
FEItem *launch = &fe->items[fe->itemCount++];
@@ -365,31 +440,45 @@ namespace
{
switch (group)
{
case GroupScenario: return "SCENARIO";
case GroupMap: return "TRACK";
case GroupVehicle: return "VEHICLE";
case GroupColor: return "COLOR";
case GroupBadge: return "BADGE";
case GroupTeam: return "TEAM";
case GroupPosition: return "POSITION";
case GroupTime: return "TIME OF DAY";
case GroupWeather: return "WEATHER";
case GroupLength: return "RACE LENGTH";
case GroupLength: return "GAME LENGTH";
}
return "";
}
// the map rows need the active scenario's list
const int *gItemNameSelection = NULL;
const char *ItemName(int group, int index)
{
switch (group)
{
case GroupMap: return kMaps[index].name;
case GroupScenario: return kScenarios[index].name;
case GroupMap:
if (gItemNameSelection != NULL && IsFootball(gItemNameSelection))
{
return kFootballMaps[index].name;
}
return kMaps[index].name;
case GroupVehicle: return kVehicles[index].name;
case GroupColor: return kColors[index].name;
case GroupBadge: return kBadges[index].name;
case GroupTeam: return kTeams[index].name;
case GroupPosition: return kPositions[index].name;
case GroupTime: return kTimes[index].name;
case GroupWeather: return kWeather[index].name;
case GroupLength: return kLengths[index].name;
case GroupLaunch: return "L A U N C H R A C E";
case GroupSteamHost: return "HOST STEAM RACE";
case GroupSteamJoin: return "JOIN STEAM RACE";
case GroupLaunch: return "L A U N C H G A M E";
case GroupSteamHost: return "HOST STEAM GAME";
case GroupSteamJoin: return "JOIN STEAM GAME";
}
return "";
}
@@ -413,13 +502,17 @@ namespace
SetBkMode(mem, TRANSPARENT);
// title
gItemNameSelection = fe->selection;
SelectObject(mem, fe->titleFont);
SetTextColor(mem, kGreenBright);
RECT title = client;
title.top = client.bottom / 28;
title.bottom = title.top + client.bottom / 10;
DrawTextA(mem, "RED PLANET - MARTIAN DEATH RACE", -1, &title,
DT_CENTER | DT_TOP | DT_SINGLELINE);
DrawTextA(mem,
IsFootball(fe->selection)
? "RED PLANET - MARTIAN FOOTBALL"
: "RED PLANET - MARTIAN DEATH RACE",
-1, &title, DT_CENTER | DT_TOP | DT_SINGLELINE);
SelectObject(mem, fe->textFont);
@@ -521,6 +614,20 @@ namespace
else
{
fe->selection[item->group] = item->index;
if (item->group == GroupScenario)
{
// the scenario swaps the map list and the
// color/badge vs team/position columns
int map_count;
ActiveMaps(fe->selection, &map_count);
if (fe->selection[GroupMap] >= map_count)
{
fe->selection[GroupMap] = 0;
}
RECT client;
GetClientRect(fe->menuWindow, &client);
LayoutMenu(fe, client.right, client.bottom);
}
InvalidateRect(fe->menuWindow, NULL, FALSE);
}
return;
@@ -655,13 +762,19 @@ namespace
extern const char *kOrdinalsBlock;
//---------------------------------------------------------------
// Egg assembly (console RPMission.ToEggString, race scenario).
// Single player uses the standalone address; a hosted network
// race lists the owner first plus one pilot per member pod.
// Egg assembly (console RPMission.ToEggString / RPFootballMission).
// Single player uses the standalone address; a hosted network game
// lists the owner first plus one pilot per member pod. Football
// adds team=/position= per pilot and the [teams] blocks; the
// owner's team/position picks seed a deterministic assignment
// across two teams (each team's first member without a runner
// becomes one; the rest alternate crusher/blocker; the runner
// wears the team's runner color, everyone else the team color).
//---------------------------------------------------------------
void BuildEggText(const FEState *fe, std::string &egg)
{
char line[256];
Logical football = IsFootball(fe->selection);
ExtraPilot extras[maxExtraPilots];
char owner_address[64];
@@ -672,11 +785,100 @@ namespace
extra_count = 0; // single player
}
//
// One flat pilot table: the owner then the extras
//
struct PilotEntry
{
const char *address;
const char *name;
const char *vehicle;
const char *color;
const char *badge;
int team; // kTeams index, -1 outside football
const char *position;
};
PilotEntry pilots[maxExtraPilots + 1];
int pilot_count = 0;
PilotEntry *owner = &pilots[pilot_count++];
owner->address = owner_address;
owner->name = fe->pilotName;
owner->vehicle = kVehicles[fe->selection[GroupVehicle]].key;
owner->color = kColors[fe->selection[GroupColor]].key;
owner->badge = kBadges[fe->selection[GroupBadge]].key;
owner->team = -1;
owner->position = NULL;
for (int p = 0; p < extra_count; ++p)
{
PilotEntry *pilot = &pilots[pilot_count++];
pilot->address = extras[p].address;
pilot->name = extras[p].name;
pilot->vehicle = extras[p].vehicle;
pilot->color = extras[p].color;
pilot->badge = extras[p].badge;
pilot->team = -1;
pilot->position = NULL;
}
//
// Football: assign teams and positions, derive colors
//
int teams_used[2] = { 0, 1 };
if (football)
{
teams_used[0] = fe->selection[GroupTeam];
teams_used[1] = (teams_used[0] + 1) % FE_COUNT(kTeams);
int runner_on_team[2] = { 0, 0 };
int line_position[2] = { 0, 0 }; // crusher/blocker rotation
owner->team = 0;
owner->position = kPositions[fe->selection[GroupPosition]].key;
if (strcmp(owner->position, "runner") == 0)
{
runner_on_team[0] = 1;
}
for (int p = 1; p < pilot_count; ++p)
{
int side = p % 2; // alternate: B, A, B, A...
side = (side == 1) ? 1 : 0;
pilots[p].team = side;
if (!runner_on_team[side])
{
pilots[p].position = "runner";
runner_on_team[side] = 1;
}
else
{
pilots[p].position =
(line_position[side]++ % 2) ? "blocker" : "crusher";
}
}
for (int p = 0; p < pilot_count; ++p)
{
const TeamEntry *team = &kTeams[teams_used[pilots[p].team]];
pilots[p].color =
(strcmp(pilots[p].position, "runner") == 0)
? team->runnerColor : team->teamColor;
pilots[p].badge = "None";
}
}
//
// [mission]
//
int map_count;
const CatalogEntry *maps = ActiveMaps(fe->selection, &map_count);
egg += "[mission]\n";
egg += "adventure=Red Planet\n";
sprintf(line, "map=%s\n", kMaps[fe->selection[GroupMap]].key);
sprintf(line, "map=%s\n", maps[fe->selection[GroupMap]].key);
egg += line;
sprintf(line, "scenario=%s\n", kScenarios[fe->selection[GroupScenario]].key);
egg += line;
egg += "scenario=race\n";
sprintf(line, "time=%s\n", kTimes[fe->selection[GroupTime]].key);
egg += line;
sprintf(line, "weather=%s\n", kWeather[fe->selection[GroupWeather]].key);
@@ -690,84 +892,118 @@ namespace
egg += line;
}
//
// [pilots] + per-pilot sections
//
egg += "[pilots]\n";
sprintf(line, "pilot=%s\n", owner_address);
egg += line;
for (int p = 0; p < extra_count; ++p)
for (int p = 0; p < pilot_count; ++p)
{
sprintf(line, "pilot=%s\n", extras[p].address);
sprintf(line, "pilot=%s\n", pilots[p].address);
egg += line;
}
sprintf(line, "[%s]\n", owner_address);
egg += line;
egg += "hostType=0\n";
egg += "dropzone=one\n";
sprintf(line, "name=%s\n", fe->pilotName);
egg += line;
egg += "bitmapindex=1\n";
egg += "loadzones=1\n";
sprintf(line, "vehicle=%s\n", kVehicles[fe->selection[GroupVehicle]].key);
egg += line;
sprintf(line, "color=%s\n", kColors[fe->selection[GroupColor]].key);
egg += line;
sprintf(line, "badge=%s\n", kBadges[fe->selection[GroupBadge]].key);
egg += line;
for (int p = 0; p < extra_count; ++p)
for (int p = 0; p < pilot_count; ++p)
{
sprintf(line, "[%s]\n", extras[p].address);
sprintf(line, "[%s]\n", pilots[p].address);
egg += line;
egg += "hostType=0\n";
egg += "dropzone=one\n";
sprintf(line, "name=%s\n", extras[p].name);
sprintf(line, "name=%s\n", pilots[p].name);
egg += line;
sprintf(line, "bitmapindex=%d\n", p + 2);
sprintf(line, "bitmapindex=%d\n", p + 1);
egg += line;
egg += "loadzones=1\n";
sprintf(line, "vehicle=%s\n", extras[p].vehicle);
sprintf(line, "vehicle=%s\n", pilots[p].vehicle);
egg += line;
sprintf(line, "color=%s\n", extras[p].color);
sprintf(line, "color=%s\n", pilots[p].color);
egg += line;
sprintf(line, "badge=%s\n", extras[p].badge);
sprintf(line, "badge=%s\n", pilots[p].badge);
egg += line;
if (football)
{
sprintf(line, "team=team::%s\n", kTeams[teams_used[pilots[p].team]].key);
egg += line;
sprintf(line, "position=%s\n", pilots[p].position);
egg += line;
}
}
egg += "[largebitmap]\n";
sprintf(line, "bitmap=BitMap::Large::%s\n", fe->pilotName);
egg += line;
for (int p = 0; p < extra_count; ++p)
//
// Football team blocks (console RPFootballMission layout)
//
if (football)
{
sprintf(line, "bitmap=BitMap::Large::%s\n", extras[p].name);
Logical team_present[2] = { False, False };
for (int p = 0; p < pilot_count; ++p)
{
team_present[pilots[p].team] = True;
}
std::string teams_list = "[teams]\n";
std::string team_sections, pilots_sections;
std::string teambitmap_list = "[teambitmap]\n";
std::string teambitmap_sections;
int bitmap_index = 1;
for (int side = 0; side < 2; ++side)
{
if (!team_present[side])
{
continue;
}
const char *team_key = kTeams[teams_used[side]].key;
sprintf(line, "team=team::%s\n", team_key);
teams_list += line;
sprintf(line, "[team::%s]\nbitmapindex=%d\npilots=pilots::%s\n",
team_key, bitmap_index++, team_key);
team_sections += line;
sprintf(line, "[pilots::%s]\n", team_key);
pilots_sections += line;
for (int p = 0; p < pilot_count; ++p)
{
if (pilots[p].team == side)
{
sprintf(line, "pilot=%s\n", pilots[p].address);
pilots_sections += line;
}
}
sprintf(line, "bitmap=BitMap::Small::team::%s\n", team_key);
teambitmap_list += line;
sprintf(line, "[BitMap::Small::team::%s]\n", team_key);
teambitmap_sections += line;
AppendNameBitmap(teambitmap_sections, 64, 16, team_key);
teambitmap_sections += "width=4\n";
}
egg += teams_list;
egg += team_sections;
egg += pilots_sections;
egg += teambitmap_list;
egg += teambitmap_sections;
}
//
// Name bitmaps + ordinals
//
egg += "[largebitmap]\n";
for (int p = 0; p < pilot_count; ++p)
{
sprintf(line, "bitmap=BitMap::Large::%s\n", pilots[p].name);
egg += line;
}
egg += "[smallbitmap]\n";
sprintf(line, "bitmap=BitMap::Small::%s\n", fe->pilotName);
egg += line;
for (int p = 0; p < extra_count; ++p)
for (int p = 0; p < pilot_count; ++p)
{
sprintf(line, "bitmap=BitMap::Small::%s\n", extras[p].name);
sprintf(line, "bitmap=BitMap::Small::%s\n", pilots[p].name);
egg += line;
}
sprintf(line, "[BitMap::Large::%s]\n", fe->pilotName);
egg += line;
AppendNameBitmap(egg, 128, 32, fe->pilotName);
egg += "width=8\n";
sprintf(line, "[BitMap::Small::%s]\n", fe->pilotName);
egg += line;
AppendNameBitmap(egg, 64, 16, fe->pilotName);
egg += "width=4\n";
for (int p = 0; p < extra_count; ++p)
for (int p = 0; p < pilot_count; ++p)
{
sprintf(line, "[BitMap::Large::%s]\n", extras[p].name);
sprintf(line, "[BitMap::Large::%s]\n", pilots[p].name);
egg += line;
AppendNameBitmap(egg, 128, 32, extras[p].name);
AppendNameBitmap(egg, 128, 32, pilots[p].name);
egg += "width=8\n";
sprintf(line, "[BitMap::Small::%s]\n", extras[p].name);
sprintf(line, "[BitMap::Small::%s]\n", pilots[p].name);
egg += line;
AppendNameBitmap(egg, 64, 16, extras[p].name);
AppendNameBitmap(egg, 64, 16, pilots[p].name);
egg += "width=4\n";
}
@@ -775,13 +1011,13 @@ namespace
// [pilots]-order names for the network console's results
strcpy(gLastPilotNamesCsv, fe->pilotName);
for (int p = 0; p < extra_count; ++p)
for (int p = 1; p < pilot_count; ++p)
{
if (strlen(gLastPilotNamesCsv) + strlen(extras[p].name) + 2 <
if (strlen(gLastPilotNamesCsv) + strlen(pilots[p].name) + 2 <
sizeof(gLastPilotNamesCsv))
{
strcat(gLastPilotNamesCsv, ",");
strcat(gLastPilotNamesCsv, extras[p].name);
strcat(gLastPilotNamesCsv, pilots[p].name);
}
}
}