Tomatoes, match tabs, shared pings, and a round of fixes

- On the results, everyone but the winner splats tomatoes where they
  click (bots aim at the winner), with a new splat sound.
- During a match the menu has the item tabs (choosing when the
  countdown allows, and otherwise levelling the sandbox's levels for
  when you're back), Play (the game's rules) and Settings.
- Pings are shared through the server (protocol 4), so other players'
  connection bars show over their heads and on their windows.
- PvP comes into lobbies with your profile and is saved.
- The choice menu opens by itself however the countdown began.
- Knocked-out fighters keep their buttons current, so Ready on the
  results toggles once per press.
- The weapon wheel wraps both ways. Readying with empty slots plays
  the equip sound for your own fighter.
- Shock Absorber: up to 50% less weapon movement and 33% less
  knockback. Only the catalog previews the next level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shweetandClaude Opus 5.5 committed 2026-11-08 18:11:06 -05:00
1 parent cdb0845da2
commit 9a41474069
29 files changed
+293 -77

No files matched your search

+3 -2
View File
@@ -14,7 +14,7 @@ Cut from the Sound Ideas Series 4000 and Guns libraries and the Universal Studio
| break | US02-96-1 CRASH, MISCELLANEOUS - SMALL OBJECTS FALLING ON CEMENT | 0.59s | 1x | 11025 | 2808 | | break | US02-96-1 CRASH, MISCELLANEOUS - SMALL OBJECTS FALLING ON CEMENT | 0.59s | 1x | 11025 | 2808 |
| cast | synth ember | | 1x | 11025 | 2556 | | cast | synth ember | | 1x | 11025 | 2556 |
| charge | synth warp | | 1x | 11025 | 5076 | | charge | synth warp | | 1x | 11025 | 5076 |
| chat | synth beeps | | 1x | 11025 | 567 | | chat | synth beeps | | 1x | 11025 | 576 |
| crawl | kept from the synth set | | | | | | crawl | kept from the synth set | | | | |
| dash | Swish 4003_47_4 | 0.17s | 1x | 11025 | 1143 | | dash | Swish 4003_47_4 | 0.17s | 1x | 11025 | 1143 |
| death | US03-34-1 KICK, HUMAN - SINGLE KICK INTO STOMACH | 0.17s | 1x | 11025 | 2916 | | death | US03-34-1 KICK, HUMAN - SINGLE KICK INTO STOMACH | 0.17s | 1x | 11025 | 2916 |
@@ -86,6 +86,7 @@ Cut from the Sound Ideas Series 4000 and Guns libraries and the Universal Studio
| slam | US03-39-2 METAL, HIT - LOUD METAL CLUNK | 0.16s | 0.7x | 11025 | 3852 | | slam | US03-39-2 METAL, HIT - LOUD METAL CLUNK | 0.16s | 0.7x | 11025 | 3852 |
| slash | Swish 4003_47_2 | 0.79s | 1x | 11025 | 1170 | | slash | Swish 4003_47_2 | 0.79s | 1x | 11025 | 1170 |
| slash_heavy | Swish 4003_47_2 | 0.79s | 0.75x | 11025 | 1908 | | slash_heavy | Swish 4003_47_2 | 0.79s | 0.75x | 11025 | 1908 |
| splat | CartoonSplat 4001_32_1 | first | 1x | 11025 | 1116 |
| step | CartoonFootsteps 4003_46_2 | 0.80s | 1x | 16000 | 891 | | step | CartoonFootsteps 4003_46_2 | 0.80s | 1x | 16000 | 891 |
| swap | kept from the demo1 set | | | | | | swap | kept from the demo1 set | | | | |
| swing | Swish 4003_47_1 | 0.40s | 1x | 11025 | 1098 | | swing | Swish 4003_47_1 | 0.40s | 1x | 11025 | 1098 |
@@ -102,4 +103,4 @@ Cut from the Sound Ideas Series 4000 and Guns libraries and the Universal Studio
| wisp | US02-45-5 ARCHERY - SHOOT FLAMING ARROW | 0.18s | 1x | 11025 | 1584 | | wisp | US02-45-5 ARCHERY - SHOOT FLAMING ARROW | 0.18s | 1x | 11025 | 1584 |
| zap | US03-12-1 ELECTRICITY - SMALL TRANSFORMER ARCS | 0.48s | 1x | 11025 | 1296 | | zap | US03-12-1 ELECTRICITY - SMALL TRANSFORMER ARCS | 0.48s | 1x | 11025 | 1296 |
The built sounds come to 144 KB as BRR. The built sounds come to 145 KB as BRR.
Binary file not shown.
+6
View File
@@ -495,6 +495,12 @@ static void packet_handle(int id, const std::string& bytes)
if (reader.get(isTyping)) 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, true);
} }
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);
}
} }
// The controls a room's fighter plays this tick: its member's for the tick if they came, or their last again (without // The controls a room's fighter plays this tick: its member's for the tick if they came, or their last again (without
+1
View File
@@ -80,6 +80,7 @@ constexpr const char* SOUND_NAMES[SOUND_COUNT] = {
"slam", "slam",
"slash", "slash",
"slash_heavy", "slash_heavy",
"splat",
"step", "step",
"swap", "swap",
"swing", "swing",
+1
View File
@@ -80,6 +80,7 @@ enum SoundId
SOUND_SLAM, SOUND_SLAM,
SOUND_SLASH, SOUND_SLASH,
SOUND_SLASH_HEAVY, SOUND_SLASH_HEAVY,
SOUND_SPLAT,
SOUND_STEP, SOUND_STEP,
SOUND_SWAP, SOUND_SWAP,
SOUND_SWING, SOUND_SWING,
+15
View File
@@ -43,6 +43,9 @@ constexpr float TAKEOFF_INSET = TILE_SIZE;
constexpr float THREAT_SPEED = 50; constexpr float THREAT_SPEED = 50;
constexpr float WHIP_SWEET_SPOT = 8; constexpr float WHIP_SWEET_SPOT = 8;
constexpr float WISP_GATHER = 1; constexpr float WISP_GATHER = 1;
// A bot lobbing tomatoes on the results: the chance of one each tick, and how far off the winner it can land.
constexpr float TOMATO_CHANCE = 0.04f;
constexpr float TOMATO_MISS = 14;
static void control_set(Controls& controls, Control control, bool isDown) static void control_set(Controls& controls, Control control, bool isDown)
{ {
@@ -409,6 +412,18 @@ bool Bot::is_path_found(BotState& state, int from, int to, bool isGrappler)
// Writes every living bot's controls for this tick, re-thinking on its interval. // Writes every living bot's controls for this tick, re-thinking on its interval.
void Bot::update(Sim& sim) void Bot::update(Sim& sim)
{ {
// On a match's results, bots other than the winner lob tomatoes at it now and then.
Match& match = sim.match;
for (int i = 0; i < sim.fighterCount && match.state == MATCH_OVER && match.winner >= 0; i++)
{
Fighter& fighter = sim.fighters[i];
if (!fighter.isBot || i == match.winner)
continue;
Vec2 miss = {sim.rng.random_get(-TOMATO_MISS, TOMATO_MISS), sim.rng.random_get(-TOMATO_MISS, TOMATO_MISS)};
bool isThrowing = sim.rng.random_get(0, 1) < TOMATO_CHANCE;
fighter.controls = {.cursor = sim.fighters[match.winner].center_get() + miss,
.buttons = (Uint16)(isThrowing << CONTROL_ATTACK)};
}
for (int i = 0; i < sim.fighterCount; i++) for (int i = 0; i < sim.fighterCount; i++)
{ {
Fighter& fighter = sim.fighters[i]; Fighter& fighter = sim.fighters[i];
+32
View File
@@ -14,6 +14,12 @@ constexpr float FLASH_LIFE = 0.05f;
constexpr float FLASH_WIDTH = 6; constexpr float FLASH_WIDTH = 6;
constexpr float RING_LIFE = 0.3f; constexpr float RING_LIFE = 0.3f;
constexpr int RING_WIDTH = 2; constexpr int RING_WIDTH = 2;
constexpr float SPLAT_CORE = 6;
constexpr float SPLAT_FADE = 0.6f;
constexpr float SPLAT_LIFE = 2;
constexpr float SPLAT_REACH = 10;
constexpr float SPLAT_SIZE_MAX = 3.5f;
constexpr float SPLAT_SIZE_MIN = 1.5f;
constexpr float AFTERIMAGE_LIFE = 0.12f; constexpr float AFTERIMAGE_LIFE = 0.12f;
constexpr float NUMBER_GAP = 8; constexpr float NUMBER_GAP = 8;
constexpr float POPUP_FADE = 0.4f; constexpr float POPUP_FADE = 0.4f;
@@ -127,6 +133,19 @@ void Effects::ring_spawn(Vec2 position, float radius, Color color)
effect_add(rings, Ring{color, position, RING_LIFE, radius}); effect_add(rings, Ring{color, position, RING_LIFE, radius});
} }
// A big blob where it landed, with smaller ones flung round it.
void Effects::splat_spawn(Vec2 position, Color color)
{
Splat splat = {.color = color, .position = position, .life = SPLAT_LIFE};
splat.sizes[0] = SPLAT_CORE;
for (int i = 1; i < SPLAT_BLOBS; i++)
{
splat.blobs[i] = vec2_polar(random_get(0, 2 * PI), random_get(SPLAT_CORE, SPLAT_REACH));
splat.sizes[i] = random_get(SPLAT_SIZE_MIN, SPLAT_SIZE_MAX);
}
effect_add(splats, splat);
}
void Effects::dps_record(int amount) void Effects::dps_record(int amount)
{ {
hits.push_back({ticks * TICK, amount}); hits.push_back({ticks * TICK, amount});
@@ -168,6 +187,10 @@ void Effects::update()
ring.life -= TICK; ring.life -= TICK;
std::erase_if(rings, [](Ring& ring) { return ring.life <= 0; }); std::erase_if(rings, [](Ring& ring) { return ring.life <= 0; });
for (Splat& splat : splats)
splat.life -= TICK;
std::erase_if(splats, [](Splat& splat) { return splat.life <= 0; });
for (Afterimage& afterimage : afterimages) for (Afterimage& afterimage : afterimages)
afterimage.life -= TICK; afterimage.life -= TICK;
std::erase_if(afterimages, [](Afterimage& afterimage) { return afterimage.life <= 0; }); std::erase_if(afterimages, [](Afterimage& afterimage) { return afterimage.life <= 0; });
@@ -209,6 +232,14 @@ void Effects::draw(Game& game)
renderer.line_draw(flash.position - across, flash.position + across, width * FLASH_CROSS, flash.color); renderer.line_draw(flash.position - across, flash.position + across, width * FLASH_CROSS, flash.color);
} }
// Each splat holds, then fades out over its last moments.
for (Splat& splat : splats)
{
Color color = color_fade(splat.color, std::min(1.0f, splat.life / SPLAT_FADE));
for (int i = 0; i < SPLAT_BLOBS; i++)
renderer.circle_draw(splat.position + splat.blobs[i], splat.sizes[i], color);
}
// Each ring eases out to its full radius as it fades. // Each ring eases out to its full radius as it fades.
for (Ring& ring : rings) for (Ring& ring : rings)
{ {
@@ -259,5 +290,6 @@ void Effects::clear()
particles.clear(); particles.clear();
popups.clear(); popups.clear();
rings.clear(); rings.clear();
splats.clear();
shake = 0; shake = 0;
} }
+14
View File
@@ -58,6 +58,18 @@ struct Ring
float radius = 0; float radius = 0;
}; };
constexpr int SPLAT_BLOBS = 6;
// A tomato's splat: blobs of colour round where it landed, fading out.
struct Splat
{
Color color;
Vec2 position;
Vec2 blobs[SPLAT_BLOBS];
float sizes[SPLAT_BLOBS];
float life = 0;
};
// A fading copy of a sprite left behind by something moving fast. // A fading copy of a sprite left behind by something moving fast.
struct Afterimage struct Afterimage
{ {
@@ -73,6 +85,7 @@ public:
void afterimage_spawn(Pose pose); void afterimage_spawn(Pose pose);
void flash_spawn(Vec2 position, float angle, float size, Color color); void flash_spawn(Vec2 position, float angle, float size, Color color);
void ring_spawn(Vec2 position, float radius, Color color); void ring_spawn(Vec2 position, float radius, Color color);
void splat_spawn(Vec2 position, Color color);
void afterimage_draw(Game& game); void afterimage_draw(Game& game);
void number_spawn(Vec2 position, int value, int target, Color color); void number_spawn(Vec2 position, int value, int target, Color color);
void dps_record(int amount); void dps_record(int amount);
@@ -91,6 +104,7 @@ private:
std::vector<Particle> particles; std::vector<Particle> particles;
std::vector<Popup> popups; std::vector<Popup> popups;
std::vector<Ring> rings; std::vector<Ring> rings;
std::vector<Splat> splats;
float shake = 0; float shake = 0;
int ticks = 0; int ticks = 0;
}; };
+4
View File
@@ -154,8 +154,12 @@ void Fighter::damage_take(Sim& sim, Damage damage)
void Fighter::update(Sim& sim) void Fighter::update(Sim& sim)
{ {
// Out of the round, it still keeps track of its buttons, so a held one (like Ready on the results) counts once.
if (!isAlive) if (!isAlive)
{
buttonsPrevious = controls.buttons;
return; return;
}
cornerPrevious = {body.box.x, body.box.y}; cornerPrevious = {body.box.x, body.box.y};
flash = std::max(0.0f, flash - FLASH_DECAY * TICK); flash = std::max(0.0f, flash - FLASH_DECAY * TICK);
shockCooldown -= TICK; shockCooldown -= TICK;
+8 -7
View File
@@ -350,14 +350,14 @@ void Input::edit_send(int device, int edit)
source.edits[source.editCount++] = edit; source.edits[source.editCount++] = edit;
} }
// A device's bound keys, buttons and sticks as one tick of fighter controls, aiming from origin, with the oldest edit // A device's bound keys, buttons and sticks as one tick of a fighter's controls, aiming from its middle, with the
// and any Ready click waiting. Number keys (on the keyboard), next and previous weapon step from the fighter's current // oldest edit and any Ready click waiting. Number keys (on the keyboard) pick a slot, and next and previous weapon step
// slot, and the fighter wraps it into range. With the menu open, only Ready and picks get through. // from the fighter's current one, wrapping round its slots. With the menu open, only Ready and picks get through.
Controls Input::controls_get(int device, Vec2 camera, Vec2 origin, int slot, bool isMenuOpen) Controls Input::controls_get(int device, Vec2 camera, Fighter& fighter, bool isMenuOpen)
{ {
Device& source = devices[device]; Device& source = devices[device];
bool isKeyboard = device == DEVICE_KEYBOARD; bool isKeyboard = device == DEVICE_KEYBOARD;
Controls controls = {.cursor = aim_get(device, camera, origin), Controls controls = {.cursor = aim_get(device, camera, fighter.center_get()),
.edit = source.editCount > 0 ? source.edits[0] : -1, .edit = source.editCount > 0 ? source.edits[0] : -1,
.slot = isMenuOpen || !isKeyboard ? -1 : slotPressed}; .slot = isMenuOpen || !isKeyboard ? -1 : slotPressed};
for (int i = 0; i < CONTROL_COUNT; i++) for (int i = 0; i < CONTROL_COUNT; i++)
@@ -371,10 +371,11 @@ Controls Input::controls_get(int device, Vec2 camera, Vec2 origin, int slot, boo
source.isReady = false; source.isReady = false;
if (isMenuOpen) if (isMenuOpen)
return controls; return controls;
int slots = fighter.weaponSlots;
if (is_control_down(device, CONTROL_NEXT_WEAPON) && !source.previous[CONTROL_NEXT_WEAPON]) if (is_control_down(device, CONTROL_NEXT_WEAPON) && !source.previous[CONTROL_NEXT_WEAPON])
controls.slot = slot + 1; controls.slot = (fighter.slot + 1) % slots;
if (is_control_down(device, CONTROL_PREVIOUS_WEAPON) && !source.previous[CONTROL_PREVIOUS_WEAPON]) if (is_control_down(device, CONTROL_PREVIOUS_WEAPON) && !source.previous[CONTROL_PREVIOUS_WEAPON])
controls.slot = slot - 1; controls.slot = (fighter.slot - 1 + slots) % slots;
return controls; return controls;
} }
+2 -1
View File
@@ -3,6 +3,7 @@
#include "common.h" #include "common.h"
#include <vector> #include <vector>
class Fighter;
struct Game; struct Game;
struct SDL_Gamepad; struct SDL_Gamepad;
struct SDL_Joystick; struct SDL_Joystick;
@@ -159,7 +160,7 @@ public:
void poll(Game& game); void poll(Game& game);
void tick_end(); void tick_end();
void bindings_reset(); void bindings_reset();
Controls controls_get(int device, Vec2 camera, Vec2 origin, int slot, bool isMenuOpen); Controls controls_get(int device, Vec2 camera, Fighter& fighter, bool isMenuOpen);
void edit_send(int device, int edit); void edit_send(int device, int edit);
Vec2 aim_get(int device, Vec2 camera, Vec2 origin); Vec2 aim_get(int device, Vec2 camera, Vec2 origin);
bool is_down(int scancode); bool is_down(int scancode);
+2 -4
View File
@@ -144,8 +144,7 @@ static bool frame_run(bool isDrawn = true)
{ {
Fighter& own = game.local_get(); Fighter& own = game.local_get();
bool isHeld = game.menu.isOpen || game.chat.isOpen || session.is_watching(); bool isHeld = game.menu.isOpen || game.chat.isOpen || session.is_watching();
Controls controls = Controls controls = game.input.controls_get(DEVICE_KEYBOARD, game.camera, own, isHeld);
game.input.controls_get(DEVICE_KEYBOARD, game.camera, own.center_get(), own.slot, isHeld);
session.update(game, controls); session.update(game, controls);
game.input.tick_end(); game.input.tick_end();
if (session.isViewChanged) if (session.isViewChanged)
@@ -161,8 +160,7 @@ static bool frame_run(bool isDrawn = true)
Fighter& fighter = game.sim.fighters[i]; Fighter& fighter = game.sim.fighters[i];
bool isMenuOpen = game.menu.isOpen && i == game.local; bool isMenuOpen = game.menu.isOpen && i == game.local;
if (game.is_local(i)) if (game.is_local(i))
fighter.controls = game.input.controls_get(game.devices[i], game.camera, fighter.center_get(), fighter.controls = game.input.controls_get(game.devices[i], game.camera, fighter, isMenuOpen);
fighter.slot, isMenuOpen);
} }
game.rollback.update(game); game.rollback.update(game);
presentation_update(game); presentation_update(game);
+34 -10
View File
@@ -4,6 +4,7 @@
#include <numeric> #include <numeric>
#include <cstring> #include <cstring>
constexpr float TOMATO_COOLDOWN = 0.25f;
constexpr Burst FINISH_BURST = {.gravity = 200, .life = 1, .size = 3, .speed = 240, .count = 40}; constexpr Burst FINISH_BURST = {.gravity = 200, .life = 1, .size = 3, .speed = 240, .count = 40};
constexpr float RACE_GRACE = 15; constexpr float RACE_GRACE = 15;
constexpr float ROUND_OVER_TIME = 2.5f; constexpr float ROUND_OVER_TIME = 2.5f;
@@ -45,9 +46,11 @@ Rules rules_clean(Rules rules)
return rules; return rules;
} }
// Fills a loadout's empty slots with random picks out of count items, none repeating what's already in it. // Fills a loadout's empty slots with random picks out of count items, none repeating what's already in it. Returns
static void picks_deal(Rng& rng, int* loadout, int slots, int count, bool isDuplicates) // whether it filled any.
static bool picks_deal(Rng& rng, int* loadout, int slots, int count, bool isDuplicates)
{ {
bool isDealt = false;
for (int i = 0; i < slots; i++) for (int i = 0; i < slots; i++)
{ {
if (loadout[i] >= 0) if (loadout[i] >= 0)
@@ -56,7 +59,9 @@ static void picks_deal(Rng& rng, int* loadout, int slots, int count, bool isDupl
while (!isDuplicates && std::find(loadout, loadout + slots, pick) != loadout + slots) while (!isDuplicates && std::find(loadout, loadout + slots, pick) != loadout + slots)
pick = rng.random_get(0, count); pick = rng.random_get(0, count);
loadout[i] = pick; loadout[i] = pick;
isDealt = true;
} }
return isDealt;
} }
// Whether a round hands out this kind of item: the first round of the match always, every round, or every period. // Whether a round hands out this kind of item: the first round of the match always, every round, or every period.
@@ -93,6 +98,8 @@ void Match::update(Sim& sim)
{ {
case MATCH_PRACTICE: case MATCH_PRACTICE:
case MATCH_OVER: case MATCH_OVER:
if (state == MATCH_OVER)
tomatoes_update(sim);
// Online, after a queued game, Ready says to play it again, and once every player still in it is set, it // Online, after a queued game, Ready says to play it again, and once every player still in it is set, it
// counts up to the server starting afresh with them. // counts up to the server starting afresh with them.
if (state == MATCH_OVER && isOnline) if (state == MATCH_OVER && isOnline)
@@ -149,8 +156,8 @@ void Match::update(Sim& sim)
edit_take(sim, fighter); edit_take(sim, fighter);
bool wasReady = isReady[i]; bool wasReady = isReady[i];
isReady[i] |= fighter.isBot || fighter.is_down(CONTROL_READY); isReady[i] |= fighter.isBot || fighter.is_down(CONTROL_READY);
if (isReady[i] && !wasReady) if (isReady[i] && !wasReady && loadout_deal(sim, i))
loadout_deal(sim, i); sim.cues->deal_play(i);
isEveryone &= isReady[i]; isEveryone &= isReady[i];
} }
if (isEveryone) if (isEveryone)
@@ -316,8 +323,6 @@ void Match::round_start(Sim& sim, bool isFirst)
mirror_copy(sim, true); mirror_copy(sim, true);
isWeaponPick &= rules.weapons.isChoice; isWeaponPick &= rules.weapons.isChoice;
isTrinketPick &= rules.trinkets.isChoice; isTrinketPick &= rules.trinkets.isChoice;
if (isWeaponPick || isTrinketPick)
sim.cues->choice_open();
std::fill(std::begin(isReady), std::end(isReady), false); std::fill(std::begin(isReady), std::end(isReady), false);
std::fill(std::begin(finishTimes), std::end(finishTimes), -1.0f); std::fill(std::begin(finishTimes), std::end(finishTimes), -1.0f);
@@ -410,6 +415,20 @@ void Match::edit_take(Sim& sim, Fighter& fighter)
} }
} }
// On the results, everyone but the winner can splat tomatoes where they click, for show, a few a second at most.
void Match::tomatoes_update(Sim& sim)
{
for (int i = 0; i < sim.fighterCount; i++)
{
Fighter& fighter = sim.fighters[i];
tomatoCooldowns[i] -= TICK;
if (i == winner || isGone[i] || tomatoCooldowns[i] > 0 || !fighter.is_pressed(CONTROL_ATTACK))
continue;
tomatoCooldowns[i] = TOMATO_COOLDOWN;
sim.cues->tomato_splat(fighter.controls.cursor, i);
}
}
// Copies the first fighter's loadout of one kind of item to every other fighter. // Copies the first fighter's loadout of one kind of item to every other fighter.
void Match::mirror_copy(Sim& sim, bool isTrinket) void Match::mirror_copy(Sim& sim, bool isTrinket)
{ {
@@ -424,10 +443,12 @@ void Match::mirror_copy(Sim& sim, bool isTrinket)
} }
} }
// Deals a fighter's empty slots at random. A mirrored kind is dealt only to the first fighter, and copied to everyone. // Deals a fighter's empty slots at random, returning whether it filled any. A mirrored kind is dealt only to the
void Match::loadout_deal(Sim& sim, int index) // first fighter, and copied to everyone.
bool Match::loadout_deal(Sim& sim, int index)
{ {
Fighter& fighter = sim.fighters[index]; Fighter& fighter = sim.fighters[index];
bool isDealt = false;
for (int kind = 0; kind < 2; kind++) for (int kind = 0; kind < 2; kind++)
{ {
bool isTrinket = kind == 1; bool isTrinket = kind == 1;
@@ -435,12 +456,15 @@ void Match::loadout_deal(Sim& sim, int index)
if (items.isMirror && index > 0) if (items.isMirror && index > 0)
continue; continue;
if (isTrinket) if (isTrinket)
isDealt |=
picks_deal(sim.rng, fighter.trinketLoadout, fighter.trinketSlots, TRINKET_COUNT, items.isDuplicates); picks_deal(sim.rng, fighter.trinketLoadout, fighter.trinketSlots, TRINKET_COUNT, items.isDuplicates);
else else
isDealt |=
picks_deal(sim.rng, fighter.weaponLoadout, fighter.weaponSlots, WEAPON_COUNT, items.isDuplicates); picks_deal(sim.rng, fighter.weaponLoadout, fighter.weaponSlots, WEAPON_COUNT, items.isDuplicates);
if (items.isMirror) if (items.isMirror)
mirror_copy(sim, isTrinket); mirror_copy(sim, isTrinket);
} }
return isDealt;
} }
// The countdown is over: slots still empty after choosing (by anyone who never readied) are dealt at random, and the // The countdown is over: slots still empty after choosing (by anyone who never readied) are dealt at random, and the
@@ -449,8 +473,6 @@ void Match::fight_start(Sim& sim)
{ {
for (int i = 0; i < sim.fighterCount; i++) for (int i = 0; i < sim.fighterCount; i++)
loadout_deal(sim, i); loadout_deal(sim, i);
if (isWeaponPick || isTrinketPick)
sim.cues->choice_close();
isWeaponPick = false; isWeaponPick = false;
isTrinketPick = false; isTrinketPick = false;
clock = 0; clock = 0;
@@ -788,6 +810,7 @@ Profile profile_get(Fighter& fighter)
std::copy(std::begin(fighter.trinketSlotLevels), std::end(fighter.trinketSlotLevels), profile.trinketSlotLevels); std::copy(std::begin(fighter.trinketSlotLevels), std::end(fighter.trinketSlotLevels), profile.trinketSlotLevels);
std::copy(std::begin(fighter.weaponItemLevels), std::end(fighter.weaponItemLevels), profile.weaponItemLevels); std::copy(std::begin(fighter.weaponItemLevels), std::end(fighter.weaponItemLevels), profile.weaponItemLevels);
std::copy(std::begin(fighter.trinketItemLevels), std::end(fighter.trinketItemLevels), profile.trinketItemLevels); std::copy(std::begin(fighter.trinketItemLevels), std::end(fighter.trinketItemLevels), profile.trinketItemLevels);
profile.isPvp = fighter.isPvp;
return profile; return profile;
} }
@@ -808,6 +831,7 @@ int Match::member_join(Sim& sim, int member, const Profile& profile)
std::copy(std::begin(profile.trinketSlotLevels), std::end(profile.trinketSlotLevels), fighter.trinketSlotLevels); std::copy(std::begin(profile.trinketSlotLevels), std::end(profile.trinketSlotLevels), fighter.trinketSlotLevels);
std::copy(std::begin(profile.weaponItemLevels), std::end(profile.weaponItemLevels), fighter.weaponItemLevels); std::copy(std::begin(profile.weaponItemLevels), std::end(profile.weaponItemLevels), fighter.weaponItemLevels);
std::copy(std::begin(profile.trinketItemLevels), std::end(profile.trinketItemLevels), fighter.trinketItemLevels); std::copy(std::begin(profile.trinketItemLevels), std::end(profile.trinketItemLevels), fighter.trinketItemLevels);
fighter.isPvp = profile.isPvp;
owners[index] = member; owners[index] = member;
std::memcpy(names[index], profile.name, sizeof(names[index])); std::memcpy(names[index], profile.name, sizeof(names[index]));
names[index][PLAYER_NAME_MAX] = '\0'; names[index][PLAYER_NAME_MAX] = '\0';
+5 -1
View File
@@ -156,6 +156,7 @@ struct Profile
int trinketSlotLevels[SLOT_MAX] = {}; int trinketSlotLevels[SLOT_MAX] = {};
int weaponItemLevels[WEAPON_COUNT] = {}; int weaponItemLevels[WEAPON_COUNT] = {};
int trinketItemLevels[TRINKET_COUNT] = {}; int trinketItemLevels[TRINKET_COUNT] = {};
bool isPvp = false;
}; };
// The default rules for a mode: a best-of-three deathmatch for two, or a three minute race for four. // The default rules for a mode: a best-of-three deathmatch for two, or a three minute race for four.
@@ -191,6 +192,8 @@ public:
float clock = 0; float clock = 0;
float countdown = READY_TIME; float countdown = READY_TIME;
float finishTimes[MAX_FIGHTERS] = {}; float finishTimes[MAX_FIGHTERS] = {};
// How long until each fighter can splat another tomato on the results.
float tomatoCooldowns[MAX_FIGHTERS] = {};
float timer = 0; float timer = 0;
int round = 0; int round = 0;
int winner = -1; int winner = -1;
@@ -248,7 +251,8 @@ private:
void fight_start(Sim& sim); void fight_start(Sim& sim);
void edit_take(Sim& sim, Fighter& fighter); void edit_take(Sim& sim, Fighter& fighter);
void mirror_copy(Sim& sim, bool isTrinket); void mirror_copy(Sim& sim, bool isTrinket);
void loadout_deal(Sim& sim, int fighter); void tomatoes_update(Sim& sim);
bool loadout_deal(Sim& sim, int fighter);
void deathmatch_update(Sim& sim); void deathmatch_update(Sim& sim);
void race_update(Sim& sim); void race_update(Sim& sim);
void queues_update(Sim& sim); void queues_update(Sim& sim);
+87 -21
View File
@@ -66,10 +66,19 @@ static Rect tab_area_get(Game& game, Rect header)
void Menu::update(Game& game) void Menu::update(Game& game)
{ {
Input& input = game.input; Input& input = game.input;
// Readying up while choosing closes the menu, back to the countdown with the loadout's tooltips. // A countdown for choosing items opens the menu on that kind's tab, however it began (here, or on the server for
// a game this screen joins), with one player on this screen, and the fight starting closes it. Readying up while
// choosing closes it too, back to the countdown with the loadout's tooltips.
Match& match = game.sim.match; Match& match = game.sim.match;
sandbox_levels_update(game);
bool isPicking = match.state == MATCH_COUNTDOWN && (match.isWeaponPick || match.isTrinketPick);
if (isPicking && !wasPicking && game.local_count() == 1)
choice_open(game);
if (!isPicking && wasPicking)
isOpen = false;
wasPicking = isPicking;
bool isReady = match.state == MATCH_COUNTDOWN && match.isReady[game.local]; bool isReady = match.state == MATCH_COUNTDOWN && match.isReady[game.local];
if (isReady && !wasReady && (match.isWeaponPick || match.isTrinketPick)) if (isReady && !wasReady && isPicking)
isOpen = false; isOpen = false;
wasReady = isReady; wasReady = isReady;
// Escape works as the menu control too. On a match's results (with the menu closed), either goes back to the // Escape works as the menu control too. On a match's results (with the menu closed), either goes back to the
@@ -186,7 +195,7 @@ void Menu::update(Game& game)
int step = int step =
(int)input.is_key_control_pressed(CONTROL_LEVEL_UP) - (int)input.is_key_control_pressed(CONTROL_LEVEL_DOWN); (int)input.is_key_control_pressed(CONTROL_LEVEL_UP) - (int)input.is_key_control_pressed(CONTROL_LEVEL_DOWN);
if (step != 0 && game.sim.match.state == MATCH_PRACTICE) if (step != 0)
level_step(game, step, mouse); level_step(game, step, mouse);
} }
@@ -194,6 +203,16 @@ void Menu::update(Game& game)
// equipped (and any copies equipped later). // equipped (and any copies equipped later).
void Menu::level_step(Game& game, int step, Vec2 mouse) void Menu::level_step(Game& game, int step, Vec2 mouse)
{ {
if (game.sim.match.state != MATCH_PRACTICE)
{
if (is_choosable(game, tab == TAB_TRINKETS))
return;
int& level = sandbox_levels_get(tab == TAB_TRINKETS)[selections[tab]];
int next = std::clamp(level + step, 0, LEVEL_COUNT - 1);
isLevelsPending |= next != level;
level = next;
return;
}
Fighter& fighter = game.local_get(); Fighter& fighter = game.local_get();
for (int kind = 0; kind < ITEM_TABS; kind++) for (int kind = 0; kind < ITEM_TABS; kind++)
{ {
@@ -214,6 +233,42 @@ void Menu::level_step(Game& game, int step, Vec2 mouse)
edit_encode({.type = EDIT_ITEM_LEVEL, .isTrinket = isTrinket, .index = item, .value = next})); edit_encode({.type = EDIT_ITEM_LEVEL, .isTrinket = isTrinket, .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,
// one at a time, until the fighter has them all.
void Menu::sandbox_levels_update(Game& game)
{
if (game.sim.match.state != MATCH_PRACTICE)
return;
Fighter& fighter = game.local_get();
for (int kind = 0; kind < ITEM_TABS; kind++)
{
bool isTrinket = kind == TAB_TRINKETS;
int* levels = sandbox_levels_get(isTrinket);
int* own = isTrinket ? fighter.trinketItemLevels : fighter.weaponItemLevels;
int count = isTrinket ? TRINKET_COUNT : WEAPON_COUNT;
for (int item = 0; item < count && isLevelsPending; item++)
{
if (levels[item] == own[item])
continue;
int device = game.devices[game.local];
if (game.input.devices[device].editCount == 0)
game.input.edit_send(
device,
edit_encode(
{.type = EDIT_ITEM_LEVEL, .isTrinket = isTrinket, .index = item, .value = levels[item]}));
return;
}
if (!isLevelsPending)
std::copy(own, own + count, levels);
}
isLevelsPending = false;
}
int* Menu::sandbox_levels_get(bool isTrinket)
{
return isTrinket ? sandboxTrinketLevels : sandboxWeaponLevels;
}
// Asks for one copy's level to move by step, through this screen's controls. // Asks for one copy's level to move by step, through this screen's controls.
void Menu::slot_level_step(Game& game, bool isTrinket, int slot, int step) void Menu::slot_level_step(Game& game, bool isTrinket, int slot, int step)
{ {
@@ -288,7 +343,7 @@ void Menu::pad_update(Game& game)
isOpen = false; isOpen = false;
game.audio.play(isOpen ? SOUND_SELECT : SOUND_SWAP); game.audio.play(isOpen ? SOUND_SELECT : SOUND_SWAP);
} }
if (tab >= ITEM_TABS || game.sim.match.state != MATCH_PRACTICE) if (tab >= ITEM_TABS)
return; return;
int level = (int)input.is_pad_pressed(DEVICE_KEYBOARD, SDL_GAMEPAD_BUTTON_NORTH) - int level = (int)input.is_pad_pressed(DEVICE_KEYBOARD, SDL_GAMEPAD_BUTTON_NORTH) -
(int)input.is_pad_pressed(DEVICE_KEYBOARD, SDL_GAMEPAD_BUTTON_WEST); (int)input.is_pad_pressed(DEVICE_KEYBOARD, SDL_GAMEPAD_BUTTON_WEST);
@@ -304,8 +359,9 @@ void Menu::hints_draw(Game& game, MenuLayout& layout)
return; return;
const char* hint = "A: Select B: Close LB/RB: Tabs"; const char* hint = "A: Select B: Close LB/RB: Tabs";
if (tab < ITEM_TABS) if (tab < ITEM_TABS)
hint = game.sim.match.state == MATCH_PRACTICE ? "A: Equip/Unequip X/Y: Level RS: Scroll Info LB/RB: Tabs" hint = game.sim.match.state == MATCH_PRACTICE
: "A: Equip/Unequip RS: Scroll Info B: Close LB/RB: Tabs"; ? "A: Equip/Unequip X/Y: Level RS: Scroll Info LB/RB: Tabs"
: "A: Equip/Unequip X/Y: Sandbox Level RS: Scroll Info B: Close LB/RB: Tabs";
else if (tab == TAB_PLAY && (page == PAGE_RULES || page == PAGE_QUEUE)) else if (tab == TAB_PLAY && (page == PAGE_RULES || page == PAGE_QUEUE))
hint = "A: Select Left/Right: Change B: Back Y: Join"; hint = "A: Select Left/Right: Change B: Back Y: Join";
else if (tab == TAB_SETTINGS) else if (tab == TAB_SETTINGS)
@@ -461,9 +517,15 @@ void Menu::items_draw(Game& game, MenuLayout& layout)
renderer.rect_draw(layout.divider, ui.tint); renderer.rect_draw(layout.divider, ui.tint);
// The description is of the selection, or of a hotbar slot clicked (or focused) on this tab while it still holds // The description is of the selection, or of a hotbar slot clicked (or focused) on this tab while it still holds
// something. // something.
// In a match (but not while choosing from this tab), the selection shows at its level in the sandbox, which is
// what levels here.
bool isMatch = game.sim.match.state != MATCH_PRACTICE;
bool isTrinket = tab == TAB_TRINKETS; bool isTrinket = tab == TAB_TRINKETS;
bool isSandboxLevel = isMatch && !is_choosable(game, isTrinket);
int item = selections[tab]; int item = selections[tab];
int level = (isTrinket ? fighter.trinketItemLevels : fighter.weaponItemLevels)[item]; int level = (isSandboxLevel ? sandbox_levels_get(isTrinket)
: isTrinket ? fighter.trinketItemLevels
: fighter.weaponItemLevels)[item];
int* slotLevel = nullptr; int* slotLevel = nullptr;
if (pinnedTab != tab) if (pinnedTab != tab)
pinnedSlot = -1; pinnedSlot = -1;
@@ -480,22 +542,24 @@ void Menu::items_draw(Game& game, MenuLayout& layout)
if (shown != described) if (shown != described)
description.scroll = 0; description.scroll = 0;
described = shown; described = shown;
Tooltip tooltip = ui.tooltip_build(game, isTrinket, item, level); // Only what can be levelled here previews its next level.
bool isLevelable = !isMatch || (isSandboxLevel && !slotLevel);
Tooltip tooltip = ui.tooltip_build(game, isTrinket, item, level, isLevelable);
if (slotLevel) if (slotLevel)
tooltip.slot = pinnedSlot; tooltip.slot = pinnedSlot;
Rect area = layout.description; Rect area = layout.description;
bool isLevelable = game.sim.match.state == MATCH_PRACTICE;
if (isLevelable) if (isLevelable)
area.h -= WIDGET_HEIGHT + LEVEL_ROW_GAP; area.h -= WIDGET_HEIGHT + LEVEL_ROW_GAP;
ui.tooltip_page_draw(game, description, area, tooltip); ui.tooltip_page_draw(game, description, area, tooltip);
// In the sandbox, a row under the description levels what it shows: the copy in a clicked hotbar slot, or else the // 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. A button greys out at the end of the levels. // selection with every copy of it equipped, and in a match, the selection's sandbox level. A button greys out at
// the end of the levels.
if (!isLevelable) if (!isLevelable)
return; return;
// The level between a minus and a plus, centred. // The level between a minus and a plus, centred.
char text[TOOLTIP_LENGTH]; char text[TOOLTIP_LENGTH];
SDL_snprintf(text, sizeof(text), "Level %d", level + 1); SDL_snprintf(text, sizeof(text), isSandboxLevel ? "Sandbox Level %d" : "Level %d", level + 1);
Vec2 size = game.text.size_get(text); Vec2 size = game.text.size_get(text);
float width = WIDGET_HEIGHT * 2 + LEVEL_ROW_GAP * 2 + size.x; float width = WIDGET_HEIGHT * 2 + LEVEL_ROW_GAP * 2 + size.x;
float x = std::round(area.x + (area.w - width) / 2); float x = std::round(area.x + (area.w - width) / 2);
@@ -522,6 +586,12 @@ void Menu::items_draw(Game& game, MenuLayout& layout)
void Menu::play_draw(Game& game, MenuLayout& layout) void Menu::play_draw(Game& game, MenuLayout& layout)
{ {
Ui& ui = game.ui; Ui& ui = game.ui;
// In a match, it's only the game's rules.
if (game.sim.match.state != MATCH_PRACTICE)
{
ui.game_summary_draw(game, game.sim.match.rules, {layout.body.x, layout.body.y});
return;
}
crumbs_draw(game, layout); crumbs_draw(game, layout);
Rect area = layout.body; Rect area = layout.body;
area.y += CRUMB_HEIGHT + DIVIDER; area.y += CRUMB_HEIGHT + DIVIDER;
@@ -1031,8 +1101,9 @@ bool Menu::is_bars_open()
return isOpen && tab < ITEM_TABS; return isOpen && tab < ITEM_TABS;
} }
// Fills tabs with the ones shown, in order, returning how many: every tab in the sandbox, both item tabs during a // Fills tabs with the ones shown, in order, returning how many: every tab in the sandbox, and in a match the item tabs
// countdown that lets fighters choose either kind, and only Settings anywhere else. // (to choose from when its countdown lets fighters, and otherwise to look at and level the sandbox's), Play (the
// game's rules) and Settings.
int Menu::tabs_get(Game& game, int* tabs) int Menu::tabs_get(Game& game, int* tabs)
{ {
Match& match = game.sim.match; Match& match = game.sim.match;
@@ -1043,13 +1114,8 @@ int Menu::tabs_get(Game& game, int* tabs)
tabs[count++] = i; tabs[count++] = i;
return count; return count;
} }
if (match.isWeaponPick || match.isTrinketPick) for (int tab : {TAB_WEAPONS, TAB_TRINKETS, TAB_PLAY, TAB_SETTINGS})
{ tabs[count++] = tab;
tabs[count++] = TAB_WEAPONS;
tabs[count++] = TAB_TRINKETS;
}
if (count == 0)
tabs[count++] = TAB_SETTINGS;
return count; return count;
} }
+7
View File
@@ -88,7 +88,12 @@ private:
int targets[ITEM_TABS] = {}; int targets[ITEM_TABS] = {};
int pinnedTab = -1; int pinnedTab = -1;
bool isPinnedTrinket = false; bool isPinnedTrinket = false;
bool wasPicking = false;
bool wasReady = false; bool wasReady = false;
// The sandbox's item levels, kept while in it, so a match can level them for when this screen is back.
int sandboxWeaponLevels[WEAPON_COUNT] = {};
int sandboxTrinketLevels[TRINKET_COUNT] = {};
bool isLevelsPending = false;
int scrolls[ITEM_TABS] = {}; int scrolls[ITEM_TABS] = {};
int selections[ITEM_TABS] = {}; int selections[ITEM_TABS] = {};
int page = PAGE_MAIN; int page = PAGE_MAIN;
@@ -112,6 +117,8 @@ private:
void loadout_set(Game& game, bool isTrinket, int slot, int item); void loadout_set(Game& game, bool isTrinket, int slot, int item);
void item_toggle(Game& game); void item_toggle(Game& game);
void level_step(Game& game, int step, Vec2 mouse); void level_step(Game& game, int step, Vec2 mouse);
void sandbox_levels_update(Game& game);
int* sandbox_levels_get(bool isTrinket);
void slot_level_step(Game& game, bool isTrinket, int slot, int step); void slot_level_step(Game& game, bool isTrinket, int slot, int step);
void pad_update(Game& game); void pad_update(Game& game);
void hints_draw(Game& game, MenuLayout& layout); void hints_draw(Game& game, MenuLayout& layout);
+5 -3
View File
@@ -27,7 +27,7 @@
#include <string_view> #include <string_view>
#include <type_traits> #include <type_traits>
constexpr int PROTOCOL_VERSION = 3; constexpr int PROTOCOL_VERSION = 4;
constexpr int LOBBY_MEMBERS_MAX = 16; constexpr int LOBBY_MEMBERS_MAX = 16;
constexpr int LOBBY_CODE_LENGTH = 4; constexpr int LOBBY_CODE_LENGTH = 4;
// Letters only, without I and O, so codes are easy to read out and type. // Letters only, without I and O, so codes are easy to read out and type.
@@ -49,6 +49,7 @@ constexpr int SERVER_URL_LENGTH = 128;
// PACKET_CHAT Uint16 length, text // PACKET_CHAT Uint16 length, text
// PACKET_TYPING Uint8 isTyping // PACKET_TYPING Uint8 isTyping
// PACKET_RENAME Uint16 length, text // PACKET_RENAME Uint16 length, text
// PACKET_PING Uint16 milliseconds this player's round trip to the server
// //
// Server to game: // Server to game:
// PACKET_STATE room, tick, state the game this player is in now, as a tick starts // PACKET_STATE room, tick, state the game this player is in now, as a tick starts
@@ -58,7 +59,7 @@ constexpr int SERVER_URL_LENGTH = 128;
// once the events before them are in, and how early // once the events before them are in, and how early
// (in ticks) this player's last controls came // (in ticks) this player's last controls came
// PACKET_CHECKSUM room, tick, Uint32 the room's state as that tick starts // PACKET_CHECKSUM room, tick, Uint32 the room's state as that tick starts
// PACKET_CHAT from, Uint16 length, text and the same for typing and renames // PACKET_CHAT from, Uint16 length, text and the same for typing, renames and pings
enum PacketType : unsigned char enum PacketType : unsigned char
{ {
PACKET_PROFILE = 1, PACKET_PROFILE = 1,
@@ -70,7 +71,8 @@ enum PacketType : unsigned char
PACKET_STATE, PACKET_STATE,
PACKET_EVENT, PACKET_EVENT,
PACKET_FRAMES, PACKET_FRAMES,
PACKET_CHECKSUM PACKET_CHECKSUM,
PACKET_PING
}; };
// Packets are plain values one after another, as they sit in memory (every platform agrees on these layouts). // Packets are plain values one after another, as they sit in memory (every platform agrees on these layouts).
+13 -6
View File
@@ -69,6 +69,7 @@ constexpr float WHIP_INNER = 2;
constexpr float WHIP_JITTER = 0.5f; constexpr float WHIP_JITTER = 0.5f;
constexpr float WHIP_OUTER = 5; constexpr float WHIP_OUTER = 5;
constexpr float WISP_FRAME_STAGGER = 0.3f; constexpr float WISP_FRAME_STAGGER = 0.3f;
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 colour fills in, like a damage
// number. // number.
@@ -390,16 +391,22 @@ void Scene::match_start()
game.menu.isOpen = false; game.menu.isOpen = false;
} }
void Scene::choice_open() // A tomato thrown on the results: a splat in the thrower's colour where it landed, spraying juice.
void Scene::tomato_splat(Vec2 position, int owner)
{ {
if (game.local_count() == 1) if (isMuted)
game.menu.choice_open(game); return;
Color color = game.color_get(owner, true);
game.effects.splat_spawn(position, color);
game.effects.burst(TOMATO_BURST, position, -PI / 2, color);
game.audio.play(SOUND_SPLAT);
} }
void Scene::choice_close() // Readying with empty slots dealt them: this screen's own fighters hear the items go in.
void Scene::deal_play(int fighter)
{ {
if (game.local_count() > 0) if (!isMuted && game.is_local(fighter))
game.menu.isOpen = false; game.audio.play(SOUND_EQUIP);
} }
void Scene::sandbox_return() void Scene::sandbox_return()
+2 -2
View File
@@ -35,8 +35,8 @@ public:
void number_spawn(Damage& damage) override; void number_spawn(Damage& damage) override;
void level_step(bool isUp) override; void level_step(bool isUp) override;
void match_start() override; void match_start() override;
void choice_open() override; void deal_play(int fighter) override;
void choice_close() override; void tomato_splat(Vec2 position, int owner) override;
void sandbox_return() override; void sandbox_return() override;
private: private:
+20 -1
View File
@@ -112,6 +112,7 @@ void Session::update(Game& game, Controls local)
if (!isJoined) if (!isJoined)
return; return;
profile_watch(game); profile_watch(game);
ping_report(game);
confirm(game); confirm(game);
rewind(game); rewind(game);
frames.erase(frames.begin(), frames.lower_bound(confirmed)); frames.erase(frames.begin(), frames.lower_bound(confirmed));
@@ -211,6 +212,14 @@ void Session::packet_handle(Game& game, const std::string& bytes)
game.chat.typing[from] = isTyping != 0; game.chat.typing[from] = isTyping != 0;
return; return;
} }
if (type == PACKET_PING)
{
Uint16 ping = 0;
for (NetMember& member : game.net.members)
if (member.id == from && from != game.net.selfId && reader.get(ping))
member.ping = ping;
return;
}
Uint16 length = 0; Uint16 length = 0;
if (!reader.get(length) || reader.at + length > bytes.size()) if (!reader.get(length) || reader.at + length > bytes.size())
return; return;
@@ -569,7 +578,17 @@ void Session::queue_send(Game& game, const Rules& rules, bool isOpen)
float Session::ping_get(Game& game, int member) float Session::ping_get(Game& game, int member)
{ {
for (NetMember& netMember : game.net.members) for (NetMember& netMember : game.net.members)
if (netMember.id == member && member == game.net.selfId) if (netMember.id == member)
return netMember.ping; return netMember.ping;
return PING_NONE; return PING_NONE;
} }
// This screen's own round trip goes to the lobby every so often, for everyone else's connection bars.
void Session::ping_report(Game& game)
{
float ping = ping_get(game, game.net.selfId);
if (ping < 0 || SDL_GetTicks() < nextPingReport)
return;
nextPingReport = SDL_GetTicks() + PING_REPORT_INTERVAL;
game.net.send(PacketWriter().put(PACKET_PING).put((Uint16)std::min(ping, (float)UINT16_MAX)).bytes, true);
}
+4
View File
@@ -8,6 +8,8 @@
struct Game; struct Game;
constexpr int SESSION_RING = 64; constexpr int SESSION_RING = 64;
// How often (in milliseconds) this screen tells the lobby its round trip.
constexpr Uint64 PING_REPORT_INTERVAL = 2000;
// A member's connection has no ping to show. // A member's connection has no ping to show.
constexpr float PING_NONE = -2; constexpr float PING_NONE = -2;
@@ -91,6 +93,7 @@ private:
// next, and whether it's told its pending one is waiting. // next, and whether it's told its pending one is waiting.
Uint64 nextRename = 0; Uint64 nextRename = 0;
bool isRenameWaiting = false; bool isRenameWaiting = false;
Uint64 nextPingReport = 0;
void start(Game& game); void start(Game& game);
void packets_take(Game& game); void packets_take(Game& game);
@@ -103,5 +106,6 @@ private:
void inputs_send(Game& game); void inputs_send(Game& game);
void local_find(Game& game); void local_find(Game& game);
void profile_watch(Game& game); void profile_watch(Game& game);
void ping_report(Game& game);
void text_send(Game& game, PacketType type, const std::string& text); void text_send(Game& game, PacketType type, const std::string& text);
}; };
+10 -5
View File
@@ -41,6 +41,7 @@ constexpr const char* KEY_HUD_SPEED = "hud_speed";
constexpr const char* KEY_VSYNC = "vsync"; constexpr const char* KEY_VSYNC = "vsync";
constexpr const char* KEY_WEAPON_SLOTS = "weapon_slots"; constexpr const char* KEY_WEAPON_SLOTS = "weapon_slots";
constexpr const char* SETTINGS_FILE = "settings.txt"; constexpr const char* SETTINGS_FILE = "settings.txt";
constexpr const char* KEY_SANDBOX_PVP = "sandbox_pvp";
constexpr const char* KEY_SANDBOX_SLOT = "sandbox_slot"; constexpr const char* KEY_SANDBOX_SLOT = "sandbox_slot";
constexpr const char* KEY_SANDBOX_TRINKET_LEVELS = "sandbox_trinket_levels"; constexpr const char* KEY_SANDBOX_TRINKET_LEVELS = "sandbox_trinket_levels";
constexpr const char* KEY_SANDBOX_TRINKETS = "sandbox_trinkets"; constexpr const char* KEY_SANDBOX_TRINKETS = "sandbox_trinkets";
@@ -201,6 +202,8 @@ static void sandbox_read(SandboxSave& sandbox, const char* key, char* value)
sandbox.feet.y = SDL_atof(value); sandbox.feet.y = SDL_atof(value);
else if (SDL_strcmp(key, KEY_SANDBOX_SLOT) == 0) else if (SDL_strcmp(key, KEY_SANDBOX_SLOT) == 0)
sandbox.slot = SDL_atoi(value); sandbox.slot = SDL_atoi(value);
else if (SDL_strcmp(key, KEY_SANDBOX_PVP) == 0)
sandbox.isPvp = SDL_atoi(value) != 0;
else if (SDL_strcmp(key, KEY_SANDBOX_WEAPONS) == 0) else if (SDL_strcmp(key, KEY_SANDBOX_WEAPONS) == 0)
list_read(value, false, sandbox.weaponLoadout, sandbox.weaponSlotLevels); list_read(value, false, sandbox.weaponLoadout, sandbox.weaponSlotLevels);
else if (SDL_strcmp(key, KEY_SANDBOX_TRINKETS) == 0) else if (SDL_strcmp(key, KEY_SANDBOX_TRINKETS) == 0)
@@ -216,8 +219,9 @@ static void sandbox_read(SandboxSave& sandbox, const char* key, char* value)
static int sandbox_write(Fighter& fighter, char* text, int size) static int sandbox_write(Fighter& fighter, char* text, int size)
{ {
int length = SDL_snprintf(text, size, "%s=%g\n%s=%g\n%s=%d\n", KEY_SANDBOX_X, fighter.center_get().x, KEY_SANDBOX_Y, int length = SDL_snprintf(text, size, "%s=%g\n%s=%g\n%s=%d\n%s=%d\n", KEY_SANDBOX_X, fighter.center_get().x,
fighter.body.box.y + fighter.body.box.h, KEY_SANDBOX_SLOT, fighter.slot); KEY_SANDBOX_Y, fighter.body.box.y + fighter.body.box.h, KEY_SANDBOX_SLOT, fighter.slot,
KEY_SANDBOX_PVP, (int)fighter.isPvp);
for (int kind = 0; kind < 2; kind++) for (int kind = 0; kind < 2; kind++)
{ {
bool isTrinket = kind == 1; bool isTrinket = kind == 1;
@@ -621,9 +625,9 @@ void Settings::customize_draw(Game& game, Rect area)
save(game); save(game);
} }
// Puts the sandbox back as it was saved, once the world and fighter exist: loadouts, levels, the slot held, and where // Puts the sandbox back as it was saved, once the world and fighter exist: loadouts, levels, the slot held, PvP, and
// the fighter stood (unless that spot is now blocked). With nothing saved yet, the fighter starts with a sword in its // where the fighter stood (unless that spot is now blocked). With nothing saved yet, the fighter starts with a sword in
// first slot. // its first slot.
void Settings::restore(Game& game) void Settings::restore(Game& game)
{ {
Fighter& fighter = game.local_get(); Fighter& fighter = game.local_get();
@@ -636,6 +640,7 @@ void Settings::restore(Game& game)
std::copy(std::begin(sandbox.trinketLoadout), std::end(sandbox.trinketLoadout), fighter.trinketLoadout); std::copy(std::begin(sandbox.trinketLoadout), std::end(sandbox.trinketLoadout), fighter.trinketLoadout);
std::copy(std::begin(sandbox.weaponLevels), std::end(sandbox.weaponLevels), fighter.weaponItemLevels); std::copy(std::begin(sandbox.weaponLevels), std::end(sandbox.weaponLevels), fighter.weaponItemLevels);
std::copy(std::begin(sandbox.trinketLevels), std::end(sandbox.trinketLevels), fighter.trinketItemLevels); std::copy(std::begin(sandbox.trinketLevels), std::end(sandbox.trinketLevels), fighter.trinketItemLevels);
fighter.isPvp = sandbox.isPvp;
// Saves from before copies had their own levels give each copy its item's level. // Saves from before copies had their own levels give each copy its item's level.
for (int i = 0; i < SLOT_MAX; i++) for (int i = 0; i < SLOT_MAX; i++)
{ {
+1
View File
@@ -22,6 +22,7 @@ struct SandboxSave
int weaponLevels[WEAPON_COUNT] = {}; int weaponLevels[WEAPON_COUNT] = {};
int weaponLoadout[SLOT_MAX]; int weaponLoadout[SLOT_MAX];
int weaponSlotLevels[SLOT_MAX]; int weaponSlotLevels[SLOT_MAX];
bool isPvp = false;
bool isSaved = false; bool isSaved = false;
}; };
+2 -2
View File
@@ -39,9 +39,9 @@ public:
{} {}
virtual void match_start() virtual void match_start()
{} {}
virtual void choice_open() virtual void deal_play(int fighter)
{} {}
virtual void choice_close() virtual void tomato_splat(Vec2 position, int owner)
{} {}
virtual void sandbox_return() virtual void sandbox_return()
{} {}
+5 -4
View File
@@ -86,7 +86,8 @@ inline constexpr TrinketDef TRINKET_DEFS[TRINKET_COUNT] = {
{0.1f, 0.25f, 1 / 3.0f}, {0.1f, 0.25f, 1 / 3.0f},
"Stud Multiplier"}, "Stud Multiplier"},
{"Dual Wield", {"Dual Wield",
"Your last-held weapon fires from\nyour off hand too, for a cut of\nits damage. Each copy adds another\nweapon, the next most recent.", "Your last-held weapon fires from\nyour off hand too, for a cut of\nits damage. Each copy adds another\nweapon, "
"the next most recent.",
{"10% Off-Hand Damage", "25% Off-Hand Damage", "33% Off-Hand Damage"}, {"10% Off-Hand Damage", "25% Off-Hand Damage", "33% Off-Hand Damage"},
{0.1f, 0.25f, 1 / 3.0f}, {0.1f, 0.25f, 1 / 3.0f},
"Guns akimbo!"}, "Guns akimbo!"},
@@ -110,9 +111,9 @@ inline constexpr TrinketDef TRINKET_DEFS[TRINKET_COUNT] = {
{"Shock Absorber", {"Shock Absorber",
"Your own weapons move you around\nless (recoil, kickback, lunges,\nrebounds, pulls and blasts), and\nyou take " "Your own weapons move you around\nless (recoil, kickback, lunges,\nrebounds, pulls and blasts), and\nyou take "
"less knockback.", "less knockback.",
{"12.5% Less Weapon Movement\n8% Less Knockback Taken", "25% Less Weapon Movement\n17% Less Knockback Taken", {"17% Less Weapon Movement\n11% Less Knockback Taken", "33% Less Weapon Movement\n22% Less Knockback Taken",
"37.5% Less Weapon Movement\n25% Less Knockback Taken"}, "50% Less Weapon Movement\n33% Less Knockback Taken"},
{0.875f, 0.75f, 0.625f}, {5 / 6.0f, 2 / 3.0f, 0.5f},
"The mechanical kind, not the\nelectrical kind."}, "The mechanical kind, not the\nelectrical kind."},
{"Shrinker", {"Shrinker",
"Makes you smaller.", "Makes you smaller.",
+7 -7
View File
@@ -1190,9 +1190,9 @@ static void effects_preview_get(Game& game, const char* now, const char* next, c
SDL_strlcpy(text, result.c_str(), size); SDL_strlcpy(text, result.c_str(), size);
} }
// Name, numbers at a level and description of a weapon or trinket. Trinkets say how many copies are equipped, and // Name, numbers at a level and description of a weapon or trinket, previewing the next level's where asked (the
// when they stack. // catalog). Trinkets say how many copies are equipped, and when they stack.
Tooltip Ui::tooltip_build(Game& game, bool isTrinket, int item, int level) Tooltip Ui::tooltip_build(Game& game, bool isTrinket, int item, int level, bool isPreview)
{ {
Tooltip tooltip; Tooltip tooltip;
tooltip.isTrinket = isTrinket; tooltip.isTrinket = isTrinket;
@@ -1214,8 +1214,8 @@ Tooltip Ui::tooltip_build(Game& game, bool isTrinket, int item, int level)
else else
SDL_strlcpy(equipped, copies > 0 ? "Equipped" : "Not Equipped", sizeof(equipped)); SDL_strlcpy(equipped, copies > 0 ? "Equipped" : "Not Equipped", sizeof(equipped));
tooltip.name = def.name; tooltip.name = def.name;
// In the sandbox, the effects show what the next level changes after an arrow, as weapons' stats do. // Previewed (in the catalog), the effects show what the next level changes after an arrow, as stats do.
bool isPreview = game.sim.match.state == MATCH_PRACTICE && level < LEVEL_COUNT - 1; isPreview &= level < LEVEL_COUNT - 1;
char effects[TOOLTIP_LENGTH]; char effects[TOOLTIP_LENGTH];
if (isPreview) if (isPreview)
effects_preview_get(game, def.effects[level], def.effects[level + 1], effects, sizeof(effects)); effects_preview_get(game, def.effects[level], def.effects[level + 1], effects, sizeof(effects));
@@ -1234,8 +1234,8 @@ Tooltip Ui::tooltip_build(Game& game, bool isTrinket, int item, int level)
const WeaponDef& def = WEAPON_DEFS[item]; const WeaponDef& def = WEAPON_DEFS[item];
tooltip.name = def.name; tooltip.name = def.name;
// In the sandbox, values that change at the next level show it after an arrow. // Previewed (in the catalog), values that change at the next level show it after an arrow.
bool isPreview = game.sim.match.state == MATCH_PRACTICE && level < LEVEL_COUNT - 1; isPreview &= level < LEVEL_COUNT - 1;
SDL_snprintf(tooltip.stats, sizeof(tooltip.stats), isPreview ? "(Level %d → Level %d)" : "(Level %d)", level + 1, SDL_snprintf(tooltip.stats, sizeof(tooltip.stats), isPreview ? "(Level %d → Level %d)" : "(Level %d)", level + 1,
level + 2); level + 2);
stats_table_build(fighter, item, level, isPreview, tooltip); stats_table_build(fighter, item, level, isPreview, tooltip);
+1 -1
View File
@@ -198,7 +198,7 @@ public:
void draw(Game& game); void draw(Game& game);
void slot_draw(Game& game, Slot slot); void slot_draw(Game& game, Slot slot);
void panel_draw(Renderer& renderer, Rect rect, Color border, float fillAlpha = -1, float stroke = PANEL_BORDER); void panel_draw(Renderer& renderer, Rect rect, Color border, float fillAlpha = -1, float stroke = PANEL_BORDER);
Tooltip tooltip_build(Game& game, bool isTrinket, int item, int level); Tooltip tooltip_build(Game& game, bool isTrinket, int item, int level, bool isPreview = false);
void stats_table_build(Fighter& fighter, int weapon, int level, bool isPreview, Tooltip& tooltip); void stats_table_build(Fighter& fighter, int weapon, int level, bool isPreview, Tooltip& tooltip);
void table_row_draw(Game& game, Tooltip& tooltip, int row, Vec2 corner, const float* columns, Font font, void table_row_draw(Game& game, Tooltip& tooltip, int row, Vec2 corner, const float* columns, Font font,
float alpha); float alpha);
+1
View File
@@ -109,6 +109,7 @@ PICKS = {
"slam": {"f": "US03-39-2", "s": 0.16, "n": 0.6, "sp": 0.7}, "slam": {"f": "US03-39-2", "s": 0.16, "n": 0.6, "sp": 0.7},
"slash": {"f": "4003_47_2", "s": 0.792, "n": 0.2}, "slash": {"f": "4003_47_2", "s": 0.792, "n": 0.2},
"slash_heavy": {"f": "4003_47_2", "s": 0.792, "n": 0.35, "sp": 0.75}, "slash_heavy": {"f": "4003_47_2", "s": 0.792, "n": 0.35, "sp": 0.75},
"splat": {"f": "4001_32_1", "n": 0.5},
"step": {"f": "4003_46_2", "s": 0.796, "n": 0.08, "g": 0.6, "r": 16000}, "step": {"f": "4003_46_2", "s": 0.796, "n": 0.08, "g": 0.6, "r": 16000},
"swap": {"keep": "demo1"}, "swap": {"keep": "demo1"},
"swing": {"f": "4003_47_1", "s": 0.395, "n": 0.3}, "swing": {"f": "4003_47_1", "s": 0.395, "n": 0.3},
+1
View File
@@ -72,6 +72,7 @@
"slam": 0.5555170721984241, "slam": 0.5555170721984241,
"slash": 0.3046866383126178, "slash": 0.3046866383126178,
"slash_heavy": 0.5043262612182655, "slash_heavy": 0.5043262612182655,
"splat": 0.35,
"step": 0.0363354998815522, "step": 0.0363354998815522,
"swap": 0.10673649790216319, "swap": 0.10673649790216319,
"swing": 0.20927146265655572, "swing": 0.20927146265655572,