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

+10
View File
@@ -238,8 +238,18 @@ void Chat::command(Game& game, const std::string& text)
system("/color <colour>: Changes your colour. Colours: Red, Blue, Yellow, Green, Orange, Purple, Cyan, Pink, " system("/color <colour>: Changes your colour. Colours: Red, Blue, Yellow, Green, Orange, Purple, Cyan, Pink, "
"Lime, Teal, Magenta, Sky, Peach, Lavender, Brown, Silver."); "Lime, Teal, Magenta, Sky, Peach, Lavender, Brown, Silver.");
system("/kick \"<player>\" \"<reason>\": Removes a player from the lobby (host only)."); system("/kick \"<player>\" \"<reason>\": Removes a player from the lobby (host only).");
system("/pvp: Turns PvP on or off in the sandbox.");
return; return;
} }
if (name == "/pvp")
{
if (game.sim.match.state != MATCH_PRACTICE)
return system("PvP can only be changed in the sandbox.");
// Sent through this screen's controls like the menu's toggle, so every machine sees it on the same tick.
bool isPvp = !game.local_get().isPvp;
game.input.edit_send(game.devices[game.local], edit_encode({.type = EDIT_PVP, .value = isPvp}));
return system(isPvp ? "PvP is now on." : "PvP is now off.");
}
if (name == "/kick") if (name == "/kick")
{ {
if (words.size() < 2) if (words.size() < 2)
+27 -13
View File
@@ -140,14 +140,17 @@ void Match::update(Sim& sim)
break; break;
case MATCH_COUNTDOWN: case MATCH_COUNTDOWN:
{ {
// Ready sticks once pressed, and bots always are. Once everyone is, the wait drops to its last second. // Ready sticks once pressed, and bots always are. Readying deals any slots still empty at random. Once
// Fighters choosing items pick them through their controls. // everyone is, the wait drops to its last second. Fighters choosing items pick them through their controls.
bool isEveryone = true; bool isEveryone = true;
for (int i = 0; i < sim.fighterCount; i++) for (int i = 0; i < sim.fighterCount; i++)
{ {
Fighter& fighter = sim.fighters[i]; Fighter& fighter = sim.fighters[i];
edit_take(sim, fighter); edit_take(sim, fighter);
bool wasReady = isReady[i];
isReady[i] |= fighter.isBot || fighter.is_down(CONTROL_READY); isReady[i] |= fighter.isBot || fighter.is_down(CONTROL_READY);
if (isReady[i] && !wasReady)
loadout_deal(sim, i);
isEveryone &= isReady[i]; isEveryone &= isReady[i];
} }
if (isEveryone) if (isEveryone)
@@ -421,20 +424,31 @@ void Match::mirror_copy(Sim& sim, bool isTrinket)
} }
} }
// The countdown is over: slots still empty after choosing are dealt at random (once, and shared, when mirrored), and // Deals a fighter's empty slots at random. A mirrored kind is dealt only to the first fighter, and copied to everyone.
// the menu closes. void Match::loadout_deal(Sim& sim, int index)
{
Fighter& fighter = sim.fighters[index];
for (int kind = 0; kind < 2; kind++)
{
bool isTrinket = kind == 1;
ItemRules& items = isTrinket ? rules.trinkets : rules.weapons;
if (items.isMirror && index > 0)
continue;
if (isTrinket)
picks_deal(sim.rng, fighter.trinketLoadout, fighter.trinketSlots, TRINKET_COUNT, items.isDuplicates);
else
picks_deal(sim.rng, fighter.weaponLoadout, fighter.weaponSlots, WEAPON_COUNT, items.isDuplicates);
if (items.isMirror)
mirror_copy(sim, isTrinket);
}
}
// The countdown is over: slots still empty after choosing (by anyone who never readied) are dealt at random, and the
// menu closes.
void Match::fight_start(Sim& sim) void Match::fight_start(Sim& sim)
{ {
for (int i = 0; i < sim.fighterCount; i++) for (int i = 0; i < sim.fighterCount; i++)
{ loadout_deal(sim, i);
Fighter& fighter = sim.fighters[i];
picks_deal(sim.rng, fighter.weaponLoadout, fighter.weaponSlots, WEAPON_COUNT, rules.weapons.isDuplicates);
picks_deal(sim.rng, fighter.trinketLoadout, fighter.trinketSlots, TRINKET_COUNT, rules.trinkets.isDuplicates);
if (i == 0 && rules.weapons.isMirror)
mirror_copy(sim, false);
if (i == 0 && rules.trinkets.isMirror)
mirror_copy(sim, true);
}
if (isWeaponPick || isTrinketPick) if (isWeaponPick || isTrinketPick)
sim.cues->choice_close(); sim.cues->choice_close();
isWeaponPick = false; isWeaponPick = false;
+1
View File
@@ -248,6 +248,7 @@ private:
void fight_start(Sim& sim); void fight_start(Sim& sim);
void edit_take(Sim& sim, Fighter& fighter); void edit_take(Sim& sim, Fighter& fighter);
void mirror_copy(Sim& sim, bool isTrinket); void mirror_copy(Sim& sim, bool isTrinket);
void loadout_deal(Sim& sim, int fighter);
void deathmatch_update(Sim& sim); void deathmatch_update(Sim& sim);
void race_update(Sim& sim); void race_update(Sim& sim);
void queues_update(Sim& sim); void queues_update(Sim& sim);
+37 -53
View File
@@ -14,7 +14,8 @@ constexpr float RIVAL_BAR_MIN = 16;
constexpr float RIVAL_DIVIDER = 1; constexpr float RIVAL_DIVIDER = 1;
constexpr float RIVAL_GAP = 4; constexpr float RIVAL_GAP = 4;
constexpr float RIVAL_ICON_GAP = 2; 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 float SPECTATE_GAP = 4;
constexpr Control SPECTATE_CONTROLS[] = {CONTROL_LEFT, CONTROL_RIGHT, CONTROL_ATTACK}; constexpr Control SPECTATE_CONTROLS[] = {CONTROL_LEFT, CONTROL_RIGHT, CONTROL_ATTACK};
constexpr float HUD_IDLE_ALPHA = 0.5f; 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 // 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. // icons before the font, then leaving the icons out.
constexpr int RIVAL_ROWS_MAX = 2; 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_STYLES = 6;
constexpr int RIVAL_ICON_STYLES = 4; constexpr int RIVAL_ICON_STYLES = 4;
constexpr Font RIVAL_FONTS[RIVAL_STYLES] = {FONT_SMALL, FONT_SMALL, FONT_TINY, FONT_TINY, FONT_SMALL, FONT_TINY}; 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) if (game.menu.isOpen)
break; 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); text_center_draw(game, text, y, ROUND_SCALE);
y += game.text.line_height_get(FONT_SMALL, ROUND_SCALE) + READY_GAP; 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", 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. // 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) static CountdownInfo countdown_info_get(Game& game, bool isPad)
{ {
Match& match = game.sim.match; 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)); SDL_strlcpy(info.round, "Final Round", sizeof(info.round));
else else
SDL_snprintf(info.round, sizeof(info.round), "Round %d", match.round + 1); 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++) for (int i = 0; i < game.sim.fighterCount; i++)
info.ready += match.isReady[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]; Binding binding = game.input.bindings[CONTROL_READY];
game.input.binding_name_get(isPad ? binding.pad : binding.key, isPad, info.key, sizeof(info.key)); game.input.binding_name_get(isPad ? binding.pad : binding.key, isPad, info.key, sizeof(info.key));
return info; return info;
@@ -758,7 +765,6 @@ void Ui::countdown_aside_draw(Game& game, bool isPad)
{ {
CountdownInfo info = countdown_info_get(game, isPad); CountdownInfo info = countdown_info_get(game, isPad);
const char* round = info.round; const char* round = info.round;
int seconds = info.seconds;
int ready = info.ready; int ready = info.ready;
const char* key = game.sim.match.isReady[game.local] ? nullptr : info.key; const char* key = game.sim.match.isReady[game.local] ? nullptr : info.key;
float margin = (CANVAS_WIDTH - MENU_WIDTH) / 2; 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); 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 // Each line at the largest size that fits the margin.
? ASIDE_LABEL_SCALE auto scale_fit = [&](const char* line, float scale) {
: 1; for (; scale > 1; scale--)
float height = if (game.text.size_get(line, FONT_SMALL, scale).x <= margin - ASIDE_PADDING * 2)
game.text.line_height_get(FONT_SMALL, labelScale) + game.text.line_height_get(FONT_SMALL, ROUND_SCALE); 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; float y = middle - height / 2;
y += line_draw(round, margin / 2, y, labelScale); y += line_draw(round, margin / 2, y, labelScale);
line_draw(info.time, margin / 2, y, timeScale);
char text[TOOLTIP_LENGTH]; 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; float rightX = CANVAS_WIDTH - margin / 2;
y = middle - (key ? LINE_HEIGHT * 3 : LINE_HEIGHT) / 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 // Name, numbers at a level and description of a weapon or trinket. Trinkets say how many copies are equipped, and
// when they stack. // 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 tooltip;
tooltip.isTrinket = isTrinket; tooltip.isTrinket = isTrinket;
tooltip.item = item; 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* loadout = isTrinket ? fighter.trinketLoadout : fighter.weaponLoadout;
int slots = isTrinket ? fighter.trinketSlots : fighter.weaponSlots; int slots = isTrinket ? fighter.trinketSlots : fighter.weaponSlots;
tooltip.slot = std::find(loadout, loadout + slots, item) - loadout; 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 // 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 // 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 // or health (with its connection's bars, online). Without icons, the health number can go to make room. Full-size
// all fit, the icons shrink to small, then the font to tiny, then the icons to tiny, then the icons are left out. Each // icons stack no more than two rows deep across the windows. When they don't all fit, the icons shrink to small, then
// kind of icon wraps onto more rows past three across, and tiny icons wrap further if they still don't fit. If even // the font to tiny, then the icons to tiny, then the icons are left out. Each kind of icon wraps onto more rows past
// names and health alone don't fit, the windows split the column evenly and the names are cut short. // 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 Ui::rivals_layout(Game& game)
{ {
RivalLayout layout; 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); perRow = std::clamp((int)((HUD_COLUMN_WIDTH + RIVAL_GAP) / (width + RIVAL_GAP)), 1, layout.count);
int cardRows = (layout.count + perRow - 1) / perRow; int cardRows = (layout.count + perRow - 1) / perRow;
perRow = (layout.count + cardRows - 1) / cardRows; 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; 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 // 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 // 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, // weapon in hand lit. Each is laid over the game as one piece, half see-through (half again for a fighter out of the
// see-through for a fighter out of the round, and fades while the mouse is over it. Hovering an icon shows the item's // round), and fades by half again while the mouse is over it.
// tooltip, as that fighter has it, in its colour.
void Ui::rivals_draw(Game& game) void Ui::rivals_draw(Game& game)
{ {
Match& match = game.sim.match; Match& match = game.sim.match;
@@ -2372,15 +2383,12 @@ void Ui::rivals_draw(Game& game)
float inset = PANEL_BORDER + RIVAL_PADDING; float inset = PANEL_BORDER + RIVAL_PADDING;
float iconSize = ICON_PIXELS[layout.size]; float iconSize = ICON_PIXELS[layout.size];
float cell = iconSize + RIVAL_ICON_GAP; float cell = iconSize + RIVAL_ICON_GAP;
int hoveredFighter = -1;
int hoveredKind = 0;
int hoveredSlot = 0;
for (int k = 0; k < layout.count; k++) for (int k = 0; k < layout.count; k++)
{ {
int index = layout.fighters[k]; int index = layout.fighters[k];
Fighter& fighter = game.sim.fighters[index]; Fighter& fighter = game.sim.fighters[index];
Rect card = layout.cards[k]; 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 color = game.color_get(index);
Color dim = color_shade(color, SECONDARY_SHADE); Color dim = color_shade(color, SECONDARY_SHADE);
renderer.layer_begin(); renderer.layer_begin();
@@ -2446,21 +2454,13 @@ void Ui::rivals_draw(Game& game)
int* loadout = isTrinket ? fighter.trinketLoadout : fighter.weaponLoadout; int* loadout = isTrinket ? fighter.trinketLoadout : fighter.weaponLoadout;
for (int slot = 0; slot < slots; slot++) 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]; int item = loadout[slot];
if (item < 0) if (item < 0)
continue; continue;
bool isLit = isTrinket || slot == fighter.slot; bool isLit = isTrinket || slot == fighter.slot;
icon_draw(game, isTrinket, item, Vec2 centre = {x + slot % layout.columns[kind] * cell + iconSize / 2,
{.color = isLit ? color : dim, .position = {box.x + box.w / 2, box.y + box.h / 2}}, y + slot / layout.columns[kind] * cell + iconSize / 2};
layout.size); icon_draw(game, isTrinket, item, {.color = isLit ? color : dim, .position = centre}, layout.size);
if (is_point_in_rect(game.input.mouse, box))
{
hoveredFighter = index;
hoveredKind = kind;
hoveredSlot = slot;
}
} }
x += layout.columns[kind] * cell; x += layout.columns[kind] * cell;
if (kind == 0) if (kind == 0)
@@ -2471,22 +2471,6 @@ void Ui::rivals_draw(Game& game)
} }
renderer.layer_end(base * hover_alpha_get(rivalHovers[index])); 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 // Choosing items with several players on this screen: a panel each below top (side by side for two, a 2x2 grid for
+2 -2
View File
@@ -79,7 +79,7 @@ struct RivalLayout
struct CountdownInfo struct CountdownInfo
{ {
char round[64] = {}; char round[64] = {};
int seconds = 0; char time[16] = {};
int ready = 0; int ready = 0;
char key[64] = {}; char key[64] = {};
}; };
@@ -198,7 +198,7 @@ public:
void draw(Game& game); void draw(Game& game);
void slot_draw(Game& game, Slot slot); void slot_draw(Game& game, Slot slot);
void panel_draw(Renderer& renderer, Rect rect, Color border, float fillAlpha = -1, float stroke = PANEL_BORDER); void panel_draw(Renderer& renderer, Rect rect, Color border, float fillAlpha = -1, float stroke = PANEL_BORDER);
Tooltip tooltip_build(Game& game, bool isTrinket, int item, int level, int owner = -1); Tooltip tooltip_build(Game& game, bool isTrinket, int item, int level);
void stats_table_build(Fighter& fighter, int weapon, int level, bool isPreview, Tooltip& tooltip); void stats_table_build(Fighter& fighter, int weapon, int level, bool isPreview, Tooltip& tooltip);
void table_row_draw(Game& game, Tooltip& tooltip, int row, Vec2 corner, const float* columns, Font font, void table_row_draw(Game& game, Tooltip& tooltip, int row, Vec2 corner, const float* columns, Font font,
float alpha); float alpha);