Raise online play to 16-player lobbies and 8-player games
Lobbies hold 16 players and the sandbox 16 fighters, games up to 8. The arena gets spawns 5-8 (outer ledges and middle platforms) and the race start eight places. The sandbox adds eight more spawns, each the open spot farthest from the rest. Rival cards wrap into rows of four, the lobby's player list goes to two tiny columns when crowded, and the server keeps frame packets to one datagram. Formatted with clang-format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
56af1c357c
commit
6003175173
18 files changed
+188
-81
No files matched your search
+18
-9
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+8
-1
@@ -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];
|
||||
|
||||
+11
-2
@@ -1,6 +1,7 @@
|
||||
#include "match.h"
|
||||
#include "sim.h"
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
#include <cstring>
|
||||
|
||||
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;
|
||||
|
||||
+5
-4
@@ -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);
|
||||
|
||||
+28
-8
@@ -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;
|
||||
@@ -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)
|
||||
|
||||
+2
-2
@@ -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<std::function<void()>> work;
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
#include <type_traits>
|
||||
|
||||
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";
|
||||
|
||||
+6
-4
@@ -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,7 +570,8 @@ 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},
|
||||
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);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+4
-2
@@ -363,12 +363,14 @@ void Settings::apply(Game& game)
|
||||
void Settings::fullscreen_apply(Game& game)
|
||||
{
|
||||
#ifdef __EMSCRIPTEN__
|
||||
EM_ASM({
|
||||
EM_ASM(
|
||||
{
|
||||
if ($0 && !document.fullscreenElement)
|
||||
document.documentElement.requestFullscreen().catch(function(error){});
|
||||
if (!$0 && document.fullscreenElement)
|
||||
document.exitFullscreen();
|
||||
}, isFullscreen);
|
||||
},
|
||||
isFullscreen);
|
||||
#else
|
||||
SDL_SetWindowFullscreen(game.window, isFullscreen);
|
||||
#endif
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
+1
-5
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ template <class Stream, class SimRef> 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);
|
||||
}
|
||||
|
||||
+19
-10
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+5
-4
@@ -336,8 +336,7 @@ std::vector<Vec2> 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)
|
||||
|
||||
+38
-6
@@ -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<Vec2> 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<std::string>& rows)
|
||||
{
|
||||
@@ -287,7 +319,7 @@ void World::map_load(std::vector<std::string>& 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;
|
||||
|
||||
+6
-3
@@ -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<Rect> obstacles;
|
||||
std::vector<Ramp> ramps;
|
||||
std::vector<Tile> 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);
|
||||
|
||||
Reference in new issue
Block a user