Room and lobby chat, /host, /fashion, player-colored lines, context tips, and American spelling

- Chat goes to your room (the sandbox, or your game) by default, and to the whole lobby with /all or a leading !.
  Lines from elsewhere say where they came from ([Sandbox], [Game 2]), lobby-wide ones from your room say [All].
  Typing shows only to your room. Protocol 8.
- Hosting: everyone is told who hosts now, and the host can hand it over with /host "player".
- /fashion <item> wears a fashion locally, like /color.
- Lines that mention a player show in that player's color, looked up as they draw.
- Tips are tagged with the item and situations they fit, and those that fit are 1.5x as likely. They turn over every
  10 seconds on one continuous run, show under the menu too, and can be turned off with Tips in HUD / Gameplay. The
  HUD hotbars hide while the menu is up (except on its item tabs).
- American spelling throughout: color, center, gray, mustache, and so on.
This commit is contained in:
shweet committed 2026-11-10 19:37:19 -05:00
1 parent f4e7784f3c
commit 93cdf46d5a
38 files changed
+603 -325

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# The simulation must run identically on every platform (desktop and web play together), so floating point is kept # The simulation must run identically on every platform (desktop and web play together), so floating point is kept
# strict: no fusing multiplies and adds (GCC and arm64 Clang do by default), no fast-math reordering. The maths # strict: no fusing multiplies and adds (GCC and arm64 Clang do by default), no fast-math reordering. The math
# library's transcendentals are replaced by src/dmath.h. # library's transcendentals are replaced by src/dmath.h.
if(MSVC) if(MSVC)
add_compile_options(/fp:precise /fp:contract-) add_compile_options(/fp:precise /fp:contract-)
+1 -1
View File
@@ -76,7 +76,7 @@ Open the UDP range in the firewall (e.g. `ufw allow 47810:47899/udp`).
- The server runs every lobby: a sandbox everyone in the lobby shares, plus the games queued in it. Each is its own simulation. All of them run on one thread, at 60 ticks a second. - The server runs every lobby: a sandbox everyone in the lobby shares, plus the games queued in it. Each is its own simulation. All of them run on one thread, at 60 ticks a second.
- Clients send their inputs ahead of time. Each tick the server takes what has arrived (repeating a player's last inputs if they're late), steps the simulation, and sends everyone the inputs it used. Every 60 ticks it also sends a checksum. - Clients send their inputs ahead of time. Each tick the server takes what has arrived (repeating a player's last inputs if they're late), steps the simulation, and sends everyone the inputs it used. Every 60 ticks it also sends a checksum.
- Clients run the same simulation about 3 ticks ahead of the server, guessing other players' inputs. When the server's inputs differ from the guess, the client rolls back and replays. - Clients run the same simulation about 3 ticks ahead of the server, guessing other players' inputs. When the server's inputs differ from the guess, the client rolls back and replays.
- The simulation is deterministic on every platform (strict floating point, own maths functions), so desktop and web play together. - The simulation is deterministic on every platform (strict floating point, own math functions), so desktop and web play together.
- Clients connect over a WebSocket first (lobbies, chat, WebRTC signaling). Then they open a WebRTC connection to the server with two data channels: a reliable one, and an unreliable one for inputs and frames. If WebRTC can't connect, everything stays on the WebSocket. - Clients connect over a WebSocket first (lobbies, chat, WebRTC signaling). Then they open a WebRTC connection to the server with two data channels: a reliable one, and an unreliable one for inputs and frames. If WebRTC can't connect, everything stays on the WebSocket.
- STUN: `stun.l.google.com:19302`. - STUN: `stun.l.google.com:19302`.
- The server address is set in Settings. Default: `wss://shweetz.net/smack-a-brother/ws`. - The server address is set in Settings. Default: `wss://shweetz.net/smack-a-brother/ws`.
+2 -2
View File
@@ -10,7 +10,7 @@
{"name": "Hard Hat", "crop": [64, 0], "size": [32, 32], "pivot": [16, 18]}, {"name": "Hard Hat", "crop": [64, 0], "size": [32, 32], "pivot": [16, 18]},
{"name": "Bandana", "crop": [96, 0], "size": [32, 32], "pivot": [16, 14]}, {"name": "Bandana", "crop": [96, 0], "size": [32, 32], "pivot": [16, 14]},
{"name": "Bow", "crop": [128, 0], "size": [32, 32], "pivot": [14, 17]}, {"name": "Bow", "crop": [128, 0], "size": [32, 32], "pivot": [14, 17]},
{"name": "Moustache", "crop": [160, 0], "size": [32, 32], "pivot": [16, 13]}, {"name": "Mustache", "crop": [160, 0], "size": [32, 32], "pivot": [16, 13]},
{"name": "Eyepatch", "crop": [0, 32], "size": [32, 32], "pivot": [15, 16]}, {"name": "Eyepatch", "crop": [0, 32], "size": [32, 32], "pivot": [15, 16]},
{"name": "Monocle", "crop": [32, 32], "size": [32, 32], "pivot": [13, 13]}, {"name": "Monocle", "crop": [32, 32], "size": [32, 32], "pivot": [13, 13]},
{"name": "Top Hat", "crop": [64, 32], "size": [32, 32], "pivot": [16, 20]}, {"name": "Top Hat", "crop": [64, 32], "size": [32, 32], "pivot": [16, 20]},
@@ -43,7 +43,7 @@
"Hard Hat": {"layers": [{"layer": 0, "frames": [{"region": 2}]}]}, "Hard Hat": {"layers": [{"layer": 0, "frames": [{"region": 2}]}]},
"Bandana": {"layers": [{"layer": 1, "frames": [{"region": 3}]}]}, "Bandana": {"layers": [{"layer": 1, "frames": [{"region": 3}]}]},
"Bow": {"layers": [{"layer": 0, "frames": [{"region": 4}]}]}, "Bow": {"layers": [{"layer": 0, "frames": [{"region": 4}]}]},
"Moustache": {"layers": [{"layer": 1, "frames": [{"region": 5}]}]}, "Mustache": {"layers": [{"layer": 1, "frames": [{"region": 5}]}]},
"Eyepatch": {"layers": [{"layer": 1, "frames": [{"region": 6}]}]}, "Eyepatch": {"layers": [{"layer": 1, "frames": [{"region": 6}]}]},
"Monocle": {"layers": [{"layer": 1, "frames": [{"region": 7}]}]}, "Monocle": {"layers": [{"layer": 1, "frames": [{"region": 7}]}]},
"Top Hat": {"layers": [{"layer": 0, "frames": [{"region": 8}]}]}, "Top Hat": {"layers": [{"layer": 0, "frames": [{"region": 8}]}]},
+45 -12
View File
@@ -427,24 +427,56 @@ static void text_handle(int id, const std::string& text)
std::printf("Lobby %s: %d kicked %d\n", code.c_str(), id, (int)target); std::printf("Lobby %s: %d kicked %d\n", code.c_str(), id, (int)target);
return lobby_exit((int)target, LEAVE_KICKED, reason); return lobby_exit((int)target, LEAVE_KICKED, reason);
} }
if (type == "host")
{
// Only a lobby's host can hand hosting over, and only to someone else in their lobby, who moves to the front
// (the first member hosts).
int64_t target = 0;
std::string& code = members[id].code;
if (message["id"].get(target) || code.empty() || lobbies[code].members.front() != id || target == id)
return error_send(id, "Only the host can hand over hosting");
std::vector<int>& order = lobbies[code].members;
auto found = std::find(order.begin(), order.end(), (int)target);
if (found == order.end())
return error_send(id, "No such player in the lobby");
std::rotate(order.begin(), found, found + 1);
std::printf("Lobby %s: %d made %d host\n", code.c_str(), id, (int)target);
return lobby_send(code, "{\"type\":\"host_change\",\"host\":" + std::to_string(target) + "}");
}
error_send(id, "Unknown message"); error_send(id, "Unknown message");
} }
// Text a member sent for the lobby (a chat line or their new name), cleaned, passed to everyone with who it's from. // Sends to whoever a member's words reach: their room (everyone playing or watching where they are), or with
static void text_relay(int id, PacketType type, PacketReader& reader, size_t max) // CHAT_LOBBY (or no room yet), the whole lobby.
static void scope_send(int id, ChatScope scope, const std::string& bytes)
{ {
Member& member = members[id];
for (int other : lobbies[member.code].members)
if (scope == CHAT_LOBBY || member.room == 0 || members[other].room == member.room)
send(other, bytes, DELIVERY_RELIABLE);
}
// Text a member sent (a chat line or their new name), cleaned, passed on with who it's from: a new name to the whole
// lobby, and a chat line, with the room its sender is in, as far as its scope reaches.
static void text_relay(int id, PacketType type, PacketReader& reader)
{
Uint8 scope = CHAT_LOBBY;
Uint16 length = 0; Uint16 length = 0;
if (!reader.get(length) || reader.at + length > reader.bytes.size()) if ((type == PACKET_CHAT && !reader.get(scope)) || !reader.get(length) || reader.at + length > reader.bytes.size())
return; return;
std::string text = text_clean(std::string_view(reader.bytes.data() + reader.at, length), max); std::string text = text_clean(std::string_view(reader.bytes.data() + reader.at, length),
type == PACKET_CHAT ? CHAT_MESSAGE_MAX : PLAYER_NAME_MAX);
if (text.empty()) if (text.empty())
return; return;
if (type == PACKET_RENAME)
members[id].name = text;
PacketWriter packet; PacketWriter packet;
packet.put(type).put(id).put((Uint16)text.size()); packet.put(type).put(id);
if (type == PACKET_CHAT)
packet.put(scope).put(members[id].room);
else
members[id].name = text;
packet.put((Uint16)text.size());
packet.bytes += text; packet.bytes += text;
lobby_send(members[id].code, packet.bytes, DELIVERY_RELIABLE); scope_send(id, scope == CHAT_ROOM ? CHAT_ROOM : CHAT_LOBBY, packet.bytes);
} }
// The game messages, in binary (see protocol.h), from a member in a lobby. // The game messages, in binary (see protocol.h), from a member in a lobby.
@@ -507,12 +539,13 @@ static void packet_handle(int id, const std::string& bytes)
return; return;
} }
if (type == PACKET_CHAT || type == PACKET_RENAME) if (type == PACKET_CHAT || type == PACKET_RENAME)
return text_relay(id, type, reader, type == PACKET_CHAT ? CHAT_MESSAGE_MAX : PLAYER_NAME_MAX); return text_relay(id, type, reader);
if (type == PACKET_TYPING) if (type == PACKET_TYPING)
{ {
Uint8 isTyping = 0; Uint8 isTyping = 0;
// Only the room sees the "..." (a line said to all shows up elsewhere without one).
if (reader.get(isTyping)) if (reader.get(isTyping))
lobby_send(member.code, PacketWriter().put(PACKET_TYPING).put(id).put(isTyping).bytes, DELIVERY_RELIABLE); scope_send(id, CHAT_ROOM, PacketWriter().put(PACKET_TYPING).put(id).put(isTyping).bytes);
} }
if (type == PACKET_WATCH) if (type == PACKET_WATCH)
{ {
@@ -587,7 +620,7 @@ static void room_step(Room& room)
} }
// A sandbox's queued game that's ready starts in a room of its own: its players leave the sandbox (bringing nothing // A sandbox's queued game that's ready starts in a room of its own: its players leave the sandbox (bringing nothing
// along but their name and colour) and go into it. // along but their name and color) and go into it.
static void game_open(int sandboxSerial, int queuer) static void game_open(int sandboxSerial, int queuer)
{ {
Room& sandbox = rooms[sandboxSerial]; Room& sandbox = rooms[sandboxSerial];
@@ -666,7 +699,7 @@ static void game_follow(int serial)
} }
// Tells a lobby's members who's in each of its rooms (the sandbox, then its games): each room's mode and the players // Tells a lobby's members who's in each of its rooms (the sandbox, then its games): each room's mode and the players
// still in it with their colours, for the lobby page's lists. // still in it with their colors, for the lobby page's lists.
static void rooms_send(const std::string& code) static void rooms_send(const std::string& code)
{ {
std::vector<int>& lobbyRooms = lobbies[code].rooms; std::vector<int>& lobbyRooms = lobbies[code].rooms;
+2 -2
View File
@@ -311,7 +311,7 @@ int Bot::surface_below(Vec2 point, float above)
return best; return best;
} }
// Links between every pair of surfaces a fighter can travel between: neighbours in a row, falls off either end, // Links between every pair of surfaces a fighter can travel between: neighbors in a row, falls off either end,
// drops through platforms, jumps within a running jump's reach, and grapple pulls straight up to platforms higher // drops through platforms, jumps within a running jump's reach, and grapple pulls straight up to platforms higher
// than that. Jumps can't go up through solid ground. // than that. Jumps can't go up through solid ground.
void Bot::links_build() void Bot::links_build()
@@ -650,7 +650,7 @@ void Bot::move_steer(Sim& sim, Fighter& fighter, BotState& state, Controls& cont
aim = link->takeoff; aim = link->takeoff;
} }
// Walks and falls carry straight on past the takeoff towards the landing. // Walks and falls carry straight on past the takeoff toward the landing.
float dx = aim - x; float dx = aim - x;
bool isArrived = std::abs(dx) <= ARRIVE_DISTANCE; bool isArrived = std::abs(dx) <= ARRIVE_DISTANCE;
bool isCarried = link && (link->kind == LINK_FALL || link->kind == LINK_WALK); bool isCarried = link && (link->kind == LINK_FALL || link->kind == LINK_WALK);
+1 -1
View File
@@ -43,7 +43,7 @@ enum LinkKind
LINK_GRAPPLE LINK_GRAPPLE
}; };
// A walkable run of floor, platform or ramp. Left and right bound where a fighter's centre can be while standing on // A walkable run of floor, platform or ramp. Left and right bound where a fighter's center can be while standing on
// it. Y is where feet stand at the left end, rising by slope per pixel across, within yMin and yMax. Platforms can be // it. Y is where feet stand at the left end, rising by slope per pixel across, within yMin and yMax. Platforms can be
// dropped through. // dropped through.
struct Surface struct Surface
+72 -22
View File
@@ -5,7 +5,6 @@
constexpr int CHAT_LINES_MAX = 100; constexpr int CHAT_LINES_MAX = 100;
// No one: a fighter with no one to talk for it (a bot, or offline, anyone but this screen's). // No one: a fighter with no one to talk for it (a bot, or offline, anyone but this screen's).
constexpr int NO_MEMBER = -2;
constexpr int CHAT_SHOWN = 6; constexpr int CHAT_SHOWN = 6;
constexpr int CHAT_HISTORY = 12; constexpr int CHAT_HISTORY = 12;
constexpr float CHAT_LINGER = 10; constexpr float CHAT_LINGER = 10;
@@ -88,7 +87,7 @@ void Chat::players_clear()
recent.clear(); recent.clear();
} }
// In a lobby's sandbox, tells everyone whenever a player turns PvP on or off, in their colour. Someone just arrived // In a lobby's sandbox, tells everyone whenever a player turns PvP on or off, in their color. Someone just arrived
// is only noted. // is only noted.
void Chat::pvps_announce(Game& game) void Chat::pvps_announce(Game& game)
{ {
@@ -103,7 +102,7 @@ void Chat::pvps_announce(Game& game)
pvps[owner] = isPvp; pvps[owner] = isPvp;
if (isChanged) if (isChanged)
system(game.session.name_get(game, owner) + (isPvp ? " turned PvP on." : " turned PvP off."), system(game.session.name_get(game, owner) + (isPvp ? " turned PvP on." : " turned PvP off."),
game.color_get(i, TINT_PLAIN)); game.color_get(i, TINT_PLAIN), owner);
} }
} }
@@ -116,16 +115,29 @@ int Chat::member_get(Game& game, int fighter)
return fighter == game.local ? game.net.selfId : NO_MEMBER; return fighter == game.local ? game.net.selfId : NO_MEMBER;
} }
void Chat::system(const std::string& text, Color color) // The color lines about a lobby member take: their fighter's in the game this screen is in, else the one they have in
// the lobby, else the fallback. This screen's own player (offline too, under no member) takes its own.
Color Chat::member_color_get(Game& game, int member, Color fallback)
{ {
lines.push_back({.text = text, .color = color, .time = seconds_get()}); if (member == game.net.selfId)
return game.color_get(game.local, TINT_PLAIN);
int fighter = game.session.fighter_get(game, member);
if (fighter >= 0)
return game.color_get(fighter, TINT_PLAIN);
int color = game.session.color_get(member);
return color >= 0 ? PALETTE[color % COLOR_COUNT] : fallback;
}
void Chat::system(const std::string& text, Color color, int member)
{
lines.push_back({.text = text, .color = color, .time = seconds_get(), .member = member});
if ((int)lines.size() > CHAT_LINES_MAX) if ((int)lines.size() > CHAT_LINES_MAX)
lines.erase(lines.begin()); lines.erase(lines.begin());
} }
// A player's message: into the log in their colour, over their head, with a sound. Messages from a player coming too // A player's message: into the log in their color (marked with where it came from, see Session::place_tag_get), over
// fast (more than a handful in a few seconds) are dropped. // their head, with a sound. Messages from a player coming too fast (more than a handful in a few seconds) are dropped.
void Chat::say(Game& game, int member, const std::string& text) void Chat::say(Game& game, int member, const std::string& text, const std::string& tag)
{ {
constexpr int BURST = 5; constexpr int BURST = 5;
constexpr float BURST_SECONDS = 4; constexpr float BURST_SECONDS = 4;
@@ -142,9 +154,9 @@ void Chat::say(Game& game, int member, const std::string& text)
return; return;
bool isOnline = game.session.isActive; bool isOnline = game.session.isActive;
int fighter = isOnline ? game.session.fighter_get(game, member) : game.local; int fighter = isOnline ? game.session.fighter_get(game, member) : game.local;
Color color = fighter >= 0 ? game.color_get(fighter, TINT_PLAIN) : WHITE; Color color = member_color_get(game, member);
std::string name = isOnline ? game.session.name_get(game, member) : std::string(game.settings.name); std::string name = isOnline ? game.session.name_get(game, member) : std::string(game.settings.name);
lines.push_back({.text = name + ": " + text, .color = color, .time = now}); lines.push_back({.text = tag + name + ": " + text, .color = color, .time = now, .member = member});
if ((int)lines.size() > CHAT_LINES_MAX) if ((int)lines.size() > CHAT_LINES_MAX)
lines.erase(lines.begin()); lines.erase(lines.begin());
bubbles[member].push_back({.text = text, .time = now}); bubbles[member].push_back({.text = text, .time = now});
@@ -155,8 +167,8 @@ void Chat::say(Game& game, int member, const std::string& text)
// Enter sends and Escape drops it. Bubbles that have faded go. // Enter sends and Escape drops it. Bubbles that have faded go.
void Chat::update(Game& game) void Chat::update(Game& game)
{ {
for (std::string& notice : game.net.notices) for (NetNotice& notice : game.net.notices)
system(notice); system(notice.text, member_color_get(game, notice.member), notice.member < 0 ? NO_MEMBER : notice.member);
game.net.notices.clear(); game.net.notices.clear();
pvps_announce(game); pvps_announce(game);
Input& input = game.input; Input& input = game.input;
@@ -171,7 +183,7 @@ void Chat::update(Game& game)
draft += *c; draft += *c;
if (input.isSubmitted) if (input.isSubmitted)
{ {
if (!input.isCancelled) if (!input.isCanceled)
submit(game); submit(game);
close(game); close(game);
} }
@@ -204,10 +216,13 @@ void Chat::submit(Game& game)
std::string text = text_clean(draft, CHAT_MESSAGE_MAX); std::string text = text_clean(draft, CHAT_MESSAGE_MAX);
if (text.empty()) if (text.empty())
return; return;
// A message starting with ! goes to the whole lobby, like /all.
if (text[0] == '/') if (text[0] == '/')
command(game, text); command(game, text);
else if (text[0] == '!' && text.size() > 1)
game.session.chat_send(game, text_clean(text.substr(1), CHAT_MESSAGE_MAX), CHAT_LOBBY);
else else
game.session.chat_send(game, text); game.session.chat_send(game, text, CHAT_ROOM);
} }
// A command line split into words, where a "quoted part" counts as one (quotes taken off). // A command line split into words, where a "quoted part" counts as one (quotes taken off).
@@ -243,7 +258,7 @@ static std::vector<std::string> arguments_split(const std::string& text)
return words; return words;
} }
// /name <name> and /color <colour> change this player's (as Profile does), /kick "<name>" "<reason>" (the host // /name <name> and /color <color> change this player's (as Profile does), /kick "<name>" "<reason>" (the host
// only) puts a player out of the lobby, and /help lists them. Answers are system lines only this player sees. // only) puts a player out of the lobby, and /help lists them. Answers are system lines only this player sees.
void Chat::command(Game& game, const std::string& text) void Chat::command(Game& game, const std::string& text)
{ {
@@ -251,7 +266,7 @@ void Chat::command(Game& game, const std::string& text)
std::string name = words[0]; std::string name = words[0];
for (char& c : name) for (char& c : name)
c = (char)SDL_tolower(c); c = (char)SDL_tolower(c);
// A name or colour is the rest of the line (quotes around it are fine too). // A name or color is the rest of the line (quotes around it are fine too).
size_t space = text.find(' '); size_t space = text.find(' ');
std::string argument = space == std::string::npos ? "" : text_clean(text.substr(space + 1), PLAYER_NAME_MAX); std::string argument = space == std::string::npos ? "" : text_clean(text.substr(space + 1), PLAYER_NAME_MAX);
if (argument.size() >= 2 && argument.front() == '"' && argument.back() == '"') if (argument.size() >= 2 && argument.front() == '"' && argument.back() == '"')
@@ -259,9 +274,12 @@ void Chat::command(Game& game, const std::string& text)
if (name == "/help") if (name == "/help")
{ {
system("/name <new name>: Changes your name (once every 30 seconds in a lobby)."); system("/name <new name>: Changes your name (once every 30 seconds in a lobby).");
system("/color <colour>: Changes your colour. Colours: Red, Blue, Yellow, Green, Orange, Purple, Cyan, Pink, " system("/color <color>: Changes your color. Colors: 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("/all <message>: Says it to the whole lobby, not just where you are. Starting with ! does too.");
system("/fashion <item>: Wears an item from Profile's Fashion, or None.");
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("/host \"<player>\": Hands hosting the lobby to another player (host only).");
system("/pvp: Turns PvP on or off in the sandbox."); system("/pvp: Turns PvP on or off in the sandbox.");
return; return;
} }
@@ -276,6 +294,36 @@ void Chat::command(Game& game, const std::string& text)
system(isPvp ? "PvP is now on." : "PvP is now off."); system(isPvp ? "PvP is now on." : "PvP is now off.");
return; return;
} }
if (name == "/all")
{
std::string message = space == std::string::npos ? "" : text_clean(text.substr(space + 1), CHAT_MESSAGE_MAX);
if (message.empty())
return system("Usage: /all <message>");
game.session.chat_send(game, message, CHAT_LOBBY);
return;
}
if (name == "/host")
{
if (words.size() < 2)
return system("Usage: /host \"<player>\"");
game.session.host_give(game, words[1]);
return;
}
if (name == "/fashion")
{
for (int item = 0; item < FASHION_COUNT; item++)
{
if (SDL_strcasecmp(argument.c_str(), FASHION_DEFS[item].name) != 0)
continue;
game.settings.fashion = item;
game.settings.changed();
Color own = member_color_get(game, game.net.selfId);
return system(item == FASHION_NONE ? "You're no longer wearing anything."
: "You're now wearing the " + std::string(FASHION_DEFS[item].name) + ".",
own);
}
return system("Usage: /fashion <item>, as named in Profile's Fashion, or None.");
}
if (name == "/kick") if (name == "/kick")
{ {
if (words.size() < 2) if (words.size() < 2)
@@ -293,10 +341,10 @@ void Chat::command(Game& game, const std::string& text)
game.settings.changed(); game.settings.changed();
// Online, the session announces it to everyone (see Session::profile_watch). // Online, the session announces it to everyone (see Session::profile_watch).
if (!game.session.isActive) if (!game.session.isActive)
system("Your name is now " + argument + "."); system("Your name is now " + argument + ".", member_color_get(game, game.net.selfId));
return; return;
} }
if (name == "/color" || name == "/colour") if (name == "/color")
{ {
for (int color = 0; color < COLOR_COUNT; color++) for (int color = 0; color < COLOR_COUNT; color++)
{ {
@@ -308,10 +356,10 @@ void Chat::command(Game& game, const std::string& text)
return system(std::string(COLOR_NAMES[color]) + " is already taken by another player."); return system(std::string(COLOR_NAMES[color]) + " is already taken by another player.");
game.settings.color = color; game.settings.color = color;
game.settings.changed(); game.settings.changed();
system("Your colour is now " + std::string(COLOR_NAMES[color]) + "."); system("Your color is now " + std::string(COLOR_NAMES[color]) + ".", PALETTE[color]);
return; return;
} }
return system("Usage: /color <colour>. Type /help for the colour list."); return system("Usage: /color <color>. Type /help for the color list.");
} }
system("Unknown command. Type /help for a list of commands."); system("Unknown command. Type /help for a list of commands.");
} }
@@ -359,6 +407,8 @@ void Chat::draw(Game& game)
for (int i = (int)lines.size() - 1; i >= 0 && shown < limit; i--, shown++) for (int i = (int)lines.size() - 1; i >= 0 && shown < limit; i--, shown++)
{ {
ChatLine& line = lines[i]; ChatLine& line = lines[i];
// A line about a player shows in their color as it is now (once known, and kept once they've gone).
Color color = line.member == NO_MEMBER ? line.color : member_color_get(game, line.member, line.color);
float age = now - line.time; float age = now - line.time;
float alpha = isOpen ? 1 : std::clamp((CHAT_LINGER - age) / CHAT_FADE, 0.0f, 1.0f); float alpha = isOpen ? 1 : std::clamp((CHAT_LINGER - age) / CHAT_FADE, 0.0f, 1.0f);
if (alpha <= 0) if (alpha <= 0)
@@ -369,7 +419,7 @@ void Chat::draw(Game& game)
y -= lineHeight; y -= lineHeight;
renderer.rect_draw({area.x, y, area.w, lineHeight}, color_fade(BLACK, CHAT_BACKING * alpha)); renderer.rect_draw({area.x, y, area.w, lineHeight}, color_fade(BLACK, CHAT_BACKING * alpha));
game.text.draw(renderer, parts[part].c_str(), game.text.draw(renderer, parts[part].c_str(),
{.color = color_fade(line.color, alpha), .position = {area.x + CHAT_PADDING, y}}); {.color = color_fade(color, alpha), .position = {area.x + CHAT_PADDING, y}});
} }
} }
} }
+9 -3
View File
@@ -8,13 +8,18 @@
struct Game; struct Game;
// A line of the chat log: a player's message, in their colour, or a system one (someone joining, leaving or renaming, // A line of the chat log: a player's message, in their color, or a system one (someone joining, leaving or renaming,
// or a command's answer), in white. Time is when it came, in seconds. // or a command's answer), in white. Time is when it came, in seconds.
// No lobby member, for chat lines about nobody.
constexpr int NO_MEMBER = -2;
struct ChatLine struct ChatLine
{ {
std::string text; std::string text;
Color color = WHITE; Color color = WHITE;
float time = 0; float time = 0;
// The player it's about (see NO_MEMBER), whose color it shows in.
int member = NO_MEMBER;
}; };
// A message over a player's head: it rises in, hangs a while, then fades away. Time is when it came, in seconds. // A message over a player's head: it rises in, hangs a while, then fades away. Time is when it came, in seconds.
@@ -40,8 +45,9 @@ public:
std::map<int, bool> typing; std::map<int, bool> typing;
void update(Game& game); void update(Game& game);
void system(const std::string& text, Color color = WHITE); void system(const std::string& text, Color color = WHITE, int member = NO_MEMBER);
void say(Game& game, int member, const std::string& text); Color member_color_get(Game& game, int member, Color fallback = WHITE);
void say(Game& game, int member, const std::string& text, const std::string& tag);
void clear(); void clear();
void players_clear(); void players_clear();
void draw(Game& game); void draw(Game& game);
+3 -3
View File
@@ -135,7 +135,7 @@ constexpr int RNG_SHIFT_B = 17;
constexpr int RNG_SHIFT_C = 5; constexpr int RNG_SHIFT_C = 5;
constexpr Uint32 RNG_SEED = 0x9E3779B9; constexpr Uint32 RNG_SEED = 0x9E3779B9;
// Describes a spray of square particles, fanned around angle by spread. Fill is how far their colour takes over their // Describes a spray of square particles, fanned around angle by spread. Fill is how far their color takes over their
// black middle. // black middle.
struct Burst struct Burst
{ {
@@ -174,7 +174,7 @@ inline bool is_rect_overlapping(Rect a, Rect b)
return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h; return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
} }
// Darkens a colour by amount, keeping its alpha. // Darkens a color by amount, keeping its alpha.
inline Color color_shade(Color color, float amount) inline Color color_shade(Color color, float amount)
{ {
return {color.r * amount, color.g * amount, color.b * amount, color.a}; return {color.r * amount, color.g * amount, color.b * amount, color.a};
@@ -185,7 +185,7 @@ inline Color color_fade(Color color, float alpha)
return {color.r, color.g, color.b, color.a * alpha}; return {color.r, color.g, color.b, color.a * alpha};
} }
// A colour amount of the way from one to another. // A color amount of the way from one to another.
inline Color color_mix(Color from, Color to, float amount) inline Color color_mix(Color from, Color to, float amount)
{ {
return {from.r + (to.r - from.r) * amount, from.g + (to.g - from.g) * amount, from.b + (to.b - from.b) * amount, return {from.r + (to.r - from.r) * amount, from.g + (to.g - from.g) * amount, from.b + (to.b - from.b) * amount,
+2 -2
View File
@@ -4,13 +4,13 @@
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
// Deterministic maths: the same answer to the bit on every platform and compiler, so desktop and web builds simulate // Deterministic math: the same answer to the bit on every platform and compiler, so desktop and web builds simulate
// identically. The C library's sin, cos, atan2, pow and friends differ between platforms in their last bits, so these // identically. The C library's sin, cos, atan2, pow and friends differ between platforms in their last bits, so these
// replace them everywhere. They use only + - * / and operations IEEE 754 defines exactly (floor, frexp, ldexp), working // replace them everywhere. They use only + - * / and operations IEEE 754 defines exactly (floor, frexp, ldexp), working
// in double and rounding to float at the end. That relies on floats being evaluated as floats (no x87) and on nothing // in double and rounding to float at the end. That relies on floats being evaluated as floats (no x87) and on nothing
// being fused into a multiply-add, which the build turns off. // being fused into a multiply-add, which the build turns off.
#if FLT_EVAL_METHOD != 0 #if FLT_EVAL_METHOD != 0
#error "Deterministic maths needs float arithmetic evaluated at float precision (FLT_EVAL_METHOD 0)" #error "Deterministic math needs float arithmetic evaluated at float precision (FLT_EVAL_METHOD 0)"
#endif #endif
namespace dmath namespace dmath
+2 -2
View File
@@ -74,7 +74,7 @@ void Effects::afterimage_draw(Game& game)
} }
// Damage numbers are popups whose whole-number scale and shake both grow with the logarithm of the damage. // Damage numbers are popups whose whole-number scale and shake both grow with the logarithm of the damage.
// Numbers take the attacker's colour and start just above the hit (bigger ones higher), so they don't cover it. A hit // Numbers take the attacker's color and start just above the hit (bigger ones higher), so they don't cover it. A hit
// on a target whose last number from the same attacker appeared moments ago folds into it, respawning it with the // on a target whose last number from the same attacker appeared moments ago folds into it, respawning it with the
// total. // total.
void Effects::number_spawn(Vec2 position, int value, int target, Color color) void Effects::number_spawn(Vec2 position, int value, int target, Color color)
@@ -122,7 +122,7 @@ void Effects::flash_spawn(Flash flash)
effect_add(flashes, flash); effect_add(flashes, flash);
} }
// A particle at position, shrinking and fading as its life runs out: an outline in its colour around a core shaded by // A particle at position, shrinking and fading as its life runs out: an outline in its color around a core shaded by
// its fill. The outline is four strips so the fading core never shows it through. // its fill. The outline is four strips so the fading core never shows it through.
void particle_draw(Renderer& renderer, Particle& particle, Vec2 position) void particle_draw(Renderer& renderer, Particle& particle, Vec2 position)
{ {
+5 -5
View File
@@ -127,7 +127,7 @@ void Fighter::state_reset(Sim& sim, float maxHealth)
isAlive = true; isAlive = true;
} }
// The knock a hit gives whatever it lands on, centred at center: away from where it came from (and a little upward), at // The knock a hit gives whatever it lands on, centerd at center: away from where it came from (and a little upward), at
// a speed that grows with the damage, times the hit's own knock. // a speed that grows with the damage, times the hit's own knock.
Vec2 knock_get(Damage& damage, Vec2 center) Vec2 knock_get(Damage& damage, Vec2 center)
{ {
@@ -727,11 +727,11 @@ float Fighter::pound_area_get()
return stat_get(STAT_SLAM_AREA) * std::sqrt(impact); return stat_get(STAT_SLAM_AREA) * std::sqrt(impact);
} }
// Moves the fighter's middle to centre, keeping its momentum, without drawing it sliding across (teleports). // Moves the fighter's middle to center, keeping its momentum, without drawing it sliding across (teleports).
void Fighter::teleport_to(Vec2 centre) void Fighter::teleport_to(Vec2 center)
{ {
body.box.x = centre.x - body.box.w / 2; body.box.x = center.x - body.box.w / 2;
body.box.y = centre.y - body.box.h / 2; body.box.y = center.y - body.box.h / 2;
cornerPrevious = {body.box.x, body.box.y}; cornerPrevious = {body.box.x, body.box.y};
} }
+3 -3
View File
@@ -18,7 +18,7 @@ constexpr float BOUNCE_KEEP = 0.75f;
constexpr float SECONDARY_SHADE = 0.5f; constexpr float SECONDARY_SHADE = 0.5f;
constexpr int COLOR_COUNT = 23; constexpr int COLOR_COUNT = 23;
constexpr int COLOR_UNASSIGNED = -1; constexpr int COLOR_UNASSIGNED = -1;
// The colours fighters can wear, in the order fighters without one are given them. // The colors fighters can wear, in the order fighters without one are given them.
constexpr Color PALETTE[COLOR_COUNT] = { constexpr Color PALETTE[COLOR_COUNT] = {
{1, 0.35f, 0.35f, 1}, {0.4f, 0.6f, 1, 1}, {1, 0.9f, 0.3f, 1}, {0.4f, 1, 0.45f, 1}, {1, 0.35f, 0.35f, 1}, {0.4f, 0.6f, 1, 1}, {1, 0.9f, 0.3f, 1}, {0.4f, 1, 0.45f, 1},
{1, 0.6f, 0.2f, 1}, {0.7f, 0.45f, 1, 1}, {0.3f, 0.95f, 1, 1}, {1, 0.5f, 0.8f, 1}, {1, 0.6f, 0.2f, 1}, {0.7f, 0.45f, 1, 1}, {0.3f, 0.95f, 1, 1}, {1, 0.5f, 0.8f, 1},
@@ -47,7 +47,7 @@ constexpr FashionDef FASHION_DEFS[] = {
{"Hard Hat", "Doesn't work here..."}, {"Hard Hat", "Doesn't work here..."},
{"Bandana", "Infinite ammo...but\nyou already have that."}, {"Bandana", "Infinite ammo...but\nyou already have that."},
{"Bow", "Stereotypical, yet cute!"}, {"Bow", "Stereotypical, yet cute!"},
{"Moustache", "Clean shave."}, {"Mustache", "Clean shave."},
{"Eyepatch", "Depth perception not needed."}, {"Eyepatch", "Depth perception not needed."},
{"Monocle", "Tea time!"}, {"Monocle", "Tea time!"},
{"Top Hat", "Dashing."}, {"Top Hat", "Dashing."},
@@ -165,7 +165,7 @@ public:
void blend(float alpha); void blend(float alpha);
void impulse_add(Vec2 impulse); void impulse_add(Vec2 impulse);
void knock_take(Vec2 knock, int owner); void knock_take(Vec2 knock, int owner);
void teleport_to(Vec2 centre); void teleport_to(Vec2 center);
void burst_add(Vec2 impulse); void burst_add(Vec2 impulse);
void charge_cancel(Sim& sim); void charge_cancel(Sim& sim);
int weapon_get(); int weapon_get();
+2 -2
View File
@@ -20,7 +20,7 @@
constexpr float PVP_OFF_SHADE = 0.4f; constexpr float PVP_OFF_SHADE = 0.4f;
// How a fighter's colour is asked for: as it shows in the world (darkened when PvP keeps it out of this screen's // How a fighter's color is asked for: as it shows in the world (darkened when PvP keeps it out of this screen's
// fights), or plain, as the HUD shows it. // fights), or plain, as the HUD shows it.
enum Tint enum Tint
{ {
@@ -85,7 +85,7 @@ struct Game
return count; return count;
} }
// A fighter's colour: its match colour (handed out when the match starts), or in the sandbox this screen's own // A fighter's color: its match color (handed out when the match starts), or in the sandbox this screen's own
// (picked in Profile, white while unassigned) until others join, when everyone // (picked in Profile, white while unassigned) until others join, when everyone
// gets one. With others in the sandbox, everything of another fighter is darkened on this screen unless it and this // gets one. With others in the sandbox, everything of another fighter is darkened on this screen unless it and this
// screen's own both have PvP on (so they can hurt each other); this screen's own always shows in full. Asked for // screen's own both have PvP on (so they can hurt each other); this screen's own always shows in full. Asked for
+2 -2
View File
@@ -122,7 +122,7 @@ void Input::poll(Game& game)
isConfirmed = false; isConfirmed = false;
isMouseMoved = false; isMouseMoved = false;
isSubmitted = false; isSubmitted = false;
isCancelled = false; isCanceled = false;
for (Device& device : devices) for (Device& device : devices)
{ {
device.presses = 0; device.presses = 0;
@@ -219,7 +219,7 @@ void Input::poll(Game& game)
isConfirmed |= !isTyping && !event.key.repeat && scancode == SDL_SCANCODE_RETURN; isConfirmed |= !isTyping && !event.key.repeat && scancode == SDL_SCANCODE_RETURN;
erases += isTyping && scancode == SDL_SCANCODE_BACKSPACE; erases += isTyping && scancode == SDL_SCANCODE_BACKSPACE;
isSubmitted |= isTyping && (scancode == SDL_SCANCODE_RETURN || scancode == SDL_SCANCODE_ESCAPE); isSubmitted |= isTyping && (scancode == SDL_SCANCODE_RETURN || scancode == SDL_SCANCODE_ESCAPE);
isCancelled |= isTyping && scancode == SDL_SCANCODE_ESCAPE; isCanceled |= isTyping && scancode == SDL_SCANCODE_ESCAPE;
// A number key picks a slot, or with Shift held, a loadout to load. // A number key picks a slot, or with Shift held, a loadout to load.
if (!isTyping && scancode >= SDL_SCANCODE_1 && scancode < SDL_SCANCODE_1 + SLOT_KEY_COUNT) if (!isTyping && scancode >= SDL_SCANCODE_1 && scancode < SDL_SCANCODE_1 + SLOT_KEY_COUNT)
(event.key.mod & SDL_KMOD_SHIFT ? loadoutPressed : slotPressed) = scancode - SDL_SCANCODE_1; (event.key.mod & SDL_KMOD_SHIFT ? loadoutPressed : slotPressed) = scancode - SDL_SCANCODE_1;
+1 -1
View File
@@ -170,7 +170,7 @@ public:
bool isQuit = false; bool isQuit = false;
bool isSubmitted = false; bool isSubmitted = false;
// Whether the text being typed was given up on (Escape) rather than entered. // Whether the text being typed was given up on (Escape) rather than entered.
bool isCancelled = false; bool isCanceled = false;
Input(); Input();
void poll(Game& game); void poll(Game& game);
+3 -3
View File
@@ -18,7 +18,7 @@ constexpr SDL_WindowFlags WINDOW_FILL = 0;
constexpr float CAMERA_FOLLOW = 0.15f; constexpr float CAMERA_FOLLOW = 0.15f;
// Where the camera's corner wants to be: this screen's living fighters' middle (or the spectated fighter) in the // Where the camera's corner wants to be: this screen's living fighters' middle (or the spectated fighter) in the
// middle, clamped to the world or centred when the world is smaller. // middle, clamped to the world or centerd when the world is smaller.
static Vec2 camera_target_get(Game& game) static Vec2 camera_target_get(Game& game)
{ {
Vec2 world = game.sim.world.size_get(); Vec2 world = game.sim.world.size_get();
@@ -44,14 +44,14 @@ static Vec2 camera_target_get(Game& game)
return target; return target;
} }
// Eases the camera towards its target. // Eases the camera toward its target.
static void camera_update(Game& game) static void camera_update(Game& game)
{ {
game.camera = game.camera + (camera_target_get(game) - game.camera) * CAMERA_FOLLOW; game.camera = game.camera + (camera_target_get(game) - game.camera) * CAMERA_FOLLOW;
} }
// One tick of the game this screen shows, with what it asks of the match offline: the menu's Play, Escape (or B) // One tick of the game this screen shows, with what it asks of the match offline: the menu's Play, Escape (or B)
// leaving the results unless it's just closed the menu, and its player's colour. The menu is this screen's alone, so // leaving the results unless it's just closed the menu, and its player's color. The menu is this screen's alone, so
// it updates before the tick (see frame_run), not in it. // it updates before the tick (see frame_run), not in it.
void game_tick(Game& game) void game_tick(Game& game)
{ {
+4 -4
View File
@@ -216,7 +216,7 @@ void Match::start(Sim& sim)
tomatoes_clear(); tomatoes_clear();
// Offline, the fighters added beyond the sandbox's are bots. Online (a queued game), the bots are whoever no member // Offline, the fighters added beyond the sandbox's are bots. Online (a queued game), the bots are whoever no member
// plays, and everyone's colour was settled already. // plays, and everyone's color was settled already.
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];
@@ -276,7 +276,7 @@ void Match::sandbox_leave(Sim& sim, int index)
sim.cues->sound_play(SOUND_UNEQUIP); sim.cues->sound_play(SOUND_UNEQUIP);
} }
// Gives every fighter a colour of their own: the one they asked for if it's free (the earlier fighter keeps it if two // Gives every fighter a color of their own: the one they asked for if it's free (the earlier fighter keeps it if two
// ask for the same), then the rest in palette order. Only the first fighter asks for now (offline, this screen's), and // ask for the same), then the rest in palette order. Only the first fighter asks for now (offline, this screen's), and
// only it wears anything. // only it wears anything.
void Match::colors_assign(Sim& sim) void Match::colors_assign(Sim& sim)
@@ -815,7 +815,7 @@ void Match::leaves_take(Sim& sim)
sim.cues->sound_play(SOUND_FINISH); sim.cues->sound_play(SOUND_FINISH);
} }
// A fighter's loadout, slots and levels as a profile, to bring it back as it was (a name and colour are added by // A fighter's loadout, slots and levels as a profile, to bring it back as it was (a name and color are added by
// whoever knows them). // whoever knows them).
Profile profile_get(Fighter& fighter) Profile profile_get(Fighter& fighter)
{ {
@@ -832,7 +832,7 @@ Profile profile_get(Fighter& fighter)
return profile; return profile;
} }
// Online, a member's fighter appears, set up from their profile, in the colour they asked for if it's free. Returns // Online, a member's fighter appears, set up from their profile, in the color they asked for if it's free. Returns
// it, or -1 when the game is full. // it, or -1 when the game is full.
int Match::member_join(Sim& sim, int member, const Profile& profile) int Match::member_join(Sim& sim, int member, const Profile& profile)
{ {
+3 -3
View File
@@ -96,7 +96,7 @@ struct Rules
// A change a player makes to their own fighter from the menu, sent through its controls so that every machine applies // A change a player makes to their own fighter from the menu, sent through its controls so that every machine applies
// it on the same tick (and rollback replays it). Item puts an item (or EDIT_EMPTY) in a slot, and is the only kind a // it on the same tick (and rollback replays it). Item puts an item (or EDIT_EMPTY) in a slot, and is the only kind a
// match takes, as a Choice pick; the rest are the sandbox's: one copy's level, an item's level (with every copy of it // match takes, as a Choice pick; the rest are the sandbox's: one copy's level, an item's level (with every copy of it
// equipped), the slot counts (index weapons, value trinkets), PvP on or off, and online, the colour (if no one else has // equipped), the slot counts (index weapons, value trinkets), PvP on or off, and online, the color (if no one else has
// it). // it).
enum EditType enum EditType
{ {
@@ -146,7 +146,7 @@ inline int pick_encode(ItemKind kind, int slot, int item)
return edit_encode({.type = EDIT_ITEM, .kind = kind, .index = slot, .value = item < 0 ? EDIT_EMPTY : item}); return edit_encode({.type = EDIT_ITEM, .kind = kind, .index = slot, .value = item < 0 ? EDIT_EMPTY : item});
} }
// A player's setup as they join online: their name, the colour they'd like (-1 for any), and their sandbox slots, // A player's setup as they join online: their name, the color they'd like (-1 for any), and their sandbox slots,
// loadouts, levels and stacking, which their fighter starts with. Plain data, sent as it sits in memory. // loadouts, levels and stacking, which their fighter starts with. Plain data, sent as it sits in memory.
struct Profile struct Profile
{ {
@@ -255,7 +255,7 @@ public:
// Whether each kind of item is handed out this round (and with Choice, being chosen during the countdown). // Whether each kind of item is handed out this round (and with Choice, being chosen during the countdown).
bool isPicks[ITEM_KIND_COUNT] = {}; bool isPicks[ITEM_KIND_COUNT] = {};
// Offline, what this screen asks of the next tick: the menu's Play, leaving the results for the sandbox, and the // Offline, what this screen asks of the next tick: the menu's Play, leaving the results for the sandbox, and the
// colour its player picked. Online, members' wishes go through the session instead. // color its player picked. Online, members' wishes go through the session instead.
int colorWanted = COLOR_UNASSIGNED; int colorWanted = COLOR_UNASSIGNED;
int fashionWanted = FASHION_NONE; int fashionWanted = FASHION_NONE;
bool isOnline = false; bool isOnline = false;
+6 -6
View File
@@ -237,7 +237,7 @@ void Menu::level_step(Game& game, int step, Vec2 mouse)
edit_encode({.type = EDIT_ITEM_LEVEL, .kind = kind, .index = item, .value = next})); edit_encode({.type = EDIT_ITEM_LEVEL, .kind = kind, .index = item, .value = next}));
} }
// Keeps the sandbox's item levels while in it. Back from a match that levelled them, they go back in as edits instead, // Keeps the sandbox's item levels while in it. Back from a match that leveled them, they go back in as edits instead,
// one at a time, until the fighter has them all. // one at a time, until the fighter has them all.
void Menu::sandbox_levels_update(Game& game) void Menu::sandbox_levels_update(Game& game)
{ {
@@ -649,7 +649,7 @@ void Menu::items_draw(Game& game, MenuLayout& layout)
if (shown != described) if (shown != described)
description.scroll = 0; description.scroll = 0;
described = shown; described = shown;
// Only what can be levelled here previews its next level. // Only what can be leveled here previews its next level.
bool isLevelable = !isMatch || (isSandboxLevel && !slotLevel); bool isLevelable = !isMatch || (isSandboxLevel && !slotLevel);
Tooltip tooltip = ui.tooltip_build(game, kind, item, level, isLevelable ? TOOLTIP_NEXT_LEVEL : TOOLTIP_PLAIN); Tooltip tooltip = ui.tooltip_build(game, kind, item, level, isLevelable ? TOOLTIP_NEXT_LEVEL : TOOLTIP_PLAIN);
if (slotLevel) if (slotLevel)
@@ -660,11 +660,11 @@ void Menu::items_draw(Game& game, MenuLayout& layout)
ui.tooltip_page_draw(game, description, area, tooltip); ui.tooltip_page_draw(game, description, area, tooltip);
// A row under the description levels what it shows: in the sandbox, the copy in a clicked hotbar slot, or else the // A row under the description levels what it shows: in the sandbox, the copy in a clicked hotbar slot, or else the
// selection with every copy of it equipped, and in a match, the selection's sandbox level. A button greys out at // selection with every copy of it equipped, and in a match, the selection's sandbox level. A button grays out at
// the end of the levels. // the end of the levels.
if (!isLevelable) if (!isLevelable)
return; return;
// The level between a minus and a plus, centred. // The level between a minus and a plus, centerd.
char text[TOOLTIP_LENGTH]; char text[TOOLTIP_LENGTH];
SDL_snprintf(text, sizeof(text), isSandboxLevel ? "Sandbox Level %d" : "Level %d", level + 1); SDL_snprintf(text, sizeof(text), isSandboxLevel ? "Sandbox Level %d" : "Level %d", level + 1);
Vec2 size = game.text.size_get(text); Vec2 size = game.text.size_get(text);
@@ -779,7 +779,7 @@ static std::string lobby_page_get()
#endif #endif
} }
// A lobby member's colour: as this screen shows their fighter when they're in the same room, or as the lobby last // A lobby member's color: as this screen shows their fighter when they're in the same room, or as the lobby last
// heard otherwise (plain until then). // heard otherwise (plain until then).
static Color member_color_get(Game& game, int member) static Color member_color_get(Game& game, int member)
{ {
@@ -882,7 +882,7 @@ void Menu::online_draw(Game& game, Rect area)
y += ROW_HEIGHT; y += ROW_HEIGHT;
// Everyone in it and the games being played, scrolling below. Each player is in the order they came (the first // Everyone in it and the games being played, scrolling below. Each player is in the order they came (the first
// hosting), in their colour, with their connection and a little of them on the right. Each game is its players // hosting), in their color, with their connection and a little of them on the right. Each game is its players
// and its mode, and clicking one from the sandbox spectates it. // and its mode, and clicking one from the sandbox spectates it.
Rect list = {area.x, y, area.w, area.y + area.h - y}; Rect list = {area.x, y, area.w, area.y + area.h - y};
float rowWidth = list.w - SCROLL_WIDTH * 2; float rowWidth = list.w - SCROLL_WIDTH * 2;
+1 -1
View File
@@ -6,7 +6,7 @@
struct Game; struct Game;
// The menu's frame: a panel (header then body) centred across the top, with the item tabs' hotbars a row under it and // The menu's frame: a panel (header then body) centerd across the top, with the item tabs' hotbars a row under it and
// the gamepad hints under those. // the gamepad hints under those.
constexpr float MENU_TOP = 24; constexpr float MENU_TOP = 24;
constexpr float MENU_WIDTH = 440; constexpr float MENU_WIDTH = 440;
+28 -3
View File
@@ -243,7 +243,7 @@ void Net::message_handle(const std::string& text)
member.id = message["id"].get(number) ? -1 : (int)number; member.id = message["id"].get(number) ? -1 : (int)number;
member.name = message["name"].get(field) ? "" : std::string(field); member.name = message["name"].get(field) ? "" : std::string(field);
members.push_back(member); members.push_back(member);
notices.push_back(member.name + " joined the lobby."); notices.push_back({member.name + " joined the lobby.", member.id});
return; return;
} }
if (type == "kicked") if (type == "kicked")
@@ -254,7 +254,7 @@ void Net::message_handle(const std::string& text)
state = NET_OFFLINE; state = NET_OFFLINE;
lobby_leave(); lobby_leave();
error = text; error = text;
notices.push_back(text); notices.push_back({text, selfId});
return; return;
} }
if (type == "member_leave") if (type == "member_leave")
@@ -263,11 +263,20 @@ void Net::message_handle(const std::string& text)
bool isKicked = false; bool isKicked = false;
if (!message["kicked"].get(isKicked) && isKicked) if (!message["kicked"].get(isKicked) && isKicked)
kicks[id] = text_clean(message["reason"].get(field) ? "" : field, KICK_REASON_MAX); kicks[id] = text_clean(message["reason"].get(field) ? "" : field, KICK_REASON_MAX);
int previous = hostId;
hostId = message["host"].get(number) ? hostId : (int)number; hostId = message["host"].get(number) ? hostId : (int)number;
NetMember* member = member_get(id); NetMember* member = member_get(id);
if (member && !isKicked) if (member && !isKicked)
notices.push_back(member->name + " left the lobby."); notices.push_back({member->name + " left the lobby.", id});
std::erase_if(members, [&](NetMember& member) { return member.id == id; }); std::erase_if(members, [&](NetMember& member) { return member.id == id; });
host_announce(previous);
return;
}
if (type == "host_change")
{
int previous = hostId;
hostId = message["host"].get(number) ? hostId : (int)number;
host_announce(previous);
return; return;
} }
if (type == "signal") if (type == "signal")
@@ -393,6 +402,22 @@ void Net::kick_send(int member, const std::string& reason)
"}"); "}");
} }
// Asks the server to hand hosting to another member (only the host may).
void Net::host_send(int member)
{
if (impl->socket && impl->socket->isOpen() && state == NET_LOBBY)
impl->socket->send("{\"type\":\"host\",\"id\":" + std::to_string(member) + "}");
}
// Tells the chat who hosts now, when that's changed from previous.
void Net::host_announce(int previous)
{
NetMember* host = member_get(hostId);
if (hostId == previous || !host)
return;
notices.push_back({hostId == selfId ? "You're now the host." : host->name + " is now the host.", hostId});
}
// Whether the WebRTC connection to the server is up, with both its channels open. // Whether the WebRTC connection to the server is up, with both its channels open.
bool Net::is_linked() bool Net::is_linked()
{ {
+10 -1
View File
@@ -17,6 +17,13 @@ enum NetState
// Someone in the lobby (this screen's player too): their server id and name, and their connection's round trip to the // Someone in the lobby (this screen's player too): their server id and name, and their connection's round trip to the
// server in milliseconds (-1 while it isn't known: this screen only measures its own). // server in milliseconds (-1 while it isn't known: this screen only measures its own).
// A line for the chat log from the server, and the member it's about (-1 for none), in whose color it shows.
struct NetNotice
{
std::string text;
int member = -1;
};
struct NetMember struct NetMember
{ {
int id = -1; int id = -1;
@@ -45,6 +52,7 @@ public:
void heartbeat(); void heartbeat();
void send(const std::string& bytes, Delivery delivery); void send(const std::string& bytes, Delivery delivery);
void kick_send(int member, const std::string& reason); void kick_send(int member, const std::string& reason);
void host_send(int member);
bool is_linked(); bool is_linked();
NetState state = NET_OFFLINE; NetState state = NET_OFFLINE;
@@ -56,7 +64,7 @@ public:
// Binary messages from the server, oldest first, for the session to take. // Binary messages from the server, oldest first, for the session to take.
std::vector<std::string> packets; std::vector<std::string> packets;
// Lines for the chat log from the server (like this screen being kicked), oldest first, for the chat to take. // Lines for the chat log from the server (like this screen being kicked), oldest first, for the chat to take.
std::vector<std::string> notices; std::vector<NetNotice> notices;
// Members the server said were kicked, with the reason (empty for none given). // Members the server said were kicked, with the reason (empty for none given).
std::map<int, std::string> kicks; std::map<int, std::string> kicks;
@@ -71,4 +79,5 @@ private:
void link_open(); void link_open();
void signal_handle(const std::string& payload); void signal_handle(const std::string& payload);
NetMember* member_get(int id); NetMember* member_get(int id);
void host_announce(int previous);
}; };
+13 -4
View File
@@ -27,7 +27,7 @@
#include <string_view> #include <string_view>
#include <type_traits> #include <type_traits>
constexpr int PROTOCOL_VERSION = 7; constexpr int PROTOCOL_VERSION = 8;
constexpr int LOBBY_MEMBERS_MAX = 16; constexpr int LOBBY_MEMBERS_MAX = 16;
constexpr int LOBBY_CODE_LENGTH = 4; constexpr int LOBBY_CODE_LENGTH = 4;
// Letters only, without I and O, so codes are easy to read out and type. // Letters only, without I and O, so codes are easy to read out and type.
@@ -46,7 +46,7 @@ constexpr int SERVER_URL_LENGTH = 128;
// PACKET_INPUTS room, ack, first, Uint16 count, Controls* this player's controls from a tick on, and the last // PACKET_INPUTS room, ack, first, Uint16 count, Controls* this player's controls from a tick on, and the last
// tick it has confirmed // tick it has confirmed
// PACKET_QUEUE Uint8 isOpen, Rules queue a game in the sandbox, or stop queueing it // PACKET_QUEUE Uint8 isOpen, Rules queue a game in the sandbox, or stop queueing it
// PACKET_CHAT Uint16 length, text // PACKET_CHAT Uint8 scope, Uint16 length, text to this player's room or the whole lobby (ChatScope)
// PACKET_TYPING Uint8 isTyping // PACKET_TYPING Uint8 isTyping
// PACKET_RENAME Uint16 length, text // PACKET_RENAME Uint16 length, text
// PACKET_PING Uint16 milliseconds this player's round trip to the server // PACKET_PING Uint16 milliseconds this player's round trip to the server
@@ -61,9 +61,18 @@ constexpr int SERVER_URL_LENGTH = 128;
// once the events before them are in, and how early // once the events before them are in, and how early
// (in ticks) this player's last controls came // (in ticks) this player's last controls came
// PACKET_CHECKSUM room, tick, Uint32 the room's state as that tick starts // PACKET_CHECKSUM room, tick, Uint32 the room's state as that tick starts
// PACKET_ROOMS Uint8 count, then for each: room, Uint8 mode, Uint8 players, then (member, Uint8 colour)* // PACKET_ROOMS Uint8 count, then for each: room, Uint8 mode, Uint8 players, then (member, Uint8 color)*
// who's in each of the lobby's rooms, the sandbox first // who's in each of the lobby's rooms, the sandbox first
// PACKET_CHAT from, Uint16 length, text and the same for typing, renames and pings // PACKET_CHAT from, Uint8 scope, room, Uint16 length, text a chat line, and the room its sender is in
// PACKET_RENAME from, Uint16 length, text and the same for typing and pings
// Who a chat line goes to: whoever plays or watches in the sender's room (the sandbox, or one game), or the whole
// lobby.
enum ChatScope : unsigned char
{
CHAT_ROOM,
CHAT_LOBBY
};
enum PacketType : unsigned char enum PacketType : unsigned char
{ {
PACKET_PROFILE = 1, PACKET_PROFILE = 1,
+5 -5
View File
@@ -58,7 +58,7 @@ void main()
} }
)"; )";
// Tints the atlas texel by the vertex colour, then mixes toward the plain colour by flash. // Tints the atlas texel by the vertex color, then mixes toward the plain color by flash.
constexpr const char* SPRITE_FRAG = R"( constexpr const char* SPRITE_FRAG = R"(
in vec2 uv; in vec2 uv;
in vec4 color; in vec4 color;
@@ -357,7 +357,7 @@ void Renderer::rect_draw(Rect rect, Color color)
{.color = color, .region = white, .position = {rect.x, rect.y}, .scale = {rect.w / white.w, rect.h / white.h}}); {.color = color, .region = white, .position = {rect.x, rect.y}, .scale = {rect.w / white.w, rect.h / white.h}});
} }
// A flat triangle through three corners, each with its own colour blended across it. // A flat triangle through three corners, each with its own color blended across it.
void Renderer::triangle_draw(const Vec2* corners, const Color* colors) void Renderer::triangle_draw(const Vec2* corners, const Color* colors)
{ {
if (vertices.size() + TRIANGLE_CORNERS > MAX_VERTICES) if (vertices.size() + TRIANGLE_CORNERS > MAX_VERTICES)
@@ -399,7 +399,7 @@ void Renderer::circle_draw(Vec2 center, float radius, Color color)
} }
// Outlines a circle one pixel thick, row by row like circle_draw, so it stays crisp and never overlaps itself. Each // Outlines a circle one pixel thick, row by row like circle_draw, so it stays crisp and never overlaps itself. Each
// row spans from its own edge in to where its neighbouring rows reach. // row spans from its own edge in to where its neighboring rows reach.
void Renderer::ring_draw(Vec2 center, float radius, Color color) void Renderer::ring_draw(Vec2 center, float radius, Color color)
{ {
int rows = std::ceil(radius); int rows = std::ceil(radius);
@@ -421,7 +421,7 @@ void Renderer::ring_draw(Vec2 center, float radius, Color color)
} }
// Fits the canvas inside a window, using whole-number scales once the window is at least canvas sized. // Fits the canvas inside a window, using whole-number scales once the window is at least canvas sized.
// The largest box with the canvas's shape that fits the window, centred. // The largest box with the canvas's shape that fits the window, centerd.
Rect Renderer::letterbox_get(float width, float height) Rect Renderer::letterbox_get(float width, float height)
{ {
float scale = std::min(width / CANVAS_WIDTH, height / CANVAS_HEIGHT); float scale = std::min(width / CANVAS_WIDTH, height / CANVAS_HEIGHT);
@@ -470,7 +470,7 @@ void Renderer::layer_begin()
} }
// Lays the layer over the canvas at alpha, as one piece, so nothing in it shows through anything else in it. The layer // Lays the layer over the canvas at alpha, as one piece, so nothing in it shows through anything else in it. The layer
// holds its colours already multiplied by their alpha (blending onto clear), so it's laid with its colour taken whole. // holds its colors already multiplied by their alpha (blending onto clear), so it's laid with its color taken whole.
void Renderer::layer_end(float alpha) void Renderer::layer_end(float alpha)
{ {
flush(); flush();
+21 -21
View File
@@ -13,7 +13,7 @@ constexpr float RUMBLE_LANDED = 0.15f;
constexpr Uint32 RUMBLE_LANDED_TIME = 60; constexpr Uint32 RUMBLE_LANDED_TIME = 60;
constexpr int BLINK_MILLISECONDS = 80; constexpr int BLINK_MILLISECONDS = 80;
// How long a claimed shot takes to turn from its old owner's colour to its new one's. // How long a claimed shot takes to turn from its old owner's color to its new one's.
constexpr float CLAIM_BLEND = 0.35f; constexpr float CLAIM_BLEND = 0.35f;
constexpr float TRAJECTORY_ALPHA = 0.12f; constexpr float TRAJECTORY_ALPHA = 0.12f;
constexpr float TRAJECTORY_TAPER = 0.25f; constexpr float TRAJECTORY_TAPER = 0.25f;
@@ -116,7 +116,7 @@ constexpr Uint32 TOMATO_LOOK_STEP = 2654435761u;
constexpr float TOMATO_REACH = 10; constexpr float TOMATO_REACH = 10;
constexpr Burst TOMATO_BURST = {.gravity = 400, .life = 0.5f, .size = 2, .speed = 140, .spread = 1.2f, .count = 12}; constexpr Burst TOMATO_BURST = {.gravity = 400, .life = 0.5f, .size = 2, .speed = 140, .spread = 1.2f, .count = 12};
// A random offset for an empowered attack or fighter to shake by, wider the more its colour fills in, like a damage // A random offset for an empowered attack or fighter to shake by, wider the more its color fills in, like a damage
// number. // number.
static Vec2 jitter_get(float fill) static Vec2 jitter_get(float fill)
{ {
@@ -124,7 +124,7 @@ static Vec2 jitter_get(float fill)
return {random_get(-reach, reach), random_get(-reach, reach)}; return {random_get(-reach, reach), random_get(-reach, reach)};
} }
// Sheds particles off the edge of an empowered shape, more the more its colour fills in. They take the shape's colour // Sheds particles off the edge of an empowered shape, more the more its color fills in. They take the shape's color
// and fill in just as far. // and fill in just as far.
static void aura_spawn(Game& game, Capsule shape, float fill, Color color) static void aura_spawn(Game& game, Capsule shape, float fill, Color color)
{ {
@@ -139,21 +139,21 @@ static void aura_spawn(Game& game, Capsule shape, float fill, Color color)
} }
} }
// A colour decayed to match a weakened strength: drained toward grey and darkened, faster than the damage drops so // A color decayed to match a weakened strength: drained toward gray and darkened, faster than the damage drops so
// weakened shots read clearly. // weakened shots read clearly.
static Color color_dim(Color color, float strength) static Color color_dim(Color color, float strength)
{ {
strength = math_pow(std::min(1.0f, strength), FALLOFF_CURVE); strength = math_pow(std::min(1.0f, strength), FALLOFF_CURVE);
float grey = (color.r + color.g + color.b) / 3; float gray = (color.r + color.g + color.b) / 3;
Color washed = {math_lerp(grey, color.r, strength), math_lerp(grey, color.g, strength), Color washed = {math_lerp(gray, color.r, strength), math_lerp(gray, color.g, strength),
math_lerp(grey, color.b, strength), color.a}; math_lerp(gray, color.b, strength), color.a};
return color_shade(washed, FALLOFF_DIM + (1 - FALLOFF_DIM) * strength); return color_shade(washed, FALLOFF_DIM + (1 - FALLOFF_DIM) * strength);
} }
// The owner's colour (darker for secondary attacks), dimming as a shot's damage falls off. // The owner's color (darker for secondary attacks), dimming as a shot's damage falls off.
static Color owner_color_get(Game& game, Attack& attack) static Color owner_color_get(Game& game, Attack& attack)
{ {
// A claimed shot turns from its old owner's colour to its new one's. // A claimed shot turns from its old owner's color to its new one's.
Color color = game.color_get(attack.owner); Color color = game.color_get(attack.owner);
if (attack.ownerPrevious >= 0) if (attack.ownerPrevious >= 0)
color = color_mix(game.color_get(attack.ownerPrevious), color, color = color_mix(game.color_get(attack.ownerPrevious), color,
@@ -177,7 +177,7 @@ struct Stroke
float taper = 1; float taper = 1;
}; };
// A beam, rope or whip, each piece coloured by the strength there, dimming along it the way damage falls off. Jitter // A beam, rope or whip, each piece colored by the strength there, dimming along it the way damage falls off. Jitter
// kinks it at every piece, moving each frame so it crackles. // kinks it at every piece, moving each frame so it crackles.
static void strength_line_draw(Game& game, Attack& attack, Stroke stroke) static void strength_line_draw(Game& game, Attack& attack, Stroke stroke)
{ {
@@ -350,7 +350,7 @@ static void reload_pose(Fighter& fighter, int key, Draw& draw)
draw.position.y += RELOAD_DROP * math_pow(weight, RELOAD_PUMP); draw.position.y += RELOAD_DROP * math_pow(weight, RELOAD_PUMP);
} }
// How far a fighter's weapons (and crosshair) fill in with its colour: with the charge as it builds, or the speed bonus // How far a fighter's weapons (and crosshair) fill in with its color: with the charge as it builds, or the speed bonus
// if that's more, then once the charge is ready blinking between full and the speed bonus alone. // if that's more, then once the charge is ready blinking between full and the speed bonus alone.
float Scene::charge_fill_get(Fighter& fighter) float Scene::charge_fill_get(Fighter& fighter)
{ {
@@ -371,7 +371,7 @@ Vec2 Scene::weapon_shake_get(Fighter& fighter, int key)
return jitter_get(std::max({fighter.charge, left * left, spin}) * CHARGE_SHAKE); return jitter_get(std::max({fighter.charge, left * left, spin}) * CHARGE_SHAKE);
} }
// A weapon's colour (and its crosshair's) through its cooldown: darkened just after it fires (keeping its hue), back to // A weapon's color (and its crosshair's) through its cooldown: darkened just after it fires (keeping its hue), back to
// color as it cools. // color as it cools.
// One cooling too quickly to see doesn't fade, so rapid fire doesn't flicker. // One cooling too quickly to see doesn't fade, so rapid fire doesn't flicker.
Color Scene::cooldown_tint_get(Fighter& fighter, int key, Color color) Color Scene::cooldown_tint_get(Fighter& fighter, int key, Color color)
@@ -572,7 +572,7 @@ void Scene::update()
} }
} }
// The match's tomatoes as splats in their throwers' colours: a big blob where each landed with smaller ones flung round // The match's tomatoes as splats in their throwers' colors: a big blob where each landed with smaller ones flung round
// it (the same on every screen), holding and then fading out. // it (the same on every screen), holding and then fading out.
void Scene::tomatoes_draw() void Scene::tomatoes_draw()
{ {
@@ -648,8 +648,8 @@ void Scene::world_draw()
for (int x = startX; x <= endX; x++) for (int x = startX; x <= endX; x++)
{ {
auto isOpen = [&](float dx, float dy) { auto isOpen = [&](float dx, float dy) {
Tile neighbour = world.tile_get(x + dx, y + dy); Tile neighbor = world.tile_get(x + dx, y + dy);
return neighbour != TILE_SOLID && neighbour != TILE_SLOPE; return neighbor != TILE_SOLID && neighbor != TILE_SLOPE;
}; };
auto isEmpty = [&](int dx) { auto isEmpty = [&](int dx) {
return world.tile_get(x + dx, y) == TILE_EMPTY; return world.tile_get(x + dx, y) == TILE_EMPTY;
@@ -832,7 +832,7 @@ void Scene::animator_update(Fighter& fighter, Animator& animator)
animator.update(stride > 0 ? std::abs(fighter.body.velocity.x) * TICK / stride : TICK * animator.art->fps); animator.update(stride > 0 ? std::abs(fighter.body.velocity.x) * TICK / stride : TICK * animator.art->fps);
} }
// The fighter's animation placed where it is right now, in its colour: feet on the ground facing its way (walking and // The fighter's animation placed where it is right now, in its color: feet on the ground facing its way (walking and
// crawling face the way they move), facing away from a wall it kicks off or slides down, spinning about its middle when // crawling face the way they move), facing away from a wall it kicks off or slides down, spinning about its middle when
// launched, and rolling as a ball resting on the ground along the surface's perpendicular. Shared with afterimages. // launched, and rolling as a ball resting on the ground along the surface's perpendicular. Shared with afterimages.
Pose Scene::fighter_pose_get(Fighter& fighter) Pose Scene::fighter_pose_get(Fighter& fighter)
@@ -929,7 +929,7 @@ void Scene::attacks_draw()
else if (kind != ATTACK_EXPLOSION) else if (kind != ATTACK_EXPLOSION)
attack_pose(attack).draw(renderer); attack_pose(attack).draw(renderer);
// A faint ring shows how far anything that explodes would reach, and pips in the shot's colour circle it, one // A faint ring shows how far anything that explodes would reach, and pips in the shot's color circle it, one
// for each ricochet it has left. // for each ricochet it has left.
if (attack.def()->isBursting && !attack.def()->isInstant) if (attack.def()->isBursting && !attack.def()->isInstant)
renderer.ring_draw(attack.position, blast_area_get(attack), color_fade(tint, BLAST_RING_ALPHA)); renderer.ring_draw(attack.position, blast_area_get(attack), color_fade(tint, BLAST_RING_ALPHA));
@@ -976,7 +976,7 @@ void Scene::magic_draw()
} }
} }
// A share from 0 to 1 that a mote of air draws from a seed, the same every frame (a bit-mixing hash, so neighbouring // A share from 0 to 1 that a mote of air draws from a seed, the same every frame (a bit-mixing hash, so neighboring
// seeds give unrelated shares). // seeds give unrelated shares).
static float mote_share_get(Uint32 seed) static float mote_share_get(Uint32 seed)
{ {
@@ -1043,7 +1043,7 @@ void Scene::tesla_draw()
} }
// The Display Collision setting: every attack's hit shapes as the hit test sees them (and a Vacuum's whole cone, where // The Display Collision setting: every attack's hit shapes as the hit test sees them (and a Vacuum's whole cone, where
// it pulls or blows), and the boxes fighters and dummies are hit in, see-through in their owner's colour. // it pulls or blows), and the boxes fighters and dummies are hit in, see-through in their owner's color.
void Scene::collision_draw() void Scene::collision_draw()
{ {
Renderer& renderer = game.renderer; Renderer& renderer = game.renderer;
@@ -1068,7 +1068,7 @@ void Scene::collision_draw()
renderer.rect_draw(dummy.box, color_fade(WHITE, COLLISION_ALPHA)); renderer.rect_draw(dummy.box, color_fade(WHITE, COLLISION_ALPHA));
} }
// Explosions draw before the player, so their filled centre never hides anyone standing in the blast. One that landed a // Explosions draw before the player, so their filled center never hides anyone standing in the blast. One that landed a
// double donk blinks bright as it lingers, like an armed mine. // double donk blinks bright as it lingers, like an armed mine.
void Scene::blasts_draw() void Scene::blasts_draw()
{ {
@@ -1254,7 +1254,7 @@ void Scene::flash_follow(Flash& flash)
flash.angle = probe.angle; flash.angle = probe.angle;
} }
// Dual Wield: the off-hand weapons, a darker shade of the fighter's colour, fanned out behind it (see // Dual Wield: the off-hand weapons, a darker shade of the fighter's color, fanned out behind it (see
// Fighter::hand_get), the furthest back first so the nearer cover them. Each shakes and reloads like the main one. // Fighter::hand_get), the furthest back first so the nearer cover them. Each shakes and reloads like the main one.
void Scene::offhand_draw(Fighter& fighter) void Scene::offhand_draw(Fighter& fighter)
{ {
+58 -19
View File
@@ -8,7 +8,7 @@
// How far past the last confirmed tick this screen plays at most, and how many of its own controls go again in each // How far past the last confirmed tick this screen plays at most, and how many of its own controls go again in each
// packet. Its controls should reach the server LEAD_LOW to LEAD_HIGH ticks early: outside that, it changes pace (at // packet. Its controls should reach the server LEAD_LOW to LEAD_HIGH ticks early: outside that, it changes pace (at
// most every PACE_WAIT ticks), towards LEAD_TARGET, by up to PACE_BURST extra ticks at once. // most every PACE_WAIT ticks), toward LEAD_TARGET, by up to PACE_BURST extra ticks at once.
constexpr int PREDICT_MAX = 30; constexpr int PREDICT_MAX = 30;
constexpr int RESEND_MAX = 30; constexpr int RESEND_MAX = 30;
constexpr int LEAD_LOW = 1; constexpr int LEAD_LOW = 1;
@@ -224,6 +224,10 @@ void Session::packet_handle(Game& game, const std::string& bytes)
member.ping = ping; member.ping = ping;
return; return;
} }
Uint8 scope = CHAT_ROOM;
int place = 0;
if (type == PACKET_CHAT && (!reader.get(scope) || !reader.get(place)))
return;
Uint16 length = 0; Uint16 length = 0;
if (!reader.get(length) || reader.at + length > bytes.size()) if (!reader.get(length) || reader.at + length > bytes.size())
return; return;
@@ -232,7 +236,7 @@ void Session::packet_handle(Game& game, const std::string& bytes)
{ {
std::string text = text_clean(raw, CHAT_MESSAGE_MAX); std::string text = text_clean(raw, CHAT_MESSAGE_MAX);
if (!text.empty()) if (!text.empty())
game.chat.say(game, from, text); game.chat.say(game, from, text, place_tag_get(place, (ChatScope)scope));
return; return;
} }
std::string name = text_clean(raw, PLAYER_NAME_MAX); std::string name = text_clean(raw, PLAYER_NAME_MAX);
@@ -243,7 +247,8 @@ void Session::packet_handle(Game& game, const std::string& bytes)
for (NetMember& member : game.net.members) for (NetMember& member : game.net.members)
if (member.id == from) if (member.id == from)
member.name = name; member.name = name;
game.chat.system(before + " changed their name to " + name_get(game, from) + "."); game.chat.system(before + " changed their name to " + name_get(game, from) + ".",
game.chat.member_color_get(game, from), from);
} }
// This screen is in a room now, as its tick starts: the game loads and plays on from there, its effects and camera // This screen is in a room now, as its tick starts: the game loads and plays on from there, its effects and camera
@@ -453,7 +458,7 @@ void Session::watch_send(Game& game, int serial)
game.net.send(PacketWriter().put(PACKET_WATCH).put(serial).bytes, DELIVERY_RELIABLE); game.net.send(PacketWriter().put(PACKET_WATCH).put(serial).bytes, DELIVERY_RELIABLE);
} }
// A member's colour in whichever room they're in, as the lobby last heard, or -1 if they're in none yet. // A member's color in whichever room they're in, as the lobby last heard, or -1 if they're in none yet.
int Session::color_get(int member) int Session::color_get(int member)
{ {
for (SessionRoom& lobbyRoom : lobbyRooms) for (SessionRoom& lobbyRoom : lobbyRooms)
@@ -531,12 +536,28 @@ void Session::text_send(Game& game, PacketType type, const std::string& text)
game.net.send(packet.bytes, DELIVERY_RELIABLE); game.net.send(packet.bytes, DELIVERY_RELIABLE);
} }
// A chat message from this screen: to the lobby through the server, or straight into the log offline. // A chat message from this screen: through the server to its room or the whole lobby, or straight into the log
void Session::chat_send(Game& game, const std::string& text) // offline.
void Session::chat_send(Game& game, const std::string& text, ChatScope scope)
{ {
if (!isActive) if (!isActive)
return game.chat.say(game, game.net.selfId, text); return game.chat.say(game, game.net.selfId, text, "");
text_send(game, PACKET_CHAT, text); PacketWriter packet;
packet.put(PACKET_CHAT).put((Uint8)scope).put((Uint16)text.size());
packet.bytes += text;
game.net.send(packet.bytes, DELIVERY_RELIABLE);
}
// What a chat line is marked with: where it came from when that isn't this screen's room ("[Sandbox] ", "[Game 2] ",
// counting the lobby's games in order), "[All] " for one said to the whole lobby from here, or nothing.
std::string Session::place_tag_get(int place, ChatScope scope)
{
if (place == room)
return scope == CHAT_LOBBY ? "[All] " : "";
for (size_t k = 0; k < lobbyRooms.size(); k++)
if (lobbyRooms[k].room == place)
return k == 0 ? "[Sandbox] " : "[Game " + std::to_string(k) + "] ";
return "[All] ";
} }
// Tells everyone this screen's chat box opened or closed (for the "..." over its fighter). // Tells everyone this screen's chat box opened or closed (for the "..." over its fighter).
@@ -546,8 +567,8 @@ void Session::typing_send(Game& game, bool isTyping)
game.net.send(PacketWriter().put(PACKET_TYPING).put((Uint8)isTyping).bytes, DELIVERY_RELIABLE); game.net.send(PacketWriter().put(PACKET_TYPING).put((Uint8)isTyping).bytes, DELIVERY_RELIABLE);
} }
// Notices this screen's name or colour changing (in Profile, or by a chat command). A new name goes to the lobby once // Notices this screen's name or color changing (in Profile, or by a chat command). A new name goes to the lobby once
// it's done being typed (at most every RENAME_WAIT), and a new colour goes through the game as an edit, so everyone // it's done being typed (at most every RENAME_WAIT), and a new color goes through the game as an edit, so everyone
// agrees (it's refused if someone else has it). // agrees (it's refused if someone else has it).
void Session::profile_watch(Game& game) void Session::profile_watch(Game& game)
{ {
@@ -589,12 +610,10 @@ int Session::rename_wait_get()
return isActive && now < nextRename ? (int)((nextRename - now + 999) / 1000) : 0; return isActive && now < nextRename ? (int)((nextRename - now + 999) / 1000) : 0;
} }
// The host puts a player out of the lobby by the name they show as (any case; a bare name works when only one player // A lobby member other than this screen's by the name they show as (any case; a bare name works when only one player
// has it). The server does the kicking, and everyone's chat says so. // has it), or -1, saying why in the chat.
void Session::kick(Game& game, const std::string& name, const std::string& reason) int Session::member_find(Game& game, const std::string& name)
{ {
if (!isActive || game.net.hostId != game.net.selfId)
return game.chat.system("Only the host can kick players.");
int found = -1; int found = -1;
int bare = -1; int bare = -1;
int bareCount = 0; int bareCount = 0;
@@ -613,10 +632,30 @@ void Session::kick(Game& game, const std::string& name, const std::string& reaso
if (found < 0 && bareCount == 1) if (found < 0 && bareCount == 1)
found = bare; found = bare;
if (found < 0) if (found < 0)
return game.chat.system(bareCount > 1 ? "More than one player is named " + name + game.chat.system(bareCount > 1 ? "More than one player is named " + name + ". Include their number, like \"" +
". Include their number, like \"" + name + " (2)\"." name + " (2)\"."
: "There's no player named " + name + " in this lobby."); : "There's no player named " + name + " in this lobby.");
game.net.kick_send(found, text_clean(reason, KICK_REASON_MAX)); return found;
}
// The host puts a player out of the lobby (see member_find). The server does the kicking, and everyone's chat says so.
void Session::kick(Game& game, const std::string& name, const std::string& reason)
{
if (!isActive || game.net.hostId != game.net.selfId)
return game.chat.system("Only the host can kick players.");
int found = member_find(game, name);
if (found >= 0)
game.net.kick_send(found, text_clean(reason, KICK_REASON_MAX));
}
// The host hands hosting to another player (see member_find). The server makes the change, and everyone's chat says so.
void Session::host_give(Game& game, const std::string& name)
{
if (!isActive || game.net.hostId != game.net.selfId)
return game.chat.system("Only the host can hand over hosting.");
int found = member_find(game, name);
if (found >= 0)
game.net.host_send(found);
} }
// Asks the server to queue a game with these rules in the lobby's sandbox (this screen's fighter standing in its // Asks the server to queue a game with these rules in the lobby's sandbox (this screen's fighter standing in its
+6 -3
View File
@@ -31,7 +31,7 @@ enum StepMode
STEP_LIVE STEP_LIVE
}; };
// One of the lobby's rooms as the server last told it: its mode, and the members playing in it with their colours. // One of the lobby's rooms as the server last told it: its mode, and the members playing in it with their colors.
struct SessionRoom struct SessionRoom
{ {
std::vector<std::pair<int, int>> players; std::vector<std::pair<int, int>> players;
@@ -77,9 +77,12 @@ public:
int fighter_get(Game& game, int member); int fighter_get(Game& game, int member);
std::string base_name_get(Game& game, int member); std::string base_name_get(Game& game, int member);
std::string name_get(Game& game, int member); std::string name_get(Game& game, int member);
void chat_send(Game& game, const std::string& text); void chat_send(Game& game, const std::string& text, ChatScope scope);
std::string place_tag_get(int place, ChatScope scope);
void typing_send(Game& game, bool isTyping); void typing_send(Game& game, bool isTyping);
void kick(Game& game, const std::string& name, const std::string& reason); void kick(Game& game, const std::string& name, const std::string& reason);
void host_give(Game& game, const std::string& name);
int member_find(Game& game, const std::string& name);
int rename_wait_get(); int rename_wait_get();
void queue_send(Game& game, const Rules& rules); void queue_send(Game& game, const Rules& rules);
void queue_cancel(Game& game); void queue_cancel(Game& game);
@@ -93,7 +96,7 @@ private:
std::map<int, std::map<int, RoomEvent>> events; std::map<int, std::map<int, RoomEvent>> events;
std::map<int, Uint32> checksums; std::map<int, Uint32> checksums;
std::map<int, Controls> held; std::map<int, Controls> held;
// Every member's chosen name (cleaned), kept current by their renames, and the colour this screen last asked for. // Every member's chosen name (cleaned), kept current by their renames, and the color this screen last asked for.
std::map<int, std::string> names; std::map<int, std::string> names;
int room = 0; int room = 0;
// The next tick to play, the first not yet confirmed, and the earliest that has to be played again. // The next tick to play, the first not yet confirmed, and the earliest that has to be played again.
+23 -15
View File
@@ -12,7 +12,7 @@ constexpr int LOADOUT_PASSES_MAX = 6;
constexpr int LINE_LENGTH = 64; constexpr int LINE_LENGTH = 64;
constexpr float BINDING_WIDTH = 116; constexpr float BINDING_WIDTH = 116;
constexpr float FIELD_WIDTH = 120; constexpr float FIELD_WIDTH = 120;
// Colour and fashion slots: their size and the gap between, and the mark on Unassigned's. // Color and fashion slots: their size and the gap between, and the mark on Unassigned's.
constexpr float CHOICE_GAP = 4; constexpr float CHOICE_GAP = 4;
constexpr float CHOICE_SIZE = 32; constexpr float CHOICE_SIZE = 32;
constexpr const char* UNASSIGNED_MARK = "?"; constexpr const char* UNASSIGNED_MARK = "?";
@@ -53,6 +53,7 @@ constexpr const char* KEY_STICK_DEADZONE = "stick_deadzone";
constexpr const char* KEY_WINDOW_SCALE = "window_scale"; constexpr const char* KEY_WINDOW_SCALE = "window_scale";
constexpr const char* KEY_CHAT = "chat"; constexpr const char* KEY_CHAT = "chat";
constexpr const char* KEY_PING = "ping"; constexpr const char* KEY_PING = "ping";
constexpr const char* KEY_TIPS = "tips";
constexpr int DEADZONE_MAX = 60; constexpr int DEADZONE_MAX = 60;
constexpr int DEADZONE_STEP = 5; constexpr int DEADZONE_STEP = 5;
constexpr const char* KEY_VSYNC = "vsync"; constexpr const char* KEY_VSYNC = "vsync";
@@ -352,6 +353,8 @@ void Settings::load(Game& game)
isChatShown = number; isChatShown = number;
if (SDL_strcmp(line, KEY_PING) == 0) if (SDL_strcmp(line, KEY_PING) == 0)
isPingShown = number; isPingShown = number;
if (SDL_strcmp(line, KEY_TIPS) == 0)
isTipsShown = number;
if (SDL_strcmp(line, KEY_WINDOW_SCALE) == 0 && if (SDL_strcmp(line, KEY_WINDOW_SCALE) == 0 &&
std::find(std::begin(WINDOW_SCALES), std::end(WINDOW_SCALES), number) != std::end(WINDOW_SCALES)) std::find(std::begin(WINDOW_SCALES), std::end(WINDOW_SCALES), number) != std::end(WINDOW_SCALES))
windowScale = number; windowScale = number;
@@ -414,6 +417,7 @@ void Settings::save(Game& game)
KEY_MUTE_UNFOCUSED, isMutedUnfocused, KEY_DAMAGE_NUMBERS, isDamageShown, KEY_RUMBLE, KEY_MUTE_UNFOCUSED, isMutedUnfocused, KEY_DAMAGE_NUMBERS, isDamageShown, KEY_RUMBLE,
isRumbling, KEY_STICK_DEADZONE, stickDeadzone, KEY_WINDOW_SCALE, windowScale, KEY_CHAT, isRumbling, KEY_STICK_DEADZONE, stickDeadzone, KEY_WINDOW_SCALE, windowScale, KEY_CHAT,
isChatShown, KEY_PING, isPingShown); isChatShown, KEY_PING, isPingShown);
length += SDL_snprintf(text + length, sizeof(text) - length, "%s=%d\n", KEY_TIPS, isTipsShown);
length += SDL_snprintf(text + length, sizeof(text) - length, "%s=%d\n%s=%d\n%s=%d\n%s=", KEY_HUD_FPS, isFpsShown, length += SDL_snprintf(text + length, sizeof(text) - length, "%s=%d\n%s=%d\n%s=%d\n%s=", KEY_HUD_FPS, isFpsShown,
KEY_FPS_LIMIT, fpsLimit, KEY_FASHION, fashion, KEY_HINTS_LEARNED); KEY_FPS_LIMIT, fpsLimit, KEY_FASHION, fashion, KEY_HINTS_LEARNED);
for (int i = 0, gap = 0; i < HINT_COUNT; i++) for (int i = 0, gap = 0; i < HINT_COUNT; i++)
@@ -614,8 +618,11 @@ void Settings::draw(Game& game, Rect area, Page& page)
{ {
ui.hint_set(game, {x, y, TOGGLE_SPACING, row.h}, "Shows the damage you\ndeal each second."); ui.hint_set(game, {x, y, TOGGLE_SPACING, row.h}, "Shows the damage you\ndeal each second.");
ui.hint_set(game, {x + TOGGLE_SPACING, y, TOGGLE_SPACING, row.h}, "Shows how fast you move."); ui.hint_set(game, {x + TOGGLE_SPACING, y, TOGGLE_SPACING, row.h}, "Shows how fast you move.");
ui.hint_set(game, {x + TOGGLE_SPACING * 2, y, TOGGLE_SPACING, row.h},
"Shows tips on the ready\nscreen and under the menu.");
isHudChanged |= ui.toggle_draw(game, {x, y}, "DPS", isDpsShown); isHudChanged |= ui.toggle_draw(game, {x, y}, "DPS", isDpsShown);
isHudChanged |= ui.toggle_draw(game, {x + TOGGLE_SPACING, y}, "Speed", isSpeedShown); isHudChanged |= ui.toggle_draw(game, {x + TOGGLE_SPACING, y}, "Speed", isSpeedShown);
isHudChanged |= ui.toggle_draw(game, {x + TOGGLE_SPACING * 2, y}, "Tips", isTipsShown);
} }
if (ui.row_get(page, ROW_HEIGHT, y)) if (ui.row_get(page, ROW_HEIGHT, y))
{ {
@@ -757,14 +764,14 @@ void Settings::controls_draw(Game& game, Page& page, Rect row)
} }
} }
// The colour this screen's player asks for in a match: their pick, the session's random roll, or none. // The color this screen's player asks for in a match: their pick, the session's random roll, or none.
int Settings::color_get() int Settings::color_get()
{ {
return color; return color;
} }
// The Settings tab's Default: everything on it back as it starts out, except the controls (which have a Default of // The Settings tab's Default: everything on it back as it starts out, except the controls (which have a Default of
// their own) and the name and colour (which are Profile's). // their own) and the name and color (which are Profile's).
void Settings::defaults_restore(Game& game) void Settings::defaults_restore(Game& game)
{ {
Settings fresh; Settings fresh;
@@ -784,6 +791,7 @@ void Settings::defaults_restore(Game& game)
windowScale = fresh.windowScale; windowScale = fresh.windowScale;
isChatShown = fresh.isChatShown; isChatShown = fresh.isChatShown;
isPingShown = fresh.isPingShown; isPingShown = fresh.isPingShown;
isTipsShown = fresh.isTipsShown;
SDL_strlcpy(serverUrl, fresh.serverUrl, sizeof(serverUrl)); SDL_strlcpy(serverUrl, fresh.serverUrl, sizeof(serverUrl));
weaponSlots = fresh.weaponSlots; weaponSlots = fresh.weaponSlots;
trinketSlots = fresh.trinketSlots; trinketSlots = fresh.trinketSlots;
@@ -818,7 +826,7 @@ void Settings::online_draw(Game& game, Rect row, float labelWidth)
save(game); save(game);
} }
// The Profile tab: the screen name shown over this screen's fighter, the colour it asks for in matches (a swatch, or // The Profile tab: the screen name shown over this screen's fighter, the color it asks for in matches (a swatch, or
// Unassigned for whichever is free) and what it wears, each in slots under a separator, and the loadouts, scrolling // Unassigned for whichever is free) and what it wears, each in slots under a separator, and the loadouts, scrolling
// when they run long, with the fighter previewed on the right in them, walking. An emptied name goes back to the // when they run long, with the fighter previewed on the right in them, walking. An emptied name goes back to the
// default. // default.
@@ -848,7 +856,7 @@ void Settings::profile_draw(Game& game, Rect area, Page& page)
Tooltip hovered; Tooltip hovered;
Color worn = color >= 0 ? PALETTE[color] : WHITE; Color worn = color >= 0 ? PALETTE[color] : WHITE;
// The colours, then Unassigned (for whichever's free) last. Hovering one names it, in a window of that colour. // The colors, then Unassigned (for whichever's free) last. Hovering one names it, in a window of that color.
if (ui.row_get(page, ROW_HEIGHT, separator.y)) if (ui.row_get(page, ROW_HEIGHT, separator.y))
ui.separator_draw(game, separator, "Color"); ui.separator_draw(game, separator, "Color");
slots_draw(COLOR_COUNT + 1, [&](int i, Rect box) { slots_draw(COLOR_COUNT + 1, [&](int i, Rect box) {
@@ -894,7 +902,7 @@ void Settings::profile_draw(Game& game, Rect area, Page& page)
stats_draw(game, page, {list.x, y, separator.w, WIDGET_HEIGHT}, hovered); stats_draw(game, page, {list.x, y, separator.w, WIDGET_HEIGHT}, hovered);
ui.page_end(game, page); ui.page_end(game, page);
// The preview: the fighter in the colour picked and what it wears, its name over it, and under it the colour's name // The preview: the fighter in the color picked and what it wears, its name over it, and under it the color's name
// (or what Unassigned means) and the worn item's. // (or what Unassigned means) and the worn item's.
char caption[LINE_LENGTH * 2]; char caption[LINE_LENGTH * 2];
SDL_snprintf(caption, sizeof(caption), "%s%s%s", color >= 0 ? COLOR_NAMES[color] : "Given at match start", SDL_snprintf(caption, sizeof(caption), "%s%s%s", color >= 0 ? COLOR_NAMES[color] : "Given at match start",
@@ -944,12 +952,12 @@ void Settings::restore(Game& game)
// Stood a pixel up, so the feet don't start in the floor. // Stood a pixel up, so the feet don't start in the floor.
Rect box = fighter.body.box; Rect box = fighter.body.box;
Vec2 centre = {sandbox.feet.x, sandbox.feet.y - box.h / 2 - 1}; Vec2 center = {sandbox.feet.x, sandbox.feet.y - box.h / 2 - 1};
for (float y = -box.h / 2; y <= box.h / 2; y += CLEAR_PROBE) for (float y = -box.h / 2; y <= box.h / 2; y += CLEAR_PROBE)
for (float x = -box.w / 2; x <= box.w / 2; x += CLEAR_PROBE) for (float x = -box.w / 2; x <= box.w / 2; x += CLEAR_PROBE)
if (game.sim.world.is_blocking_at({centre.x + x, centre.y + y})) if (game.sim.world.is_blocking_at({center.x + x, center.y + y}))
return; return;
fighter.teleport_to(centre); fighter.teleport_to(center);
} }
// Saves once anything has changed and the mouse has let go: rules as soon as they're edited, and the sandbox as a // Saves once anything has changed and the mouse has let go: rules as soon as they're edited, and the sandbox as a
@@ -1114,9 +1122,9 @@ void Settings::loadouts_draw(Game& game, Rect row, Tooltip& hovered)
{ {
if (items[i] < 0) if (items[i] < 0)
continue; continue;
Vec2 centre = {row.x + LABEL_WIDTH + i * (size + LOADOUT_GAP) + size / 2, y}; Vec2 center = {row.x + LABEL_WIDTH + i * (size + LOADOUT_GAP) + size / 2, y};
ui.icon_draw(game, kind, items[i], {.color = ui.tint, .position = centre}, ICON_SMALL); ui.icon_draw(game, kind, items[i], {.color = ui.tint, .position = center}, ICON_SMALL);
Rect box = {centre.x - size / 2, centre.y - size / 2, size, size}; Rect box = {center.x - size / 2, center.y - size / 2, size, size};
if (is_point_in_rect(game.input.mouse, box)) if (is_point_in_rect(game.input.mouse, box))
hovered = ui.tooltip_build(game, kind, items[i], loadout.levels[kind][i]); hovered = ui.tooltip_build(game, kind, items[i], loadout.levels[kind][i]);
} }
@@ -1167,9 +1175,9 @@ void Settings::stats_draw(Game& game, Page& page, Rect row, Tooltip& hovered)
ui.label_draw(game, "None yet", {value, y}); ui.label_draw(game, "None yet", {value, y});
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
Vec2 centre = {value + i * (size + LOADOUT_GAP) + size / 2, y + WIDGET_HEIGHT / 2}; Vec2 center = {value + i * (size + LOADOUT_GAP) + size / 2, y + WIDGET_HEIGHT / 2};
ui.icon_draw(game, kind, items[i], {.color = ui.tint, .position = centre}, ICON_SMALL); ui.icon_draw(game, kind, items[i], {.color = ui.tint, .position = center}, ICON_SMALL);
Rect box = {centre.x - size / 2, centre.y - size / 2, size, size}; Rect box = {center.x - size / 2, center.y - size / 2, size, size};
if (is_point_in_rect(game.input.mouse, box)) if (is_point_in_rect(game.input.mouse, box))
hovered = ui.tooltip_build(game, kind, items[i], sandbox.item_levels_get(kind)[items[i]]); hovered = ui.tooltip_build(game, kind, items[i], sandbox.item_levels_get(kind)[items[i]]);
} }
+3 -2
View File
@@ -49,8 +49,8 @@ struct Loadout
// The player's preferences, kept as key=value lines in the user's prefs folder: saved after every change and read // The player's preferences, kept as key=value lines in the user's prefs folder: saved after every change and read
// back at startup. Apply pushes them all to the window, renderer, audio and the sandbox fighter's slots. The menu's // back at startup. Apply pushes them all to the window, renderer, audio and the sandbox fighter's slots. The menu's
// Settings tab edits them (and the control bindings, which live in Input), and Profile the name and colour. Colour // Settings tab edits them (and the control bindings, which live in Input), and Profile the name and color. Color
// is a palette index, unassigned (given one at the match) or random (session colour, rolled once per run). The match // is a palette index, unassigned (given one at the match) or random (session color, rolled once per run). The match
// rules and the sandbox (see SandboxSave) are kept too: rules as soon as they change, the sandbox when the game quits // rules and the sandbox (see SandboxSave) are kept too: rules as soon as they change, the sandbox when the game quits
// or a match starts (and every few seconds on the web, where tabs close without quitting), restored once at startup // or a match starts (and every few seconds on the web, where tabs close without quitting), restored once at startup
// (restore). Update saves anything changed once the mouse lets go. // (restore). Update saves anything changed once the mouse lets go.
@@ -82,6 +82,7 @@ public:
bool isRumbling = true; bool isRumbling = true;
bool isShaking = true; bool isShaking = true;
bool isSpeedShown = false; bool isSpeedShown = false;
bool isTipsShown = true;
bool isVsync = true; bool isVsync = true;
// The server online play goes through (see protocol.h), and whether the Play tab's rules are unfolded. // The server online play goes through (see protocol.h), and whether the Play tab's rules are unfolded.
char serverUrl[SERVER_URL_LENGTH] = {}; char serverUrl[SERVER_URL_LENGTH] = {};
+1 -1
View File
@@ -1,5 +1,5 @@
# The simulation: every source that steps a game, free of SDL, shared by the game and the server. Floating point is # The simulation: every source that steps a game, free of SDL, shared by the game and the server. Floating point is
# kept strict wherever it's used (see the top CMakeLists), as inline maths in the headers compiles into its users too. # kept strict wherever it's used (see the top CMakeLists), as inline math in the headers compiles into its users too.
set(SIM_SOURCES set(SIM_SOURCES
${CMAKE_CURRENT_LIST_DIR}/bot.cpp ${CMAKE_CURRENT_LIST_DIR}/bot.cpp
${CMAKE_CURRENT_LIST_DIR}/fighter.cpp ${CMAKE_CURRENT_LIST_DIR}/fighter.cpp
+1 -1
View File
@@ -27,7 +27,7 @@ void Sim::tick()
} }
// A fresh online game for some of a lobby's fighters (by index) to play by the rules, bots filling the rest when the // A fresh online game for some of a lobby's fighters (by index) to play by the rules, bots filling the rest when the
// rules say to. Their members, names and colours come along, and it starts from the seed, so everyone builds the same // rules say to. Their members, names and colors come along, and it starts from the seed, so everyone builds the same
// one. The bots keep their difficulty. // one. The bots keep their difficulty.
void Sim::game_start(Match& lobby, const int* members, int count, const Rules& rules, Uint32 seed) void Sim::game_start(Match& lobby, const int* members, int count, const Rules& rules, Uint32 seed)
{ {
+1 -1
View File
@@ -19,7 +19,7 @@ void Sprites::init(Game& game)
} }
// What a pose wears, its layers on the player nulls they're named after (as drawn: placed, flipped, scaled and turned // What a pose wears, its layers on the player nulls they're named after (as drawn: placed, flipped, scaled and turned
// with the pose), in its colour. // with the pose), in its color.
void Sprites::fashion_draw(Renderer& renderer, Pose& pose, int worn) void Sprites::fashion_draw(Renderer& renderer, Pose& pose, int worn)
{ {
Anm2Animation* animation = worn > FASHION_NONE && worn < FASHION_COUNT && pose.art Anm2Animation* animation = worn > FASHION_NONE && worn < FASHION_COUNT && pose.art
+229 -141
View File
@@ -10,7 +10,7 @@ constexpr float LOADOUT_FADE = 0.25f;
constexpr float LOADOUT_GAP = 4; constexpr float LOADOUT_GAP = 4;
constexpr float LOADOUT_DIVIDER = 2; constexpr float LOADOUT_DIVIDER = 2;
constexpr float ICON_SIZE = 24; constexpr float ICON_SIZE = 24;
// How big a fashion item's cell is (its art is centred in it). // How big a fashion item's cell is (its art is centerd in it).
constexpr float FASHION_CELL = 32; constexpr float FASHION_CELL = 32;
constexpr float RIVAL_BAR_MIN = 16; constexpr float RIVAL_BAR_MIN = 16;
constexpr float RIVAL_DIVIDER = 1; constexpr float RIVAL_DIVIDER = 1;
@@ -116,6 +116,10 @@ constexpr float FIELD_INSET = 4;
constexpr float SLIDER_INSET = 3; constexpr float SLIDER_INSET = 3;
constexpr float TEXT_INSET = (WIDGET_HEIGHT - LINE_HEIGHT) / 2; constexpr float TEXT_INSET = (WIDGET_HEIGHT - LINE_HEIGHT) / 2;
constexpr float HINT_FADE = 0.5f; constexpr float HINT_FADE = 0.5f;
// How much likelier a tip that fits what's at hand is to be picked than one that doesn't.
constexpr float TIP_FIT_WEIGHT = 1.5f;
// How long tips take to fade in and out.
constexpr float TIP_FADE = 0.4f;
constexpr const char* HINT_SPACING = " "; constexpr const char* HINT_SPACING = " ";
// How wide a row of first-run hints may run, clear of the hotbars in the corners. // How wide a row of first-run hints may run, clear of the hotbars in the corners.
constexpr float HINT_ROW_WIDTH = 360; constexpr float HINT_ROW_WIDTH = 360;
@@ -142,103 +146,123 @@ constexpr Vec2 LABEL_OFFSETS[ICON_TINY] = {{4, 2}, {3, 2}};
constexpr Vec2 ICON_LABEL_OFFSET = {LABEL_OFFSETS[ICON_FULL].x - (SLOT_SIZE - ICON_SIZE) / 2, constexpr Vec2 ICON_LABEL_OFFSET = {LABEL_OFFSETS[ICON_FULL].x - (SLOT_SIZE - ICON_SIZE) / 2,
LABEL_OFFSETS[ICON_FULL].y - (SLOT_SIZE - ICON_SIZE) / 2}; LABEL_OFFSETS[ICON_FULL].y - (SLOT_SIZE - ICON_SIZE) / 2};
// The situations a tip fits, combined as flags: playing online or offline, and the menu being open.
enum TipContext
{
TIP_ONLINE = 1,
TIP_OFFLINE = 2,
TIP_MENU = 4
};
// A tip for the ready screen, and what it's about: a weapon or trinket by name, and the situations it fits (see
// TipContext). One that fits what's at hand (that item equipped, or one of those situations) is TIP_FIT_WEIGHT times as
// likely to be picked (see tip_pick).
struct Tip
{
const char* text;
const char* item = nullptr;
int contexts = 0;
};
// Tips shown under the ready button, one at random: movement and knockback, cooldowns and Charge, projectiles, each // Tips shown under the ready button, one at random: movement and knockback, cooldowns and Charge, projectiles, each
// Weapon and Trinket in turn, then the menus. // Weapon and Trinket in turn, then the menus.
constexpr const char* TIPS[] = { constexpr Tip TIPS[] = {
"Getting knocked into walls hurts! Do your best to reduce your speed when attacked!", {"Getting knocked into walls hurts! Do your best to reduce your speed when attacked!"},
"Don't get cornered! Knockback damage can add up fast!", {"Don't get cornered! Knockback damage can add up fast!"},
"Crouch to reduce knockback!", {"Crouch to reduce knockback!"},
"Crawl around by crouching and moving!", {"Crawl around by crouching and moving!"},
"Roll around on sloped surfaces with Down!", {"Roll around on sloped surfaces with Down!"},
"Platforms block shots. Use them as cover!", {"Platforms block shots. Use them as cover!"},
"Weapons don't share cooldowns! Switch Weapons to maximize damage!", {"Weapons don't share cooldowns! Switch Weapons to maximize damage!"},
"When a Weapon enters cooldown, it will fade and return to normal when it's done!", {"When a Weapon enters cooldown, it will fade and return to normal when it's done!"},
"Your cursor's color and vibration can show you your Weapon's cooldown or Charge!", {"Your cursor's color and vibration can show you your Weapon's cooldown or Charge!"},
"Let go of a Charge early for a Basic Attack that hits harder!", {"Let go of a Charge early for a Basic Attack that hits harder!"},
"Your Charge carries over when you swap Weapons!", {"Your Charge carries over when you swap Weapons!"},
"Higher damaging attacks will glow and vibrate!", {"Higher damaging attacks will glow and vibrate!"},
"Going faster makes you glow and vibrate, showing that your attacks deal more damage!", {"Going faster makes you glow and vibrate, showing that your attacks deal more damage!"},
"Almost every attack counts as a projectile! See how you can change their traits with Trinkets!", {"Almost every attack counts as a projectile! See how you can change their traits with Trinkets!"},
"Projectiles inherit your velocity. Go fast for big damage!", {"Projectiles inherit your velocity. Go fast for big damage!"},
"Projectiles lose damage the longer they're out. Get close!", {"Projectiles lose damage the longer they're out. Get close!"},
"Projectiles will fade with time; this fade shows their damage potential!", {"Projectiles will fade with time; this fade shows their damage potential!"},
"Increasing a projectile's lifetime or speed can both increase its range!", {"Increasing a projectile's lifetime or speed can both increase its range!"},
"Explosions hit hardest at their center!", {"Explosions hit hardest at their center!"},
"Your explosions don't hurt, and only push you. Try rocket jumping!", {"Your explosions don't hurt, and only push you. Try rocket jumping!"},
"Recoil is movement. Fire away from where you want to go!", {"Recoil is movement. Fire away from where you want to go!"},
"Mix and match Weapons and Trinkets for nigh-infinite amounts of combos!", {"Mix and match Weapons and Trinkets for nigh-infinite amounts of combos!", nullptr, TIP_MENU},
"The Fist's Charged Attack doesn't always send you straight up! Change your aim!", {"The Fist's Charged Attack doesn't always send you straight up! Change your aim!", "Fist"},
"Slam a Hammer into surfaces to fly far!", {"Slam a Hammer into surfaces to fly far!", "Hammer"},
"The Lance's Charged Attack lunges you forward!", {"The Lance's Charged Attack lunges you forward!", "Lance"},
"Crack the Whip against a wall to be pulled toward it!", {"Crack the Whip against a wall to be pulled toward it!", "Whip"},
"The Whip's Charged Attack only blasts at the very tip, but does big damage!", {"The Whip's Charged Attack only blasts at the very tip, but does big damage!", "Whip"},
"Charge a Flail to throw its ball, and Charge again to reel it back!", {"Charge a Flail to throw its ball, and Charge again to reel it back!", "Flail"},
"You can have the Flail's ball extended and another Weapon equipped for a dangerous combo!", {"You can have the Flail's ball extended and another Weapon equipped for a dangerous combo!", "Flail"},
"Charged Boomerangs are platforms only you can ride!", {"Charged Boomerangs are platforms only you can ride!", "Boomerang"},
"Charge recalls your Boomerangs!", {"Charge recalls your Boomerangs!", "Boomerang"},
"A Grapple's Charged Attack yanks you to your target!", {"A Grapple's Charged Attack yanks you to your target!", "Grapple"},
"Swing around on or release a latched Grapple for big speed!", {"Swing around on or release a latched Grapple for big speed!", "Grapple"},
"You can have a Grapple latched and another Weapon equipped for an acrobatic fight!", {"You can have a Grapple latched and another Weapon equipped for an acrobatic fight!", "Grapple"},
"A Bow's arrows stuck in walls are semi-solid platforms!", {"A Bow's arrows stuck in walls are semi-solid platforms!", "Bow"},
"The Bow's arrows fly faster and further with Charge!", {"The Bow's arrows fly faster and further with Charge!", "Bow"},
"Fire a wide volley with the Uzi's Charged Attack!", {"Fire a wide volley with the Uzi's Charged Attack!", "Uzi"},
"Use a Shotgun's recoil for great movement!", {"Use a Shotgun's recoil for great movement!", "Shotgun"},
"The Bolter's projectiles can cling onto surfaces, perfect for area denial!", {"The Bolter's projectiles can cling onto surfaces, perfect for area denial!", "Bolter"},
"Cook a Grenade Launcher's grenade via Charging to explode it sooner!", {"Cook a Grenade Launcher's grenade via Charging to explode it sooner!", "Grenade Launcher"},
"Fully Charging a Grenade Launcher will cause an explosion in your face!", {"Fully Charging a Grenade Launcher will cause an explosion in your face!", "Grenade Launcher"},
"Hit the Grenade Launcher's projectile and explosion at the same time for big damage! Double Donk!", {"Hit the Grenade Launcher's projectile and explosion at the same time for big damage! Double Donk!",
"Mines need a moment to arm before they'll go off!", "Grenade Launcher"},
"The Flamethrower's Charged Attack detonates mid-air! Space an explosion!", {"Mines need a moment to arm before they'll go off!", "Mine"},
"The Vacuum's Charged Attack deals big knockback! Send foes flying!", {"The Flamethrower's Charged Attack detonates mid-air! Space an explosion!", "Flamethrower"},
"The Vacuum's Charged Attack turns enemy shots into your own!", {"The Vacuum's Charged Attack deals big knockback! Send foes flying!", "Vacuum"},
"Hold your own projectiles in the Vacuum's suction to keep them fresh!", {"The Vacuum's Charged Attack turns enemy shots into your own!", "Vacuum"},
"The Vacuum's suction and Charged Attack can move projectiles around. Manipulate the battlefield!", {"Hold your own projectiles in the Vacuum's suction to keep them fresh!", "Vacuum"},
"Use the Grimoire's wisps as a close-range attack or to pepper from afar!", {"The Vacuum's suction and Charged Attack can move projectiles around. Manipulate the battlefield!", "Vacuum"},
"Use the Grimoire's teleporting Charged Attack to escape danger!", {"Use the Grimoire's wisps as a close-range attack or to pepper from afar!", "Grimoire"},
{"Use the Grimoire's teleporting Charged Attack to escape danger!", "Grimoire"},
"Diazepam reduces Weapon spread, perfect for concentrated attacks!", {"Diazepam reduces Weapon spread, perfect for concentrated attacks!", "Diazepam"},
"Splitter copies fan wider with each level!", {"Splitter copies fan wider with each level!", "Splitter"},
"The off-hand Weapon from Dual Wield is the one you last switched from!", {"The off-hand Weapon from Dual Wield is the one you last switched from!", "Dual Wield"},
"Ricochet makes projectiles deal more damage the more they've bounced!", {"Ricochet makes projectiles deal more damage the more they've bounced!", "Ricochet"},
"Ricochet bounces reset a projectile's lifetime!", {"Ricochet bounces reset a projectile's lifetime!", "Ricochet"},
"Tesla Coil arcs electricity between your projectiles. Make traps or wide danger zones!", {"Tesla Coil arcs electricity between your projectiles. Make traps or wide danger zones!", "Tesla Coil"},
"The kickback the Recoil Coil gives is perfect for movement!", {"The kickback the Recoil Coil gives is perfect for movement!", "Recoil Coil"},
"Stay in place with a Shock Absorber!", {"Stay in place with a Shock Absorber!", "Shock Absorber"},
"Embiggener makes you a bigger target, but gives a solid damage buff!", {"Embiggener makes you a bigger target, but gives a solid damage buff!", "Embiggener"},
"Make yourself a living hazard with a Pebble Helmet!", {"Make yourself a living hazard with a Pebble Helmet!", "Pebble Helmet"},
"With a Speedometer, moving fast and attacking deals bigger damage!", {"With a Speedometer, moving fast and attacking deals bigger damage!", "Speedometer"},
"A pair of Wings will make you dash to where you're aiming when you jump mid-air!", {"A pair of Wings will make you dash to where you're aiming when you jump mid-air!", "Wings"},
"With the Feather, hold Up in the air to float!", {"With the Feather, hold Up in the air to float!", "Feather"},
"With Iron Weights, faster falls produce bigger blasts!", {"With Iron Weights, faster falls produce bigger blasts!", "Iron Weights"},
"Cling, slide, and kick off walls with a Climb Claw!", {"Cling, slide, and kick off walls with a Climb Claw!", "Climb Claw"},
"Create a Lobby and share its link or code with friends for multiplayer fun!", {"Create a Lobby and share its link or code with friends for multiplayer fun!", nullptr, TIP_OFFLINE},
"When in a Lobby, queue up a game in the menu!", {"When in a Lobby, queue up a game in the menu!", nullptr, TIP_ONLINE},
"Queuing takes place in the Sandbox! Holler fighters over to your queued area and Ready up!", {"Queuing takes place in the Sandbox! Holler fighters over to your queued area and Ready up!", nullptr, TIP_ONLINE},
"Change a game's rules underneath the Rules option!", {"Change a game's rules underneath the Rules option!", nullptr, TIP_MENU},
"If a game has a time limit, the winner will be decided based on who has the most health!", {"If a game has a time limit, the winner will be decided based on who has the most health!"},
"Is your name \"Fighter\"? Change it in Profile!", {"Is your name \"Fighter\"? Change it in Profile!", nullptr, TIP_MENU},
"Change your fighter and HUD color in Profile!", {"Change your fighter and HUD color in Profile!", nullptr, TIP_MENU},
"Feeling naked and unsexy? Wear some Fashion within your Profile!", {"Feeling naked and unsexy? Wear some Fashion within your Profile!", nullptr, TIP_MENU},
"Set and load custom gear Loadouts in your Profile!", {"Set and load custom gear Loadouts in your Profile!", nullptr, TIP_MENU},
"Save a gear Loadout and use Shift + a number to equip it!", {"Save a gear Loadout and use Shift + a number to equip it!", nullptr, TIP_MENU},
"Turn on DPS in Settings to see the damage you deal each second!", {"Turn on DPS in Settings to see the damage you deal each second!", nullptr, TIP_MENU},
"Turn on Speed in Settings to see how fast you're going!", {"Turn on Speed in Settings to see how fast you're going!", nullptr, TIP_MENU},
"Turn on FPS in Settings to see how smoothly the game runs!", {"Turn on FPS in Settings to see how smoothly the game runs!", nullptr, TIP_MENU},
"Is previewing your aim too intrusive? Toggle Aim Preview off in Settings!", {"Is previewing your aim too intrusive? Toggle Aim Preview off in Settings!", nullptr, TIP_MENU},
"Want to see how things actually collide? Toggle Display Collision in Settings!", {"Want to see how things actually collide? Toggle Display Collision in Settings!", nullptr, TIP_MENU},
"Game too loud? Turn down the Volume or Mute it in Settings!", {"Game too loud? Turn down the Volume or Mute it in Settings!", nullptr, TIP_MENU},
"Don't like the Controls? Tweak them to your liking in Settings!", {"Don't like the Controls? Tweak them to your liking in Settings!", nullptr, TIP_MENU},
"Want to experiment with more Weapon or Trinket slots in the Sandbox? Change them in Settings!", {"Want to experiment with more Weapon or Trinket slots in the Sandbox? Change them in Settings!", nullptr,
"Want to fight in the Sandbox with others? Check PvP in Settings or use the /pvp command!", TIP_MENU},
"The Dummy in the Sandbox doesn't feel pain! Beat it as much as you'd like, it enjoys it!", {"Want to fight in the Sandbox with others? Check PvP in Settings or use the /pvp command!", nullptr, TIP_ONLINE},
"Throw tomatoes to mock a game's winner in between rounds by clicking anywhere on screen!", {"The Dummy in the Sandbox doesn't feel pain! Beat it as much as you'd like, it enjoys it!"},
"Sling slurs and trashtalk all you wish with a Chat message!", {"Throw tomatoes to mock a game's winner in between rounds by clicking anywhere on screen!"},
"A number of useful / commands are present in Chat! Type /help to see!", {"Sling slurs and trashtalk all you wish with a Chat message!", nullptr, TIP_ONLINE},
"Want to see the sexy person who made this game? Check About in the menu!", {"A number of useful / commands are present in Chat! Type /help to see!", nullptr, TIP_ONLINE},
{"Want to see the sexy person who made this game? Check About in the menu!", nullptr, TIP_MENU},
}; };
static CountdownInfo countdown_info_get(Game& game); static CountdownInfo countdown_info_get(Game& game);
@@ -287,8 +311,9 @@ void Ui::draw(Game& game)
shade = color_shade(tint, SECONDARY_SHADE); shade = color_shade(tint, SECONDARY_SHADE);
Vec2 mouse = game.input.mouse; Vec2 mouse = game.input.mouse;
// The menu covers the match overlays. Hotbars sit under them, except in the sandbox menu they slide into. With // The menu covers the match overlays. Hotbars sit under them (hidden while the menu is up, except on its item tabs,
// several players on this screen in a match, each gets a compact panel along the bottom instead. // which they slide under). With several players on this screen in a match, each gets a compact panel along the
// bottom instead.
statusRect = {}; statusRect = {};
queues_draw(game); queues_draw(game);
bars_draw(game); bars_draw(game);
@@ -298,7 +323,7 @@ void Ui::draw(Game& game)
offscreen_draw(game, camera); offscreen_draw(game, camera);
bool isBarsRaised = game.menu.is_bars_open(); bool isBarsRaised = game.menu.is_bars_open();
bool isShared = game.sim.match.state != MATCH_PRACTICE && game.local_count() > 1; bool isShared = game.sim.match.state != MATCH_PRACTICE && game.local_count() > 1;
if (!isBarsRaised && !isShared && !isWatching) if (!game.menu.isOpen && !isShared && !isWatching)
{ {
cooldowns_draw(game); cooldowns_draw(game);
hotbar_draw(game); hotbar_draw(game);
@@ -317,6 +342,8 @@ void Ui::draw(Game& game)
game.menu.draw(game); game.menu.draw(game);
if (game.sim.match.state == MATCH_COUNTDOWN && game.menu.isOpen && !isShared) if (game.sim.match.state == MATCH_COUNTDOWN && game.menu.isOpen && !isShared)
countdown_aside_draw(game); countdown_aside_draw(game);
else if (game.menu.isOpen)
tip_draw(game, tip_low_get());
if (isBarsRaised) if (isBarsRaised)
hotbar_draw(game); hotbar_draw(game);
crosshairs_draw(game, camera); crosshairs_draw(game, camera);
@@ -327,7 +354,7 @@ void Ui::draw(Game& game)
} }
// Stacked above each living fighter: in a deathmatch its health bar, then (in a match, or a lobby's sandbox with others // Stacked above each living fighter: in a deathmatch its health bar, then (in a match, or a lobby's sandbox with others
// in it) its name in its colour, then the local fighter's charge bar while charging, which blinks once full. // in it) its name in its color, then the local fighter's charge bar while charging, which blinks once full.
void Ui::bars_draw(Game& game) void Ui::bars_draw(Game& game)
{ {
Match& match = game.sim.match; Match& match = game.sim.match;
@@ -356,7 +383,7 @@ void Ui::bars_draw(Game& game)
bool isNamed = isMatch || (game.session.isActive && game.sim.fighterCount > 1); bool isNamed = isMatch || (game.session.isActive && game.sim.fighterCount > 1);
if (isNamed) if (isNamed)
{ {
// Online, the player's connection beside it, the two centred together. // Online, the player's connection beside it, the two centerd together.
const char* name = name_get(game, i); const char* name = name_get(game, i);
Vec2 size = game.text.size_get(name); Vec2 size = game.text.size_get(name);
bottom -= size.y; bottom -= size.y;
@@ -394,11 +421,11 @@ void Ui::match_draw(Game& game)
{ {
Match& match = game.sim.match; Match& match = game.sim.match;
char text[TOOLTIP_LENGTH]; char text[TOOLTIP_LENGTH];
isTipChosen &= match.state == MATCH_COUNTDOWN;
Input& input = game.input; Input& input = game.input;
if (match.state == MATCH_PRACTICE) if (match.state == MATCH_PRACTICE)
{ {
hints_draw(game); if (!game.menu.isOpen)
hints_draw(game);
return; return;
} }
@@ -407,7 +434,7 @@ void Ui::match_draw(Game& game)
race_draw(game); race_draw(game);
else else
{ {
// The score: each fighter's wins in its own colour, the dashes between them white. It's centred along the top, // The score: each fighter's wins in its own color, the dashes between them white. It's centerd along the top,
// unless a long weapon hotbar reaches it in the HUD, when it moves right to clear the hotbar (the clock under // unless a long weapon hotbar reaches it in the HUD, when it moves right to clear the hotbar (the clock under
// it moves with it). It stays put while the menu moves the hotbar. // it moves with it). It stays put while the menu moves the hotbar.
Renderer& renderer = game.renderer; Renderer& renderer = game.renderer;
@@ -460,9 +487,6 @@ void Ui::match_draw(Game& game)
CountdownInfo info = countdown_info_get(game); CountdownInfo info = countdown_info_get(game);
bool isWatching = game.session.is_watching(game); bool isWatching = game.session.is_watching(game);
bool isLocalReady = match.isReady[game.local] || isWatching; bool isLocalReady = match.isReady[game.local] || isWatching;
if (!isTipChosen)
tip = random_get(0, std::size(TIPS));
isTipChosen = true;
if (game.menu.isOpen) if (game.menu.isOpen)
break; break;
@@ -490,8 +514,7 @@ void Ui::match_draw(Game& game)
widgets_enable(true); widgets_enable(true);
y += READY_BUTTON_HEIGHT + READY_GAP; y += READY_BUTTON_HEIGHT + READY_GAP;
SDL_snprintf(text, sizeof(text), "Tip: %s", TIPS[tip]); tip_draw(game, y);
text_center_draw(game, text, y, 1);
y += LINE_HEIGHT + TIP_GAP; y += LINE_HEIGHT + TIP_GAP;
loadoutTop = y; loadoutTop = y;
if (!game.menu.isOpen && game.local_count() == 1) if (!game.menu.isOpen && game.local_count() == 1)
@@ -520,7 +543,7 @@ void Ui::match_draw(Game& game)
SDL_snprintf(text, sizeof(text), "%s Wins!", name_get(game, match.winner)); SDL_snprintf(text, sizeof(text), "%s Wins!", name_get(game, match.winner));
else else
SDL_strlcpy(text, "Draw", sizeof(text)); SDL_strlcpy(text, "Draw", sizeof(text));
// A win is announced in the winner's colour. // A win is announced in the winner's color.
text_center_draw(game, text, BANNER_Y, ROUND_SCALE, text_center_draw(game, text, BANNER_Y, ROUND_SCALE,
match.winner >= 0 ? game.color_get(match.winner) : tint); match.winner >= 0 ? game.color_get(match.winner) : tint);
break; break;
@@ -579,7 +602,7 @@ static void time_format(char* text, int size, float seconds)
SDL_snprintf(text, size, "%d:%05.2f", minutes, seconds - minutes * SECONDS_PER_MINUTE); SDL_snprintf(text, size, "%d:%05.2f", minutes, seconds - minutes * SECONDS_PER_MINUTE);
} }
// Along the top of a race: the clock, and a track with a dot in each racer's colour showing how far along the course // Along the top of a race: the clock, and a track with a dot in each racer's color showing how far along the course
// it is. // it is.
void Ui::race_draw(Game& game) void Ui::race_draw(Game& game)
{ {
@@ -602,7 +625,7 @@ void Ui::race_draw(Game& game)
} }
} }
// The race's finishing order from y down, one line per racer in its colour, with its time (or DNF). Returns the y // The race's finishing order from y down, one line per racer in its color, with its time (or DNF). Returns the y
// below the last line. // below the last line.
float Ui::results_draw(Game& game, float y) float Ui::results_draw(Game& game, float y)
{ {
@@ -736,7 +759,7 @@ static float beside_icon_get(Game& game, Tooltip& tooltip, Font font)
return width; return width;
} }
// The local fighter's loadout laid out in a centred grid between top and bottom: weapons and trinkets side by side // The local fighter's loadout laid out in a centerd grid between top and bottom: weapons and trinkets side by side
// when they all fit in one row, otherwise weapons above trinkets, a kind's panels wrapping onto more rows as needed, // when they all fit in one row, otherwise weapons above trinkets, a kind's panels wrapping onto more rows as needed,
// with a line between the two kinds (down between them, or across). Every panel is as wide as the widest so they line // with a line between the two kinds (down between them, or across). Every panel is as wide as the widest so they line
// up, and a row as tall as its kind's tallest. It never moves up over what's above it: it takes the fullest style that // up, and a row as tall as its kind's tallest. It never moves up over what's above it: it takes the fullest style that
@@ -880,7 +903,7 @@ void Ui::loadout_draw(Game& game, float alpha, float top)
} }
} }
// Text centred across the canvas with its top at y. // Text centerd across the canvas with its top at y.
// What the countdown shows: the round's name, whole seconds left (or Starting... once everyone's ready), how many are // 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. // ready, and the key that readies up.
static CountdownInfo countdown_info_get(Game& game) static CountdownInfo countdown_info_get(Game& game)
@@ -903,6 +926,72 @@ static CountdownInfo countdown_info_get(Game& game)
return info; return info;
} }
// Tips show on the ready screen and while the menu is up, from one continuous run that turns over every TIP_DURATION
// whether or not they're showing. Each fits what's at hand when it's picked (the first, on the first tick).
void Ui::tips_update(Game& game)
{
tipAge += TICK;
if (tipAge < TIP_DURATION)
return;
tip = tip_pick(game);
tipAge = 0;
}
// The tip showing (unless tips are turned off), centered at y in small text, fading in as it comes and out before the
// next.
void Ui::tip_draw(Game& game, float y)
{
if (!game.settings.isTipsShown)
return;
char text[TOOLTIP_LENGTH];
SDL_snprintf(text, sizeof(text), "Tip: %s", TIPS[tip].text);
float alpha = std::clamp(std::min(tipAge, TIP_DURATION - tipAge) / TIP_FADE, 0.0f, 1.0f);
text_center_draw(game, text, y, 1, alpha);
}
// Where a tip goes under the menu: below the hotbars' row (and the gamepad hints, when they show).
float Ui::tip_low_get()
{
return BAR_Y + SLOT_SIZE + HINT_GAP + (isFocusShown ? LINE_HEIGHT : 0);
}
// Picks the next tip (never the one showing), at random but TIP_FIT_WEIGHT times as likely for one that fits what's at
// hand: about a weapon or trinket this screen's fighter has equipped, or fitting how it's playing (online or offline)
// or the menu being open.
int Ui::tip_pick(Game& game)
{
Fighter& local = game.local_get();
int contexts = (game.session.isActive ? TIP_ONLINE : TIP_OFFLINE) | (game.menu.isOpen ? TIP_MENU : 0);
auto is_equipped = [&](const char* name) {
for (ItemKind kind : ITEM_KINDS)
{
int item = item_find(kind, name);
int* loadout = local.loadout_get(kind);
int slots = local.slots_get(kind);
if (item >= 0 && std::find(loadout, loadout + slots, item) != loadout + slots)
return true;
}
return false;
};
float weights[std::size(TIPS)];
float total = 0;
for (size_t i = 0; i < std::size(TIPS); i++)
{
const Tip& candidate = TIPS[i];
bool isFit = (candidate.contexts & contexts) || (candidate.item && is_equipped(candidate.item));
weights[i] = (int)i == tip ? 0 : isFit ? TIP_FIT_WEIGHT : 1;
total += weights[i];
}
float roll = random_get(0, total);
for (size_t i = 0; i < std::size(TIPS); i++)
{
roll -= weights[i];
if (roll < 0)
return i;
}
return std::size(TIPS) - 1;
}
// Marks a first-run hint learned once this screen's fighter has done what it says (a full charge for the charged // Marks a first-run hint learned once this screen's fighter has done what it says (a full charge for the charged
// attack), keeping it in the settings across launches, and fades learned ones out. // attack), keeping it in the settings across launches, and fades learned ones out.
void Ui::hints_update(Game& game) void Ui::hints_update(Game& game)
@@ -931,7 +1020,7 @@ void Ui::hints_update(Game& game)
} }
} }
// The first-run hints still showing, each with its key, centred in rows along the bottom of the sandbox, a row going up // The first-run hints still showing, each with its key, centerd in rows along the bottom of the sandbox, a row going up
// when they'd run too wide for one. // when they'd run too wide for one.
void Ui::hints_draw(Game& game) void Ui::hints_draw(Game& game)
{ {
@@ -1052,9 +1141,7 @@ void Ui::countdown_aside_draw(Game& game)
line_draw("to Ready Up", rightX, y, 1); line_draw("to Ready Up", rightX, y, 1);
} }
y = BAR_Y + SLOT_SIZE + HINT_GAP + (isFocusShown ? LINE_HEIGHT : 0); tip_draw(game, tip_low_get());
SDL_snprintf(text, sizeof(text), "Tip: %s", TIPS[tip]);
text_center_draw(game, text, y, 1);
} }
void Ui::text_center_draw(Game& game, const char* text, float y, float scale, float alpha) void Ui::text_center_draw(Game& game, const char* text, float y, float scale, float alpha)
@@ -1462,7 +1549,7 @@ Vec2 Ui::tooltip_size(Game& game, Tooltip& tooltip, Font font)
name.y + stats.y + table.y + description.y + flavor.y}; name.y + stats.y + table.y + description.y + flavor.y};
} }
// Stacks the name (in the tooltip's own colour, if it has one), gray stats, description and any gray flavor quote from // Stacks the name (in the tooltip's own color, if it has one), gray stats, description and any gray flavor quote from
// draw.position down, faded by draw.color's alpha. // draw.position down, faded by draw.color's alpha.
void Ui::tooltip_text_draw(Game& game, Tooltip& tooltip, Draw draw, Font font) void Ui::tooltip_text_draw(Game& game, Tooltip& tooltip, Draw draw, Font font)
{ {
@@ -1564,11 +1651,11 @@ void Ui::tooltip_draw(Game& game, Tooltip& tooltip)
panel.y + TOOLTIP_PADDING + ICON_SIZE / 2}}); panel.y + TOOLTIP_PADDING + ICON_SIZE / 2}});
} }
// A tooltip's item icon (or fashion item's art), centred on the placement, with the hotbar slot it's in numbered over // A tooltip's item icon (or fashion item's art), centerd on the placement, with the hotbar slot it's in numbered over
// its top left corner as on the hotbar. // its top left corner as on the hotbar.
void Ui::tooltip_icon_draw(Game& game, Tooltip& tooltip, Draw placement) void Ui::tooltip_icon_draw(Game& game, Tooltip& tooltip, Draw placement)
{ {
// A fashion item's art sits centred in its cell, which is centred on the placement. // A fashion item's art sits centerd in its cell, which is centerd on the placement.
float half = FASHION_CELL / 2; float half = FASHION_CELL / 2;
if (tooltip.fashion > 0) if (tooltip.fashion > 0)
game.sprites.fashion_icon_draw( game.sprites.fashion_icon_draw(
@@ -1630,7 +1717,7 @@ void Ui::label_draw(Game& game, const char* text, Vec2 position)
{.color = widget_color(false), .position = {position.x, position.y + TEXT_INSET}}); {.color = widget_color(false), .position = {position.x, position.y + TEXT_INSET}});
} }
// Notes what the option in box does, to show (see hint_draw) while the mouse is over it, even greyed out. // Notes what the option in box does, to show (see hint_draw) while the mouse is over it, even grayed out.
void Ui::hint_set(Game& game, Rect box, const char* text) void Ui::hint_set(Game& game, Rect box, const char* text)
{ {
if (isWidgetLive && !isFocusShown && is_point_in_rect(game.input.mouse, box)) if (isWidgetLive && !isFocusShown && is_point_in_rect(game.input.mouse, box))
@@ -1722,7 +1809,7 @@ bool Ui::radio_draw(Game& game, Vec2 position, const char* label, int& value, in
return isChanged; return isChanged;
} }
// A box with its label centred, lighting up under the mouse like a toggle. Returns whether it was clicked. Its focus // A box with its label centerd, lighting up under the mouse like a toggle. Returns whether it was clicked. Its focus
// id is its label unless given one (for labels that change or repeat). // id is its label unless given one (for labels that change or repeat).
bool Ui::button_draw(Game& game, Rect box, const char* label, const void* id) bool Ui::button_draw(Game& game, Rect box, const char* label, const void* id)
{ {
@@ -1756,7 +1843,7 @@ bool Ui::choice_draw(Game& game, Rect box, int& value, int option)
return isChanged; return isChanged;
} }
// A colour to pick (see choice_draw), filling its slot, more of it once picked. // A color to pick (see choice_draw), filling its slot, more of it once picked.
bool Ui::swatch_draw(Game& game, Rect box, Color color, int& value, int option) bool Ui::swatch_draw(Game& game, Rect box, Color color, int& value, int option)
{ {
bool isChanged = choice_draw(game, box, value, option); bool isChanged = choice_draw(game, box, value, option);
@@ -1948,7 +2035,7 @@ bool Ui::number_widget(Game& game, Rect box, float& value, NumberRange range, vo
return value != before; return value != before;
} }
// A section title centred in a row, with a line either side of it. // A section title centerd in a row, with a line either side of it.
void Ui::separator_draw(Game& game, Rect row, const char* label) void Ui::separator_draw(Game& game, Rect row, const char* label)
{ {
Vec2 size = game.text.size_get(label); Vec2 size = game.text.size_get(label);
@@ -1992,21 +2079,21 @@ bool Ui::fold_draw(Game& game, Rect row, const char* label, bool isOpen)
return isClicked; return isClicked;
} }
// A fighter walking on the spot at the right of an area, in a colour and wearing worn, its name over it and a caption // A fighter walking on the spot at the right of an area, in a color and wearing worn, its name over it and a caption
// under it (a line each). // under it (a line each).
void Ui::fighter_preview_draw(Game& game, Rect area, const char* name, Color color, const char* caption, int worn) void Ui::fighter_preview_draw(Game& game, Rect area, const char* name, Color color, const char* caption, int worn)
{ {
// The name, the fighter at its tallest in what it wears, and the caption, stacked and centred in the column (by // The name, the fighter at its tallest in what it wears, and the caption, stacked and centerd in the column (by
// their most, so it holds still as it walks). // their most, so it holds still as it walks).
float centre = area.x + area.w - FIGHTER_PREVIEW_WIDTH / 2; float center = area.x + area.w - FIGHTER_PREVIEW_WIDTH / 2;
Vec2 nameSize = game.text.size_get(name); Vec2 nameSize = game.text.size_get(name);
float body = std::max(game.sprites.player.rise_get(PREVIEW_ANIMATION), game.sprites.fashion_height_get(worn)); float body = std::max(game.sprites.player.rise_get(PREVIEW_ANIMATION), game.sprites.fashion_height_get(worn));
float captionHeight = caption && *caption ? game.text.size_get(caption).y : 0; float captionHeight = caption && *caption ? game.text.size_get(caption).y : 0;
float height = nameSize.y + PREVIEW_NAME_GAP + body * PREVIEW_SCALE + PREVIEW_CAPTION_GAP + captionHeight; float height = nameSize.y + PREVIEW_NAME_GAP + body * PREVIEW_SCALE + PREVIEW_CAPTION_GAP + captionHeight;
float top = std::round(area.y + (area.h - height) / 2); float top = std::round(area.y + (area.h - height) / 2);
game.text.draw(game.renderer, name, {.color = color, .position = {std::round(centre - nameSize.x / 2), top}}); game.text.draw(game.renderer, name, {.color = color, .position = {std::round(center - nameSize.x / 2), top}});
float feet = std::round(top + nameSize.y + PREVIEW_NAME_GAP + body * PREVIEW_SCALE); float feet = std::round(top + nameSize.y + PREVIEW_NAME_GAP + body * PREVIEW_SCALE);
player_draw(game, {centre, feet}, color, PREVIEW_SCALE, worn); player_draw(game, {center, feet}, color, PREVIEW_SCALE, worn);
float y = feet + PREVIEW_CAPTION_GAP; float y = feet + PREVIEW_CAPTION_GAP;
for (const char* start = caption; start && *start;) for (const char* start = caption; start && *start;)
{ {
@@ -2014,7 +2101,7 @@ void Ui::fighter_preview_draw(Game& game, Rect area, const char* name, Color col
char line[CAPTION_LENGTH]; char line[CAPTION_LENGTH];
SDL_strlcpy(line, start, std::min<int>(end ? end - start + 1 : sizeof(line), sizeof(line))); SDL_strlcpy(line, start, std::min<int>(end ? end - start + 1 : sizeof(line), sizeof(line)));
Vec2 size = game.text.size_get(line); Vec2 size = game.text.size_get(line);
game.text.draw(game.renderer, line, {.color = shade, .position = {std::round(centre - size.x / 2), y}}); game.text.draw(game.renderer, line, {.color = shade, .position = {std::round(center - size.x / 2), y}});
y += size.y; y += size.y;
start = end ? end + 1 : nullptr; start = end ? end + 1 : nullptr;
} }
@@ -2246,6 +2333,7 @@ static void bars_arrange(CooldownBar* bars)
void Ui::update(Game& game) void Ui::update(Game& game)
{ {
hints_update(game); hints_update(game);
tips_update(game);
spectate_update(game); spectate_update(game);
// The HUD's elements change while the mouse is over them, and change back after. // The HUD's elements change while the mouse is over them, and change back after.
@@ -2341,7 +2429,7 @@ void Ui::cooldowns_draw(Game& game)
} }
} }
// A weapon's or trinket's HUD icon, centred on the placement. // A weapon's or trinket's HUD icon, centerd on the placement.
void Ui::icon_draw(Game& game, ItemKind kind, int item, Draw placement, IconSize size) void Ui::icon_draw(Game& game, ItemKind kind, int item, Draw placement, IconSize size)
{ {
char name[TOOLTIP_LENGTH]; char name[TOOLTIP_LENGTH];
@@ -2349,7 +2437,7 @@ void Ui::icon_draw(Game& game, ItemKind kind, int item, Draw placement, IconSize
(kind == ITEM_TRINKET ? game.sprites.trinkets : game.sprites.icons).draw(game.renderer, name, 0, placement); (kind == ITEM_TRINKET ? game.sprites.trinkets : game.sprites.icons).draw(game.renderer, name, 0, placement);
} }
// An arrow at the screen's edge for every other living fighter out of view, pointing toward it, in its colour, with // An arrow at the screen's edge for every other living fighter out of view, pointing toward it, in its color, with
// its name just inside. // its name just inside.
void Ui::offscreen_draw(Game& game, Vec2 camera) void Ui::offscreen_draw(Game& game, Vec2 camera)
{ {
@@ -2381,7 +2469,7 @@ void Ui::offscreen_draw(Game& game, Vec2 camera)
} }
} }
// Each of this screen's fighters' crosshairs in its colour, filling in and shaking with its weapon's charge and // Each of this screen's fighters' crosshairs in its color, filling in and shaking with its weapon's charge and
// cooldown, and darkened through the cooldown: at the mouse for the keyboard's (and whenever the menu is open), // cooldown, and darkened through the cooldown: at the mouse for the keyboard's (and whenever the menu is open),
// otherwise where its stick aims. With none (a bot playing), the mouse's. // otherwise where its stick aims. With none (a bot playing), the mouse's.
void Ui::crosshairs_draw(Game& game, Vec2 camera) void Ui::crosshairs_draw(Game& game, Vec2 camera)
@@ -2409,7 +2497,7 @@ void Ui::crosshairs_draw(Game& game, Vec2 camera)
{.color = tint, .position = {std::floor(input.mouse.x), std::floor(input.mouse.y)}}); {.color = tint, .position = {std::floor(input.mouse.x), std::floor(input.mouse.y)}});
} }
// With several players on this screen in a match, a compact panel each along the bottom in their colour: name and // With several players on this screen in a match, a compact panel each along the bottom in their color: name and
// health, then rows of small weapon and trinket slots, the held weapon lit. // health, then rows of small weapon and trinket slots, the held weapon lit.
void Ui::panels_draw(Game& game) void Ui::panels_draw(Game& game)
{ {
@@ -2532,7 +2620,7 @@ void Ui::spectate_update(Game& game)
} }
} }
// While spectating, who's being watched, in their colour, near the bottom, with the controls that switch. // While spectating, who's being watched, in their color, near the bottom, with the controls that switch.
void Ui::spectate_draw(Game& game) void Ui::spectate_draw(Game& game)
{ {
if (spectated < 0) if (spectated < 0)
@@ -2646,7 +2734,7 @@ RivalLayout Ui::rivals_layout(Game& game)
return layout; return layout;
} }
// 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 color: its name centerd (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, half see-through (half again for a fighter out of the // 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. // round), and fades by half again while the mouse is over it.
@@ -2716,7 +2804,7 @@ void Ui::rivals_draw(Game& game)
y += layout.line + RIVAL_PADDING; y += layout.line + RIVAL_PADDING;
} }
// Weapons then trinkets, each in its columns, centred with the divider between. // Weapons then trinkets, each in its columns, centerd with the divider between.
if (!layout.isIconShown) if (!layout.isIconShown)
{ {
renderer.layer_end(base * hover_alpha_get(rivalHovers[index])); renderer.layer_end(base * hover_alpha_get(rivalHovers[index]));
@@ -2734,9 +2822,9 @@ void Ui::rivals_draw(Game& game)
if (item < 0) if (item < 0)
continue; continue;
bool isLit = kind == ITEM_TRINKET || slot == fighter.slot; bool isLit = kind == ITEM_TRINKET || slot == fighter.slot;
Vec2 centre = {x + slot % layout.columns[kind] * cell + iconSize / 2, Vec2 center = {x + slot % layout.columns[kind] * cell + iconSize / 2,
y + slot / layout.columns[kind] * cell + iconSize / 2}; y + slot / layout.columns[kind] * cell + iconSize / 2};
icon_draw(game, kind, item, {.color = isLit ? color : dim, .position = centre}, layout.size); icon_draw(game, kind, item, {.color = isLit ? color : dim, .position = center}, layout.size);
} }
x += layout.columns[kind] * cell; x += layout.columns[kind] * cell;
if (kind == ITEM_WEAPON) if (kind == ITEM_WEAPON)
@@ -2769,7 +2857,7 @@ void Ui::picks_draw(Game& game, float top)
std::floor(height)}); std::floor(height)});
} }
// One player's choosing panel in their colour: their name, which kind they're choosing (bumpers switch when both // One player's choosing panel in their color: their name, which kind they're choosing (bumpers switch when both
// are), the highlighted item's name, whether they're ready, the items to choose from and the slots they fill. On a // are), the highlighted item's name, whether they're ready, the items to choose from and the slots they fill. On a
// gamepad the D-pad or stick moves the highlight, A puts the item in the first empty slot (or the last) and B empties // gamepad the D-pad or stick moves the highlight, A puts the item in the first empty slot (or the last) and B empties
// the last filled slot. The keyboard's player can also click items in, click a slot to fill it, right-click to empty // the last filled slot. The keyboard's player can also click items in, click a slot to fill it, right-click to empty
@@ -2938,8 +3026,8 @@ void Ui::focus_begin(Game& game)
: direction == NAV_DOWN ? b.y - (a.y + a.h) : direction == NAV_DOWN ? b.y - (a.y + a.h)
: direction == NAV_LEFT ? a.x - (b.x + b.w) : direction == NAV_LEFT ? a.x - (b.x + b.w)
: b.x - (a.x + a.w); : b.x - (a.x + a.w);
float centre = isVertical ? (b.y + b.h / 2) - (a.y + a.h / 2) : (b.x + b.w / 2) - (a.x + a.w / 2); float center = isVertical ? (b.y + b.h / 2) - (a.y + a.h / 2) : (b.x + b.w / 2) - (a.x + a.w / 2);
bool isAhead = (direction == NAV_UP || direction == NAV_LEFT) ? centre < -1 : centre > 1; bool isAhead = (direction == NAV_UP || direction == NAV_LEFT) ? center < -1 : center > 1;
float aside = isVertical ? std::max({0.0f, b.x - (a.x + a.w), a.x - (b.x + b.w)}) float aside = isVertical ? std::max({0.0f, b.x - (a.x + a.w), a.x - (b.x + b.w)})
: std::max({0.0f, b.y - (a.y + a.h), a.y - (b.y + b.h)}); : std::max({0.0f, b.y - (a.y + a.h), a.y - (b.y + b.h)});
if (!isAhead || (!isVertical && aside > 0)) if (!isAhead || (!isVertical && aside > 0))
@@ -3031,8 +3119,8 @@ bool Ui::is_focused(const void* id, int sub)
// past the rows shown. Whatever scrolls says so with scroll_mark, so the move is tried again. // past the rows shown. Whatever scrolls says so with scroll_mark, so the move is tried again.
int Ui::scroll_take(Rect area) int Ui::scroll_take(Rect area)
{ {
Vec2 centre = {focusRect.x + focusRect.w / 2, focusRect.y + focusRect.h / 2}; Vec2 center = {focusRect.x + focusRect.w / 2, focusRect.y + focusRect.h / 2};
if (!scrollWant || !isFocusShown || !is_point_in_rect(centre, area)) if (!scrollWant || !isFocusShown || !is_point_in_rect(center, area))
return 0; return 0;
return scrollWant == NAV_UP ? -1 : 1; return scrollWant == NAV_UP ? -1 : 1;
} }
@@ -3042,7 +3130,7 @@ void Ui::scroll_mark()
isScrolled = true; isScrolled = true;
} }
// Online, in the lobby's sandbox, each queued game: its area on the ground in the queuer's colour (a mark on the floor // Online, in the lobby's sandbox, each queued game: its area on the ground in the queuer's color (a mark on the floor
// for each place in it, lit once taken), the game's mode icon and a "!" bobbing over the queuer, and under the area, // for each place in it, lit once taken), the game's mode icon and a "!" bobbing over the queuer, and under the area,
// how many have readied up and (standing in it) the key to ready up. Hovering the queuer or its icon shows the game. // how many have readied up and (standing in it) the key to ready up. Hovering the queuer or its icon shows the game.
void Ui::queues_draw(Game& game) void Ui::queues_draw(Game& game)
+10 -3
View File
@@ -11,6 +11,8 @@ constexpr int CHAMFER = 2;
constexpr int TOOLTIP_LENGTH = 512; constexpr int TOOLTIP_LENGTH = 512;
constexpr int WEAPON_HANDS = 2; constexpr int WEAPON_HANDS = 2;
constexpr float HOTBAR_MARGIN = 8; constexpr float HOTBAR_MARGIN = 8;
// How long each tip shows before the next.
constexpr float TIP_DURATION = 10;
constexpr float PANEL_BORDER = 2; constexpr float PANEL_BORDER = 2;
constexpr float SLOT_GAP = 4; constexpr float SLOT_GAP = 4;
constexpr float SLOT_SIZE = 28; constexpr float SLOT_SIZE = 28;
@@ -108,7 +110,7 @@ struct Tooltip
int slot = -1; int slot = -1;
// A fashion item it's about instead (none for 0), whose art is its icon. // A fashion item it's about instead (none for 0), whose art is its icon.
int fashion = 0; int fashion = 0;
// The window's colour, when it isn't the interface's own (a colour choice's tooltip wears that colour). // The window's color, when it isn't the interface's own (a color choice's tooltip wears that color).
Color border = CLEAR; Color border = CLEAR;
}; };
@@ -186,7 +188,7 @@ struct Slot
bool isTarget = false; bool isTarget = false;
}; };
// Everything is tinted with the local fighter's colour (tint, and a darker shade). Fighter bars and names, the match // Everything is tinted with the local fighter's color (tint, and a darker shade). Fighter bars and names, the match
// banner and score (with a tip each round, chosen when its countdown starts), the weapon and trinket hotbars, tooltips // banner and score (with a tip each round, chosen when its countdown starts), the weapon and trinket hotbars, tooltips
// and the crosshair, plus drawing helpers the menu shares. Menus also get immediate-mode widgets that draw and // and the crosshair, plus drawing helpers the menu shares. Menus also get immediate-mode widgets that draw and
// handle the mouse in one call each frame, returning whether their value changed. Widgets begin sets their fade and // handle the mouse in one call each frame, returning whether their value changed. Widgets begin sets their fade and
@@ -326,10 +328,11 @@ private:
float loadoutTop = 0; float loadoutTop = 0;
float widgetAlpha = 1; float widgetAlpha = 1;
int tip = 0; int tip = 0;
// How long the tip showing has shown (see tips_update), starting full so the first is picked at once.
float tipAge = TIP_DURATION;
// The second of the countdown last beeped for (see countdown_beep). // The second of the countdown last beeped for (see countdown_beep).
int countdownBeeped = 0; int countdownBeeped = 0;
bool isActiveSeen = false; bool isActiveSeen = false;
bool isTipChosen = false;
bool isWidgetEnabled = true; bool isWidgetEnabled = true;
bool isWidgetLive = false; bool isWidgetLive = false;
@@ -342,6 +345,10 @@ private:
void countdown_aside_draw(Game& game); void countdown_aside_draw(Game& game);
void countdown_beep(Game& game); void countdown_beep(Game& game);
void hints_update(Game& game); void hints_update(Game& game);
int tip_pick(Game& game);
void tips_update(Game& game);
void tip_draw(Game& game, float y);
float tip_low_get();
void hints_draw(Game& game); void hints_draw(Game& game);
void text_center_draw(Game& game, const char* text, float y, float scale, float alpha = 1); void text_center_draw(Game& game, const char* text, float y, float scale, float alpha = 1);
void text_center_draw(Game& game, const char* text, float y, float scale, Color color); void text_center_draw(Game& game, const char* text, float y, float scale, Color color);
+19 -19
View File
@@ -138,7 +138,7 @@ constexpr Burst TRAIL_BURST = {.life = 0.25f, .size = 2, .speed = 120, .spread =
constexpr Burst WARP_BURST = {.gravity = -60, .life = 0.4f, .size = 2, .speed = 50, .spread = PI, .count = 1}; constexpr Burst WARP_BURST = {.gravity = -60, .life = 0.4f, .size = 2, .speed = 50, .spread = PI, .count = 1};
constexpr Burst PORTAL_BURST = {.life = 0.4f, .size = 3, .speed = 140, .spread = PI, .count = 14}; constexpr Burst PORTAL_BURST = {.life = 0.4f, .size = 3, .speed = 140, .spread = PI, .count = 14};
// Flies a thrown weapon back towards the hand, reporting whether it got there. // Flies a thrown weapon back toward the hand, reporting whether it got there.
static bool is_caught(Fighter& fighter, Attack& attack) static bool is_caught(Fighter& fighter, Attack& attack)
{ {
Vec2 toHand = fighter.hand_get() - attack.position; Vec2 toHand = fighter.hand_get() - attack.position;
@@ -415,7 +415,7 @@ Vec2 muzzle_get(Attack& attack)
return attack.origin + vec2_polar(attack.angle, muzzle.x) + vec2_polar(attack.angle + PI / 2, muzzle.y * side); return attack.origin + vec2_polar(attack.angle, muzzle.x) + vec2_polar(attack.angle + PI / 2, muzzle.y * side);
} }
// How much of an attack's black fill its colour takes over, from none at plain damage to all at FILL_STRENGTH. // How much of an attack's black fill its color takes over, from none at plain damage to all at FILL_STRENGTH.
float fill_get(float strength) float fill_get(float strength)
{ {
return std::clamp((strength - 1) / (FILL_STRENGTH - 1), 0.0f, 1.0f); return std::clamp((strength - 1) / (FILL_STRENGTH - 1), 0.0f, 1.0f);
@@ -615,8 +615,8 @@ void Weapons::fire(Sim& sim, Fighter& fighter, float charge)
} }
} }
// Whether the fighter's box would fit with its middle at centre, clear of walls (and inside the map). // Whether the fighter's box would fit with its middle at center, clear of walls (and inside the map).
static bool is_box_clear(Sim& sim, Fighter& fighter, Vec2 centre) static bool is_box_clear(Sim& sim, Fighter& fighter, Vec2 center)
{ {
Rect box = fighter.body.box; Rect box = fighter.body.box;
int columns = (int)(box.w / TELEPORT_PROBE) + 1; int columns = (int)(box.w / TELEPORT_PROBE) + 1;
@@ -624,7 +624,7 @@ static bool is_box_clear(Sim& sim, Fighter& fighter, Vec2 centre)
for (int row = 0; row <= rows; row++) for (int row = 0; row <= rows; row++)
for (int column = 0; column <= columns; column++) for (int column = 0; column <= columns; column++)
{ {
Vec2 point = {centre.x - box.w / 2 + box.w * column / columns, centre.y - box.h / 2 + box.h * row / rows}; Vec2 point = {center.x - box.w / 2 + box.w * column / columns, center.y - box.h / 2 + box.h * row / rows};
if (sim.world.is_blocking_at(point)) if (sim.world.is_blocking_at(point))
return false; return false;
} }
@@ -635,17 +635,17 @@ static bool is_box_clear(Sim& sim, Fighter& fighter, Vec2 centre)
// the fighter until its box fits there. Returns the fighter's own middle if nowhere fits. // the fighter until its box fits there. Returns the fighter's own middle if nowhere fits.
static Vec2 teleport_spot(Sim& sim, Fighter& fighter, Vec2 target, float range) static Vec2 teleport_spot(Sim& sim, Fighter& fighter, Vec2 target, float range)
{ {
Vec2 centre = fighter.center_get(); Vec2 center = fighter.center_get();
Vec2 offset = target - centre; Vec2 offset = target - center;
float distance = std::min(range, vec2_length(offset)); float distance = std::min(range, vec2_length(offset));
Vec2 direction = vec2_normalize(offset); Vec2 direction = vec2_normalize(offset);
for (float along = distance; along > 0; along -= TELEPORT_STEP) for (float along = distance; along > 0; along -= TELEPORT_STEP)
{ {
Vec2 spot = centre + direction * along; Vec2 spot = center + direction * along;
if (is_box_clear(sim, fighter, spot)) if (is_box_clear(sim, fighter, spot))
return spot; return spot;
} }
return centre; return center;
} }
// The fighter's speed along the aim that an inheriting shot gains. Launched fighters lend nothing. // The fighter's speed along the aim that an inheriting shot gains. Launched fighters lend nothing.
@@ -663,7 +663,7 @@ static float bonus_get(Fighter& fighter, const AttackDef& def, Vec2 aim)
return 1 + (SPEED_DAMAGE + fighter.stat_get(STAT_SPEED_DAMAGE)) * speed / SPEED_UNIT; return 1 + (SPEED_DAMAGE + fighter.stat_get(STAT_SPEED_DAMAGE)) * speed / SPEED_UNIT;
} }
// How much a fighter's colour fills in from the bonus its held weapon would get firing right now. // How much a fighter's color fills in from the bonus its held weapon would get firing right now.
float Weapons::buff_get(Fighter& fighter) float Weapons::buff_get(Fighter& fighter)
{ {
int held = fighter.weapon_get(); int held = fighter.weapon_get();
@@ -873,7 +873,7 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot)
attack.stats.speed *= power; attack.stats.speed *= power;
// One piece per volley shot per split, each fanned around the aim (or set side by side when the volley has no // One piece per volley shot per split, each fanned around the aim (or set side by side when the volley has no
// fan). The centre split stays primary and the rest are secondary. // fan). The center split stays primary and the rest are secondary.
for (int v = 0; v < stats.volley; v++) for (int v = 0; v < stats.volley; v++)
{ {
float place = stats.volley > 1 ? (float)v / (stats.volley - 1) - 0.5f : 0; float place = stats.volley > 1 ? (float)v / (stats.volley - 1) - 0.5f : 0;
@@ -937,7 +937,7 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot)
if (variant == VARIANT_CHARGED) if (variant == VARIANT_CHARGED)
sim.cues->shake_add(def.shake * FIRE_SHAKE); sim.cues->shake_add(def.shake * FIRE_SHAKE);
// An uppercut launches its fighter the way it rises, cancelling any speed against that first. Shock Absorber // An uppercut launches its fighter the way it rises, canceling any speed against that first. Shock Absorber
// softens every push the shot gives its own fighter: lunges, kickback and Recoil Coil. // softens every push the shot gives its own fighter: lunges, kickback and Recoil Coil.
float absorb = fighter.stat_get(STAT_MOVEMENT); float absorb = fighter.stat_get(STAT_MOVEMENT);
Vec2 lunge = kind == ATTACK_UPPERCUT ? vec2_polar(attack.angle, 1) : aim; Vec2 lunge = kind == ATTACK_UPPERCUT ? vec2_polar(attack.angle, 1) : aim;
@@ -1544,7 +1544,7 @@ void Weapons::attack_hit(Sim& sim, Attack& attack, int target, Vec2 point)
} }
impact_play(sim, def.kind); impact_play(sim, def.kind);
// Explosions deal full damage at their centre, down to a fraction at the edge, by the target's nearest point. A // Explosions deal full damage at their center, down to a fraction at the edge, by the target's nearest point. A
// Vacuum's suck bites harder the closer to its muzzle. // Vacuum's suck bites harder the closer to its muzzle.
float taper = is_cone(def) ? cone_strength_get(attack, point) : 1; float taper = is_cone(def) ? cone_strength_get(attack, point) : 1;
taper *= def.kind == ATTACK_GRENADE ? GRENADE_CONTACT_SHARE : 1; taper *= def.kind == ATTACK_GRENADE ? GRENADE_CONTACT_SHARE : 1;
@@ -1783,7 +1783,7 @@ void Weapons::teleport_move(Sim& sim, Attack& attack)
} }
// Tesla Coil: a web of lightning between every weapon the fighter has out in the world (anything but explosions). // Tesla Coil: a web of lightning between every weapon the fighter has out in the world (anything but explosions).
// Each piece arcs to its nearest few neighbours within reach, so everything nearby is connected without the // Each piece arcs to its nearest few neighbors within reach, so everything nearby is connected without the
// links growing out of hand for rapid-fire weapons. Each coil makes the links thicker. // links growing out of hand for rapid-fire weapons. Each coil makes the links thicker.
std::vector<Capsule> Weapons::chain_get(Sim& sim, Fighter& fighter) std::vector<Capsule> Weapons::chain_get(Sim& sim, Fighter& fighter)
{ {
@@ -1881,8 +1881,8 @@ std::vector<Path> Weapons::paths_get(Sim& sim, Fighter& fighter)
Vec2 from = fighter.center_get(); Vec2 from = fighter.center_get();
Path path = {.area = stats.area * def.scale * fighter.stat_get(STAT_ATTACK_SIZE)}; Path path = {.area = stats.area * def.scale * fighter.stat_get(STAT_ATTACK_SIZE)};
float length = vec2_length(spot - from); float length = vec2_length(spot - from);
for (float travelled = 0; travelled < length; travelled += TRAJECTORY_STEP) for (float traveled = 0; traveled < length; traveled += TRAJECTORY_STEP)
path.steps.push_back(from + (spot - from) * (travelled / length)); path.steps.push_back(from + (spot - from) * (traveled / length));
path.steps.push_back(spot); path.steps.push_back(spot);
paths.push_back(path); paths.push_back(path);
return paths; return paths;
@@ -1926,7 +1926,7 @@ std::vector<Path> Weapons::paths_get(Sim& sim, Fighter& fighter)
int bounces = def.isBouncy ? (int)fighter.stat_get(STAT_RICOCHETS) : 0; int bounces = def.isBouncy ? (int)fighter.stat_get(STAT_RICOCHETS) : 0;
Path path; Path path;
bool isEnded = stats.range <= TRAJECTORY_REACH; bool isEnded = stats.range <= TRAJECTORY_REACH;
for (float travelled = area > 0 ? MUZZLE_DISTANCE : 0; travelled < reach; travelled += TRAJECTORY_STEP) for (float traveled = area > 0 ? MUZZLE_DISTANCE : 0; traveled < reach; traveled += TRAJECTORY_STEP)
{ {
float time = TRAJECTORY_STEP / std::max(1.0f, vec2_length(velocity)); float time = TRAJECTORY_STEP / std::max(1.0f, vec2_length(velocity));
Vec2 next = point + velocity * time; Vec2 next = point + velocity * time;
@@ -2043,7 +2043,7 @@ void Weapons::attack_explode(Sim& sim, Attack& attack)
} }
// Queues an explosion that hits everything inside it next tick, weaker toward its edge. Pushing blasts fling every // Queues an explosion that hits everything inside it next tick, weaker toward its edge. Pushing blasts fling every
// fighter away from the centre, harder the closer they are. // fighter away from the center, harder the closer they are.
void Weapons::blast_spawn(Sim& sim, Blast blast) void Weapons::blast_spawn(Sim& sim, Blast blast)
{ {
float reach = blast.area * KNOCKBACK_REACH; float reach = blast.area * KNOCKBACK_REACH;
@@ -2226,7 +2226,7 @@ static void shot_restore(Attack& shot, float share)
// to fly straight out of the cone from its point (along the aim, give or take where in the cone it is), at its own // to fly straight out of the cone from its point (along the aim, give or take where in the cone it is), at its own
// speed plus the blow's, and pull speeds each toward the point. A shot stuck to a surface comes loose first (a // speed plus the blow's, and pull speeds each toward the point. A shot stuck to a surface comes loose first (a
// grapple's hook too), popping out toward the point at the cone's own speed when pulled, and glancing off the surface // grapple's hook too), popping out toward the point at the cone's own speed when pulled, and glancing off the surface
// if it would head straight back in. A claiming attack makes other fighters' shots its owner's, burst into its colour // if it would head straight back in. A claiming attack makes other fighters' shots its owner's, burst into its color
// (all but those tied to their fighter's weapon: on a rope or chain, waiting to be set off, or the weapon itself). The // (all but those tied to their fighter's weapon: on a rope or chain, waiting to be set off, or the weapon itself). The
// owner's own shots then get back restore's share of what they set off with (see shot_restore). // owner's own shots then get back restore's share of what they set off with (see shot_restore).
void Weapons::shots_drift(Sim& sim, Attack& source, float blow, float pull, float restore) void Weapons::shots_drift(Sim& sim, Attack& source, float blow, float pull, float restore)
+4 -4
View File
@@ -489,7 +489,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
}, },
{ {
.name = "Lance", .name = "Lance",
.blurb = "Pokes a distance ahead of you.\n\nCharge to lunge ahead\nwith a harder, further\ntravelling poke.", .blurb = "Pokes a distance ahead of you.\n\nCharge to lunge ahead\nwith a harder, further\ntraveling poke.",
.flavor = "Works best when riding an ostrich.", .flavor = "Works best when riding an ostrich.",
.hit = HIT_PIERCE, .hit = HIT_PIERCE,
.normal = {.label = "poke", .normal = {.label = "poke",
@@ -1338,9 +1338,9 @@ inline constexpr AttackDef EXPLOSION_DEF = {.kind = ATTACK_EXPLOSION,
// Its hit shape is a capsule from position to tip with the given radius. // Its hit shape is a capsule from position to tip with the given radius.
// A charged hook's speed is how fast it pulls the player in, rather than how fast it flies. // A charged hook's speed is how fast it pulls the player in, rather than how fast it flies.
// Copies (extra Splitter pieces and Doubler repeats) don't count toward how many of a weapon are out. Weapon is what // Copies (extra Splitter pieces and Doubler repeats) don't count toward how many of a weapon are out. Weapon is what
// fired it, and key the owner's weapon slot it belongs to (see Fighter::weapon_key), or -1 if none. Travelled is // fired it, and key the owner's weapon slot it belongs to (see Fighter::weapon_key), or -1 if none. Traveled is
// how far a ranged shot has flown, which its damage falls off with. Bonus is the speed bonus it was fired with // how far a ranged shot has flown, which its damage falls off with. Bonus is the speed bonus it was fired with
// (explosions carry their source's whole strength), which its colour fills in with. It holds no pointers (what it is // (explosions carry their source's whole strength), which its color fills in with. It holds no pointers (what it is
// comes from its weapon, whether it's charged, and whether it's a blast), so its bytes mean the same on every platform // comes from its weapon, whether it's charged, and whether it's a blast), so its bytes mean the same on every platform
// and the game state can be sent whole to a player joining. // and the game state can be sent whole to a player joining.
struct Attack struct Attack
@@ -1365,7 +1365,7 @@ struct Attack
float fade = 0; float fade = 0;
float hitCooldown = 0; float hitCooldown = 0;
float length = 0; float length = 0;
// When it was claimed from another fighter, whose colour it turns from (see Weapons::shots_drift). // When it was claimed from another fighter, whose color it turns from (see Weapons::shots_drift).
float claimTime = 0; float claimTime = 0;
float lifeStart = 0; float lifeStart = 0;
float lifetime = 0; float lifetime = 0;
+1 -1
View File
@@ -365,7 +365,7 @@ void World::ramps_load(std::vector<std::string>& rows)
} }
} }
// Knocks a block out of the grid in a shower of debris in the breaker's colour, to grow back later. // Knocks a block out of the grid in a shower of debris in the breaker's color, to grow back later.
void World::block_break(Sim& sim, Dummy& block, int breaker) void World::block_break(Sim& sim, Dummy& block, int breaker)
{ {
tiles[block.tile] = TILE_EMPTY; tiles[block.tile] = TILE_EMPTY;