Split the simulation from presentation into an SDL-free library

Sim holds one whole game (tick, snapshots, checksum) and talks to the screen only
through Cues, which the client's Scene plays and the server ignores. All drawing
moved to scene.cpp. Match takes this screen's wishes as request flags instead of
reading the menu, input and settings. The sim library builds without SDL and the
server links it, ready to run games itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shweetandClaude Opus 5.5 committed 2026-11-08 01:46:07 -05:00
1 parent 2007fb7edc
commit 465f75f6d7
38 files changed
+2225 -1965

No files matched your search

+155 -168
View File
@@ -1,7 +1,5 @@
#include "match.h"
#include "game.h"
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_scancode.h>
#include "sim.h"
#include <algorithm>
constexpr Burst FINISH_BURST = {.gravity = 200, .life = 1, .size = 3, .speed = 240, .count = 40};
@@ -70,82 +68,77 @@ 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)
void Match::update(Sim& sim)
{
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));
// Offline, the menu's Play starts a game, and leaving the results goes back to the sandbox.
bool isPlay = !isOnline && isPlayRequested;
bool isSandbox = !isOnline && state == MATCH_OVER && isSandboxRequested;
isPlayRequested = false;
isSandboxRequested = false;
if (state != MATCH_PRACTICE)
leaves_take(game);
leaves_take(sim);
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])
for (int i = 0; i < sim.fighterCount && state == MATCH_OVER && isOnline; i++)
if (sim.fighters[i].is_pressed(CONTROL_READY) && !isGone[i])
{
isReady[i] = !isReady[i];
game.audio.play(isReady[i] ? SOUND_EQUIP : SOUND_UNEQUIP);
sim.cues->sound_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++)
queues_update(sim);
for (int i = 0; i < sim.fighterCount && state == MATCH_PRACTICE; i++)
{
Fighter& fighter = game.fighters[i];
edit_take(game, fighter);
Fighter& fighter = sim.fighters[i];
edit_take(sim, 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);
sim.cues->sound_play(isQueueReady[i] ? SOUND_EQUIP : SOUND_UNEQUIP);
continue;
}
fighter.position_reset(game);
std::erase_if(game.weapons.attacks, [&](Attack& attack) { return attack.owner == i; });
fighter.position_reset(sim);
std::erase_if(sim.weapons.attacks, [&](Attack& attack) { return attack.owner == i; });
}
if (isPlay)
start(game);
start(sim);
else if (isSandbox && state == MATCH_OVER)
sandbox_return(game);
sandbox_return(sim);
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++)
for (int i = 0; i < sim.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
edit_take(game, fighter);
Fighter& fighter = sim.fighters[i];
edit_take(sim, 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);
fight_start(sim);
break;
}
case MATCH_FIGHT:
clock += TICK;
if (rules.mode == MODE_RACE)
race_update(game);
race_update(sim);
else
deathmatch_update(game);
deathmatch_update(sim);
break;
case MATCH_ROUND_OVER:
if (timer > 0)
@@ -157,93 +150,87 @@ void Match::update(Game& game)
break;
}
round += winner >= 0;
round_start(game, false);
round_start(sim, 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)
void Match::start(Sim& sim)
{
if (state == MATCH_PRACTICE)
sandbox = game.local_get();
sandbox = sim.fighters[0];
winner = -1;
game.fighterCount = rules.players;
int humans = sim.fighterCount;
sim.fighterCount = rules.players;
if (rules.mode == MODE_RACE)
game.world.course_build(game.rng);
sim.world.course_build(sim.rng);
else
game.world.arena_load();
game.bot.graph_build(game.world);
sim.world.arena_load();
sim.bot.graph_build(sim.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++)
// Offline, the fighters added beyond the sandbox's are bots. Online (a queued game), the bots are whoever no member
// plays, and everyone's colour was settled already.
for (int i = 0; i < sim.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
fighter.isBot = isOnline ? owners[i] < 0 : game.devices[i] == DEVICE_NONE || (i == game.local && fighter.isBot);
Fighter& fighter = sim.fighters[i];
fighter.isBot = isOnline ? owners[i] < 0 : i >= humans || 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);
colors_assign(sim);
sim.cues->match_start();
round_start(sim, 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)
// Another player on this screen joins the sandbox, starting from the first fighter's sandbox loadout and settings, with
// PvP off. Returns its fighter, or -1 when there's no room.
int Match::sandbox_join(Sim& sim)
{
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();
if (sim.fighterCount >= MAX_FIGHTERS)
return -1;
int index = sim.fighterCount++;
Fighter& fighter = sim.fighters[index];
Fighter& host = sim.fighters[0];
fighter = host;
fighter.init(sim, index);
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.isBot = false;
fighter.isPvp = false;
fighter.controls = {};
game.devices[index] = device;
game.input.devices[device].isClaimed = true;
colors_assign(game);
game.audio.play(SOUND_EQUIP);
colors_assign(sim);
sim.cues->sound_play(SOUND_EQUIP);
return index;
}
// 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)
void Match::sandbox_leave(Sim& sim, 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::vector<Attack>& attacks = sim.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++)
for (int i = index; i + 1 < sim.fighterCount; i++)
{
game.fighters[i] = game.fighters[i + 1];
game.fighters[i].index = i;
game.devices[i] = game.devices[i + 1];
sim.fighters[i] = sim.fighters[i + 1];
sim.fighters[i].index = i;
}
game.fighterCount--;
game.devices[game.fighterCount] = DEVICE_NONE;
colors_assign(game);
game.audio.play(SOUND_UNEQUIP);
sim.fighterCount--;
colors_assign(sim);
sim.cues->sound_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)
// ask for the same), then the rest in palette order. Only the first fighter asks for now (offline, this screen's).
void Match::colors_assign(Sim& sim)
{
int wanted[MAX_FIGHTERS];
std::fill(std::begin(wanted), std::end(wanted), COLOR_UNASSIGNED);
wanted[game.local] = game.settings.color_get();
wanted[0] = colorWanted;
bool isTaken[COLOR_COUNT] = {};
for (int i = 0; i < MAX_FIGHTERS; i++)
{
@@ -265,14 +252,14 @@ void Match::colors_assign(Game& game)
// 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)
void Match::round_start(Sim& sim, 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++)
sim.weapons.attacks.clear();
for (int i = 0; i < sim.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
Fighter& fighter = sim.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)
@@ -281,7 +268,7 @@ void Match::round_start(Game& game, bool isFirst)
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,
picks_deal(sim.rng, fighter.weaponLoadout, fighter.weaponSlots, WEAPON_COUNT,
rules.weapons.isDuplicates);
}
if (isTrinketPick)
@@ -290,21 +277,21 @@ void Match::round_start(Game& game, bool isFirst)
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,
picks_deal(sim.rng, fighter.trinketLoadout, fighter.trinketSlots, TRINKET_COUNT,
rules.trinkets.isDuplicates);
}
fighter.state_reset(game, health_get());
fighter.state_reset(sim, 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);
mirror_copy(sim, false);
if (isTrinketPick && rules.trinkets.isMirror)
mirror_copy(game, true);
mirror_copy(sim, true);
isWeaponPick &= rules.weapons.isChoice;
isTrinketPick &= rules.trinkets.isChoice;
if ((isWeaponPick || isTrinketPick) && game.local_count() == 1)
game.menu.choice_open(game);
if (isWeaponPick || isTrinketPick)
sim.cues->choice_open();
std::fill(std::begin(isReady), std::end(isReady), false);
std::fill(std::begin(finishTimes), std::end(finishTimes), -1.0f);
@@ -323,7 +310,7 @@ void Match::round_start(Game& game, bool isFirst)
// 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)
void Match::edit_take(Sim& sim, Fighter& fighter)
{
int code = fighter.controls.edit;
if (code < 0)
@@ -354,16 +341,16 @@ void Match::edit_take(Game& game, Fighter& fighter)
loadout[i] = loadout[i] == item ? -1 : loadout[i];
loadout[edit.index] = item;
if (kind.isMirror)
for (int i = 0; i < game.fighterCount; i++)
for (int i = 0; i < sim.fighterCount; i++)
std::copy(loadout, loadout + count,
isTrinket ? game.fighters[i].trinketLoadout : game.fighters[i].weaponLoadout);
isTrinket ? sim.fighters[i].trinketLoadout : sim.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);
level_step(sim, copy, level - copy);
}
return;
case EDIT_ITEM_LEVEL:
@@ -371,7 +358,7 @@ void Match::edit_take(Game& game, Fighter& fighter)
if (!isSandbox || edit.index >= items)
return;
int& itemLevel = (isTrinket ? fighter.trinketItemLevels : fighter.weaponItemLevels)[edit.index];
game.ui.level_step(game, itemLevel, level - itemLevel);
level_step(sim, itemLevel, level - itemLevel);
for (int i = 0; i < SLOT_MAX; i++)
if (loadout[i] == edit.index)
(isTrinket ? fighter.trinketSlotLevels : fighter.weaponSlotLevels)[i] = itemLevel;
@@ -388,7 +375,7 @@ void Match::edit_take(Game& game, Fighter& fighter)
case EDIT_COLOR:
{
bool isTaken = edit.value >= COLOR_COUNT;
for (int i = 0; i < game.fighterCount; i++)
for (int i = 0; i < sim.fighterCount; i++)
isTaken |= i != fighter.index && colors[i] == edit.value;
if (isSandbox && !isTaken)
colors[fighter.index] = edit.value;
@@ -398,12 +385,12 @@ void Match::edit_take(Game& game, Fighter& fighter)
}
// Copies the first fighter's loadout of one kind of item to every other fighter.
void Match::mirror_copy(Game& game, bool isTrinket)
void Match::mirror_copy(Sim& sim, bool isTrinket)
{
Fighter& first = game.fighters[0];
for (int i = 1; i < game.fighterCount; i++)
Fighter& first = sim.fighters[0];
for (int i = 1; i < sim.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
Fighter& fighter = sim.fighters[i];
if (isTrinket)
std::copy(first.trinketLoadout, first.trinketLoadout + SLOT_MAX, fighter.trinketLoadout);
else
@@ -413,20 +400,20 @@ void Match::mirror_copy(Game& game, bool isTrinket)
// 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)
void Match::fight_start(Sim& sim)
{
for (int i = 0; i < game.fighterCount; i++)
for (int i = 0; i < sim.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);
Fighter& fighter = sim.fighters[i];
picks_deal(sim.rng, fighter.weaponLoadout, fighter.weaponSlots, WEAPON_COUNT, rules.weapons.isDuplicates);
picks_deal(sim.rng, fighter.trinketLoadout, fighter.trinketSlots, TRINKET_COUNT, rules.trinkets.isDuplicates);
if (i == 0 && rules.weapons.isMirror)
mirror_copy(game, false);
mirror_copy(sim, false);
if (i == 0 && rules.trinkets.isMirror)
mirror_copy(game, true);
mirror_copy(sim, true);
}
if ((isWeaponPick || isTrinketPick) && game.local_count() > 0)
game.menu.isOpen = false;
if (isWeaponPick || isTrinketPick)
sim.cues->choice_close();
isWeaponPick = false;
isTrinketPick = false;
clock = 0;
@@ -435,14 +422,14 @@ void Match::fight_start(Game& game)
// 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)
void Match::deathmatch_update(Sim& sim)
{
int alive = 0;
winner = -1;
for (int i = 0; i < game.fighterCount; i++)
for (int i = 0; i < sim.fighterCount; i++)
{
alive += game.fighters[i].isAlive;
winner = game.fighters[i].isAlive ? i : winner;
alive += sim.fighters[i].isAlive;
winner = sim.fighters[i].isAlive ? i : winner;
}
bool isTimeUp = time_limit_get() > 0 && clock >= time_limit_get();
if (alive > 1 && !isTimeUp)
@@ -451,9 +438,9 @@ void Match::deathmatch_update(Game& game)
if (alive > 1)
{
float best = 0;
for (int i = 0; i < game.fighterCount; i++)
for (int i = 0; i < sim.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
Fighter& fighter = sim.fighters[i];
if (!fighter.isAlive || fighter.health < best)
continue;
winner = fighter.health == best ? -1 : i;
@@ -464,13 +451,13 @@ void Match::deathmatch_update(Game& game)
wins[winner]++;
state = MATCH_ROUND_OVER;
timer = ROUND_OVER_TIME;
game.audio.play(SOUND_FINISH);
sim.cues->sound_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)
bool Match::is_final_round(Sim& sim)
{
for (int i = 0; i < game.fighterCount; i++)
for (int i = 0; i < sim.fighterCount; i++)
if (wins[i] != rules.wins_get() - 1)
return false;
return rules.mode != MODE_RACE;
@@ -496,22 +483,22 @@ bool Match::is_racing()
// 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)
void Match::race_update(Sim& sim)
{
int running = 0;
for (int i = 0; i < game.fighterCount; i++)
for (int i = 0; i < sim.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
if (fighter.isAlive && game.world.is_touching(fighter.body.box, TILE_FINISH))
Fighter& fighter = sim.fighters[i];
if (fighter.isAlive && sim.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)
fighter.charge_cancel(sim);
for (Attack& attack : sim.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);
sim.cues->burst_spawn(FINISH_BURST, fighter.center_get(), 0, i);
sim.cues->sound_play(SOUND_FINISH);
}
running += fighter.isAlive;
}
@@ -521,10 +508,10 @@ void Match::race_update(Game& game)
if (running > 0 && !isGraceOver && !isTimeUp)
return;
float furthest = -INFINITY;
for (int i = 0; i < game.fighterCount && finishTimes[std::max(0, winner)] < 0; i++)
for (int i = 0; i < sim.fighterCount && finishTimes[std::max(0, winner)] < 0; i++)
{
float x = game.fighters[i].center_get().x;
if (game.fighters[i].isAlive && x > furthest)
float x = sim.fighters[i].center_get().x;
if (sim.fighters[i].isAlive && x > furthest)
{
winner = i;
furthest = x;
@@ -534,18 +521,18 @@ void Match::race_update(Game& game)
}
// 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)
void Match::sandbox_return(Sim& sim)
{
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;
sim.fighterCount = 1;
sim.world.sandbox_load();
sim.bot.graph_build(sim.world);
sim.weapons.attacks.clear();
sim.cues->sandbox_return();
Fighter& fighter = game.local_get();
Fighter& fighter = sim.fighters[0];
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);
@@ -553,20 +540,20 @@ void Match::sandbox_return(Game& game)
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);
fighter.state_reset(sim, 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)
void Match::queue_open(Sim& sim, int fighter, const Rules& wanted)
{
World& world = game.world;
World& world = sim.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];
Fighter& queuer = sim.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.
@@ -588,7 +575,7 @@ void Match::queue_open(Game& game, int fighter, const Rules& wanted)
return false;
}
Rect area = {left, floor - QUEUE_HEIGHT, width, QUEUE_HEIGHT};
for (int i = 0; i < game.fighterCount; i++)
for (int i = 0; i < sim.fighterCount; i++)
if (i != fighter && queues[i].isActive && is_rect_overlapping(area, queues[i].area))
return false;
return true;
@@ -610,40 +597,40 @@ void Match::queue_open(Game& game, int fighter, const Rules& wanted)
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; });
queuer.position_set(sim, {queue.area.x + QUEUE_MARGIN + QUEUE_SLOT_WIDTH / 2, queue.area.y + queue.area.h});
std::erase_if(sim.weapons.attacks, [&](Attack& attack) { return attack.owner == fighter; });
queuedIn[fighter] = fighter;
isQueueReady[fighter] = false;
game.audio.play(SOUND_EQUIP);
sim.cues->sound_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 Match::queue_members_get(Sim& sim, int queuer, int* members)
{
int count = 0;
members[count++] = queuer;
for (int i = 0; i < game.fighterCount; i++)
for (int i = 0; i < sim.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)
bool Match::is_queue_full(Sim& sim, int queuer)
{
int members[MAX_FIGHTERS];
return queue_members_get(game, queuer, members) >= queues[queuer].rules.players;
return queue_members_get(sim, 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)
bool Match::is_queue_set(Sim& sim, 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);
int count = queue_members_get(sim, queuer, members);
bool isReadied = true;
for (int i = 0; i < count; i++)
isReadied &= isQueueReady[members[i]];
@@ -652,10 +639,10 @@ bool Match::is_queue_set(Game& game, int queuer)
// 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)
int Match::queue_startable_get(Sim& sim)
{
for (int queuer = 0; queuer < game.fighterCount; queuer++)
if (is_queue_set(game, queuer) && queues[queuer].readyTicks >= QUEUE_START_TICKS)
for (int queuer = 0; queuer < sim.fighterCount; queuer++)
if (is_queue_set(sim, queuer) && queues[queuer].readyTicks >= QUEUE_START_TICKS)
return queuer;
return -1;
}
@@ -682,11 +669,11 @@ void Match::queue_slots_shift(int from, int count)
// 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)
void Match::queues_update(Sim& sim)
{
for (int i = 0; i < game.fighterCount; i++)
for (int i = 0; i < sim.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
Fighter& fighter = sim.fighters[i];
if (isAway[i])
{
fighter.controls = {};
@@ -698,7 +685,7 @@ void Match::queues_update(Game& game)
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++)
for (int j = 0; j < sim.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};
@@ -714,27 +701,27 @@ void Match::queues_update(Game& game)
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;
for (int i = 0; i < sim.fighterCount; i++)
queues[i].readyTicks = is_queue_set(sim, 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 Match::humans_left_get(Sim& sim)
{
int count = 0;
for (int i = 0; i < game.fighterCount; i++)
for (int i = 0; i < sim.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)
void Match::leaves_take(Sim& sim)
{
bool isLeft = false;
for (int i = 0; i < game.fighterCount; i++)
for (int i = 0; i < sim.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
Fighter& fighter = sim.fighters[i];
if (fighter.controls.edit < 0 || edit_decode(fighter.controls.edit).type != EDIT_LEAVE)
continue;
fighter.controls.edit = -1;
@@ -744,18 +731,18 @@ void Match::leaves_take(Game& game)
isForfeited[i] = state != MATCH_OVER;
isReady[i] = false;
fighter.isAlive = false;
std::erase_if(game.weapons.attacks, [&](Attack& attack) { return attack.owner == i; });
std::erase_if(sim.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++)
for (int i = 0; i < sim.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);
sim.cues->sound_play(SOUND_FINISH);
}