#pragma once #include "fighter.h" #include "protocol.h" struct Sim; constexpr int BEST_OF_MAX = 9; constexpr int PLAYERS_MAX = 4; constexpr int PLAYERS_MIN = 2; constexpr int TIMER_MAX = 10; // A queued game's area: room per player, a margin each end, its height, how finely the ground's checked for one, and // how far below its floor feet still count as in it. constexpr float QUEUE_SLOT_WIDTH = 24; constexpr float QUEUE_MARGIN = 6; constexpr float QUEUE_HEIGHT = 40; constexpr float QUEUE_PROBE_STEP = 4; constexpr float QUEUE_FEET_SLACK = 3; constexpr int QUEUE_START_TICKS = 60; constexpr int BASE_HEALTH_MAX = 10000; constexpr int BASE_HEALTH_MIN = 10; constexpr int PERIOD_MAX = 10; constexpr int PERIOD_MIN = 2; constexpr float MULTIPLIER_MAX = 5; constexpr float COUNTDOWN_TIME = 1; constexpr float READY_TIME = 15; constexpr float CHOICE_SLOT_TIME = 5; enum MatchMode { MODE_DEATHMATCH, MODE_RACE }; enum MatchState { MATCH_PRACTICE, MATCH_COUNTDOWN, MATCH_FIGHT, MATCH_ROUND_OVER, MATCH_OVER }; // When a match hands out items: once at its start, every round, or every period rounds. enum PickTiming { PICK_FIRST_ROUND, PICK_EVERY_ROUND, PICK_PERIOD }; // How a match hands out one kind of item: how many slots, dealt at random or chosen in the menu, on which rounds, the // level they start at (1 to 3), whether they gain one each round, whether every fighter mirrors one loadout, whether // a loadout can hold the same item more than once (copies always stack; see Fighter). struct ItemRules { int period = 2; int slots = SLOTS_DEFAULT; int startLevel = 1; int timing = PICK_FIRST_ROUND; bool isChoice = true; bool isDuplicates = false; bool isLevelUp = true; bool isMirror = false; bool operator==(const ItemRules&) const = default; }; // A match's settings, picked in the menu's Play tab. A deathmatch is best of an odd number of rounds, won by taking // more than half of them. Health is the first round's, multiplied by the multiplier for each round after. Timer is a // round's limit in minutes (0 for none). Single player fills every other slot with bots, and bot fill only decides it // in multiplayer. struct Rules { ItemRules trinkets; ItemRules weapons; float health = 250; float healthMultiplier = 2; int bestOf = 3; int mode = MODE_DEATHMATCH; int players = PLAYERS_MIN; int timer = 0; bool isBotFill = false; bool operator==(const Rules&) const = default; int wins_get() const { return bestOf / 2 + 1; } }; // A change a player makes to their own fighter from the menu, sent through its controls so that every machine applies // it on the same tick (and rollback replays it). Item puts an item (or EDIT_EMPTY) in a slot, and is the only kind a // match takes, as a Choice pick; the rest are the sandbox's: one copy's level, an item's level (with every copy of it // equipped), the slot counts (index weapons, value trinkets), PvP on or off, and online, the colour (if no one else has // it). enum EditType { EDIT_ITEM, EDIT_SLOT_LEVEL, EDIT_ITEM_LEVEL, EDIT_SLOTS, EDIT_PVP, EDIT_COLOR, // Online, leaving a queued game (forfeiting it if it's still on). EDIT_LEAVE }; struct Edit { int type = EDIT_ITEM; bool isTrinket = false; int index = 0; int value = 0; }; constexpr int EDIT_EMPTY = 31; constexpr int EDIT_FIELD_BITS = 5; constexpr int EDIT_FIELD_MASK = (1 << EDIT_FIELD_BITS) - 1; constexpr int EDIT_KIND_SHIFT = EDIT_FIELD_BITS * 2; constexpr int EDIT_TYPE_SHIFT = EDIT_KIND_SHIFT + 1; // An edit packed into one number for a fighter's controls (-1 is none). inline int edit_encode(Edit edit) { return (edit.value & EDIT_FIELD_MASK) | (edit.index & EDIT_FIELD_MASK) << EDIT_FIELD_BITS | edit.isTrinket << EDIT_KIND_SHIFT | edit.type << EDIT_TYPE_SHIFT; } inline Edit edit_decode(int code) { return {.type = code >> EDIT_TYPE_SHIFT, .isTrinket = (code >> EDIT_KIND_SHIFT & 1) != 0, .index = code >> EDIT_FIELD_BITS & EDIT_FIELD_MASK, .value = code & EDIT_FIELD_MASK}; } // Putting an item in a slot (or emptying it, for an item below 0). inline int pick_encode(bool isTrinket, int slot, int item) { return edit_encode( {.type = EDIT_ITEM, .isTrinket = isTrinket, .index = slot, .value = item < 0 ? EDIT_EMPTY : item}); } // The default rules for a mode: a best-of-three deathmatch for two, or a three minute race for four. Rules rules_get(int mode); Rules rules_clean(Rules rules); // Online, a game queued in the lobby's sandbox by one of its fighters: its rules, and the area on the ground where // whoever wants to play it gathers (the queuer standing at its left). struct Queue { Rules rules; Rect area; // How many ticks it's been ready to start for (it starts after QUEUE_START_TICKS). int readyTicks = 0; bool isActive = false; }; // Best-of-three deathmatch, or a single race, played by the rules. Practice is the sandbox with the dummy and the menu. // Each round opens with a long countdown to ready up in (cut short once every fighter is ready), at the round's level // and health. Countdown is how long it ran from: longer on rounds where fighters choose items. On the rounds items are // handed out, they're dealt at random, or chosen in the menu during the countdown with any slots left empty dealt at // random when it ends. A deathmatch round goes to the last fighter standing, or when the timer runs out to whoever has // the most health left. A drawn round is replayed. A race is won by the first over the finish line (finishers leave the // course), ending once everyone is over, a while after the winner, or when the timer runs out (then the one furthest // along wins if nobody finished). Forfeiting, or Escape after a match, goes back to the sandbox with the loadout and // levels from before it. Enter after a match plays again. class Match { public: Rules rules = rules_get(MODE_DEATHMATCH); MatchState state = MATCH_PRACTICE; float clock = 0; float countdown = READY_TIME; float finishTimes[MAX_FIGHTERS] = {}; float timer = 0; int round = 0; int winner = -1; int wins[MAX_FIGHTERS] = {}; int colors[MAX_FIGHTERS] = {0, 1, 2, 3}; // Online, which lobby member plays each fighter (-1 for none) and their name, kept with the game so every machine // (and a player joining) agrees who's who as fighters come and go. int owners[MAX_FIGHTERS] = {-1, -1, -1, -1}; char names[MAX_FIGHTERS][PLAYER_NAME_MAX + 1] = {}; bool isReady[MAX_FIGHTERS] = {}; // Online, in the lobby's sandbox: each fighter's queued game, which queue's area it's standing in (-1 for none), // whether it's readied up there, and whether it's away playing a queued game (faded, and left alone meanwhile). // In a queued game, whether each fighter has left it (out for good), and whether that was before it was over. Queue queues[MAX_FIGHTERS]; int queuedIn[MAX_FIGHTERS] = {-1, -1, -1, -1}; bool isQueueReady[MAX_FIGHTERS] = {}; bool isAway[MAX_FIGHTERS] = {}; bool isGone[MAX_FIGHTERS] = {}; bool isForfeited[MAX_FIGHTERS] = {}; // After a queued game, how many ticks everyone's been set to play it again for (see Session). int againTicks = 0; bool isTrinketPick = false; bool isWeaponPick = false; // Offline, what this screen asks of the next tick: the menu's Play, leaving the results for the sandbox, and the // colour its player picked. Online, members' wishes go through the session instead. int colorWanted = COLOR_UNASSIGNED; bool isOnline = false; bool isPlayRequested = false; bool isSandboxRequested = false; Fighter sandbox; void update(Sim& sim); void start(Sim& sim); void colors_assign(Sim& sim); void sandbox_return(Sim& sim); int sandbox_join(Sim& sim); void sandbox_leave(Sim& sim, int fighter); bool is_racing(); float health_get(); bool is_final_round(Sim& sim); float time_limit_get(); void queue_open(Sim& sim, int fighter, const Rules& rules); int queue_members_get(Sim& sim, int queuer, int* members); bool is_queue_full(Sim& sim, int queuer); int queue_startable_get(Sim& sim); bool is_queue_set(Sim& sim, int queuer); void queue_slots_shift(int from, int count); int humans_left_get(Sim& sim); private: void round_start(Sim& sim, bool isFirst); void fight_start(Sim& sim); void edit_take(Sim& sim, Fighter& fighter); void mirror_copy(Sim& sim, bool isTrinket); void deathmatch_update(Sim& sim); void race_update(Sim& sim); void queues_update(Sim& sim); void leaves_take(Sim& sim); };