Files
smack-a-brother/src/main.cpp
T
shweetandClaude Opus 5.5 2007fb7edc Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 01:30:33 -05:00

305 lines
10 KiB
C++

#include "game.h"
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <algorithm>
constexpr int WINDOW_SCALE = 2;
constexpr double MAX_FRAME_TIME = 0.25;
#ifdef SDL_PLATFORM_EMSCRIPTEN
constexpr SDL_WindowFlags WINDOW_FILL = SDL_WINDOW_FILL_DOCUMENT;
#else
constexpr SDL_WindowFlags WINDOW_FILL = 0;
#endif
constexpr float CAMERA_FOLLOW = 0.15f;
// Where the camera's corner wants to be: this screen's living fighters' middle (or the spectated fighter) in the
// middle, clamped to the world or centred when the world is smaller.
static Vec2 camera_target_get(Game& game)
{
Vec2 world = game.world.size_get();
Vec2 canvas = {CANVAS_WIDTH, CANVAS_HEIGHT};
Vec2 center = {};
int count = 0;
for (int i = 0; i < game.fighterCount; i++)
{
if (!game.is_local(i) || !game.fighters[i].isAlive)
continue;
center = center + game.fighters[i].center_get();
count++;
}
// Spectating, it follows whoever is being watched instead.
if (game.ui.spectated >= 0)
{
center = game.fighters[game.ui.spectated].center_get();
count = 1;
}
Vec2 target = (count > 0 ? center * (1.0f / count) : game.local_get().center_get()) - canvas * 0.5f;
target.x = world.x > canvas.x ? std::clamp(target.x, 0.0f, world.x - canvas.x) : (world.x - canvas.x) / 2;
target.y = world.y > canvas.y ? std::clamp(target.y, 0.0f, world.y - canvas.y) : (world.y - canvas.y) / 2;
return target;
}
// Eases the camera towards its target.
static void camera_update(Game& game)
{
game.camera = game.camera + (camera_target_get(game) - game.camera) * CAMERA_FOLLOW;
}
// One simulation tick: everything rollback saves and replays. Fighters hold still through a round's countdown. The
// menu is this screen's alone, so it updates before the tick (see frame_run), not in it.
void game_tick(Game& game)
{
game.match.update(game);
if (game.match.state != MATCH_COUNTDOWN)
game.bot.update(game);
for (int i = 0; i < game.fighterCount && game.match.state != MATCH_COUNTDOWN; i++)
game.fighters[i].update(game);
game.weapons.update(game);
game.world.update(game);
game.input.tick_end();
}
// Steps what only this screen sees, once per live tick: particles, HUD fades and the following camera.
static void presentation_update(Game& game)
{
game.effects.update();
game.ui.update(game);
game.sprites.update(game);
game.cameraPrevious = game.camera;
camera_update(game);
}
// Draws the world part way between the last two ticks. Fighters and attacks are swapped
// for blended copies while drawing, then the real simulation state is put back.
static void game_draw(Game& game)
{
Rect boxes[MAX_FIGHTERS];
std::vector<Attack> attacks = game.weapons.attacks;
for (int i = 0; i < game.fighterCount; i++)
{
boxes[i] = game.fighters[i].body.box;
game.fighters[i].blend(game.alpha);
}
game.weapons.blend(game.alpha);
Vec2 camera = vec2_lerp(game.cameraPrevious, game.camera, game.alpha);
if (game.settings.isShaking)
camera = camera + game.effects.shake_get();
game.renderer.camera = {std::round(camera.x), std::round(camera.y)};
game.world.draw(game);
game.bot.draw(game);
game.effects.afterimage_draw(game);
game.weapons.blast_draw(game);
for (int i = 0; i < game.fighterCount; i++)
game.fighters[i].draw(game);
game.weapons.draw(game);
if (game.settings.isCollisionShown)
game.weapons.collision_draw(game);
game.effects.draw(game);
game.ui.draw(game);
game.renderer.present();
for (int i = 0; i < game.fighterCount; i++)
game.fighters[i].body.box = boxes[i];
game.weapons.attacks = attacks;
}
// Sets up everything the simulation needs: the world, the bot graph, fighters, weapons, trinkets and the seeded
// generator (RPG_SEED, every number its own odd state, or the clock). Without a renderer the sprites come back empty,
// which the simulation never needs.
static void simulation_init(Game& game)
{
game.world.init(game);
game.bot.init(game);
for (int i = 0; i < MAX_FIGHTERS; i++)
game.fighters[i].init(game, i);
game.local_get().isBot = SDL_getenv("RPG_BOT_LOCAL");
game.rollback.init();
const char* seed = SDL_getenv("RPG_SEED");
game.rng.state = seed ? (Uint32)SDL_atoi(seed) * 2 + 1 : (Uint32)SDL_GetPerformanceCounter() | 1;
}
// The frame clock: when the last frame began, and the time not yet simulated.
struct FrameClock
{
Uint64 previous = 0;
double accumulator = 0;
};
static Game game;
static FrameClock frameClock;
// One frame: input, as many fixed ticks as the time since the last frame allows, then drawing (unless it's not to be
// drawn). Returns whether to keep going.
static bool frame_run(bool isDrawn = true)
{
Uint64 now = SDL_GetPerformanceCounter();
frameClock.accumulator +=
std::min((now - frameClock.previous) / (double)SDL_GetPerformanceFrequency(), MAX_FRAME_TIME);
frameClock.previous = now;
game.input.poll(game);
game.net.update();
game.chat.update(game);
Session& session = game.session;
for (; frameClock.accumulator >= TICK; frameClock.accumulator -= TICK)
{
// Online, this screen's own work happens in the game it shows (the arena while it plays or watches a queued
// game), and the session runs the shared games, taking this screen's controls; otherwise each fighter on this
// screen takes its device's. Watching, they do nothing.
session.view_enter(game);
game.menu.update(game);
if (session.is_wanted(game))
{
Fighter& own = game.local_get();
bool isHeld = game.menu.isOpen || game.chat.isOpen || session.is_watching();
Controls controls =
game.input.controls_get(DEVICE_KEYBOARD, game.camera, own.center_get(), own.slot, isHeld);
session.view_leave(game);
session.update(game, controls);
game.input.tick_end();
session.view_enter(game);
if (session.isViewChanged)
{
session.isViewChanged = false;
game.camera = game.cameraPrevious = camera_target_get(game);
}
presentation_update(game);
session.view_leave(game);
continue;
}
session.view_leave(game);
for (int i = 0; i < game.fighterCount; i++)
{
Fighter& fighter = game.fighters[i];
bool isMenuOpen = game.menu.isOpen && i == game.local;
if (game.is_local(i))
fighter.controls = game.input.controls_get(game.devices[i], game.camera, fighter.center_get(),
fighter.slot, isMenuOpen);
}
game.rollback.update(game);
presentation_update(game);
}
game.settings.update(game);
game.alpha = frameClock.accumulator / TICK;
if (isDrawn)
{
session.view_enter(game);
game_draw(game);
session.view_leave(game);
}
return !game.input.isQuit;
}
#ifdef SDL_PLATFORM_EMSCRIPTEN
static void frame_callback()
{
frame_run();
}
// A hidden tab gets no frames from the browser, but online the shared game can't wait for it: everyone else would
// stall. So a worker's timer (which the browser doesn't hold back the way it does a hidden page's own) calls this
// every 16 ms while the tab is hidden, to keep the game running, undrawn.
extern "C" EMSCRIPTEN_KEEPALIVE void frame_hidden()
{
if (game.session.is_wanted(game))
frame_run(false);
}
#endif
// Runs the game in a window (on the web, filling the page and driven by the browser's frames), or with "--balance N"
// plays N bot matches headless and prints the results ("--balance N race" plays races).
// The lobby to join on starting, if any: from the page's ?lobby= in the browser, or --join on the command line.
static std::string lobby_requested(int argc, char** argv)
{
#ifdef SDL_PLATFORM_EMSCRIPTEN
char* code = (char*)EM_ASM_PTR({
var code = new URLSearchParams(location.search).get('lobby');
return code ? stringToNewUTF8(code) : 0;
});
std::string lobby = code ? code : "";
free(code);
return lobby;
#else
for (int i = 1; i + 1 < argc; i++)
if (SDL_strcmp(argv[i], "--join") == 0)
return argv[i + 1];
return "";
#endif
}
int main(int argc, char** argv)
{
int balanceMatches = argc > 2 && SDL_strcmp(argv[1], "--balance") == 0 ? SDL_atoi(argv[2]) : 0;
if (balanceMatches > 0)
{
simulation_init(game);
bool isRace = argc > 3 && SDL_strcmp(argv[3], "race") == 0;
game.balance.matches_run(game, balanceMatches, isRace ? MODE_RACE : MODE_DEATHMATCH);
return 0;
}
if (!SDL_Init(SDL_INIT_VIDEO))
{
SDL_Log("Failed to init SDL: %s", SDL_GetError());
return 1;
}
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD))
SDL_Log("Gamepads unavailable: %s", SDL_GetError());
game.settings.load(game);
game.window =
SDL_CreateWindow(GAME_NAME, CANVAS_WIDTH * WINDOW_SCALE, CANVAS_HEIGHT * WINDOW_SCALE,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY | WINDOW_FILL);
if (!game.window || !game.renderer.init(game.window))
{
SDL_Log("Failed to create window: %s", SDL_GetError());
return 1;
}
game.text.init(game.renderer);
game.audio.init();
game.ui.init(game);
game.sprites.init(game);
simulation_init(game);
game.renderer.atlas_upload();
game.settings.apply(game);
game.settings.restore(game);
std::string lobby = lobby_requested(argc, argv);
if (!lobby.empty())
{
game.net.lobby_join(game, lobby.c_str());
game.menu.online_open();
}
game.camera = camera_target_get(game);
game.cameraPrevious = game.camera;
SDL_HideCursor();
frameClock.previous = SDL_GetPerformanceCounter();
#ifdef SDL_PLATFORM_EMSCRIPTEN
// The browser calls each frame from here on; main returns but the game lives on.
emscripten_set_main_loop(frame_callback, 0, false);
EM_ASM({
var ticker = new Worker(URL.createObjectURL(
new Blob(["setInterval(function() { postMessage(0); }, 16);"], {type : "text/javascript"})));
ticker.onmessage = function()
{
if (document.hidden)
_frame_hidden();
};
});
return 0;
#endif
while (frame_run())
{
}
game.settings.save(game);
game.net.shutdown();
SDL_Quit();
return 0;
}