Football: every player picks their own team and position
Closes the gap left by the football commit. Members publish their picks as lobby member data (tm/ps) alongside their loadout, and the owner publishes the scenario (sc) so members know when the picks matter. The lobby room turns into a team sheet: each member row shows their team and position, and MY TEAM / MY POSITION buttons cycle the local pick and republish it live, so everyone watches the sides fill out before the host launches. The egg builder honors explicit picks and only fills gaps: - pilots who chose a team get it; the rest are spread across the host team and one other so the sides stay balanced - a team with no volunteer runner promotes its first UNPICKED member, never overriding someone who chose crusher or blocker - if two pilots on a team both claim runner, the first keeps it and the second lines up - colors stay derived: runner in the team runner color, everyone else in the team color Verified: a three-pilot egg puts the host on crusher in team color with an unpicked teammate promoted to runner in runner color, the other side gets its own runner, and the teams/pilots blocks group correctly; the lobby room cycles picks and repaints the roster; both scenarios still pass their single-player regressions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+216
-37
@@ -250,6 +250,8 @@ namespace
|
|||||||
char vehicle[24];
|
char vehicle[24];
|
||||||
char color[16];
|
char color[16];
|
||||||
char badge[24];
|
char badge[24];
|
||||||
|
char team[32]; // football pick, "" = assign for them
|
||||||
|
char position[16];
|
||||||
};
|
};
|
||||||
|
|
||||||
// lobby-fed override (real personas + loadouts); empty = env parsing
|
// lobby-fed override (real personas + loadouts); empty = env parsing
|
||||||
@@ -273,6 +275,8 @@ namespace
|
|||||||
gHostedPilots[i].color[0] ? gHostedPilots[i].color : "Red");
|
gHostedPilots[i].color[0] ? gHostedPilots[i].color : "Red");
|
||||||
strcpy(extras[i].badge,
|
strcpy(extras[i].badge,
|
||||||
gHostedPilots[i].badge[0] ? gHostedPilots[i].badge : "None");
|
gHostedPilots[i].badge[0] ? gHostedPilots[i].badge : "None");
|
||||||
|
strcpy(extras[i].team, gHostedPilots[i].team);
|
||||||
|
strcpy(extras[i].position, gHostedPilots[i].position);
|
||||||
}
|
}
|
||||||
return gHostedPilotCount;
|
return gHostedPilotCount;
|
||||||
}
|
}
|
||||||
@@ -324,6 +328,7 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
ExtraPilot *pilot = &extras[extra_count];
|
ExtraPilot *pilot = &extras[extra_count];
|
||||||
|
memset(pilot, 0, sizeof(*pilot));
|
||||||
sprintf(pilot->address, "%s:%d", entry, console_port + 1);
|
sprintf(pilot->address, "%s:%d", entry, console_port + 1);
|
||||||
sprintf(pilot->name, "PILOT %d", extra_count + 2);
|
sprintf(pilot->name, "PILOT %d", extra_count + 2);
|
||||||
strcpy(pilot->vehicle, kVehicles[
|
strcpy(pilot->vehicle, kVehicles[
|
||||||
@@ -797,6 +802,7 @@ namespace
|
|||||||
const char *badge;
|
const char *badge;
|
||||||
int team; // kTeams index, -1 outside football
|
int team; // kTeams index, -1 outside football
|
||||||
const char *position;
|
const char *position;
|
||||||
|
Logical chosePosition; // picked it themselves
|
||||||
};
|
};
|
||||||
PilotEntry pilots[maxExtraPilots + 1];
|
PilotEntry pilots[maxExtraPilots + 1];
|
||||||
int pilot_count = 0;
|
int pilot_count = 0;
|
||||||
@@ -809,6 +815,7 @@ namespace
|
|||||||
owner->badge = kBadges[fe->selection[GroupBadge]].key;
|
owner->badge = kBadges[fe->selection[GroupBadge]].key;
|
||||||
owner->team = -1;
|
owner->team = -1;
|
||||||
owner->position = NULL;
|
owner->position = NULL;
|
||||||
|
owner->chosePosition = False;
|
||||||
for (int p = 0; p < extra_count; ++p)
|
for (int p = 0; p < extra_count; ++p)
|
||||||
{
|
{
|
||||||
PilotEntry *pilot = &pilots[pilot_count++];
|
PilotEntry *pilot = &pilots[pilot_count++];
|
||||||
@@ -819,47 +826,121 @@ namespace
|
|||||||
pilot->badge = extras[p].badge;
|
pilot->badge = extras[p].badge;
|
||||||
pilot->team = -1;
|
pilot->team = -1;
|
||||||
pilot->position = NULL;
|
pilot->position = NULL;
|
||||||
|
pilot->chosePosition = False;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Football: assign teams and positions, derive colors
|
// Football: every pilot's own team/position pick is honored;
|
||||||
|
// unset picks get filled in, then each team is normalized to
|
||||||
|
// exactly one runner (the console's rule).
|
||||||
//
|
//
|
||||||
int teams_used[2] = { 0, 1 };
|
|
||||||
if (football)
|
if (football)
|
||||||
{
|
{
|
||||||
teams_used[0] = fe->selection[GroupTeam];
|
//
|
||||||
teams_used[1] = (teams_used[0] + 1) % FE_COUNT(kTeams);
|
// 1. Teams: the host's pick, then each member's, then
|
||||||
|
// alternate whoever did not choose across the two
|
||||||
int runner_on_team[2] = { 0, 0 };
|
// most-used teams so the sides stay balanced.
|
||||||
int line_position[2] = { 0, 0 }; // crusher/blocker rotation
|
//
|
||||||
|
owner->team = fe->selection[GroupTeam];
|
||||||
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)
|
for (int p = 1; p < pilot_count; ++p)
|
||||||
{
|
{
|
||||||
int side = p % 2; // alternate: B, A, B, A...
|
for (int t = 0; t < FE_COUNT(kTeams); ++t)
|
||||||
side = (side == 1) ? 1 : 0;
|
|
||||||
pilots[p].team = side;
|
|
||||||
if (!runner_on_team[side])
|
|
||||||
{
|
{
|
||||||
pilots[p].position = "runner";
|
if (strcmp(extras[p - 1].team, kTeams[t].key) == 0)
|
||||||
runner_on_team[side] = 1;
|
{
|
||||||
}
|
pilots[p].team = t;
|
||||||
else
|
break;
|
||||||
{
|
}
|
||||||
pilots[p].position =
|
|
||||||
(line_position[side]++ % 2) ? "blocker" : "crusher";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int fallback_team = (owner->team + 1) % FE_COUNT(kTeams);
|
||||||
|
int unassigned = 0;
|
||||||
|
for (int p = 1; p < pilot_count; ++p)
|
||||||
|
{
|
||||||
|
if (pilots[p].team < 0)
|
||||||
|
{
|
||||||
|
pilots[p].team = (unassigned++ % 2) ? owner->team : fallback_team;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 2. Positions: honor each pick, default the rest to the
|
||||||
|
// line, then give every team exactly one runner.
|
||||||
|
//
|
||||||
|
for (int p = 1; p < pilot_count; ++p)
|
||||||
|
{
|
||||||
|
const char *pick = extras[p - 1].position;
|
||||||
|
for (int i = 0; i < FE_COUNT(kPositions); ++i)
|
||||||
|
{
|
||||||
|
if (strcmp(pick, kPositions[i].key) == 0)
|
||||||
|
{
|
||||||
|
pilots[p].position = kPositions[i].key;
|
||||||
|
pilots[p].chosePosition = True;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
owner->position = kPositions[fe->selection[GroupPosition]].key;
|
||||||
|
owner->chosePosition = True;
|
||||||
|
|
||||||
|
for (int t = 0; t < FE_COUNT(kTeams); ++t)
|
||||||
|
{
|
||||||
|
int members = 0, runner = -1, spare = -1, line = 0;
|
||||||
|
for (int p = 0; p < pilot_count; ++p)
|
||||||
|
{
|
||||||
|
if (pilots[p].team != t)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++members;
|
||||||
|
if (spare < 0 && !pilots[p].chosePosition)
|
||||||
|
{
|
||||||
|
spare = p; // nobody's plans to disturb
|
||||||
|
}
|
||||||
|
if (pilots[p].position != NULL &&
|
||||||
|
strcmp(pilots[p].position, "runner") == 0)
|
||||||
|
{
|
||||||
|
if (runner < 0)
|
||||||
|
{
|
||||||
|
runner = p; // first runner keeps the ball
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// two pilots claimed it - the later one lines up
|
||||||
|
pilots[p].position = NULL;
|
||||||
|
pilots[p].chosePosition = False;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (members == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (runner < 0 && spare >= 0)
|
||||||
|
{
|
||||||
|
// no volunteer: the first pilot without a pick of
|
||||||
|
// their own runs (explicit picks are never overridden)
|
||||||
|
pilots[spare].position = "runner";
|
||||||
|
runner = spare;
|
||||||
|
}
|
||||||
|
for (int p = 0; p < pilot_count; ++p)
|
||||||
|
{
|
||||||
|
if (pilots[p].team == t && p != runner &&
|
||||||
|
pilots[p].position == NULL)
|
||||||
|
{
|
||||||
|
pilots[p].position = (line++ % 2) ? "crusher" : "blocker";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 3. Colors follow team and position (runner wears the
|
||||||
|
// team's runner color - that is how you spot the ball)
|
||||||
|
//
|
||||||
for (int p = 0; p < pilot_count; ++p)
|
for (int p = 0; p < pilot_count; ++p)
|
||||||
{
|
{
|
||||||
const TeamEntry *team = &kTeams[teams_used[pilots[p].team]];
|
const TeamEntry *team = &kTeams[pilots[p].team];
|
||||||
pilots[p].color =
|
pilots[p].color =
|
||||||
(strcmp(pilots[p].position, "runner") == 0)
|
(strcmp(pilots[p].position, "runner") == 0)
|
||||||
? team->runnerColor : team->teamColor;
|
? team->runnerColor : team->teamColor;
|
||||||
@@ -921,7 +1002,7 @@ namespace
|
|||||||
egg += line;
|
egg += line;
|
||||||
if (football)
|
if (football)
|
||||||
{
|
{
|
||||||
sprintf(line, "team=team::%s\n", kTeams[teams_used[pilots[p].team]].key);
|
sprintf(line, "team=team::%s\n", kTeams[pilots[p].team].key);
|
||||||
egg += line;
|
egg += line;
|
||||||
sprintf(line, "position=%s\n", pilots[p].position);
|
sprintf(line, "position=%s\n", pilots[p].position);
|
||||||
egg += line;
|
egg += line;
|
||||||
@@ -933,24 +1014,26 @@ namespace
|
|||||||
//
|
//
|
||||||
if (football)
|
if (football)
|
||||||
{
|
{
|
||||||
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 teams_list = "[teams]\n";
|
||||||
std::string team_sections, pilots_sections;
|
std::string team_sections, pilots_sections;
|
||||||
std::string teambitmap_list = "[teambitmap]\n";
|
std::string teambitmap_list = "[teambitmap]\n";
|
||||||
std::string teambitmap_sections;
|
std::string teambitmap_sections;
|
||||||
int bitmap_index = 1;
|
int bitmap_index = 1;
|
||||||
for (int side = 0; side < 2; ++side)
|
for (int side = 0; side < FE_COUNT(kTeams); ++side)
|
||||||
{
|
{
|
||||||
if (!team_present[side])
|
Logical team_present = False;
|
||||||
|
for (int p = 0; p < pilot_count; ++p)
|
||||||
|
{
|
||||||
|
if (pilots[p].team == side)
|
||||||
|
{
|
||||||
|
team_present = True;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!team_present)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const char *team_key = kTeams[teams_used[side]].key;
|
const char *team_key = kTeams[side].key;
|
||||||
sprintf(line, "team=team::%s\n", team_key);
|
sprintf(line, "team=team::%s\n", team_key);
|
||||||
teams_list += line;
|
teams_list += line;
|
||||||
sprintf(line, "[team::%s]\nbitmapindex=%d\npilots=pilots::%s\n",
|
sprintf(line, "[team::%s]\nbitmapindex=%d\npilots=pilots::%s\n",
|
||||||
@@ -1297,6 +1380,102 @@ void
|
|||||||
strcpy(badge, kBadges[b].key);
|
strcpy(badge, kBadges[b].key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//########################################################################
|
||||||
|
// Football team / position picks (the lobby publishes and cycles these)
|
||||||
|
//########################################################################
|
||||||
|
|
||||||
|
int
|
||||||
|
RPL4FrontEnd_TeamCount()
|
||||||
|
{
|
||||||
|
return FE_COUNT(kTeams);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_TeamName(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= FE_COUNT(kTeams))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return kTeams[index].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_TeamKey(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= FE_COUNT(kTeams))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return kTeams[index].key;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
RPL4FrontEnd_PositionCount()
|
||||||
|
{
|
||||||
|
return FE_COUNT(kPositions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_PositionName(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= FE_COUNT(kPositions))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return kPositions[index].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_PositionKey(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= FE_COUNT(kPositions))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return kPositions[index].key;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
RPL4FrontEnd_GetTeamIndex()
|
||||||
|
{
|
||||||
|
return gHavePersist ? gPersistSelection[GroupTeam] : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RPL4FrontEnd_SetTeamIndex(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= FE_COUNT(kTeams))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gPersistSelection[GroupTeam] = index;
|
||||||
|
gHavePersist = True;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
RPL4FrontEnd_GetPositionIndex()
|
||||||
|
{
|
||||||
|
return gHavePersist ? gPersistSelection[GroupPosition] : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
RPL4FrontEnd_SetPositionIndex(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= FE_COUNT(kPositions))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gPersistSelection[GroupPosition] = index;
|
||||||
|
gHavePersist = True;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logical
|
||||||
|
RPL4FrontEnd_IsFootballSelected()
|
||||||
|
{
|
||||||
|
return gHavePersist && IsFootball(gPersistSelection);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
RPL4FrontEnd_SetHostedPilots(
|
RPL4FrontEnd_SetHostedPilots(
|
||||||
const char *owner_address,
|
const char *owner_address,
|
||||||
|
|||||||
@@ -50,6 +50,39 @@ int
|
|||||||
void
|
void
|
||||||
RPL4FrontEnd_GetLoadout(char *vehicle, char *color, char *badge);
|
RPL4FrontEnd_GetLoadout(char *vehicle, char *color, char *badge);
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Football team / position: the catalogs, and this player's picks.
|
||||||
|
// The lobby publishes the picks as member data and lets members
|
||||||
|
// cycle them in the room; the host's egg builder honors them.
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
int
|
||||||
|
RPL4FrontEnd_TeamCount();
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_TeamName(int index);
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_TeamKey(int index);
|
||||||
|
|
||||||
|
int
|
||||||
|
RPL4FrontEnd_PositionCount();
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_PositionName(int index);
|
||||||
|
const char *
|
||||||
|
RPL4FrontEnd_PositionKey(int index);
|
||||||
|
|
||||||
|
int
|
||||||
|
RPL4FrontEnd_GetTeamIndex();
|
||||||
|
void
|
||||||
|
RPL4FrontEnd_SetTeamIndex(int index);
|
||||||
|
int
|
||||||
|
RPL4FrontEnd_GetPositionIndex();
|
||||||
|
void
|
||||||
|
RPL4FrontEnd_SetPositionIndex(int index);
|
||||||
|
|
||||||
|
// True when this player's setup menu has Football selected (the host's
|
||||||
|
// pick decides the mission; members use it to know what to show).
|
||||||
|
Logical
|
||||||
|
RPL4FrontEnd_IsFootballSelected();
|
||||||
|
|
||||||
// Hosted-race pilots fed by the Steam lobby: overrides the
|
// Hosted-race pilots fed by the Steam lobby: overrides the
|
||||||
// RP412HOSTPODS parsing with real personas and loadouts. owner_address
|
// RP412HOSTPODS parsing with real personas and loadouts. owner_address
|
||||||
// is this pod's mesh IP (the FakeIP); count 0 clears the override.
|
// is this pod's mesh IP (the FakeIP); count 0 clears the override.
|
||||||
@@ -60,6 +93,9 @@ struct FEHostedPilot
|
|||||||
char vehicle[24];
|
char vehicle[24];
|
||||||
char color[16];
|
char color[16];
|
||||||
char badge[24];
|
char badge[24];
|
||||||
|
// football: the member's own picks, empty = assign one for them
|
||||||
|
char team[32]; // team key ("Red/Pink", ...)
|
||||||
|
char position[16]; // "runner" / "crusher" / "blocker"
|
||||||
};
|
};
|
||||||
void
|
void
|
||||||
RPL4FrontEnd_SetHostedPilots(
|
RPL4FrontEnd_SetHostedPilots(
|
||||||
|
|||||||
+117
-7
@@ -45,6 +45,7 @@ namespace
|
|||||||
int gLastGoNonce = 0; // launches we already answered
|
int gLastGoNonce = 0; // launches we already answered
|
||||||
int gShownResultsNonce = 0; // score sheets we already displayed
|
int gShownResultsNonce = 0; // score sheets we already displayed
|
||||||
const char kResultsKey[] = "res";
|
const char kResultsKey[] = "res";
|
||||||
|
const char kScenarioKey[] = "sc";
|
||||||
|
|
||||||
// async call-result plumbing
|
// async call-result plumbing
|
||||||
Logical gCallDone = False;
|
Logical gCallDone = False;
|
||||||
@@ -120,6 +121,12 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logical IsOwner()
|
||||||
|
{
|
||||||
|
return gInLobby &&
|
||||||
|
SteamMatchmaking()->GetLobbyOwner(gLobby) == SteamUser()->GetSteamID();
|
||||||
|
}
|
||||||
|
|
||||||
void PublishMemberData()
|
void PublishMemberData()
|
||||||
{
|
{
|
||||||
char value[64];
|
char value[64];
|
||||||
@@ -140,12 +147,30 @@ namespace
|
|||||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "vh", vehicle);
|
SteamMatchmaking()->SetLobbyMemberData(gLobby, "vh", vehicle);
|
||||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "cl", color);
|
SteamMatchmaking()->SetLobbyMemberData(gLobby, "cl", color);
|
||||||
SteamMatchmaking()->SetLobbyMemberData(gLobby, "bd", badge);
|
SteamMatchmaking()->SetLobbyMemberData(gLobby, "bd", badge);
|
||||||
|
|
||||||
|
// football: this member's own team and position pick
|
||||||
|
SteamMatchmaking()->SetLobbyMemberData(gLobby, "tm",
|
||||||
|
RPL4FrontEnd_TeamKey(RPL4FrontEnd_GetTeamIndex()));
|
||||||
|
SteamMatchmaking()->SetLobbyMemberData(gLobby, "ps",
|
||||||
|
RPL4FrontEnd_PositionKey(RPL4FrontEnd_GetPositionIndex()));
|
||||||
|
|
||||||
|
// the owner also publishes the scenario, so members know
|
||||||
|
// whether their team/position pick matters
|
||||||
|
if (IsOwner())
|
||||||
|
{
|
||||||
|
SteamMatchmaking()->SetLobbyData(gLobby, kScenarioKey,
|
||||||
|
RPL4FrontEnd_IsFootballSelected() ? "football" : "race");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logical IsOwner()
|
Logical FootballLobby()
|
||||||
{
|
{
|
||||||
return gInLobby &&
|
if (!gInLobby)
|
||||||
SteamMatchmaking()->GetLobbyOwner(gLobby) == SteamUser()->GetSteamID();
|
{
|
||||||
|
return False;
|
||||||
|
}
|
||||||
|
const char *scenario = SteamMatchmaking()->GetLobbyData(gLobby, kScenarioKey);
|
||||||
|
return (scenario != NULL && strcmp(scenario, "football") == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
@@ -162,6 +187,8 @@ namespace
|
|||||||
char vehicle[24];
|
char vehicle[24];
|
||||||
char color[16];
|
char color[16];
|
||||||
char badge[24];
|
char badge[24];
|
||||||
|
char team[32]; // football pick
|
||||||
|
char position[16];
|
||||||
Logical published;
|
Logical published;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -196,6 +223,12 @@ namespace
|
|||||||
strncpy(member->badge,
|
strncpy(member->badge,
|
||||||
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "bd"),
|
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "bd"),
|
||||||
sizeof(member->badge) - 1);
|
sizeof(member->badge) - 1);
|
||||||
|
strncpy(member->team,
|
||||||
|
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "tm"),
|
||||||
|
sizeof(member->team) - 1);
|
||||||
|
strncpy(member->position,
|
||||||
|
SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "ps"),
|
||||||
|
sizeof(member->position) - 1);
|
||||||
member->published =
|
member->published =
|
||||||
member->ip[0] != '\0' && member->consolePort > 0 && member->gamePort > 0;
|
member->ip[0] != '\0' && member->consolePort > 0 && member->gamePort > 0;
|
||||||
}
|
}
|
||||||
@@ -241,6 +274,8 @@ namespace
|
|||||||
strncpy(pilot->vehicle, members[i].vehicle, sizeof(pilot->vehicle) - 1);
|
strncpy(pilot->vehicle, members[i].vehicle, sizeof(pilot->vehicle) - 1);
|
||||||
strncpy(pilot->color, members[i].color, sizeof(pilot->color) - 1);
|
strncpy(pilot->color, members[i].color, sizeof(pilot->color) - 1);
|
||||||
strncpy(pilot->badge, members[i].badge, sizeof(pilot->badge) - 1);
|
strncpy(pilot->badge, members[i].badge, sizeof(pilot->badge) - 1);
|
||||||
|
strncpy(pilot->team, members[i].team, sizeof(pilot->team) - 1);
|
||||||
|
strncpy(pilot->position, members[i].position, sizeof(pilot->position) - 1);
|
||||||
|
|
||||||
if (pods[0] != '\0')
|
if (pods[0] != '\0')
|
||||||
{
|
{
|
||||||
@@ -274,8 +309,11 @@ namespace
|
|||||||
HFONT titleFont;
|
HFONT titleFont;
|
||||||
RECT launchRect; // owner only
|
RECT launchRect; // owner only
|
||||||
RECT leaveRect;
|
RECT leaveRect;
|
||||||
|
RECT teamRect; // football: cycle my team
|
||||||
|
RECT positionRect; // football: cycle my position
|
||||||
Logical launchClicked;
|
Logical launchClicked;
|
||||||
Logical leaveClicked;
|
Logical leaveClicked;
|
||||||
|
Logical pickChanged;
|
||||||
Logical closed;
|
Logical closed;
|
||||||
MemberInfo members[kMaxLobbyMembers];
|
MemberInfo members[kMaxLobbyMembers];
|
||||||
int memberCount;
|
int memberCount;
|
||||||
@@ -303,8 +341,9 @@ namespace
|
|||||||
RECT title = client;
|
RECT title = client;
|
||||||
title.top = client.bottom / 28;
|
title.top = client.bottom / 28;
|
||||||
title.bottom = title.top + client.bottom / 10;
|
title.bottom = title.top + client.bottom / 10;
|
||||||
DrawTextA(mem, "STEAM RACE LOBBY", -1, &title,
|
DrawTextA(mem,
|
||||||
DT_CENTER | DT_TOP | DT_SINGLELINE);
|
FootballLobby() ? "STEAM FOOTBALL LOBBY" : "STEAM RACE LOBBY",
|
||||||
|
-1, &title, DT_CENTER | DT_TOP | DT_SINGLELINE);
|
||||||
|
|
||||||
SelectObject(mem, room->textFont);
|
SelectObject(mem, room->textFont);
|
||||||
int row_h = client.bottom / 16;
|
int row_h = client.bottom / 16;
|
||||||
@@ -328,7 +367,25 @@ namespace
|
|||||||
is_owner_row ? " [HOST]" : "");
|
is_owner_row ? " [HOST]" : "");
|
||||||
DrawTextA(mem, text, -1, &row, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
DrawTextA(mem, text, -1, &row, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||||
|
|
||||||
if (member->vehicle[0] != '\0')
|
if (FootballLobby())
|
||||||
|
{
|
||||||
|
// football: the roster is a team sheet
|
||||||
|
if (member->team[0] != '\0')
|
||||||
|
{
|
||||||
|
const char *position_name = "?";
|
||||||
|
for (int i = 0; i < RPL4FrontEnd_PositionCount(); ++i)
|
||||||
|
{
|
||||||
|
if (strcmp(member->position, RPL4FrontEnd_PositionKey(i)) == 0)
|
||||||
|
{
|
||||||
|
position_name = RPL4FrontEnd_PositionName(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sprintf(text, "%s - %s", member->team, position_name);
|
||||||
|
DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (member->vehicle[0] != '\0')
|
||||||
{
|
{
|
||||||
sprintf(text, "%s / %s", member->vehicle, member->color);
|
sprintf(text, "%s / %s", member->vehicle, member->color);
|
||||||
DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
|
DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
|
||||||
@@ -347,11 +404,30 @@ namespace
|
|||||||
-1, &hint, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
-1, &hint, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||||
|
|
||||||
HBRUSH frame = CreateSolidBrush(kGreenBright);
|
HBRUSH frame = CreateSolidBrush(kGreenBright);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Football: my own team and position, click to cycle
|
||||||
|
//
|
||||||
|
if (FootballLobby())
|
||||||
|
{
|
||||||
|
FrameRect(mem, &room->teamRect, frame);
|
||||||
|
SetTextColor(mem, kGreenBright);
|
||||||
|
sprintf(text, "MY TEAM: %s",
|
||||||
|
RPL4FrontEnd_TeamName(RPL4FrontEnd_GetTeamIndex()));
|
||||||
|
DrawTextA(mem, text, -1, &room->teamRect,
|
||||||
|
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||||
|
|
||||||
|
FrameRect(mem, &room->positionRect, frame);
|
||||||
|
sprintf(text, "MY POSITION: %s",
|
||||||
|
RPL4FrontEnd_PositionName(RPL4FrontEnd_GetPositionIndex()));
|
||||||
|
DrawTextA(mem, text, -1, &room->positionRect,
|
||||||
|
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||||
|
}
|
||||||
if (IsOwner())
|
if (IsOwner())
|
||||||
{
|
{
|
||||||
FrameRect(mem, &room->launchRect, frame);
|
FrameRect(mem, &room->launchRect, frame);
|
||||||
SetTextColor(mem, kGreenBright);
|
SetTextColor(mem, kGreenBright);
|
||||||
DrawTextA(mem, "L A U N C H R A C E", -1, &room->launchRect,
|
DrawTextA(mem, "L A U N C H G A M E", -1, &room->launchRect,
|
||||||
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||||
}
|
}
|
||||||
FrameRect(mem, &room->leaveRect, frame);
|
FrameRect(mem, &room->leaveRect, frame);
|
||||||
@@ -395,6 +471,18 @@ namespace
|
|||||||
{
|
{
|
||||||
room->leaveClicked = True;
|
room->leaveClicked = True;
|
||||||
}
|
}
|
||||||
|
else if (FootballLobby() && PtInRect(&room->teamRect, point))
|
||||||
|
{
|
||||||
|
RPL4FrontEnd_SetTeamIndex(
|
||||||
|
(RPL4FrontEnd_GetTeamIndex() + 1) % RPL4FrontEnd_TeamCount());
|
||||||
|
room->pickChanged = True;
|
||||||
|
}
|
||||||
|
else if (FootballLobby() && PtInRect(&room->positionRect, point))
|
||||||
|
{
|
||||||
|
RPL4FrontEnd_SetPositionIndex(
|
||||||
|
(RPL4FrontEnd_GetPositionIndex() + 1) % RPL4FrontEnd_PositionCount());
|
||||||
|
room->pickChanged = True;
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -465,6 +553,16 @@ namespace
|
|||||||
room.leaveRect.top = client.bottom - 3 * row_h;
|
room.leaveRect.top = client.bottom - 3 * row_h;
|
||||||
room.leaveRect.bottom = client.bottom - row_h;
|
room.leaveRect.bottom = client.bottom - row_h;
|
||||||
|
|
||||||
|
// football pick buttons, above the launch button
|
||||||
|
room.teamRect.left = (client.right - col_w) / 2;
|
||||||
|
room.teamRect.right = room.teamRect.left + col_w;
|
||||||
|
room.teamRect.top = client.bottom - 12 * row_h;
|
||||||
|
room.teamRect.bottom = room.teamRect.top + 2 * row_h;
|
||||||
|
room.positionRect.left = room.teamRect.left;
|
||||||
|
room.positionRect.right = room.teamRect.right;
|
||||||
|
room.positionRect.top = client.bottom - 9 * row_h;
|
||||||
|
room.positionRect.bottom = room.positionRect.top + 2 * row_h;
|
||||||
|
|
||||||
gRoom = &room;
|
gRoom = &room;
|
||||||
PublishMemberData();
|
PublishMemberData();
|
||||||
|
|
||||||
@@ -489,6 +587,18 @@ namespace
|
|||||||
outcome = LobbyRoomClosed;
|
outcome = LobbyRoomClosed;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
//
|
||||||
|
// A pick changed: republish so everyone's roster updates
|
||||||
|
//
|
||||||
|
if (room.pickChanged)
|
||||||
|
{
|
||||||
|
room.pickChanged = False;
|
||||||
|
PublishMemberData();
|
||||||
|
room.memberCount = CollectMembers(room.members);
|
||||||
|
InvalidateRect(room.window, NULL, FALSE);
|
||||||
|
RedrawWindow(room.window, NULL, NULL, RDW_UPDATENOW);
|
||||||
|
}
|
||||||
|
|
||||||
if (room.leaveClicked)
|
if (room.leaveClicked)
|
||||||
{
|
{
|
||||||
SteamMatchmaking()->LeaveLobby(gLobby);
|
SteamMatchmaking()->LeaveLobby(gLobby);
|
||||||
|
|||||||
@@ -166,10 +166,16 @@ library so a dev console tool and the game share one egg builder.
|
|||||||
`[teams]` / `[team::X]` / `[pilots::X]` / `[teambitmap]` blocks in
|
`[teams]` / `[team::X]` / `[pilots::X]` / `[teambitmap]` blocks in
|
||||||
RPFootballMission's layout. Colors are derived, not chosen: the
|
RPFootballMission's layout. Colors are derived, not chosen: the
|
||||||
runner wears the team's runner color, everyone else the team color.
|
runner wears the team's runner color, everyone else the team color.
|
||||||
Multi-pod games alternate pilots between two teams and auto-assign
|
**Every player picks their own team and position** (2026-07-24):
|
||||||
one runner per team (the host's own position pick is honored).
|
members publish their picks as lobby member data (`tm`/`ps`), the
|
||||||
*Open:* lobby members cannot yet choose their own team/position —
|
room shows the roster as a team sheet and gives each member MY TEAM
|
||||||
the host's pick seeds a deterministic assignment.
|
/ MY POSITION buttons that cycle and republish live, and the owner
|
||||||
|
publishes the scenario (`sc`) so members know when those picks
|
||||||
|
matter. The egg builder honors every explicit pick and only fills
|
||||||
|
gaps: unpicked pilots are spread across the sides, a team with no
|
||||||
|
volunteer runner promotes its first *unpicked* member (explicit
|
||||||
|
picks are never overridden), and a second pilot claiming runner on
|
||||||
|
one team is moved to the line.
|
||||||
3. Results screen — **shipped**, on the cockpit displays, with the owner
|
3. Results screen — **shipped**, on the cockpit displays, with the owner
|
||||||
publishing the score sheet to lobby members.
|
publishing the score sheet to lobby members.
|
||||||
4. Steam model — lobby-owner-as-console **confirmed and verified** on
|
4. Steam model — lobby-owner-as-console **confirmed and verified** on
|
||||||
|
|||||||
Reference in New Issue
Block a user