diff --git a/server/server.cpp b/server/server.cpp index 5988578..265bf5e 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -37,7 +37,8 @@ constexpr const char INPUTS_LABEL[] = "inputs"; constexpr size_t MESSAGE_MAX = 65536; constexpr int CODE_TRIES = 100; constexpr int FRAME_RING = 256; -constexpr int FRAMES_PER_PACKET = 30; +// Frames go out a packet of at most this many bytes at a time, so a lossy one fits one datagram and is never split. +constexpr size_t FRAME_BYTES_MAX = 1100; constexpr int INPUT_REACH = 120; constexpr int CHECKSUM_INTERVAL = 60; constexpr int CATCH_UP_TICKS = 5; @@ -66,9 +67,10 @@ struct Room int tick = 0; }; -// Someone connected: their name, socket and WebRTC connection (with its reliable channel and lossy one), their lobby (empty until they make or join one), the room they play in -// (0 for none), the last tick of it they've confirmed, how early their controls last came, and the fighter they -// brought, kept while they're away in a game so they come back to the sandbox as they left. +// Someone connected: their name, socket and WebRTC connection (with its reliable channel and lossy one), their lobby +// (empty until they make or join one), the room they play in (0 for none), the last tick of it they've confirmed, how +// early their controls last came, and the fighter they brought, kept while they're away in a game so they come back to +// the sandbox as they left. struct Member { Profile profile; @@ -135,8 +137,7 @@ static void send(int id, const std::string& message, bool isBinary = false, bool member.socket->send(message); } catch (const std::exception&) - { - } + {} } // Answers a member's WebRTC offer with a connection of the server's own, passing its description and candidates back @@ -606,7 +607,7 @@ static void game_follow(int serial) rooms.erase(serial); } -// Sends everyone in a room the frames they haven't confirmed (up to a packet's worth), and every so often the state's +// Sends everyone in a room the frames they haven't confirmed (as many as fit a packet), and every so often the state's // checksum. static void frames_send(int serial) { @@ -616,7 +617,14 @@ static void frames_send(int serial) if (member.room != serial) continue; int first = std::max(member.ack + 1, room.tick - FRAME_RING + 1); - int count = std::min(room.tick - first, FRAMES_PER_PACKET); + int count = 0; + for (size_t bytes = 0; first + count < room.tick; count++) + { + bytes += + sizeof(Uint16) + sizeof(Uint8) + room.frames[(first + count) % FRAME_RING].count * sizeof(Controls); + if (bytes > FRAME_BYTES_MAX && count > 0) + break; + } if (count <= 0) continue; PacketWriter packet; @@ -633,7 +641,8 @@ static void frames_send(int serial) if (room.tick % CHECKSUM_INTERVAL == 0) for (auto& [id, member] : members) if (member.room == serial) - send(id, PacketWriter().put(PACKET_CHECKSUM).put(serial).put(room.tick).put(room.sim.checksum_get()).bytes, + send(id, + PacketWriter().put(PACKET_CHECKSUM).put(serial).put(room.tick).put(room.sim.checksum_get()).bytes, true); } diff --git a/src/fighter.cpp b/src/fighter.cpp index 717591e..fae4c07 100644 --- a/src/fighter.cpp +++ b/src/fighter.cpp @@ -246,10 +246,10 @@ void Fighter::update(Sim& sim) { float impact = std::clamp(fallSpeed / POUND_SPEED, POUND_SCALE_MIN, POUND_SCALE_MAX); sim.weapons.blast_spawn(sim, {.position = feet, - .area = POUND_AREAS[trinket_level_get(TRINKET_WEIGHTS)] * std::sqrt(impact), - .damage = trinket_sum_get(TRINKET_WEIGHTS, POUND_DAMAGES) * impact, - .owner = index, - .sound = SOUND_POUND}); + .area = POUND_AREAS[trinket_level_get(TRINKET_WEIGHTS)] * std::sqrt(impact), + .damage = trinket_sum_get(TRINKET_WEIGHTS, POUND_DAMAGES) * impact, + .owner = index, + .sound = SOUND_POUND}); } // A kick holds its pose briefly, and a launch tumbles faster the faster it flies. @@ -526,12 +526,12 @@ void Fighter::bounce_resolve(Sim& sim, Vec2 before) int slam = (impact - SLAM_SPEED) * SLAM_DAMAGE; if (slam > 0) sim.weapons.damage_deal(sim, {.point = center_get(), - .source = center_get(), - .knock = 0, - .amount = slam, - .owner = launcher, - .target = index, - .sound = SOUND_HIT_BLUNT_HEAVY}); + .source = center_get(), + .knock = 0, + .amount = slam, + .owner = launcher, + .target = index, + .sound = SOUND_HIT_BLUNT_HEAVY}); isLaunched = vec2_length(body.velocity) >= LAUNCH_END_SPEED; } diff --git a/src/fighter.h b/src/fighter.h index 62a0161..cd20783 100644 --- a/src/fighter.h +++ b/src/fighter.h @@ -13,7 +13,7 @@ struct Attack; struct Sim; constexpr int ECHO_MAX = 64; -constexpr int MAX_FIGHTERS = SPAWN_COUNT; +constexpr int MAX_FIGHTERS = SPAWN_MAX; constexpr float BOUNCE_KEEP = 0.75f; constexpr float SECONDARY_SHADE = 0.5f; constexpr int COLOR_COUNT = 16; diff --git a/src/game.h b/src/game.h index 520725a..cbc33de 100644 --- a/src/game.h +++ b/src/game.h @@ -45,10 +45,17 @@ struct Game Vec2 camera; Vec2 cameraPrevious; float alpha = 1; - int devices[MAX_FIGHTERS] = {DEVICE_KEYBOARD, DEVICE_NONE, DEVICE_NONE, DEVICE_NONE}; + int devices[MAX_FIGHTERS]; int local = 0; SDL_Window* window = nullptr; + // This screen starts with the keyboard playing the first fighter. + Game() + { + std::fill(std::begin(devices), std::end(devices), DEVICE_NONE); + devices[0] = DEVICE_KEYBOARD; + } + Fighter& local_get() { return sim.fighters[local]; diff --git a/src/match.cpp b/src/match.cpp index 7ef9a3f..8420c5b 100644 --- a/src/match.cpp +++ b/src/match.cpp @@ -1,6 +1,7 @@ #include "match.h" #include "sim.h" #include +#include #include constexpr Burst FINISH_BURST = {.gravity = 200, .life = 1, .size = 3, .speed = 240, .count = 40}; @@ -69,6 +70,14 @@ static int level_get(ItemRules& items, int round) return std::clamp(items.startLevel - 1 + (items.isLevelUp ? round : 0), 0, LEVEL_COUNT - 1); } +// Fighters start in palette order, played by no one and in no queue. +Match::Match() +{ + std::iota(std::begin(colors), std::end(colors), 0); + std::fill(std::begin(owners), std::end(owners), -1); + std::fill(std::begin(queuedIn), std::end(queuedIn), -1); +} + void Match::update(Sim& sim) { timer -= TICK; @@ -752,8 +761,8 @@ 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 whoever -// knows them). +// A fighter's loadout, slots and levels as a profile, to bring it back as it was (a name and colour are added by +// whoever knows them). Profile profile_get(Fighter& fighter) { Profile profile; diff --git a/src/match.h b/src/match.h index 5866275..d8014a0 100644 --- a/src/match.h +++ b/src/match.h @@ -6,7 +6,7 @@ struct Sim; constexpr int BEST_OF_MAX = 9; -constexpr int PLAYERS_MAX = 4; +constexpr int PLAYERS_MAX = 8; constexpr int PLAYERS_MIN = 2; constexpr int TIMER_MAX = 10; // A queued game's area: room per player, a margin each end, its height, how finely the ground's checked for one, and @@ -195,17 +195,17 @@ public: int round = 0; int winner = -1; int wins[MAX_FIGHTERS] = {}; - int colors[MAX_FIGHTERS] = {0, 1, 2, 3}; + int colors[MAX_FIGHTERS]; // Online, which lobby member plays each fighter (-1 for none) and their name, kept with the game so every machine // (and a player joining) agrees who's who as fighters come and go. - int owners[MAX_FIGHTERS] = {-1, -1, -1, -1}; + int owners[MAX_FIGHTERS]; char names[MAX_FIGHTERS][PLAYER_NAME_MAX + 1] = {}; bool isReady[MAX_FIGHTERS] = {}; // Online, in the lobby's sandbox: each fighter's queued game, which queue's area it's standing in (-1 for none), // and whether it's readied up there. // In a queued game, whether each fighter has left it (out for good), and whether that was before it was over. Queue queues[MAX_FIGHTERS]; - int queuedIn[MAX_FIGHTERS] = {-1, -1, -1, -1}; + int queuedIn[MAX_FIGHTERS]; bool isQueueReady[MAX_FIGHTERS] = {}; bool isGone[MAX_FIGHTERS] = {}; bool isForfeited[MAX_FIGHTERS] = {}; @@ -221,6 +221,7 @@ public: bool isSandboxRequested = false; Fighter sandbox; + Match(); void update(Sim& sim); void start(Sim& sim); void colors_assign(Sim& sim); diff --git a/src/menu.cpp b/src/menu.cpp index 388c0ae..00af937 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -32,6 +32,10 @@ constexpr float PLAY_HEIGHT = 22; constexpr float RADIO_GAP = 12; constexpr float ROW_HEIGHT = 18; constexpr float TEXT_ROW_INSET = 1; +// The lobby's player list, when crowded into two columns: the gap between its tiny rows, and the widest ping it leaves +// room for. +constexpr float COMPACT_ROW_GAP = 2; +constexpr const char* PING_SAMPLE = "999ms"; constexpr int LABEL_LENGTH = 64; constexpr float SCROLL_HEAD_MIN = 8; constexpr float SCROLL_WIDTH = 6; @@ -301,7 +305,7 @@ void Menu::hints_draw(Game& game, MenuLayout& layout) const char* hint = "A: Select B: Close LB/RB: Tabs"; if (tab < ITEM_TABS) hint = game.sim.match.state == MATCH_PRACTICE ? "A: Equip/Unequip X/Y: Level RS: Scroll Info LB/RB: Tabs" - : "A: Equip/Unequip RS: Scroll Info B: Close LB/RB: Tabs"; + : "A: Equip/Unequip RS: Scroll Info B: Close LB/RB: Tabs"; else if (tab == TAB_PLAY && (page == PAGE_RULES || page == PAGE_QUEUE)) hint = "A: Select Left/Right: Change B: Back Y: Join"; else if (tab == TAB_SETTINGS) @@ -661,23 +665,39 @@ void Menu::online_draw(Game& game, Rect area) ui.widgets_enable(true); y += ROW_HEIGHT; - // Everyone in it, in the order they came (the first hosting), in their colours, with their connections. + // Everyone in it, in the order they came (the first hosting), in their colours, with their connections. When they + // don't all fit down the page, they go in two columns in the tiny font, names cut short to fit. ui.separator_draw(game, {area.x, y, area.w, WIDGET_HEIGHT}, "Players"); y += ROW_HEIGHT; - for (NetMember& member : net.members) + int count = net.members.size(); + bool isCompact = y + count * ROW_HEIGHT > area.y + area.h; + Font font = isCompact ? FONT_TINY : FONT_SMALL; + float rowHeight = isCompact ? text.line_height_get(FONT_TINY) + COMPACT_ROW_GAP : ROW_HEIGHT; + float inset = isCompact ? 0 : TEXT_ROW_INSET; + int rows = isCompact ? (count + 1) / 2 : count; + float columnWidth = isCompact ? std::floor((area.w - PADDING) / 2) : area.w; + float pingWidth = text.size_get(PING_SAMPLE).x + PADDING * 2; + for (int k = 0; k < count; k++) { + NetMember& member = net.members[k]; bool isSelf = member.id == net.selfId; std::string name = session.isActive ? session.name_get(game, member.id) : member.name; - SDL_snprintf(line, sizeof(line), "- %s%s%s", name.c_str(), member.id == net.hostId ? " [Host]" : "", - isSelf ? " [You]" : ""); + const char* tags = member.id == net.hostId ? (isSelf ? " [Host] [You]" : " [Host]") : isSelf ? " [You]" : ""; + float room = columnWidth - (member.ping >= 0 ? pingWidth : 0); + SDL_snprintf(line, sizeof(line), "- %s%s", name.c_str(), tags); + while (text.size_get(line, font).x > room && !name.empty()) + { + name.pop_back(); + SDL_snprintf(line, sizeof(line), "- %s...%s", name.c_str(), tags); + } int fighter = session.fighter_get(game, member.id); Color color = fighter >= 0 && game.sim.match.state == MATCH_PRACTICE ? game.color_get(fighter, true) : ui.tint; - text.draw(game.renderer, line, {.color = color, .position = {area.x, y + TEXT_ROW_INSET}}); - float x = area.x + text.size_get(line).x + PADDING * 2; + Vec2 corner = {area.x + (k / rows) * (columnWidth + PADDING), y + (k % rows) * rowHeight + inset}; + text.draw(game.renderer, line, {.color = color, .position = corner}, font); if (member.ping >= 0) - ui.ping_draw(game, {x, y + TEXT_ROW_INSET}, member.ping, 1); - y += ROW_HEIGHT; + ui.ping_draw(game, {corner.x + text.size_get(line, font).x + PADDING * 2, corner.y}, member.ping, 1); } + y += rows * rowHeight; if (game.session.isDesynced) status_draw("Out of sync with the lobby: rejoin to fix it", true); else if (game.session.isActive && !game.session.isJoined) diff --git a/src/net.cpp b/src/net.cpp index 67dfa41..e83e368 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -392,8 +392,8 @@ bool Net::is_linked() return impl->link.is_open(); } -// Once a frame: handles what the network reported (on desktop), pings the server over the link, and on desktop keeps the -// server connection alive. +// Once a frame: handles what the network reported (on desktop), pings the server over the link, and on desktop keeps +// the server connection alive. void Net::update() { std::vector> work; diff --git a/src/protocol.h b/src/protocol.h index 6029c82..1673637 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -28,7 +28,7 @@ #include constexpr int PROTOCOL_VERSION = 3; -constexpr int LOBBY_MEMBERS_MAX = 4; +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. constexpr const char LOBBY_CODE_LETTERS[] = "ABCDEFGHJKLMNPQRSTUVWXYZ"; diff --git a/src/scene.cpp b/src/scene.cpp index 93ae0c8..e91bbe1 100644 --- a/src/scene.cpp +++ b/src/scene.cpp @@ -125,7 +125,6 @@ struct Stroke float jitter = 0; }; - // A beam, rope or whip, each piece coloured by the strength there, dimming along it the way damage falls off. Beams // start as far along as they've travelled. Jitter kinks it at every piece, moving each frame so it crackles. static void strength_line_draw(Game& game, Attack& attack, Stroke stroke) @@ -528,7 +527,8 @@ void Scene::world_draw() } } -// Debug view: bot.surfaces as lines, bot.links as thin lines from takeoff to landing, and each bot's remaining path in white. +// Debug view: bot.surfaces as lines, bot.links as thin lines from takeoff to landing, and each bot's remaining path in +// white. void Scene::bot_draw() { Bot& bot = game.sim.bot; @@ -570,8 +570,9 @@ void Scene::fighter_draw(Fighter& fighter) bool isLocal = fighter.index == game.local && game.devices[fighter.index] != DEVICE_NONE; if (isLocal && game.sim.match.is_racing()) for (int row = 0; row < CHECKPOINT_HEIGHT; row++) - renderer.rect_draw({std::round(fighter.safe.x - BOX_WIDTH / 2), std::round(fighter.safe.y) - row - 1, BOX_WIDTH, 1}, - color_fade(color, CHECKPOINT_ALPHA * (1 - row / CHECKPOINT_HEIGHT))); + renderer.rect_draw( + {std::round(fighter.safe.x - BOX_WIDTH / 2), std::round(fighter.safe.y) - row - 1, BOX_WIDTH, 1}, + color_fade(color, CHECKPOINT_ALPHA * (1 - row / CHECKPOINT_HEIGHT))); float fill = game.sim.weapons.buff_get(fighter); Vec2 camera = renderer.camera; @@ -585,7 +586,8 @@ void Scene::fighter_draw(Fighter& fighter) float pound = fighter.pound_area_get(); if (pound > 0) - renderer.ring_draw({fighter.center_get().x, fighter.body.box.y + fighter.body.box.h}, pound, color_fade(color, POUND_RING_ALPHA)); + renderer.ring_draw({fighter.center_get().x, fighter.body.box.y + fighter.body.box.h}, pound, + color_fade(color, POUND_RING_ALPHA)); if (isLocal && !game.menu.isOpen && game.settings.isAimShown) paths_draw(fighter); } diff --git a/src/settings.cpp b/src/settings.cpp index 0f305ae..8822240 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -363,12 +363,14 @@ void Settings::apply(Game& game) void Settings::fullscreen_apply(Game& game) { #ifdef __EMSCRIPTEN__ - EM_ASM({ - if ($0 && !document.fullscreenElement) - document.documentElement.requestFullscreen().catch(function(error){}); - if (!$0 && document.fullscreenElement) - document.exitFullscreen(); - }, isFullscreen); + EM_ASM( + { + if ($0 && !document.fullscreenElement) + document.documentElement.requestFullscreen().catch(function(error){}); + if (!$0 && document.fullscreenElement) + document.exitFullscreen(); + }, + isFullscreen); #else SDL_SetWindowFullscreen(game.window, isFullscreen); #endif diff --git a/src/settings.h b/src/settings.h index 1681ff3..8b0276a 100644 --- a/src/settings.h +++ b/src/settings.h @@ -11,7 +11,6 @@ struct Page; constexpr int NAME_LENGTH = 16; - // The sandbox as it was left: where the fighter stood (its feet), the slot it held, and its loadouts and levels. struct SandboxSave { diff --git a/src/sim.cpp b/src/sim.cpp index 873e39f..8cb5730 100644 --- a/src/sim.cpp +++ b/src/sim.cpp @@ -103,11 +103,7 @@ void level_step(Sim& sim, int& level, int step) Snapshot Sim::snapshot_save() { - Snapshot snapshot = {.match = match, - .weapons = weapons, - .world = world, - .rng = rng, - .fighterCount = fighterCount}; + Snapshot snapshot = {.match = match, .weapons = weapons, .world = world, .rng = rng, .fighterCount = fighterCount}; std::copy(std::begin(fighters), std::end(fighters), snapshot.fighters); std::copy(std::begin(bot.states), std::end(bot.states), snapshot.bots); return snapshot; diff --git a/src/sim.h b/src/sim.h index 0589ce8..a8c2ff2 100644 --- a/src/sim.h +++ b/src/sim.h @@ -9,23 +9,40 @@ class Cues { public: virtual ~Cues() = default; - virtual void sound_play(SoundId sound) {} - virtual void charge_play(Fighter& fighter) {} - virtual void charge_stop(Fighter& fighter) {} - virtual void burst_spawn(Burst burst, Vec2 position, float angle, int owner) {} - virtual void attack_burst_spawn(Burst burst, Vec2 position, float angle, Attack& attack) {} - virtual void shake_add(float amount) {} - virtual void flash_spawn(Vec2 position, float angle, float size, int owner) {} - virtual void fighter_afterimage_spawn(Fighter& fighter) {} - virtual void attack_afterimage_spawn(Attack& attack) {} - virtual void fighter_aura_spawn(Fighter& fighter) {} - virtual void attack_aura_spawn(Attack& attack) {} - virtual void number_spawn(Damage& damage) {} - virtual void level_step(bool isUp) {} - virtual void match_start() {} - virtual void choice_open() {} - virtual void choice_close() {} - virtual void sandbox_return() {} + virtual void sound_play(SoundId sound) + {} + virtual void charge_play(Fighter& fighter) + {} + virtual void charge_stop(Fighter& fighter) + {} + virtual void burst_spawn(Burst burst, Vec2 position, float angle, int owner) + {} + virtual void attack_burst_spawn(Burst burst, Vec2 position, float angle, Attack& attack) + {} + virtual void shake_add(float amount) + {} + virtual void flash_spawn(Vec2 position, float angle, float size, int owner) + {} + virtual void fighter_afterimage_spawn(Fighter& fighter) + {} + virtual void attack_afterimage_spawn(Attack& attack) + {} + virtual void fighter_aura_spawn(Fighter& fighter) + {} + virtual void attack_aura_spawn(Attack& attack) + {} + virtual void number_spawn(Damage& damage) + {} + virtual void level_step(bool isUp) + {} + virtual void match_start() + {} + virtual void choice_open() + {} + virtual void choice_close() + {} + virtual void sandbox_return() + {} }; inline Cues CUES_NONE; diff --git a/src/state.cpp b/src/state.cpp index fb83bdb..d656122 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -122,7 +122,7 @@ template static void state_walk(Stream& stream, Sim stream.list(world.obstacles); stream.list(world.ramps); stream.list(world.tiles); - stream.raw(world.spawns, SPAWN_COUNT); + stream.raw(world.spawns, SPAWN_MAX); stream.one(world.height); stream.one(world.width); } diff --git a/src/ui.cpp b/src/ui.cpp index 6ef16c4..240fd07 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -23,6 +23,7 @@ constexpr float HUD_HOVER_FADE = 0.15f; constexpr float RIVAL_PADDING = 2; constexpr int RIVAL_COLUMNS_MAX = 3; constexpr int RIVAL_FULL_SLOTS = 4; +constexpr int RIVALS_PER_ROW = 4; constexpr float COOLDOWN_SLOTS = 2; constexpr float MINI_ICON_SCALE = 0.5f; constexpr float MINI_SLOT = 16; @@ -490,7 +491,8 @@ void Ui::race_draw(Game& game) for (int i = 0; i < game.sim.fighterCount; i++) { Fighter& fighter = game.sim.fighters[i]; - float progress = game.sim.match.finishTimes[i] >= 0 ? 1 : std::clamp(fighter.center_get().x / length, 0.0f, 1.0f); + float progress = + game.sim.match.finishTimes[i] >= 0 ? 1 : std::clamp(fighter.center_get().x / length, 0.0f, 1.0f); float x = std::round(left + progress * TRACK_WIDTH - TRACK_DOT / 2); Rect dot = {x, std::round(y - TRACK_DOT / 2), TRACK_DOT, TRACK_DOT}; game.renderer.rect_draw(dot, game.color_get(i)); @@ -2192,11 +2194,12 @@ void Ui::spectate_draw(Game& game) text_center_draw(game, text, y + LINE_HEIGHT, 1, shade); } -// Lays out the other fighters' windows in a row along the bottom right, never left of the screen's middle (nor over the -// local hotbar and cooldowns). A window is as wide as its icon row, but no wider than one holding two weapons and two -// trinkets at full size: past that, or when the row doesn't fit, icons shrink to small, then tiny. Each kind wraps onto -// more rows past three across, and tiny icons wrap further if they still don't fit. Names and health use the small -// font, or the tiny one for every window if any doesn't fit. +// Lays out the other fighters' windows in rows of up to four along the bottom right (more rows stacking upwards, as +// even as they go), never left of the screen's middle (nor over the local hotbar and cooldowns). A window is as wide as +// its icon row, but no wider than one holding two weapons and two trinkets at full size: past that, or when the row +// doesn't fit, icons shrink to small, then tiny. Each kind wraps onto more rows past three across, and tiny icons wrap +// further if they still don't fit. Names and health use the small font, or the tiny one for every window if any doesn't +// fit. RivalLayout Ui::rivals_layout(Game& game) { RivalLayout layout; @@ -2212,6 +2215,8 @@ RivalLayout Ui::rivals_layout(Game& game) } if (layout.count == 0) return layout; + int cardRows = (layout.count + RIVALS_PER_ROW - 1) / RIVALS_PER_ROW; + int perRow = (layout.count + cardRows - 1) / cardRows; float step = SLOT_SIZE + SLOT_GAP; float left = @@ -2241,7 +2246,7 @@ RivalLayout Ui::rivals_layout(Game& game) width = std::ceil(std::max(icons, minimum)); layout.size = (IconSize)size; layout.rows = rows; - bool isFit = icons <= widthMax && layout.count * width + (layout.count - 1) * RIVAL_GAP <= room; + bool isFit = icons <= widthMax && perRow * width + (perRow - 1) * RIVAL_GAP <= room; if (isFit) size = ICON_SIZE_COUNT; if (size == ICON_SIZE_COUNT) @@ -2261,10 +2266,14 @@ RivalLayout Ui::rivals_layout(Game& game) float healthHeight = game.sim.match.is_racing() ? 0 : layout.line + RIVAL_PADDING; float icons = layout.rows * (ICON_PIXELS[layout.size] + RIVAL_ICON_GAP) - RIVAL_ICON_GAP; float height = inset + layout.line + RIVAL_PADDING + healthHeight + icons + inset; - float x = CANVAS_WIDTH - HOTBAR_MARGIN - layout.count * width - (layout.count - 1) * RIVAL_GAP; for (int k = 0; k < layout.count; k++) - layout.cards[k] = {std::round(x + k * (width + RIVAL_GAP)), CANVAS_HEIGHT - HOTBAR_MARGIN - height, width, - height}; + { + int row = k / perRow; + int inRow = std::min(perRow, layout.count - row * perRow); + float x = CANVAS_WIDTH - HOTBAR_MARGIN - inRow * width - (inRow - 1) * RIVAL_GAP; + float y = CANVAS_HEIGHT - HOTBAR_MARGIN - height - row * (height + RIVAL_GAP); + layout.cards[k] = {std::round(x + (k % perRow) * (width + RIVAL_GAP)), y, width, height}; + } return layout; } diff --git a/src/weapon.cpp b/src/weapon.cpp index f6ec124..3cf3953 100644 --- a/src/weapon.cpp +++ b/src/weapon.cpp @@ -336,8 +336,7 @@ std::vector lash_get(Attack& attack) static bool is_hostile(Sim& sim, int owner, int target) { Match& match = sim.match; - return match.state != MATCH_PRACTICE || - (sim.fighters[owner].isPvp && sim.fighters[target].isPvp); + return match.state != MATCH_PRACTICE || (sim.fighters[owner].isPvp && sim.fighters[target].isPvp); } static bool is_target(Sim& sim, int target, int owner) @@ -760,7 +759,8 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot) if (weapon.flash > 0) sim.cues->flash_spawn(muzzle, vec2_angle(aim), weapon.flash, fighter.index); if (weapon.isEjecting) - sim.cues->burst_spawn(CASING_BURST, attack.origin, vec2_angle(aim) - side * (PI / 2 + CASING_BACK), fighter.index); + sim.cues->burst_spawn(CASING_BURST, attack.origin, vec2_angle(aim) - side * (PI / 2 + CASING_BACK), + fighter.index); if (isCharged) sim.cues->shake_add(def.shake * FIRE_SHAKE); @@ -1051,7 +1051,8 @@ void Weapons::attack_move(Sim& sim, Attack& attack) break; } attack.position = next; - sim.cues->burst_spawn(SMOKE_BURST, attack.position - vec2_polar(attack.angle, ROCKET_TAIL), attack.angle + PI, attack.owner); + sim.cues->burst_spawn(SMOKE_BURST, attack.position - vec2_polar(attack.angle, ROCKET_TAIL), + attack.angle + PI, attack.owner); bool isSpent = attack.time > ROCKET_LIFETIME || vec2_length(attack.position - attack.origin) > range; if (sim.world.is_blocking_at(attack.position) || isSpent) @@ -1370,15 +1371,15 @@ void Weapons::contact_update(Sim& sim, Fighter& fighter) (std::max(box.y, other.y) + std::min(box.y + box.h, other.y + other.h)) / 2}; damage_deal( sim, { - .point = point, - .source = fighter.center_get(), - .hitstop = damage >= HITSTOP_MIN_DAMAGE ? std::min(HITSTOP_MAX, damage * HITSTOP_PER_DAMAGE) : 0, - .knock = CONTACT_KNOCK, - .amount = damage, - .owner = fighter.index, - .target = target, - .sound = hit_sound_get(HIT_BLUNT, damage >= HEAVY_DAMAGE), - }); + .point = point, + .source = fighter.center_get(), + .hitstop = damage >= HITSTOP_MIN_DAMAGE ? std::min(HITSTOP_MAX, damage * HITSTOP_PER_DAMAGE) : 0, + .knock = CONTACT_KNOCK, + .amount = damage, + .owner = fighter.index, + .target = target, + .sound = hit_sound_get(HIT_BLUNT, damage >= HEAVY_DAMAGE), + }); fighter.contactCooldown = CONTACT_INTERVAL; return; } @@ -1507,15 +1508,15 @@ void Weapons::teleport_move(Sim& sim, Attack& attack) sim.cues->shake_add(attack.def()->shake); } blast_spawn(sim, { - .position = spot, - .area = attack.stats.area * attack.scale, - .damage = attack.stats.damage, - .owner = attack.owner, - .sound = SOUND_TELEPORT, - .weapon = attack.weapon, - .isGentle = true, - .isSecondary = attack.isSecondary, - }); + .position = spot, + .area = attack.stats.area * attack.scale, + .damage = attack.stats.damage, + .owner = attack.owner, + .sound = SOUND_TELEPORT, + .weapon = attack.weapon, + .isGentle = true, + .isSecondary = attack.isSecondary, + }); } // Tesla Coil: a web of lightning between every weapon the fighter has out in the world (anything but explosions). @@ -1579,12 +1580,12 @@ void Weapons::tesla_update(Sim& sim) continue; shock = TESLA_INTERVAL; damage_deal(sim, {.point = point, - .source = fighter.center_get(), - .knock = KNOCK_SLIGHT, - .amount = (int)damage, - .owner = i, - .target = target, - .sound = SOUND_ZAP}); + .source = fighter.center_get(), + .knock = KNOCK_SLIGHT, + .amount = (int)damage, + .owner = i, + .target = target, + .sound = SOUND_ZAP}); } } } @@ -1695,17 +1696,17 @@ void Weapons::piece_launch(Sim& sim, Attack& piece) Vec2 lash = piece.tip - piece.origin; if (!piece.isCopy && vec2_length(lash) < piece.stats.range - RAY_STEP) sim.fighters[piece.owner].impulse_add(vec2_normalize(lash) * WHIP_PULL * - sim.fighters[piece.owner].trinket_get(TRINKET_ABSORBER, 1)); + sim.fighters[piece.owner].trinket_get(TRINKET_ABSORBER, 1)); if (piece.isCharged) blast_spawn(sim, { - .position = piece.tip, - .area = piece.stats.area * piece.scale / piece.stats.scale, - .damage = piece.stats.damage, - .owner = piece.owner, - .sound = SOUND_NONE, // the charged crack already carries it - .weapon = piece.weapon, - .isSecondary = piece.isSecondary, - }); + .position = piece.tip, + .area = piece.stats.area * piece.scale / piece.stats.scale, + .damage = piece.stats.damage, + .owner = piece.owner, + .sound = SOUND_NONE, // the charged crack already carries it + .weapon = piece.weapon, + .isSecondary = piece.isSecondary, + }); } if (piece.def()->kind == ATTACK_BEAM) @@ -1752,16 +1753,16 @@ void Weapons::attack_explode(Sim& sim, Attack& attack) attack.isDead = true; attack.hitsLeft = 0; blast_spawn(sim, { - .position = attack.position, - .area = blast_area_get(attack), - .bonus = strength_get(attack, attack.position), - .damage = attack.stats.damage * multiplier_get(attack, attack.position), - .owner = attack.owner, - .sound = SOUND_EXPLOSION, - .weapon = attack.weapon, - .isPushing = true, - .isSecondary = attack.isSecondary, - }); + .position = attack.position, + .area = blast_area_get(attack), + .bonus = strength_get(attack, attack.position), + .damage = attack.stats.damage * multiplier_get(attack, attack.position), + .owner = attack.owner, + .sound = SOUND_EXPLOSION, + .weapon = attack.weapon, + .isPushing = true, + .isSecondary = attack.isSecondary, + }); } // Queues an explosion that hits everything inside it next tick, weaker toward its edge. Pushing blasts fling every diff --git a/src/world.cpp b/src/world.cpp index b7c6011..8ee0823 100644 --- a/src/world.cpp +++ b/src/world.cpp @@ -7,11 +7,11 @@ constexpr const char* ARENA[] = { "####################################################", "###..............................................###", "###..............................................###", "######........................................######", - "##................................................##", "##................................................##", + "##................................................##", "##.......................7........................##", "##...................==========...................##", "##................................................##", "##................................................##", "####.........3........................4.........####", "####.......======..................======.......####", "##................................................##", - "##................................................##", "##................................................##", + "##................................................##", "##..5.....................8....................6..##", "##======.............==========.............======##", "##................................................##", "##................................................##", "##...........1........................2...........##", "##.........======..................======.........##", "#######......................................#######", @@ -24,7 +24,7 @@ constexpr const char* CHUNK_START[COURSE_HEIGHT] = { "..................", "..................", "..................", "..................", "..................", "..................", "..................", "..................", "..................", "..................", "..................", "..................", "..................", "..................", "..................", - "...1..2..3..4.....", "##################", "##################", "##################", "##################", + ".1.2.3.4.5.6.7.8..", "##################", "##################", "##################", "##################", "##################", "##################", }; @@ -212,7 +212,8 @@ static bool is_solid(Tile tile) } // The arena with the practice dummy standing on the first surface below the middle of the map, and the player -// spawning right underneath it, on the next surface down. +// spawning right underneath it, on the next surface down. Its marked spawns are joined by more spread over its ground, +// for a lobby's sandbox full of players. void World::sandbox_load() { arena_load(); @@ -233,6 +234,37 @@ void World::sandbox_load() float groundY = groundRow * TILE_SIZE; Ramp* ramp = ramp_find({centerX, groundY + TILE_SIZE / 2.0f}); spawns[0] = {centerX, ramp ? ramp->y_at(centerX) : groundY}; + spawns_spread(SPAWN_MARKS); +} + +// Fills the spawns from one on with spots on flat open ground (solid or a platform underfoot, room for a fighter +// above), each the farthest from every spawn before it. +void World::spawns_spread(int from) +{ + std::vector spots; + for (int y = 1; y + 1 < height; y++) + for (int x = 0; x < width; x++) + { + Tile below = tile_get(x, y + 1); + bool isFloor = below == TILE_SOLID || below == TILE_PLATFORM; + if (isFloor && tile_get(x, y) == TILE_EMPTY && tile_get(x, y - 1) == TILE_EMPTY) + spots.push_back({(x + 0.5f) * TILE_SIZE, (y + 1.0f) * TILE_SIZE}); + } + for (int i = from; i < SPAWN_MAX && !spots.empty(); i++) + { + float farthest = -1; + for (Vec2 spot : spots) + { + float nearest = INFINITY; + for (int j = 0; j < i; j++) + nearest = std::min(nearest, vec2_dot(spot - spawns[j], spot - spawns[j])); + if (nearest > farthest) + { + farthest = nearest; + spawns[i] = spot; + } + } + } } void World::arena_load() @@ -264,7 +296,7 @@ void World::course_build(Rng& rng) map_load(rows); } -// Fills the grid from rows of symbols: # solid, = platform, B breakable block, ~ pit, F finish line, 1 to 4 spawns. +// Fills the grid from rows of symbols: # solid, = platform, B breakable block, ~ pit, F finish line, 1 to 8 spawns. // Every block gets a dummy to take its hits. void World::map_load(std::vector& rows) { @@ -287,7 +319,7 @@ void World::map_load(std::vector& rows) tile = TILE_PIT; else if (symbol == 'F') tile = TILE_FINISH; - else if (symbol >= '1' && symbol < '1' + SPAWN_COUNT) + else if (symbol >= '1' && symbol < '1' + SPAWN_MARKS) spawns[symbol - '1'] = {(x + 0.5f) * TILE_SIZE, (y + 1.0f) * TILE_SIZE}; if (symbol != 'B') continue; diff --git a/src/world.h b/src/world.h index d2fcabf..359c769 100644 --- a/src/world.h +++ b/src/world.h @@ -78,10 +78,12 @@ struct Dummy constexpr int BLOCK_STAGES = 3; constexpr int COURSE_HEIGHT = 22; -constexpr int SPAWN_COUNT = 4; +// Maps mark up to SPAWN_MARKS spawns, 1 to 8. The sandbox has SPAWN_MAX, the rest found on its open ground. +constexpr int SPAWN_MARKS = 8; +constexpr int SPAWN_MAX = 16; // Tile grid of any size, its collision, and the things standing in it. Spawns are where fighters stand at the start -// of a round, marked 1 to 4 in the map. Obstacles are extra solid boxes for the body moving right now, like a +// of a round, marked 1 to 8 in the map. Obstacles are extra solid boxes for the body moving right now, like a // fighter's own solid attacks. The map is the deathmatch arena or a race course stitched from chunks. Ramp tiles are // solid below their ramp's line, and bodies meet ramps by their own rules rather than the grid's. class World @@ -91,13 +93,14 @@ public: std::vector obstacles; std::vector ramps; std::vector tiles; - Vec2 spawns[SPAWN_COUNT]; + Vec2 spawns[SPAWN_MAX]; int height = 0; int width = 0; void update(Sim& sim); void arena_load(); void sandbox_load(); + void spawns_spread(int from); void course_build(Rng& rng); void block_break(Sim& sim, Dummy& block, int breaker); void body_move(Body& body);