From 93cdf46d5a40dcf5aee79e9f00b96ec6da120082 Mon Sep 17 00:00:00 2001 From: shweet Date: Tue, 10 Nov 2026 19:37:19 -0500 Subject: [PATCH] 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 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. --- CMakeLists.txt | 2 +- README.md | 2 +- resources/gfx/fashion.anm2 | 4 +- server/server.cpp | 57 ++++-- src/bot.cpp | 4 +- src/bot.h | 2 +- src/chat.cpp | 94 +++++++--- src/chat.h | 12 +- src/common.h | 6 +- src/dmath.h | 4 +- src/effects.cpp | 4 +- src/fighter.cpp | 10 +- src/fighter.h | 6 +- src/game.h | 4 +- src/input.cpp | 4 +- src/input.h | 2 +- src/main.cpp | 6 +- src/match.cpp | 8 +- src/match.h | 6 +- src/menu.cpp | 12 +- src/menu.h | 2 +- src/net.cpp | 31 +++- src/net.h | 11 +- src/protocol.h | 17 +- src/renderer.cpp | 10 +- src/scene.cpp | 42 ++--- src/session.cpp | 77 ++++++-- src/session.h | 9 +- src/settings.cpp | 38 ++-- src/settings.h | 5 +- src/sim.cmake | 2 +- src/sim.cpp | 2 +- src/sprites.cpp | 2 +- src/ui.cpp | 370 +++++++++++++++++++++++-------------- src/ui.h | 13 +- src/weapon.cpp | 38 ++-- src/weapon.h | 8 +- src/world.cpp | 2 +- 38 files changed, 603 insertions(+), 325 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 86afca7..8c1b096 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED 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 -# 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. if(MSVC) add_compile_options(/fp:precise /fp:contract-) diff --git a/README.md b/README.md index eca357b..0d0bf4a 100644 --- a/README.md +++ b/README.md @@ -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. - 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. -- 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. - STUN: `stun.l.google.com:19302`. - The server address is set in Settings. Default: `wss://shweetz.net/smack-a-brother/ws`. diff --git a/resources/gfx/fashion.anm2 b/resources/gfx/fashion.anm2 index 236adb3..e0abbf0 100644 --- a/resources/gfx/fashion.anm2 +++ b/resources/gfx/fashion.anm2 @@ -10,7 +10,7 @@ {"name": "Hard Hat", "crop": [64, 0], "size": [32, 32], "pivot": [16, 18]}, {"name": "Bandana", "crop": [96, 0], "size": [32, 32], "pivot": [16, 14]}, {"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": "Monocle", "crop": [32, 32], "size": [32, 32], "pivot": [13, 13]}, {"name": "Top Hat", "crop": [64, 32], "size": [32, 32], "pivot": [16, 20]}, @@ -43,7 +43,7 @@ "Hard Hat": {"layers": [{"layer": 0, "frames": [{"region": 2}]}]}, "Bandana": {"layers": [{"layer": 1, "frames": [{"region": 3}]}]}, "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}]}]}, "Monocle": {"layers": [{"layer": 1, "frames": [{"region": 7}]}]}, "Top Hat": {"layers": [{"layer": 0, "frames": [{"region": 8}]}]}, diff --git a/server/server.cpp b/server/server.cpp index e0c12a7..18ac7bd 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -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); 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& 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"); } -// Text a member sent for the lobby (a chat line or their new name), cleaned, passed to everyone with who it's from. -static void text_relay(int id, PacketType type, PacketReader& reader, size_t max) +// Sends to whoever a member's words reach: their room (everyone playing or watching where they are), or with +// 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; - 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; - 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()) return; - if (type == PACKET_RENAME) - members[id].name = text; 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; - 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. @@ -507,12 +539,13 @@ static void packet_handle(int id, const std::string& bytes) return; } 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) { Uint8 isTyping = 0; + // Only the room sees the "..." (a line said to all shows up elsewhere without one). 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) { @@ -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 -// 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) { 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 -// 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) { std::vector& lobbyRooms = lobbies[code].rooms; diff --git a/src/bot.cpp b/src/bot.cpp index 7881c41..62e681c 100644 --- a/src/bot.cpp +++ b/src/bot.cpp @@ -311,7 +311,7 @@ int Bot::surface_below(Vec2 point, float above) 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 // than that. Jumps can't go up through solid ground. void Bot::links_build() @@ -650,7 +650,7 @@ void Bot::move_steer(Sim& sim, Fighter& fighter, BotState& state, Controls& cont 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; bool isArrived = std::abs(dx) <= ARRIVE_DISTANCE; bool isCarried = link && (link->kind == LINK_FALL || link->kind == LINK_WALK); diff --git a/src/bot.h b/src/bot.h index a9389c4..6710d80 100644 --- a/src/bot.h +++ b/src/bot.h @@ -43,7 +43,7 @@ enum LinkKind 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 // dropped through. struct Surface diff --git a/src/chat.cpp b/src/chat.cpp index 8f25aa6..fbf049b 100644 --- a/src/chat.cpp +++ b/src/chat.cpp @@ -5,7 +5,6 @@ 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). -constexpr int NO_MEMBER = -2; constexpr int CHAT_SHOWN = 6; constexpr int CHAT_HISTORY = 12; constexpr float CHAT_LINGER = 10; @@ -88,7 +87,7 @@ void Chat::players_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. void Chat::pvps_announce(Game& game) { @@ -103,7 +102,7 @@ void Chat::pvps_announce(Game& game) pvps[owner] = isPvp; if (isChanged) 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; } -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) 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 -// fast (more than a handful in a few seconds) are dropped. -void Chat::say(Game& game, int member, const std::string& text) +// A player's message: into the log in their color (marked with where it came from, see Session::place_tag_get), over +// 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, const std::string& tag) { constexpr int BURST = 5; constexpr float BURST_SECONDS = 4; @@ -142,9 +154,9 @@ void Chat::say(Game& game, int member, const std::string& text) return; bool isOnline = game.session.isActive; 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); - 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) lines.erase(lines.begin()); 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. void Chat::update(Game& game) { - for (std::string& notice : game.net.notices) - system(notice); + for (NetNotice& notice : game.net.notices) + system(notice.text, member_color_get(game, notice.member), notice.member < 0 ? NO_MEMBER : notice.member); game.net.notices.clear(); pvps_announce(game); Input& input = game.input; @@ -171,7 +183,7 @@ void Chat::update(Game& game) draft += *c; if (input.isSubmitted) { - if (!input.isCancelled) + if (!input.isCanceled) submit(game); close(game); } @@ -204,10 +216,13 @@ void Chat::submit(Game& game) std::string text = text_clean(draft, CHAT_MESSAGE_MAX); if (text.empty()) return; + // A message starting with ! goes to the whole lobby, like /all. if (text[0] == '/') 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 - 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). @@ -243,7 +258,7 @@ static std::vector arguments_split(const std::string& text) return words; } -// /name and /color change this player's (as Profile does), /kick "" "" (the host +// /name and /color change this player's (as Profile does), /kick "" "" (the host // 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) { @@ -251,7 +266,7 @@ void Chat::command(Game& game, const std::string& text) std::string name = words[0]; for (char& c : name) 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(' '); std::string argument = space == std::string::npos ? "" : text_clean(text.substr(space + 1), PLAYER_NAME_MAX); if (argument.size() >= 2 && argument.front() == '"' && argument.back() == '"') @@ -259,9 +274,12 @@ void Chat::command(Game& game, const std::string& text) if (name == "/help") { system("/name : Changes your name (once every 30 seconds in a lobby)."); - system("/color : Changes your colour. Colours: Red, Blue, Yellow, Green, Orange, Purple, Cyan, Pink, " + system("/color : Changes your color. Colors: Red, Blue, Yellow, Green, Orange, Purple, Cyan, Pink, " "Lime, Teal, Magenta, Sky, Peach, Lavender, Brown, Silver."); + system("/all : Says it to the whole lobby, not just where you are. Starting with ! does too."); + system("/fashion : Wears an item from Profile's Fashion, or None."); system("/kick \"\" \"\": Removes a player from the lobby (host only)."); + system("/host \"\": Hands hosting the lobby to another player (host only)."); system("/pvp: Turns PvP on or off in the sandbox."); return; } @@ -276,6 +294,36 @@ void Chat::command(Game& game, const std::string& text) system(isPvp ? "PvP is now on." : "PvP is now off."); 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 "); + game.session.chat_send(game, message, CHAT_LOBBY); + return; + } + if (name == "/host") + { + if (words.size() < 2) + return system("Usage: /host \"\""); + 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 , as named in Profile's Fashion, or None."); + } if (name == "/kick") { if (words.size() < 2) @@ -293,10 +341,10 @@ void Chat::command(Game& game, const std::string& text) game.settings.changed(); // Online, the session announces it to everyone (see Session::profile_watch). if (!game.session.isActive) - system("Your name is now " + argument + "."); + system("Your name is now " + argument + ".", member_color_get(game, game.net.selfId)); return; } - if (name == "/color" || name == "/colour") + if (name == "/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."); game.settings.color = color; 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 system("Usage: /color . Type /help for the colour list."); + return system("Usage: /color . Type /help for the color list."); } 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++) { 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 alpha = isOpen ? 1 : std::clamp((CHAT_LINGER - age) / CHAT_FADE, 0.0f, 1.0f); if (alpha <= 0) @@ -369,7 +419,7 @@ void Chat::draw(Game& game) y -= lineHeight; renderer.rect_draw({area.x, y, area.w, lineHeight}, color_fade(BLACK, CHAT_BACKING * alpha)); 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}}); } } } diff --git a/src/chat.h b/src/chat.h index 6c0a650..bcbabd5 100644 --- a/src/chat.h +++ b/src/chat.h @@ -8,13 +8,18 @@ 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. +// No lobby member, for chat lines about nobody. +constexpr int NO_MEMBER = -2; + struct ChatLine { std::string text; Color color = WHITE; 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. @@ -40,8 +45,9 @@ public: std::map typing; void update(Game& game); - void system(const std::string& text, Color color = WHITE); - void say(Game& game, int member, const std::string& text); + void system(const std::string& text, Color color = WHITE, int member = NO_MEMBER); + 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 players_clear(); void draw(Game& game); diff --git a/src/common.h b/src/common.h index 69325f5..6b008a0 100644 --- a/src/common.h +++ b/src/common.h @@ -135,7 +135,7 @@ constexpr int RNG_SHIFT_B = 17; constexpr int RNG_SHIFT_C = 5; 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. 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; } -// Darkens a colour by amount, keeping its alpha. +// Darkens a color by amount, keeping its alpha. inline Color color_shade(Color color, float amount) { 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}; } -// 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) { return {from.r + (to.r - from.r) * amount, from.g + (to.g - from.g) * amount, from.b + (to.b - from.b) * amount, diff --git a/src/dmath.h b/src/dmath.h index 0035ab1..fe50707 100644 --- a/src/dmath.h +++ b/src/dmath.h @@ -4,13 +4,13 @@ #include #include -// 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 // 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 // being fused into a multiply-add, which the build turns off. #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 namespace dmath diff --git a/src/effects.cpp b/src/effects.cpp index 68d93bd..67ceb73 100644 --- a/src/effects.cpp +++ b/src/effects.cpp @@ -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. -// 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 // total. 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); } -// 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. void particle_draw(Renderer& renderer, Particle& particle, Vec2 position) { diff --git a/src/fighter.cpp b/src/fighter.cpp index 7f4da55..14d8d10 100644 --- a/src/fighter.cpp +++ b/src/fighter.cpp @@ -127,7 +127,7 @@ void Fighter::state_reset(Sim& sim, float maxHealth) 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. 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); } -// Moves the fighter's middle to centre, keeping its momentum, without drawing it sliding across (teleports). -void Fighter::teleport_to(Vec2 centre) +// Moves the fighter's middle to center, keeping its momentum, without drawing it sliding across (teleports). +void Fighter::teleport_to(Vec2 center) { - body.box.x = centre.x - body.box.w / 2; - body.box.y = centre.y - body.box.h / 2; + body.box.x = center.x - body.box.w / 2; + body.box.y = center.y - body.box.h / 2; cornerPrevious = {body.box.x, body.box.y}; } diff --git a/src/fighter.h b/src/fighter.h index 8b649e5..f9f8879 100644 --- a/src/fighter.h +++ b/src/fighter.h @@ -18,7 +18,7 @@ constexpr float BOUNCE_KEEP = 0.75f; constexpr float SECONDARY_SHADE = 0.5f; constexpr int COLOR_COUNT = 23; 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] = { {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}, @@ -47,7 +47,7 @@ constexpr FashionDef FASHION_DEFS[] = { {"Hard Hat", "Doesn't work here..."}, {"Bandana", "Infinite ammo...but\nyou already have that."}, {"Bow", "Stereotypical, yet cute!"}, - {"Moustache", "Clean shave."}, + {"Mustache", "Clean shave."}, {"Eyepatch", "Depth perception not needed."}, {"Monocle", "Tea time!"}, {"Top Hat", "Dashing."}, @@ -165,7 +165,7 @@ public: void blend(float alpha); void impulse_add(Vec2 impulse); void knock_take(Vec2 knock, int owner); - void teleport_to(Vec2 centre); + void teleport_to(Vec2 center); void burst_add(Vec2 impulse); void charge_cancel(Sim& sim); int weapon_get(); diff --git a/src/game.h b/src/game.h index f0ca785..9ba5b20 100644 --- a/src/game.h +++ b/src/game.h @@ -20,7 +20,7 @@ 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. enum Tint { @@ -85,7 +85,7 @@ struct Game 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 // 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 diff --git a/src/input.cpp b/src/input.cpp index d493fb6..f6049e7 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -122,7 +122,7 @@ void Input::poll(Game& game) isConfirmed = false; isMouseMoved = false; isSubmitted = false; - isCancelled = false; + isCanceled = false; for (Device& device : devices) { device.presses = 0; @@ -219,7 +219,7 @@ void Input::poll(Game& game) isConfirmed |= !isTyping && !event.key.repeat && scancode == SDL_SCANCODE_RETURN; erases += isTyping && scancode == SDL_SCANCODE_BACKSPACE; 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. 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; diff --git a/src/input.h b/src/input.h index d19c6d4..7678e24 100644 --- a/src/input.h +++ b/src/input.h @@ -170,7 +170,7 @@ public: bool isQuit = false; bool isSubmitted = false; // Whether the text being typed was given up on (Escape) rather than entered. - bool isCancelled = false; + bool isCanceled = false; Input(); void poll(Game& game); diff --git a/src/main.cpp b/src/main.cpp index 3e3889d..a46d577 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,7 +18,7 @@ constexpr SDL_WindowFlags WINDOW_FILL = 0; 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 -// 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) { Vec2 world = game.sim.world.size_get(); @@ -44,14 +44,14 @@ static Vec2 camera_target_get(Game& game) return target; } -// Eases the camera towards its target. +// Eases the camera toward its target. static void camera_update(Game& game) { 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) -// 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. void game_tick(Game& game) { diff --git a/src/match.cpp b/src/match.cpp index 6fc0be7..2b73fa3 100644 --- a/src/match.cpp +++ b/src/match.cpp @@ -216,7 +216,7 @@ void Match::start(Sim& sim) tomatoes_clear(); // 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++) { Fighter& fighter = sim.fighters[i]; @@ -276,7 +276,7 @@ void Match::sandbox_leave(Sim& sim, int index) 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 // only it wears anything. void Match::colors_assign(Sim& sim) @@ -815,7 +815,7 @@ void Match::leaves_take(Sim& sim) 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). Profile profile_get(Fighter& fighter) { @@ -832,7 +832,7 @@ Profile profile_get(Fighter& fighter) 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. int Match::member_join(Sim& sim, int member, const Profile& profile) { diff --git a/src/match.h b/src/match.h index 37eee58..e4b8b78 100644 --- a/src/match.h +++ b/src/match.h @@ -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 // 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 -// 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). 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}); } -// 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. 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). 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 - // 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 fashionWanted = FASHION_NONE; bool isOnline = false; diff --git a/src/menu.cpp b/src/menu.cpp index b356bd0..d83dccd 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -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})); } -// 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. void Menu::sandbox_levels_update(Game& game) { @@ -649,7 +649,7 @@ void Menu::items_draw(Game& game, MenuLayout& layout) if (shown != described) description.scroll = 0; 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); Tooltip tooltip = ui.tooltip_build(game, kind, item, level, isLevelable ? TOOLTIP_NEXT_LEVEL : TOOLTIP_PLAIN); if (slotLevel) @@ -660,11 +660,11 @@ void Menu::items_draw(Game& game, MenuLayout& layout) 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 - // 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. if (!isLevelable) return; - // The level between a minus and a plus, centred. + // The level between a minus and a plus, centerd. char text[TOOLTIP_LENGTH]; SDL_snprintf(text, sizeof(text), isSandboxLevel ? "Sandbox Level %d" : "Level %d", level + 1); Vec2 size = game.text.size_get(text); @@ -779,7 +779,7 @@ static std::string lobby_page_get() #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). static Color member_color_get(Game& game, int member) { @@ -882,7 +882,7 @@ void Menu::online_draw(Game& game, Rect area) y += ROW_HEIGHT; // 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. Rect list = {area.x, y, area.w, area.y + area.h - y}; float rowWidth = list.w - SCROLL_WIDTH * 2; diff --git a/src/menu.h b/src/menu.h index d7458b3..7e61554 100644 --- a/src/menu.h +++ b/src/menu.h @@ -6,7 +6,7 @@ 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. constexpr float MENU_TOP = 24; constexpr float MENU_WIDTH = 440; diff --git a/src/net.cpp b/src/net.cpp index 653ad53..487df86 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -243,7 +243,7 @@ void Net::message_handle(const std::string& text) member.id = message["id"].get(number) ? -1 : (int)number; member.name = message["name"].get(field) ? "" : std::string(field); members.push_back(member); - notices.push_back(member.name + " joined the lobby."); + notices.push_back({member.name + " joined the lobby.", member.id}); return; } if (type == "kicked") @@ -254,7 +254,7 @@ void Net::message_handle(const std::string& text) state = NET_OFFLINE; lobby_leave(); error = text; - notices.push_back(text); + notices.push_back({text, selfId}); return; } if (type == "member_leave") @@ -263,11 +263,20 @@ void Net::message_handle(const std::string& text) bool isKicked = false; if (!message["kicked"].get(isKicked) && isKicked) kicks[id] = text_clean(message["reason"].get(field) ? "" : field, KICK_REASON_MAX); + int previous = hostId; hostId = message["host"].get(number) ? hostId : (int)number; NetMember* member = member_get(id); 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; }); + host_announce(previous); + return; + } + if (type == "host_change") + { + int previous = hostId; + hostId = message["host"].get(number) ? hostId : (int)number; + host_announce(previous); return; } 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. bool Net::is_linked() { diff --git a/src/net.h b/src/net.h index d59e905..a25827e 100644 --- a/src/net.h +++ b/src/net.h @@ -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 // 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 { int id = -1; @@ -45,6 +52,7 @@ public: void heartbeat(); void send(const std::string& bytes, Delivery delivery); void kick_send(int member, const std::string& reason); + void host_send(int member); bool is_linked(); NetState state = NET_OFFLINE; @@ -56,7 +64,7 @@ public: // Binary messages from the server, oldest first, for the session to take. std::vector packets; // Lines for the chat log from the server (like this screen being kicked), oldest first, for the chat to take. - std::vector notices; + std::vector notices; // Members the server said were kicked, with the reason (empty for none given). std::map kicks; @@ -71,4 +79,5 @@ private: void link_open(); void signal_handle(const std::string& payload); NetMember* member_get(int id); + void host_announce(int previous); }; diff --git a/src/protocol.h b/src/protocol.h index 0e83dae..84b31c7 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -27,7 +27,7 @@ #include #include -constexpr int PROTOCOL_VERSION = 7; +constexpr int PROTOCOL_VERSION = 8; constexpr int LOBBY_MEMBERS_MAX = 16; constexpr int LOBBY_CODE_LENGTH = 4; // 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 // tick it has confirmed // 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_RENAME Uint16 length, text // 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 // (in ticks) this player's last controls came // 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 -// 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 { PACKET_PROFILE = 1, diff --git a/src/renderer.cpp b/src/renderer.cpp index 857550a..c9b4c87 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -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"( in vec2 uv; 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}}); } -// 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) { 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 -// 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) { 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. -// 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) { 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 -// 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) { flush(); diff --git a/src/scene.cpp b/src/scene.cpp index 5f6a803..f0d4a2d 100644 --- a/src/scene.cpp +++ b/src/scene.cpp @@ -13,7 +13,7 @@ constexpr float RUMBLE_LANDED = 0.15f; constexpr Uint32 RUMBLE_LANDED_TIME = 60; 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 TRAJECTORY_ALPHA = 0.12f; constexpr float TRAJECTORY_TAPER = 0.25f; @@ -116,7 +116,7 @@ constexpr Uint32 TOMATO_LOOK_STEP = 2654435761u; constexpr float TOMATO_REACH = 10; 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. 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)}; } -// 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. 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. static Color color_dim(Color color, float strength) { strength = math_pow(std::min(1.0f, strength), FALLOFF_CURVE); - float grey = (color.r + color.g + color.b) / 3; - Color washed = {math_lerp(grey, color.r, strength), math_lerp(grey, color.g, strength), - math_lerp(grey, color.b, strength), color.a}; + float gray = (color.r + color.g + color.b) / 3; + Color washed = {math_lerp(gray, color.r, strength), math_lerp(gray, color.g, strength), + math_lerp(gray, color.b, strength), color.a}; 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) { - // 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); if (attack.ownerPrevious >= 0) color = color_mix(game.color_get(attack.ownerPrevious), color, @@ -177,7 +177,7 @@ struct Stroke 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. 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); } -// 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. 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); } -// 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. // 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) @@ -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. void Scene::tomatoes_draw() { @@ -648,8 +648,8 @@ void Scene::world_draw() for (int x = startX; x <= endX; x++) { auto isOpen = [&](float dx, float dy) { - Tile neighbour = world.tile_get(x + dx, y + dy); - return neighbour != TILE_SOLID && neighbour != TILE_SLOPE; + Tile neighbor = world.tile_get(x + dx, y + dy); + return neighbor != TILE_SOLID && neighbor != TILE_SLOPE; }; auto isEmpty = [&](int dx) { 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); } -// 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 // 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) @@ -929,7 +929,7 @@ void Scene::attacks_draw() else if (kind != ATTACK_EXPLOSION) 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. if (attack.def()->isBursting && !attack.def()->isInstant) 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). 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 -// 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() { Renderer& renderer = game.renderer; @@ -1068,7 +1068,7 @@ void Scene::collision_draw() 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. void Scene::blasts_draw() { @@ -1254,7 +1254,7 @@ void Scene::flash_follow(Flash& flash) 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. void Scene::offhand_draw(Fighter& fighter) { diff --git a/src/session.cpp b/src/session.cpp index 5b44159..68592b6 100644 --- a/src/session.cpp +++ b/src/session.cpp @@ -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 // 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 RESEND_MAX = 30; constexpr int LEAD_LOW = 1; @@ -224,6 +224,10 @@ void Session::packet_handle(Game& game, const std::string& bytes) member.ping = ping; return; } + Uint8 scope = CHAT_ROOM; + int place = 0; + if (type == PACKET_CHAT && (!reader.get(scope) || !reader.get(place))) + return; Uint16 length = 0; if (!reader.get(length) || reader.at + length > bytes.size()) return; @@ -232,7 +236,7 @@ void Session::packet_handle(Game& game, const std::string& bytes) { std::string text = text_clean(raw, CHAT_MESSAGE_MAX); if (!text.empty()) - game.chat.say(game, from, text); + game.chat.say(game, from, text, place_tag_get(place, (ChatScope)scope)); return; } 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) if (member.id == from) 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 @@ -453,7 +458,7 @@ void Session::watch_send(Game& game, int serial) 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) { 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); } -// A chat message from this screen: to the lobby through the server, or straight into the log offline. -void Session::chat_send(Game& game, const std::string& text) +// A chat message from this screen: through the server to its room or the whole lobby, or straight into the log +// offline. +void Session::chat_send(Game& game, const std::string& text, ChatScope scope) { if (!isActive) - return game.chat.say(game, game.net.selfId, text); - text_send(game, PACKET_CHAT, text); + return game.chat.say(game, game.net.selfId, 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). @@ -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); } -// Notices this screen's name or colour 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 +// 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 color goes through the game as an edit, so everyone // agrees (it's refused if someone else has it). 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; } -// 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 -// has it). The server does the kicking, and everyone's chat says so. -void Session::kick(Game& game, const std::string& name, const std::string& reason) +// 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), or -1, saying why in the chat. +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 bare = -1; 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) found = bare; if (found < 0) - return game.chat.system(bareCount > 1 ? "More than one player is named " + name + - ". Include their number, like \"" + name + " (2)\"." - : "There's no player named " + name + " in this lobby."); - game.net.kick_send(found, text_clean(reason, KICK_REASON_MAX)); + game.chat.system(bareCount > 1 ? "More than one player is named " + name + ". Include their number, like \"" + + name + " (2)\"." + : "There's no player named " + name + " in this lobby."); + 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 diff --git a/src/session.h b/src/session.h index cd5940e..bbbaab9 100644 --- a/src/session.h +++ b/src/session.h @@ -31,7 +31,7 @@ enum StepMode 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 { std::vector> players; @@ -77,9 +77,12 @@ public: int fighter_get(Game& game, int member); std::string base_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 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(); void queue_send(Game& game, const Rules& rules); void queue_cancel(Game& game); @@ -93,7 +96,7 @@ private: std::map> events; std::map checksums; std::map 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 names; int room = 0; // The next tick to play, the first not yet confirmed, and the earliest that has to be played again. diff --git a/src/settings.cpp b/src/settings.cpp index a8410bd..63cc514 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -12,7 +12,7 @@ constexpr int LOADOUT_PASSES_MAX = 6; constexpr int LINE_LENGTH = 64; constexpr float BINDING_WIDTH = 116; 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_SIZE = 32; 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_CHAT = "chat"; constexpr const char* KEY_PING = "ping"; +constexpr const char* KEY_TIPS = "tips"; constexpr int DEADZONE_MAX = 60; constexpr int DEADZONE_STEP = 5; constexpr const char* KEY_VSYNC = "vsync"; @@ -352,6 +353,8 @@ void Settings::load(Game& game) isChatShown = number; if (SDL_strcmp(line, KEY_PING) == 0) isPingShown = number; + if (SDL_strcmp(line, KEY_TIPS) == 0) + isTipsShown = number; if (SDL_strcmp(line, KEY_WINDOW_SCALE) == 0 && std::find(std::begin(WINDOW_SCALES), std::end(WINDOW_SCALES), number) != std::end(WINDOW_SCALES)) windowScale = number; @@ -414,6 +417,7 @@ void Settings::save(Game& game) KEY_MUTE_UNFOCUSED, isMutedUnfocused, KEY_DAMAGE_NUMBERS, isDamageShown, KEY_RUMBLE, isRumbling, KEY_STICK_DEADZONE, stickDeadzone, KEY_WINDOW_SCALE, windowScale, KEY_CHAT, 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, KEY_FPS_LIMIT, fpsLimit, KEY_FASHION, fashion, KEY_HINTS_LEARNED); 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 + 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 + 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)) { @@ -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() { return color; } // 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) { Settings fresh; @@ -784,6 +791,7 @@ void Settings::defaults_restore(Game& game) windowScale = fresh.windowScale; isChatShown = fresh.isChatShown; isPingShown = fresh.isPingShown; + isTipsShown = fresh.isTipsShown; SDL_strlcpy(serverUrl, fresh.serverUrl, sizeof(serverUrl)); weaponSlots = fresh.weaponSlots; trinketSlots = fresh.trinketSlots; @@ -818,7 +826,7 @@ void Settings::online_draw(Game& game, Rect row, float labelWidth) 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 // when they run long, with the fighter previewed on the right in them, walking. An emptied name goes back to the // default. @@ -848,7 +856,7 @@ void Settings::profile_draw(Game& game, Rect area, Page& page) Tooltip hovered; 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)) ui.separator_draw(game, separator, "Color"); 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); 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. char caption[LINE_LENGTH * 2]; 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. 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 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; - 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 @@ -1114,9 +1122,9 @@ void Settings::loadouts_draw(Game& game, Rect row, Tooltip& hovered) { if (items[i] < 0) continue; - Vec2 centre = {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); - Rect box = {centre.x - size / 2, centre.y - size / 2, size, size}; + Vec2 center = {row.x + LABEL_WIDTH + i * (size + LOADOUT_GAP) + size / 2, y}; + ui.icon_draw(game, kind, items[i], {.color = ui.tint, .position = center}, ICON_SMALL); + Rect box = {center.x - size / 2, center.y - size / 2, size, size}; if (is_point_in_rect(game.input.mouse, box)) 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}); for (int i = 0; i < count; i++) { - Vec2 centre = {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); - Rect box = {centre.x - size / 2, centre.y - size / 2, size, size}; + Vec2 center = {value + i * (size + LOADOUT_GAP) + size / 2, y + WIDGET_HEIGHT / 2}; + ui.icon_draw(game, kind, items[i], {.color = ui.tint, .position = center}, ICON_SMALL); + Rect box = {center.x - size / 2, center.y - size / 2, size, size}; if (is_point_in_rect(game.input.mouse, box)) hovered = ui.tooltip_build(game, kind, items[i], sandbox.item_levels_get(kind)[items[i]]); } diff --git a/src/settings.h b/src/settings.h index 3c45cce..8951098 100644 --- a/src/settings.h +++ b/src/settings.h @@ -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 // 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 -// is a palette index, unassigned (given one at the match) or random (session colour, rolled once per run). The match +// 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 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 // 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. @@ -82,6 +82,7 @@ public: bool isRumbling = true; bool isShaking = true; bool isSpeedShown = false; + bool isTipsShown = true; bool isVsync = true; // The server online play goes through (see protocol.h), and whether the Play tab's rules are unfolded. char serverUrl[SERVER_URL_LENGTH] = {}; diff --git a/src/sim.cmake b/src/sim.cmake index 5fe4c1c..78d6589 100644 --- a/src/sim.cmake +++ b/src/sim.cmake @@ -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 -# 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 ${CMAKE_CURRENT_LIST_DIR}/bot.cpp ${CMAKE_CURRENT_LIST_DIR}/fighter.cpp diff --git a/src/sim.cpp b/src/sim.cpp index 68d3324..bee64e3 100644 --- a/src/sim.cpp +++ b/src/sim.cpp @@ -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 -// 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. void Sim::game_start(Match& lobby, const int* members, int count, const Rules& rules, Uint32 seed) { diff --git a/src/sprites.cpp b/src/sprites.cpp index d6c3ea3..654559c 100644 --- a/src/sprites.cpp +++ b/src/sprites.cpp @@ -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 -// with the pose), in its colour. +// with the pose), in its color. void Sprites::fashion_draw(Renderer& renderer, Pose& pose, int worn) { Anm2Animation* animation = worn > FASHION_NONE && worn < FASHION_COUNT && pose.art diff --git a/src/ui.cpp b/src/ui.cpp index e86cd4f..77f9f15 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -10,7 +10,7 @@ constexpr float LOADOUT_FADE = 0.25f; constexpr float LOADOUT_GAP = 4; constexpr float LOADOUT_DIVIDER = 2; 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 RIVAL_BAR_MIN = 16; constexpr float RIVAL_DIVIDER = 1; @@ -116,6 +116,10 @@ constexpr float FIELD_INSET = 4; constexpr float SLIDER_INSET = 3; constexpr float TEXT_INSET = (WIDGET_HEIGHT - LINE_HEIGHT) / 2; 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 = " "; // How wide a row of first-run hints may run, clear of the hotbars in the corners. 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, 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 // Weapon and Trinket in turn, then the menus. -constexpr const char* TIPS[] = { - "Getting knocked into walls hurts! Do your best to reduce your speed when attacked!", - "Don't get cornered! Knockback damage can add up fast!", - "Crouch to reduce knockback!", - "Crawl around by crouching and moving!", - "Roll around on sloped surfaces with Down!", - "Platforms block shots. Use them as cover!", +constexpr Tip TIPS[] = { + {"Getting knocked into walls hurts! Do your best to reduce your speed when attacked!"}, + {"Don't get cornered! Knockback damage can add up fast!"}, + {"Crouch to reduce knockback!"}, + {"Crawl around by crouching and moving!"}, + {"Roll around on sloped surfaces with Down!"}, + {"Platforms block shots. Use them as cover!"}, - "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!", - "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!", - "Your Charge carries over when you swap Weapons!", - "Higher damaging attacks will glow and vibrate!", - "Going faster makes you glow and vibrate, showing that your attacks deal more 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!"}, + {"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!"}, + {"Your Charge carries over when you swap Weapons!"}, + {"Higher damaging attacks will glow and vibrate!"}, + {"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!", - "Projectiles inherit your velocity. Go fast for big damage!", - "Projectiles lose damage the longer they're out. Get close!", - "Projectiles will fade with time; this fade shows their damage potential!", - "Increasing a projectile's lifetime or speed can both increase its range!", - "Explosions hit hardest at their center!", - "Your explosions don't hurt, and only push you. Try rocket jumping!", - "Recoil is movement. Fire away from where you want to go!", + {"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 lose damage the longer they're out. Get close!"}, + {"Projectiles will fade with time; this fade shows their damage potential!"}, + {"Increasing a projectile's lifetime or speed can both increase its range!"}, + {"Explosions hit hardest at their center!"}, + {"Your explosions don't hurt, and only push you. Try rocket jumping!"}, + {"Recoil is movement. Fire away from where you want to go!"}, - "Mix and match Weapons and Trinkets for nigh-infinite amounts of combos!", - "The Fist's Charged Attack doesn't always send you straight up! Change your aim!", - "Slam a Hammer into surfaces to fly far!", - "The Lance's Charged Attack lunges you forward!", - "Crack the Whip against a wall to be pulled toward it!", - "The Whip's Charged Attack only blasts at the very tip, but does big damage!", - "Charge a Flail to throw its ball, and Charge again to reel it back!", - "You can have the Flail's ball extended and another Weapon equipped for a dangerous combo!", - "Charged Boomerangs are platforms only you can ride!", - "Charge recalls your Boomerangs!", - "A Grapple's Charged Attack yanks you to your target!", - "Swing around on or release a latched Grapple for big speed!", - "You can have a Grapple latched and another Weapon equipped for an acrobatic fight!", - "A Bow's arrows stuck in walls are semi-solid platforms!", - "The Bow's arrows fly faster and further with Charge!", - "Fire a wide volley with the Uzi's Charged Attack!", - "Use a Shotgun's recoil for great movement!", - "The Bolter's projectiles can cling onto surfaces, perfect for area denial!", - "Cook a Grenade Launcher's grenade via Charging to explode it sooner!", - "Fully Charging a Grenade Launcher will cause an explosion in your face!", - "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!", - "The Flamethrower's Charged Attack detonates mid-air! Space an explosion!", - "The Vacuum's Charged Attack deals big knockback! Send foes flying!", - "The Vacuum's Charged Attack turns enemy shots into your own!", - "Hold your own projectiles in the Vacuum's suction to keep them fresh!", - "The Vacuum's suction and Charged Attack can move projectiles around. Manipulate the battlefield!", - "Use the Grimoire's wisps as a close-range attack or to pepper from afar!", - "Use the Grimoire's teleporting Charged Attack to escape danger!", + {"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!", "Fist"}, + {"Slam a Hammer into surfaces to fly far!", "Hammer"}, + {"The Lance's Charged Attack lunges you forward!", "Lance"}, + {"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!", "Whip"}, + {"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!", "Flail"}, + {"Charged Boomerangs are platforms only you can ride!", "Boomerang"}, + {"Charge recalls your Boomerangs!", "Boomerang"}, + {"A Grapple's Charged Attack yanks you to your target!", "Grapple"}, + {"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!", "Grapple"}, + {"A Bow's arrows stuck in walls are semi-solid platforms!", "Bow"}, + {"The Bow's arrows fly faster and further with Charge!", "Bow"}, + {"Fire a wide volley with the Uzi's Charged Attack!", "Uzi"}, + {"Use a Shotgun's recoil for great movement!", "Shotgun"}, + {"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!", "Grenade Launcher"}, + {"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!", + "Grenade Launcher"}, + {"Mines need a moment to arm before they'll go off!", "Mine"}, + {"The Flamethrower's Charged Attack detonates mid-air! Space an explosion!", "Flamethrower"}, + {"The Vacuum's Charged Attack deals big knockback! Send foes flying!", "Vacuum"}, + {"The Vacuum's Charged Attack turns enemy shots into your own!", "Vacuum"}, + {"Hold your own projectiles in the Vacuum's suction to keep them fresh!", "Vacuum"}, + {"The Vacuum's suction and Charged Attack can move projectiles around. Manipulate the battlefield!", "Vacuum"}, + {"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!", - "Splitter copies fan wider with each level!", - "The off-hand Weapon from Dual Wield is the one you last switched from!", - "Ricochet makes projectiles deal more damage the more they've bounced!", - "Ricochet bounces reset a projectile's lifetime!", - "Tesla Coil arcs electricity between your projectiles. Make traps or wide danger zones!", - "The kickback the Recoil Coil gives is perfect for movement!", - "Stay in place with a Shock Absorber!", - "Embiggener makes you a bigger target, but gives a solid damage buff!", - "Make yourself a living hazard with a Pebble Helmet!", - "With a Speedometer, moving fast and attacking deals bigger damage!", - "A pair of Wings will make you dash to where you're aiming when you jump mid-air!", - "With the Feather, hold Up in the air to float!", - "With Iron Weights, faster falls produce bigger blasts!", - "Cling, slide, and kick off walls with a Climb Claw!", + {"Diazepam reduces Weapon spread, perfect for concentrated attacks!", "Diazepam"}, + {"Splitter copies fan wider with each level!", "Splitter"}, + {"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"}, + {"Ricochet bounces reset a projectile's lifetime!", "Ricochet"}, + {"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!", "Recoil Coil"}, + {"Stay in place with a Shock Absorber!", "Shock Absorber"}, + {"Embiggener makes you a bigger target, but gives a solid damage buff!", "Embiggener"}, + {"Make yourself a living hazard with a Pebble Helmet!", "Pebble Helmet"}, + {"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!", "Wings"}, + {"With the Feather, hold Up in the air to float!", "Feather"}, + {"With Iron Weights, faster falls produce bigger blasts!", "Iron Weights"}, + {"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!", - "When in a Lobby, queue up a game in the menu!", - "Queuing takes place in the Sandbox! Holler fighters over to your queued area and Ready up!", - "Change a game's rules underneath the Rules option!", - "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!", - "Change your fighter and HUD color in Profile!", - "Feeling naked and unsexy? Wear some Fashion within your Profile!", - "Set and load custom gear Loadouts in your Profile!", - "Save a gear Loadout and use Shift + a number to equip it!", - "Turn on DPS in Settings to see the damage you deal each second!", - "Turn on Speed in Settings to see how fast you're going!", - "Turn on FPS in Settings to see how smoothly the game runs!", - "Is previewing your aim too intrusive? Toggle Aim Preview off in Settings!", - "Want to see how things actually collide? Toggle Display Collision in Settings!", - "Game too loud? Turn down the Volume or Mute it in Settings!", - "Don't like the Controls? Tweak them to your liking in Settings!", - "Want to experiment with more Weapon or Trinket slots in the Sandbox? Change them in Settings!", - "Want to fight in the Sandbox with others? Check PvP in Settings or use the /pvp command!", - "The Dummy in the Sandbox doesn't feel pain! Beat it as much as you'd like, it enjoys it!", - "Throw tomatoes to mock a game's winner in between rounds by clicking anywhere on screen!", - "Sling slurs and trashtalk all you wish with a Chat message!", - "A number of useful / commands are present in Chat! Type /help to see!", - "Want to see the sexy person who made this game? Check About in the menu!", + {"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!", nullptr, TIP_ONLINE}, + {"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!", nullptr, TIP_MENU}, + {"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!", nullptr, TIP_MENU}, + {"Change your fighter and HUD color in Profile!", nullptr, TIP_MENU}, + {"Feeling naked and unsexy? Wear some Fashion within your Profile!", nullptr, TIP_MENU}, + {"Set and load custom gear Loadouts in your Profile!", nullptr, TIP_MENU}, + {"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!", nullptr, TIP_MENU}, + {"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!", nullptr, TIP_MENU}, + {"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!", nullptr, TIP_MENU}, + {"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!", nullptr, TIP_MENU}, + {"Want to experiment with more Weapon or Trinket slots in the Sandbox? Change them in Settings!", nullptr, + TIP_MENU}, + {"Want to fight in the Sandbox with others? Check PvP in Settings or use the /pvp command!", nullptr, TIP_ONLINE}, + {"The Dummy in the Sandbox doesn't feel pain! Beat it as much as you'd like, it enjoys it!"}, + {"Throw tomatoes to mock a game's winner in between rounds by clicking anywhere on screen!"}, + {"Sling slurs and trashtalk all you wish with a Chat message!", nullptr, TIP_ONLINE}, + {"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); @@ -287,8 +311,9 @@ void Ui::draw(Game& game) shade = color_shade(tint, SECONDARY_SHADE); Vec2 mouse = game.input.mouse; - // The menu covers the match overlays. Hotbars sit under them, except in the sandbox menu they slide into. With - // several players on this screen in a match, each gets a compact panel along the bottom instead. + // The menu covers the match overlays. Hotbars sit under them (hidden while the menu is up, except on its item tabs, + // which they slide under). With several players on this screen in a match, each gets a compact panel along the + // bottom instead. statusRect = {}; queues_draw(game); bars_draw(game); @@ -298,7 +323,7 @@ void Ui::draw(Game& game) offscreen_draw(game, camera); bool isBarsRaised = game.menu.is_bars_open(); 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); hotbar_draw(game); @@ -317,6 +342,8 @@ void Ui::draw(Game& game) game.menu.draw(game); if (game.sim.match.state == MATCH_COUNTDOWN && game.menu.isOpen && !isShared) countdown_aside_draw(game); + else if (game.menu.isOpen) + tip_draw(game, tip_low_get()); if (isBarsRaised) hotbar_draw(game); 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 -// 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) { 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); 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); Vec2 size = game.text.size_get(name); bottom -= size.y; @@ -394,11 +421,11 @@ void Ui::match_draw(Game& game) { Match& match = game.sim.match; char text[TOOLTIP_LENGTH]; - isTipChosen &= match.state == MATCH_COUNTDOWN; Input& input = game.input; if (match.state == MATCH_PRACTICE) { - hints_draw(game); + if (!game.menu.isOpen) + hints_draw(game); return; } @@ -407,7 +434,7 @@ void Ui::match_draw(Game& game) race_draw(game); 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 // it moves with it). It stays put while the menu moves the hotbar. Renderer& renderer = game.renderer; @@ -460,9 +487,6 @@ void Ui::match_draw(Game& game) CountdownInfo info = countdown_info_get(game); bool isWatching = game.session.is_watching(game); bool isLocalReady = match.isReady[game.local] || isWatching; - if (!isTipChosen) - tip = random_get(0, std::size(TIPS)); - isTipChosen = true; if (game.menu.isOpen) break; @@ -490,8 +514,7 @@ void Ui::match_draw(Game& game) widgets_enable(true); y += READY_BUTTON_HEIGHT + READY_GAP; - SDL_snprintf(text, sizeof(text), "Tip: %s", TIPS[tip]); - text_center_draw(game, text, y, 1); + tip_draw(game, y); y += LINE_HEIGHT + TIP_GAP; loadoutTop = y; 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)); else 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, match.winner >= 0 ? game.color_get(match.winner) : tint); 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); } -// 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. 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. 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; } -// 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, // 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 @@ -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 // ready, and the key that readies up. static CountdownInfo countdown_info_get(Game& game) @@ -903,6 +926,72 @@ static CountdownInfo countdown_info_get(Game& game) 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 // attack), keeping it in the settings across launches, and fades learned ones out. 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. 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); } - y = BAR_Y + SLOT_SIZE + HINT_GAP + (isFocusShown ? LINE_HEIGHT : 0); - SDL_snprintf(text, sizeof(text), "Tip: %s", TIPS[tip]); - text_center_draw(game, text, y, 1); + tip_draw(game, tip_low_get()); } 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}; } -// 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. 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}}); } -// 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. 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; if (tooltip.fashion > 0) 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}}); } -// 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) { 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; } -// 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). 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; } -// 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 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; } -// 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) { 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; } -// 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). 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). - 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); 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 height = nameSize.y + PREVIEW_NAME_GAP + body * PREVIEW_SCALE + PREVIEW_CAPTION_GAP + captionHeight; 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); - player_draw(game, {centre, feet}, color, PREVIEW_SCALE, worn); + player_draw(game, {center, feet}, color, PREVIEW_SCALE, worn); float y = feet + PREVIEW_CAPTION_GAP; 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]; SDL_strlcpy(line, start, std::min(end ? end - start + 1 : sizeof(line), sizeof(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; start = end ? end + 1 : nullptr; } @@ -2246,6 +2333,7 @@ static void bars_arrange(CooldownBar* bars) void Ui::update(Game& game) { hints_update(game); + tips_update(game); spectate_update(game); // 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) { 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); } -// 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. 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), // otherwise where its stick aims. With none (a bot playing), the mouse's. 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)}}); } -// 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. 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) { if (spectated < 0) @@ -2646,7 +2734,7 @@ RivalLayout Ui::rivals_layout(Game& game) 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 // 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. @@ -2716,7 +2804,7 @@ void Ui::rivals_draw(Game& game) 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) { renderer.layer_end(base * hover_alpha_get(rivalHovers[index])); @@ -2734,9 +2822,9 @@ void Ui::rivals_draw(Game& game) if (item < 0) continue; 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}; - 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; if (kind == ITEM_WEAPON) @@ -2769,7 +2857,7 @@ void Ui::picks_draw(Game& game, float top) 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 // 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 @@ -2938,8 +3026,8 @@ void Ui::focus_begin(Game& game) : direction == NAV_DOWN ? b.y - (a.y + a.h) : direction == NAV_LEFT ? a.x - (b.x + b.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); - bool isAhead = (direction == NAV_UP || direction == NAV_LEFT) ? centre < -1 : centre > 1; + 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) ? center < -1 : center > 1; 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)}); 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. int Ui::scroll_take(Rect area) { - Vec2 centre = {focusRect.x + focusRect.w / 2, focusRect.y + focusRect.h / 2}; - if (!scrollWant || !isFocusShown || !is_point_in_rect(centre, area)) + Vec2 center = {focusRect.x + focusRect.w / 2, focusRect.y + focusRect.h / 2}; + if (!scrollWant || !isFocusShown || !is_point_in_rect(center, area)) return 0; return scrollWant == NAV_UP ? -1 : 1; } @@ -3042,7 +3130,7 @@ void Ui::scroll_mark() 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, // 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) diff --git a/src/ui.h b/src/ui.h index bdb7d9b..dd521a6 100644 --- a/src/ui.h +++ b/src/ui.h @@ -11,6 +11,8 @@ constexpr int CHAMFER = 2; constexpr int TOOLTIP_LENGTH = 512; constexpr int WEAPON_HANDS = 2; 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 SLOT_GAP = 4; constexpr float SLOT_SIZE = 28; @@ -108,7 +110,7 @@ struct Tooltip int slot = -1; // A fashion item it's about instead (none for 0), whose art is its icon. 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; }; @@ -186,7 +188,7 @@ struct Slot 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 // 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 @@ -326,10 +328,11 @@ private: float loadoutTop = 0; float widgetAlpha = 1; 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). int countdownBeeped = 0; bool isActiveSeen = false; - bool isTipChosen = false; bool isWidgetEnabled = true; bool isWidgetLive = false; @@ -342,6 +345,10 @@ private: void countdown_aside_draw(Game& game); void countdown_beep(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 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); diff --git a/src/weapon.cpp b/src/weapon.cpp index 6755609..3637a7f 100644 --- a/src/weapon.cpp +++ b/src/weapon.cpp @@ -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 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) { 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); } -// 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) { 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). -static bool is_box_clear(Sim& sim, Fighter& fighter, Vec2 centre) +// 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 center) { Rect box = fighter.body.box; 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 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)) 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. static Vec2 teleport_spot(Sim& sim, Fighter& fighter, Vec2 target, float range) { - Vec2 centre = fighter.center_get(); - Vec2 offset = target - centre; + Vec2 center = fighter.center_get(); + Vec2 offset = target - center; float distance = std::min(range, vec2_length(offset)); Vec2 direction = vec2_normalize(offset); 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)) return spot; } - return centre; + return center; } // 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; } -// 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) { int held = fighter.weapon_get(); @@ -873,7 +873,7 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot) 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 - // 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++) { 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) 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. float absorb = fighter.stat_get(STAT_MOVEMENT); 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); - // 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. float taper = is_cone(def) ? cone_strength_get(attack, point) : 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). -// 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. std::vector Weapons::chain_get(Sim& sim, Fighter& fighter) { @@ -1881,8 +1881,8 @@ std::vector Weapons::paths_get(Sim& sim, Fighter& fighter) Vec2 from = fighter.center_get(); Path path = {.area = stats.area * def.scale * fighter.stat_get(STAT_ATTACK_SIZE)}; float length = vec2_length(spot - from); - for (float travelled = 0; travelled < length; travelled += TRAJECTORY_STEP) - path.steps.push_back(from + (spot - from) * (travelled / length)); + for (float traveled = 0; traveled < length; traveled += TRAJECTORY_STEP) + path.steps.push_back(from + (spot - from) * (traveled / length)); path.steps.push_back(spot); paths.push_back(path); return paths; @@ -1926,7 +1926,7 @@ std::vector Weapons::paths_get(Sim& sim, Fighter& fighter) int bounces = def.isBouncy ? (int)fighter.stat_get(STAT_RICOCHETS) : 0; Path path; 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)); 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 -// 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) { 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 // 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 -// 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 // 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) diff --git a/src/weapon.h b/src/weapon.h index 6d8b7f2..f1cbacb 100644 --- a/src/weapon.h +++ b/src/weapon.h @@ -489,7 +489,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = { }, { .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.", .hit = HIT_PIERCE, .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. // 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 -// 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 -// (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 // and the game state can be sent whole to a player joining. struct Attack @@ -1365,7 +1365,7 @@ struct Attack float fade = 0; float hitCooldown = 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 lifeStart = 0; float lifetime = 0; diff --git a/src/world.cpp b/src/world.cpp index 7a3f99c..a881673 100644 --- a/src/world.cpp +++ b/src/world.cpp @@ -365,7 +365,7 @@ void World::ramps_load(std::vector& 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) { tiles[block.tile] = TILE_EMPTY;