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
+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;
|
||||
|
||||
Reference in new issue
Block a user