Initial commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shweetandClaude Opus 5.5 committed 2026-11-08 01:30:33 -05:00
commit 2007fb7edc
200 files changed
+27710

No files matched your search

+761
View File
@@ -0,0 +1,761 @@
#include "match.h"
#include "game.h"
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_scancode.h>
#include <algorithm>
constexpr Burst FINISH_BURST = {.gravity = 200, .life = 1, .size = 3, .speed = 240, .count = 40};
constexpr float RACE_GRACE = 15;
constexpr float ROUND_OVER_TIME = 2.5f;
constexpr float SECONDS_PER_MINUTE = 60;
constexpr int RACE_PLAYERS = 4;
constexpr int RACE_TIMER = 3;
Rules rules_get(int mode)
{
Rules rules = {.mode = mode};
if (mode == MODE_RACE)
{
rules.players = RACE_PLAYERS;
rules.timer = RACE_TIMER;
}
return rules;
}
// Rules from someone else (online), kept in the ranges the menu allows. Only deathmatches can be queued for now.
Rules rules_clean(Rules rules)
{
rules.mode = MODE_DEATHMATCH;
rules.players = std::clamp(rules.players, PLAYERS_MIN, PLAYERS_MAX);
rules.bestOf = std::clamp(rules.bestOf, 1, BEST_OF_MAX) | 1;
rules.timer = std::clamp(rules.timer, 0, TIMER_MAX);
rules.health = std::clamp(rules.health, (float)BASE_HEALTH_MIN, (float)BASE_HEALTH_MAX);
if (!(rules.health == rules.health))
rules.health = rules_get(MODE_DEATHMATCH).health;
rules.healthMultiplier = std::clamp(rules.healthMultiplier, 1.0f, MULTIPLIER_MAX);
if (!(rules.healthMultiplier == rules.healthMultiplier))
rules.healthMultiplier = 1;
for (ItemRules* items : {&rules.weapons, &rules.trinkets})
{
items->slots = std::clamp(items->slots, 1, SLOT_MAX);
items->period = std::clamp(items->period, PERIOD_MIN, PERIOD_MAX);
items->startLevel = std::clamp(items->startLevel, 1, LEVEL_COUNT);
items->timing = std::clamp(items->timing, (int)PICK_FIRST_ROUND, (int)PICK_PERIOD);
}
return rules;
}
// Fills a loadout's empty slots with random picks out of count items, none repeating what's already in it.
static void picks_deal(Rng& rng, int* loadout, int slots, int count, bool isDuplicates)
{
for (int i = 0; i < slots; i++)
{
if (loadout[i] >= 0)
continue;
int pick = rng.random_get(0, count);
while (!isDuplicates && std::find(loadout, loadout + slots, pick) != loadout + slots)
pick = rng.random_get(0, count);
loadout[i] = pick;
}
}
// Whether a round hands out this kind of item: the first round of the match always, every round, or every period.
static bool is_pick_round(ItemRules& items, int round, bool isFirst)
{
return isFirst || items.timing == PICK_EVERY_ROUND || (items.timing == PICK_PERIOD && round % items.period == 0);
}
static int level_get(ItemRules& items, int round)
{
return std::clamp(items.startLevel - 1 + (items.isLevelUp ? round : 0), 0, LEVEL_COUNT - 1);
}
void Match::update(Game& game)
{
timer -= TICK;
// The menu's Play starts a game, and Escape (or B) leaves the results for the sandbox, unless it's just closed the
// menu.
Input& input = game.input;
bool isOver = state == MATCH_OVER;
// Online, this screen's keys can't change the shared game (starting matches online is the host's, through the
// session).
bool isLocal = !game.session.isActive;
bool isPlay = isLocal && input.isPlayRequested;
bool isSandbox = isLocal && isOver && !game.menu.isOpen && !game.menu.isEscapeTaken &&
(input.is_pressed(SDL_SCANCODE_ESCAPE) || input.is_pad_tapped(SDL_GAMEPAD_BUTTON_EAST));
if (state != MATCH_PRACTICE)
leaves_take(game);
switch (state)
{
case MATCH_PRACTICE:
case MATCH_OVER:
// Online, after a queued game, Ready says to play it again (see Session).
for (int i = 0; i < game.fighterCount && state == MATCH_OVER && game.session.isActive; i++)
if (game.fighters[i].is_pressed(CONTROL_READY) && !isGone[i])
{
isReady[i] = !isReady[i];
game.audio.play(isReady[i] ? SOUND_EQUIP : SOUND_UNEQUIP);
}
// In the sandbox, every fighter's edits land, and Ready (R) puts a fighter back at its spawn, still, with
// its attacks cleared away. (Keys typed into a text field never reach the controls.)
// Online, Ready in a queued game's area readies up for it instead.
if (state == MATCH_PRACTICE)
queues_update(game);
for (int i = 0; i < game.fighterCount && state == MATCH_PRACTICE; i++)
{
Fighter& fighter = game.fighters[i];
edit_take(game, fighter);
if (!fighter.is_pressed(CONTROL_READY))
continue;
if (queuedIn[i] >= 0)
{
isQueueReady[i] = !isQueueReady[i];
game.audio.play(isQueueReady[i] ? SOUND_EQUIP : SOUND_UNEQUIP);
continue;
}
fighter.position_reset(game);
std::erase_if(game.weapons.attacks, [&](Attack& attack) { return attack.owner == i; });
}
if (isPlay)
start(game);
else if (isSandbox && state == MATCH_OVER)
sandbox_return(game);
break;
case MATCH_COUNTDOWN:
{
// Ready sticks once pressed, and bots always are. Once everyone is, the wait drops to its last second.
// Fighters choosing items pick them through their controls.
bool isEveryone = true;
for (int i = 0; i < game.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
edit_take(game, fighter);
isReady[i] |= fighter.isBot || fighter.is_down(CONTROL_READY);
isEveryone &= isReady[i];
}
if (isEveryone)
timer = std::min(timer, COUNTDOWN_TIME);
if (timer <= 0)
fight_start(game);
break;
}
case MATCH_FIGHT:
clock += TICK;
if (rules.mode == MODE_RACE)
race_update(game);
else
deathmatch_update(game);
break;
case MATCH_ROUND_OVER:
if (timer > 0)
break;
if (winner >= 0 && wins[winner] >= rules.wins_get())
{
state = MATCH_OVER;
std::fill(std::begin(isReady), std::end(isReady), false);
break;
}
round += winner >= 0;
round_start(game, false);
break;
}
}
// Loads the arena (without the dummy) or builds a fresh course, maps it for the bots, sets everyone's slots to the
// rules' and starts the first round. In single player, every fighter but this screen's is a bot.
void Match::start(Game& game)
{
if (state == MATCH_PRACTICE)
sandbox = game.local_get();
winner = -1;
game.fighterCount = rules.players;
if (rules.mode == MODE_RACE)
game.world.course_build(game.rng);
else
game.world.arena_load();
game.bot.graph_build(game.world);
std::fill(std::begin(wins), std::end(wins), 0);
round = 0;
// Online (a queued game), the bots are whoever no member plays, and everyone's colour was settled already.
bool isOnline = game.session.isActive;
for (int i = 0; i < game.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
fighter.isBot = isOnline ? owners[i] < 0 : game.devices[i] == DEVICE_NONE || (i == game.local && fighter.isBot);
fighter.slots_set(rules.weapons.slots, rules.trinkets.slots);
}
if (!isOnline)
colors_assign(game);
// The menu closes for whoever plays it on this screen (online, a queued game starting doesn't close a bystander's).
if (game.local_count() > 0)
game.menu.isOpen = false;
round_start(game, true);
}
// Another player on this screen joins the sandbox on a gamepad, starting from this screen's sandbox loadout and
// settings, with PvP off.
void Match::sandbox_join(Game& game, int device)
{
if (game.fighterCount >= MAX_FIGHTERS)
return;
int index = game.fighterCount++;
Fighter& fighter = game.fighters[index];
fighter = game.local_get();
fighter.init(game, index);
Fighter& host = game.local_get();
std::copy(std::begin(host.weaponLoadout), std::end(host.weaponLoadout), fighter.weaponLoadout);
std::copy(std::begin(host.trinketLoadout), std::end(host.trinketLoadout), fighter.trinketLoadout);
fighter.isPvp = false;
fighter.controls = {};
game.devices[index] = device;
game.input.devices[device].isClaimed = true;
colors_assign(game);
game.audio.play(SOUND_EQUIP);
}
// A fighter leaves the sandbox with its attacks, and the ones after it move down a place.
void Match::sandbox_leave(Game& game, int index)
{
int device = game.devices[index];
if (device > DEVICE_KEYBOARD)
game.input.devices[device].isClaimed = false;
std::vector<Attack>& attacks = game.weapons.attacks;
std::erase_if(attacks, [&](Attack& attack) { return attack.owner == index; });
for (Attack& attack : attacks)
attack.owner -= attack.owner > index;
for (int i = index; i + 1 < game.fighterCount; i++)
{
game.fighters[i] = game.fighters[i + 1];
game.fighters[i].index = i;
game.devices[i] = game.devices[i + 1];
}
game.fighterCount--;
game.devices[game.fighterCount] = DEVICE_NONE;
colors_assign(game);
game.audio.play(SOUND_UNEQUIP);
}
// Gives every fighter a colour of their own: the one they asked for if it's free (the earlier fighter keeps it if two
// ask for the same), then the rest in palette order. Only this screen's player asks for now.
void Match::colors_assign(Game& game)
{
int wanted[MAX_FIGHTERS];
std::fill(std::begin(wanted), std::end(wanted), COLOR_UNASSIGNED);
wanted[game.local] = game.settings.color_get();
bool isTaken[COLOR_COUNT] = {};
for (int i = 0; i < MAX_FIGHTERS; i++)
{
colors[i] = COLOR_UNASSIGNED;
if (wanted[i] >= 0 && !isTaken[wanted[i]])
{
colors[i] = wanted[i];
isTaken[wanted[i]] = true;
}
}
for (int i = 0; i < MAX_FIGHTERS; i++)
{
if (colors[i] >= 0)
continue;
colors[i] = std::find(isTaken, isTaken + COLOR_COUNT, false) - isTaken;
isTaken[colors[i]] = true;
}
}
// Sets everyone's levels and health for the round, and hands out items if it's their round: dealt at random, or
// emptied for choosing in the menu (bots are dealt theirs) with the menu opened to them.
void Match::round_start(Game& game, bool isFirst)
{
isWeaponPick = is_pick_round(rules.weapons, round, isFirst);
isTrinketPick = is_pick_round(rules.trinkets, round, isFirst);
game.weapons.attacks.clear();
for (int i = 0; i < game.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
fighter.levels_fill(level_get(rules.weapons, round), level_get(rules.trinkets, round));
// Mirrored items are dealt once, to the first fighter, and copied to the rest below. Bots choose nothing then.
if (isWeaponPick)
{
std::fill(fighter.weaponLoadout, fighter.weaponLoadout + fighter.weaponSlots, -1);
bool isDealt =
rules.weapons.isMirror ? i == 0 && !rules.weapons.isChoice : !rules.weapons.isChoice || fighter.isBot;
if (isDealt)
picks_deal(game.rng, fighter.weaponLoadout, fighter.weaponSlots, WEAPON_COUNT,
rules.weapons.isDuplicates);
}
if (isTrinketPick)
{
std::fill(fighter.trinketLoadout, fighter.trinketLoadout + fighter.trinketSlots, -1);
bool isDealt = rules.trinkets.isMirror ? i == 0 && !rules.trinkets.isChoice
: !rules.trinkets.isChoice || fighter.isBot;
if (isDealt)
picks_deal(game.rng, fighter.trinketLoadout, fighter.trinketSlots, TRINKET_COUNT,
rules.trinkets.isDuplicates);
}
fighter.state_reset(game, health_get());
// Someone who left a queued game stays out of it.
fighter.isAlive &= !isGone[i];
}
if (isWeaponPick && rules.weapons.isMirror)
mirror_copy(game, false);
if (isTrinketPick && rules.trinkets.isMirror)
mirror_copy(game, true);
isWeaponPick &= rules.weapons.isChoice;
isTrinketPick &= rules.trinkets.isChoice;
if ((isWeaponPick || isTrinketPick) && game.local_count() == 1)
game.menu.choice_open(game);
std::fill(std::begin(isReady), std::end(isReady), false);
std::fill(std::begin(finishTimes), std::end(finishTimes), -1.0f);
clock = 0;
state = MATCH_COUNTDOWN;
// Choosing takes longer the more there is to choose: each kind being chosen adds time for every slot past its
// first.
countdown = READY_TIME;
if (isWeaponPick)
countdown += CHOICE_SLOT_TIME * (rules.weapons.slots - 1);
if (isTrinketPick)
countdown += CHOICE_SLOT_TIME * (rules.trinkets.slots - 1);
timer = countdown;
}
// Applies the edit in a fighter's controls, if any (see Edit). In a match only Choice picks count: an item goes into
// its slot as long as that kind is being chosen and the slot is in use, leaving any other slot it was in unless
// duplicates are allowed, and mirrored items go into every fighter's slot. The sandbox takes every kind of edit.
void Match::edit_take(Game& game, Fighter& fighter)
{
int code = fighter.controls.edit;
if (code < 0)
return;
fighter.controls.edit = -1;
Edit edit = edit_decode(code);
bool isSandbox = state == MATCH_PRACTICE;
bool isTrinket = edit.isTrinket;
int count = isTrinket ? fighter.trinketSlots : fighter.weaponSlots;
int items = isTrinket ? TRINKET_COUNT : WEAPON_COUNT;
int* loadout = isTrinket ? fighter.trinketLoadout : fighter.weaponLoadout;
int level = std::clamp(edit.value, 0, LEVEL_COUNT - 1);
switch (edit.type)
{
case EDIT_ITEM:
{
bool isPicking = isSandbox || (state == MATCH_COUNTDOWN && (isTrinket ? isTrinketPick : isWeaponPick));
int item = edit.value == EDIT_EMPTY ? -1 : edit.value;
if (!isPicking || edit.index >= count || item >= items)
return;
if (isSandbox)
{
fighter.item_set(isTrinket, edit.index, item);
return;
}
ItemRules& kind = isTrinket ? rules.trinkets : rules.weapons;
for (int i = 0; i < count && !kind.isDuplicates && item >= 0; i++)
loadout[i] = loadout[i] == item ? -1 : loadout[i];
loadout[edit.index] = item;
if (kind.isMirror)
for (int i = 0; i < game.fighterCount; i++)
std::copy(loadout, loadout + count,
isTrinket ? game.fighters[i].trinketLoadout : game.fighters[i].weaponLoadout);
return;
}
case EDIT_SLOT_LEVEL:
if (isSandbox && edit.index < count && loadout[edit.index] >= 0)
{
int& copy = (isTrinket ? fighter.trinketSlotLevels : fighter.weaponSlotLevels)[edit.index];
game.ui.level_step(game, copy, level - copy);
}
return;
case EDIT_ITEM_LEVEL:
{
if (!isSandbox || edit.index >= items)
return;
int& itemLevel = (isTrinket ? fighter.trinketItemLevels : fighter.weaponItemLevels)[edit.index];
game.ui.level_step(game, itemLevel, level - itemLevel);
for (int i = 0; i < SLOT_MAX; i++)
if (loadout[i] == edit.index)
(isTrinket ? fighter.trinketSlotLevels : fighter.weaponSlotLevels)[i] = itemLevel;
return;
}
case EDIT_SLOTS:
if (isSandbox)
fighter.slots_set(std::clamp(edit.index, 1, SLOT_MAX), std::clamp(edit.value, 1, SLOT_MAX));
return;
case EDIT_PVP:
if (isSandbox)
fighter.isPvp = edit.value != 0;
return;
case EDIT_COLOR:
{
bool isTaken = edit.value >= COLOR_COUNT;
for (int i = 0; i < game.fighterCount; i++)
isTaken |= i != fighter.index && colors[i] == edit.value;
if (isSandbox && !isTaken)
colors[fighter.index] = edit.value;
return;
}
}
}
// Copies the first fighter's loadout of one kind of item to every other fighter.
void Match::mirror_copy(Game& game, bool isTrinket)
{
Fighter& first = game.fighters[0];
for (int i = 1; i < game.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
if (isTrinket)
std::copy(first.trinketLoadout, first.trinketLoadout + SLOT_MAX, fighter.trinketLoadout);
else
std::copy(first.weaponLoadout, first.weaponLoadout + SLOT_MAX, fighter.weaponLoadout);
}
}
// The countdown is over: slots still empty after choosing are dealt at random (once, and shared, when mirrored), and
// the menu closes.
void Match::fight_start(Game& game)
{
for (int i = 0; i < game.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
picks_deal(game.rng, fighter.weaponLoadout, fighter.weaponSlots, WEAPON_COUNT, rules.weapons.isDuplicates);
picks_deal(game.rng, fighter.trinketLoadout, fighter.trinketSlots, TRINKET_COUNT, rules.trinkets.isDuplicates);
if (i == 0 && rules.weapons.isMirror)
mirror_copy(game, false);
if (i == 0 && rules.trinkets.isMirror)
mirror_copy(game, true);
}
if ((isWeaponPick || isTrinketPick) && game.local_count() > 0)
game.menu.isOpen = false;
isWeaponPick = false;
isTrinketPick = false;
clock = 0;
state = MATCH_FIGHT;
}
// The round ends with one fighter (or none) left standing, or when time runs out with whoever has the most health,
// drawn if that's shared.
void Match::deathmatch_update(Game& game)
{
int alive = 0;
winner = -1;
for (int i = 0; i < game.fighterCount; i++)
{
alive += game.fighters[i].isAlive;
winner = game.fighters[i].isAlive ? i : winner;
}
bool isTimeUp = time_limit_get() > 0 && clock >= time_limit_get();
if (alive > 1 && !isTimeUp)
return;
if (alive > 1)
{
float best = 0;
for (int i = 0; i < game.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
if (!fighter.isAlive || fighter.health < best)
continue;
winner = fighter.health == best ? -1 : i;
best = fighter.health;
}
}
if (winner >= 0)
wins[winner]++;
state = MATCH_ROUND_OVER;
timer = ROUND_OVER_TIME;
game.audio.play(SOUND_FINISH);
}
// Whether the coming deathmatch round decides the match whoever wins it: every fighter is a win short of taking it.
bool Match::is_final_round(Game& game)
{
for (int i = 0; i < game.fighterCount; i++)
if (wins[i] != rules.wins_get() - 1)
return false;
return rules.mode != MODE_RACE;
}
// A round's health: the rules' for the first, multiplied for each round after.
float Match::health_get()
{
return rules.health * math_pow(rules.healthMultiplier, round);
}
// A round's time limit in seconds, or 0 for none.
float Match::time_limit_get()
{
return rules.timer * SECONDS_PER_MINUTE;
}
// Whether hits only knock fighters about: any part of a race.
bool Match::is_racing()
{
return rules.mode == MODE_RACE && state != MATCH_PRACTICE;
}
// Sees fighters over the finish line, then ends the race once nobody is left running, a while after the winner
// finished, or when time runs out, when the racer furthest along wins if nobody has finished.
void Match::race_update(Game& game)
{
int running = 0;
for (int i = 0; i < game.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
if (fighter.isAlive && game.world.is_touching(fighter.body.box, TILE_FINISH))
{
finishTimes[i] = clock;
winner = winner < 0 ? i : winner;
fighter.isAlive = false;
fighter.charge_cancel(game);
for (Attack& attack : game.weapons.attacks)
attack.isDead |= attack.owner == i;
game.effects.burst(FINISH_BURST, fighter.center_get(), 0, game.color_get(i));
game.audio.play(SOUND_FINISH);
}
running += fighter.isAlive;
}
bool isGraceOver = winner >= 0 && clock > finishTimes[winner] + RACE_GRACE;
bool isTimeUp = time_limit_get() > 0 && clock > time_limit_get();
if (running > 0 && !isGraceOver && !isTimeUp)
return;
float furthest = -INFINITY;
for (int i = 0; i < game.fighterCount && finishTimes[std::max(0, winner)] < 0; i++)
{
float x = game.fighters[i].center_get().x;
if (game.fighters[i].isAlive && x > furthest)
{
winner = i;
furthest = x;
}
}
state = MATCH_OVER;
}
// Back to the sandbox: the arena and its dummy, the local fighter alone with its own slots, loadout and levels again.
void Match::sandbox_return(Game& game)
{
state = MATCH_PRACTICE;
isWeaponPick = false;
isTrinketPick = false;
game.fighterCount = 1;
game.world.sandbox_load();
game.bot.graph_build(game.world);
game.weapons.attacks.clear();
game.menu.isOpen = false;
Fighter& fighter = game.local_get();
fighter.slots_set(sandbox.weaponSlots, sandbox.trinketSlots);
std::copy(std::begin(sandbox.weaponLoadout), std::end(sandbox.weaponLoadout), fighter.weaponLoadout);
std::copy(std::begin(sandbox.trinketLoadout), std::end(sandbox.trinketLoadout), fighter.trinketLoadout);
std::copy(std::begin(sandbox.weaponItemLevels), std::end(sandbox.weaponItemLevels), fighter.weaponItemLevels);
std::copy(std::begin(sandbox.trinketItemLevels), std::end(sandbox.trinketItemLevels), fighter.trinketItemLevels);
std::copy(std::begin(sandbox.weaponSlotLevels), std::end(sandbox.weaponSlotLevels), fighter.weaponSlotLevels);
std::copy(std::begin(sandbox.trinketSlotLevels), std::end(sandbox.trinketSlotLevels), fighter.trinketSlotLevels);
fighter.state_reset(game, 1);
}
// Online, opens a fighter's queued game: an area on the ground big enough for everyone it's for, as near the fighter
// as there's room (on the floor below it, or the nearest stretch of floor to either side that's flat, clear overhead
// and not another queue's), with the fighter standing still at its left end, readied up.
void Match::queue_open(Game& game, int fighter, const Rules& wanted)
{
World& world = game.world;
Queue& queue = queues[fighter];
queue = {.rules = rules_clean(wanted), .isActive = true};
float width = queue.rules.players * QUEUE_SLOT_WIDTH + QUEUE_MARGIN * 2;
Vec2 size = world.size_get();
Fighter& queuer = game.fighters[fighter];
Vec2 feet = {queuer.body.box.x + queuer.body.box.w / 2, queuer.body.box.y + queuer.body.box.h};
// The top of the first floor at or below a point, if there's one.
auto floor_find = [&](float x, float from) {
for (float y = std::max(from, 1.0f); y < size.y; y++)
if (world.is_blocking_at({x, y}) && !world.is_blocking_at({x, y - 1}))
return y;
return -1.0f;
};
auto is_fitting = [&](float left, float floor) {
if (left < 0 || left + width > size.x || floor < QUEUE_HEIGHT)
return false;
for (float x = left; x <= left + width; x += QUEUE_PROBE_STEP)
{
if (!world.is_blocking_at({x, floor + 1}) || world.is_blocking_at({x, floor - 1}))
return false;
for (float up = QUEUE_PROBE_STEP; up <= QUEUE_HEIGHT; up += QUEUE_PROBE_STEP)
if (world.is_solid_at({x, floor - up}))
return false;
}
Rect area = {left, floor - QUEUE_HEIGHT, width, QUEUE_HEIGHT};
for (int i = 0; i < game.fighterCount; i++)
if (i != fighter && queues[i].isActive && is_rect_overlapping(area, queues[i].area))
return false;
return true;
};
float start = std::round(feet.x - QUEUE_MARGIN - QUEUE_SLOT_WIDTH / 2);
queue.area = {start, feet.y - QUEUE_HEIGHT, width, QUEUE_HEIGHT};
for (float offset = 0; offset < size.x; offset += QUEUE_PROBE_STEP)
{
bool isFound = false;
for (float left : {start + offset, start - offset})
{
float floor = floor_find(left + width / 2, std::round(feet.y - TILE_SIZE));
if (floor < 0 || !is_fitting(left, floor))
continue;
queue.area = {left, floor - QUEUE_HEIGHT, width, QUEUE_HEIGHT};
isFound = true;
break;
}
if (isFound)
break;
}
queuer.position_set(game, {queue.area.x + QUEUE_MARGIN + QUEUE_SLOT_WIDTH / 2, queue.area.y + queue.area.h});
std::erase_if(game.weapons.attacks, [&](Attack& attack) { return attack.owner == fighter; });
queuedIn[fighter] = fighter;
isQueueReady[fighter] = false;
game.audio.play(SOUND_EQUIP);
}
// Who's in a queued game's area: the queuer first, then the rest in fighter order. Returns how many.
int Match::queue_members_get(Game& game, int queuer, int* members)
{
int count = 0;
members[count++] = queuer;
for (int i = 0; i < game.fighterCount; i++)
if (i != queuer && queuedIn[i] == queuer)
members[count++] = i;
return count;
}
// Whether a queued game's area has everyone it's for.
bool Match::is_queue_full(Game& game, int queuer)
{
int members[MAX_FIGHTERS];
return queue_members_get(game, queuer, members) >= queues[queuer].rules.players;
}
// Whether a queued game is set to start: everyone in its area has readied up, and either it's full or it fills its
// empty places with bots (and isn't over full). The queuer readying is what starts one with bots.
bool Match::is_queue_set(Game& game, int queuer)
{
Queue& queue = queues[queuer];
if (!queue.isActive || isAway[queuer])
return false;
int members[MAX_FIGHTERS];
int count = queue_members_get(game, queuer, members);
bool isReadied = true;
for (int i = 0; i < count; i++)
isReadied &= isQueueReady[members[i]];
return isReadied && (count == queue.rules.players || (queue.rules.isBotFill && count < queue.rules.players));
}
// The queued game to start now, if any: one that's been set to for a moment (so a change of mind can stop it, and
// the Ready that set it is let go before its countdown, which would take it).
int Match::queue_startable_get(Game& game)
{
for (int queuer = 0; queuer < game.fighterCount; queuer++)
if (is_queue_set(game, queuer) && queues[queuer].readyTicks >= QUEUE_START_TICKS)
return queuer;
return -1;
}
// Moves the per-fighter queue state down a place from a fighter leaving, so it stays with its fighters.
void Match::queue_slots_shift(int from, int count)
{
for (int i = from; i + 1 < count; i++)
{
queues[i] = queues[i + 1];
isAway[i] = isAway[i + 1];
isGone[i] = isGone[i + 1];
}
if (count > 0)
{
queues[count - 1] = {};
isAway[count - 1] = false;
isGone[count - 1] = false;
}
std::fill(std::begin(queuedIn), std::end(queuedIn), -1);
std::fill(std::begin(isQueueReady), std::end(isQueueReady), false);
}
// Who stands in which queued game's area this tick (anyone away playing stands in none, and a queuer always in its
// own). Stepping out unreadies. A queuer can't move or attack (only crouch, and ready up), and anyone away does
// nothing.
void Match::queues_update(Game& game)
{
for (int i = 0; i < game.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
if (isAway[i])
{
fighter.controls = {};
queuedIn[i] = -1;
isQueueReady[i] = false;
continue;
}
int in = -1;
if (queues[i].isActive)
in = i;
Vec2 feet = {fighter.body.box.x + fighter.body.box.w / 2, fighter.body.box.y + fighter.body.box.h};
for (int j = 0; j < game.fighterCount && in < 0; j++)
{
Rect area = queues[j].area;
area = {area.x - QUEUE_FEET_SLACK, area.y, area.w + QUEUE_FEET_SLACK * 2, area.h + QUEUE_FEET_SLACK};
if (queues[j].isActive && !isAway[j] && fighter.isAlive && is_point_in_rect(feet, area))
in = j;
}
if (in != queuedIn[i])
isQueueReady[i] = false;
queuedIn[i] = in;
if (queues[i].isActive)
{
Uint16 kept = 1 << CONTROL_DOWN | 1 << CONTROL_READY;
fighter.controls.buttons &= kept;
}
}
for (int i = 0; i < game.fighterCount; i++)
queues[i].readyTicks = is_queue_set(game, i) ? queues[i].readyTicks + 1 : 0;
}
// In a queued game, how many players (not bots) haven't left it.
int Match::humans_left_get(Game& game)
{
int count = 0;
for (int i = 0; i < game.fighterCount; i++)
count += owners[i] >= 0 && !isGone[i];
return count;
}
// Online, in a queued game, a player leaving it (their Leave edit): they're out for good, which counts as a KO. With
// only one player left after that, they win the match outright.
void Match::leaves_take(Game& game)
{
bool isLeft = false;
for (int i = 0; i < game.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
if (fighter.controls.edit < 0 || edit_decode(fighter.controls.edit).type != EDIT_LEAVE)
continue;
fighter.controls.edit = -1;
if (isGone[i])
continue;
isGone[i] = true;
isForfeited[i] = state != MATCH_OVER;
isReady[i] = false;
fighter.isAlive = false;
std::erase_if(game.weapons.attacks, [&](Attack& attack) { return attack.owner == i; });
isLeft = true;
}
if (!isLeft || state == MATCH_OVER)
return;
int left = -1;
for (int i = 0; i < game.fighterCount; i++)
left = owners[i] >= 0 && !isGone[i] ? (left == -1 ? i : -2) : left;
if (left == -2)
return;
winner = left;
state = MATCH_OVER;
std::fill(std::begin(isReady), std::end(isReady), false);
game.audio.play(SOUND_FINISH);
}