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:
1 parent
2007fb7edc
commit
465f75f6d7
38 files changed
+2225
-1965
No files matched your search
+4
-1
@@ -37,6 +37,8 @@ add_library(simdjson STATIC external/simdjson/singleheader/simdjson.cpp)
|
||||
target_include_directories(simdjson SYSTEM PUBLIC external/simdjson/singleheader)
|
||||
target_compile_definitions(simdjson PUBLIC SIMDJSON_EXCEPTIONS=0)
|
||||
|
||||
include(src/sim.cmake)
|
||||
|
||||
# Networking: WebRTC data channels between players and a WebSocket to the server. On desktop it's
|
||||
# libdatachannel (static, data channels only, no media); in the browser, datachannel-wasm gives the same API over the
|
||||
# browser's own WebRTC.
|
||||
@@ -54,9 +56,10 @@ else()
|
||||
endif()
|
||||
|
||||
file(GLOB SOURCES CONFIGURE_DEPENDS src/*.cpp)
|
||||
list(REMOVE_ITEM SOURCES ${SIM_SOURCES})
|
||||
add_executable(smack_a_brother ${SOURCES})
|
||||
target_include_directories(smack_a_brother SYSTEM PRIVATE external/stb)
|
||||
target_link_libraries(smack_a_brother PRIVATE SDL3::SDL3-static simdjson ${NETWORK_LIBRARY})
|
||||
target_link_libraries(smack_a_brother PRIVATE sim SDL3::SDL3-static simdjson ${NETWORK_LIBRARY})
|
||||
set_target_properties(smack_a_brother PROPERTIES OUTPUT_NAME smack-a-brother)
|
||||
|
||||
file(CREATE_LINK ${CMAKE_SOURCE_DIR}/resources ${CMAKE_BINARY_DIR}/resources SYMBOLIC)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# The server. Built as part of the game's desktop build, or on its own (for the VPS, without SDL):
|
||||
# The server, which runs games with the simulation library. Built as part of the game's desktop build, or on its own
|
||||
# (for the VPS, without SDL):
|
||||
# cmake -S server -B build-server -DCMAKE_BUILD_TYPE=Release && cmake --build build-server
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
project(smack_a_brother_server LANGUAGES C CXX)
|
||||
@@ -13,6 +14,9 @@ if(NOT TARGET datachannel-static)
|
||||
set(NO_TESTS ON CACHE BOOL "" FORCE)
|
||||
add_subdirectory(${EXTERNAL}/libdatachannel ${CMAKE_BINARY_DIR}/libdatachannel EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
if(NOT TARGET sim)
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/../src/sim.cmake)
|
||||
endif()
|
||||
if(NOT TARGET simdjson)
|
||||
add_library(simdjson STATIC ${EXTERNAL}/simdjson/singleheader/simdjson.cpp)
|
||||
target_include_directories(simdjson SYSTEM PUBLIC ${EXTERNAL}/simdjson/singleheader)
|
||||
@@ -21,5 +25,5 @@ endif()
|
||||
|
||||
add_executable(smack_a_brother_server server.cpp)
|
||||
target_include_directories(smack_a_brother_server PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../src)
|
||||
target_link_libraries(smack_a_brother_server PRIVATE LibDataChannel::LibDataChannelStatic simdjson)
|
||||
target_link_libraries(smack_a_brother_server PRIVATE sim LibDataChannel::LibDataChannelStatic simdjson)
|
||||
set_target_properties(smack_a_brother_server PROPERTIES OUTPUT_NAME smack-a-brother-server)
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
#include "audio.h"
|
||||
#include "common.h"
|
||||
#include "renderer.h"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <stb_vorbis.c>
|
||||
@@ -132,7 +132,7 @@ int Audio::play(SoundId id)
|
||||
return -1;
|
||||
Sound& sound = sounds[id];
|
||||
Uint64 now = SDL_GetTicksNS();
|
||||
if (!device || sound.frames <= 0 || isMuted || now - sound.played < SOUND_GAP)
|
||||
if (!device || sound.frames <= 0 || now - sound.played < SOUND_GAP)
|
||||
return -1;
|
||||
sound.played = now;
|
||||
|
||||
|
||||
@@ -113,8 +113,6 @@ struct Sound
|
||||
class Audio
|
||||
{
|
||||
public:
|
||||
bool isMuted = false;
|
||||
|
||||
void init();
|
||||
int play(SoundId id);
|
||||
void stop(int voice);
|
||||
|
||||
+21
-23
@@ -44,7 +44,7 @@ static void tallies_print(const char* title, Tally* tallies, int count)
|
||||
// (The bytes themselves can differ between builds, in the padding between fields, which nothing reads.)
|
||||
static void state_test(Game& game, bool isFirst)
|
||||
{
|
||||
std::string bytes = state_save(game);
|
||||
std::string bytes = state_save(game.sim);
|
||||
const char* imported = SDL_getenv("RPG_STATE_IMPORT_HEX");
|
||||
if (isFirst && SDL_getenv("RPG_STATE_PRINT"))
|
||||
{
|
||||
@@ -67,13 +67,13 @@ static void state_test(Game& game, bool isFirst)
|
||||
bytes += (char)(value(digit[0]) << 4 | value(digit[1]));
|
||||
}
|
||||
}
|
||||
for (Fighter& fighter : game.fighters)
|
||||
for (Fighter& fighter : game.sim.fighters)
|
||||
fighter = Fighter{};
|
||||
game.weapons.attacks.clear();
|
||||
game.world.dummies.clear();
|
||||
if (!state_load(game, bytes))
|
||||
game.sim.weapons.attacks.clear();
|
||||
game.sim.world.dummies.clear();
|
||||
if (!state_load(game.sim, bytes))
|
||||
SDL_Log("State test: the saved state didn't load");
|
||||
else if (!imported && state_save(game) != bytes)
|
||||
else if (!imported && state_save(game.sim) != bytes)
|
||||
SDL_Log("State test: the state saved after loading differs");
|
||||
}
|
||||
|
||||
@@ -86,9 +86,7 @@ void Balance::matches_run(Game& game, int count, MatchMode mode)
|
||||
for (int i = 0; i < TRINKET_COUNT; i++)
|
||||
trinkets[i].name = TRINKET_DEFS[i].name;
|
||||
|
||||
game.audio.isMuted = true;
|
||||
game.effects.isMuted = true;
|
||||
for (Fighter& fighter : game.fighters)
|
||||
for (Fighter& fighter : game.sim.fighters)
|
||||
fighter.isBot = true;
|
||||
int limit = MATCH_TIME_LIMIT / TICK;
|
||||
int timeouts = 0;
|
||||
@@ -104,38 +102,38 @@ void Balance::matches_run(Game& game, int count, MatchMode mode)
|
||||
for (int match = 0; match < count; match++)
|
||||
{
|
||||
// Items dealt at random and kept at their start level, so a weapon's win rate measures the weapon.
|
||||
game.match.rules = rules_get(mode);
|
||||
for (ItemRules* items : {&game.match.rules.weapons, &game.match.rules.trinkets})
|
||||
game.sim.match.rules = rules_get(mode);
|
||||
for (ItemRules* items : {&game.sim.match.rules.weapons, &game.sim.match.rules.trinkets})
|
||||
{
|
||||
items->isChoice = false;
|
||||
items->isLevelUp = false;
|
||||
}
|
||||
game.match.start(game);
|
||||
game.sim.match.start(game.sim);
|
||||
int tick = 0;
|
||||
for (; game.match.state != MATCH_OVER && tick < limit; tick++)
|
||||
for (; game.sim.match.state != MATCH_OVER && tick < limit; tick++)
|
||||
{
|
||||
game_tick(game);
|
||||
hash = (hash ^ game.rollback.checksum_get(game)) * HASH_PRIME;
|
||||
hash = (hash ^ game.sim.checksum_get()) * HASH_PRIME;
|
||||
if (tick == STATE_TEST_TICK && isStateTest)
|
||||
state_test(game, match == 0);
|
||||
}
|
||||
ticks += tick;
|
||||
if (count >= PROGRESS_STEPS && (match + 1) % (count / PROGRESS_STEPS) == 0)
|
||||
SDL_Log("%d / %d matches", match + 1, count);
|
||||
bool isStalled = game.match.state != MATCH_OVER;
|
||||
bool isStalled = game.sim.match.state != MATCH_OVER;
|
||||
timeouts += isStalled;
|
||||
racers += game.fighterCount;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
finishes += game.match.finishTimes[i] >= 0;
|
||||
if (game.match.winner >= 0)
|
||||
racers += game.sim.fighterCount;
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
finishes += game.sim.match.finishTimes[i] >= 0;
|
||||
if (game.sim.match.winner >= 0)
|
||||
{
|
||||
wins++;
|
||||
winTimes += game.match.finishTimes[game.match.winner];
|
||||
winTimes += game.sim.match.finishTimes[game.sim.match.winner];
|
||||
}
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
{
|
||||
Fighter& fighter = game.fighters[i];
|
||||
bool isWinner = i == game.match.winner;
|
||||
Fighter& fighter = game.sim.fighters[i];
|
||||
bool isWinner = i == game.sim.match.winner;
|
||||
for (int weapon : fighter.weaponLoadout)
|
||||
{
|
||||
if (weapon < 0)
|
||||
|
||||
+54
-86
@@ -1,7 +1,7 @@
|
||||
#include "bot.h"
|
||||
#include "game.h"
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
#include "sim.h"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
|
||||
constexpr int PURSUIT_TICKS = 60;
|
||||
constexpr int SURFACE_CLEARANCE = 2;
|
||||
@@ -28,11 +28,9 @@ constexpr float GRAPPLE_COST = 20;
|
||||
constexpr float GRAPPLE_GRIP = 4;
|
||||
constexpr float GRAPPLE_RISE_MAX = 200;
|
||||
constexpr float GRAPPLE_TIME = 2.5f;
|
||||
constexpr float GOAL_SPAN = 0.5f;
|
||||
constexpr float JUMP_COST = 40;
|
||||
constexpr float JUMP_REACH = 0.8f;
|
||||
constexpr float JUMP_RISE_MAX = 64;
|
||||
constexpr float LINK_WIDTH = 1;
|
||||
constexpr float RAY_STEP = 4;
|
||||
constexpr float RECOVER_AIM = 48;
|
||||
constexpr float RECOVER_TIME = 0.25f;
|
||||
@@ -41,7 +39,6 @@ constexpr float STRAFE_DISTANCE = 40;
|
||||
constexpr float STUCK_SPEED = 5;
|
||||
constexpr float STUCK_TIME = 0.5f;
|
||||
constexpr float SEAM_TOLERANCE = 2;
|
||||
constexpr float SURFACE_WIDTH = 2;
|
||||
constexpr float TAKEOFF_INSET = TILE_SIZE;
|
||||
constexpr float THREAT_SPEED = 50;
|
||||
constexpr float WHIP_SWEET_SPOT = 8;
|
||||
@@ -70,12 +67,12 @@ static int grapple_slot_get(Fighter& fighter)
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool is_sight_clear(Game& game, Vec2 from, Vec2 to)
|
||||
static bool is_sight_clear(Sim& sim, Vec2 from, Vec2 to)
|
||||
{
|
||||
Vec2 delta = to - from;
|
||||
int steps = vec2_length(delta) / RAY_STEP;
|
||||
for (int i = 1; i < steps; i++)
|
||||
if (game.world.is_blocking_at(from + delta * ((float)i / steps)))
|
||||
if (sim.world.is_blocking_at(from + delta * ((float)i / steps)))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
@@ -123,14 +120,14 @@ static void seen_add(BotState& state, Vec2 position)
|
||||
|
||||
// Where a launched fighter will come to rest: its flight played forward with the real launch physics, bouncing off
|
||||
// whatever it meets, until it slows down or the look-ahead runs out.
|
||||
static Vec2 landing_predict(Game& game, Fighter& fighter)
|
||||
static Vec2 landing_predict(Sim& sim, Fighter& fighter)
|
||||
{
|
||||
Body body = fighter.body;
|
||||
for (int i = 0; i < PURSUIT_TICKS && vec2_length(body.velocity) >= LAUNCH_END_SPEED; i++)
|
||||
{
|
||||
Vec2 before = body.velocity * (1 - KNOCK_DECAY * TICK) + Vec2{0, GRAVITY * TICK};
|
||||
body.velocity = before;
|
||||
game.world.body_move(body);
|
||||
sim.world.body_move(body);
|
||||
if (body.velocity.x == 0 && std::abs(before.x) > BOUNCE_MIN)
|
||||
body.velocity.x = -before.x * BOUNCE_KEEP;
|
||||
if (body.velocity.y == 0 && std::abs(before.y) > BOUNCE_MIN)
|
||||
@@ -141,11 +138,11 @@ static Vec2 landing_predict(Game& game, Fighter& fighter)
|
||||
|
||||
// Launched toward a slam, grab whatever softens it and use it toward the wall: a hammer swing to rebound off it,
|
||||
// flames to fly into and brake, or a shot whose recoil pushes back. Reports whether it's doing so.
|
||||
static bool is_recovering(Game& game, Fighter& fighter, Controls& controls)
|
||||
static bool is_recovering(Sim& sim, Fighter& fighter, Controls& controls)
|
||||
{
|
||||
Vec2 velocity = fighter.body.velocity;
|
||||
Vec2 center = fighter.center_get();
|
||||
bool isHeading = !is_sight_clear(game, center, center + velocity * RECOVER_TIME);
|
||||
bool isHeading = !is_sight_clear(sim, center, center + velocity * RECOVER_TIME);
|
||||
if (!fighter.isLaunched || vec2_length(velocity) < SLAM_SPEED || !isHeading)
|
||||
return false;
|
||||
|
||||
@@ -175,10 +172,10 @@ static bool is_recovering(Game& game, Fighter& fighter, Controls& controls)
|
||||
}
|
||||
|
||||
// Whether an enemy attack in flight is on course to pass through the fighter soon.
|
||||
static bool is_threatened(Game& game, Fighter& fighter)
|
||||
static bool is_threatened(Sim& sim, Fighter& fighter)
|
||||
{
|
||||
Vec2 center = fighter.center_get();
|
||||
for (Attack& attack : game.weapons.attacks)
|
||||
for (Attack& attack : sim.weapons.attacks)
|
||||
{
|
||||
float speed = vec2_length(attack.velocity);
|
||||
if (attack.owner == fighter.index || attack.isDead || attack.isAttached || speed < THREAT_SPEED)
|
||||
@@ -191,13 +188,13 @@ static bool is_threatened(Game& game, Fighter& fighter)
|
||||
return false;
|
||||
}
|
||||
|
||||
void Bot::init(Game& game)
|
||||
void Bot::init(World& world)
|
||||
{
|
||||
isDrawn = SDL_getenv("RPG_BOT_DEBUG");
|
||||
const char* chosen = SDL_getenv("RPG_BOT_DIFFICULTY");
|
||||
isDrawn = std::getenv("RPG_BOT_DEBUG");
|
||||
const char* chosen = std::getenv("RPG_BOT_DIFFICULTY");
|
||||
if (chosen)
|
||||
level = std::clamp(SDL_atoi(chosen), 0, BOT_LEVEL_COUNT - 1);
|
||||
graph_build(game.world);
|
||||
level = std::clamp(std::atoi(chosen), 0, BOT_LEVEL_COUNT - 1);
|
||||
graph_build(world);
|
||||
}
|
||||
|
||||
void Bot::graph_build(World& world)
|
||||
@@ -410,37 +407,37 @@ bool Bot::is_path_found(BotState& state, int from, int to, bool isGrappler)
|
||||
}
|
||||
|
||||
// Writes every living bot's controls for this tick, re-thinking on its interval.
|
||||
void Bot::update(Game& game)
|
||||
void Bot::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];
|
||||
BotState& state = states[i];
|
||||
if (!fighter.isBot || !fighter.isAlive)
|
||||
continue;
|
||||
|
||||
bool isRacing = game.match.is_racing();
|
||||
bool isRacing = sim.match.is_racing();
|
||||
state.thinkTimer -= TICK;
|
||||
if (state.thinkTimer <= 0 && isRacing)
|
||||
race_think(fighter, state);
|
||||
else if (state.thinkTimer <= 0)
|
||||
think(game, fighter, state);
|
||||
think(sim, fighter, state);
|
||||
if (state.target >= 0)
|
||||
seen_add(state, game.fighters[state.target].center_get());
|
||||
seen_add(state, sim.fighters[state.target].center_get());
|
||||
Controls controls = {.cursor = fighter.controls.cursor};
|
||||
if (is_recovering(game, fighter, controls))
|
||||
if (is_recovering(sim, fighter, controls))
|
||||
{
|
||||
fighter.controls = controls;
|
||||
continue;
|
||||
}
|
||||
move_steer(game, fighter, state, controls);
|
||||
move_steer(sim, fighter, state, controls);
|
||||
if (isRacing && !state.isGrappling)
|
||||
block_attack(game, fighter, state, controls);
|
||||
block_attack(sim, fighter, state, controls);
|
||||
else if (!state.isGrappling)
|
||||
attack_aim(game, fighter, state, controls);
|
||||
attack_aim(sim, fighter, state, controls);
|
||||
wings_steer(fighter, state, controls);
|
||||
if (!state.isGrappling && !isRacing)
|
||||
trinket_steer(game, fighter, state, controls);
|
||||
trinket_steer(sim, fighter, state, controls);
|
||||
fighter.controls = controls;
|
||||
}
|
||||
}
|
||||
@@ -449,10 +446,10 @@ void Bot::update(Game& game)
|
||||
// strafe and alertness, and where to stand: out of reach of an enemy holding a full charge (unless cornered),
|
||||
// drifting about when in
|
||||
// band with a clear shot, otherwise at band distance from the target on the surface under it.
|
||||
void Bot::think(Game& game, Fighter& fighter, BotState& state)
|
||||
void Bot::think(Sim& sim, Fighter& fighter, BotState& state)
|
||||
{
|
||||
const BotDifficulty& skill = BOT_DIFFICULTIES[level];
|
||||
Rng& rng = game.rng;
|
||||
Rng& rng = sim.rng;
|
||||
int previous = state.target;
|
||||
state.thinkTimer = skill.thinkInterval;
|
||||
state.aimError = rng.random_get(-skill.aimError, skill.aimError);
|
||||
@@ -460,11 +457,11 @@ void Bot::think(Game& game, Fighter& fighter, BotState& state)
|
||||
state.strafe = (int)rng.random_get(0, 3) - 1;
|
||||
state.target = -1;
|
||||
Vec2 center = fighter.center_get();
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < sim.fighterCount; i++)
|
||||
{
|
||||
Fighter& other = game.fighters[i];
|
||||
Fighter& other = sim.fighters[i];
|
||||
bool isCloser = state.target < 0 || vec2_length(other.center_get() - center) <
|
||||
vec2_length(game.fighters[state.target].center_get() - center);
|
||||
vec2_length(sim.fighters[state.target].center_get() - center);
|
||||
if (i != fighter.index && other.isAlive && isCloser)
|
||||
state.target = i;
|
||||
}
|
||||
@@ -473,9 +470,9 @@ void Bot::think(Game& game, Fighter& fighter, BotState& state)
|
||||
if (state.target != previous)
|
||||
state.seenCount = 0;
|
||||
if (state.seenCount == 0)
|
||||
seen_add(state, game.fighters[state.target].center_get());
|
||||
seen_add(state, sim.fighters[state.target].center_get());
|
||||
|
||||
Fighter& target = game.fighters[state.target];
|
||||
Fighter& target = sim.fighters[state.target];
|
||||
Vec2 seen = seen_get(state, skill.reactionTicks);
|
||||
float distance = vec2_length(seen - fighter.hand_get());
|
||||
float bestMiss = INFINITY;
|
||||
@@ -518,7 +515,7 @@ void Bot::think(Game& game, Fighter& fighter, BotState& state)
|
||||
}
|
||||
|
||||
bool isInBand = distance >= hint.rangeMin && distance <= hint.rangeMax;
|
||||
if (isInBand && is_sight_clear(game, fighter.hand_get(), seen))
|
||||
if (isInBand && is_sight_clear(sim, fighter.hand_get(), seen))
|
||||
{
|
||||
state.goal = center;
|
||||
bool isNarrow = hint.rangeMax - hint.rangeMin < STRAFE_DISTANCE * 2;
|
||||
@@ -530,7 +527,7 @@ void Bot::think(Game& game, Fighter& fighter, BotState& state)
|
||||
}
|
||||
|
||||
// A launched target is chased to where it will come down.
|
||||
Vec2 chase = target.isLaunched ? landing_predict(game, target) : seen;
|
||||
Vec2 chase = target.isLaunched ? landing_predict(sim, target) : seen;
|
||||
float standoff = (hint.rangeMin + hint.rangeMax) * GOAL_SPAN;
|
||||
int goalSurface = surface_below(chase, -BOX_HEIGHT);
|
||||
if (goalSurface < 0)
|
||||
@@ -559,14 +556,14 @@ void Bot::race_think(Fighter& fighter, BotState& state)
|
||||
// Follows the path: walk to each link's takeoff and act there (jump, drop through or walk off), steer for the
|
||||
// landing while airborne, and walk to the goal once on its surface. A bot that stops moving short of where it's
|
||||
// going jumps.
|
||||
void Bot::move_steer(Game& game, Fighter& fighter, BotState& state, Controls& controls)
|
||||
void Bot::move_steer(Sim& sim, Fighter& fighter, BotState& state, Controls& controls)
|
||||
{
|
||||
Body& body = fighter.body;
|
||||
float x = fighter.center_get().x;
|
||||
int current = surface_find(fighter);
|
||||
if (state.isGrappling)
|
||||
{
|
||||
grapple_steer(game, fighter, state, controls);
|
||||
grapple_steer(sim, fighter, state, controls);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -635,7 +632,7 @@ void Bot::move_steer(Game& game, Fighter& fighter, BotState& state, Controls& co
|
||||
}
|
||||
if (isCarried)
|
||||
state.landing = link->landing;
|
||||
if (isStuck || (state.isAlert && is_threatened(game, fighter)))
|
||||
if (isStuck || (state.isAlert && is_threatened(sim, fighter)))
|
||||
{
|
||||
state.landing = aim;
|
||||
state.isJumping = true;
|
||||
@@ -646,11 +643,11 @@ void Bot::move_steer(Game& game, Fighter& fighter, BotState& state, Controls& co
|
||||
// Grapple climb: switch to the grapple, charge it, then fire the instant grapple at the platform above and ride the
|
||||
// pull. Let go once the feet clear the platform (or the pull ends), then steer onto it like any landing. Gives up
|
||||
// after a while.
|
||||
void Bot::grapple_steer(Game& game, Fighter& fighter, BotState& state, Controls& controls)
|
||||
void Bot::grapple_steer(Sim& sim, Fighter& fighter, BotState& state, Controls& controls)
|
||||
{
|
||||
state.grappleTimer += TICK;
|
||||
int slot = grapple_slot_get(fighter);
|
||||
Attack* hook = game.weapons.hook_get(fighter);
|
||||
Attack* hook = sim.weapons.hook_get(fighter);
|
||||
bool isAbove = fighter.body.box.y + fighter.body.box.h < state.landingY - FEET_TOLERANCE;
|
||||
controls.cursor = {state.landing, state.landingY + GRAPPLE_GRIP};
|
||||
|
||||
@@ -694,12 +691,12 @@ void Bot::wings_steer(Fighter& fighter, BotState& state, Controls& controls)
|
||||
// or wall jump by accident.
|
||||
// - Wings, besides saving short jumps: dash in to close the last stretch with a short-range weapon, or out of the way
|
||||
// of an attack about to hit while alert.
|
||||
void Bot::trinket_steer(Game& game, Fighter& fighter, BotState& state, Controls& controls)
|
||||
void Bot::trinket_steer(Sim& sim, Fighter& fighter, BotState& state, Controls& controls)
|
||||
{
|
||||
Body& body = fighter.body;
|
||||
if (body.isGrounded || fighter.isLaunched || state.target < 0)
|
||||
return;
|
||||
Fighter& target = game.fighters[state.target];
|
||||
Fighter& target = sim.fighters[state.target];
|
||||
Vec2 center = fighter.center_get();
|
||||
Vec2 targetCenter = target.center_get();
|
||||
float feet = body.box.y + body.box.h;
|
||||
@@ -708,7 +705,7 @@ void Bot::trinket_steer(Game& game, Fighter& fighter, BotState& state, Controls&
|
||||
bool isDashReady = fighter.is_trinket_equipped(TRINKET_WINGS) && fighter.trinket_cooldown_get(TRINKET_WINGS) <= 0;
|
||||
|
||||
float targetFeet = target.body.box.y + target.body.box.h;
|
||||
bool isOnSolid = game.world.is_solid_at({targetCenter.x, targetFeet + 1});
|
||||
bool isOnSolid = sim.world.is_solid_at({targetCenter.x, targetFeet + 1});
|
||||
bool isPounding = fighter.is_trinket_equipped(TRINKET_WEIGHTS) && isFalling && target.body.isGrounded &&
|
||||
isOnSolid && targetFeet > feet + TILE_SIZE && std::abs(targetCenter.x - center.x) < POUND_REACH;
|
||||
if (isPounding)
|
||||
@@ -718,7 +715,7 @@ void Bot::trinket_steer(Game& game, Fighter& fighter, BotState& state, Controls&
|
||||
}
|
||||
|
||||
int climb = fighter.trinket_get(TRINKET_CLIMBING, 0);
|
||||
int wall = climb > 0 ? fighter.wall_get(game) : 0;
|
||||
int wall = climb > 0 ? fighter.wall_get(sim) : 0;
|
||||
bool isWantingUp = state.landingY < feet - CLIMB_RISE || targetCenter.y < center.y - 2 * TILE_SIZE;
|
||||
if (wall != 0 && isWantingUp)
|
||||
{
|
||||
@@ -748,7 +745,7 @@ void Bot::trinket_steer(Game& game, Fighter& fighter, BotState& state, Controls&
|
||||
BotHint hint = weapon >= 0 ? hint_get(fighter, fighter.slot, state.isChargePlanned) : BotHint{};
|
||||
bool isChasing = weapon >= 0 && hint.rangeMax <= DASH_SHORT_RANGE && distance > hint.rangeMax &&
|
||||
distance < hint.rangeMax + DASH_CHASE;
|
||||
bool isDodging = state.isAlert && is_threatened(game, fighter);
|
||||
bool isDodging = state.isAlert && is_threatened(sim, fighter);
|
||||
if (!isChasing && !isDodging)
|
||||
return;
|
||||
float side = targetCenter.x < center.x ? 1 : -1;
|
||||
@@ -805,12 +802,12 @@ static Vec2 aim_point_get(Fighter& fighter, int weaponSlot, bool isCharged, Vec2
|
||||
// switches to the planned slot, holds attack in band once aimed, and charges while close enough to need it soon,
|
||||
// firing once full, aimed and in band (or once it has waited too long). Mines detonate when the target stands inside
|
||||
// one's blast.
|
||||
void Bot::attack_aim(Game& game, Fighter& fighter, BotState& state, Controls& controls)
|
||||
void Bot::attack_aim(Sim& sim, Fighter& fighter, BotState& state, Controls& controls)
|
||||
{
|
||||
if (state.target < 0)
|
||||
return;
|
||||
const BotDifficulty& skill = BOT_DIFFICULTIES[level];
|
||||
Fighter& target = game.fighters[state.target];
|
||||
Fighter& target = sim.fighters[state.target];
|
||||
Vec2 seen = seen_get(state, skill.reactionTicks);
|
||||
Vec2 velocity = (seen - seen_get(state, skill.reactionTicks + 1)) * (1 / TICK);
|
||||
Vec2 hand = fighter.hand_get();
|
||||
@@ -826,7 +823,7 @@ void Bot::attack_aim(Game& game, Fighter& fighter, BotState& state, Controls& co
|
||||
return;
|
||||
|
||||
// A hook that struck lets go at once, or once a charged one has yanked the bot in to its target.
|
||||
Attack* hook = game.weapons.hook_get(fighter);
|
||||
Attack* hook = sim.weapons.hook_get(fighter);
|
||||
if (hook)
|
||||
{
|
||||
if (!hook->isPulling)
|
||||
@@ -839,18 +836,18 @@ void Bot::attack_aim(Game& game, Fighter& fighter, BotState& state, Controls& co
|
||||
bool isAimed = std::abs(turn) < AIM_READY;
|
||||
bool isNarrow = hint.rangeMax - hint.rangeMin < STRAFE_DISTANCE * 2;
|
||||
bool isInBand =
|
||||
distance <= hint.rangeMax && (distance >= hint.rangeMin || !isNarrow) && is_sight_clear(game, hand, seen);
|
||||
distance <= hint.rangeMax && (distance >= hint.rangeMin || !isNarrow) && is_sight_clear(sim, hand, seen);
|
||||
state.chargeHeld = fighter.charge >= 1 ? state.chargeHeld + TICK : 0;
|
||||
|
||||
// Thrown weapons come back once they're past the target or have hung out a while, and mines go off when the
|
||||
// target is inside one's blast.
|
||||
for (Attack& attack : game.weapons.attacks)
|
||||
for (Attack& attack : sim.weapons.attacks)
|
||||
{
|
||||
if (attack.owner != fighter.index || attack.isDead || attack.isReturning)
|
||||
continue;
|
||||
AttackKind kind = attack.def()->kind;
|
||||
bool isPast = vec2_length(attack.position - hand) > distance + BOOMERANG_OVERSHOOT;
|
||||
if (kind == ATTACK_BOOMERANG && isPast && game.weapons.is_busy(fighter, fighter.weapon_key(fighter.slot)))
|
||||
if (kind == ATTACK_BOOMERANG && isPast && sim.weapons.is_busy(fighter, fighter.weapon_key(fighter.slot)))
|
||||
{
|
||||
control_tap(fighter, controls, CONTROL_CHARGE);
|
||||
return;
|
||||
@@ -889,9 +886,9 @@ void Bot::attack_aim(Game& game, Fighter& fighter, BotState& state, Controls& co
|
||||
// Racing: hacks at the nearest unbroken block in its body's way, just ahead in the way it's walking, holding attack
|
||||
// with a weapon that strikes on its own (not a mine detonator, and a grapple only as a last resort). Holding off the
|
||||
// stuck-jump while it chops.
|
||||
void Bot::block_attack(Game& game, Fighter& fighter, BotState& state, Controls& controls)
|
||||
void Bot::block_attack(Sim& sim, Fighter& fighter, BotState& state, Controls& controls)
|
||||
{
|
||||
World& world = game.world;
|
||||
World& world = sim.world;
|
||||
Rect box = fighter.body.box;
|
||||
bool isLeft = controls.buttons >> CONTROL_LEFT & 1;
|
||||
Rect ahead = {isLeft ? box.x - BLOCK_SEEK : box.x, box.y, box.w + BLOCK_SEEK, box.h};
|
||||
@@ -926,7 +923,7 @@ void Bot::block_attack(Game& game, Fighter& fighter, BotState& state, Controls&
|
||||
if (fighter.slot != best)
|
||||
controls.slot = best;
|
||||
|
||||
Attack* hook = game.weapons.hook_get(fighter);
|
||||
Attack* hook = sim.weapons.hook_get(fighter);
|
||||
if (hook)
|
||||
{
|
||||
control_tap(fighter, controls, CONTROL_ATTACK);
|
||||
@@ -935,32 +932,3 @@ void Bot::block_attack(Game& game, Fighter& fighter, BotState& state, Controls&
|
||||
controls.cursor = target;
|
||||
control_set(controls, CONTROL_ATTACK, fighter.slot == best);
|
||||
}
|
||||
|
||||
// Debug view: surfaces as lines, links as thin lines from takeoff to landing, and each bot's remaining path in white.
|
||||
void Bot::draw(Game& game)
|
||||
{
|
||||
if (!isDrawn)
|
||||
return;
|
||||
|
||||
Renderer& renderer = game.renderer;
|
||||
for (Surface& surface : surfaces)
|
||||
renderer.line_draw({surface.left, surface.y_at(surface.left)}, {surface.right, surface.y_at(surface.right)},
|
||||
SURFACE_WIDTH, GRAY);
|
||||
for (Link& link : links)
|
||||
renderer.line_draw({link.takeoff, surfaces[link.from].y_at(link.takeoff)},
|
||||
{link.landing, surfaces[link.to].y_at(link.landing)}, LINK_WIDTH,
|
||||
color_fade(GRAY, GOAL_SPAN));
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
{
|
||||
BotState& state = states[i];
|
||||
if (!game.fighters[i].isBot)
|
||||
continue;
|
||||
for (int step = state.pathStep; step < state.pathLength; step++)
|
||||
{
|
||||
Link& link = links[state.path[step]];
|
||||
renderer.line_draw({link.takeoff, surfaces[link.from].y_at(link.takeoff)},
|
||||
{link.landing, surfaces[link.to].y_at(link.landing)}, SURFACE_WIDTH, WHITE);
|
||||
}
|
||||
renderer.circle_draw(state.goal, SURFACE_WIDTH * 2, WHITE);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
#include "fighter.h"
|
||||
|
||||
struct Game;
|
||||
constexpr float GOAL_SPAN = 0.5f;
|
||||
|
||||
struct Sim;
|
||||
|
||||
constexpr int BOT_HISTORY = 20;
|
||||
constexpr int BOT_PATH_MAX = 16;
|
||||
@@ -116,14 +118,15 @@ struct BotState
|
||||
// each bot's path. Level is the difficulty every bot plays at, from RPG_BOT_DIFFICULTY (0 to 2).
|
||||
class Bot
|
||||
{
|
||||
friend class Scene;
|
||||
|
||||
public:
|
||||
BotState states[MAX_FIGHTERS];
|
||||
int level = BOT_NORMAL;
|
||||
|
||||
void init(Game& game);
|
||||
void init(World& world);
|
||||
void graph_build(World& world);
|
||||
void update(Game& game);
|
||||
void draw(Game& game);
|
||||
void update(Sim& sim);
|
||||
|
||||
private:
|
||||
std::vector<Link> links;
|
||||
@@ -136,12 +139,12 @@ private:
|
||||
int surface_find(Fighter& fighter);
|
||||
int surface_below(Vec2 point, float above);
|
||||
bool is_path_found(BotState& state, int from, int to, bool isGrappler);
|
||||
void think(Game& game, Fighter& fighter, BotState& state);
|
||||
void think(Sim& sim, Fighter& fighter, BotState& state);
|
||||
void race_think(Fighter& fighter, BotState& state);
|
||||
void move_steer(Game& game, Fighter& fighter, BotState& state, Controls& controls);
|
||||
void grapple_steer(Game& game, Fighter& fighter, BotState& state, Controls& controls);
|
||||
void move_steer(Sim& sim, Fighter& fighter, BotState& state, Controls& controls);
|
||||
void grapple_steer(Sim& sim, Fighter& fighter, BotState& state, Controls& controls);
|
||||
void wings_steer(Fighter& fighter, BotState& state, Controls& controls);
|
||||
void trinket_steer(Game& game, Fighter& fighter, BotState& state, Controls& controls);
|
||||
void attack_aim(Game& game, Fighter& fighter, BotState& state, Controls& controls);
|
||||
void block_attack(Game& game, Fighter& fighter, BotState& state, Controls& controls);
|
||||
void trinket_steer(Sim& sim, Fighter& fighter, BotState& state, Controls& controls);
|
||||
void attack_aim(Sim& sim, Fighter& fighter, BotState& state, Controls& controls);
|
||||
void block_attack(Sim& sim, Fighter& fighter, BotState& state, Controls& controls);
|
||||
};
|
||||
+7
-7
@@ -92,7 +92,7 @@ void Chat::players_clear()
|
||||
int Chat::member_get(Game& game, int fighter)
|
||||
{
|
||||
if (game.session.isActive)
|
||||
return game.match.owners[fighter];
|
||||
return game.sim.match.owners[fighter];
|
||||
return fighter == game.local ? game.net.selfId : NO_MEMBER;
|
||||
}
|
||||
|
||||
@@ -267,8 +267,8 @@ void Chat::command(Game& game, const std::string& text)
|
||||
if (SDL_strcasecmp(argument.c_str(), COLOR_NAMES[color]) != 0)
|
||||
continue;
|
||||
int own = game.session.fighter_get(game, game.net.selfId);
|
||||
for (int i = 0; i < game.fighterCount && game.session.isActive; i++)
|
||||
if (i != own && game.match.colors[i] == color)
|
||||
for (int i = 0; i < game.sim.fighterCount && game.session.isActive; i++)
|
||||
if (i != own && game.sim.match.colors[i] == color)
|
||||
return system(std::string(COLOR_NAMES[color]) + " is already taken by another player.");
|
||||
game.settings.color = color;
|
||||
game.settings.changed();
|
||||
@@ -284,7 +284,7 @@ void Chat::command(Game& game, const std::string& text)
|
||||
Rect Chat::log_area_get(Game& game)
|
||||
{
|
||||
float bottom = CANVAS_HEIGHT - HOTBAR_MARGIN;
|
||||
MatchState state = (MatchState)game.match.state;
|
||||
MatchState state = (MatchState)game.sim.match.state;
|
||||
if (state != MATCH_PRACTICE && state != MATCH_COUNTDOWN)
|
||||
{
|
||||
RivalLayout rivals = game.ui.rivals_layout(game);
|
||||
@@ -343,10 +343,10 @@ void Chat::draw(Game& game)
|
||||
void Chat::overhead_draw(Game& game)
|
||||
{
|
||||
float now = seconds_get();
|
||||
float above = game.match.state == MATCH_PRACTICE ? BUBBLE_ABOVE : BUBBLE_MATCH_ABOVE;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
float above = game.sim.match.state == MATCH_PRACTICE ? BUBBLE_ABOVE : BUBBLE_MATCH_ABOVE;
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
{
|
||||
Fighter& fighter = game.fighters[i];
|
||||
Fighter& fighter = game.sim.fighters[i];
|
||||
int member = member_get(game, i);
|
||||
if (!fighter.isAlive || member == NO_MEMBER)
|
||||
continue;
|
||||
|
||||
+20
-14
@@ -1,11 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "dmath.h"
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
using Uint8 = std::uint8_t;
|
||||
using Uint16 = std::uint16_t;
|
||||
using Uint32 = std::uint32_t;
|
||||
using Uint64 = std::uint64_t;
|
||||
|
||||
constexpr const char* GAME_ID = "smack-a-brother";
|
||||
constexpr const char* GAME_NAME = "Smack a Brother";
|
||||
constexpr int CANVAS_HEIGHT = 360;
|
||||
@@ -124,18 +128,26 @@ inline float angle_lerp(float from, float to, float t)
|
||||
return from + std::remainder(to - from, 2 * PI) * t;
|
||||
}
|
||||
|
||||
// Cosmetic randomness: particles, flicker, shake. Gameplay draws from Rng instead.
|
||||
inline float random_get(float min, float max)
|
||||
{
|
||||
return min + SDL_randf() * (max - min);
|
||||
}
|
||||
|
||||
constexpr int RNG_FRACTION_BITS = 24;
|
||||
constexpr int RNG_SHIFT_A = 13;
|
||||
constexpr int RNG_SHIFT_B = 17;
|
||||
constexpr int RNG_SHIFT_C = 5;
|
||||
constexpr Uint32 RNG_SEED = 0x9E3779B9;
|
||||
|
||||
// Describes a spray of square particles, fanned around angle by spread. Fill is how far their colour takes over their
|
||||
// black middle.
|
||||
struct Burst
|
||||
{
|
||||
float angle = 0;
|
||||
float fill = 0;
|
||||
float gravity = 0;
|
||||
float life = 0.4f;
|
||||
float size = 3;
|
||||
float speed = 100;
|
||||
float spread = PI;
|
||||
int count = 8;
|
||||
};
|
||||
|
||||
// A small seeded xorshift generator for gameplay, saved with the simulation so a replay draws the same numbers.
|
||||
struct Rng
|
||||
{
|
||||
@@ -209,9 +221,3 @@ template <class T, int N> struct FixedList
|
||||
count = kept;
|
||||
}
|
||||
};
|
||||
|
||||
// Absolute path to a file inside the resources folder next to the executable.
|
||||
inline std::string path_get(const char* relative)
|
||||
{
|
||||
return std::string(SDL_GetBasePath()) + "resources/" + relative;
|
||||
}
|
||||
@@ -44,8 +44,6 @@ template <typename T> static void effect_add(std::vector<T>& list, const T& effe
|
||||
// Sprays particles at position, rotated by angle on top of the burst's own angle.
|
||||
void Effects::burst(Burst burst, Vec2 position, float angle, Color color)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
for (int i = 0; i < burst.count; i++)
|
||||
{
|
||||
float direction = angle + burst.angle + random_get(-burst.spread, burst.spread);
|
||||
@@ -59,8 +57,6 @@ void Effects::burst(Burst burst, Vec2 position, float angle, Color color)
|
||||
// Callers may ask every tick. Copies are only kept every few ticks so trails stay spaced out.
|
||||
void Effects::afterimage_spawn(Pose pose)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
if (ticks % AFTERIMAGE_INTERVAL == 0)
|
||||
effect_add(afterimages, Afterimage{pose, AFTERIMAGE_LIFE});
|
||||
}
|
||||
@@ -82,8 +78,6 @@ void Effects::afterimage_draw(Game& game)
|
||||
// total.
|
||||
void Effects::number_spawn(Vec2 position, int value, int target, Color color)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
for (int i = (int)popups.size() - 1; i >= 0; i--)
|
||||
{
|
||||
Popup& popup = popups[i];
|
||||
@@ -109,8 +103,6 @@ void Effects::number_spawn(Vec2 position, int value, int target, Color color)
|
||||
|
||||
void Effects::popup_spawn(Vec2 position, const char* text, Color color)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
Popup popup = {
|
||||
.color = color,
|
||||
.position = position,
|
||||
@@ -125,15 +117,11 @@ void Effects::popup_spawn(Vec2 position, const char* text, Color color)
|
||||
|
||||
void Effects::flash_spawn(Vec2 position, float angle, float size, Color color)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
effect_add(flashes, Flash{color, position, angle, FLASH_LIFE, size});
|
||||
}
|
||||
|
||||
void Effects::dps_record(int amount)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
hits.push_back({ticks * TICK, amount});
|
||||
}
|
||||
|
||||
@@ -148,8 +136,6 @@ float Effects::dps_get()
|
||||
|
||||
void Effects::shake_add(float amount)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
shake = std::min(SHAKE_TRAUMA_MAX, shake + amount);
|
||||
}
|
||||
|
||||
|
||||
+1
-18
@@ -5,20 +5,6 @@
|
||||
|
||||
struct Game;
|
||||
|
||||
// Describes a spray of square particles, fanned around angle by spread. Fill is how far their colour takes over their
|
||||
// black middle.
|
||||
struct Burst
|
||||
{
|
||||
float angle = 0;
|
||||
float fill = 0;
|
||||
float gravity = 0;
|
||||
float life = 0.4f;
|
||||
float size = 3;
|
||||
float speed = 100;
|
||||
float spread = PI;
|
||||
int count = 8;
|
||||
};
|
||||
|
||||
struct Particle
|
||||
{
|
||||
Color color;
|
||||
@@ -70,13 +56,10 @@ struct Afterimage
|
||||
float life = 0;
|
||||
};
|
||||
|
||||
// Particles, afterimages, text popups, screen shake and the damage-per-second tally. While muted (during a
|
||||
// rollback replay) it ignores new requests.
|
||||
// Particles, afterimages, text popups, screen shake and the damage-per-second tally.
|
||||
class Effects
|
||||
{
|
||||
public:
|
||||
bool isMuted = false;
|
||||
|
||||
void burst(Burst burst, Vec2 position, float angle, Color color);
|
||||
void afterimage_spawn(Pose pose);
|
||||
void flash_spawn(Vec2 position, float angle, float size, Color color);
|
||||
|
||||
+83
-212
@@ -1,9 +1,7 @@
|
||||
#include "fighter.h"
|
||||
#include "game.h"
|
||||
#include <SDL3/SDL_timer.h>
|
||||
#include "sim.h"
|
||||
#include <algorithm>
|
||||
|
||||
constexpr int BLINK_MILLISECONDS = 80;
|
||||
constexpr float AIR_ACCELERATION = 400;
|
||||
constexpr float BOUNCE_SHAKE = 0.0005f;
|
||||
constexpr float SLAM_DAMAGE = 0.06f;
|
||||
@@ -12,9 +10,6 @@ constexpr float KNOCK_LIFT = 0.35f;
|
||||
constexpr float KNOCK_MAX = 4500;
|
||||
constexpr float KNOCK_PER_DAMAGE = 30;
|
||||
constexpr float LAUNCH_START_SPEED = 300;
|
||||
constexpr float CHECKPOINT_ALPHA = 0.3f;
|
||||
constexpr float CHECKPOINT_HEIGHT = 24;
|
||||
constexpr float POUND_RING_ALPHA = 0.25f;
|
||||
|
||||
constexpr float AFTERIMAGE_SPEED = 440;
|
||||
constexpr float AIR_FRICTION = 600;
|
||||
@@ -37,7 +32,6 @@ constexpr float LAND_SPEED = 150;
|
||||
constexpr float MAX_FALL = 520;
|
||||
constexpr float ROPE_MIN = 24;
|
||||
constexpr float SLIDE_DUST_INTERVAL = 0.08f;
|
||||
constexpr float STEP_SPEED = 40;
|
||||
constexpr float SWING_ACCELERATION = 500;
|
||||
constexpr float SWING_SPEED_MAX = 420;
|
||||
constexpr float POUND_AREAS[LEVEL_COUNT] = {30, 40, 50};
|
||||
@@ -59,23 +53,16 @@ constexpr float CROUCH_DAMP = 0.5f;
|
||||
constexpr float KICK_POSE_TIME = 0.2f;
|
||||
constexpr float SPIN_GRAVITY = 0.5f;
|
||||
constexpr float CRAWL_SPEED = 0.35f;
|
||||
constexpr float CRAWL_STRIDE = 5;
|
||||
constexpr float DOWNHILL_BOOST = 0.6f;
|
||||
constexpr float MOMENTUM_DRAG = 400;
|
||||
constexpr float ROLL_RADIUS = 10;
|
||||
constexpr float SLIDE_FRICTION = 150;
|
||||
constexpr float SLIDE_MAX = 700;
|
||||
constexpr float SLIDE_MIN = 60;
|
||||
constexpr float TUMBLE_RATE = 0.012f;
|
||||
constexpr float WALK_STRIDE = 7;
|
||||
// Footsteps per animation frame walked or crawled: one per four-frame walk cycle, one per half crawl cycle.
|
||||
constexpr float WALK_STEP_FRAMES = 4;
|
||||
constexpr float CRAWL_STEP_FRAMES = 2;
|
||||
|
||||
// Animations drawn facing their own way, picked by the way the fighter moves (right, then left). The rest are
|
||||
// mirrored to face left.
|
||||
constexpr const char* CRAWL_ANIMATIONS[] = {"Crawl Right", "Crawl Left"};
|
||||
constexpr const char* WALK_ANIMATIONS[] = {"Walk Right", "Walk Left"};
|
||||
constexpr Burst LAND_DUST = {
|
||||
.angle = -PI / 2, .gravity = 150, .life = 0.35f, .speed = 90, .spread = PI * 0.45f, .count = 10};
|
||||
constexpr Burst DEATH_BURST = {.gravity = 300, .life = 0.8f, .size = 4, .speed = 260, .count = 40};
|
||||
@@ -87,12 +74,12 @@ constexpr Burst STEP_DUST = {
|
||||
.angle = -PI / 2, .gravity = 150, .life = 0.25f, .size = 2, .speed = 40, .spread = PI / 2, .count = 2};
|
||||
|
||||
// Makes this fighter number, with empty loadouts, standing at its spawn.
|
||||
void Fighter::init(Game& game, int number)
|
||||
void Fighter::init(Sim& sim, int number)
|
||||
{
|
||||
index = number;
|
||||
std::fill(std::begin(weaponLoadout), std::end(weaponLoadout), -1);
|
||||
std::fill(std::begin(trinketLoadout), std::end(trinketLoadout), -1);
|
||||
state_reset(game, 1);
|
||||
state_reset(sim, 1);
|
||||
}
|
||||
|
||||
// Uses the first weapons and trinkets slots of each loadout, emptying the rest, and holds a slot in use.
|
||||
@@ -106,15 +93,15 @@ void Fighter::slots_set(int weapons, int trinkets)
|
||||
}
|
||||
|
||||
// Back at its spawn and still: no velocity, nothing charging, no echoes, not launched or stuck in hitstop.
|
||||
void Fighter::position_reset(Game& game)
|
||||
void Fighter::position_reset(Sim& sim)
|
||||
{
|
||||
position_set(game, game.world.spawns[index]);
|
||||
position_set(sim, sim.world.spawns[index]);
|
||||
}
|
||||
|
||||
// Puts the fighter still with its feet at a point, nothing charging.
|
||||
void Fighter::position_set(Game& game, Vec2 spawn)
|
||||
void Fighter::position_set(Sim& sim, Vec2 spawn)
|
||||
{
|
||||
charge_cancel(game);
|
||||
charge_cancel(sim);
|
||||
body = {.box = {spawn.x - BOX_WIDTH / 2, spawn.y - BOX_HEIGHT, BOX_WIDTH, BOX_HEIGHT}};
|
||||
cornerPrevious = {body.box.x, body.box.y};
|
||||
safe = spawn;
|
||||
@@ -125,9 +112,9 @@ void Fighter::position_set(Game& game, Vec2 spawn)
|
||||
|
||||
// Back at its spawn, alive, at full health and still, holding its first slot, with nothing charging or cooling
|
||||
// down. Loadouts and levels stay as they are.
|
||||
void Fighter::state_reset(Game& game, float maxHealth)
|
||||
void Fighter::state_reset(Sim& sim, float maxHealth)
|
||||
{
|
||||
position_reset(game);
|
||||
position_reset(sim);
|
||||
std::fill(std::begin(cooldowns), std::end(cooldowns), 0.0f);
|
||||
flash = 0;
|
||||
health = maxHealth;
|
||||
@@ -140,11 +127,11 @@ void Fighter::state_reset(Game& game, float maxHealth)
|
||||
// up to a speed that grows with the damage. Knocks top up rather than stack, so rapid small hits can't juggle. A hard
|
||||
// enough knock launches it. Shock Absorber softens it. Running out of health kills it in a burst, and its attacks
|
||||
// vanish with it.
|
||||
void Fighter::damage_take(Game& game, Damage damage)
|
||||
void Fighter::damage_take(Sim& sim, Damage damage)
|
||||
{
|
||||
// Races and the sandbox never cost health.
|
||||
bool isRacing = game.match.is_racing();
|
||||
health -= isRacing || game.match.state == MATCH_PRACTICE ? 0 : damage.amount;
|
||||
bool isRacing = sim.match.is_racing();
|
||||
health -= isRacing || sim.match.state == MATCH_PRACTICE ? 0 : damage.amount;
|
||||
flash = 1;
|
||||
hitstop = std::max(hitstop, damage.hitstop);
|
||||
|
||||
@@ -160,15 +147,15 @@ void Fighter::damage_take(Game& game, Damage damage)
|
||||
return;
|
||||
|
||||
isAlive = false;
|
||||
charge_cancel(game);
|
||||
for (Attack& attack : game.weapons.attacks)
|
||||
charge_cancel(sim);
|
||||
for (Attack& attack : sim.weapons.attacks)
|
||||
attack.isDead |= attack.owner == index;
|
||||
game.audio.play(SOUND_DEATH);
|
||||
game.effects.burst(DEATH_BURST, center_get(), 0, game.color_get(index));
|
||||
game.effects.shake_add(DEATH_SHAKE);
|
||||
sim.cues->sound_play(SOUND_DEATH);
|
||||
sim.cues->burst_spawn(DEATH_BURST, center_get(), 0, index);
|
||||
sim.cues->shake_add(DEATH_SHAKE);
|
||||
}
|
||||
|
||||
void Fighter::update(Game& game)
|
||||
void Fighter::update(Sim& sim)
|
||||
{
|
||||
if (!isAlive)
|
||||
return;
|
||||
@@ -184,18 +171,18 @@ void Fighter::update(Game& game)
|
||||
for (float& cooldown : cooldowns)
|
||||
cooldown = std::max(0.0f, cooldown - TICK);
|
||||
kickTime += TICK;
|
||||
Attack* hook = game.weapons.hook_get(*this);
|
||||
slot_update(game);
|
||||
Attack* hook = sim.weapons.hook_get(*this);
|
||||
slot_update(sim);
|
||||
// A slide keeps rolling through the air for as long as S is held, like off a kicker or jumping mid-slide.
|
||||
bool wasCrouching = isCrouching;
|
||||
bool isTucked = is_down(CONTROL_DOWN) && !isLaunched;
|
||||
isCrouching = body.isGrounded && isTucked;
|
||||
if (isCrouching != wasCrouching && body.isGrounded && !isSliding)
|
||||
game.audio.play(isCrouching ? SOUND_DUCK : SOUND_UNDUCK);
|
||||
sim.cues->sound_play(isCrouching ? SOUND_DUCK : SOUND_UNDUCK);
|
||||
bool isSlideKept = isSliding && (std::abs(body.velocity.x) > SLIDE_MIN || !body.isGrounded);
|
||||
isSliding = isTucked && (body.isGrounded ? body.slope != 0 || isSlideKept : isSliding);
|
||||
size_update();
|
||||
isSpinning = game.weapons.is_spinning(*this);
|
||||
isSpinning = sim.weapons.is_spinning(*this);
|
||||
run_update(hook);
|
||||
body.isDropping = is_down(CONTROL_DOWN);
|
||||
|
||||
@@ -204,7 +191,7 @@ void Fighter::update(Game& game)
|
||||
jumpBuffer = isJumpPressed ? JUMP_BUFFER : jumpBuffer - TICK;
|
||||
coyoteTimer = body.isGrounded ? COYOTE_TIME : coyoteTimer - TICK;
|
||||
Vec2 feet = {center_get().x, body.box.y + body.box.h};
|
||||
climb_update(game);
|
||||
climb_update(sim);
|
||||
|
||||
if (jumpBuffer > 0 && coyoteTimer > 0 && !isLaunched)
|
||||
{
|
||||
@@ -212,10 +199,10 @@ void Fighter::update(Game& game)
|
||||
jumpBuffer = 0;
|
||||
coyoteTimer = 0;
|
||||
isJumping = true;
|
||||
game.audio.play(SOUND_JUMP);
|
||||
game.effects.burst(STEP_DUST, feet, 0, game.color_get(index));
|
||||
sim.cues->sound_play(SOUND_JUMP);
|
||||
sim.cues->burst_spawn(STEP_DUST, feet, 0, index);
|
||||
}
|
||||
dash_update(game, isJumpPressed);
|
||||
dash_update(sim, isJumpPressed);
|
||||
|
||||
isJumping &= body.velocity.y < 0 && !hook;
|
||||
if (isJumping && !isJumpHeld && body.velocity.y < -JUMP_RELEASE_SPEED)
|
||||
@@ -228,12 +215,12 @@ void Fighter::update(Game& game)
|
||||
bool isPounding = is_down(CONTROL_DOWN) && is_trinket_equipped(TRINKET_WEIGHTS);
|
||||
float fallSpeed = body.velocity.y;
|
||||
float runSpeed = body.velocity.x;
|
||||
game.world.obstacles = game.weapons.solids_get(*this);
|
||||
game.world.body_move(body);
|
||||
game.world.obstacles.clear();
|
||||
sim.world.obstacles = sim.weapons.solids_get(*this);
|
||||
sim.world.body_move(body);
|
||||
sim.world.obstacles.clear();
|
||||
if (isLaunched)
|
||||
bounce_resolve(game, {runSpeed, fallSpeed});
|
||||
safe_update(game);
|
||||
bounce_resolve(sim, {runSpeed, fallSpeed});
|
||||
safe_update(sim);
|
||||
|
||||
wallMemory -= TICK;
|
||||
bool isWallHit = body.velocity.x == 0 && std::abs(runSpeed) > WALL_IMPACT_SPEED;
|
||||
@@ -245,20 +232,20 @@ void Fighter::update(Game& game)
|
||||
if (wallMemory <= 0)
|
||||
wallSpeed = 0;
|
||||
if (vec2_length(body.velocity) > AFTERIMAGE_SPEED || dashTimer > 0)
|
||||
game.effects.afterimage_spawn(pose_get(game));
|
||||
sim.cues->fighter_afterimage_spawn(*this);
|
||||
feet = {center_get().x, body.box.y + body.box.h};
|
||||
|
||||
if (body.isGrounded && !wasGrounded && fallSpeed > LAND_SPEED)
|
||||
{
|
||||
game.audio.play(SOUND_LAND);
|
||||
game.effects.burst(LAND_DUST, feet, 0, game.color_get(index));
|
||||
sim.cues->sound_play(SOUND_LAND);
|
||||
sim.cues->burst_spawn(LAND_DUST, feet, 0, index);
|
||||
}
|
||||
// Iron Weights slam: the harder the landing, the harder the blast. Damage follows the landing speed, and the
|
||||
// radius its square root.
|
||||
if (body.isGrounded && !wasGrounded && fallSpeed > LAND_SPEED && isPounding)
|
||||
{
|
||||
float impact = std::clamp(fallSpeed / POUND_SPEED, POUND_SCALE_MIN, POUND_SCALE_MAX);
|
||||
game.weapons.blast_spawn(game, {.position = feet,
|
||||
sim.weapons.blast_spawn(sim, {.position = feet,
|
||||
.area = POUND_AREAS[trinket_level_get(TRINKET_WEIGHTS)] * std::sqrt(impact),
|
||||
.damage = trinket_sum_get(TRINKET_WEIGHTS, POUND_DAMAGES) * impact,
|
||||
.owner = index,
|
||||
@@ -288,15 +275,15 @@ void Fighter::update(Game& game)
|
||||
if (stepDistance <= 0)
|
||||
{
|
||||
stepDistance += isWalking ? WALK_STRIDE * WALK_STEP_FRAMES : CRAWL_STRIDE * CRAWL_STEP_FRAMES;
|
||||
game.audio.play(isWalking ? SOUND_STEP : SOUND_CRAWL);
|
||||
sim.cues->sound_play(isWalking ? SOUND_STEP : SOUND_CRAWL);
|
||||
if (isWalking)
|
||||
game.effects.burst(STEP_DUST, feet, 0, game.color_get(index));
|
||||
sim.cues->burst_spawn(STEP_DUST, feet, 0, index);
|
||||
}
|
||||
}
|
||||
else
|
||||
stepDistance = 0;
|
||||
if (isSliding && body.isGrounded && std::floor(roll / PI) != std::floor(rollBefore / PI))
|
||||
game.audio.play(SOUND_ROLL);
|
||||
sim.cues->sound_play(SOUND_ROLL);
|
||||
|
||||
// Facing follows the cursor's side of the body, holding still when it's nearly straight above or below, so the
|
||||
// hand (which sits on the facing side) can't flip back and forth under it.
|
||||
@@ -304,7 +291,7 @@ void Fighter::update(Game& game)
|
||||
if (std::abs(cursorX) > FACING_DEADZONE)
|
||||
isFacingLeft = cursorX < 0;
|
||||
aim = vec2_normalize(controls.cursor - hand_get());
|
||||
charge_update(game);
|
||||
charge_update(sim);
|
||||
buttonsPrevious = controls.buttons;
|
||||
}
|
||||
|
||||
@@ -404,7 +391,7 @@ void Fighter::rope_update(Attack& hook)
|
||||
// The controls' slot picks a weapon slot, wrapping around, and the level buttons level the weapon copy in it. The
|
||||
// weapon switched away from becomes the off-hand weapon for Dual Wield (a copy of the same weapon counts when weapons
|
||||
// stack), and a thrown flail ball is dropped.
|
||||
void Fighter::slot_update(Game& game)
|
||||
void Fighter::slot_update(Sim& sim)
|
||||
{
|
||||
int previous = slot;
|
||||
if (controls.slot != -1)
|
||||
@@ -412,16 +399,16 @@ void Fighter::slot_update(Game& game)
|
||||
|
||||
int held = weapon_get();
|
||||
if (slot != previous)
|
||||
game.audio.play(SOUND_SELECT);
|
||||
sim.cues->sound_play(SOUND_SELECT);
|
||||
if (slot != previous && weaponLoadout[previous] >= 0 && weapon_key(previous) != weapon_key(slot))
|
||||
{
|
||||
offSlot = previous;
|
||||
game.weapons.attacks_release(*this, ATTACK_FLAIL);
|
||||
sim.weapons.attacks_release(*this, ATTACK_FLAIL);
|
||||
}
|
||||
|
||||
int step = (int)is_pressed(CONTROL_LEVEL_UP) - (int)is_pressed(CONTROL_LEVEL_DOWN);
|
||||
if (step != 0 && held >= 0 && game.match.state == MATCH_PRACTICE)
|
||||
game.ui.level_step(game, weaponSlotLevels[slot], step);
|
||||
if (step != 0 && held >= 0 && sim.match.state == MATCH_PRACTICE)
|
||||
level_step(sim, weaponSlotLevels[slot], step);
|
||||
}
|
||||
|
||||
// Shrinker, Embiggener and crouching: resizes the box around the feet to the fighter's current size.
|
||||
@@ -437,7 +424,7 @@ void Fighter::size_update()
|
||||
// cursor, adding a push on top of the current velocity and holding it briefly without gravity. Stacked copies don't
|
||||
// add speed (the best copy's counts) but each holds a dash: the cooldown builds up a dash's worth per dash, and dashing
|
||||
// works while there's a dash's worth left to build up before all of them are spent.
|
||||
void Fighter::dash_update(Game& game, bool isJumpPressed)
|
||||
void Fighter::dash_update(Sim& sim, bool isJumpPressed)
|
||||
{
|
||||
float speed = trinket_best_get(TRINKET_WINGS, 0);
|
||||
dashTimer -= TICK;
|
||||
@@ -451,16 +438,16 @@ void Fighter::dash_update(Game& game, bool isJumpPressed)
|
||||
jumpBuffer = 0;
|
||||
burst_add(aim * speed);
|
||||
dashCooldown += DASH_COOLDOWN;
|
||||
game.audio.play(SOUND_DASH);
|
||||
game.effects.burst(STEP_DUST, center_get(), 0, game.color_get(index));
|
||||
sim.cues->sound_play(SOUND_DASH);
|
||||
sim.cues->burst_spawn(STEP_DUST, center_get(), 0, index);
|
||||
}
|
||||
|
||||
// Climb Claw, by level. 1: pushing into a wall slows the slide down it, trailing dust. 2: pressing away kicks
|
||||
// off, on a cooldown so it can't climb forever. 3: Jump wall jumps freely.
|
||||
void Fighter::climb_update(Game& game)
|
||||
void Fighter::climb_update(Sim& sim)
|
||||
{
|
||||
int level = trinket_get(TRINKET_CLIMBING, 0);
|
||||
int wall = wall_get(game);
|
||||
int wall = wall_get(sim);
|
||||
kickTimer -= TICK;
|
||||
slideWall = 0;
|
||||
if (level == 0 || wall == 0 || body.isGrounded || isLaunched)
|
||||
@@ -473,20 +460,20 @@ void Fighter::climb_update(Game& game)
|
||||
body.velocity.y = std::min(body.velocity.y, CLING_SPEED);
|
||||
slideWall = wall;
|
||||
if (slideTimer <= 0)
|
||||
game.effects.burst(SLIDE_DUST, wall_point_get(wall), wall > 0 ? PI : 0, game.color_get(index));
|
||||
sim.cues->burst_spawn(SLIDE_DUST, wall_point_get(wall), wall > 0 ? PI : 0, index);
|
||||
slideTimer = slideTimer <= 0 ? SLIDE_DUST_INTERVAL : slideTimer;
|
||||
}
|
||||
|
||||
Control away = wall > 0 ? CONTROL_LEFT : CONTROL_RIGHT;
|
||||
if (level >= 2 && kickTimer <= 0 && is_pressed(away))
|
||||
{
|
||||
wall_push(game, wall, KICK_VELOCITY);
|
||||
wall_push(sim, wall, KICK_VELOCITY);
|
||||
kickTimer = KICK_COOLDOWN;
|
||||
isJumping = false;
|
||||
}
|
||||
if (level >= 3 && jumpBuffer > 0)
|
||||
{
|
||||
wall_push(game, wall, WALL_JUMP_VELOCITY);
|
||||
wall_push(sim, wall, WALL_JUMP_VELOCITY);
|
||||
jumpBuffer = 0;
|
||||
isJumping = true;
|
||||
}
|
||||
@@ -494,20 +481,20 @@ void Fighter::climb_update(Game& game)
|
||||
|
||||
// Pushes off the wall with at least the base velocity. A hard hit into the wall in the last moment is reflected
|
||||
// back out, a little stronger, with some of it turned into extra lift.
|
||||
void Fighter::wall_push(Game& game, int wall, Vec2 base)
|
||||
void Fighter::wall_push(Sim& sim, int wall, Vec2 base)
|
||||
{
|
||||
float outward = std::max(base.x, wallSpeed * WALL_REFLECT);
|
||||
body.velocity = {-wall * outward, base.y - wallSpeed * WALL_LIFT};
|
||||
wallSpeed = 0;
|
||||
kickPose = KICK_POSE_TIME;
|
||||
game.audio.play(SOUND_JUMP);
|
||||
game.effects.burst(KICK_DUST, wall_point_get(wall), wall > 0 ? PI : 0, game.color_get(index));
|
||||
sim.cues->sound_play(SOUND_JUMP);
|
||||
sim.cues->burst_spawn(KICK_DUST, wall_point_get(wall), wall > 0 ? PI : 0, index);
|
||||
}
|
||||
|
||||
// Launched fighters bounce off whatever stopped them, keeping most of their speed, with a thud, dust and shake that
|
||||
// grow with the impact. Hard impacts slam them for damage, credited to their launcher. The launch ends once they've
|
||||
// slowed down.
|
||||
void Fighter::bounce_resolve(Game& game, Vec2 before)
|
||||
void Fighter::bounce_resolve(Sim& sim, Vec2 before)
|
||||
{
|
||||
float impact = 0;
|
||||
Vec2 normal = body.normal;
|
||||
@@ -532,13 +519,13 @@ void Fighter::bounce_resolve(Game& game, Vec2 before)
|
||||
}
|
||||
if (impact > 0)
|
||||
{
|
||||
game.audio.play(SOUND_BOUNCE);
|
||||
game.effects.burst(BOUNCE_DUST, center_get(), 0, game.color_get(index));
|
||||
game.effects.shake_add(impact * BOUNCE_SHAKE);
|
||||
sim.cues->sound_play(SOUND_BOUNCE);
|
||||
sim.cues->burst_spawn(BOUNCE_DUST, center_get(), 0, index);
|
||||
sim.cues->shake_add(impact * BOUNCE_SHAKE);
|
||||
}
|
||||
int slam = (impact - SLAM_SPEED) * SLAM_DAMAGE;
|
||||
if (slam > 0)
|
||||
game.weapons.damage_deal(game, {.point = center_get(),
|
||||
sim.weapons.damage_deal(sim, {.point = center_get(),
|
||||
.source = center_get(),
|
||||
.knock = 0,
|
||||
.amount = slam,
|
||||
@@ -550,9 +537,9 @@ void Fighter::bounce_resolve(Game& game, Vec2 before)
|
||||
|
||||
// Remembers where the feet last stood with solid support under both edges, and puts the fighter back there, still and
|
||||
// in a puff, when it touches a pit.
|
||||
void Fighter::safe_update(Game& game)
|
||||
void Fighter::safe_update(Sim& sim)
|
||||
{
|
||||
World& world = game.world;
|
||||
World& world = sim.world;
|
||||
Rect box = body.box;
|
||||
float feet = box.y + box.h;
|
||||
auto isSupported = [&](float x) {
|
||||
@@ -564,15 +551,15 @@ void Fighter::safe_update(Game& game)
|
||||
if (!world.is_touching(box, TILE_PIT))
|
||||
return;
|
||||
|
||||
game.effects.burst(LAND_DUST, center_get(), 0, game.color_get(index));
|
||||
game.audio.play(SOUND_FALL);
|
||||
charge_cancel(game);
|
||||
sim.cues->burst_spawn(LAND_DUST, center_get(), 0, index);
|
||||
sim.cues->sound_play(SOUND_FALL);
|
||||
charge_cancel(sim);
|
||||
body.box.x = safe.x - box.w / 2;
|
||||
body.box.y = safe.y - box.h;
|
||||
body.velocity = {};
|
||||
cornerPrevious = {body.box.x, body.box.y};
|
||||
isLaunched = false;
|
||||
game.effects.burst(LAND_DUST, center_get(), 0, game.color_get(index));
|
||||
sim.cues->burst_spawn(LAND_DUST, center_get(), 0, index);
|
||||
}
|
||||
|
||||
// Where the player touches the wall on the given side, around hand height.
|
||||
@@ -583,14 +570,14 @@ Vec2 Fighter::wall_point_get(int wall)
|
||||
}
|
||||
|
||||
// Which side a wall touches the player on: -1 left, 1 right, 0 neither.
|
||||
int Fighter::wall_get(Game& game)
|
||||
int Fighter::wall_get(Sim& sim)
|
||||
{
|
||||
Rect box = body.box;
|
||||
float middle = box.y + box.h / 2;
|
||||
float low = box.y + box.h - 2;
|
||||
auto isWall = [&](float x) {
|
||||
return game.world.is_solid_at({x, middle}) ||
|
||||
(game.world.tile_at({x, low}) == TILE_SLOPE && game.world.is_solid_at({x, low}));
|
||||
return sim.world.is_solid_at({x, middle}) ||
|
||||
(sim.world.tile_at({x, low}) == TILE_SLOPE && sim.world.is_solid_at({x, low}));
|
||||
};
|
||||
if (isWall(box.x - 1))
|
||||
return -1;
|
||||
@@ -603,22 +590,21 @@ int Fighter::wall_get(Game& game)
|
||||
// charges and fires on release, falling back to a normal attack if let go early. Charge lives on the player so it
|
||||
// survives weapon swaps. A fresh press of either lets go of a grapple, keeping the swing's momentum. Charge calls the
|
||||
// boomerangs back once they're all out, and either reels in a flail.
|
||||
void Fighter::charge_update(Game& game)
|
||||
void Fighter::charge_update(Sim& sim)
|
||||
{
|
||||
Audio& audio = game.audio;
|
||||
Attack* hook = game.weapons.hook_get(*this);
|
||||
Attack* hook = sim.weapons.hook_get(*this);
|
||||
bool isLeftPressed = is_pressed(CONTROL_ATTACK);
|
||||
bool isRightPressed = is_pressed(CONTROL_CHARGE);
|
||||
if (is_released(CONTROL_ATTACK))
|
||||
game.weapons.wisps_release(*this);
|
||||
sim.weapons.wisps_release(*this);
|
||||
|
||||
if (hook && (isLeftPressed || isRightPressed))
|
||||
{
|
||||
game.weapons.attacks_release(*this, ATTACK_HOOK);
|
||||
sim.weapons.attacks_release(*this, ATTACK_HOOK);
|
||||
isFireBlocked = isLeftPressed;
|
||||
return;
|
||||
}
|
||||
if (game.weapons.is_recalled(game, *this))
|
||||
if (sim.weapons.is_recalled(sim, *this))
|
||||
{
|
||||
isFireBlocked = isLeftPressed;
|
||||
return;
|
||||
@@ -626,7 +612,7 @@ void Fighter::charge_update(Game& game)
|
||||
|
||||
isFireBlocked &= is_down(CONTROL_ATTACK);
|
||||
if (is_down(CONTROL_ATTACK) && !isFireBlocked)
|
||||
game.weapons.fire(game, *this, 0);
|
||||
sim.weapons.fire(sim, *this, 0);
|
||||
|
||||
if (isRightPressed && weapon_get() >= 0)
|
||||
isCharging = true;
|
||||
@@ -636,58 +622,24 @@ void Fighter::charge_update(Game& game)
|
||||
float previous = charge;
|
||||
charge = std::min(1.0f, charge + TICK / CHARGE_TIME * trinket_get(TRINKET_DRINK, 1));
|
||||
if (previous < CHARGE_VISIBLE && charge >= CHARGE_VISIBLE)
|
||||
chargeVoice = audio.play(SOUND_CHARGE);
|
||||
sim.cues->charge_play(*this);
|
||||
if (charge >= 1)
|
||||
{
|
||||
audio.stop(chargeVoice);
|
||||
chargeVoice = -1;
|
||||
audio.play(SOUND_READY);
|
||||
game.effects.burst(READY_BURST, hand_get(), 0, game.color_get(index));
|
||||
sim.cues->charge_stop(*this);
|
||||
sim.cues->sound_play(SOUND_READY);
|
||||
sim.cues->burst_spawn(READY_BURST, hand_get(), 0, index);
|
||||
}
|
||||
}
|
||||
|
||||
if (isCharging && is_released(CONTROL_CHARGE))
|
||||
{
|
||||
game.weapons.fire(game, *this, charge);
|
||||
audio.stop(chargeVoice);
|
||||
chargeVoice = -1;
|
||||
sim.weapons.fire(sim, *this, charge);
|
||||
sim.cues->charge_stop(*this);
|
||||
charge = 0;
|
||||
isCharging = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Draws the fighter between its weapons, all shaking and filling in with the bonus its attacks would get right now.
|
||||
// Behind it, in a race, a glow rising from the ground it would respawn on (its own only). Over it, the reach of an Iron
|
||||
// Weights slam about to land. The local fighter also sees where its weapon would fire.
|
||||
void Fighter::draw(Game& game)
|
||||
{
|
||||
if (!isAlive)
|
||||
return;
|
||||
Renderer& renderer = game.renderer;
|
||||
Color color = game.color_get(index);
|
||||
bool isLocal = index == game.local && game.devices[index] != DEVICE_NONE;
|
||||
if (isLocal && game.match.is_racing())
|
||||
for (int row = 0; row < CHECKPOINT_HEIGHT; row++)
|
||||
renderer.rect_draw({std::round(safe.x - BOX_WIDTH / 2), std::round(safe.y) - row - 1, BOX_WIDTH, 1},
|
||||
color_fade(color, CHECKPOINT_ALPHA * (1 - row / CHECKPOINT_HEIGHT)));
|
||||
|
||||
float fill = game.weapons.buff_get(*this);
|
||||
Vec2 camera = renderer.camera;
|
||||
renderer.camera = camera + game.weapons.jitter_get(fill);
|
||||
Pose pose = pose_get(game);
|
||||
pose.placement.flash = std::max(pose.placement.flash, fill);
|
||||
game.weapons.offhand_draw(game, *this);
|
||||
pose.draw(renderer);
|
||||
game.weapons.held_draw(game, *this);
|
||||
renderer.camera = camera;
|
||||
|
||||
float pound = pound_area_get();
|
||||
if (pound > 0)
|
||||
renderer.ring_draw({center_get().x, body.box.y + body.box.h}, pound, color_fade(color, POUND_RING_ALPHA));
|
||||
if (isLocal && !game.menu.isOpen && game.settings.isAimShown)
|
||||
game.weapons.trajectory_draw(game, *this);
|
||||
}
|
||||
|
||||
// How much of a trinket's own cooldown is left, from 1 just used to 0 ready. Wings dashes and Climb Claw kicks
|
||||
// have one, and every other trinket is always 0.
|
||||
float Fighter::trinket_cooldown_get(int trinket)
|
||||
@@ -737,10 +689,9 @@ void Fighter::burst_add(Vec2 impulse)
|
||||
}
|
||||
|
||||
// Drops any charge in progress and silences its sound.
|
||||
void Fighter::charge_cancel(Game& game)
|
||||
void Fighter::charge_cancel(Sim& sim)
|
||||
{
|
||||
game.audio.stop(chargeVoice);
|
||||
chargeVoice = -1;
|
||||
sim.cues->charge_stop(*this);
|
||||
charge = 0;
|
||||
isCharging = false;
|
||||
}
|
||||
@@ -907,81 +858,6 @@ void Fighter::blend(float alpha)
|
||||
body.box.y = math_lerp(cornerPrevious.y, body.box.y, alpha);
|
||||
}
|
||||
|
||||
// The animation for what the fighter is doing: tumbling when launched, kicking off or sliding down a wall, rolling,
|
||||
// crawling, crouching, jumping or falling, walking, or standing.
|
||||
const char* Fighter::animation_get()
|
||||
{
|
||||
if (isLaunched)
|
||||
return "Hurt";
|
||||
if (kickPose > 0)
|
||||
return "Kick";
|
||||
if (slideWall != 0)
|
||||
return "Slide";
|
||||
if (isSliding)
|
||||
return "Roll";
|
||||
if (is_crawling())
|
||||
return CRAWL_ANIMATIONS[body.velocity.x < 0];
|
||||
if (isCrouching)
|
||||
return "Crouch";
|
||||
if (!body.isGrounded)
|
||||
return body.velocity.y < 0 ? "Jump" : "Fall";
|
||||
if (std::abs(body.velocity.x) > STEP_SPEED)
|
||||
return WALK_ANIMATIONS[body.velocity.x < 0];
|
||||
return "Idle";
|
||||
}
|
||||
|
||||
static bool is_pair(const char* const (&pair)[2], const char* animation)
|
||||
{
|
||||
return animation == pair[0] || animation == pair[1];
|
||||
}
|
||||
|
||||
// Plays the fighter's animation for this tick. Walking and crawling cycle one frame per stride of ground covered, the
|
||||
// rest play in time. Turning round mid-stride carries on the cycle in the other direction's animation.
|
||||
void Fighter::animator_update(Animator& animator)
|
||||
{
|
||||
const char* animation = animation_get();
|
||||
bool isWalking = is_pair(WALK_ANIMATIONS, animation);
|
||||
bool isCrawling = is_pair(CRAWL_ANIMATIONS, animation);
|
||||
bool isTurning = isWalking && is_pair(WALK_ANIMATIONS, animator.animation) ||
|
||||
isCrawling && is_pair(CRAWL_ANIMATIONS, animator.animation);
|
||||
float stride = isWalking ? WALK_STRIDE : isCrawling ? CRAWL_STRIDE : 0;
|
||||
float frame = animator.frame;
|
||||
animator.play(animation);
|
||||
if (isTurning)
|
||||
animator.frame = frame;
|
||||
animator.update(stride > 0 ? std::abs(body.velocity.x) * TICK / stride : TICK * animator.art->fps);
|
||||
}
|
||||
|
||||
// The fighter's animation placed where it is right now, in its colour: feet on the ground facing its way (walking and
|
||||
// crawling face the way they move), facing away from a wall it kicks off or slides down, spinning about its middle when
|
||||
// launched, and rolling as a ball resting on the ground along the surface's perpendicular. Shared with afterimages.
|
||||
Pose Fighter::pose_get(Game& game)
|
||||
{
|
||||
Animator& animator = game.sprites.fighters[index];
|
||||
bool isDirected = is_pair(WALK_ANIMATIONS, animator.animation) || is_pair(CRAWL_ANIMATIONS, animator.animation);
|
||||
float side = is_left() && !isDirected ? -1 : 1;
|
||||
Draw draw = {.color = game.color_get(index),
|
||||
.position = {center_get().x, body.box.y + body.box.h},
|
||||
.scale = {side * scale, scale},
|
||||
.flash = flash};
|
||||
if (isLaunched)
|
||||
{
|
||||
draw.position = center_get();
|
||||
draw.angle = tumble;
|
||||
}
|
||||
else if (kickPose > 0)
|
||||
draw.scale.x = (body.velocity.x < 0 ? -1 : 1) * scale;
|
||||
else if (slideWall != 0)
|
||||
draw.scale.x = -slideWall * scale;
|
||||
else if (isSliding)
|
||||
{
|
||||
Vec2 up = body.isGrounded ? vec2_normalize({body.slope, -1}) : Vec2{0, -1};
|
||||
draw.position = draw.position + up * (ROLL_RADIUS * scale);
|
||||
draw.angle = roll;
|
||||
}
|
||||
return animator.pose_get(draw);
|
||||
}
|
||||
|
||||
Vec2 Fighter::center_get()
|
||||
{
|
||||
return {body.box.x + body.box.w / 2, body.box.y + body.box.h / 2};
|
||||
@@ -1004,11 +880,6 @@ bool Fighter::is_crawling()
|
||||
return isCrouching && !isSliding && std::abs(body.velocity.x) > STEP_SPEED / 2;
|
||||
}
|
||||
|
||||
bool Fighter::is_flashing()
|
||||
{
|
||||
return charge >= 1 && SDL_GetTicks() / BLINK_MILLISECONDS % 2;
|
||||
}
|
||||
|
||||
bool Fighter::is_left()
|
||||
{
|
||||
return isFacingLeft;
|
||||
|
||||
+23
-23
@@ -1,12 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "anm2.h"
|
||||
#include "input.h"
|
||||
#include "renderer.h"
|
||||
#include "trinket.h"
|
||||
#include "world.h"
|
||||
|
||||
constexpr float CRAWL_STRIDE = 5;
|
||||
constexpr float ROLL_RADIUS = 10;
|
||||
constexpr float STEP_SPEED = 40;
|
||||
constexpr float WALK_STRIDE = 7;
|
||||
|
||||
struct Attack;
|
||||
struct Sim;
|
||||
|
||||
constexpr int ECHO_MAX = 64;
|
||||
constexpr int MAX_FIGHTERS = SPAWN_COUNT;
|
||||
@@ -53,6 +57,8 @@ constexpr float ABSORB_KNOCK_SHARE = 2 / 3.0f;
|
||||
// ground until it slows.
|
||||
class Fighter
|
||||
{
|
||||
friend class Scene;
|
||||
|
||||
public:
|
||||
Body body;
|
||||
Controls controls;
|
||||
@@ -90,19 +96,18 @@ public:
|
||||
bool isPvp = false;
|
||||
bool isSliding = false;
|
||||
|
||||
void init(Game& game, int number);
|
||||
void state_reset(Game& game, float maxHealth);
|
||||
void position_reset(Game& game);
|
||||
void position_set(Game& game, Vec2 feet);
|
||||
void init(Sim& sim, int number);
|
||||
void state_reset(Sim& sim, float maxHealth);
|
||||
void position_reset(Sim& sim);
|
||||
void position_set(Sim& sim, Vec2 feet);
|
||||
void slots_set(int weapons, int trinkets);
|
||||
void damage_take(Game& game, Damage damage);
|
||||
void update(Game& game);
|
||||
void draw(Game& game);
|
||||
void damage_take(Sim& sim, Damage damage);
|
||||
void update(Sim& sim);
|
||||
void blend(float alpha);
|
||||
void impulse_add(Vec2 impulse, bool isDamped = true);
|
||||
void teleport_to(Vec2 centre);
|
||||
void burst_add(Vec2 impulse);
|
||||
void charge_cancel(Game& game);
|
||||
void charge_cancel(Sim& sim);
|
||||
int weapon_get();
|
||||
int weapon_key(int weaponSlot);
|
||||
int off_slot_get();
|
||||
@@ -119,9 +124,6 @@ public:
|
||||
float trinket_cooldown_get(int trinket);
|
||||
float knock_absorb_get();
|
||||
float pound_area_get();
|
||||
const char* animation_get();
|
||||
void animator_update(Animator& animator);
|
||||
Pose pose_get(Game& game);
|
||||
Vec2 center_get();
|
||||
Vec2 hand_get();
|
||||
Vec2 offhand_get();
|
||||
@@ -130,9 +132,8 @@ public:
|
||||
bool is_pressed(Control control);
|
||||
bool is_released(Control control);
|
||||
bool is_trinket_equipped(int trinket);
|
||||
int wall_get(Game& game);
|
||||
int wall_get(Sim& sim);
|
||||
bool is_crawling();
|
||||
bool is_flashing();
|
||||
bool is_left();
|
||||
|
||||
private:
|
||||
@@ -151,7 +152,6 @@ private:
|
||||
float rollSpin = 0;
|
||||
float tumble = 0;
|
||||
float wallSpeed = 0;
|
||||
int chargeVoice = -1;
|
||||
int slideWall = 0;
|
||||
Uint16 buttonsPrevious = 0;
|
||||
bool isCharging = false;
|
||||
@@ -159,15 +159,15 @@ private:
|
||||
bool isFacingLeft = false;
|
||||
bool isJumping = false;
|
||||
|
||||
void slot_update(Game& game);
|
||||
void safe_update(Game& game);
|
||||
void slot_update(Sim& sim);
|
||||
void safe_update(Sim& sim);
|
||||
void size_update();
|
||||
void dash_update(Game& game, bool isJumpPressed);
|
||||
void climb_update(Game& game);
|
||||
void dash_update(Sim& sim, bool isJumpPressed);
|
||||
void climb_update(Sim& sim);
|
||||
Vec2 wall_point_get(int wall);
|
||||
void wall_push(Game& game, int wall, Vec2 base);
|
||||
void bounce_resolve(Game& game, Vec2 before);
|
||||
void wall_push(Sim& sim, int wall, Vec2 base);
|
||||
void bounce_resolve(Sim& sim, Vec2 before);
|
||||
void run_update(Attack* hook);
|
||||
void rope_update(Attack& hook);
|
||||
void charge_update(Game& game);
|
||||
void charge_update(Sim& sim);
|
||||
};
|
||||
+14
-23
@@ -2,78 +2,69 @@
|
||||
|
||||
#include "audio.h"
|
||||
#include "balance.h"
|
||||
#include "bot.h"
|
||||
#include "chat.h"
|
||||
#include "menu.h"
|
||||
#include "effects.h"
|
||||
#include "fighter.h"
|
||||
#include "input.h"
|
||||
#include "match.h"
|
||||
#include "net.h"
|
||||
#include "renderer.h"
|
||||
#include "rollback.h"
|
||||
#include "scene.h"
|
||||
#include "session.h"
|
||||
#include "settings.h"
|
||||
#include "sim.h"
|
||||
#include "sprites.h"
|
||||
#include "text.h"
|
||||
#include "trinket.h"
|
||||
#include "ui.h"
|
||||
#include "weapon.h"
|
||||
#include "world.h"
|
||||
|
||||
constexpr float PVP_OFF_SHADE = 0.4f;
|
||||
constexpr float AWAY_SHADE = 0.2f;
|
||||
|
||||
// Every manager in one place, so systems can reach each other through a single reference.
|
||||
// Devices say which input plays each fighter on this screen (none for bots). Fighters, the match and rng belong to the
|
||||
// simulation and are saved in snapshots. Local is the fighter this screen
|
||||
// Devices say which input plays each fighter on this screen (none for bots). Sim is the game being
|
||||
// played, saved in snapshots. Local is the fighter this screen
|
||||
// controls and follows. Camera, alpha, settings and the window are presentation. Alpha is how far drawing sits between
|
||||
// the previous tick and the current one. Net is the connection to the server and other players.
|
||||
struct Game
|
||||
{
|
||||
Audio audio;
|
||||
Balance balance;
|
||||
Bot bot;
|
||||
Chat chat;
|
||||
Menu menu;
|
||||
Effects effects;
|
||||
Fighter fighters[MAX_FIGHTERS];
|
||||
Input input;
|
||||
Match match;
|
||||
Net net;
|
||||
Renderer renderer;
|
||||
Rollback rollback;
|
||||
Settings settings;
|
||||
Scene scene{*this};
|
||||
Session session;
|
||||
Sim sim;
|
||||
Sprites sprites;
|
||||
Text text;
|
||||
Ui ui;
|
||||
Weapons weapons;
|
||||
World world;
|
||||
Rng rng;
|
||||
Vec2 camera;
|
||||
Vec2 cameraPrevious;
|
||||
float alpha = 1;
|
||||
int devices[MAX_FIGHTERS] = {DEVICE_KEYBOARD, DEVICE_NONE, DEVICE_NONE, DEVICE_NONE};
|
||||
int fighterCount = 1;
|
||||
int local = 0;
|
||||
SDL_Window* window = nullptr;
|
||||
|
||||
Fighter& local_get()
|
||||
{
|
||||
return fighters[local];
|
||||
return sim.fighters[local];
|
||||
}
|
||||
|
||||
// Whether a fighter is played on this screen, by its keyboard and mouse or one of its gamepads.
|
||||
bool is_local(int fighter)
|
||||
{
|
||||
return fighter < fighterCount && devices[fighter] != DEVICE_NONE && !fighters[fighter].isBot;
|
||||
return fighter < sim.fighterCount && devices[fighter] != DEVICE_NONE && !sim.fighters[fighter].isBot;
|
||||
}
|
||||
|
||||
int local_count()
|
||||
{
|
||||
int count = 0;
|
||||
for (int i = 0; i < fighterCount; i++)
|
||||
for (int i = 0; i < sim.fighterCount; i++)
|
||||
count += is_local(i);
|
||||
return count;
|
||||
}
|
||||
@@ -85,17 +76,17 @@ struct Game
|
||||
// plain (as the HUD's tint is), it's never darkened.
|
||||
Color color_get(int fighter, bool isPlain = false)
|
||||
{
|
||||
if (match.state == MATCH_PRACTICE && fighterCount == 1)
|
||||
if (sim.match.state == MATCH_PRACTICE && sim.fighterCount == 1)
|
||||
{
|
||||
int color = settings.color_get();
|
||||
return color >= 0 ? PALETTE[color] : WHITE;
|
||||
}
|
||||
Color color = PALETTE[match.colors[fighter]];
|
||||
Color color = PALETTE[sim.match.colors[fighter]];
|
||||
// Away playing a queued game, a fighter stands faded in the lobby.
|
||||
if (match.state == MATCH_PRACTICE && match.isAway[fighter] && !isPlain)
|
||||
if (sim.match.state == MATCH_PRACTICE && sim.match.isAway[fighter] && !isPlain)
|
||||
return color_shade(color, AWAY_SHADE);
|
||||
bool isHostile = fighters[fighter].isPvp && fighters[local].isPvp;
|
||||
bool isPeaceful = !isPlain && fighter != local && match.state == MATCH_PRACTICE && !isHostile;
|
||||
bool isHostile = sim.fighters[fighter].isPvp && sim.fighters[local].isPvp;
|
||||
bool isPeaceful = !isPlain && fighter != local && sim.match.state == MATCH_PRACTICE && !isHostile;
|
||||
return isPeaceful ? color_shade(color, PVP_OFF_SHADE) : color;
|
||||
}
|
||||
};
|
||||
|
||||
+34
-44
@@ -19,21 +19,21 @@ constexpr float CAMERA_FOLLOW = 0.15f;
|
||||
// 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 world = game.sim.world.size_get();
|
||||
Vec2 canvas = {CANVAS_WIDTH, CANVAS_HEIGHT};
|
||||
Vec2 center = {};
|
||||
int count = 0;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
{
|
||||
if (!game.is_local(i) || !game.fighters[i].isAlive)
|
||||
if (!game.is_local(i) || !game.sim.fighters[i].isAlive)
|
||||
continue;
|
||||
center = center + game.fighters[i].center_get();
|
||||
center = center + game.sim.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();
|
||||
center = game.sim.fighters[game.ui.spectated].center_get();
|
||||
count = 1;
|
||||
}
|
||||
Vec2 target = (count > 0 ? center * (1.0f / count) : game.local_get().center_get()) - canvas * 0.5f;
|
||||
@@ -48,18 +48,20 @@ 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.
|
||||
// One tick of the game this screen shows, with what it asks of the match offline: the menu's Play, Escape (or B)
|
||||
// leaving the results unless it's just closed the menu, and its player's colour. 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();
|
||||
Match& match = game.sim.match;
|
||||
Input& input = game.input;
|
||||
match.isOnline = game.session.isActive;
|
||||
match.isPlayRequested = input.isPlayRequested;
|
||||
match.isSandboxRequested = !game.menu.isOpen && !game.menu.isEscapeTaken &&
|
||||
(input.is_pressed(SDL_SCANCODE_ESCAPE) || input.is_pad_tapped(SDL_GAMEPAD_BUTTON_EAST));
|
||||
match.colorWanted = game.settings.color_get();
|
||||
game.sim.tick();
|
||||
input.tick_end();
|
||||
}
|
||||
|
||||
// Steps what only this screen sees, once per live tick: particles, HUD fades and the following camera.
|
||||
@@ -67,7 +69,7 @@ static void presentation_update(Game& game)
|
||||
{
|
||||
game.effects.update();
|
||||
game.ui.update(game);
|
||||
game.sprites.update(game);
|
||||
game.scene.update();
|
||||
game.cameraPrevious = game.camera;
|
||||
camera_update(game);
|
||||
}
|
||||
@@ -77,50 +79,37 @@ static void presentation_update(Game& game)
|
||||
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++)
|
||||
std::vector<Attack> attacks = game.sim.weapons.attacks;
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
{
|
||||
boxes[i] = game.fighters[i].body.box;
|
||||
game.fighters[i].blend(game.alpha);
|
||||
boxes[i] = game.sim.fighters[i].body.box;
|
||||
game.sim.fighters[i].blend(game.alpha);
|
||||
}
|
||||
game.weapons.blend(game.alpha);
|
||||
game.sim.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.scene.draw();
|
||||
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;
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
game.sim.fighters[i].body.box = boxes[i];
|
||||
game.sim.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.
|
||||
// Sets up the game, seeded from RPG_SEED (every number its own odd state) or the clock. Its cues go nowhere until a
|
||||
// window shows them.
|
||||
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);
|
||||
const char* seed = SDL_getenv("RPG_SEED");
|
||||
game.sim.init(seed ? (Uint32)SDL_atoi(seed) * 2 + 1 : (Uint32)SDL_GetPerformanceCounter() | 1);
|
||||
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.
|
||||
@@ -173,9 +162,9 @@ static bool frame_run(bool isDrawn = true)
|
||||
continue;
|
||||
}
|
||||
session.view_leave(game);
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
{
|
||||
Fighter& fighter = game.fighters[i];
|
||||
Fighter& fighter = game.sim.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(),
|
||||
@@ -265,6 +254,7 @@ int main(int argc, char** argv)
|
||||
game.ui.init(game);
|
||||
game.sprites.init(game);
|
||||
simulation_init(game);
|
||||
game.sim.cues = &game.scene;
|
||||
game.renderer.atlas_upload();
|
||||
game.settings.apply(game);
|
||||
game.settings.restore(game);
|
||||
|
||||
+155
-168
@@ -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);
|
||||
}
|
||||
+28
-22
@@ -3,7 +3,7 @@
|
||||
#include "fighter.h"
|
||||
#include "protocol.h"
|
||||
|
||||
struct Game;
|
||||
struct Sim;
|
||||
|
||||
constexpr int BEST_OF_MAX = 9;
|
||||
constexpr int PLAYERS_MAX = 4;
|
||||
@@ -197,33 +197,39 @@ public:
|
||||
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(Game& game);
|
||||
void start(Game& game);
|
||||
void colors_assign(Game& game);
|
||||
void sandbox_return(Game& game);
|
||||
void sandbox_join(Game& game, int device);
|
||||
void sandbox_leave(Game& game, int fighter);
|
||||
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(Game& game);
|
||||
bool is_final_round(Sim& sim);
|
||||
float time_limit_get();
|
||||
void queue_open(Game& game, int fighter, const Rules& rules);
|
||||
int queue_members_get(Game& game, int queuer, int* members);
|
||||
bool is_queue_full(Game& game, int queuer);
|
||||
int queue_startable_get(Game& game);
|
||||
bool is_queue_set(Game& game, int queuer);
|
||||
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(Game& game);
|
||||
int humans_left_get(Sim& sim);
|
||||
|
||||
private:
|
||||
void round_start(Game& game, bool isFirst);
|
||||
void fight_start(Game& game);
|
||||
void edit_take(Game& game, Fighter& fighter);
|
||||
void mirror_copy(Game& game, bool isTrinket);
|
||||
void deathmatch_update(Game& game);
|
||||
void race_update(Game& game);
|
||||
void queues_update(Game& game);
|
||||
void leaves_take(Game& game);
|
||||
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);
|
||||
};
|
||||
+35
-21
@@ -47,7 +47,7 @@ constexpr const char* TIMING_NAMES[] = {"First Round", "Every Round", "Period"};
|
||||
// The header's tab strip: the whole header, less room on the right for the match's buttons.
|
||||
static Rect tab_area_get(Game& game, Rect header)
|
||||
{
|
||||
bool isMatch = game.match.state != MATCH_PRACTICE;
|
||||
bool isMatch = game.sim.match.state != MATCH_PRACTICE;
|
||||
return {header.x, header.y, header.w - (isMatch ? HEADER_BUTTON_WIDTH * 2 : 0), header.h};
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ void Menu::update(Game& game)
|
||||
{
|
||||
Input& input = game.input;
|
||||
// Readying up while choosing closes the menu, back to the countdown with the loadout's tooltips.
|
||||
Match& match = game.match;
|
||||
Match& match = game.sim.match;
|
||||
bool isReady = match.state == MATCH_COUNTDOWN && match.isReady[game.local];
|
||||
if (isReady && !wasReady && (match.isWeaponPick || match.isTrinketPick))
|
||||
isOpen = false;
|
||||
@@ -95,7 +95,7 @@ void Menu::update(Game& game)
|
||||
game.audio.play(SOUND_SWAP);
|
||||
if (isOpen)
|
||||
{
|
||||
game.local_get().charge_cancel(game);
|
||||
game.local_get().charge_cancel(game.sim);
|
||||
targets_reset(game);
|
||||
}
|
||||
}
|
||||
@@ -190,7 +190,7 @@ void Menu::update(Game& game)
|
||||
|
||||
int step =
|
||||
(int)input.is_key_control_pressed(CONTROL_LEVEL_UP) - (int)input.is_key_control_pressed(CONTROL_LEVEL_DOWN);
|
||||
if (step != 0 && game.match.state == MATCH_PRACTICE)
|
||||
if (step != 0 && game.sim.match.state == MATCH_PRACTICE)
|
||||
level_step(game, step, mouse);
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ void Menu::pad_update(Game& game)
|
||||
isOpen = false;
|
||||
game.audio.play(isOpen ? SOUND_SELECT : SOUND_SWAP);
|
||||
}
|
||||
if (tab >= ITEM_TABS || game.match.state != MATCH_PRACTICE)
|
||||
if (tab >= ITEM_TABS || game.sim.match.state != MATCH_PRACTICE)
|
||||
return;
|
||||
int level = (int)input.is_pad_pressed(DEVICE_KEYBOARD, SDL_GAMEPAD_BUTTON_NORTH) -
|
||||
(int)input.is_pad_pressed(DEVICE_KEYBOARD, SDL_GAMEPAD_BUTTON_WEST);
|
||||
@@ -308,7 +308,7 @@ void Menu::hints_draw(Game& game, MenuLayout& layout)
|
||||
return;
|
||||
const char* hint = "A: Select B: Close LB/RB: Tabs";
|
||||
if (tab < ITEM_TABS)
|
||||
hint = game.match.state == MATCH_PRACTICE ? "A: Equip/Unequip X/Y: Level RS: Scroll Info LB/RB: Tabs"
|
||||
hint = game.sim.match.state == MATCH_PRACTICE ? "A: Equip/Unequip X/Y: Level RS: Scroll Info LB/RB: Tabs"
|
||||
: "A: Equip/Unequip RS: Scroll Info B: Close LB/RB: Tabs";
|
||||
else if (tab == TAB_PLAY && (page == PAGE_RULES || page == PAGE_QUEUE))
|
||||
hint = "A: Select Left/Right: Change B: Back Y: Join";
|
||||
@@ -376,7 +376,7 @@ void Menu::header_draw(Game& game, MenuLayout& layout)
|
||||
ui.tint);
|
||||
}
|
||||
|
||||
Match& match = game.match;
|
||||
Match& match = game.sim.match;
|
||||
if (match.state == MATCH_PRACTICE)
|
||||
return;
|
||||
Rect button = {strip.x + strip.w, strip.y + PADDING / 2, HEADER_BUTTON_WIDTH - PADDING, strip.h - PADDING};
|
||||
@@ -398,7 +398,7 @@ void Menu::header_draw(Game& game, MenuLayout& layout)
|
||||
isOpen = false;
|
||||
}
|
||||
else if (isForfeitArmed)
|
||||
match.sandbox_return(game);
|
||||
match.sandbox_return(game.sim);
|
||||
isForfeitArmed = !isForfeitArmed;
|
||||
}
|
||||
|
||||
@@ -468,7 +468,7 @@ void Menu::items_draw(Game& game, MenuLayout& layout)
|
||||
described = shown;
|
||||
Tooltip tooltip = ui.tooltip_build(game, isTrinket, item, level);
|
||||
Rect area = layout.description;
|
||||
bool isLevelable = game.match.state == MATCH_PRACTICE;
|
||||
bool isLevelable = game.sim.match.state == MATCH_PRACTICE;
|
||||
if (isLevelable)
|
||||
area.h -= WIDGET_HEIGHT + LEVEL_ROW_GAP;
|
||||
ui.tooltip_page_draw(game, description, area, tooltip);
|
||||
@@ -513,7 +513,7 @@ void Menu::play_draw(Game& game, MenuLayout& layout)
|
||||
if (page == PAGE_RULES || page == PAGE_QUEUE)
|
||||
{
|
||||
bool isQueue = page == PAGE_QUEUE;
|
||||
rules_draw(game, area, isQueue ? queueRules : game.match.rules, isQueue);
|
||||
rules_draw(game, area, isQueue ? queueRules : game.sim.match.rules, isQueue);
|
||||
return;
|
||||
}
|
||||
if (page == PAGE_ONLINE)
|
||||
@@ -693,7 +693,7 @@ void Menu::online_draw(Game& game, Rect area)
|
||||
SDL_snprintf(line, sizeof(line), "- %s%s%s", name.c_str(), member.id == net.hostId ? " [Host]" : "",
|
||||
isSelf ? " [You]" : "");
|
||||
int fighter = session.fighter_get(game, member.id);
|
||||
Color color = fighter >= 0 && game.match.state == MATCH_PRACTICE ? game.color_get(fighter, true) : ui.tint;
|
||||
Color color = fighter >= 0 && game.sim.match.state == MATCH_PRACTICE ? game.color_get(fighter, true) : ui.tint;
|
||||
text.draw(game.renderer, line, {.color = color, .position = {area.x, y + TEXT_ROW_INSET}});
|
||||
float x = area.x + text.size_get(line).x + PADDING * 2;
|
||||
if (!isSelf && !member.isConnected)
|
||||
@@ -889,18 +889,32 @@ void Menu::play_button_draw(Game& game, Rect button, bool isQueue)
|
||||
void Menu::sandbox_players_join(Game& game)
|
||||
{
|
||||
Input& input = game.input;
|
||||
if (game.match.state != MATCH_PRACTICE || game.session.isActive)
|
||||
if (game.sim.match.state != MATCH_PRACTICE || game.session.isActive)
|
||||
return;
|
||||
// This runs in the tick, so it reads the presses since the last tick (taps), not since the last frame.
|
||||
auto is_back = [&](int device) {
|
||||
return (input.devices[device].taps >> SDL_GAMEPAD_BUTTON_BACK & 1) != 0;
|
||||
};
|
||||
for (int i = game.fighterCount - 1; i > 0; i--)
|
||||
if (game.devices[i] > DEVICE_KEYBOARD && is_back(game.devices[i]))
|
||||
game.match.sandbox_leave(game, i);
|
||||
for (int i = game.sim.fighterCount - 1; i > 0; i--)
|
||||
{
|
||||
int device = game.devices[i];
|
||||
if (device <= DEVICE_KEYBOARD || !is_back(device))
|
||||
continue;
|
||||
input.devices[device].isClaimed = false;
|
||||
game.sim.match.sandbox_leave(game.sim, i);
|
||||
std::copy(game.devices + i + 1, std::end(game.devices), game.devices + i);
|
||||
game.devices[MAX_FIGHTERS - 1] = DEVICE_NONE;
|
||||
}
|
||||
for (int device = DEVICE_KEYBOARD + 1; device < DEVICE_COUNT; device++)
|
||||
if (input.devices[device].pad && !input.devices[device].isClaimed && is_back(device))
|
||||
game.match.sandbox_join(game, device);
|
||||
{
|
||||
if (!input.devices[device].pad || input.devices[device].isClaimed || !is_back(device))
|
||||
continue;
|
||||
int index = game.sim.match.sandbox_join(game.sim);
|
||||
if (index < 0)
|
||||
continue;
|
||||
game.devices[index] = device;
|
||||
input.devices[device].isClaimed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Gamepads join the game from the rules: Y on an unclaimed one takes the first bot's slot, Back on a joined one hands
|
||||
@@ -908,7 +922,7 @@ void Menu::sandbox_players_join(Game& game)
|
||||
void Menu::players_join(Game& game)
|
||||
{
|
||||
Input& input = game.input;
|
||||
int players = game.match.rules.players;
|
||||
int players = game.sim.match.rules.players;
|
||||
for (int i = 0; i < MAX_FIGHTERS; i++)
|
||||
{
|
||||
int device = game.devices[i];
|
||||
@@ -945,7 +959,7 @@ void Menu::online_open()
|
||||
void Menu::choice_open(Game& game)
|
||||
{
|
||||
isOpen = true;
|
||||
tab = game.match.isWeaponPick ? TAB_WEAPONS : TAB_TRINKETS;
|
||||
tab = game.sim.match.isWeaponPick ? TAB_WEAPONS : TAB_TRINKETS;
|
||||
targets_reset(game);
|
||||
}
|
||||
|
||||
@@ -1005,7 +1019,7 @@ bool Menu::is_bars_open()
|
||||
// countdown that lets fighters choose either kind, and only Settings anywhere else.
|
||||
int Menu::tabs_get(Game& game, int* tabs)
|
||||
{
|
||||
Match& match = game.match;
|
||||
Match& match = game.sim.match;
|
||||
int count = 0;
|
||||
if (match.state == MATCH_PRACTICE)
|
||||
{
|
||||
@@ -1027,7 +1041,7 @@ int Menu::tabs_get(Game& game, int* tabs)
|
||||
// countdown lets fighters choose that kind (the other kind's tab is there to look at).
|
||||
bool Menu::is_choosable(Game& game, bool isTrinket)
|
||||
{
|
||||
Match& match = game.match;
|
||||
Match& match = game.sim.match;
|
||||
if (match.state == MATCH_PRACTICE)
|
||||
return true;
|
||||
return match.state == MATCH_COUNTDOWN && (isTrinket ? match.isTrinketPick : match.isWeaponPick);
|
||||
|
||||
+14
-1
@@ -1,13 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
#include <cstdint>
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct SDL_GLContextState;
|
||||
struct SDL_Window;
|
||||
|
||||
// Cosmetic randomness: particles, flicker, shake. Gameplay draws from Rng instead.
|
||||
inline float random_get(float min, float max)
|
||||
{
|
||||
return min + SDL_randf() * (max - min);
|
||||
}
|
||||
|
||||
// Absolute path to a file inside the resources folder next to the executable.
|
||||
inline std::string path_get(const char* relative)
|
||||
{
|
||||
return std::string(SDL_GetBasePath()) + "resources/" + relative;
|
||||
}
|
||||
|
||||
// One textured quad request. Region is in atlas pixels, origin is the pivot inside the region.
|
||||
struct Draw
|
||||
{
|
||||
|
||||
+12
-83
@@ -4,9 +4,6 @@
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
constexpr Uint32 FNV_OFFSET = 2166136261u;
|
||||
constexpr Uint32 FNV_PRIME = 16777619u;
|
||||
|
||||
void Rollback::init()
|
||||
{
|
||||
isTesting = SDL_getenv("RPG_ROLLBACK_TEST");
|
||||
@@ -21,103 +18,35 @@ void Rollback::update(Game& game)
|
||||
}
|
||||
|
||||
int slot = tick % ROLLBACK_DEPTH;
|
||||
snapshots[slot] = snapshot_save(game);
|
||||
snapshots[slot] = game.sim.snapshot_save();
|
||||
inputs[slot] = game.input;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
controls[slot][i] = game.fighters[i].controls;
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
controls[slot][i] = game.sim.fighters[i].controls;
|
||||
game_tick(game);
|
||||
tick++;
|
||||
if (tick < ROLLBACK_DEPTH)
|
||||
return;
|
||||
|
||||
Snapshot live = snapshot_save(game);
|
||||
Snapshot live = game.sim.snapshot_save();
|
||||
Input liveInput = game.input;
|
||||
Uint32 expected = checksum_get(game);
|
||||
Uint32 expected = game.sim.checksum_get();
|
||||
|
||||
int oldest = tick % ROLLBACK_DEPTH;
|
||||
snapshot_load(game, snapshots[oldest]);
|
||||
game.audio.isMuted = true;
|
||||
game.effects.isMuted = true;
|
||||
game.sim.snapshot_load(snapshots[oldest]);
|
||||
game.scene.isMuted = true;
|
||||
for (int i = 0; i < ROLLBACK_DEPTH; i++)
|
||||
{
|
||||
int replayed = (oldest + i) % ROLLBACK_DEPTH;
|
||||
game.input = inputs[replayed];
|
||||
for (int f = 0; f < game.fighterCount; f++)
|
||||
game.fighters[f].controls = controls[replayed][f];
|
||||
for (int f = 0; f < game.sim.fighterCount; f++)
|
||||
game.sim.fighters[f].controls = controls[replayed][f];
|
||||
game_tick(game);
|
||||
}
|
||||
game.audio.isMuted = false;
|
||||
game.effects.isMuted = false;
|
||||
game.scene.isMuted = false;
|
||||
|
||||
Uint32 result = checksum_get(game);
|
||||
Uint32 result = game.sim.checksum_get();
|
||||
if (result != expected)
|
||||
SDL_Log("Rollback desync at tick %d: live %08x, replayed %08x", tick, expected, result);
|
||||
snapshot_load(game, live);
|
||||
game.sim.snapshot_load(live);
|
||||
game.input = liveInput;
|
||||
}
|
||||
|
||||
Snapshot Rollback::snapshot_save(Game& game)
|
||||
{
|
||||
Snapshot snapshot = {.match = game.match,
|
||||
.weapons = game.weapons,
|
||||
.world = game.world,
|
||||
.rng = game.rng,
|
||||
.fighterCount = game.fighterCount};
|
||||
std::copy(std::begin(game.fighters), std::end(game.fighters), snapshot.fighters);
|
||||
std::copy(std::begin(game.bot.states), std::end(game.bot.states), snapshot.bots);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
void Rollback::snapshot_load(Game& game, Snapshot& snapshot)
|
||||
{
|
||||
std::copy(std::begin(snapshot.fighters), std::end(snapshot.fighters), game.fighters);
|
||||
std::copy(std::begin(snapshot.bots), std::end(snapshot.bots), game.bot.states);
|
||||
game.fighterCount = snapshot.fighterCount;
|
||||
game.weapons = snapshot.weapons;
|
||||
game.world = snapshot.world;
|
||||
game.rng = snapshot.rng;
|
||||
game.match = snapshot.match;
|
||||
}
|
||||
|
||||
static void hash_add(Uint32& hash, float value)
|
||||
{
|
||||
Uint32 bits;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
hash = (hash ^ bits) * FNV_PRIME;
|
||||
}
|
||||
|
||||
// A fingerprint of the simulation state that matters for a desync: the fighters, every attack, the dummies, the
|
||||
// match and the random generator.
|
||||
Uint32 Rollback::checksum_get(Game& game)
|
||||
{
|
||||
Uint32 hash = FNV_OFFSET;
|
||||
hash_add(hash, (float)game.match.state + game.match.timer);
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
{
|
||||
Fighter& fighter = game.fighters[i];
|
||||
for (float value : {fighter.body.box.x, fighter.body.box.y, fighter.body.velocity.x, fighter.body.velocity.y,
|
||||
fighter.aim.x, fighter.aim.y, fighter.charge, fighter.health, fighter.hitstop})
|
||||
hash_add(hash, value);
|
||||
for (float cooldown : fighter.cooldowns)
|
||||
hash_add(hash, cooldown);
|
||||
// What the menu's edits change: loadouts, levels, slot counts, stacking and PvP.
|
||||
for (int slot = 0; slot < SLOT_MAX; slot++)
|
||||
for (int value : {fighter.weaponLoadout[slot], fighter.trinketLoadout[slot], fighter.weaponSlotLevels[slot],
|
||||
fighter.trinketSlotLevels[slot]})
|
||||
hash_add(hash, (float)value);
|
||||
for (int level : fighter.weaponItemLevels)
|
||||
hash_add(hash, (float)level);
|
||||
for (int level : fighter.trinketItemLevels)
|
||||
hash_add(hash, (float)level);
|
||||
for (int value : {fighter.weaponSlots, fighter.trinketSlots, (int)fighter.isPvp})
|
||||
hash_add(hash, (float)value);
|
||||
}
|
||||
for (Attack& attack : game.weapons.attacks)
|
||||
for (float value : {attack.position.x, attack.position.y, attack.velocity.x, attack.velocity.y, attack.angle,
|
||||
attack.time, (float)attack.hitsLeft})
|
||||
hash_add(hash, value);
|
||||
for (Dummy& dummy : game.world.dummies)
|
||||
for (float value : {dummy.flash, dummy.wobble, dummy.wobbleSpeed})
|
||||
hash_add(hash, value);
|
||||
return (hash ^ game.rng.state) * FNV_PRIME;
|
||||
}
|
||||
+1
-22
@@ -1,30 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "bot.h"
|
||||
#include "menu.h"
|
||||
#include "input.h"
|
||||
#include "match.h"
|
||||
#include "fighter.h"
|
||||
#include "trinket.h"
|
||||
#include "weapon.h"
|
||||
#include "world.h"
|
||||
#include "sim.h"
|
||||
|
||||
struct Game;
|
||||
|
||||
constexpr int ROLLBACK_DEPTH = 8;
|
||||
|
||||
// Everything a simulation tick reads and writes, so the game can be rewound to it.
|
||||
struct Snapshot
|
||||
{
|
||||
BotState bots[MAX_FIGHTERS];
|
||||
Fighter fighters[MAX_FIGHTERS];
|
||||
Match match;
|
||||
Weapons weapons;
|
||||
World world;
|
||||
Rng rng;
|
||||
int fighterCount = 0;
|
||||
};
|
||||
|
||||
// Steps the simulation. With RPG_ROLLBACK_TEST set it also keeps the last few ticks' snapshots, inputs and fighter
|
||||
// controls, and every tick rewinds to the oldest, replays them muted and logs if the result differs from what happened
|
||||
// live.
|
||||
@@ -33,9 +15,6 @@ class Rollback
|
||||
public:
|
||||
void init();
|
||||
void update(Game& game);
|
||||
Snapshot snapshot_save(Game& game);
|
||||
void snapshot_load(Game& game, Snapshot& snapshot);
|
||||
Uint32 checksum_get(Game& game);
|
||||
|
||||
private:
|
||||
Snapshot snapshots[ROLLBACK_DEPTH];
|
||||
|
||||
+975
@@ -0,0 +1,975 @@
|
||||
#include "scene.h"
|
||||
#include "game.h"
|
||||
#include <SDL3/SDL_timer.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
constexpr int BLINK_MILLISECONDS = 80;
|
||||
|
||||
constexpr float TRAJECTORY_ALPHA = 0.12f;
|
||||
constexpr float TRAJECTORY_TAPER = 0.25f;
|
||||
|
||||
constexpr float LINK_WIDTH = 1;
|
||||
constexpr float SURFACE_WIDTH = 2;
|
||||
|
||||
constexpr float BLOCK_WARN = 1;
|
||||
constexpr Vec2 CORNER_DIRECTIONS[] = {{-1, -1}, {1, -1}, {1, 1}, {-1, 1}};
|
||||
constexpr int CORNER_SIZE = 2;
|
||||
constexpr float RAMP_OUTLINE = 2;
|
||||
|
||||
constexpr float CHECKPOINT_ALPHA = 0.3f;
|
||||
constexpr float CHECKPOINT_HEIGHT = 24;
|
||||
// Animations drawn facing their own way, picked by the way the fighter moves (right, then left). The rest are
|
||||
// mirrored to face left.
|
||||
constexpr const char* CRAWL_ANIMATIONS[] = {"Crawl Right", "Crawl Left"};
|
||||
constexpr float POUND_RING_ALPHA = 0.25f;
|
||||
constexpr const char* WALK_ANIMATIONS[] = {"Walk Right", "Walk Left"};
|
||||
|
||||
constexpr float ARROW_FADE = 0.6f;
|
||||
constexpr Burst AURA_BURST = {.life = 0.3f, .size = 2, .speed = 40, .spread = 0.3f, .count = 1};
|
||||
constexpr float BEAM_INNER = 4;
|
||||
constexpr float BEAM_OUTER = 10;
|
||||
constexpr float BOLT_JITTER = 4;
|
||||
constexpr float BOLT_SEGMENT = 8;
|
||||
constexpr float BOLT_WIDTH = 2;
|
||||
constexpr float BOOMERANG_SPIN = 20;
|
||||
constexpr float CHAIN_SPACING = 8;
|
||||
// How hard a charging weapon shakes against an attack filled in as far.
|
||||
constexpr float CHARGE_SHAKE = 0.5f;
|
||||
constexpr float COLLISION_ALPHA = 0.25f;
|
||||
constexpr float EXPLOSION_FLASH = 0.05f;
|
||||
constexpr float EXPLOSION_GROW = 4;
|
||||
constexpr float EXPLOSION_OUTLINE = 2;
|
||||
constexpr float FALLOFF_CURVE = 1.5f;
|
||||
constexpr float FALLOFF_DIM = 0.15f;
|
||||
constexpr float FALLOFF_STEP = 6;
|
||||
constexpr float FILL_JITTER = 2;
|
||||
constexpr float FILL_PARTICLES = 60;
|
||||
constexpr float FLAME_FADE = 3;
|
||||
constexpr float FLAME_GROW = 0.8f;
|
||||
constexpr float FLAME_SHRINK = 0.6f;
|
||||
constexpr float FLAME_STAGGER = 1.12f;
|
||||
constexpr float GRAPPLE_HANDLE = 7;
|
||||
constexpr float HELD_ICON_SCALE = 0.6f;
|
||||
constexpr float KICK_DECAY = 0.82f;
|
||||
constexpr float KICK_SWEEP = 0.1f;
|
||||
constexpr float MINE_BLINK = 0.6f;
|
||||
constexpr float MINE_BLINK_ON = 0.1f;
|
||||
constexpr float PIP_GAP = 3;
|
||||
constexpr float PIP_SIZE = 2;
|
||||
constexpr float PIP_SPIN = 8;
|
||||
constexpr float RELOAD_DROP = 5;
|
||||
constexpr float RELOAD_PUMP = 4;
|
||||
constexpr float RELOAD_START = 0.2f;
|
||||
constexpr float ROPE_INNER = 1;
|
||||
constexpr float ROPE_OUTER = 3;
|
||||
constexpr float TELEPORT_CORE = 0.4f;
|
||||
constexpr float TELEPORT_RING_ALPHA = 0.35f;
|
||||
constexpr float WHIP_INNER = 2;
|
||||
constexpr float WHIP_JITTER = 0.5f;
|
||||
constexpr float WHIP_OUTER = 5;
|
||||
constexpr float WISP_FRAME_STAGGER = 0.3f;
|
||||
|
||||
// A random offset for an empowered attack or fighter to shake by, wider the more its colour fills in, like a damage
|
||||
// number.
|
||||
static Vec2 jitter_get(float fill)
|
||||
{
|
||||
float reach = FILL_JITTER * fill;
|
||||
return {random_get(-reach, reach), random_get(-reach, reach)};
|
||||
}
|
||||
|
||||
// Sheds particles off the edge of an empowered shape, more the more its colour fills in. They take the shape's colour
|
||||
// and fill in just as far.
|
||||
static void aura_spawn(Game& game, Capsule shape, float fill, Color color)
|
||||
{
|
||||
Burst burst = AURA_BURST;
|
||||
burst.fill = fill;
|
||||
int count = FILL_PARTICLES * fill * TICK + random_get(0, 1);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
float angle = random_get(-PI, PI);
|
||||
Vec2 point = vec2_lerp(shape.from, shape.to, random_get(0, 1)) + vec2_polar(angle, shape.radius);
|
||||
game.effects.burst(burst, point, angle, color);
|
||||
}
|
||||
}
|
||||
|
||||
// A colour decayed to match a weakened strength: drained toward grey and darkened, faster than the damage drops so
|
||||
// weakened shots read clearly.
|
||||
static Color color_dim(Color color, float strength)
|
||||
{
|
||||
strength = math_pow(std::min(1.0f, strength), FALLOFF_CURVE);
|
||||
float grey = (color.r + color.g + color.b) / 3;
|
||||
Color washed = {math_lerp(grey, color.r, strength), math_lerp(grey, color.g, strength),
|
||||
math_lerp(grey, color.b, strength), color.a};
|
||||
return color_shade(washed, FALLOFF_DIM + (1 - FALLOFF_DIM) * strength);
|
||||
}
|
||||
|
||||
// The owner's colour (darker for secondary attacks), dimming as a shot's damage falls off.
|
||||
static Color owner_color_get(Game& game, Attack& attack)
|
||||
{
|
||||
return color_shade(game.color_get(attack.owner), attack.isSecondary ? SECONDARY_SHADE : 1);
|
||||
}
|
||||
|
||||
static Color tint_get(Game& game, Attack& attack)
|
||||
{
|
||||
return color_dim(owner_color_get(game, attack), strength_get(attack, attack.tip));
|
||||
}
|
||||
|
||||
// A path drawn as an outlined line: an outer width around a black core of the inner width, kinked by jitter.
|
||||
struct Stroke
|
||||
{
|
||||
std::vector<Vec2> points;
|
||||
float outer = 0;
|
||||
float inner = 0;
|
||||
float jitter = 0;
|
||||
};
|
||||
|
||||
|
||||
// A beam, rope or whip, each piece coloured by the strength there, dimming along it the way damage falls off. Beams
|
||||
// start as far along as they've travelled. Jitter kinks it at every piece, moving each frame so it crackles.
|
||||
static void strength_line_draw(Game& game, Attack& attack, Stroke stroke)
|
||||
{
|
||||
Color color = owner_color_get(game, attack);
|
||||
float base = attack.bonus * (1 + attack.ricochets * RICOCHET_BONUS);
|
||||
Vec2 shake = jitter_get(fill_get(base));
|
||||
std::vector<Vec2> points;
|
||||
std::vector<float> distances;
|
||||
float distance = attack.def()->kind == ATTACK_BEAM ? attack.travelled : 0;
|
||||
for (size_t i = 0; i + 1 < stroke.points.size(); i++)
|
||||
{
|
||||
Vec2 from = stroke.points[i];
|
||||
Vec2 delta = stroke.points[i + 1] - from;
|
||||
Vec2 normal = vec2_normalize({-delta.y, delta.x});
|
||||
float length = vec2_length(delta);
|
||||
int steps = length / FALLOFF_STEP + 1;
|
||||
for (int step = 0; step < steps; step++)
|
||||
{
|
||||
bool isEnd = i == 0 && step == 0;
|
||||
float nudge = isEnd ? 0 : random_get(-stroke.jitter, stroke.jitter);
|
||||
points.push_back(from + delta * ((float)step / steps) + normal * nudge + shake);
|
||||
distances.push_back(distance + length * (step + 0.5f) / steps);
|
||||
}
|
||||
distance += length;
|
||||
}
|
||||
if (stroke.points.empty())
|
||||
return;
|
||||
points.push_back(stroke.points.back() + shake);
|
||||
|
||||
for (int layer = 0; layer < 2; layer++)
|
||||
{
|
||||
for (size_t i = 0; i + 1 < points.size(); i++)
|
||||
{
|
||||
float strength = base * falloff_at(distances[i]);
|
||||
if (layer == 0)
|
||||
game.renderer.line_draw(points[i], points[i + 1], stroke.outer, color_dim(color, strength));
|
||||
else
|
||||
game.renderer.line_draw(points[i], points[i + 1], stroke.inner, color_shade(color, fill_get(strength)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A kinked line from one end of the capsule to the other, its radius as the width. Each kink is nudged sideways
|
||||
// by up to jitter.
|
||||
static void bolt_draw(Renderer& renderer, Capsule bolt, float jitter, Color color)
|
||||
{
|
||||
Vec2 delta = bolt.to - bolt.from;
|
||||
Vec2 normal = vec2_normalize({-delta.y, delta.x});
|
||||
int kinks = vec2_length(delta) / BOLT_SEGMENT + 1;
|
||||
Vec2 previous = bolt.from;
|
||||
|
||||
for (int k = 1; k <= kinks; k++)
|
||||
{
|
||||
float nudge = k == kinks ? 0 : random_get(-jitter, jitter);
|
||||
Vec2 point = bolt.from + delta * ((float)k / kinks) + normal * nudge;
|
||||
renderer.line_draw(previous, point, bolt.radius, color);
|
||||
previous = point;
|
||||
}
|
||||
}
|
||||
|
||||
// Fills every pixel inside any of the capsules, a row of runs at a time, so where they overlap stays even.
|
||||
static void capsules_fill(Renderer& renderer, Capsule* shapes, int count, Color color)
|
||||
{
|
||||
Rect bounds = {INFINITY, INFINITY, -INFINITY, -INFINITY};
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Capsule& shape = shapes[i];
|
||||
bounds.x = std::min({bounds.x, shape.from.x - shape.radius, shape.to.x - shape.radius});
|
||||
bounds.y = std::min({bounds.y, shape.from.y - shape.radius, shape.to.y - shape.radius});
|
||||
bounds.w = std::max({bounds.w, shape.from.x + shape.radius, shape.to.x + shape.radius});
|
||||
bounds.h = std::max({bounds.h, shape.from.y + shape.radius, shape.to.y + shape.radius});
|
||||
}
|
||||
auto isInside = [&](Vec2 point) {
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Vec2 line = shapes[i].to - shapes[i].from;
|
||||
float length = vec2_dot(line, line);
|
||||
float t = length > 0 ? std::clamp(vec2_dot(point - shapes[i].from, line) / length, 0.0f, 1.0f) : 0;
|
||||
if (vec2_length(point - (shapes[i].from + line * t)) <= shapes[i].radius)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (float y = std::floor(bounds.y); y <= bounds.h; y++)
|
||||
{
|
||||
float start = -INFINITY;
|
||||
for (float x = std::floor(bounds.x); x <= bounds.w + 1; x++)
|
||||
{
|
||||
bool isIn = x <= bounds.w && isInside({x + 0.5f, y + 0.5f});
|
||||
if (isIn && start == -INFINITY)
|
||||
start = x;
|
||||
if (!isIn && start != -INFINITY)
|
||||
{
|
||||
renderer.rect_draw({start, y, x - start, 1}, color);
|
||||
start = -INFINITY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The held weapon's turn away from the aim: a quick eased sweep from the kick to the follow-through, which then
|
||||
// settles back to the aim.
|
||||
static float kick_get(Fighter& fighter)
|
||||
{
|
||||
if (fighter.kickTime < KICK_SWEEP)
|
||||
{
|
||||
float t = fighter.kickTime / KICK_SWEEP;
|
||||
return math_lerp(fighter.kickFrom, fighter.kickTo, t * (2 - t));
|
||||
}
|
||||
return fighter.kickTo * math_pow(KICK_DECAY, (fighter.kickTime - KICK_SWEEP) / TICK);
|
||||
}
|
||||
|
||||
// A reloading weapon's pose through its cooldown: once the kick has played out it tips up to stand vertical, dips
|
||||
// down and back like a pump, then swings back to the aim just as it's ready.
|
||||
static void reload_pose(Fighter& fighter, int key, float cooldown, Draw& draw)
|
||||
{
|
||||
if (!WEAPON_DEFS[fighter.weaponLoadout[key]].isReloading || cooldown <= 0)
|
||||
return;
|
||||
float total = fighter.level_get(key).cooldown / fighter.trinket_get(TRINKET_GLOVES, 1);
|
||||
float t = std::clamp((1 - cooldown / total - RELOAD_START) / (1 - RELOAD_START), 0.0f, 1.0f);
|
||||
float weight = math_sin(PI * t);
|
||||
draw.angle = angle_lerp(draw.angle, -PI / 2, weight);
|
||||
draw.position.y += RELOAD_DROP * math_pow(weight, RELOAD_PUMP);
|
||||
}
|
||||
|
||||
// How far a fighter's weapons fill in with its colour: with the charge as it builds, or the speed bonus if that's more,
|
||||
// then once the charge is ready blinking between full and the speed bonus alone.
|
||||
static float charge_fill_get(Game& game, Fighter& fighter)
|
||||
{
|
||||
float buff = game.sim.weapons.buff_get(fighter);
|
||||
if (fighter.charge >= 1)
|
||||
return game.scene.is_flashing(fighter) ? 1.0f : buff;
|
||||
return std::max(buff, fighter.charge);
|
||||
}
|
||||
|
||||
static bool is_pair(const char* const (&pair)[2], const char* animation)
|
||||
{
|
||||
return animation == pair[0] || animation == pair[1];
|
||||
}
|
||||
|
||||
Scene::Scene(Game& game) : game(game)
|
||||
{
|
||||
std::fill(std::begin(chargeVoices), std::end(chargeVoices), -1);
|
||||
}
|
||||
|
||||
void Scene::sound_play(SoundId sound)
|
||||
{
|
||||
if (!isMuted)
|
||||
game.audio.play(sound);
|
||||
}
|
||||
|
||||
void Scene::charge_play(Fighter& fighter)
|
||||
{
|
||||
if (!isMuted)
|
||||
chargeVoices[fighter.index] = game.audio.play(SOUND_CHARGE);
|
||||
}
|
||||
|
||||
void Scene::charge_stop(Fighter& fighter)
|
||||
{
|
||||
game.audio.stop(chargeVoices[fighter.index]);
|
||||
chargeVoices[fighter.index] = -1;
|
||||
}
|
||||
|
||||
void Scene::burst_spawn(Burst burst, Vec2 position, float angle, int owner)
|
||||
{
|
||||
if (!isMuted)
|
||||
game.effects.burst(burst, position, angle, game.color_get(owner));
|
||||
}
|
||||
|
||||
void Scene::attack_burst_spawn(Burst burst, Vec2 position, float angle, Attack& attack)
|
||||
{
|
||||
if (!isMuted)
|
||||
game.effects.burst(burst, position, angle, tint_get(game, attack));
|
||||
}
|
||||
|
||||
void Scene::shake_add(float amount)
|
||||
{
|
||||
if (!isMuted)
|
||||
game.effects.shake_add(amount);
|
||||
}
|
||||
|
||||
void Scene::flash_spawn(Vec2 position, float angle, float size, int owner)
|
||||
{
|
||||
if (!isMuted)
|
||||
game.effects.flash_spawn(position, angle, size, game.color_get(owner));
|
||||
}
|
||||
|
||||
void Scene::fighter_afterimage_spawn(Fighter& fighter)
|
||||
{
|
||||
if (!isMuted)
|
||||
game.effects.afterimage_spawn(fighter_pose_get(fighter));
|
||||
}
|
||||
|
||||
void Scene::attack_afterimage_spawn(Attack& attack)
|
||||
{
|
||||
if (!isMuted)
|
||||
game.effects.afterimage_spawn(attack_pose(attack));
|
||||
}
|
||||
|
||||
void Scene::fighter_aura_spawn(Fighter& fighter)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
Rect box = fighter.body.box;
|
||||
float radius = box.w / 2;
|
||||
aura_spawn(game, {{box.x + radius, box.y + radius}, {box.x + radius, box.y + box.h - radius}, radius},
|
||||
game.sim.weapons.buff_get(fighter), game.color_get(fighter.index));
|
||||
}
|
||||
|
||||
void Scene::attack_aura_spawn(Attack& attack)
|
||||
{
|
||||
if (!isMuted)
|
||||
aura_spawn(game, {attack.position, attack.tip, attack.radius}, fill_get(strength_get(attack, attack.tip)),
|
||||
tint_get(game, attack));
|
||||
}
|
||||
|
||||
void Scene::number_spawn(Damage& damage)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
game.effects.number_spawn(damage.point, damage.amount, damage.target, game.color_get(damage.owner));
|
||||
game.effects.dps_record(damage.amount);
|
||||
}
|
||||
|
||||
// A level going up or down chimes, and outside the menu pops up over this screen's fighter.
|
||||
void Scene::level_step(bool isUp)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
game.audio.play(isUp ? SOUND_LEVEL_UP : SOUND_LEVEL_DOWN);
|
||||
if (game.menu.isOpen)
|
||||
return;
|
||||
Fighter& fighter = game.local_get();
|
||||
Vec2 head = {fighter.center_get().x, fighter.body.box.y};
|
||||
game.effects.popup_spawn(head, isUp ? "Level Up" : "Level Down", isUp ? game.ui.tint : game.ui.shade);
|
||||
}
|
||||
|
||||
// The menu closes for whoever plays a starting match on this screen (online, a queued game starting doesn't close a
|
||||
// bystander's).
|
||||
void Scene::match_start()
|
||||
{
|
||||
if (game.local_count() > 0)
|
||||
game.menu.isOpen = false;
|
||||
}
|
||||
|
||||
void Scene::choice_open()
|
||||
{
|
||||
if (game.local_count() == 1)
|
||||
game.menu.choice_open(game);
|
||||
}
|
||||
|
||||
void Scene::choice_close()
|
||||
{
|
||||
if (game.local_count() > 0)
|
||||
game.menu.isOpen = false;
|
||||
}
|
||||
|
||||
void Scene::sandbox_return()
|
||||
{
|
||||
game.menu.isOpen = false;
|
||||
}
|
||||
|
||||
// A fully charged fighter's weapons blink, by the clock rather than the game.
|
||||
bool Scene::is_flashing(Fighter& fighter)
|
||||
{
|
||||
return fighter.charge >= 1 && SDL_GetTicks() / BLINK_MILLISECONDS % 2;
|
||||
}
|
||||
|
||||
// Each tick, every fighter's animator plays whatever its fighter is doing.
|
||||
void Scene::update()
|
||||
{
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
animator_update(game.sim.fighters[i], game.sprites.fighters[i]);
|
||||
}
|
||||
|
||||
// The game between its last two ticks: the world, the bots' map (when debugging), afterimages, blasts under the
|
||||
// fighters, the fighters, their attacks, and the hit shapes when they're shown.
|
||||
void Scene::draw()
|
||||
{
|
||||
world_draw();
|
||||
bot_draw();
|
||||
game.effects.afterimage_draw(game);
|
||||
blasts_draw();
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
fighter_draw(game.sim.fighters[i]);
|
||||
attacks_draw();
|
||||
if (game.settings.isCollisionShown)
|
||||
collision_draw();
|
||||
}
|
||||
|
||||
// A very faint line along each path the held weapon's attack would take if fired now, fading out over the last quarter
|
||||
// of its length (so a path cut short by a wall fades too), ringing the blast it would make where it ends.
|
||||
void Scene::paths_draw(Fighter& fighter)
|
||||
{
|
||||
Color base = game.color_get(fighter.index);
|
||||
for (Path& path : game.sim.weapons.paths_get(game.sim, fighter))
|
||||
{
|
||||
int last = path.steps.size() - 1;
|
||||
for (int i = 0; i < last; i++)
|
||||
{
|
||||
float fade = std::clamp((last - i) / (last * TRAJECTORY_TAPER), 0.0f, 1.0f);
|
||||
game.renderer.line_draw(path.steps[i], path.steps[i + 1], 1, color_fade(base, TRAJECTORY_ALPHA * fade));
|
||||
}
|
||||
if (path.area > 0)
|
||||
game.renderer.ring_draw(path.steps.back(), path.area, color_fade(base, BLAST_RING_ALPHA));
|
||||
}
|
||||
}
|
||||
|
||||
void Scene::world_draw()
|
||||
{
|
||||
Renderer& renderer = game.renderer;
|
||||
World& world = game.sim.world;
|
||||
Anm2& tileArt = game.sprites.tiles;
|
||||
int startX = std::max(0, (int)std::floor(renderer.camera.x / TILE_SIZE));
|
||||
int startY = std::max(0, (int)std::floor(renderer.camera.y / TILE_SIZE));
|
||||
int endX = std::min(world.width - 1, (int)std::floor((renderer.camera.x + CANVAS_WIDTH) / TILE_SIZE));
|
||||
int endY = std::min(world.height - 1, (int)std::floor((renderer.camera.y + CANVAS_HEIGHT) / TILE_SIZE));
|
||||
|
||||
for (int y = startY; y <= endY; y++)
|
||||
{
|
||||
for (int x = startX; x <= endX; x++)
|
||||
{
|
||||
auto isOpen = [&](float dx, float dy) {
|
||||
Tile neighbour = world.tile_get(x + dx, y + dy);
|
||||
return neighbour != TILE_SOLID && neighbour != TILE_SLOPE;
|
||||
};
|
||||
auto isEmpty = [&](int dx) {
|
||||
return world.tile_get(x + dx, y) == TILE_EMPTY;
|
||||
};
|
||||
Tile tile = world.tile_get(x, y);
|
||||
Vec2 position = {(float)x * TILE_SIZE, (float)y * TILE_SIZE};
|
||||
|
||||
if (tile == TILE_SOLID)
|
||||
{
|
||||
int mask = isOpen(0, -1) | isOpen(1, 0) << 1 | isOpen(0, 1) << 2 | isOpen(-1, 0) << 3;
|
||||
tileArt.draw(renderer, "Wall", mask, {.position = position});
|
||||
|
||||
for (Vec2 corner : CORNER_DIRECTIONS)
|
||||
{
|
||||
if (isOpen(corner.x, 0) || isOpen(0, corner.y) || !isOpen(corner.x, corner.y))
|
||||
continue;
|
||||
float cornerX = corner.x < 0 ? position.x : position.x + TILE_SIZE - CORNER_SIZE;
|
||||
float cornerY = corner.y < 0 ? position.y : position.y + TILE_SIZE - CORNER_SIZE;
|
||||
renderer.rect_draw({cornerX, cornerY, CORNER_SIZE, CORNER_SIZE}, WHITE);
|
||||
}
|
||||
}
|
||||
else if (tile == TILE_PLATFORM)
|
||||
{
|
||||
int mask = isEmpty(1) | isEmpty(-1) << 1;
|
||||
tileArt.draw(renderer, "Platform", mask, {.position = position});
|
||||
}
|
||||
else if (tile == TILE_PIT || tile == TILE_FINISH)
|
||||
tileArt.draw(renderer, tile == TILE_PIT ? "Pit" : "Finish", 0, {.position = position});
|
||||
}
|
||||
}
|
||||
|
||||
// Ramps: an outline just inside the slope and its flat top, and a wall down the high end where it stands free.
|
||||
for (Ramp& ramp : world.ramps)
|
||||
{
|
||||
if (ramp.right < renderer.camera.x || ramp.left > renderer.camera.x + CANVAS_WIDTH)
|
||||
continue;
|
||||
float edge = ramp.anchor + (ramp.top - ramp.base) / ramp.slope;
|
||||
Vec2 low = {ramp.anchor, ramp.base};
|
||||
Vec2 high = {edge, ramp.top};
|
||||
Vec2 end = {ramp.slope < 0 ? ramp.right : ramp.left, ramp.top};
|
||||
Vec2 inset = vec2_normalize({ramp.slope, -1}) * (-RAMP_OUTLINE / 2);
|
||||
renderer.line_draw(low + inset, high + inset, RAMP_OUTLINE, WHITE);
|
||||
renderer.line_draw({high.x, high.y + RAMP_OUTLINE / 2}, {end.x, end.y + RAMP_OUTLINE / 2}, RAMP_OUTLINE, WHITE);
|
||||
|
||||
float side = ramp.slope < 0 ? 1 : -1;
|
||||
Vec2 beyond = {end.x + side * TILE_SIZE / 2, ramp.top + TILE_SIZE / 2};
|
||||
if (world.tile_at(beyond) != TILE_SOLID)
|
||||
renderer.line_draw({end.x - side * RAMP_OUTLINE / 2, ramp.top},
|
||||
{end.x - side * RAMP_OUTLINE / 2, ramp.base}, RAMP_OUTLINE, WHITE);
|
||||
}
|
||||
|
||||
// Blocks crack as they lose health, and a broken one shows a faint outline just before it grows back.
|
||||
for (Dummy& dummy : world.dummies)
|
||||
{
|
||||
if (dummy.tile < 0)
|
||||
continue;
|
||||
Vec2 position = {dummy.box.x, dummy.box.y};
|
||||
if (world.tiles[dummy.tile] == TILE_BLOCK)
|
||||
{
|
||||
int stage = std::min(BLOCK_STAGES - 1, (int)((1 - dummy.health / BLOCK_HEALTH) * BLOCK_STAGES));
|
||||
tileArt.draw(renderer, "Block", stage, {.position = position, .flash = dummy.flash});
|
||||
}
|
||||
else if (dummy.regrow < BLOCK_WARN)
|
||||
tileArt.draw(renderer, "Block", 0,
|
||||
{.color = color_fade(GRAY, 1 - dummy.regrow / BLOCK_WARN), .position = position});
|
||||
}
|
||||
|
||||
for (Dummy& dummy : world.dummies)
|
||||
{
|
||||
if (dummy.tile >= 0)
|
||||
continue;
|
||||
Vec2 base = {dummy.box.x + dummy.box.w / 2, dummy.box.y + dummy.box.h};
|
||||
game.sprites.dummy.draw(renderer, "Idle", 0, {.position = base, .angle = dummy.wobble, .flash = dummy.flash});
|
||||
}
|
||||
}
|
||||
|
||||
// Debug view: bot.surfaces as lines, bot.links as thin lines from takeoff to landing, and each bot's remaining path in white.
|
||||
void Scene::bot_draw()
|
||||
{
|
||||
Bot& bot = game.sim.bot;
|
||||
if (!bot.isDrawn)
|
||||
return;
|
||||
|
||||
Renderer& renderer = game.renderer;
|
||||
for (Surface& surface : bot.surfaces)
|
||||
renderer.line_draw({surface.left, surface.y_at(surface.left)}, {surface.right, surface.y_at(surface.right)},
|
||||
SURFACE_WIDTH, GRAY);
|
||||
for (Link& link : bot.links)
|
||||
renderer.line_draw({link.takeoff, bot.surfaces[link.from].y_at(link.takeoff)},
|
||||
{link.landing, bot.surfaces[link.to].y_at(link.landing)}, LINK_WIDTH,
|
||||
color_fade(GRAY, GOAL_SPAN));
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
{
|
||||
BotState& state = bot.states[i];
|
||||
if (!game.sim.fighters[i].isBot)
|
||||
continue;
|
||||
for (int step = state.pathStep; step < state.pathLength; step++)
|
||||
{
|
||||
Link& link = bot.links[state.path[step]];
|
||||
renderer.line_draw({link.takeoff, bot.surfaces[link.from].y_at(link.takeoff)},
|
||||
{link.landing, bot.surfaces[link.to].y_at(link.landing)}, SURFACE_WIDTH, WHITE);
|
||||
}
|
||||
renderer.circle_draw(state.goal, SURFACE_WIDTH * 2, WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
// Draws the fighter between its weapons, all shaking and filling in with the bonus its attacks would get right now.
|
||||
// Behind it, in a race, a glow rising from the ground it would respawn on (its own only). Over it, the reach of an Iron
|
||||
// Weights slam about to land. The local fighter also sees where its weapon would fire.
|
||||
void Scene::fighter_draw(Fighter& fighter)
|
||||
{
|
||||
if (!fighter.isAlive)
|
||||
return;
|
||||
Renderer& renderer = game.renderer;
|
||||
Color color = game.color_get(fighter.index);
|
||||
bool isLocal = fighter.index == game.local && game.devices[fighter.index] != DEVICE_NONE;
|
||||
if (isLocal && game.sim.match.is_racing())
|
||||
for (int row = 0; row < CHECKPOINT_HEIGHT; row++)
|
||||
renderer.rect_draw({std::round(fighter.safe.x - BOX_WIDTH / 2), std::round(fighter.safe.y) - row - 1, BOX_WIDTH, 1},
|
||||
color_fade(color, CHECKPOINT_ALPHA * (1 - row / CHECKPOINT_HEIGHT)));
|
||||
|
||||
float fill = game.sim.weapons.buff_get(fighter);
|
||||
Vec2 camera = renderer.camera;
|
||||
renderer.camera = camera + jitter_get(fill);
|
||||
Pose pose = fighter_pose_get(fighter);
|
||||
pose.placement.flash = std::max(pose.placement.flash, fill);
|
||||
offhand_draw(fighter);
|
||||
pose.draw(renderer);
|
||||
held_draw(fighter);
|
||||
renderer.camera = camera;
|
||||
|
||||
float pound = fighter.pound_area_get();
|
||||
if (pound > 0)
|
||||
renderer.ring_draw({fighter.center_get().x, fighter.body.box.y + fighter.body.box.h}, pound, color_fade(color, POUND_RING_ALPHA));
|
||||
if (isLocal && !game.menu.isOpen && game.settings.isAimShown)
|
||||
paths_draw(fighter);
|
||||
}
|
||||
|
||||
// The animation for what the fighter is doing: tumbling when launched, kicking off or sliding down a wall, rolling,
|
||||
// crawling, crouching, jumping or falling, walking, or standing.
|
||||
const char* Scene::animation_get(Fighter& fighter)
|
||||
{
|
||||
if (fighter.isLaunched)
|
||||
return "Hurt";
|
||||
if (fighter.kickPose > 0)
|
||||
return "Kick";
|
||||
if (fighter.slideWall != 0)
|
||||
return "Slide";
|
||||
if (fighter.isSliding)
|
||||
return "Roll";
|
||||
if (fighter.is_crawling())
|
||||
return CRAWL_ANIMATIONS[fighter.body.velocity.x < 0];
|
||||
if (fighter.isCrouching)
|
||||
return "Crouch";
|
||||
if (!fighter.body.isGrounded)
|
||||
return fighter.body.velocity.y < 0 ? "Jump" : "Fall";
|
||||
if (std::abs(fighter.body.velocity.x) > STEP_SPEED)
|
||||
return WALK_ANIMATIONS[fighter.body.velocity.x < 0];
|
||||
return "Idle";
|
||||
}
|
||||
|
||||
// Plays the fighter's animation for this tick. Walking and crawling cycle one frame per stride of ground covered, the
|
||||
// rest play in time. Turning round mid-stride carries on the cycle in the other direction's animation.
|
||||
void Scene::animator_update(Fighter& fighter, Animator& animator)
|
||||
{
|
||||
const char* animation = animation_get(fighter);
|
||||
bool isWalking = is_pair(WALK_ANIMATIONS, animation);
|
||||
bool isCrawling = is_pair(CRAWL_ANIMATIONS, animation);
|
||||
bool isTurning = isWalking && is_pair(WALK_ANIMATIONS, animator.animation) ||
|
||||
isCrawling && is_pair(CRAWL_ANIMATIONS, animator.animation);
|
||||
float stride = isWalking ? WALK_STRIDE : isCrawling ? CRAWL_STRIDE : 0;
|
||||
float frame = animator.frame;
|
||||
animator.play(animation);
|
||||
if (isTurning)
|
||||
animator.frame = frame;
|
||||
animator.update(stride > 0 ? std::abs(fighter.body.velocity.x) * TICK / stride : TICK * animator.art->fps);
|
||||
}
|
||||
|
||||
// The fighter's animation placed where it is right now, in its colour: feet on the ground facing its way (walking and
|
||||
// crawling face the way they move), facing away from a wall it kicks off or slides down, spinning about its middle when
|
||||
// launched, and rolling as a ball resting on the ground along the surface's perpendicular. Shared with afterimages.
|
||||
Pose Scene::fighter_pose_get(Fighter& fighter)
|
||||
{
|
||||
Animator& animator = game.sprites.fighters[fighter.index];
|
||||
bool isDirected = is_pair(WALK_ANIMATIONS, animator.animation) || is_pair(CRAWL_ANIMATIONS, animator.animation);
|
||||
float side = fighter.is_left() && !isDirected ? -1 : 1;
|
||||
Draw draw = {.color = game.color_get(fighter.index),
|
||||
.position = {fighter.center_get().x, fighter.body.box.y + fighter.body.box.h},
|
||||
.scale = {side * fighter.scale, fighter.scale},
|
||||
.flash = fighter.flash};
|
||||
if (fighter.isLaunched)
|
||||
{
|
||||
draw.position = fighter.center_get();
|
||||
draw.angle = fighter.tumble;
|
||||
}
|
||||
else if (fighter.kickPose > 0)
|
||||
draw.scale.x = (fighter.body.velocity.x < 0 ? -1 : 1) * fighter.scale;
|
||||
else if (fighter.slideWall != 0)
|
||||
draw.scale.x = -fighter.slideWall * fighter.scale;
|
||||
else if (fighter.isSliding)
|
||||
{
|
||||
Vec2 up = fighter.body.isGrounded ? vec2_normalize({fighter.body.slope, -1}) : Vec2{0, -1};
|
||||
draw.position = draw.position + up * (ROLL_RADIUS * fighter.scale);
|
||||
draw.angle = fighter.roll;
|
||||
}
|
||||
return animator.pose_get(draw);
|
||||
}
|
||||
|
||||
void Scene::attacks_draw()
|
||||
{
|
||||
Renderer& renderer = game.renderer;
|
||||
|
||||
for (Attack& attack : game.sim.weapons.attacks)
|
||||
{
|
||||
Fighter& fighter = game.sim.fighters[attack.owner];
|
||||
Color tint = tint_get(game, attack);
|
||||
AttackKind kind = attack.def()->kind;
|
||||
if (kind == ATTACK_HOOK)
|
||||
{
|
||||
// The rope runs from the end of the grapple's base in the hand.
|
||||
Vec2 hand = attack.isOffhand ? fighter.offhand_get() : fighter.hand_get();
|
||||
Vec2 base = hand + vec2_polar(vec2_angle(attack.position - hand), GRAPPLE_HANDLE);
|
||||
strength_line_draw(game, attack, {{base, attack.position}, ROPE_OUTER, ROPE_INNER});
|
||||
}
|
||||
|
||||
if (kind == ATTACK_SPIN || kind == ATTACK_FLAIL)
|
||||
{
|
||||
Vec2 hand = fighter.hand_get();
|
||||
Vec2 delta = attack.position - hand;
|
||||
int links = vec2_length(delta) / CHAIN_SPACING;
|
||||
for (int i = 1; i < links; i++)
|
||||
game.sprites.effects.draw(renderer, "Chain", 0,
|
||||
{.color = tint,
|
||||
.position = hand + delta * ((float)i / links),
|
||||
.flash = fill_get(strength_get(attack, attack.tip))});
|
||||
}
|
||||
|
||||
float progress = attack.time / attack.duration;
|
||||
if (kind == ATTACK_BEAM)
|
||||
{
|
||||
float width = (1 - progress) * attack.scale;
|
||||
strength_line_draw(game, attack, {{attack.position, attack.tip}, BEAM_OUTER * width, BEAM_INNER * width});
|
||||
}
|
||||
else if (kind == ATTACK_WHIP)
|
||||
{
|
||||
// A plain lash keeps its full width while it unfurls, then thins away after the crack. The tip sprite
|
||||
// rides the end of the whip, pointing the way it runs.
|
||||
Vec2 shake = jitter_get(fill_get(strength_get(attack, attack.tip)));
|
||||
float fade = attack.isCharged ? progress : std::max(0.0f, (progress - WHIP_UNFURL) / (1 - WHIP_UNFURL));
|
||||
float width = (1 - fade) * attack.scale;
|
||||
std::vector<Vec2> points =
|
||||
attack.isCharged ? std::vector<Vec2>{attack.position, attack.tip} : lash_get(attack);
|
||||
Vec2 end = points.back();
|
||||
Vec2 before = points[points.size() - 2];
|
||||
strength_line_draw(game, attack,
|
||||
{points, WHIP_OUTER * width, WHIP_INNER * width, attack.isCharged ? 0 : WHIP_JITTER});
|
||||
game.sprites.effects.draw(renderer, "Whip Tip", 0,
|
||||
{.color = color_fade(tint, 1 - fade),
|
||||
.position = end + shake,
|
||||
.scale = {attack.scale, attack.scale},
|
||||
.angle = vec2_angle(end - before)});
|
||||
}
|
||||
else if (kind != ATTACK_EXPLOSION)
|
||||
attack_pose(attack).draw(renderer);
|
||||
|
||||
// A faint ring shows how far anything that explodes would reach, and pips in the shot's colour circle it, one
|
||||
// for each ricochet it has left.
|
||||
if (kind == ATTACK_ROCKET || kind == ATTACK_FIREBALL || kind == ATTACK_MINE)
|
||||
renderer.ring_draw(attack.position, blast_area_get(attack), color_fade(tint, BLAST_RING_ALPHA));
|
||||
for (int i = 0; i < attack.bounces && kind != ATTACK_BEAM && !attack.isAttached; i++)
|
||||
{
|
||||
float angle = attack.time * PIP_SPIN + 2 * PI * i / attack.bounces;
|
||||
Vec2 pip = attack.position + vec2_polar(angle, attack.radius + PIP_GAP);
|
||||
renderer.rect_draw({std::round(pip.x - PIP_SIZE / 2), std::round(pip.y - PIP_SIZE / 2), PIP_SIZE, PIP_SIZE},
|
||||
tint);
|
||||
}
|
||||
}
|
||||
magic_draw();
|
||||
tesla_draw();
|
||||
}
|
||||
|
||||
// The Grimoire's teleport, drawn rather than sprited: a faint ring where its blast will go with a core swelling inside
|
||||
// it while it gathers. (Its wisps are drawn with the other attacks.)
|
||||
void Scene::magic_draw()
|
||||
{
|
||||
for (Attack& attack : game.sim.weapons.attacks)
|
||||
{
|
||||
AttackKind kind = attack.def()->kind;
|
||||
Color color = tint_get(game, attack);
|
||||
if (kind == ATTACK_TELEPORT && !attack.isDead)
|
||||
{
|
||||
float progress = std::min(1.0f, attack.time / attack.duration);
|
||||
float area = attack.stats.area * attack.scale;
|
||||
game.renderer.ring_draw(attack.position, area, color_fade(color, TELEPORT_RING_ALPHA));
|
||||
game.renderer.circle_draw(attack.position, area * TELEPORT_CORE * progress, color_fade(color, progress));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Each link is a jagged bolt whose kinks move every frame so it crackles.
|
||||
void Scene::tesla_draw()
|
||||
{
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
for (Capsule& link : game.sim.weapons.chain_get(game.sim, game.sim.fighters[i]))
|
||||
bolt_draw(game.renderer, {link.from, link.to, BOLT_WIDTH}, BOLT_JITTER, game.color_get(i));
|
||||
}
|
||||
|
||||
// The Display Collision setting: every attack's hit shapes as the hit test sees them, and the boxes fighters and
|
||||
// dummies are hit in, see-through in their owner's colour.
|
||||
void Scene::collision_draw()
|
||||
{
|
||||
Renderer& renderer = game.renderer;
|
||||
for (Attack& attack : game.sim.weapons.attacks)
|
||||
{
|
||||
Capsule shapes[WHIP_RAYS];
|
||||
int count = shapes_get(game.sim, attack, shapes);
|
||||
capsules_fill(renderer, shapes, count, color_fade(owner_color_get(game, attack), COLLISION_ALPHA));
|
||||
}
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
if (game.sim.fighters[i].isAlive)
|
||||
renderer.rect_draw(game.sim.fighters[i].body.box, color_fade(game.color_get(i), COLLISION_ALPHA));
|
||||
for (Dummy& dummy : game.sim.world.dummies)
|
||||
if (dummy.tile < 0)
|
||||
renderer.rect_draw(dummy.box, color_fade(WHITE, COLLISION_ALPHA));
|
||||
}
|
||||
|
||||
// Explosions draw before the player, so their filled centre never hides anyone standing in the blast.
|
||||
void Scene::blasts_draw()
|
||||
{
|
||||
for (Attack& attack : game.sim.weapons.attacks)
|
||||
{
|
||||
if (attack.def()->kind != ATTACK_EXPLOSION)
|
||||
continue;
|
||||
float progress = attack.time / attack.duration;
|
||||
float radius = attack.stats.range * std::min(1.0f, progress * EXPLOSION_GROW);
|
||||
Color color = color_dim(owner_color_get(game, attack), attack.bonus);
|
||||
float fill = fill_get(attack.bonus);
|
||||
Vec2 center = attack.position + jitter_get(fill);
|
||||
game.renderer.circle_draw(center, radius, color_fade(color, 1 - progress));
|
||||
if (attack.time > EXPLOSION_FLASH)
|
||||
game.renderer.circle_draw(center, radius - EXPLOSION_OUTLINE,
|
||||
color_fade(color_shade(color, fill), 1 - progress));
|
||||
}
|
||||
}
|
||||
|
||||
// How a sprite-based attack looks right now: its weapon, or the projectile or effect it fired. Shared by the live
|
||||
// attack and its afterimages.
|
||||
Pose Scene::attack_pose(Attack& attack)
|
||||
{
|
||||
const AttackDef& def = *attack.def();
|
||||
float progress = attack.time / attack.duration;
|
||||
float side = attack.isLeft ? -1 : 1;
|
||||
Anm2& effects = game.sprites.effects;
|
||||
Pose pose = {.art = &game.sprites.weapons, .animation = WEAPON_DEFS[attack.weapon].name};
|
||||
Draw& draw = pose.placement;
|
||||
draw = {.position = attack.position, .scale = {attack.scale, attack.scale * side}, .angle = attack.angle};
|
||||
Color tint = tint_get(game, attack);
|
||||
draw.color = tint;
|
||||
draw.flash = fill_get(strength_get(attack, attack.tip));
|
||||
draw.position = draw.position + jitter_get(draw.flash);
|
||||
|
||||
switch (def.kind)
|
||||
{
|
||||
case ATTACK_SWING:
|
||||
draw.position = attack.origin;
|
||||
break;
|
||||
case ATTACK_BOOMERANG:
|
||||
draw.scale.y = attack.scale;
|
||||
draw.angle = attack.time * BOOMERANG_SPIN;
|
||||
break;
|
||||
case ATTACK_SLASH:
|
||||
draw.color = color_fade(tint, std::min(1.0f, 2 * (1 - progress)));
|
||||
pose = {&effects, "Slash", draw};
|
||||
break;
|
||||
case ATTACK_ARROW:
|
||||
{
|
||||
float fade = std::clamp((attack.duration - attack.time) / ARROW_FADE, 0.0f, 1.0f);
|
||||
draw.color = color_fade(tint, attack.isAttached ? fade * fade : 1);
|
||||
pose = {&effects, "Arrow", draw};
|
||||
break;
|
||||
}
|
||||
case ATTACK_ROCKET:
|
||||
pose = {&effects, "Rocket", draw};
|
||||
break;
|
||||
case ATTACK_PUNCH:
|
||||
draw.color = color_fade(tint, std::min(1.0f, 2 * (1 - progress)));
|
||||
pose = {&effects, "Punch", draw};
|
||||
break;
|
||||
case ATTACK_UPPERCUT:
|
||||
// The fist is drawn rising, so it stands upright when aimed straight ahead and leans with the launch.
|
||||
// Facing left mirrors it.
|
||||
draw.color = color_fade(tint, std::min(1.0f, 2 * (1 - progress)));
|
||||
draw.scale = {attack.scale * side, attack.scale};
|
||||
draw.angle = attack.angle + PI / 2;
|
||||
pose = {&effects, "Uppercut", draw, attack.time * effects.fps};
|
||||
break;
|
||||
case ATTACK_BULLET:
|
||||
pose = {&effects, "Slug", draw};
|
||||
break;
|
||||
case ATTACK_PROJECTILE:
|
||||
pose = {&effects, "Bullet", draw};
|
||||
break;
|
||||
case ATTACK_HOOK:
|
||||
pose.animation = "Grapple Tip";
|
||||
break;
|
||||
case ATTACK_MINE:
|
||||
draw.scale.y = attack.scale;
|
||||
draw.angle = 0;
|
||||
if (is_mine_armed(attack) && std::fmod(attack.time, MINE_BLINK) < MINE_BLINK_ON)
|
||||
draw.flash = 1;
|
||||
pose = {&effects, "Mine", draw};
|
||||
break;
|
||||
case ATTACK_FLAME:
|
||||
case ATTACK_FIREBALL:
|
||||
{
|
||||
// Flames flicker through their animation, the spread jitter staggering them. Loose flames grow and fade
|
||||
// out. Fireballs point their tip back along the path.
|
||||
bool isFlame = def.kind == ATTACK_FLAME;
|
||||
float size = attack.scale * (isFlame ? FLAME_SHRINK + FLAME_GROW * progress : 1);
|
||||
draw.scale = {size, size};
|
||||
draw.angle = isFlame ? 0 : attack.angle - PI / 2;
|
||||
if (isFlame)
|
||||
draw.color = color_fade(tint, std::min(1.0f, (1 - progress) * FLAME_FADE));
|
||||
pose = {&effects, "Flame", draw, (attack.time + attack.aimAngle * FLAME_STAGGER) * effects.fps};
|
||||
break;
|
||||
}
|
||||
case ATTACK_SPIN:
|
||||
case ATTACK_FLAIL:
|
||||
pose = {&effects, "Flail Ball", draw};
|
||||
break;
|
||||
case ATTACK_WISP:
|
||||
{
|
||||
// Wisps burn like little flames while they hover, tails flickering upward, then streak head-first along
|
||||
// their path, each a little out of step with the rest.
|
||||
bool isFlying = attack.velocity.x != 0 || attack.velocity.y != 0;
|
||||
draw.scale = {attack.scale, attack.scale};
|
||||
draw.angle = isFlying ? attack.angle : PI / 2;
|
||||
pose = {&effects, "Wisp", draw, (attack.time + attack.slot * WISP_FRAME_STAGGER) * effects.fps};
|
||||
break;
|
||||
}
|
||||
case ATTACK_TELEPORT:
|
||||
// Drawn as its marker and sparkles instead.
|
||||
pose.art = nullptr;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return pose;
|
||||
}
|
||||
|
||||
// Draws the selected weapon in the player's hand, aimed at the cursor, unless it is out attacking. It fills in as it
|
||||
// charges (see charge_fill_get) and shakes harder the further the charge has got. A charging mine weapon holds up the
|
||||
// mine it's about to place instead.
|
||||
void Scene::held_draw(Fighter& fighter)
|
||||
{
|
||||
int held = fighter.weapon_key(fighter.slot);
|
||||
if (held < 0 || (game.sim.weapons.is_busy(fighter, held) && !game.sim.weapons.tether_get(fighter, held, false)))
|
||||
return;
|
||||
|
||||
float side = fighter.is_left() ? -1 : 1;
|
||||
Draw draw = {
|
||||
.color = game.color_get(fighter.index),
|
||||
.position = fighter.hand_get() + jitter_get(fighter.charge * CHARGE_SHAKE),
|
||||
.scale = {1, side},
|
||||
.angle = vec2_angle(fighter.aim) + kick_get(fighter) * side,
|
||||
.flash = charge_fill_get(game, fighter),
|
||||
};
|
||||
if (fighter.charge > 0 && WEAPON_DEFS[fighter.weaponLoadout[held]].charged.kind == ATTACK_MINE)
|
||||
{
|
||||
draw.scale = {1, 1};
|
||||
draw.angle = 0;
|
||||
game.sprites.effects.draw(game.renderer, "Mine", 0, draw);
|
||||
return;
|
||||
}
|
||||
reload_pose(fighter, held, fighter.cooldowns[held], draw);
|
||||
hand_draw(fighter, held, false, draw);
|
||||
}
|
||||
|
||||
// Dual Wield: the off-hand weapon, a darker shade of the fighter's colour, held in the far hand behind it.
|
||||
void Scene::offhand_draw(Fighter& fighter)
|
||||
{
|
||||
int off = fighter.off_slot_get();
|
||||
off = off < 0 ? -1 : fighter.weapon_key(off);
|
||||
if (!fighter.is_trinket_equipped(TRINKET_DUAL) || off < 0 ||
|
||||
(game.sim.weapons.is_busy(fighter, off) && !game.sim.weapons.tether_get(fighter, off, true)))
|
||||
return;
|
||||
|
||||
float side = fighter.is_left() ? -1 : 1;
|
||||
Draw draw = {
|
||||
.color = color_shade(game.color_get(fighter.index), SECONDARY_SHADE),
|
||||
.position = fighter.offhand_get() + jitter_get(fighter.charge * CHARGE_SHAKE),
|
||||
.scale = {1, side},
|
||||
.angle = vec2_angle(fighter.aim),
|
||||
.flash = charge_fill_get(game, fighter),
|
||||
};
|
||||
reload_pose(fighter, off, fighter.cooldowns[off], draw);
|
||||
hand_draw(fighter, off, true, draw);
|
||||
}
|
||||
|
||||
// Draws a weapon in a hand. While its whip or hook is out, the hand holds only its base ("<name> Base"), pointing
|
||||
// down the lash or rope.
|
||||
void Scene::hand_draw(Fighter& fighter, int weaponSlot, bool isOffhand, Draw draw)
|
||||
{
|
||||
Attack* tether = game.sim.weapons.tether_get(fighter, weaponSlot, isOffhand);
|
||||
std::string animation = WEAPON_DEFS[fighter.weaponLoadout[weaponSlot]].name;
|
||||
if (tether)
|
||||
{
|
||||
Vec2 end = tether->def()->kind == ATTACK_WHIP ? tether->tip : tether->position;
|
||||
draw.angle = vec2_angle(end - draw.position);
|
||||
animation += " Base";
|
||||
}
|
||||
// A weapon without held art yet shows its icon, small and upright.
|
||||
if (!game.sprites.weapons.animation_get(animation.c_str()))
|
||||
{
|
||||
draw.scale = {HELD_ICON_SCALE * (draw.scale.y < 0 ? -1 : 1), HELD_ICON_SCALE};
|
||||
draw.angle = 0;
|
||||
game.sprites.icons.draw(game.renderer, animation.c_str(), 0, draw);
|
||||
return;
|
||||
}
|
||||
game.sprites.weapons.draw(game.renderer, animation.c_str(), 0, draw);
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "anm2.h"
|
||||
#include "sim.h"
|
||||
|
||||
struct Game;
|
||||
|
||||
// Shows the game this screen is on: draws its world, fighters and attacks, animates its fighters, and plays the cues it
|
||||
// gives as it runs. While muted (replays and games this screen isn't showing), cues pass without a sound or a particle,
|
||||
// though the menu still follows the match. Each fighter's charging sound is kept here, by fighter.
|
||||
class Scene : public Cues
|
||||
{
|
||||
public:
|
||||
bool isMuted = false;
|
||||
|
||||
explicit Scene(Game& game);
|
||||
void update();
|
||||
void draw();
|
||||
bool is_flashing(Fighter& fighter);
|
||||
|
||||
void sound_play(SoundId sound) override;
|
||||
void charge_play(Fighter& fighter) override;
|
||||
void charge_stop(Fighter& fighter) override;
|
||||
void burst_spawn(Burst burst, Vec2 position, float angle, int owner) override;
|
||||
void attack_burst_spawn(Burst burst, Vec2 position, float angle, Attack& attack) override;
|
||||
void shake_add(float amount) override;
|
||||
void flash_spawn(Vec2 position, float angle, float size, int owner) override;
|
||||
void fighter_afterimage_spawn(Fighter& fighter) override;
|
||||
void attack_afterimage_spawn(Attack& attack) override;
|
||||
void fighter_aura_spawn(Fighter& fighter) override;
|
||||
void attack_aura_spawn(Attack& attack) override;
|
||||
void number_spawn(Damage& damage) override;
|
||||
void level_step(bool isUp) override;
|
||||
void match_start() override;
|
||||
void choice_open() override;
|
||||
void choice_close() override;
|
||||
void sandbox_return() override;
|
||||
|
||||
private:
|
||||
Game& game;
|
||||
int chargeVoices[MAX_FIGHTERS];
|
||||
|
||||
void world_draw();
|
||||
void bot_draw();
|
||||
void fighter_draw(Fighter& fighter);
|
||||
const char* animation_get(Fighter& fighter);
|
||||
void animator_update(Fighter& fighter, Animator& animator);
|
||||
Pose fighter_pose_get(Fighter& fighter);
|
||||
void attacks_draw();
|
||||
void magic_draw();
|
||||
void tesla_draw();
|
||||
void paths_draw(Fighter& fighter);
|
||||
void collision_draw();
|
||||
void blasts_draw();
|
||||
Pose attack_pose(Attack& attack);
|
||||
void held_draw(Fighter& fighter);
|
||||
void offhand_draw(Fighter& fighter);
|
||||
void hand_draw(Fighter& fighter, int weaponSlot, bool isOffhand, Draw draw);
|
||||
};
|
||||
+94
-94
@@ -169,11 +169,11 @@ void Session::host_start(Game& game)
|
||||
Fighter own = game.local_get();
|
||||
for (Device& device : game.input.devices)
|
||||
device.isClaimed = false;
|
||||
game.fighterCount = 1;
|
||||
game.fighters[0] = own;
|
||||
game.fighters[0].index = 0;
|
||||
game.weapons.attacks.clear();
|
||||
Match& match = game.match;
|
||||
game.sim.fighterCount = 1;
|
||||
game.sim.fighters[0] = own;
|
||||
game.sim.fighters[0].index = 0;
|
||||
game.sim.weapons.attacks.clear();
|
||||
Match& match = game.sim.match;
|
||||
std::fill(std::begin(match.owners), std::end(match.owners), -1);
|
||||
match.owners[0] = game.net.selfId;
|
||||
SDL_strlcpy(match.names[0], game.settings.name, sizeof(match.names[0]));
|
||||
@@ -218,20 +218,20 @@ void Session::stop(Game& game)
|
||||
return;
|
||||
isActive = false;
|
||||
int own = -1;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
if (isJoined && game.match.owners[i] == game.net.selfId)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
if (isJoined && game.sim.match.owners[i] == game.net.selfId)
|
||||
own = i;
|
||||
if (own >= 0)
|
||||
game.fighters[0] = game.fighters[own];
|
||||
game.fighters[0].index = 0;
|
||||
game.fighterCount = 1;
|
||||
game.weapons.attacks.clear();
|
||||
std::fill(std::begin(game.match.owners), std::end(game.match.owners), -1);
|
||||
game.sim.fighters[0] = game.sim.fighters[own];
|
||||
game.sim.fighters[0].index = 0;
|
||||
game.sim.fighterCount = 1;
|
||||
game.sim.weapons.attacks.clear();
|
||||
std::fill(std::begin(game.sim.match.owners), std::end(game.sim.match.owners), -1);
|
||||
game.local = 0;
|
||||
std::fill(std::begin(game.devices), std::end(game.devices), DEVICE_NONE);
|
||||
game.devices[0] = DEVICE_KEYBOARD;
|
||||
game.fighters[0].isBot = false;
|
||||
Match& match = game.match;
|
||||
game.sim.fighters[0].isBot = false;
|
||||
Match& match = game.sim.match;
|
||||
std::fill(std::begin(match.queues), std::end(match.queues), Queue{});
|
||||
std::fill(std::begin(match.isAway), std::end(match.isAway), false);
|
||||
std::fill(std::begin(match.queuedIn), std::end(match.queuedIn), -1);
|
||||
@@ -319,7 +319,7 @@ void Session::update(Game& game, Controls local)
|
||||
else
|
||||
{
|
||||
int end = tick;
|
||||
game.rollback.snapshot_load(game, snapshots[rollbackTo % SNAPSHOT_RING]);
|
||||
game.sim.snapshot_load(snapshots[rollbackTo % SNAPSHOT_RING]);
|
||||
arena_restore(game, rollbackTo % SNAPSHOT_RING);
|
||||
tick = rollbackTo;
|
||||
while (tick < end)
|
||||
@@ -371,7 +371,7 @@ void Session::update(Game& game, Controls local)
|
||||
void Session::step(Game& game, bool isLive)
|
||||
{
|
||||
int slot = tick % SNAPSHOT_RING;
|
||||
snapshots[slot] = game.rollback.snapshot_save(game);
|
||||
snapshots[slot] = game.sim.snapshot_save();
|
||||
arena_save(game, slot);
|
||||
snapshotTicks[slot] = tick;
|
||||
checksums[slot] = checksum_full(game);
|
||||
@@ -379,23 +379,23 @@ void Session::step(Game& game, bool isLive)
|
||||
|
||||
bool isArenaShown = arena.isActive && (isWatching || is_playing_arena(game));
|
||||
controls_give(game, false);
|
||||
game.audio.isMuted = game.effects.isMuted = !isLive || isArenaShown;
|
||||
game.scene.isMuted = !isLive || isArenaShown;
|
||||
game_tick(game);
|
||||
if (arena.isActive)
|
||||
{
|
||||
arena_swap(game);
|
||||
arena_locals_find(game);
|
||||
controls_give(game, true);
|
||||
game.audio.isMuted = game.effects.isMuted = !isLive || !isArenaShown;
|
||||
game.scene.isMuted = !isLive || !isArenaShown;
|
||||
game_tick(game);
|
||||
arena_locals_find(game);
|
||||
arena_swap(game);
|
||||
arena_after(game);
|
||||
}
|
||||
game.audio.isMuted = game.effects.isMuted = false;
|
||||
game.scene.isMuted = false;
|
||||
if (!arena.isActive)
|
||||
{
|
||||
int queuer = game.match.queue_startable_get(game);
|
||||
int queuer = game.sim.match.queue_startable_get(game.sim);
|
||||
if (queuer >= 0)
|
||||
arena_start(game, queuer);
|
||||
}
|
||||
@@ -408,13 +408,13 @@ void Session::step(Game& game, bool isLive)
|
||||
void Session::controls_give(Game& game, bool isArena)
|
||||
{
|
||||
// While the arena's swapped in, the sandbox is what's set aside.
|
||||
Match& lobby = isArena ? arena.state.match : game.match;
|
||||
int lobbyCount = isArena ? arena.state.fighterCount : game.fighterCount;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
Match& lobby = isArena ? arena.state.match : game.sim.match;
|
||||
int lobbyCount = isArena ? arena.state.fighterCount : game.sim.fighterCount;
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
{
|
||||
Fighter& fighter = game.fighters[i];
|
||||
int owner = game.match.owners[i];
|
||||
bool isIdle = isArena ? owner >= 0 && game.match.isGone[i] : game.match.isAway[i];
|
||||
Fighter& fighter = game.sim.fighters[i];
|
||||
int owner = game.sim.match.owners[i];
|
||||
bool isIdle = isArena ? owner >= 0 && game.sim.match.isGone[i] : game.sim.match.isAway[i];
|
||||
auto log = logs.find(owner);
|
||||
if (isIdle || owner < 0 || log == logs.end())
|
||||
{
|
||||
@@ -444,17 +444,17 @@ void Session::controls_give(Game& game, bool isArena)
|
||||
// colour they asked for if it's free; one leaving takes its attacks with it, and the fighters after it move down.
|
||||
void Session::events_apply(Game& game, int at)
|
||||
{
|
||||
Match& match = game.match;
|
||||
Match& match = game.sim.match;
|
||||
for (SessionEvent& event : events)
|
||||
{
|
||||
if (event.tick != at)
|
||||
continue;
|
||||
if (event.type == EVENT_JOIN && game.fighterCount < MAX_FIGHTERS)
|
||||
if (event.type == EVENT_JOIN && game.sim.fighterCount < MAX_FIGHTERS)
|
||||
{
|
||||
int index = game.fighterCount++;
|
||||
Fighter& fighter = game.fighters[index];
|
||||
int index = game.sim.fighterCount++;
|
||||
Fighter& fighter = game.sim.fighters[index];
|
||||
fighter = Fighter{};
|
||||
fighter.init(game, index);
|
||||
fighter.init(game.sim, index);
|
||||
const Profile& profile = event.profile;
|
||||
fighter.slots_set(std::clamp(profile.weaponSlots, 1, SLOT_MAX),
|
||||
std::clamp(profile.trinketSlots, 1, SLOT_MAX));
|
||||
@@ -480,33 +480,33 @@ void Session::events_apply(Game& game, int at)
|
||||
}
|
||||
if (event.type == EVENT_LEAVE)
|
||||
{
|
||||
int index = std::find(match.owners, match.owners + game.fighterCount, event.member) - match.owners;
|
||||
if (index >= game.fighterCount)
|
||||
int index = std::find(match.owners, match.owners + game.sim.fighterCount, event.member) - match.owners;
|
||||
if (index >= game.sim.fighterCount)
|
||||
continue;
|
||||
std::vector<Attack>& attacks = game.weapons.attacks;
|
||||
std::vector<Attack>& attacks = game.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 < game.sim.fighterCount; i++)
|
||||
{
|
||||
game.fighters[i] = game.fighters[i + 1];
|
||||
game.fighters[i].index = i;
|
||||
game.bot.states[i] = game.bot.states[i + 1];
|
||||
game.sim.fighters[i] = game.sim.fighters[i + 1];
|
||||
game.sim.fighters[i].index = i;
|
||||
game.sim.bot.states[i] = game.sim.bot.states[i + 1];
|
||||
match.owners[i] = match.owners[i + 1];
|
||||
match.colors[i] = match.colors[i + 1];
|
||||
SDL_memcpy(match.names[i], match.names[i + 1], sizeof(match.names[i]));
|
||||
}
|
||||
match.queue_slots_shift(index, game.fighterCount);
|
||||
game.fighterCount--;
|
||||
match.owners[game.fighterCount] = -1;
|
||||
match.queue_slots_shift(index, game.sim.fighterCount);
|
||||
game.sim.fighterCount--;
|
||||
match.owners[game.sim.fighterCount] = -1;
|
||||
}
|
||||
if (event.type == EVENT_QUEUE || event.type == EVENT_UNQUEUE)
|
||||
{
|
||||
int index = std::find(match.owners, match.owners + game.fighterCount, event.member) - match.owners;
|
||||
if (index >= game.fighterCount || match.isAway[index])
|
||||
int index = std::find(match.owners, match.owners + game.sim.fighterCount, event.member) - match.owners;
|
||||
if (index >= game.sim.fighterCount || match.isAway[index])
|
||||
continue;
|
||||
if (event.type == EVENT_QUEUE)
|
||||
match.queue_open(game, index, event.rules);
|
||||
match.queue_open(game.sim, index, event.rules);
|
||||
else
|
||||
{
|
||||
match.queues[index].isActive = false;
|
||||
@@ -630,11 +630,11 @@ void Session::packet_handle(Game& game, int from, const std::string& bytes, bool
|
||||
isJoined = true;
|
||||
std::erase_if(events, [&](SessionEvent& event) { return event.tick < at; });
|
||||
int self = game.net.selfId;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
{
|
||||
int owner = game.match.owners[i];
|
||||
int owner = game.sim.match.owners[i];
|
||||
if (owner >= 0 && owner != self && !names.contains(owner))
|
||||
names[owner] = text_clean(game.match.names[i], PLAYER_NAME_MAX);
|
||||
names[owner] = text_clean(game.sim.match.names[i], PLAYER_NAME_MAX);
|
||||
if (owner < 0 || owner == self)
|
||||
continue;
|
||||
logs[owner].restart(at);
|
||||
@@ -705,12 +705,12 @@ void Session::join_grant(Game& game, int member, const Profile& profile)
|
||||
state = state_full_save(game);
|
||||
else
|
||||
{
|
||||
Snapshot now = game.rollback.snapshot_save(game);
|
||||
Snapshot now = game.sim.snapshot_save();
|
||||
Arena arenaNow = arena;
|
||||
game.rollback.snapshot_load(game, snapshots[from % SNAPSHOT_RING]);
|
||||
game.sim.snapshot_load(snapshots[from % SNAPSHOT_RING]);
|
||||
arena_restore(game, from % SNAPSHOT_RING);
|
||||
state = state_full_save(game);
|
||||
game.rollback.snapshot_load(game, now);
|
||||
game.sim.snapshot_load(now);
|
||||
arena = arenaNow;
|
||||
}
|
||||
event_add(game, event);
|
||||
@@ -865,9 +865,9 @@ void Session::local_find(Game& game)
|
||||
game.local = 0;
|
||||
for (int i = 0; i < MAX_FIGHTERS; i++)
|
||||
{
|
||||
bool isOwn = i < game.fighterCount && game.match.owners[i] == game.net.selfId;
|
||||
bool isOwn = i < game.sim.fighterCount && game.sim.match.owners[i] == game.net.selfId;
|
||||
game.devices[i] = isOwn ? DEVICE_KEYBOARD : DEVICE_NONE;
|
||||
game.fighters[i].isBot = false;
|
||||
game.sim.fighters[i].isBot = false;
|
||||
if (isOwn)
|
||||
game.local = i;
|
||||
}
|
||||
@@ -876,8 +876,8 @@ void Session::local_find(Game& game)
|
||||
// Which fighter a member plays, or -1 if they have none (yet).
|
||||
int Session::fighter_get(Game& game, int member)
|
||||
{
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
if (game.match.owners[i] == member)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
if (game.sim.match.owners[i] == member)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
@@ -903,8 +903,8 @@ std::string Session::name_get(Game& game, int member)
|
||||
return game.ui.name_get(game, fighter);
|
||||
std::string name = base_name_get(game, member);
|
||||
int same = 1;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
same += game.match.owners[i] >= 0 && base_name_get(game, game.match.owners[i]) == name;
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
same += game.sim.match.owners[i] >= 0 && base_name_get(game, game.sim.match.owners[i]) == name;
|
||||
return same > 1 ? name + " (" + std::to_string(same) + ")" : name;
|
||||
}
|
||||
|
||||
@@ -1054,8 +1054,8 @@ void Session::queue_decide(Game& game, int member, bool isOpen, const Rules& rul
|
||||
// Whether this screen's fighter has a game queued in the sandbox.
|
||||
bool Session::is_queued(Game& game)
|
||||
{
|
||||
Match& lobby = isSwapped ? arena.state.match : game.match;
|
||||
int count = isSwapped ? arena.state.fighterCount : game.fighterCount;
|
||||
Match& lobby = isSwapped ? arena.state.match : game.sim.match;
|
||||
int count = isSwapped ? arena.state.fighterCount : game.sim.fighterCount;
|
||||
for (int i = 0; i < count; i++)
|
||||
if (lobby.owners[i] == game.net.selfId)
|
||||
return lobby.queues[i].isActive;
|
||||
@@ -1077,8 +1077,8 @@ bool Session::is_playing_arena(Game& game)
|
||||
{
|
||||
if (!arena.isActive)
|
||||
return false;
|
||||
Match& match = isSwapped ? game.match : arena.state.match;
|
||||
int count = isSwapped ? game.fighterCount : arena.state.fighterCount;
|
||||
Match& match = isSwapped ? game.sim.match : arena.state.match;
|
||||
int count = isSwapped ? game.sim.fighterCount : arena.state.fighterCount;
|
||||
for (int i = 0; i < count; i++)
|
||||
if (match.owners[i] == game.net.selfId && !match.isGone[i])
|
||||
return true;
|
||||
@@ -1110,8 +1110,8 @@ void Session::arena_leave(Game& game)
|
||||
// The arena's players still in it, by name: "Sneed, Bob and Alice".
|
||||
std::string Session::arena_names_get(Game& game)
|
||||
{
|
||||
Match& match = isSwapped ? game.match : arena.state.match;
|
||||
int count = isSwapped ? game.fighterCount : arena.state.fighterCount;
|
||||
Match& match = isSwapped ? game.sim.match : arena.state.match;
|
||||
int count = isSwapped ? game.sim.fighterCount : arena.state.fighterCount;
|
||||
std::vector<std::string> names;
|
||||
for (int i = 0; i < count; i++)
|
||||
if (match.owners[i] >= 0 && !match.isGone[i])
|
||||
@@ -1126,13 +1126,13 @@ std::string Session::arena_names_get(Game& game)
|
||||
// their map), and which fighter is this screen's.
|
||||
void Session::arena_swap(Game& game)
|
||||
{
|
||||
std::swap(game.fighters, arena.state.fighters);
|
||||
std::swap(game.fighterCount, arena.state.fighterCount);
|
||||
std::swap(game.match, arena.state.match);
|
||||
std::swap(game.weapons, arena.state.weapons);
|
||||
std::swap(game.world, arena.state.world);
|
||||
std::swap(game.rng, arena.state.rng);
|
||||
std::swap(game.bot, arena.bot);
|
||||
std::swap(game.sim.fighters, arena.state.fighters);
|
||||
std::swap(game.sim.fighterCount, arena.state.fighterCount);
|
||||
std::swap(game.sim.match, arena.state.match);
|
||||
std::swap(game.sim.weapons, arena.state.weapons);
|
||||
std::swap(game.sim.world, arena.state.world);
|
||||
std::swap(game.sim.rng, arena.state.rng);
|
||||
std::swap(game.sim.bot, arena.bot);
|
||||
std::swap(game.devices, arena.devices);
|
||||
std::swap(game.local, arena.local);
|
||||
}
|
||||
@@ -1143,7 +1143,7 @@ void Session::arena_locals_find(Game& game)
|
||||
game.local = 0;
|
||||
for (int i = 0; i < MAX_FIGHTERS; i++)
|
||||
{
|
||||
bool isOwn = i < game.fighterCount && game.match.owners[i] == game.net.selfId && !game.match.isGone[i];
|
||||
bool isOwn = i < game.sim.fighterCount && game.sim.match.owners[i] == game.net.selfId && !game.sim.match.isGone[i];
|
||||
game.devices[i] = isOwn ? DEVICE_KEYBOARD : DEVICE_NONE;
|
||||
if (isOwn)
|
||||
game.local = i;
|
||||
@@ -1154,9 +1154,9 @@ void Session::arena_locals_find(Game& game)
|
||||
// chat starts afresh for it.
|
||||
void Session::arena_start(Game& game, int queuer)
|
||||
{
|
||||
Match& lobby = game.match;
|
||||
Match& lobby = game.sim.match;
|
||||
int members[MAX_FIGHTERS];
|
||||
int count = lobby.queue_members_get(game, queuer, members);
|
||||
int count = lobby.queue_members_get(game.sim, queuer, members);
|
||||
Rules rules = lobby.queues[queuer].rules;
|
||||
lobby.queues[queuer].isActive = false;
|
||||
arena_build(game, rules, members, count);
|
||||
@@ -1171,7 +1171,7 @@ void Session::arena_start(Game& game, int queuer)
|
||||
// a seed of the sandbox's random generator and the tick, so every machine builds the same one.
|
||||
void Session::arena_build(Game& game, const Rules& rules, const int* members, int count)
|
||||
{
|
||||
Match& lobby = game.match;
|
||||
Match& lobby = game.sim.match;
|
||||
int owners[MAX_FIGHTERS];
|
||||
int colors[MAX_FIGHTERS];
|
||||
char names[MAX_FIGHTERS][PLAYER_NAME_MAX + 1];
|
||||
@@ -1185,11 +1185,11 @@ void Session::arena_build(Game& game, const Rules& rules, const int* members, in
|
||||
lobby.isQueueReady[i] = false;
|
||||
lobby.queuedIn[i] = -1;
|
||||
lobby.queues[i].isActive = false;
|
||||
std::erase_if(game.weapons.attacks, [&](Attack& attack) { return attack.owner == i; });
|
||||
std::erase_if(game.sim.weapons.attacks, [&](Attack& attack) { return attack.owner == i; });
|
||||
}
|
||||
Uint32 seed = game.rng.state ^ (Uint32)tick * ARENA_SEED_MIX;
|
||||
Uint32 seed = game.sim.rng.state ^ (Uint32)tick * ARENA_SEED_MIX;
|
||||
int players = rules.isBotFill ? std::max(rules.players, count) : count;
|
||||
int botLevel = game.bot.level;
|
||||
int botLevel = game.sim.bot.level;
|
||||
|
||||
arena.isActive = true;
|
||||
arena.serial = tick;
|
||||
@@ -1197,19 +1197,19 @@ void Session::arena_build(Game& game, const Rules& rules, const int* members, in
|
||||
arena.bot = Bot{};
|
||||
arena.bot.level = botLevel;
|
||||
arena_swap(game);
|
||||
Match& match = game.match;
|
||||
Match& match = game.sim.match;
|
||||
match = Match{};
|
||||
match.rules = rules;
|
||||
match.rules.players = players;
|
||||
game.rng.state = seed ? seed : RNG_SEED;
|
||||
game.fighterCount = players;
|
||||
game.sim.rng.state = seed ? seed : RNG_SEED;
|
||||
game.sim.fighterCount = players;
|
||||
bool isTaken[COLOR_COUNT] = {};
|
||||
for (int k = 0; k < count; k++)
|
||||
isTaken[colors[k] % COLOR_COUNT] = true;
|
||||
for (int k = 0; k < players; k++)
|
||||
{
|
||||
game.fighters[k] = Fighter{};
|
||||
game.fighters[k].init(game, k);
|
||||
game.sim.fighters[k] = Fighter{};
|
||||
game.sim.fighters[k].init(game.sim, k);
|
||||
match.owners[k] = k < count ? owners[k] : -1;
|
||||
if (k < count)
|
||||
{
|
||||
@@ -1221,7 +1221,7 @@ void Session::arena_build(Game& game, const Rules& rules, const int* members, in
|
||||
isTaken[match.colors[k]] = true;
|
||||
}
|
||||
arena_locals_find(game);
|
||||
match.start(game);
|
||||
match.start(game.sim);
|
||||
arena.graphSerial = arena.serial;
|
||||
arena_locals_find(game);
|
||||
arena_swap(game);
|
||||
@@ -1231,7 +1231,7 @@ void Session::arena_build(Game& game, const Rules& rules, const int* members, in
|
||||
// over; and once a finished match's players all ask to play again, it starts afresh with them.
|
||||
void Session::arena_after(Game& game)
|
||||
{
|
||||
Match& lobby = game.match;
|
||||
Match& lobby = game.sim.match;
|
||||
Match& match = arena.state.match;
|
||||
int count = arena.state.fighterCount;
|
||||
int members[MAX_FIGHTERS];
|
||||
@@ -1280,7 +1280,7 @@ void Session::arena_end(Game& game)
|
||||
{
|
||||
arena.isActive = false;
|
||||
isWatching = false;
|
||||
std::fill(std::begin(game.match.isAway), std::end(game.match.isAway), false);
|
||||
std::fill(std::begin(game.sim.match.isAway), std::end(game.sim.match.isAway), false);
|
||||
}
|
||||
|
||||
// Keeps the arena as it is before a tick, beside the sandbox's snapshot, to rewind to (none if it's not on).
|
||||
@@ -1290,7 +1290,7 @@ void Session::arena_save(Game& game, int slot)
|
||||
if (!arena.isActive)
|
||||
return;
|
||||
arena_swap(game);
|
||||
arenaSnapshots[slot] = game.rollback.snapshot_save(game);
|
||||
arenaSnapshots[slot] = game.sim.snapshot_save();
|
||||
arena_swap(game);
|
||||
}
|
||||
|
||||
@@ -1303,9 +1303,9 @@ void Session::arena_restore(Game& game, int slot)
|
||||
return;
|
||||
arena.serial = serial;
|
||||
arena_swap(game);
|
||||
game.rollback.snapshot_load(game, arenaSnapshots[slot]);
|
||||
game.sim.snapshot_load(arenaSnapshots[slot]);
|
||||
if (arena.graphSerial != serial)
|
||||
game.bot.graph_build(game.world);
|
||||
game.sim.bot.graph_build(game.sim.world);
|
||||
arena.graphSerial = serial;
|
||||
arena_locals_find(game);
|
||||
arena_swap(game);
|
||||
@@ -1314,11 +1314,11 @@ void Session::arena_restore(Game& game, int slot)
|
||||
// The checksum of the sandbox and, while it's on, the arena.
|
||||
Uint32 Session::checksum_full(Game& game)
|
||||
{
|
||||
Uint32 hash = game.rollback.checksum_get(game);
|
||||
Uint32 hash = game.sim.checksum_get();
|
||||
if (!arena.isActive)
|
||||
return hash;
|
||||
arena_swap(game);
|
||||
hash = (hash ^ game.rollback.checksum_get(game)) * ARENA_SEED_MIX + (Uint32)arena.serial;
|
||||
hash = (hash ^ game.sim.checksum_get()) * ARENA_SEED_MIX + (Uint32)arena.serial;
|
||||
arena_swap(game);
|
||||
return hash;
|
||||
}
|
||||
@@ -1326,7 +1326,7 @@ Uint32 Session::checksum_full(Game& game)
|
||||
// The whole shared state for a newcomer: the sandbox's, then the arena's (after the tick it started) if it's on.
|
||||
std::string Session::state_full_save(Game& game)
|
||||
{
|
||||
std::string lobby = state_save(game);
|
||||
std::string lobby = state_save(game.sim);
|
||||
PacketWriter packet;
|
||||
packet.put((Uint32)lobby.size());
|
||||
packet.bytes += lobby;
|
||||
@@ -1334,7 +1334,7 @@ std::string Session::state_full_save(Game& game)
|
||||
if (!arena.isActive)
|
||||
return packet.bytes;
|
||||
arena_swap(game);
|
||||
packet.bytes += state_save(game);
|
||||
packet.bytes += state_save(game.sim);
|
||||
arena_swap(game);
|
||||
return packet.bytes;
|
||||
}
|
||||
@@ -1348,16 +1348,16 @@ bool Session::state_full_load(Game& game, const std::string& bytes)
|
||||
std::string lobby = bytes.substr(reader.at, length);
|
||||
reader.at += length;
|
||||
int serial = -1;
|
||||
if (!reader.get(serial) || !state_load(game, lobby))
|
||||
if (!reader.get(serial) || !state_load(game.sim, lobby))
|
||||
return false;
|
||||
arena.isActive = serial >= 0;
|
||||
if (!arena.isActive)
|
||||
return true;
|
||||
arena.serial = serial;
|
||||
arena.bot = Bot{};
|
||||
arena.bot.level = game.bot.level;
|
||||
arena.bot.level = game.sim.bot.level;
|
||||
arena_swap(game);
|
||||
bool isLoaded = state_load(game, bytes.substr(reader.at));
|
||||
bool isLoaded = state_load(game.sim, bytes.substr(reader.at));
|
||||
arena.graphSerial = serial;
|
||||
arena_locals_find(game);
|
||||
arena_swap(game);
|
||||
|
||||
+10
-10
@@ -259,7 +259,7 @@ void Settings::load(Game& game)
|
||||
std::fill(std::begin(sandbox.trinketLoadout), std::end(sandbox.trinketLoadout), -1);
|
||||
std::fill(std::begin(sandbox.weaponSlotLevels), std::end(sandbox.weaponSlotLevels), -1);
|
||||
std::fill(std::begin(sandbox.trinketSlotLevels), std::end(sandbox.trinketSlotLevels), -1);
|
||||
savedRules = game.match.rules;
|
||||
savedRules = game.sim.match.rules;
|
||||
char* text = (char*)SDL_LoadFile(file_path_get().c_str(), nullptr);
|
||||
if (!text)
|
||||
return;
|
||||
@@ -304,7 +304,7 @@ void Settings::load(Game& game)
|
||||
weaponSlots = std::clamp(number, 1, SLOT_MAX);
|
||||
if (SDL_strcmp(line, KEY_TRINKET_SLOTS) == 0)
|
||||
trinketSlots = std::clamp(number, 1, SLOT_MAX);
|
||||
rules_read(game.match.rules, line, value);
|
||||
rules_read(game.sim.match.rules, line, value);
|
||||
sandbox_read(sandbox, line, value);
|
||||
for (int i = 0; i < CONTROL_COUNT; i++)
|
||||
{
|
||||
@@ -316,7 +316,7 @@ void Settings::load(Game& game)
|
||||
}
|
||||
}
|
||||
SDL_free(text);
|
||||
savedRules = game.match.rules;
|
||||
savedRules = game.sim.match.rules;
|
||||
}
|
||||
|
||||
void Settings::save(Game& game)
|
||||
@@ -336,7 +336,7 @@ void Settings::save(Game& game)
|
||||
length += SDL_snprintf(text + length, sizeof(text) - length, "%s%s=%d,%d\n", KEY_BIND, CONTROL_KEYS[i],
|
||||
binding.key, binding.pad);
|
||||
}
|
||||
Match& match = game.match;
|
||||
Match& match = game.sim.match;
|
||||
length += rules_write(match.rules, text + length, sizeof(text) - length);
|
||||
length += sandbox_write(match.state == MATCH_PRACTICE ? game.local_get() : match.sandbox, text + length,
|
||||
sizeof(text) - length);
|
||||
@@ -354,7 +354,7 @@ void Settings::apply(Game& game)
|
||||
fullscreen_apply(game);
|
||||
game.renderer.vsync_set(isVsync);
|
||||
game.audio.gain_set(isMuted ? 0 : volume);
|
||||
if (game.match.state == MATCH_PRACTICE)
|
||||
if (game.sim.match.state == MATCH_PRACTICE)
|
||||
{
|
||||
Fighter& fighter = game.local_get();
|
||||
fighter.slots_set(weaponSlots, trinketSlots);
|
||||
@@ -402,7 +402,7 @@ void Settings::draw(Game& game, Rect area, Page& page)
|
||||
|
||||
// The sandbox's slots only show in the sandbox.
|
||||
bool isSandbox = false;
|
||||
if (game.match.state == MATCH_PRACTICE)
|
||||
if (game.sim.match.state == MATCH_PRACTICE)
|
||||
{
|
||||
if (ui.row_get(page, ROW_HEIGHT, y))
|
||||
ui.separator_draw(game, {row.x, y, row.w, row.h}, "Sandbox");
|
||||
@@ -586,7 +586,7 @@ void Settings::defaults_restore(Game& game)
|
||||
fullscreen_apply(game);
|
||||
game.renderer.vsync_set(isVsync);
|
||||
game.audio.gain_set(isMuted ? 0 : volume);
|
||||
if (game.match.state == MATCH_PRACTICE)
|
||||
if (game.sim.match.state == MATCH_PRACTICE)
|
||||
game.input.edit_send(game.devices[game.local],
|
||||
edit_encode({.type = EDIT_SLOTS, .index = weaponSlots, .value = trinketSlots}));
|
||||
isDirty = true;
|
||||
@@ -664,7 +664,7 @@ void Settings::restore(Game& game)
|
||||
Vec2 centre = {sandbox.feet.x, sandbox.feet.y - box.h / 2 - 1};
|
||||
for (float y = -box.h / 2; y <= box.h / 2; y += CLEAR_PROBE)
|
||||
for (float x = -box.w / 2; x <= box.w / 2; x += CLEAR_PROBE)
|
||||
if (game.world.is_blocking_at({centre.x + x, centre.y + y}))
|
||||
if (game.sim.world.is_blocking_at({centre.x + x, centre.y + y}))
|
||||
return;
|
||||
fighter.teleport_to(centre);
|
||||
}
|
||||
@@ -673,8 +673,8 @@ void Settings::restore(Game& game)
|
||||
// match starts from it.
|
||||
void Settings::update(Game& game)
|
||||
{
|
||||
bool isPractice = game.match.state == MATCH_PRACTICE;
|
||||
isDirty |= !(game.match.rules == savedRules) || (wasPractice && !isPractice);
|
||||
bool isPractice = game.sim.match.state == MATCH_PRACTICE;
|
||||
isDirty |= !(game.sim.match.rules == savedRules) || (wasPractice && !isPractice);
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// A browser tab can close at any moment without a quit, so the sandbox is saved every so often there.
|
||||
Uint64 now = SDL_GetTicks();
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# The simulation: every source that steps a game, free of SDL, shared by the game and the server. Floating point is
|
||||
# kept strict wherever it's used (see the top CMakeLists), as inline maths in the headers compiles into its users too.
|
||||
set(SIM_SOURCES
|
||||
${CMAKE_CURRENT_LIST_DIR}/bot.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/fighter.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/match.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/sim.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/state.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/weapon.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/world.cpp)
|
||||
add_library(sim STATIC ${SIM_SOURCES})
|
||||
target_include_directories(sim PUBLIC ${CMAKE_CURRENT_LIST_DIR})
|
||||
target_compile_features(sim PUBLIC cxx_std_20)
|
||||
if(MSVC)
|
||||
target_compile_options(sim PUBLIC /fp:precise /fp:contract-)
|
||||
else()
|
||||
target_compile_options(sim PUBLIC -ffp-contract=off -fno-fast-math)
|
||||
endif()
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
#include "sim.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
constexpr Uint32 FNV_OFFSET = 2166136261u;
|
||||
constexpr Uint32 FNV_PRIME = 16777619u;
|
||||
|
||||
// Sets up a game from scratch: the sandbox world, the bot graph, fighters and the seeded generator.
|
||||
void Sim::init(Uint32 seed)
|
||||
{
|
||||
world.sandbox_load();
|
||||
bot.init(world);
|
||||
for (int i = 0; i < MAX_FIGHTERS; i++)
|
||||
fighters[i].init(*this, i);
|
||||
rng.state = seed;
|
||||
}
|
||||
|
||||
void Sim::tick()
|
||||
{
|
||||
match.update(*this);
|
||||
if (match.state != MATCH_COUNTDOWN)
|
||||
bot.update(*this);
|
||||
for (int i = 0; i < fighterCount && match.state != MATCH_COUNTDOWN; i++)
|
||||
fighters[i].update(*this);
|
||||
weapons.update(*this);
|
||||
world.update(*this);
|
||||
}
|
||||
|
||||
// Moves a level by a step within the levels there are, cueing whether it went up or down.
|
||||
void level_step(Sim& sim, int& level, int step)
|
||||
{
|
||||
int next = std::clamp(level + step, 0, LEVEL_COUNT - 1);
|
||||
if (next == level)
|
||||
return;
|
||||
sim.cues->level_step(next > level);
|
||||
level = next;
|
||||
}
|
||||
|
||||
Snapshot Sim::snapshot_save()
|
||||
{
|
||||
Snapshot snapshot = {.match = match,
|
||||
.weapons = weapons,
|
||||
.world = world,
|
||||
.rng = rng,
|
||||
.fighterCount = fighterCount};
|
||||
std::copy(std::begin(fighters), std::end(fighters), snapshot.fighters);
|
||||
std::copy(std::begin(bot.states), std::end(bot.states), snapshot.bots);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
void Sim::snapshot_load(Snapshot& snapshot)
|
||||
{
|
||||
std::copy(std::begin(snapshot.fighters), std::end(snapshot.fighters), fighters);
|
||||
std::copy(std::begin(snapshot.bots), std::end(snapshot.bots), bot.states);
|
||||
fighterCount = snapshot.fighterCount;
|
||||
weapons = snapshot.weapons;
|
||||
world = snapshot.world;
|
||||
rng = snapshot.rng;
|
||||
match = snapshot.match;
|
||||
}
|
||||
|
||||
static void hash_add(Uint32& hash, float value)
|
||||
{
|
||||
Uint32 bits;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
hash = (hash ^ bits) * FNV_PRIME;
|
||||
}
|
||||
|
||||
// A fingerprint of the simulation state that matters for a desync: the fighters, every attack, the dummies, the
|
||||
// match and the random generator.
|
||||
Uint32 Sim::checksum_get()
|
||||
{
|
||||
Uint32 hash = FNV_OFFSET;
|
||||
hash_add(hash, (float)match.state + match.timer);
|
||||
for (int i = 0; i < fighterCount; i++)
|
||||
{
|
||||
Fighter& fighter = fighters[i];
|
||||
for (float value : {fighter.body.box.x, fighter.body.box.y, fighter.body.velocity.x, fighter.body.velocity.y,
|
||||
fighter.aim.x, fighter.aim.y, fighter.charge, fighter.health, fighter.hitstop})
|
||||
hash_add(hash, value);
|
||||
for (float cooldown : fighter.cooldowns)
|
||||
hash_add(hash, cooldown);
|
||||
// What the menu's edits change: loadouts, levels, slot counts, stacking and PvP.
|
||||
for (int slot = 0; slot < SLOT_MAX; slot++)
|
||||
for (int value : {fighter.weaponLoadout[slot], fighter.trinketLoadout[slot], fighter.weaponSlotLevels[slot],
|
||||
fighter.trinketSlotLevels[slot]})
|
||||
hash_add(hash, (float)value);
|
||||
for (int level : fighter.weaponItemLevels)
|
||||
hash_add(hash, (float)level);
|
||||
for (int level : fighter.trinketItemLevels)
|
||||
hash_add(hash, (float)level);
|
||||
for (int value : {fighter.weaponSlots, fighter.trinketSlots, (int)fighter.isPvp})
|
||||
hash_add(hash, (float)value);
|
||||
}
|
||||
for (Attack& attack : weapons.attacks)
|
||||
for (float value : {attack.position.x, attack.position.y, attack.velocity.x, attack.velocity.y, attack.angle,
|
||||
attack.time, (float)attack.hitsLeft})
|
||||
hash_add(hash, value);
|
||||
for (Dummy& dummy : world.dummies)
|
||||
for (float value : {dummy.flash, dummy.wobble, dummy.wobbleSpeed})
|
||||
hash_add(hash, value);
|
||||
return (hash ^ rng.state) * FNV_PRIME;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "bot.h"
|
||||
#include "match.h"
|
||||
|
||||
// What a game shows and plays as it runs. The simulation calls these as things happen and never reads anything back.
|
||||
// This base does nothing, for the server, balance runs and replays no one should hear. The client's Scene plays them.
|
||||
class Cues
|
||||
{
|
||||
public:
|
||||
virtual ~Cues() = default;
|
||||
virtual void sound_play(SoundId sound) {}
|
||||
virtual void charge_play(Fighter& fighter) {}
|
||||
virtual void charge_stop(Fighter& fighter) {}
|
||||
virtual void burst_spawn(Burst burst, Vec2 position, float angle, int owner) {}
|
||||
virtual void attack_burst_spawn(Burst burst, Vec2 position, float angle, Attack& attack) {}
|
||||
virtual void shake_add(float amount) {}
|
||||
virtual void flash_spawn(Vec2 position, float angle, float size, int owner) {}
|
||||
virtual void fighter_afterimage_spawn(Fighter& fighter) {}
|
||||
virtual void attack_afterimage_spawn(Attack& attack) {}
|
||||
virtual void fighter_aura_spawn(Fighter& fighter) {}
|
||||
virtual void attack_aura_spawn(Attack& attack) {}
|
||||
virtual void number_spawn(Damage& damage) {}
|
||||
virtual void level_step(bool isUp) {}
|
||||
virtual void match_start() {}
|
||||
virtual void choice_open() {}
|
||||
virtual void choice_close() {}
|
||||
virtual void sandbox_return() {}
|
||||
};
|
||||
|
||||
inline Cues CUES_NONE;
|
||||
|
||||
// Everything a tick reads and writes, so a game can be rewound to it.
|
||||
struct Snapshot
|
||||
{
|
||||
BotState bots[MAX_FIGHTERS];
|
||||
Fighter fighters[MAX_FIGHTERS];
|
||||
Match match;
|
||||
Weapons weapons;
|
||||
World world;
|
||||
Rng rng;
|
||||
int fighterCount = 0;
|
||||
};
|
||||
|
||||
// One whole game: everything a tick reads and writes, the bots' map of its world, and where its cues go.
|
||||
struct Sim
|
||||
{
|
||||
Bot bot;
|
||||
Fighter fighters[MAX_FIGHTERS];
|
||||
Match match;
|
||||
Weapons weapons;
|
||||
World world;
|
||||
Rng rng;
|
||||
Cues* cues = &CUES_NONE;
|
||||
int fighterCount = 1;
|
||||
|
||||
void init(Uint32 seed);
|
||||
void tick();
|
||||
Snapshot snapshot_save();
|
||||
void snapshot_load(Snapshot& snapshot);
|
||||
Uint32 checksum_get();
|
||||
};
|
||||
|
||||
void level_step(Sim& sim, int& level, int step);
|
||||
@@ -20,10 +20,3 @@ void Sprites::init(Game& game)
|
||||
for (Animator& animator : fighters)
|
||||
animator.art = &player;
|
||||
}
|
||||
|
||||
// Each tick, every fighter's animator plays whatever its fighter is doing.
|
||||
void Sprites::update(Game& game)
|
||||
{
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
game.fighters[i].animator_update(fighters[i]);
|
||||
}
|
||||
@@ -28,5 +28,4 @@ public:
|
||||
Animator fighters[MAX_FIGHTERS];
|
||||
|
||||
void init(Game& game);
|
||||
void update(Game& game);
|
||||
};
|
||||
+16
-16
@@ -1,5 +1,5 @@
|
||||
#include "state.h"
|
||||
#include "game.h"
|
||||
#include "sim.h"
|
||||
#include <cstring>
|
||||
#include <type_traits>
|
||||
|
||||
@@ -108,16 +108,16 @@ struct Checker
|
||||
|
||||
// Both directions walk the same fields in the same order. Weapons' pending attacks aren't among them: they only exist
|
||||
// inside a tick, and the state is taken between ticks.
|
||||
template <class Stream, class GameRef> static void state_walk(Stream& stream, GameRef& game)
|
||||
template <class Stream, class SimRef> static void state_walk(Stream& stream, SimRef& sim)
|
||||
{
|
||||
stream.one(game.fighterCount);
|
||||
stream.raw(game.fighters, MAX_FIGHTERS);
|
||||
stream.one(game.match);
|
||||
stream.one(game.rng);
|
||||
stream.raw(game.bot.states, MAX_FIGHTERS);
|
||||
stream.one(game.weapons.orbitClock);
|
||||
stream.list(game.weapons.attacks);
|
||||
World& world = game.world;
|
||||
stream.one(sim.fighterCount);
|
||||
stream.raw(sim.fighters, MAX_FIGHTERS);
|
||||
stream.one(sim.match);
|
||||
stream.one(sim.rng);
|
||||
stream.raw(sim.bot.states, MAX_FIGHTERS);
|
||||
stream.one(sim.weapons.orbitClock);
|
||||
stream.list(sim.weapons.attacks);
|
||||
World& world = sim.world;
|
||||
stream.list(world.dummies);
|
||||
stream.list(world.obstacles);
|
||||
stream.list(world.ramps);
|
||||
@@ -127,18 +127,18 @@ template <class Stream, class GameRef> static void state_walk(Stream& stream, Ga
|
||||
stream.one(world.width);
|
||||
}
|
||||
|
||||
std::string state_save(Game& game)
|
||||
std::string state_save(Sim& sim)
|
||||
{
|
||||
Writer writer;
|
||||
writer.one(STATE_MAGIC);
|
||||
writer.one(layout_get());
|
||||
state_walk(writer, game);
|
||||
state_walk(writer, sim);
|
||||
return writer.bytes;
|
||||
}
|
||||
|
||||
// Loads the state if it's whole and from a build that lays things out the same, then rebuilds what's derived from it.
|
||||
// A refused state leaves the game as it was.
|
||||
bool state_load(Game& game, const std::string& bytes)
|
||||
bool state_load(Sim& sim, const std::string& bytes)
|
||||
{
|
||||
Reader check = {bytes};
|
||||
Uint32 magic = 0;
|
||||
@@ -150,13 +150,13 @@ bool state_load(Game& game, const std::string& bytes)
|
||||
|
||||
// Checked whole first, so a truncated state can't leave the game half loaded.
|
||||
Checker checker = {bytes, check.at};
|
||||
state_walk(checker, game);
|
||||
state_walk(checker, sim);
|
||||
if (checker.isBad || checker.at != bytes.size())
|
||||
return false;
|
||||
Reader reader = {bytes};
|
||||
reader.one(magic);
|
||||
reader.one(layout);
|
||||
state_walk(reader, game);
|
||||
game.bot.graph_build(game.world);
|
||||
state_walk(reader, sim);
|
||||
sim.bot.graph_build(sim.world);
|
||||
return true;
|
||||
}
|
||||
+3
-3
@@ -2,12 +2,12 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
struct Game;
|
||||
struct Sim;
|
||||
|
||||
// The whole simulation as bytes, to hand a player joining a game already running. The game state is plain data that
|
||||
// lays out the same on every platform (no pointers or heap inside the structs; see Attack and FixedList), so most of it
|
||||
// goes as it sits in memory, and the few lists as a count and their items. It starts with a layout signature (the sizes
|
||||
// of everything), so a build that lays things out differently refuses it rather than loading garbage. The menu and
|
||||
// everything else this screen alone sees isn't part of it, nor is the bots' map of the world, rebuilt on loading.
|
||||
std::string state_save(Game& game);
|
||||
bool state_load(Game& game, const std::string& bytes);
|
||||
std::string state_save(Sim& sim);
|
||||
bool state_load(Sim& sim, const std::string& bytes);
|
||||
+70
-88
@@ -167,12 +167,12 @@ void Ui::init(Game& game)
|
||||
const char* Ui::name_get(Game& game, int fighter)
|
||||
{
|
||||
// Online, a player's name, numbered when someone earlier in the game has the same one ("Fighter", "Fighter (2)").
|
||||
if (game.session.isActive && game.match.owners[fighter] >= 0)
|
||||
if (game.session.isActive && game.sim.match.owners[fighter] >= 0)
|
||||
{
|
||||
std::string name = game.session.base_name_get(game, game.match.owners[fighter]);
|
||||
std::string name = game.session.base_name_get(game, game.sim.match.owners[fighter]);
|
||||
int same = 1;
|
||||
for (int i = 0; i < fighter; i++)
|
||||
same += game.match.owners[i] >= 0 && game.session.base_name_get(game, game.match.owners[i]) == name;
|
||||
same += game.sim.match.owners[i] >= 0 && game.session.base_name_get(game, game.sim.match.owners[i]) == name;
|
||||
if (same > 1)
|
||||
name += " (" + std::to_string(same) + ")";
|
||||
SDL_strlcpy(onlineNames[fighter], name.c_str(), sizeof(onlineNames[fighter]));
|
||||
@@ -200,7 +200,7 @@ void Ui::draw(Game& game)
|
||||
game.renderer.camera = {};
|
||||
offscreen_draw(game, camera);
|
||||
bool isBarsRaised = game.menu.is_bars_open();
|
||||
bool isShared = game.match.state != MATCH_PRACTICE && game.local_count() > 1;
|
||||
bool isShared = game.sim.match.state != MATCH_PRACTICE && game.local_count() > 1;
|
||||
if (!isBarsRaised && !isShared && !isWatching)
|
||||
{
|
||||
hotbar_draw(game);
|
||||
@@ -218,7 +218,7 @@ void Ui::draw(Game& game)
|
||||
}
|
||||
game.chat.draw(game);
|
||||
game.menu.draw(game);
|
||||
if (game.match.state == MATCH_COUNTDOWN && game.menu.isOpen && !isShared)
|
||||
if (game.sim.match.state == MATCH_COUNTDOWN && game.menu.isOpen && !isShared)
|
||||
countdown_aside_draw(game, game.input.devices[DEVICE_KEYBOARD].isAiming);
|
||||
if (isBarsRaised)
|
||||
hotbar_draw(game);
|
||||
@@ -229,34 +229,16 @@ void Ui::draw(Game& game)
|
||||
isActiveSeen = false;
|
||||
}
|
||||
|
||||
// Moves a level by step within range with a sound. Outside the menu it also pops up above the player.
|
||||
void Ui::level_step(Game& game, int& level, int step)
|
||||
{
|
||||
int next = std::clamp(level + step, 0, LEVEL_COUNT - 1);
|
||||
if (next == level)
|
||||
return;
|
||||
|
||||
bool isUp = next > level;
|
||||
level = next;
|
||||
game.audio.play(isUp ? SOUND_LEVEL_UP : SOUND_LEVEL_DOWN);
|
||||
if (game.menu.isOpen)
|
||||
return;
|
||||
|
||||
Fighter& fighter = game.local_get();
|
||||
Vec2 head = {fighter.center_get().x, fighter.body.box.y};
|
||||
game.effects.popup_spawn(head, isUp ? "Level Up" : "Level Down", isUp ? tint : shade);
|
||||
}
|
||||
|
||||
// Stacked above each living fighter: in a deathmatch its health bar, then (in a match, or a lobby's sandbox with others
|
||||
// in it) its name in its colour, then the local fighter's charge bar while charging, which blinks once full.
|
||||
void Ui::bars_draw(Game& game)
|
||||
{
|
||||
Match& match = game.match;
|
||||
Match& match = game.sim.match;
|
||||
Renderer& renderer = game.renderer;
|
||||
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
{
|
||||
Fighter& fighter = game.fighters[i];
|
||||
Fighter& fighter = game.sim.fighters[i];
|
||||
if (!fighter.isAlive)
|
||||
continue;
|
||||
Rect box = fighter.body.box;
|
||||
@@ -274,7 +256,7 @@ void Ui::bars_draw(Game& game)
|
||||
bottom = bar.y - BAR_GAP;
|
||||
}
|
||||
// Names show in a match, and in a lobby's sandbox once there's someone else to tell apart.
|
||||
bool isNamed = isMatch || (game.session.isActive && game.fighterCount > 1);
|
||||
bool isNamed = isMatch || (game.session.isActive && game.sim.fighterCount > 1);
|
||||
if (isNamed)
|
||||
{
|
||||
// Online, the player's connection beside it, the two centred together.
|
||||
@@ -293,7 +275,7 @@ void Ui::bars_draw(Game& game)
|
||||
if (i != game.local || fighter.charge < CHARGE_VISIBLE || game.session.is_watching())
|
||||
continue;
|
||||
float fill = (fighter.charge - CHARGE_VISIBLE) / (1 - CHARGE_VISIBLE);
|
||||
bool isShown = fighter.charge < 1 || fighter.is_flashing();
|
||||
bool isShown = fighter.charge < 1 || game.scene.is_flashing(fighter);
|
||||
Rect bar = {std::round(centerX - CHARGE_BAR_WIDTH / 2), bottom - CHARGE_BAR_HEIGHT, CHARGE_BAR_WIDTH,
|
||||
CHARGE_BAR_HEIGHT};
|
||||
bar_draw(renderer, bar, isShown ? fill : 0, color);
|
||||
@@ -314,7 +296,7 @@ void Ui::bar_draw(Renderer& renderer, Rect bar, float fill, Color color)
|
||||
// fight, the end of a round and the end of the match.
|
||||
void Ui::match_draw(Game& game)
|
||||
{
|
||||
Match& match = game.match;
|
||||
Match& match = game.sim.match;
|
||||
char text[TOOLTIP_LENGTH];
|
||||
isTipChosen &= match.state == MATCH_COUNTDOWN;
|
||||
Input& input = game.input;
|
||||
@@ -342,14 +324,14 @@ void Ui::match_draw(Game& game)
|
||||
// with it).
|
||||
Renderer& renderer = game.renderer;
|
||||
int length = 0;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
length += SDL_snprintf(text + length, sizeof(text) - length, i > 0 ? " - %d" : "%d", match.wins[i]);
|
||||
Vec2 scale = {SCORE_SCALE, SCORE_SCALE};
|
||||
float scoreWidth = game.text.size_get(text, FONT_SMALL, SCORE_SCALE).x;
|
||||
float hotbarRight = HOTBAR_MARGIN + game.local_get().weaponSlots * (SLOT_SIZE + SLOT_GAP) - SLOT_GAP;
|
||||
float x = std::round(std::max((CANVAS_WIDTH - scoreWidth) / 2, hotbarRight + SCORE_HOTBAR_GAP));
|
||||
float center = x + scoreWidth / 2;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
{
|
||||
char piece[TOOLTIP_LENGTH];
|
||||
if (i > 0)
|
||||
@@ -395,7 +377,7 @@ void Ui::match_draw(Game& game)
|
||||
text_center_draw(game, text, y, ROUND_SCALE);
|
||||
y += game.text.line_height_get(FONT_SMALL, ROUND_SCALE) + READY_GAP;
|
||||
SDL_snprintf(text, sizeof(text), isLocalReady ? "%d/%d Ready" : "%d/%d Ready - Press %s to Ready Up",
|
||||
info.ready, game.fighterCount, info.key);
|
||||
info.ready, game.sim.fighterCount, info.key);
|
||||
text_center_draw(game, text, y, 1);
|
||||
y += LINE_HEIGHT + READY_GAP;
|
||||
if (game.local_count() > 1 && (match.isWeaponPick || match.isTrinketPick))
|
||||
@@ -468,7 +450,7 @@ void Ui::match_draw(Game& game)
|
||||
text_center_draw(game, text, y, 1);
|
||||
y += LINE_HEIGHT * 2;
|
||||
}
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
{
|
||||
if (match.owners[i] < 0 || match.isGone[i])
|
||||
continue;
|
||||
@@ -498,17 +480,17 @@ static void time_format(char* text, int size, float seconds)
|
||||
void Ui::race_draw(Game& game)
|
||||
{
|
||||
char text[TOOLTIP_LENGTH];
|
||||
time_format(text, sizeof(text), game.match.clock);
|
||||
time_format(text, sizeof(text), game.sim.match.clock);
|
||||
text_center_draw(game, text, HOTBAR_MARGIN, SCORE_SCALE);
|
||||
|
||||
float left = std::round((CANVAS_WIDTH - TRACK_WIDTH) / 2);
|
||||
float y = HOTBAR_MARGIN + game.text.line_height_get(FONT_SMALL, SCORE_SCALE) + TRACK_DOT;
|
||||
game.renderer.rect_draw({left, y - TRACK_HEIGHT / 2, TRACK_WIDTH, TRACK_HEIGHT}, shade);
|
||||
float length = game.world.size_get().x;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
float length = game.sim.world.size_get().x;
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
{
|
||||
Fighter& fighter = game.fighters[i];
|
||||
float progress = game.match.finishTimes[i] >= 0 ? 1 : std::clamp(fighter.center_get().x / length, 0.0f, 1.0f);
|
||||
Fighter& fighter = game.sim.fighters[i];
|
||||
float progress = game.sim.match.finishTimes[i] >= 0 ? 1 : std::clamp(fighter.center_get().x / length, 0.0f, 1.0f);
|
||||
float x = std::round(left + progress * TRACK_WIDTH - TRACK_DOT / 2);
|
||||
Rect dot = {x, std::round(y - TRACK_DOT / 2), TRACK_DOT, TRACK_DOT};
|
||||
game.renderer.rect_draw(dot, game.color_get(i));
|
||||
@@ -519,16 +501,16 @@ void Ui::race_draw(Game& game)
|
||||
// below the last line.
|
||||
float Ui::results_draw(Game& game, float y)
|
||||
{
|
||||
Match& match = game.match;
|
||||
Match& match = game.sim.match;
|
||||
int order[MAX_FIGHTERS];
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
order[i] = i;
|
||||
auto rank = [&](int fighter) {
|
||||
return match.finishTimes[fighter] >= 0 ? match.finishTimes[fighter] : INFINITY;
|
||||
};
|
||||
std::stable_sort(order, order + game.fighterCount, [&](int a, int b) { return rank(a) < rank(b); });
|
||||
std::stable_sort(order, order + game.sim.fighterCount, [&](int a, int b) { return rank(a) < rank(b); });
|
||||
|
||||
for (int place = 0; place < game.fighterCount; place++)
|
||||
for (int place = 0; place < game.sim.fighterCount; place++)
|
||||
{
|
||||
int fighter = order[place];
|
||||
char time[TOOLTIP_LENGTH];
|
||||
@@ -730,16 +712,16 @@ void Ui::loadout_draw(Game& game, float alpha, float top)
|
||||
// What the countdown shows: the round's name, whole seconds left, how many are ready, and the key that readies up.
|
||||
static CountdownInfo countdown_info_get(Game& game, bool isPad)
|
||||
{
|
||||
Match& match = game.match;
|
||||
Match& match = game.sim.match;
|
||||
CountdownInfo info;
|
||||
if (match.rules.mode == MODE_RACE)
|
||||
SDL_strlcpy(info.round, "Race", sizeof(info.round));
|
||||
else if (match.is_final_round(game))
|
||||
else if (match.is_final_round(game.sim))
|
||||
SDL_strlcpy(info.round, "Final Round", sizeof(info.round));
|
||||
else
|
||||
SDL_snprintf(info.round, sizeof(info.round), "Round %d", match.round + 1);
|
||||
info.seconds = (int)std::ceil(match.timer);
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
info.ready += match.isReady[i];
|
||||
Binding binding = game.input.bindings[CONTROL_READY];
|
||||
game.input.binding_name_get(isPad ? binding.pad : binding.key, isPad, info.key, sizeof(info.key));
|
||||
@@ -756,7 +738,7 @@ void Ui::countdown_aside_draw(Game& game, bool isPad)
|
||||
const char* round = info.round;
|
||||
int seconds = info.seconds;
|
||||
int ready = info.ready;
|
||||
const char* key = game.match.isReady[game.local] ? nullptr : info.key;
|
||||
const char* key = game.sim.match.isReady[game.local] ? nullptr : info.key;
|
||||
float margin = (CANVAS_WIDTH - MENU_WIDTH) / 2;
|
||||
float middle = MENU_TOP + (HEADER_HEIGHT + BODY_HEIGHT) / 2;
|
||||
auto line_draw = [&](const char* line, float centerX, float y, float scale) {
|
||||
@@ -780,7 +762,7 @@ void Ui::countdown_aside_draw(Game& game, bool isPad)
|
||||
|
||||
float rightX = CANVAS_WIDTH - margin / 2;
|
||||
y = middle - (key ? LINE_HEIGHT * 3 : LINE_HEIGHT) / 2;
|
||||
SDL_snprintf(text, sizeof(text), "%d/%d Ready", ready, game.fighterCount);
|
||||
SDL_snprintf(text, sizeof(text), "%d/%d Ready", ready, game.sim.fighterCount);
|
||||
y += line_draw(text, rightX, y, 1);
|
||||
if (key)
|
||||
{
|
||||
@@ -816,10 +798,10 @@ void Ui::status_draw(Game& game)
|
||||
float y = HOTBAR_MARGIN;
|
||||
float rowHeight = game.text.line_height_get(FONT_SMALL, DPS_SCALE);
|
||||
|
||||
if (game.match.state != MATCH_PRACTICE && !game.match.is_racing())
|
||||
if (game.sim.match.state != MATCH_PRACTICE && !game.sim.match.is_racing())
|
||||
{
|
||||
Fighter& fighter = game.local_get();
|
||||
float maxHealth = game.match.health_get();
|
||||
float maxHealth = game.sim.match.health_get();
|
||||
Rect bar = {right - STATUS_BAR_WIDTH, std::round(y + (rowHeight - STATUS_BAR_HEIGHT) / 2), STATUS_BAR_WIDTH,
|
||||
STATUS_BAR_HEIGHT};
|
||||
bar_draw(game.renderer, bar, std::max(0.0f, fighter.health) / maxHealth, tint);
|
||||
@@ -1163,7 +1145,7 @@ Tooltip Ui::tooltip_build(Game& game, bool isTrinket, int item, int level, int o
|
||||
Tooltip tooltip;
|
||||
tooltip.isTrinket = isTrinket;
|
||||
tooltip.item = item;
|
||||
Fighter& fighter = owner >= 0 ? game.fighters[owner] : game.local_get();
|
||||
Fighter& fighter = owner >= 0 ? game.sim.fighters[owner] : game.local_get();
|
||||
|
||||
if (isTrinket)
|
||||
{
|
||||
@@ -1176,7 +1158,7 @@ Tooltip Ui::tooltip_build(Game& game, bool isTrinket, int item, int level, int o
|
||||
SDL_strlcpy(equipped, copies > 0 ? "Equipped" : "Not Equipped", sizeof(equipped));
|
||||
tooltip.name = def.name;
|
||||
// In the sandbox, the effects show what the next level changes after a ">", as weapons' stats do.
|
||||
bool isPreview = game.match.state == MATCH_PRACTICE && level < LEVEL_COUNT - 1;
|
||||
bool isPreview = game.sim.match.state == MATCH_PRACTICE && level < LEVEL_COUNT - 1;
|
||||
char effects[TOOLTIP_LENGTH];
|
||||
if (isPreview)
|
||||
effects_preview_get(game, def.effects[level], def.effects[level + 1], effects, sizeof(effects));
|
||||
@@ -1196,7 +1178,7 @@ Tooltip Ui::tooltip_build(Game& game, bool isTrinket, int item, int level, int o
|
||||
const WeaponDef& def = WEAPON_DEFS[item];
|
||||
tooltip.name = def.name;
|
||||
// In the sandbox, values that change at the next level show it after a ">".
|
||||
bool isPreview = game.match.state == MATCH_PRACTICE && level < LEVEL_COUNT - 1;
|
||||
bool isPreview = game.sim.match.state == MATCH_PRACTICE && level < LEVEL_COUNT - 1;
|
||||
SDL_snprintf(tooltip.stats, sizeof(tooltip.stats), isPreview ? "(Level %d, > Level %d)" : "(Level %d)", level + 1,
|
||||
level + 2);
|
||||
stats_table_build(fighter, item, level, isPreview, tooltip);
|
||||
@@ -2015,9 +1997,9 @@ void Ui::icon_draw(Game& game, bool isTrinket, int item, Draw placement, IconSiz
|
||||
void Ui::offscreen_draw(Game& game, Vec2 camera)
|
||||
{
|
||||
Vec2 half = {CANVAS_WIDTH / 2.0f, CANVAS_HEIGHT / 2.0f};
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
{
|
||||
Fighter& fighter = game.fighters[i];
|
||||
Fighter& fighter = game.sim.fighters[i];
|
||||
Vec2 offset = fighter.center_get() - camera - half;
|
||||
bool isVisible = std::abs(offset.x) < half.x && std::abs(offset.y) < half.y;
|
||||
if (i == game.local || !fighter.isAlive || isVisible)
|
||||
@@ -2048,14 +2030,14 @@ void Ui::crosshairs_draw(Game& game, Vec2 camera)
|
||||
{
|
||||
Input& input = game.input;
|
||||
bool isAny = false;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
{
|
||||
if (!game.is_local(i))
|
||||
continue;
|
||||
isAny = true;
|
||||
bool isMouse = (game.devices[i] == DEVICE_KEYBOARD && !input.devices[DEVICE_KEYBOARD].isAiming) ||
|
||||
(i == game.local && game.menu.isOpen);
|
||||
Vec2 aim = isMouse ? input.mouse : game.fighters[i].controls.cursor - camera;
|
||||
Vec2 aim = isMouse ? input.mouse : game.sim.fighters[i].controls.cursor - camera;
|
||||
game.sprites.ui.draw(game.renderer, "Crosshair", 0,
|
||||
{.color = game.color_get(i), .position = {std::floor(aim.x), std::floor(aim.y)}});
|
||||
}
|
||||
@@ -2070,7 +2052,7 @@ void Ui::panels_draw(Game& game)
|
||||
{
|
||||
int players[MAX_FIGHTERS];
|
||||
int count = 0;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
if (game.is_local(i))
|
||||
players[count++] = i;
|
||||
float width = (CANVAS_WIDTH - HOTBAR_MARGIN * 2 - LOADOUT_GAP * (count - 1)) / count;
|
||||
@@ -2078,7 +2060,7 @@ void Ui::panels_draw(Game& game)
|
||||
for (int k = 0; k < count; k++)
|
||||
{
|
||||
int index = players[k];
|
||||
Fighter& fighter = game.fighters[index];
|
||||
Fighter& fighter = game.sim.fighters[index];
|
||||
Color color = game.color_get(index);
|
||||
Color dim = color_shade(color, SECONDARY_SHADE);
|
||||
Rect panel = {std::round(HOTBAR_MARGIN + k * (width + LOADOUT_GAP)), top, std::floor(width),
|
||||
@@ -2088,11 +2070,11 @@ void Ui::panels_draw(Game& game)
|
||||
float y = panel.y + PICK_PADDING;
|
||||
const char* name = name_get(game, index);
|
||||
game.text.draw(game.renderer, name, {.color = color, .position = {x, y}});
|
||||
if (!game.match.is_racing())
|
||||
if (!game.sim.match.is_racing())
|
||||
{
|
||||
float barX = x + game.text.size_get(name).x + PICK_PADDING;
|
||||
Rect bar = {barX, y + 2, panel.x + panel.w - PICK_PADDING - barX, HEALTH_BAR_HEIGHT};
|
||||
bar_draw(game.renderer, bar, std::max(0.0f, fighter.health) / game.match.health_get(), color);
|
||||
bar_draw(game.renderer, bar, std::max(0.0f, fighter.health) / game.sim.match.health_get(), color);
|
||||
}
|
||||
int fit = (panel.w - PICK_PADDING * 2 + MINI_STEP - MINI_SLOT) / MINI_STEP;
|
||||
for (int kind = 0; kind < 2; kind++)
|
||||
@@ -2120,19 +2102,19 @@ void Ui::panels_draw(Game& game)
|
||||
// deathmatch, or over the line in a race) and someone else is in it.
|
||||
static bool is_spectating(Game& game)
|
||||
{
|
||||
MatchState state = (MatchState)game.match.state;
|
||||
MatchState state = (MatchState)game.sim.match.state;
|
||||
// Watching a queued game from the lobby, it's whoever's still in it.
|
||||
if (game.session.is_watching())
|
||||
{
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
if (game.fighters[i].isAlive)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
if (game.sim.fighters[i].isAlive)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
if ((state != MATCH_FIGHT && state != MATCH_ROUND_OVER) || game.local_count() != 1 || game.local_get().isAlive)
|
||||
return false;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
if (i != game.local && game.fighters[i].isAlive)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
if (i != game.local && game.sim.fighters[i].isAlive)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
@@ -2162,13 +2144,13 @@ void Ui::spectate_update(Game& game)
|
||||
}
|
||||
|
||||
auto is_watchable = [&](int fighter) {
|
||||
return fighter >= 0 && fighter < game.fighterCount && (fighter != game.local || isWatching) &&
|
||||
game.fighters[fighter].isAlive;
|
||||
return fighter >= 0 && fighter < game.sim.fighterCount && (fighter != game.local || isWatching) &&
|
||||
game.sim.fighters[fighter].isAlive;
|
||||
};
|
||||
auto next_get = [&](int from, int step) {
|
||||
for (int k = 1; k <= game.fighterCount; k++)
|
||||
for (int k = 1; k <= game.sim.fighterCount; k++)
|
||||
{
|
||||
int fighter = ((from + step * k) % game.fighterCount + game.fighterCount) % game.fighterCount;
|
||||
int fighter = ((from + step * k) % game.sim.fighterCount + game.sim.fighterCount) % game.sim.fighterCount;
|
||||
if (is_watchable(fighter))
|
||||
return fighter;
|
||||
}
|
||||
@@ -2176,7 +2158,7 @@ void Ui::spectate_update(Game& game)
|
||||
};
|
||||
if (!is_watchable(spectated))
|
||||
{
|
||||
int taker = spectated >= 0 ? game.fighters[spectated].launcher : game.local_get().launcher;
|
||||
int taker = spectated >= 0 ? game.sim.fighters[spectated].launcher : game.local_get().launcher;
|
||||
spectated = is_watchable(taker) ? taker : next_get(spectated >= 0 ? spectated : game.local, 1);
|
||||
return;
|
||||
}
|
||||
@@ -2220,13 +2202,13 @@ RivalLayout Ui::rivals_layout(Game& game)
|
||||
RivalLayout layout;
|
||||
Fighter& local = game.local_get();
|
||||
int slots[2] = {};
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
{
|
||||
if (i == game.local && !game.session.is_watching())
|
||||
continue;
|
||||
layout.fighters[layout.count++] = i;
|
||||
slots[0] = std::max(slots[0], game.fighters[i].weaponSlots);
|
||||
slots[1] = std::max(slots[1], game.fighters[i].trinketSlots);
|
||||
slots[0] = std::max(slots[0], game.sim.fighters[i].weaponSlots);
|
||||
slots[1] = std::max(slots[1], game.sim.fighters[i].trinketSlots);
|
||||
}
|
||||
if (layout.count == 0)
|
||||
return layout;
|
||||
@@ -2242,7 +2224,7 @@ RivalLayout Ui::rivals_layout(Game& game)
|
||||
};
|
||||
float widthMax = icons_width(ICON_FULL, RIVAL_FULL_SLOTS) + inset * 2;
|
||||
char health[TOOLTIP_LENGTH];
|
||||
SDL_snprintf(health, sizeof(health), "%d", (int)std::ceil(game.match.health_get()));
|
||||
SDL_snprintf(health, sizeof(health), "%d", (int)std::ceil(game.sim.match.health_get()));
|
||||
float minimum = game.text.size_get(health, FONT_TINY).x + RIVAL_PADDING + RIVAL_BAR_MIN + inset * 2;
|
||||
float width = 0;
|
||||
for (int size = ICON_FULL; size < ICON_SIZE_COUNT; size++)
|
||||
@@ -2268,7 +2250,7 @@ RivalLayout Ui::rivals_layout(Game& game)
|
||||
}
|
||||
|
||||
float inner = width - inset * 2;
|
||||
layout.font = game.match.is_racing() || game.text.size_get(health).x + RIVAL_PADDING + RIVAL_BAR_MIN <= inner
|
||||
layout.font = game.sim.match.is_racing() || game.text.size_get(health).x + RIVAL_PADDING + RIVAL_BAR_MIN <= inner
|
||||
? FONT_SMALL
|
||||
: FONT_TINY;
|
||||
for (int k = 0; k < layout.count; k++)
|
||||
@@ -2276,7 +2258,7 @@ RivalLayout Ui::rivals_layout(Game& game)
|
||||
layout.font = FONT_TINY;
|
||||
layout.line = game.text.line_height_get(layout.font);
|
||||
|
||||
float healthHeight = game.match.is_racing() ? 0 : layout.line + RIVAL_PADDING;
|
||||
float healthHeight = game.sim.match.is_racing() ? 0 : layout.line + RIVAL_PADDING;
|
||||
float icons = layout.rows * (ICON_PIXELS[layout.size] + RIVAL_ICON_GAP) - RIVAL_ICON_GAP;
|
||||
float height = inset + layout.line + RIVAL_PADDING + healthHeight + icons + inset;
|
||||
float x = CANVAS_WIDTH - HOTBAR_MARGIN - layout.count * width - (layout.count - 1) * RIVAL_GAP;
|
||||
@@ -2292,7 +2274,7 @@ RivalLayout Ui::rivals_layout(Game& game)
|
||||
// icon shows the item's tooltip, as that fighter has it, in its colour.
|
||||
void Ui::rivals_draw(Game& game)
|
||||
{
|
||||
Match& match = game.match;
|
||||
Match& match = game.sim.match;
|
||||
if (match.state == MATCH_PRACTICE || match.state == MATCH_COUNTDOWN)
|
||||
return;
|
||||
Renderer& renderer = game.renderer;
|
||||
@@ -2306,7 +2288,7 @@ void Ui::rivals_draw(Game& game)
|
||||
for (int k = 0; k < layout.count; k++)
|
||||
{
|
||||
int index = layout.fighters[k];
|
||||
Fighter& fighter = game.fighters[index];
|
||||
Fighter& fighter = game.sim.fighters[index];
|
||||
Rect card = layout.cards[k];
|
||||
float idle = fighter.isAlive || match.is_racing() ? HUD_IDLE_ALPHA : RIVAL_OUT_ALPHA;
|
||||
float alpha = index == spectated ? 1 : idle + (1 - idle) * rivalHovers[index];
|
||||
@@ -2379,7 +2361,7 @@ void Ui::rivals_draw(Game& game)
|
||||
|
||||
if (hoveredFighter < 0 || game.menu.isOpen)
|
||||
return;
|
||||
Fighter& fighter = game.fighters[hoveredFighter];
|
||||
Fighter& fighter = game.sim.fighters[hoveredFighter];
|
||||
bool isTrinket = hoveredKind == 1;
|
||||
int item = (isTrinket ? fighter.trinketLoadout : fighter.weaponLoadout)[hoveredSlot];
|
||||
int level = (isTrinket ? fighter.trinketSlotLevels : fighter.weaponSlotLevels)[hoveredSlot];
|
||||
@@ -2399,7 +2381,7 @@ void Ui::picks_draw(Game& game, float top)
|
||||
{
|
||||
int players[MAX_FIGHTERS];
|
||||
int count = 0;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
if (game.is_local(i))
|
||||
players[count++] = i;
|
||||
int columns = std::min(count, 2);
|
||||
@@ -2420,9 +2402,9 @@ void Ui::picks_draw(Game& game, float top)
|
||||
// it, or press a slot's number.
|
||||
void Ui::pick_panel_draw(Game& game, int index, Rect panel)
|
||||
{
|
||||
Match& match = game.match;
|
||||
Match& match = game.sim.match;
|
||||
Input& input = game.input;
|
||||
Fighter& fighter = game.fighters[index];
|
||||
Fighter& fighter = game.sim.fighters[index];
|
||||
int device = game.devices[index];
|
||||
bool isKeyboard = device == DEVICE_KEYBOARD;
|
||||
bool& isTrinket = isPickTrinkets[index];
|
||||
@@ -2692,14 +2674,14 @@ void Ui::scroll_mark()
|
||||
void Ui::queues_draw(Game& game)
|
||||
{
|
||||
hoveredQueue = -1;
|
||||
Match& match = game.match;
|
||||
Match& match = game.sim.match;
|
||||
if (!game.session.isActive || match.state != MATCH_PRACTICE)
|
||||
return;
|
||||
Renderer& renderer = game.renderer;
|
||||
Input& input = game.input;
|
||||
Vec2 mouse = input.mouse + renderer.camera;
|
||||
float bob = std::round(math_sin(SDL_GetTicks() / 1000.0f * QUEUE_ICON_SPEED) * QUEUE_ICON_BOB);
|
||||
for (int q = 0; q < game.fighterCount; q++)
|
||||
for (int q = 0; q < game.sim.fighterCount; q++)
|
||||
{
|
||||
Queue& queue = match.queues[q];
|
||||
if (!queue.isActive || match.isAway[q])
|
||||
@@ -2707,7 +2689,7 @@ void Ui::queues_draw(Game& game)
|
||||
Color color = game.color_get(q, true);
|
||||
Rect area = queue.area;
|
||||
int members[MAX_FIGHTERS];
|
||||
int count = match.queue_members_get(game, q, members);
|
||||
int count = match.queue_members_get(game.sim, q, members);
|
||||
renderer.rect_draw(area, color_fade(color, QUEUE_FILL_ALPHA));
|
||||
Color edge = color_fade(color, QUEUE_EDGE_ALPHA);
|
||||
renderer.rect_draw({area.x, area.y, 1, area.h}, edge);
|
||||
@@ -2720,7 +2702,7 @@ void Ui::queues_draw(Game& game)
|
||||
k < count ? color : color_shade(color, SECONDARY_SHADE));
|
||||
}
|
||||
|
||||
Fighter& queuer = game.fighters[q];
|
||||
Fighter& queuer = game.sim.fighters[q];
|
||||
Rect box = queuer.body.box;
|
||||
Vec2 icon = {std::round(box.x + box.w / 2), std::round(box.y - QUEUE_ICON_RISE + bob)};
|
||||
float bang = game.text.size_get("!", FONT_SMALL, 2).x;
|
||||
@@ -2763,9 +2745,9 @@ void Ui::queues_draw(Game& game)
|
||||
// The game a hovered queuer has queued, in a tooltip by the mouse.
|
||||
void Ui::queue_tooltip_draw(Game& game)
|
||||
{
|
||||
if (hoveredQueue < 0 || hoveredQueue >= game.fighterCount || game.menu.isOpen)
|
||||
if (hoveredQueue < 0 || hoveredQueue >= game.sim.fighterCount || game.menu.isOpen)
|
||||
return;
|
||||
const Rules& rules = game.match.queues[hoveredQueue].rules;
|
||||
const Rules& rules = game.sim.match.queues[hoveredQueue].rules;
|
||||
Vec2 size = game_summary_size(game, rules);
|
||||
Vec2 mouse = game.input.mouse;
|
||||
Rect panel = {mouse.x + TOOLTIP_OFFSET, mouse.y + TOOLTIP_OFFSET, size.x + TOOLTIP_PADDING * 2,
|
||||
|
||||
@@ -184,7 +184,6 @@ public:
|
||||
void init(Game& game);
|
||||
void update(Game& game);
|
||||
void draw(Game& game);
|
||||
void level_step(Game& game, int& level, int step);
|
||||
void slot_draw(Game& game, Slot slot);
|
||||
void panel_draw(Renderer& renderer, Rect rect, Color border, float fillAlpha = -1);
|
||||
Tooltip tooltip_build(Game& game, bool isTrinket, int item, int level, int owner = -1);
|
||||
|
||||
+226
-795
File diff suppressed because it is too large.
Load diff
+45
-35
@@ -1,14 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "anm2.h"
|
||||
#include "audio.h"
|
||||
#include "renderer.h"
|
||||
#include "common.h"
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
constexpr float BLAST_RING_ALPHA = 0.2f;
|
||||
constexpr float RICOCHET_BONUS = 0.5f;
|
||||
constexpr int WHIP_RAYS = 9;
|
||||
constexpr float WHIP_UNFURL = 0.6f;
|
||||
|
||||
class Fighter;
|
||||
struct Dummy;
|
||||
struct Game;
|
||||
struct Sim;
|
||||
|
||||
constexpr int LEVEL_COUNT = 3;
|
||||
constexpr int WEAPON_COUNT = 16;
|
||||
@@ -786,6 +790,14 @@ struct Capsule
|
||||
float radius = 0;
|
||||
};
|
||||
|
||||
// How attacks weaken and fill in, and the shapes they hit with, shared with drawing them.
|
||||
float falloff_at(float distance);
|
||||
float blast_area_get(Attack& attack);
|
||||
float strength_get(Attack& attack, Vec2 point);
|
||||
float fill_get(float strength);
|
||||
std::vector<Vec2> lash_get(Attack& attack);
|
||||
int shapes_get(Sim& sim, Attack& attack, Capsule* shapes);
|
||||
|
||||
// One hit's worth of damage and the feedback that goes with it. Source is where it came from, which a fighter gets
|
||||
// knocked away from, and knock scales how hard. Targets are numbered fighters first, then dummies
|
||||
// from MAX_FIGHTERS on.
|
||||
@@ -845,58 +857,56 @@ struct Echo
|
||||
int weaponSlot = 0;
|
||||
};
|
||||
|
||||
// One line the held weapon's attack would follow if fired now, through its steps, and the blast it would ring where
|
||||
// it ends (zero area for none).
|
||||
struct Path
|
||||
{
|
||||
std::vector<Vec2> steps;
|
||||
float area = 0;
|
||||
};
|
||||
|
||||
// Owns every attack in the world, spawns attacks from fighters and resolves
|
||||
// their hits. Each attack records the fighter that owns it.
|
||||
class Weapons
|
||||
{
|
||||
friend class Scene;
|
||||
|
||||
public:
|
||||
std::vector<Attack> attacks;
|
||||
float orbitClock = 0;
|
||||
|
||||
void fire(Game& game, Fighter& fighter, float charge);
|
||||
void update(Game& game);
|
||||
void draw(Game& game);
|
||||
void blast_draw(Game& game);
|
||||
void collision_draw(Game& game);
|
||||
void fire(Sim& sim, Fighter& fighter, float charge);
|
||||
void update(Sim& sim);
|
||||
void blend(float alpha);
|
||||
void held_draw(Game& game, Fighter& fighter);
|
||||
void offhand_draw(Game& game, Fighter& fighter);
|
||||
void trajectory_draw(Game& game, Fighter& fighter);
|
||||
std::vector<Path> paths_get(Sim& sim, Fighter& fighter);
|
||||
float buff_get(Fighter& fighter);
|
||||
float charge_fill_get(Fighter& fighter);
|
||||
Vec2 jitter_get(float fill);
|
||||
void blast_spawn(Game& game, Blast blast);
|
||||
void blast_spawn(Sim& sim, Blast blast);
|
||||
Attack* hook_get(Fighter& fighter);
|
||||
void attacks_release(Fighter& fighter, AttackKind kind);
|
||||
bool is_recalled(Game& game, Fighter& fighter);
|
||||
bool is_recalled(Sim& sim, Fighter& fighter);
|
||||
bool is_busy(Fighter& fighter, int key);
|
||||
bool is_spinning(Fighter& fighter);
|
||||
void damage_deal(Game& game, Damage damage);
|
||||
void contact_update(Game& game, Fighter& fighter);
|
||||
void wisp_move(Game& game, Attack& attack);
|
||||
void damage_deal(Sim& sim, Damage damage);
|
||||
void contact_update(Sim& sim, Fighter& fighter);
|
||||
void wisp_move(Sim& sim, Attack& attack);
|
||||
void wisps_release(Fighter& fighter);
|
||||
void teleport_move(Game& game, Attack& attack);
|
||||
void magic_draw(Game& game);
|
||||
void teleport_move(Sim& sim, Attack& attack);
|
||||
std::vector<Rect> solids_get(Fighter& fighter);
|
||||
|
||||
private:
|
||||
std::vector<Attack> pending;
|
||||
|
||||
bool is_spawned(Game& game, Fighter& fighter, Shot shot);
|
||||
void attack_move(Game& game, Attack& attack);
|
||||
bool is_spawned(Sim& sim, Fighter& fighter, Shot shot);
|
||||
void attack_move(Sim& sim, Attack& attack);
|
||||
void attack_remember(Attack& attack);
|
||||
void attack_hit(Game& game, Attack& attack, int target, Vec2 point);
|
||||
std::vector<Capsule> chain_get(Game& game, Fighter& fighter);
|
||||
void tesla_update(Game& game);
|
||||
void tesla_draw(Game& game);
|
||||
Pose attack_pose(Game& game, Attack& attack);
|
||||
void attack_hit(Sim& sim, Attack& attack, int target, Vec2 point);
|
||||
std::vector<Capsule> chain_get(Sim& sim, Fighter& fighter);
|
||||
void tesla_update(Sim& sim);
|
||||
Attack* tether_get(Fighter& fighter, int key, bool isOffhand);
|
||||
void hand_draw(Game& game, Fighter& fighter, int weaponSlot, bool isOffhand, Draw draw);
|
||||
float kick_get(Fighter& fighter);
|
||||
void hook_attach(Game& game, Attack& attack);
|
||||
void piece_launch(Game& game, Attack& piece);
|
||||
void attack_explode(Game& game, Attack& attack);
|
||||
void flail_move(Game& game, Attack& attack);
|
||||
void orbits_update(Game& game);
|
||||
void attacks_cap(Game& game);
|
||||
void hook_attach(Sim& sim, Attack& attack);
|
||||
void piece_launch(Sim& sim, Attack& piece);
|
||||
void attack_explode(Sim& sim, Attack& attack);
|
||||
void flail_move(Sim& sim, Attack& attack);
|
||||
void orbits_update(Sim& sim);
|
||||
void attacks_cap(Sim& sim);
|
||||
};
|
||||
+10
-112
@@ -1,7 +1,7 @@
|
||||
#include "world.h"
|
||||
#include "game.h"
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
#include "sim.h"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
constexpr const char* ARENA[] = {
|
||||
@@ -185,7 +185,6 @@ constexpr const char* const* COURSE_CHUNKS[] = {CHUNK_DOORS, CHUNK_PITS, CHUN
|
||||
CHUNK_VALLEY, CHUNK_STEEP, CHUNK_SKATE};
|
||||
|
||||
constexpr int COURSE_LENGTH = 8;
|
||||
constexpr int CORNER_SIZE = 2;
|
||||
constexpr int PLATFORM_HEIGHT = 8;
|
||||
constexpr float BODY_EPSILON = 0.01f;
|
||||
constexpr float BODY_STEP = TILE_SIZE / 2.0f;
|
||||
@@ -195,32 +194,23 @@ constexpr int STEEP_RAMP = 4;
|
||||
constexpr float JUMP_SLACK = 60;
|
||||
constexpr float RAMP_ANGLE_STEP = PI / 12;
|
||||
constexpr float RAMP_EPSILON = 0.001f;
|
||||
constexpr float RAMP_OUTLINE = 2;
|
||||
constexpr float STICK_SLACK = 1;
|
||||
constexpr float STICK_SLOPE = 1.75f;
|
||||
constexpr float STEP_UP = BOX_WIDTH / 2 * STICK_SLOPE + STICK_SLACK;
|
||||
constexpr Burst BREAK_BURST = {.gravity = 500, .life = 0.6f, .size = 4, .speed = 160, .count = 14};
|
||||
constexpr float BLOCK_HEALTH = 30;
|
||||
constexpr float BLOCK_REGROW = 8;
|
||||
constexpr float BLOCK_WARN = 1;
|
||||
constexpr float DUMMY_HEIGHT = 28;
|
||||
constexpr float DUMMY_WIDTH = 20;
|
||||
constexpr float FLASH_DECAY = 8;
|
||||
constexpr float WOBBLE_DAMPING = 5;
|
||||
constexpr float WOBBLE_LIMIT = 0.7f;
|
||||
constexpr float WOBBLE_STIFFNESS = 140;
|
||||
constexpr Vec2 CORNER_DIRECTIONS[] = {{-1, -1}, {1, -1}, {1, 1}, {-1, 1}};
|
||||
|
||||
static bool is_solid(Tile tile)
|
||||
{
|
||||
return tile == TILE_SOLID || tile == TILE_BLOCK;
|
||||
}
|
||||
|
||||
void World::init(Game& game)
|
||||
{
|
||||
sandbox_load();
|
||||
}
|
||||
|
||||
// The arena with the practice dummy standing on the first surface below the middle of the map, and the player
|
||||
// spawning right underneath it, on the next surface down.
|
||||
void World::sandbox_load()
|
||||
@@ -256,7 +246,7 @@ void World::arena_load()
|
||||
void World::course_build(Rng& rng)
|
||||
{
|
||||
std::vector<std::string> rows(CHUNK_START, CHUNK_START + COURSE_HEIGHT);
|
||||
const char* forced = SDL_getenv("RPG_FORCE_CHUNK");
|
||||
const char* forced = std::getenv("RPG_FORCE_CHUNK");
|
||||
int previous = -1;
|
||||
for (int i = 0; i < COURSE_LENGTH; i++)
|
||||
{
|
||||
@@ -265,7 +255,7 @@ void World::course_build(Rng& rng)
|
||||
pick = rng.random_get(0, std::size(COURSE_CHUNKS));
|
||||
previous = pick;
|
||||
if (forced)
|
||||
pick = SDL_atoi(forced);
|
||||
pick = std::atoi(forced);
|
||||
for (int y = 0; y < COURSE_HEIGHT; y++)
|
||||
rows[y] += COURSE_CHUNKS[pick][y];
|
||||
}
|
||||
@@ -353,17 +343,17 @@ void World::ramps_load(std::vector<std::string>& rows)
|
||||
}
|
||||
|
||||
// Knocks a block out of the grid in a shower of debris in the breaker's colour, to grow back later.
|
||||
void World::block_break(Game& game, Dummy& block, int breaker)
|
||||
void World::block_break(Sim& sim, Dummy& block, int breaker)
|
||||
{
|
||||
tiles[block.tile] = TILE_EMPTY;
|
||||
block.regrow = BLOCK_REGROW;
|
||||
Vec2 center = {block.box.x + block.box.w / 2, block.box.y + block.box.h / 2};
|
||||
game.effects.burst(BREAK_BURST, center, 0, game.color_get(breaker));
|
||||
game.audio.play(SOUND_BREAK);
|
||||
sim.cues->burst_spawn(BREAK_BURST, center, 0, breaker);
|
||||
sim.cues->sound_play(SOUND_BREAK);
|
||||
}
|
||||
|
||||
// Dummies wobble back upright, and broken blocks grow back once their time is up and no fighter stands in them.
|
||||
void World::update(Game& game)
|
||||
void World::update(Sim& sim)
|
||||
{
|
||||
for (Dummy& dummy : dummies)
|
||||
{
|
||||
@@ -371,8 +361,8 @@ void World::update(Game& game)
|
||||
{
|
||||
dummy.regrow -= TICK;
|
||||
bool isClear = true;
|
||||
for (int i = 0; i < game.fighterCount; i++)
|
||||
isClear &= !game.fighters[i].isAlive || !is_rect_overlapping(game.fighters[i].body.box, dummy.box);
|
||||
for (int i = 0; i < sim.fighterCount; i++)
|
||||
isClear &= !sim.fighters[i].isAlive || !is_rect_overlapping(sim.fighters[i].body.box, dummy.box);
|
||||
if (dummy.regrow <= 0 && isClear)
|
||||
{
|
||||
tiles[dummy.tile] = TILE_BLOCK;
|
||||
@@ -386,98 +376,6 @@ void World::update(Game& game)
|
||||
}
|
||||
}
|
||||
|
||||
void World::draw(Game& game)
|
||||
{
|
||||
Renderer& renderer = game.renderer;
|
||||
Anm2& tileArt = game.sprites.tiles;
|
||||
int startX = std::max(0, (int)std::floor(renderer.camera.x / TILE_SIZE));
|
||||
int startY = std::max(0, (int)std::floor(renderer.camera.y / TILE_SIZE));
|
||||
int endX = std::min(width - 1, (int)std::floor((renderer.camera.x + CANVAS_WIDTH) / TILE_SIZE));
|
||||
int endY = std::min(height - 1, (int)std::floor((renderer.camera.y + CANVAS_HEIGHT) / TILE_SIZE));
|
||||
|
||||
for (int y = startY; y <= endY; y++)
|
||||
{
|
||||
for (int x = startX; x <= endX; x++)
|
||||
{
|
||||
auto isOpen = [&](float dx, float dy) {
|
||||
Tile neighbour = tile_get(x + dx, y + dy);
|
||||
return neighbour != TILE_SOLID && neighbour != TILE_SLOPE;
|
||||
};
|
||||
auto isEmpty = [&](int dx) {
|
||||
return tile_get(x + dx, y) == TILE_EMPTY;
|
||||
};
|
||||
Tile tile = tile_get(x, y);
|
||||
Vec2 position = {(float)x * TILE_SIZE, (float)y * TILE_SIZE};
|
||||
|
||||
if (tile == TILE_SOLID)
|
||||
{
|
||||
int mask = isOpen(0, -1) | isOpen(1, 0) << 1 | isOpen(0, 1) << 2 | isOpen(-1, 0) << 3;
|
||||
tileArt.draw(renderer, "Wall", mask, {.position = position});
|
||||
|
||||
for (Vec2 corner : CORNER_DIRECTIONS)
|
||||
{
|
||||
if (isOpen(corner.x, 0) || isOpen(0, corner.y) || !isOpen(corner.x, corner.y))
|
||||
continue;
|
||||
float cornerX = corner.x < 0 ? position.x : position.x + TILE_SIZE - CORNER_SIZE;
|
||||
float cornerY = corner.y < 0 ? position.y : position.y + TILE_SIZE - CORNER_SIZE;
|
||||
renderer.rect_draw({cornerX, cornerY, CORNER_SIZE, CORNER_SIZE}, WHITE);
|
||||
}
|
||||
}
|
||||
else if (tile == TILE_PLATFORM)
|
||||
{
|
||||
int mask = isEmpty(1) | isEmpty(-1) << 1;
|
||||
tileArt.draw(renderer, "Platform", mask, {.position = position});
|
||||
}
|
||||
else if (tile == TILE_PIT || tile == TILE_FINISH)
|
||||
tileArt.draw(renderer, tile == TILE_PIT ? "Pit" : "Finish", 0, {.position = position});
|
||||
}
|
||||
}
|
||||
|
||||
// Ramps: an outline just inside the slope and its flat top, and a wall down the high end where it stands free.
|
||||
for (Ramp& ramp : ramps)
|
||||
{
|
||||
if (ramp.right < renderer.camera.x || ramp.left > renderer.camera.x + CANVAS_WIDTH)
|
||||
continue;
|
||||
float edge = ramp.anchor + (ramp.top - ramp.base) / ramp.slope;
|
||||
Vec2 low = {ramp.anchor, ramp.base};
|
||||
Vec2 high = {edge, ramp.top};
|
||||
Vec2 end = {ramp.slope < 0 ? ramp.right : ramp.left, ramp.top};
|
||||
Vec2 inset = vec2_normalize({ramp.slope, -1}) * (-RAMP_OUTLINE / 2);
|
||||
renderer.line_draw(low + inset, high + inset, RAMP_OUTLINE, WHITE);
|
||||
renderer.line_draw({high.x, high.y + RAMP_OUTLINE / 2}, {end.x, end.y + RAMP_OUTLINE / 2}, RAMP_OUTLINE, WHITE);
|
||||
|
||||
float side = ramp.slope < 0 ? 1 : -1;
|
||||
Vec2 beyond = {end.x + side * TILE_SIZE / 2, ramp.top + TILE_SIZE / 2};
|
||||
if (tile_at(beyond) != TILE_SOLID)
|
||||
renderer.line_draw({end.x - side * RAMP_OUTLINE / 2, ramp.top},
|
||||
{end.x - side * RAMP_OUTLINE / 2, ramp.base}, RAMP_OUTLINE, WHITE);
|
||||
}
|
||||
|
||||
// Blocks crack as they lose health, and a broken one shows a faint outline just before it grows back.
|
||||
for (Dummy& dummy : dummies)
|
||||
{
|
||||
if (dummy.tile < 0)
|
||||
continue;
|
||||
Vec2 position = {dummy.box.x, dummy.box.y};
|
||||
if (tiles[dummy.tile] == TILE_BLOCK)
|
||||
{
|
||||
int stage = std::min(BLOCK_STAGES - 1, (int)((1 - dummy.health / BLOCK_HEALTH) * BLOCK_STAGES));
|
||||
tileArt.draw(renderer, "Block", stage, {.position = position, .flash = dummy.flash});
|
||||
}
|
||||
else if (dummy.regrow < BLOCK_WARN)
|
||||
tileArt.draw(renderer, "Block", 0,
|
||||
{.color = color_fade(GRAY, 1 - dummy.regrow / BLOCK_WARN), .position = position});
|
||||
}
|
||||
|
||||
for (Dummy& dummy : dummies)
|
||||
{
|
||||
if (dummy.tile >= 0)
|
||||
continue;
|
||||
Vec2 base = {dummy.box.x + dummy.box.w / 2, dummy.box.y + dummy.box.h};
|
||||
game.sprites.dummy.draw(renderer, "Idle", 0, {.position = base, .angle = dummy.wobble, .flash = dummy.flash});
|
||||
}
|
||||
}
|
||||
|
||||
// Moves a body by its velocity for one tick, in steps no longer than half a tile so fast bodies can't skip over
|
||||
// or sink into walls.
|
||||
void World::body_move(Body& body)
|
||||
|
||||
+5
-5
@@ -5,7 +5,9 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct Game;
|
||||
constexpr float BLOCK_HEALTH = 30;
|
||||
|
||||
struct Sim;
|
||||
|
||||
enum Tile : unsigned char
|
||||
{
|
||||
@@ -93,13 +95,11 @@ public:
|
||||
int height = 0;
|
||||
int width = 0;
|
||||
|
||||
void init(Game& game);
|
||||
void update(Game& game);
|
||||
void draw(Game& game);
|
||||
void update(Sim& sim);
|
||||
void arena_load();
|
||||
void sandbox_load();
|
||||
void course_build(Rng& rng);
|
||||
void block_break(Game& game, Dummy& block, int breaker);
|
||||
void block_break(Sim& sim, Dummy& block, int breaker);
|
||||
void body_move(Body& body);
|
||||
Tile tile_get(int x, int y);
|
||||
Tile tile_at(Vec2 point);
|
||||
|
||||
Reference in new issue
Block a user