Profile stats, first-run hints, tiered balance, a wave-driven whip, and more options
- Profile Stats: damage dealt, kills and wins (single player and multiplayer) and the most and least played weapons and trinkets, counted from running totals in the simulation so online rewinds correct them. Protocol 6. - First-run control hints in the sandbox (and Chat in lobbies), each gone for good once used. - The ready countdown and a timed round's last five seconds beep (new countdown sound), and those seconds show big. When time runs out, everyone short of the most health dies. - Weapon balance tuned into tiers: Whip and Grenade Launcher on top, then melee, ranged, and utility (Vacuum, Grapple, Uzi) at the bottom. - The whip's lash is a chain of links a wave rolls down: windup, loop, crack, then a floppy recoil. - Muzzles come from Muzzle nulls in weapons.anm2 (read in at build time), for shots and the flash, which now follows the weapon. - Settings: Quit, Scale (1x-4x or Free), FPS meter and limit beside Vsync, Mute on Unfocus, Damage Numbers, Rumble, Stick Deadzone, and an Online section (Host, Chat, Ping) shared with the Multiplayer page. - The menu header lays out tabs and buttons by their names. About is sandbox-only. The score no longer shifts with the menu's tab. - Smaller meters, 90 tips (all ending in !), and a shorter lobby welcome.
This commit is contained in:
1 parent
e9d8cac926
commit
b96defa1e6
32 files changed
+1368
-396
No files matched your search
+125
-43
@@ -1,7 +1,9 @@
|
||||
#include "weapon.h"
|
||||
#include "sim.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <string_view>
|
||||
|
||||
constexpr int ATTACKS_MAX = 255;
|
||||
constexpr int DAMAGE_STEP = 5;
|
||||
@@ -67,8 +69,21 @@ constexpr float UPPERCUT_REACH = 16;
|
||||
constexpr float VOLLEY_GAP = 6;
|
||||
constexpr float WHIP_CONE = 0.25f;
|
||||
constexpr float WHIP_HANDLE = 4;
|
||||
constexpr float WHIP_COIL_GAP = 3;
|
||||
constexpr int WHIP_COIL_STEPS = 48;
|
||||
// A plain lash's motion, as shares of its lifetime: the handle draws back until WHIP_WINDUP, drives forward until
|
||||
// WHIP_THROW and stops, the loop that sends off reaches the tip as it cracks at PATH_OUT, and from WHIP_SETTLE the
|
||||
// hand draws it back in. Its heading is in radians off the aim, toward the side it rolls over.
|
||||
constexpr int WHIP_LINKS = 16;
|
||||
constexpr float WHIP_WINDUP = 0.12f;
|
||||
constexpr float WHIP_THROW = 0.24f;
|
||||
constexpr float WHIP_SETTLE = 0.6f;
|
||||
constexpr float WHIP_RAISED = 1.6f;
|
||||
constexpr float WHIP_REAR = 2.5f;
|
||||
constexpr float WHIP_DROOP = -0.9f;
|
||||
constexpr float WHIP_TAPER = 0.7f;
|
||||
constexpr float WHIP_KICK = 2.6f;
|
||||
constexpr float WHIP_WOBBLES = 1;
|
||||
constexpr float WHIP_DAMPING = 0.02f;
|
||||
constexpr float WHIP_SAG = 0.6f;
|
||||
constexpr float WHIP_PULL = 220;
|
||||
constexpr float WOBBLE_IMPULSE = 0.15f;
|
||||
// Vacuum: how fast its pull speeds fighters and shots up toward it, how much of an airblast's knock is left at the edge
|
||||
@@ -364,37 +379,101 @@ float strength_get(Attack& attack, Vec2 point)
|
||||
return attack.bonus * multiplier_get(attack, point);
|
||||
}
|
||||
|
||||
// A weapon's muzzle as weapons.anm2 places it: from the hand along the aim (x) and to its side (y, flipped facing
|
||||
// left).
|
||||
struct Muzzle
|
||||
{
|
||||
const char* weapon;
|
||||
float x;
|
||||
float y;
|
||||
};
|
||||
|
||||
// Each weapon's muzzle by its place in WEAPON_DEFS, from the Muzzle nulls in weapons.anm2 (read in at build time, see
|
||||
// sim.cmake), or MUZZLE_DISTANCE straight ahead for a weapon without one.
|
||||
static constexpr std::array<Vec2, WEAPON_COUNT> MUZZLES = [] {
|
||||
constexpr Muzzle NULLS[] = {WEAPON_MUZZLES{nullptr, 0, 0}};
|
||||
std::array<Vec2, WEAPON_COUNT> muzzles;
|
||||
for (int weapon = 0; weapon < WEAPON_COUNT; weapon++)
|
||||
{
|
||||
muzzles[weapon] = {MUZZLE_DISTANCE, 0};
|
||||
for (const Muzzle& null : NULLS)
|
||||
if (null.weapon && std::string_view(null.weapon) == WEAPON_DEFS[weapon].name)
|
||||
muzzles[weapon] = {null.x, null.y};
|
||||
}
|
||||
return muzzles;
|
||||
}();
|
||||
|
||||
// Where an attack's shots leave its weapon, held at its origin and pointing along its angle: the weapon's muzzle (see
|
||||
// MUZZLES), turned to the angle and mirrored facing left.
|
||||
Vec2 muzzle_get(Attack& attack)
|
||||
{
|
||||
Vec2 muzzle = MUZZLES[attack.weapon];
|
||||
float side = attack.isLeft ? -1 : 1;
|
||||
return attack.origin + vec2_polar(attack.angle, muzzle.x) + vec2_polar(attack.angle + PI / 2, muzzle.y * side);
|
||||
}
|
||||
|
||||
// How much of an attack's black fill its colour takes over, from none at plain damage to all at FILL_STRENGTH.
|
||||
float fill_get(float strength)
|
||||
{
|
||||
return std::clamp((strength - 1) / (FILL_STRENGTH - 1), 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
// A plain lash rolling out: straight from the handle to its front, then the rest of the whip still wound there in an
|
||||
// even spiral (its rings WHIP_COIL_GAP apart), curling up and in toward its end, so the coil unwinds as it unrolls. It
|
||||
// lies straight as it cracks, and coming back it winds up again on the other side, below.
|
||||
// Where a plain lash's handle points at a share of its lifetime, off the aim (see WHIP_WINDUP): eased back slowly,
|
||||
// driven forward sharply over the top, held along the aim through the crack, then drooping.
|
||||
static float lash_heading_get(float time)
|
||||
{
|
||||
time = std::max(0.0f, time);
|
||||
if (time < WHIP_WINDUP)
|
||||
{
|
||||
float ease = 1 - time / WHIP_WINDUP;
|
||||
return math_lerp(WHIP_RAISED, WHIP_REAR, 1 - ease * ease);
|
||||
}
|
||||
if (time < WHIP_THROW)
|
||||
{
|
||||
float drive = (time - WHIP_WINDUP) / (WHIP_THROW - WHIP_WINDUP);
|
||||
return math_lerp(WHIP_REAR, 0, drive * drive * (3 - 2 * drive));
|
||||
}
|
||||
if (time < PATH_OUT)
|
||||
return 0;
|
||||
return WHIP_DROOP * std::min(1.0f, (time - PATH_OUT) / (WHIP_SETTLE - PATH_OUT));
|
||||
}
|
||||
|
||||
// The way a heading off a plain lash's aim points, turning toward the side it rolls over.
|
||||
static Vec2 lash_direction_get(Attack& attack, float heading)
|
||||
{
|
||||
Vec2 forward = vec2_polar(attack.aimAngle, 1);
|
||||
Vec2 up = attack.isLeft ? Vec2{-forward.y, forward.x} : Vec2{forward.y, -forward.x};
|
||||
return forward * math_cos(heading) + up * math_sin(heading);
|
||||
}
|
||||
|
||||
// A plain lash as a chain of links from the end of its handle. Each link points where the handle pointed a moment
|
||||
// before, longer toward the tip but less so for the lighter links, so the handle's stop rolls a loop down it that
|
||||
// straightens the tip as it cracks. After, the tip snaps back over and wobbles out ever slower while the slack whip
|
||||
// sags, and the hand draws it in.
|
||||
std::vector<Vec2> lash_get(Attack& attack)
|
||||
{
|
||||
Vec2 direction = vec2_polar(attack.angle, 1);
|
||||
Vec2 up = attack.isLeft ? Vec2{-direction.y, direction.x} : Vec2{direction.y, -direction.x};
|
||||
up = attack.progress_get() > PATH_OUT ? up * -1 : up;
|
||||
float coil = attack.length - vec2_length(attack.tip - attack.position);
|
||||
std::vector<Vec2> points = {attack.position, attack.tip};
|
||||
if (coil < 1)
|
||||
return points;
|
||||
float time = attack.progress_get();
|
||||
float recoil = std::max(0.0f, (time - PATH_OUT) / (1 - PATH_OUT));
|
||||
float slack = time < PATH_OUT ? std::max(0.0f, 1 - time / WHIP_THROW) : std::min(1.0f, 2 * recoil);
|
||||
float kick = time < PATH_OUT
|
||||
? 0
|
||||
: WHIP_KICK * math_sin(2 * PI * WHIP_WOBBLES * std::sqrt(recoil)) * math_pow(WHIP_DAMPING, recoil);
|
||||
float drawn = time < WHIP_SETTLE ? 1 : 1 - (time - WHIP_SETTLE) / (1 - WHIP_SETTLE);
|
||||
float step = attack.length * drawn / WHIP_LINKS;
|
||||
Vec2 forward = vec2_polar(attack.aimAngle, 1);
|
||||
Vec2 up = lash_direction_get(attack, PI / 2);
|
||||
// Slack, it sags toward straight down: behind the hand while drawn back, ahead of it after the crack.
|
||||
float rest = time < PATH_OUT ? WHIP_REAR : 0;
|
||||
float down = rest + std::remainder(math_atan2(up.y, forward.y) - rest, 2 * PI);
|
||||
|
||||
// An even spiral bends at each point in a circle as wide as the square root of twice the whip left beyond it times
|
||||
// its ring spacing over a full turn.
|
||||
float turn = direction.x * up.y - direction.y * up.x > 0 ? 1 : -1;
|
||||
float spacing = WHIP_COIL_GAP * attack.scale / (2 * PI);
|
||||
float heading = attack.angle;
|
||||
float step = coil / WHIP_COIL_STEPS;
|
||||
Vec2 point = attack.tip;
|
||||
for (int i = 0; i < WHIP_COIL_STEPS; i++)
|
||||
std::vector<Vec2> points = {attack.position};
|
||||
for (int i = 0; i < WHIP_LINKS; i++)
|
||||
{
|
||||
heading += turn * step / std::sqrt(2 * spacing * (coil - i * step));
|
||||
point = point + vec2_polar(heading, step);
|
||||
points.push_back(point);
|
||||
float along = (i + 0.5f) / WHIP_LINKS;
|
||||
float heading = lash_heading_get(time - (PATH_OUT - WHIP_THROW) * math_pow(along, WHIP_TAPER));
|
||||
heading += kick * along * along;
|
||||
heading += (down - heading) * WHIP_SAG * slack * along;
|
||||
points.push_back(points.back() + lash_direction_get(attack, heading) * step);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
@@ -465,7 +544,7 @@ static Vec2 ray_march(Vec2 start, float angle, float range, auto isBlocked)
|
||||
}
|
||||
|
||||
// The capsules an attack hits with right now, returning how many. A plain whip fans rays across its cone from the
|
||||
// hand, out to its unfurled front and short of walls. Everything else is one capsule from position to tip.
|
||||
// hand, out as far ahead as its lash reaches and short of walls. Everything else is one capsule from position to tip.
|
||||
int shapes_get(Sim& sim, Attack& attack, Capsule* shapes)
|
||||
{
|
||||
if (attack.def()->kind != ATTACK_WHIP || attack.is_charged())
|
||||
@@ -476,7 +555,10 @@ int shapes_get(Sim& sim, Attack& attack, Capsule* shapes)
|
||||
auto isBlocked = [&](Vec2 point) {
|
||||
return sim.world.is_blocking_at(point);
|
||||
};
|
||||
float front = vec2_length(attack.tip - attack.origin);
|
||||
float front = 0;
|
||||
Vec2 forward = vec2_polar(attack.aimAngle, 1);
|
||||
for (Vec2 point : lash_get(attack))
|
||||
front = std::max(front, vec2_dot(point - attack.origin, forward));
|
||||
for (int ray = 0; ray < WHIP_RAYS; ray++)
|
||||
{
|
||||
float angle = attack.aimAngle + WHIP_CONE * (2.0f * ray / (WHIP_RAYS - 1) - 1);
|
||||
@@ -748,7 +830,7 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot)
|
||||
.isLeft = fighter.is_left(),
|
||||
.isSecondary = shot.isSecondary,
|
||||
};
|
||||
attack.position = attack.origin + aim * MUZZLE_DISTANCE;
|
||||
attack.position = muzzle_get(attack);
|
||||
|
||||
// Charging burns a grenade's fuse down (fully charged, it goes off in the barrel, see below).
|
||||
if (kind == ATTACK_GRENADE)
|
||||
@@ -808,8 +890,8 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot)
|
||||
piece.aimAngle += offset;
|
||||
piece.velocity = vec2_polar(piece.angle, stats.speed + push);
|
||||
// A slash sets off a little ahead of the hand, clear of its fighter.
|
||||
float muzzle = kind == ATTACK_SLASH ? SLASH_DISTANCE * piece.scale + SLASH_GAP : MUZZLE_DISTANCE;
|
||||
piece.position = piece.origin + vec2_polar(piece.angle, muzzle);
|
||||
float slash = SLASH_DISTANCE * piece.scale + SLASH_GAP;
|
||||
piece.position = kind == ATTACK_SLASH ? piece.origin + vec2_polar(piece.angle, slash) : muzzle_get(piece);
|
||||
piece.isSecondary |= split != splits / 2;
|
||||
piece.isCopy = shot.isEcho || split != splits / 2;
|
||||
if (def.isOrbiting)
|
||||
@@ -843,10 +925,9 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot)
|
||||
}
|
||||
sim.cues->sound_play(shot.isEcho ? SOUND_ECHO : def.sound);
|
||||
|
||||
Vec2 muzzle = attack.origin + aim * MUZZLE_DISTANCE;
|
||||
float side = fighter.is_left() ? -1 : 1;
|
||||
if (weapon.flash > 0)
|
||||
sim.cues->flash_spawn(muzzle, vec2_angle(aim), weapon.flash, fighter.index);
|
||||
sim.cues->flash_spawn(attack, weapon.flash);
|
||||
if (weapon.isEjecting)
|
||||
sim.cues->burst_spawn(CASING_BURST, attack.origin, vec2_angle(aim) - side * (PI / 2 + CASING_BACK),
|
||||
fighter.index);
|
||||
@@ -1077,10 +1158,8 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
|
||||
case ATTACK_WHIP:
|
||||
{
|
||||
// A charged crack stays strung from the end of the handle in the hand to where it cracked, hitting at
|
||||
// once. A plain lash rolls out from the handle along its path and back, going with the hand, in one sweep
|
||||
// across its cone: in from the upper edge as it unrolls, straight along the aim as it cracks at full reach
|
||||
// (short of any wall), and on down to the lower edge as it comes back. It hits anywhere in the cone out to
|
||||
// its front until it cracks.
|
||||
// once. A plain lash goes with the hand, as long as its reach along the aim (short of any wall), its handle
|
||||
// and links moving as lash_get says. It hits anywhere in its cone as far as it reaches until it cracks.
|
||||
if (attack.is_charged())
|
||||
{
|
||||
attack.origin = fighter.hand_get(attack.hand);
|
||||
@@ -1092,15 +1171,12 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
|
||||
auto isBlocked = [&](Vec2 point) {
|
||||
return sim.world.is_blocking_at(point);
|
||||
};
|
||||
float out = vec2_length(attack.position - attack.origin);
|
||||
float unfurl = std::min(1.0f, progress / PATH_OUT);
|
||||
float sweep = progress < PATH_OUT ? unfurl - 1 : (progress - PATH_OUT) / (1 - PATH_OUT);
|
||||
attack.angle = attack.aimAngle + (attack.isLeft ? -1 : 1) * WHIP_CONE * std::min(1.0f, sweep);
|
||||
Vec2 reach = ray_march(attack.origin, attack.angle, reach_at_get(attack), isBlocked);
|
||||
Vec2 reach = ray_march(attack.origin, attack.aimAngle, reach_at_get(attack), isBlocked);
|
||||
attack.angle = vec2_angle(lash_direction_get(attack, lash_heading_get(progress)));
|
||||
attack.position = attack.origin + vec2_polar(attack.angle, WHIP_HANDLE);
|
||||
attack.length = std::max(0.0f, vec2_length(reach - attack.position));
|
||||
attack.tip = attack.position + vec2_polar(attack.angle, std::clamp(out - WHIP_HANDLE, 0.0f, attack.length));
|
||||
attack.hitsLeft = unfurl < 1 ? attack.hitsLeft : 0;
|
||||
attack.length = std::max(0.0f, vec2_length(reach - attack.origin) - WHIP_HANDLE);
|
||||
attack.tip = lash_get(attack).back();
|
||||
attack.hitsLeft = progress < PATH_OUT ? attack.hitsLeft : 0;
|
||||
break;
|
||||
}
|
||||
case ATTACK_SPIN:
|
||||
@@ -1543,6 +1619,8 @@ void Weapons::damage_deal(Sim& sim, Damage damage)
|
||||
float side = box.x + box.w / 2 > attacker.center_get().x ? 1 : -1;
|
||||
attacker.hitstop_add(damage.hitstop);
|
||||
damage.knock *= attacker.stat_get(STAT_KNOCKBACK);
|
||||
bool isCounted = damage.target != damage.owner && !(damage.target < MAX_FIGHTERS && sim.match.is_racing());
|
||||
attacker.damageDealt += isCounted ? damage.amount : 0;
|
||||
if (damage.target < MAX_FIGHTERS)
|
||||
sim.fighters[damage.target].damage_take(sim, damage);
|
||||
else
|
||||
@@ -1829,7 +1907,11 @@ std::vector<Path> Weapons::paths_get(Sim& sim, Fighter& fighter)
|
||||
Vec2 aim = vec2_polar(vec2_angle(fighter.aim) + offset, 1);
|
||||
float speed = stats.speed * fighter.stat_get(STAT_PROJECTILE_SPEED) + push_get(fighter, aim);
|
||||
Vec2 velocity = isArrow ? aim * speed : aim;
|
||||
Vec2 point = fighter.hand_get() + aim * MUZZLE_DISTANCE;
|
||||
Attack probe = {.origin = fighter.hand_get(),
|
||||
.angle = vec2_angle(aim),
|
||||
.weapon = fighter.weaponLoadout[key],
|
||||
.isLeft = fighter.is_left()};
|
||||
Vec2 point = muzzle_get(probe);
|
||||
int bounces = def.isBouncy ? (int)fighter.stat_get(STAT_RICOCHETS) : 0;
|
||||
Path path;
|
||||
bool isEnded = stats.range <= TRAJECTORY_REACH;
|
||||
|
||||
Reference in new issue
Block a user