Weapons built from traits, fashion, Hourglass, About tab, and Ithaca
- Every attack is now assembled from traits on its definition instead of per-weapon code: an anchor and path (thrusts, out-and-back, arcs), shared flight physics (gravity, drag, acceleration, homing, and one wall response: end, burst, stick, crawl, bounce, stop, pass, return, latch), and flags like bouncy, drifting, tethered, orbiting, instant and arming. Melee are projectiles too. - Shots have a lifetime and a fade (when damage bottoms out). Range is speed times lifetime. Ricochets and the Vacuum wind lifetime back. - The Vacuum pulls anyone's shots, and its airblast flips them out along the aim, claiming enemies' (fading into your colour). - New trinket: Hourglass, longer projectile lifetime and range. Ropes and chains lengthen with it and the Accelerator. - Fashion: 23 items worn on the head, face or torso, chosen in Profile. - Flamethrower flames hit continuously, the whip coils and sweeps, bouncing shots roll over ramps, and Ricochet relights grenade fuses. - Profile tab, colour tooltips, option hints on every setting, an About tab with credits and links, and the Ithaca font. - Loadouts lock once readied, tomatoes clear with the match, and the round-over pause is 4 seconds. - CC0 license. Tools and CLAUDE.md are no longer tracked, and the web shell moves to web/.
This commit is contained in:
1 parent
e55596b276
commit
e9d8cac926
101 files changed
+5787
-7148
No files matched your search
+97
-21
@@ -85,6 +85,21 @@ struct Member
|
||||
int room = 0;
|
||||
};
|
||||
|
||||
// How a member comes into a room: their fighter joining it from their profile, or already seated (a game set up with
|
||||
// them in it) or spectating, with nothing to add.
|
||||
enum RoomEntry
|
||||
{
|
||||
ENTRY_SEATED,
|
||||
ENTRY_JOIN
|
||||
};
|
||||
|
||||
// Why a member leaves a lobby: of their own accord (or dropping out), or kicked by the host.
|
||||
enum LeaveReason
|
||||
{
|
||||
LEAVE_QUIT,
|
||||
LEAVE_KICKED
|
||||
};
|
||||
|
||||
// A lobby: its members in the order they joined (the first hosting), and its rooms, the sandbox first.
|
||||
struct Lobby
|
||||
{
|
||||
@@ -119,7 +134,7 @@ static int nextRoom = 1;
|
||||
// Sends a member a lobby message (text) or a game one (binary): binary goes over their data channels once both are
|
||||
// open (the lossy one for frames), and the socket until then. A connection can drop between checking it and sending,
|
||||
// which throws. It's let go: its closing comes in later.
|
||||
static void send(int id, const std::string& message, bool isBinary = false, bool isLossy = false)
|
||||
static void send(int id, const std::string& message, Delivery delivery = DELIVERY_TEXT)
|
||||
{
|
||||
auto found = members.find(id);
|
||||
if (found == members.end())
|
||||
@@ -127,10 +142,11 @@ static void send(int id, const std::string& message, bool isBinary = false, bool
|
||||
Member& member = found->second;
|
||||
auto data = reinterpret_cast<const std::byte*>(message.data());
|
||||
bool isLinked = member.channel && member.channel->isOpen() && member.inputs && member.inputs->isOpen();
|
||||
bool isBinary = delivery != DELIVERY_TEXT;
|
||||
try
|
||||
{
|
||||
if (isBinary && isLinked)
|
||||
(isLossy ? member.inputs : member.channel)->send(data, message.size());
|
||||
(delivery == DELIVERY_LOSSY ? member.inputs : member.channel)->send(data, message.size());
|
||||
else if (isBinary && member.socket->isOpen())
|
||||
member.socket->send(rtc::binary(data, data + message.size()));
|
||||
else if (member.socket->isOpen())
|
||||
@@ -210,10 +226,10 @@ static void error_send(int id, const char* message)
|
||||
send(id, "{\"type\":\"error\",\"message\":" + json_quote(message) + "}");
|
||||
}
|
||||
|
||||
static void lobby_send(const std::string& code, const std::string& message, bool isBinary = false)
|
||||
static void lobby_send(const std::string& code, const std::string& message, Delivery delivery = DELIVERY_TEXT)
|
||||
{
|
||||
for (int id : lobbies[code].members)
|
||||
send(id, message, isBinary);
|
||||
send(id, message, delivery);
|
||||
}
|
||||
|
||||
// A name as the lobby shows it: printable characters only, cut to length, or "Player" when nothing's left.
|
||||
@@ -257,12 +273,12 @@ static void event_add(int serial, RoomEvent event)
|
||||
packet.put(PACKET_EVENT).put(serial).put((int)room.events.size() - 1).put(event);
|
||||
for (auto& [id, member] : members)
|
||||
if (member.room == serial)
|
||||
send(id, packet.bytes, true);
|
||||
send(id, packet.bytes, DELIVERY_RELIABLE);
|
||||
}
|
||||
|
||||
// Puts a member in a room as its next tick starts: their fighter joins (from their profile, or their seat in a game
|
||||
// already set up for them), and they get the room as it is then, with every event it's had.
|
||||
static void room_enter(int id, int serial, bool isJoining)
|
||||
static void room_enter(int id, int serial, RoomEntry entry)
|
||||
{
|
||||
Member& member = members[id];
|
||||
Room& room = rooms[serial];
|
||||
@@ -272,10 +288,10 @@ static void room_enter(int id, int serial, bool isJoining)
|
||||
PacketWriter packet;
|
||||
packet.put(PACKET_STATE).put(serial).put(room.tick);
|
||||
packet.bytes += state_save(room.sim);
|
||||
send(id, packet.bytes, true);
|
||||
send(id, packet.bytes, DELIVERY_RELIABLE);
|
||||
for (size_t i = 0; i < room.events.size(); i++)
|
||||
send(id, PacketWriter().put(PACKET_EVENT).put(serial).put((int)i).put(room.events[i]).bytes, true);
|
||||
if (isJoining)
|
||||
send(id, PacketWriter().put(PACKET_EVENT).put(serial).put((int)i).put(room.events[i]).bytes, DELIVERY_RELIABLE);
|
||||
if (entry == ENTRY_JOIN)
|
||||
event_add(serial, {.type = ROOM_JOIN, .member = id, .profile = member.profile});
|
||||
}
|
||||
|
||||
@@ -322,7 +338,7 @@ static void lobby_enter(int id, const std::string& code)
|
||||
|
||||
// Takes a member out of its lobby, telling the rest (and who hosts now, and why if they were kicked). An empty lobby is
|
||||
// gone, with its rooms.
|
||||
static void lobby_exit(int id, const std::string& kick = "", bool isKicked = false)
|
||||
static void lobby_exit(int id, LeaveReason leave = LEAVE_QUIT, const std::string& kick = "")
|
||||
{
|
||||
std::string code = members[id].code;
|
||||
if (code.empty())
|
||||
@@ -339,7 +355,7 @@ static void lobby_exit(int id, const std::string& kick = "", bool isKicked = fal
|
||||
std::printf("Lobby %s closed (%zu open)\n", code.c_str(), lobbies.size());
|
||||
return;
|
||||
}
|
||||
std::string reason = isKicked ? ",\"kicked\":true,\"reason\":" + json_quote(kick) : "";
|
||||
std::string reason = leave == LEAVE_KICKED ? ",\"kicked\":true,\"reason\":" + json_quote(kick) : "";
|
||||
lobby_send(code, "{\"type\":\"member_leave\",\"id\":" + std::to_string(id) +
|
||||
",\"host\":" + std::to_string(lobby.members.front()) + reason + "}");
|
||||
}
|
||||
@@ -409,7 +425,7 @@ static void text_handle(int id, const std::string& text)
|
||||
std::string reason = text_clean(message["reason"].get(raw) ? "" : raw, KICK_REASON_MAX);
|
||||
send((int)target, "{\"type\":\"kicked\",\"reason\":" + json_quote(reason) + "}");
|
||||
std::printf("Lobby %s: %d kicked %d\n", code.c_str(), id, (int)target);
|
||||
return lobby_exit((int)target, reason, true);
|
||||
return lobby_exit((int)target, LEAVE_KICKED, reason);
|
||||
}
|
||||
error_send(id, "Unknown message");
|
||||
}
|
||||
@@ -428,7 +444,7 @@ static void text_relay(int id, PacketType type, PacketReader& reader, size_t max
|
||||
PacketWriter packet;
|
||||
packet.put(type).put(id).put((Uint16)text.size());
|
||||
packet.bytes += text;
|
||||
lobby_send(members[id].code, packet.bytes, true);
|
||||
lobby_send(members[id].code, packet.bytes, DELIVERY_RELIABLE);
|
||||
}
|
||||
|
||||
// The game messages, in binary (see protocol.h), from a member in a lobby.
|
||||
@@ -450,7 +466,7 @@ static void packet_handle(int id, const std::string& bytes)
|
||||
member.profile.color = std::clamp(member.profile.color, -1, COLOR_COUNT - 1);
|
||||
std::memcpy(member.profile.name, member.name.c_str(), member.name.size() + 1);
|
||||
if (member.room == 0)
|
||||
room_enter(id, lobbies[member.code].rooms.front(), true);
|
||||
room_enter(id, lobbies[member.code].rooms.front(), ENTRY_JOIN);
|
||||
return;
|
||||
}
|
||||
if (type == PACKET_INPUTS)
|
||||
@@ -467,6 +483,9 @@ static void packet_handle(int id, const std::string& bytes)
|
||||
Room& room = rooms[serial];
|
||||
member.ack = std::max(member.ack, std::min(ack, room.tick - 1));
|
||||
member.lead = first + count - 1 - room.tick;
|
||||
// A spectator's controls play nothing, so aren't kept.
|
||||
if (room.sim.match.fighter_find(room.sim, id) < 0)
|
||||
return;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Controls controls;
|
||||
@@ -493,13 +512,38 @@ static void packet_handle(int id, const std::string& bytes)
|
||||
{
|
||||
Uint8 isTyping = 0;
|
||||
if (reader.get(isTyping))
|
||||
lobby_send(member.code, PacketWriter().put(PACKET_TYPING).put(id).put(isTyping).bytes, true);
|
||||
lobby_send(member.code, PacketWriter().put(PACKET_TYPING).put(id).put(isTyping).bytes, DELIVERY_RELIABLE);
|
||||
}
|
||||
if (type == PACKET_WATCH)
|
||||
{
|
||||
// From the sandbox, spectating one of the lobby's games. Spectating, back to the sandbox. Someone playing in a
|
||||
// game leaves it by its own means.
|
||||
int serial = 0;
|
||||
std::vector<int>& lobbyRooms = lobbies[member.code].rooms;
|
||||
int sandbox = lobbyRooms.front();
|
||||
bool isInSandbox = member.room == sandbox;
|
||||
auto room = rooms.find(member.room);
|
||||
bool isPlaying = room != rooms.end() && room->second.sim.match.fighter_find(room->second.sim, id) >= 0;
|
||||
if (!reader.get(serial))
|
||||
return;
|
||||
bool isGame = serial != sandbox && std::find(lobbyRooms.begin(), lobbyRooms.end(), serial) != lobbyRooms.end();
|
||||
if (isGame && isInSandbox)
|
||||
{
|
||||
room_exit(id);
|
||||
room_enter(id, serial, ENTRY_SEATED);
|
||||
}
|
||||
else if (serial == 0 && !isInSandbox && !isPlaying && member.room != 0)
|
||||
{
|
||||
room_exit(id);
|
||||
room_enter(id, sandbox, ENTRY_JOIN);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type == PACKET_PING)
|
||||
{
|
||||
Uint16 ping = 0;
|
||||
if (reader.get(ping))
|
||||
lobby_send(member.code, PacketWriter().put(PACKET_PING).put(id).put(ping).bytes, true);
|
||||
lobby_send(member.code, PacketWriter().put(PACKET_PING).put(id).put(ping).bytes, DELIVERY_RELIABLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,7 +609,7 @@ static void game_open(int sandboxSerial, int queuer)
|
||||
for (int k = 0; k < count; k++)
|
||||
{
|
||||
room_exit(players[k]);
|
||||
room_enter(players[k], serial, false);
|
||||
room_enter(players[k], serial, ENTRY_SEATED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,7 +632,7 @@ static void game_follow(int serial)
|
||||
if (match.isGone[k])
|
||||
{
|
||||
room_exit(owner);
|
||||
room_enter(owner, sandbox, true);
|
||||
room_enter(owner, sandbox, ENTRY_JOIN);
|
||||
continue;
|
||||
}
|
||||
seats[humans++] = k;
|
||||
@@ -606,13 +650,43 @@ static void game_follow(int serial)
|
||||
{
|
||||
int owner = match.owners[seats[k]];
|
||||
room_exit(owner);
|
||||
room_enter(owner, next, false);
|
||||
room_enter(owner, next, ENTRY_SEATED);
|
||||
}
|
||||
}
|
||||
// Anyone still here was spectating, and goes back to the sandbox.
|
||||
for (auto& [id, member] : members)
|
||||
{
|
||||
if (member.room != serial)
|
||||
continue;
|
||||
room_exit(id);
|
||||
room_enter(id, sandbox, ENTRY_JOIN);
|
||||
}
|
||||
std::erase(lobbies[game.code].rooms, serial);
|
||||
rooms.erase(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.
|
||||
static void rooms_send(const std::string& code)
|
||||
{
|
||||
std::vector<int>& lobbyRooms = lobbies[code].rooms;
|
||||
PacketWriter packet;
|
||||
packet.put(PACKET_ROOMS).put((Uint8)lobbyRooms.size());
|
||||
for (int serial : lobbyRooms)
|
||||
{
|
||||
Match& match = rooms[serial].sim.match;
|
||||
int fighters = rooms[serial].sim.fighterCount;
|
||||
Uint8 count = 0;
|
||||
for (int k = 0; k < fighters; k++)
|
||||
count += match.owners[k] >= 0 && !match.isGone[k];
|
||||
packet.put(serial).put((Uint8)match.rules.mode).put(count);
|
||||
for (int k = 0; k < fighters; k++)
|
||||
if (match.owners[k] >= 0 && !match.isGone[k])
|
||||
packet.put(match.owners[k]).put((Uint8)match.colors[k]);
|
||||
}
|
||||
lobby_send(code, packet.bytes, DELIVERY_RELIABLE);
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -642,14 +716,14 @@ static void frames_send(int serial)
|
||||
for (int i = 0; i < frame.count; i++)
|
||||
packet.put(frame.controls[i]);
|
||||
}
|
||||
send(id, packet.bytes, true, true);
|
||||
send(id, packet.bytes, DELIVERY_LOSSY);
|
||||
}
|
||||
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,
|
||||
true);
|
||||
DELIVERY_RELIABLE);
|
||||
}
|
||||
|
||||
// One tick of every room, in every lobby: the sandbox, then its games, then the frames out.
|
||||
@@ -669,6 +743,8 @@ static void rooms_step()
|
||||
int queuer = room.sim.match.queue_startable_get(room.sim);
|
||||
if (queuer >= 0)
|
||||
game_open(serial, queuer);
|
||||
if (room.tick % CHECKSUM_INTERVAL == 0)
|
||||
rooms_send(room.code);
|
||||
}
|
||||
else
|
||||
game_follow(serial);
|
||||
|
||||
Reference in new issue
Block a user