Deal items on ready, /pvp, and calmer rival windows

- Readying in a countdown deals any empty slots at random right away,
  and the countdown reads "Starting..." once everyone is ready.
- /pvp toggles PvP in the sandbox, offline or in a lobby.
- Rival windows sit at half opacity (half again when knocked out, and
  when hovered), show no item tooltips, and drop to small icons past
  two full-size rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shweetandClaude Opus 5.5 committed 2026-11-08 16:57:11 -05:00
1 parent 6f112ab231
commit 5d6ebf2bba
5 files changed
+77 -68

No files matched your search

+37 -53
View File
@@ -14,7 +14,8 @@ constexpr float RIVAL_BAR_MIN = 16;
constexpr float RIVAL_DIVIDER = 1;
constexpr float RIVAL_GAP = 4;
constexpr float RIVAL_ICON_GAP = 2;
constexpr float RIVAL_OUT_ALPHA = 0.25f;
constexpr float RIVAL_ALPHA = 0.5f;
constexpr float RIVAL_OUT_SHARE = 0.5f;
constexpr float SPECTATE_GAP = 4;
constexpr Control SPECTATE_CONTROLS[] = {CONTROL_LEFT, CONTROL_RIGHT, CONTROL_ATTACK};
constexpr float HUD_IDLE_ALPHA = 0.5f;
@@ -25,6 +26,8 @@ constexpr int RIVAL_COLUMNS_MAX = 3;
// What the other fighters' windows try, from largest to smallest, until they all fit in a couple of rows: shrinking
// icons before the font, then leaving the icons out.
constexpr int RIVAL_ROWS_MAX = 2;
// The most rows of full-size icons the windows can stack up between them before the icons shrink.
constexpr int RIVAL_FULL_ICON_ROWS_MAX = 2;
constexpr int RIVAL_STYLES = 6;
constexpr int RIVAL_ICON_STYLES = 4;
constexpr Font RIVAL_FONTS[RIVAL_STYLES] = {FONT_SMALL, FONT_SMALL, FONT_TINY, FONT_TINY, FONT_SMALL, FONT_TINY};
@@ -393,7 +396,7 @@ void Ui::match_draw(Game& game)
if (game.menu.isOpen)
break;
SDL_snprintf(text, sizeof(text), "%s %d", info.round, info.seconds);
SDL_snprintf(text, sizeof(text), "%s %s", info.round, info.time);
text_center_draw(game, text, y, ROUND_SCALE);
y += game.text.line_height_get(FONT_SMALL, ROUND_SCALE) + READY_GAP;
SDL_snprintf(text, sizeof(text), isLocalReady ? "%d/%d Ready" : "%d/%d Ready - Press %s to Ready Up",
@@ -731,7 +734,8 @@ void Ui::loadout_draw(Game& game, float alpha, float top)
}
// Text centred across the canvas with its top at y.
// What the countdown shows: the round's name, whole seconds left, how many are ready, and the key that readies up.
// What the countdown shows: the round's name, whole seconds left (or Starting... once everyone's ready), how many are
// ready, and the key that readies up.
static CountdownInfo countdown_info_get(Game& game, bool isPad)
{
Match& match = game.sim.match;
@@ -742,9 +746,12 @@ static CountdownInfo countdown_info_get(Game& game, bool isPad)
SDL_strlcpy(info.round, "Final Round", sizeof(info.round));
else
SDL_snprintf(info.round, sizeof(info.round), "Round %d", match.round + 1);
info.seconds = (int)std::ceil(match.timer);
for (int i = 0; i < game.sim.fighterCount; i++)
info.ready += match.isReady[i];
if (info.ready == game.sim.fighterCount)
SDL_strlcpy(info.time, "Starting...", sizeof(info.time));
else
SDL_snprintf(info.time, sizeof(info.time), "%d", (int)std::ceil(match.timer));
Binding binding = game.input.bindings[CONTROL_READY];
game.input.binding_name_get(isPad ? binding.pad : binding.key, isPad, info.key, sizeof(info.key));
return info;
@@ -758,7 +765,6 @@ void Ui::countdown_aside_draw(Game& game, bool isPad)
{
CountdownInfo info = countdown_info_get(game, isPad);
const char* round = info.round;
int seconds = info.seconds;
int ready = info.ready;
const char* key = game.sim.match.isReady[game.local] ? nullptr : info.key;
float margin = (CANVAS_WIDTH - MENU_WIDTH) / 2;
@@ -771,16 +777,20 @@ void Ui::countdown_aside_draw(Game& game, bool isPad)
return game.text.line_height_get(FONT_SMALL, scale);
};
float labelScale = game.text.size_get(round, FONT_SMALL, ASIDE_LABEL_SCALE).x <= margin - ASIDE_PADDING * 2
? ASIDE_LABEL_SCALE
: 1;
float height =
game.text.line_height_get(FONT_SMALL, labelScale) + game.text.line_height_get(FONT_SMALL, ROUND_SCALE);
// Each line at the largest size that fits the margin.
auto scale_fit = [&](const char* line, float scale) {
for (; scale > 1; scale--)
if (game.text.size_get(line, FONT_SMALL, scale).x <= margin - ASIDE_PADDING * 2)
break;
return scale;
};
float labelScale = scale_fit(round, ASIDE_LABEL_SCALE);
float timeScale = scale_fit(info.time, ROUND_SCALE);
float height = game.text.line_height_get(FONT_SMALL, labelScale) + game.text.line_height_get(FONT_SMALL, timeScale);
float y = middle - height / 2;
y += line_draw(round, margin / 2, y, labelScale);
line_draw(info.time, margin / 2, y, timeScale);
char text[TOOLTIP_LENGTH];
SDL_snprintf(text, sizeof(text), "%d", seconds);
line_draw(text, margin / 2, y, ROUND_SCALE);
float rightX = CANVAS_WIDTH - margin / 2;
y = middle - (key ? LINE_HEIGHT * 3 : LINE_HEIGHT) / 2;
@@ -1177,12 +1187,12 @@ static void effects_preview_get(Game& game, const char* now, const char* next, c
// Name, numbers at a level and description of a weapon or trinket. Trinkets say how many copies are equipped, and
// when they stack.
Tooltip Ui::tooltip_build(Game& game, bool isTrinket, int item, int level, int owner)
Tooltip Ui::tooltip_build(Game& game, bool isTrinket, int item, int level)
{
Tooltip tooltip;
tooltip.isTrinket = isTrinket;
tooltip.item = item;
Fighter& fighter = owner >= 0 ? game.sim.fighters[owner] : game.local_get();
Fighter& fighter = game.local_get();
int* loadout = isTrinket ? fighter.trinketLoadout : fighter.weaponLoadout;
int slots = isTrinket ? fighter.trinketSlots : fighter.weaponSlots;
tooltip.slot = std::find(loadout, loadout + slots, item) - loadout;
@@ -2271,10 +2281,11 @@ static float rival_ping_get(Game& game, int fighter)
// Lays out the other fighters' windows in the HUD's right column, from the bottom up to the readouts above, in no more
// than two rows: as many to a row as fit across (the rows as even as they go). A window is as wide as its icons, name
// or health (with its connection's bars, online). Without icons, the health number can go to make room. When they don't
// all fit, the icons shrink to small, then the font to tiny, then the icons to tiny, then the icons are left out. Each
// kind of icon wraps onto more rows past three across, and tiny icons wrap further if they still don't fit. If even
// names and health alone don't fit, the windows split the column evenly and the names are cut short.
// or health (with its connection's bars, online). Without icons, the health number can go to make room. Full-size
// icons stack no more than two rows deep across the windows. When they don't all fit, the icons shrink to small, then
// the font to tiny, then the icons to tiny, then the icons are left out. Each kind of icon wraps onto more rows past
// three across, and tiny icons wrap further if they still don't fit. If even names and health alone don't fit, the
// windows split the column evenly and the names are cut short.
RivalLayout Ui::rivals_layout(Game& game)
{
RivalLayout layout;
@@ -2336,7 +2347,8 @@ RivalLayout Ui::rivals_layout(Game& game)
perRow = std::clamp((int)((HUD_COLUMN_WIDTH + RIVAL_GAP) / (width + RIVAL_GAP)), 1, layout.count);
int cardRows = (layout.count + perRow - 1) / perRow;
perRow = (layout.count + cardRows - 1) / cardRows;
isFit = width <= HUD_COLUMN_WIDTH && cardRows <= RIVAL_ROWS_MAX &&
bool isFullTall = layout.size == ICON_FULL && cardRows * layout.rows > RIVAL_FULL_ICON_ROWS_MAX;
isFit = width <= HUD_COLUMN_WIDTH && cardRows <= RIVAL_ROWS_MAX && !isFullTall &&
cardRows * (height + RIVAL_GAP) - RIVAL_GAP <= room;
}
}
@@ -2359,9 +2371,8 @@ RivalLayout Ui::rivals_layout(Game& game)
// Every other fighter's window in a match, bordered in its colour: its name centred (cut short if it doesn't fit), its
// health (online with its connection's bars after), then (with room) its weapons, a divider, and its trinkets, the
// weapon in hand lit. Each is laid over the game as one piece,
// see-through for a fighter out of the round, and fades while the mouse is over it. Hovering an icon shows the item's
// tooltip, as that fighter has it, in its colour.
// weapon in hand lit. Each is laid over the game as one piece, half see-through (half again for a fighter out of the
// round), and fades by half again while the mouse is over it.
void Ui::rivals_draw(Game& game)
{
Match& match = game.sim.match;
@@ -2372,15 +2383,12 @@ void Ui::rivals_draw(Game& game)
float inset = PANEL_BORDER + RIVAL_PADDING;
float iconSize = ICON_PIXELS[layout.size];
float cell = iconSize + RIVAL_ICON_GAP;
int hoveredFighter = -1;
int hoveredKind = 0;
int hoveredSlot = 0;
for (int k = 0; k < layout.count; k++)
{
int index = layout.fighters[k];
Fighter& fighter = game.sim.fighters[index];
Rect card = layout.cards[k];
float base = fighter.isAlive || match.is_racing() || index == spectated ? 1 : RIVAL_OUT_ALPHA;
float base = RIVAL_ALPHA * (fighter.isAlive || match.is_racing() || index == spectated ? 1 : RIVAL_OUT_SHARE);
Color color = game.color_get(index);
Color dim = color_shade(color, SECONDARY_SHADE);
renderer.layer_begin();
@@ -2446,21 +2454,13 @@ void Ui::rivals_draw(Game& game)
int* loadout = isTrinket ? fighter.trinketLoadout : fighter.weaponLoadout;
for (int slot = 0; slot < slots; slot++)
{
Rect box = {x + slot % layout.columns[kind] * cell, y + slot / layout.columns[kind] * cell, iconSize,
iconSize};
int item = loadout[slot];
if (item < 0)
continue;
bool isLit = isTrinket || slot == fighter.slot;
icon_draw(game, isTrinket, item,
{.color = isLit ? color : dim, .position = {box.x + box.w / 2, box.y + box.h / 2}},
layout.size);
if (is_point_in_rect(game.input.mouse, box))
{
hoveredFighter = index;
hoveredKind = kind;
hoveredSlot = slot;
}
Vec2 centre = {x + slot % layout.columns[kind] * cell + iconSize / 2,
y + slot / layout.columns[kind] * cell + iconSize / 2};
icon_draw(game, isTrinket, item, {.color = isLit ? color : dim, .position = centre}, layout.size);
}
x += layout.columns[kind] * cell;
if (kind == 0)
@@ -2471,22 +2471,6 @@ void Ui::rivals_draw(Game& game)
}
renderer.layer_end(base * hover_alpha_get(rivalHovers[index]));
}
if (hoveredFighter < 0 || game.menu.isOpen)
return;
Fighter& fighter = game.sim.fighters[hoveredFighter];
bool isTrinket = hoveredKind == 1;
int item = (isTrinket ? fighter.trinketLoadout : fighter.weaponLoadout)[hoveredSlot];
int level = (isTrinket ? fighter.trinketSlotLevels : fighter.weaponSlotLevels)[hoveredSlot];
Tooltip tooltip = tooltip_build(game, isTrinket, item, level, hoveredFighter);
tooltip.slot = hoveredSlot;
// The tooltip takes that fighter's colour for the moment, so it's clear whose item it is.
Color colors[2] = {tint, shade};
tint = game.color_get(hoveredFighter);
shade = color_shade(tint, SECONDARY_SHADE);
tooltip_draw(game, tooltip);
tint = colors[0];
shade = colors[1];
}
// Choosing items with several players on this screen: a panel each below top (side by side for two, a 2x2 grid for