commit
2007fb7edc
200 files changed
+27710
No files matched your search
+761
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user