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;