Everything ricochets, desync resync, aim previews that fall, and suffocation
- Ricochet now bounces every attack that moves. Swings sweep back, the uppercut bounces its fighter, punches and Lance pokes come loose and thrust again from the wall, then a held Lance turns and flies home. A whip lash's crack glances the reach a wall cut off as a strand. Vacuum cones glance into further cones that suck around corners. Bolts, grapple hooks and charged cracks bounce, and so do shots heading home (boomerangs, flail balls, hooks). - Every ricochet plays its sound (flames too), adds its damage (the charged crack's blast too), and gives the attack its hits back. With bounces left, a bouncing shot hops off anything it touches, even at rest, until they're spent. Rays fired from inside a wall back out first instead of bouncing on the spot. - Aim previews: falling shots arc at their real speed, instants (beams, cracks) take their real path and full range per bounce, Vacuum cones show their glances, and every cone draws at its spread width. - A client out of step with the server asks for its room again and resyncs. The checksum covers much more of the match. Protocol 12. - Fighters stuck inside solid ground slowly suffocate (not in the sandbox or races). - Sword slashes smaller (basic 25%, charged slash scales halved). The Good Stuff: "Aye, me bottle o'scrumpy!"
This commit is contained in:
1 parent
37560547d4
commit
7b98ff1d6b
13 files changed
+551
-156
No files matched your search
Binary file not shown.
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 15 KiB |
@@ -577,6 +577,15 @@ static void packet_handle(int id, const std::string& bytes)
|
||||
}
|
||||
return;
|
||||
}
|
||||
// A player out of step with their room gets it again, as when they came in (but already seated).
|
||||
if (type == PACKET_RESYNC)
|
||||
{
|
||||
int serial = 0;
|
||||
if (!reader.get(serial) || serial != member.room || !rooms.contains(serial))
|
||||
return;
|
||||
std::printf("Lobby %s: %d resynced room %d at tick %d\n", member.code.c_str(), id, serial, rooms[serial].tick);
|
||||
return room_enter(id, serial, ENTRY_SEATED);
|
||||
}
|
||||
if (type == PACKET_QUEUE)
|
||||
{
|
||||
Uint8 isOpen = 0;
|
||||
|
||||
+47
-18
@@ -9,6 +9,9 @@ constexpr float KNOCK_BASE = 60;
|
||||
constexpr float KNOCK_LIFT = 0.35f;
|
||||
constexpr float KNOCK_MAX = 4500;
|
||||
constexpr float KNOCK_PER_DAMAGE = 30;
|
||||
// How often a fighter stuck in solid ground loses health, and how much.
|
||||
constexpr int SUFFOCATE_TICKS = 5;
|
||||
constexpr int SUFFOCATE_DAMAGE = 1;
|
||||
constexpr float LAUNCH_START_SPEED = 300;
|
||||
|
||||
constexpr float AFTERIMAGE_SPEED = 440;
|
||||
@@ -241,9 +244,13 @@ void Fighter::update(Sim& sim)
|
||||
sim.world.body_move(body);
|
||||
sim.world.obstacles.clear();
|
||||
sim.world.ledges.clear();
|
||||
if (isLaunched)
|
||||
// A riding attack's bounce comes first (see Weapons::ride_bounce), so a fighter never bounces twice off one
|
||||
// surface.
|
||||
bool isRidden = sim.weapons.ride_bounce(sim, *this, {runSpeed, fallSpeed});
|
||||
if (isLaunched && !isRidden)
|
||||
bounce_resolve(sim, {runSpeed, fallSpeed});
|
||||
safe_update(sim);
|
||||
suffocation_update(sim);
|
||||
|
||||
wallMemory -= TICK;
|
||||
bool isWallHit = body.velocity.x == 0 && std::abs(runSpeed) > WALL_IMPACT_SPEED;
|
||||
@@ -518,32 +525,37 @@ void Fighter::wall_push(Sim& sim, int wall, Vec2 base)
|
||||
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(Sim& sim, Vec2 before)
|
||||
// Bounces off whatever stopped it this move (before is the speed it had), keeping keep of its speed into the surface (a
|
||||
// slope's, or a wall's, ceiling's or floor's). Returns how hard it hit, or 0 when it didn't.
|
||||
float Fighter::surface_bounce(Vec2 before, float keep)
|
||||
{
|
||||
float impact = 0;
|
||||
Vec2 normal = body.normal;
|
||||
float into = vec2_dot(before, normal);
|
||||
if (into < -BOUNCE_MIN)
|
||||
{
|
||||
body.velocity = before - normal * (into * (1 + BOUNCE_KEEP));
|
||||
impact = -into;
|
||||
body.velocity = before - normal * (into * (1 + keep));
|
||||
return -into;
|
||||
}
|
||||
else
|
||||
if (body.velocity.x == 0 && std::abs(before.x) > BOUNCE_MIN)
|
||||
{
|
||||
if (body.velocity.x == 0 && std::abs(before.x) > BOUNCE_MIN)
|
||||
{
|
||||
body.velocity.x = -before.x * BOUNCE_KEEP;
|
||||
impact = std::abs(before.x);
|
||||
}
|
||||
if (body.velocity.y == 0 && std::abs(before.y) > BOUNCE_MIN)
|
||||
{
|
||||
body.velocity.y = -before.y * BOUNCE_KEEP;
|
||||
impact = std::max(impact, std::abs(before.y));
|
||||
}
|
||||
body.velocity.x = -before.x * keep;
|
||||
impact = std::abs(before.x);
|
||||
}
|
||||
if (body.velocity.y == 0 && std::abs(before.y) > BOUNCE_MIN)
|
||||
{
|
||||
body.velocity.y = -before.y * keep;
|
||||
impact = std::max(impact, std::abs(before.y));
|
||||
}
|
||||
return impact;
|
||||
}
|
||||
|
||||
// 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(Sim& sim, Vec2 before)
|
||||
{
|
||||
float impact = surface_bounce(before, BOUNCE_KEEP);
|
||||
if (impact > 0)
|
||||
{
|
||||
sim.cues->sound_play(SOUND_BOUNCE);
|
||||
@@ -562,6 +574,23 @@ void Fighter::bounce_resolve(Sim& sim, Vec2 before)
|
||||
isLaunched = vec2_length(body.velocity) >= LAUNCH_END_SPEED;
|
||||
}
|
||||
|
||||
// Stuck inside solid ground (its middle in it, however it got there), a fighter suffocates: SUFFOCATE_DAMAGE every
|
||||
// SUFFOCATE_TICKS, flashing as for any hit but not knocked about. Races and the sandbox cost no health, as with any
|
||||
// hit.
|
||||
void Fighter::suffocation_update(Sim& sim)
|
||||
{
|
||||
suffocation = sim.world.is_solid_at(center_get()) ? suffocation + 1 : 0;
|
||||
if (suffocation == 0 || suffocation % SUFFOCATE_TICKS != 0)
|
||||
return;
|
||||
damage_take(sim, {.point = center_get(),
|
||||
.source = center_get(),
|
||||
.knock = 0,
|
||||
.amount = SUFFOCATE_DAMAGE,
|
||||
.owner = index,
|
||||
.target = index,
|
||||
.sound = SOUND_NONE});
|
||||
}
|
||||
|
||||
// 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(Sim& sim)
|
||||
|
||||
@@ -137,6 +137,8 @@ public:
|
||||
// When each weapon slot was last switched away from, counting switches (0 for never), for Dual Wield.
|
||||
int slotSwitches[SLOT_MAX] = {};
|
||||
int switches = 0;
|
||||
// How many ticks in a row its middle has been inside solid ground (see suffocation_update).
|
||||
int suffocation = 0;
|
||||
int trinketItemLevels[TRINKET_COUNT] = {};
|
||||
int trinketSlotLevels[SLOT_MAX] = {};
|
||||
int trinketLoadout[SLOT_MAX];
|
||||
@@ -160,6 +162,8 @@ public:
|
||||
void position_set(Sim& sim, Vec2 feet);
|
||||
void slots_set(int weapons, int trinkets);
|
||||
void damage_take(Sim& sim, Damage damage);
|
||||
void suffocation_update(Sim& sim);
|
||||
float surface_bounce(Vec2 before, float keep);
|
||||
void die(Sim& sim);
|
||||
void update(Sim& sim);
|
||||
void blend(float alpha);
|
||||
|
||||
+5
-2
@@ -27,7 +27,7 @@
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
constexpr int PROTOCOL_VERSION = 11;
|
||||
constexpr int PROTOCOL_VERSION = 12;
|
||||
constexpr int LOBBY_MEMBERS_MAX = 16;
|
||||
constexpr int LOBBY_CODE_LENGTH = 4;
|
||||
// The longest a lobby's name and password can be.
|
||||
@@ -57,6 +57,8 @@ constexpr int SERVER_URL_LENGTH = 128;
|
||||
// PACKET_PING Uint16 milliseconds this player's round trip to the server
|
||||
// PACKET_WATCH room spectate one of the lobby's games (from its sandbox),
|
||||
// or 0 to go back to the sandbox
|
||||
// PACKET_RESYNC room this player's game went out of step with the room's:
|
||||
// send it again
|
||||
//
|
||||
// Server to game:
|
||||
// PACKET_STATE room, tick, state the game this player is in now, as a tick starts
|
||||
@@ -92,7 +94,8 @@ enum PacketType : unsigned char
|
||||
PACKET_CHECKSUM,
|
||||
PACKET_PING,
|
||||
PACKET_ROOMS,
|
||||
PACKET_WATCH
|
||||
PACKET_WATCH,
|
||||
PACKET_RESYNC
|
||||
};
|
||||
|
||||
// How a message goes: lobby text over the socket, or game bytes over the data channels, either reliably and in order
|
||||
|
||||
+25
-15
@@ -935,18 +935,22 @@ void Scene::attacks_draw()
|
||||
renderer.ring_draw(attack.position, blast_area_get(attack), color_fade(tint, BLAST_RING_ALPHA));
|
||||
// A Vacuum's cone is outlined: steady while it sucks, fading as an airblast passes.
|
||||
// Its fill fades toward the far end, as its strength does.
|
||||
if (is_cone(*attack.def()))
|
||||
// Bounced off walls by Ricochet, it's drawn as each cone it glances into.
|
||||
Attack cones[CONES_MAX];
|
||||
int count = is_cone(*attack.def()) ? cones_get(game.sim, attack, cones) : 0;
|
||||
for (int c = 0; c < count; c++)
|
||||
{
|
||||
Vec2 tip = attack.position + vec2_polar(attack.aimAngle, cone_reach_get(attack));
|
||||
Attack& cone = cones[c];
|
||||
Vec2 tip = cone.position + vec2_polar(cone.aimAngle, cone_reach_get(cone));
|
||||
float alpha = kind == ATTACK_GUST ? 1 - progress : 1;
|
||||
cone_fill_draw(renderer, attack.position, tip, VACUUM_CONE, color_fade(tint, alpha * CONE_FILL_ALPHA));
|
||||
cone_fill_draw(renderer, cone.position, tip, cone_get(cone), color_fade(tint, alpha * CONE_FILL_ALPHA));
|
||||
if (kind == ATTACK_VACUUM)
|
||||
suck_draw(attack, tint);
|
||||
suck_draw(cone, tint);
|
||||
else
|
||||
gust_draw(attack, tint);
|
||||
cone_outline_draw(renderer, attack.position, tip, VACUUM_CONE, color_fade(tint, alpha * CONE_ALPHA));
|
||||
gust_draw(cone, tint);
|
||||
cone_outline_draw(renderer, cone.position, tip, cone_get(cone), color_fade(tint, alpha * CONE_ALPHA));
|
||||
}
|
||||
for (int i = 0; i < attack.bounces && kind != ATTACK_BEAM && !attack.isAttached; i++)
|
||||
for (int i = 0; i < attack.bounces && !attack.def()->isInstant && count == 0 && !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);
|
||||
@@ -1001,8 +1005,8 @@ void Scene::suck_draw(Attack& attack, Color color)
|
||||
Uint32 seed = ((Uint32)std::floor(phase) * SUCK_MOTES + i) * MOTE_KEYS + attack.owner;
|
||||
float side = mote_share_get(seed) * 2 - 1;
|
||||
float fade = std::min({1.0f, progress / SUCK_FADE_IN, (1 - progress) / SUCK_FADE_OUT});
|
||||
Vec2 mote =
|
||||
attack.position + vec2_polar(attack.aimAngle + side * VACUUM_CONE, cone_reach_get(attack) * (1 - progress));
|
||||
Vec2 mote = attack.position +
|
||||
vec2_polar(attack.aimAngle + side * cone_get(attack), cone_reach_get(attack) * (1 - progress));
|
||||
Particle particle = {.color = color, .life = fade, .lifeMax = 1, .size = SUCK_MOTE_SIZE};
|
||||
particle_draw(game.renderer, particle, mote);
|
||||
}
|
||||
@@ -1023,8 +1027,8 @@ void Scene::gust_draw(Attack& attack, Color color)
|
||||
float delay = GUST_STAGGER * mote_share_get(seed + 2);
|
||||
float own = std::clamp((progress - delay) / (1 - delay), 0.0f, 1.0f);
|
||||
float sweep = 1 - (1 - own) * (1 - own);
|
||||
Vec2 mote =
|
||||
attack.position + vec2_polar(attack.aimAngle + side * VACUUM_CONE, cone_reach_get(attack) * reach * sweep);
|
||||
Vec2 mote = attack.position +
|
||||
vec2_polar(attack.aimAngle + side * cone_get(attack), cone_reach_get(attack) * reach * sweep);
|
||||
Particle particle = {.color = color, .life = 1 - own, .lifeMax = 1, .size = SUCK_MOTE_SIZE};
|
||||
particle_draw(game.renderer, particle, mote);
|
||||
}
|
||||
@@ -1055,10 +1059,16 @@ void Scene::collision_draw()
|
||||
capsules_fill(renderer, shapes, count, color);
|
||||
if (cone_get(attack) <= 0)
|
||||
continue;
|
||||
float range = cone_reach_get(attack);
|
||||
Rect bounds = {attack.position.x - range, attack.position.y - range, attack.position.x + range,
|
||||
attack.position.y + range};
|
||||
shape_fill(renderer, bounds, [&](Vec2 point) { return is_in_cone(attack, point); }, color);
|
||||
Attack cones[CONES_MAX];
|
||||
int coneCount = cones_get(game.sim, attack, cones);
|
||||
for (int c = 0; c < coneCount; c++)
|
||||
{
|
||||
Attack& cone = cones[c];
|
||||
float range = cone_reach_get(cone);
|
||||
Rect bounds = {cone.position.x - range, cone.position.y - range, cone.position.x + range,
|
||||
cone.position.y + range};
|
||||
shape_fill(renderer, bounds, [&](Vec2 point) { return is_in_cone(cone, point); }, color);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||
if (game.sim.fighters[i].isAlive)
|
||||
|
||||
@@ -128,6 +128,13 @@ void Session::update(Game& game, Controls local)
|
||||
}
|
||||
check = checksums.erase(check);
|
||||
}
|
||||
// Out of step, it asks for the room again (at most every RESYNC_WAIT), which puts it right (see state_load).
|
||||
if (isDesynced && SDL_GetTicks() >= nextResync)
|
||||
{
|
||||
nextResync = SDL_GetTicks() + RESYNC_WAIT;
|
||||
game.net.send(PacketWriter().put(PACKET_RESYNC).put(room).bytes, DELIVERY_RELIABLE);
|
||||
game.chat.system("Out of step with the server. Resyncing...");
|
||||
}
|
||||
|
||||
int advance = 1;
|
||||
if (tick - paced >= PACE_WAIT && (lead < LEAD_LOW || lead > LEAD_HIGH))
|
||||
|
||||
@@ -10,6 +10,8 @@ struct Game;
|
||||
constexpr int SESSION_RING = 64;
|
||||
// How often (in milliseconds) this screen tells the lobby its round trip.
|
||||
constexpr Uint64 PING_REPORT_INTERVAL = 2000;
|
||||
// The least time between asking for the room again while out of step with it.
|
||||
constexpr Uint64 RESYNC_WAIT = 2000;
|
||||
// A member's connection has no ping to show.
|
||||
constexpr float PING_NONE = -2;
|
||||
|
||||
@@ -118,6 +120,8 @@ private:
|
||||
Uint64 nextRename = 0;
|
||||
bool isRenameWaiting = false;
|
||||
Uint64 nextPingReport = 0;
|
||||
// When this screen may next ask for its room again, out of step (see PACKET_RESYNC).
|
||||
Uint64 nextResync = 0;
|
||||
|
||||
void start(Game& game);
|
||||
void packets_take(Game& game);
|
||||
|
||||
+13
-1
@@ -136,12 +136,20 @@ Uint32 Sim::checksum_get()
|
||||
{
|
||||
Uint32 hash = FNV_OFFSET;
|
||||
hash_add(hash, (float)match.state + match.timer);
|
||||
for (float value : {(float)match.round, (float)match.winner, match.clock})
|
||||
hash_add(hash, value);
|
||||
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 (int value : {(int)fighter.isAlive, (int)fighter.isLaunched, fighter.slot, fighter.launcher, match.wins[i]})
|
||||
hash_add(hash, (float)value);
|
||||
Queue& queue = match.queues[i];
|
||||
if (queue.isActive)
|
||||
for (float value : {queue.area.x, queue.area.y, queue.area.w, (float)queue.rules.players})
|
||||
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.
|
||||
@@ -158,7 +166,11 @@ Uint32 Sim::checksum_get()
|
||||
}
|
||||
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})
|
||||
attack.time, (float)attack.hitsLeft, (float)attack.owner, attack.bonus,
|
||||
(float)attack.ricochets, attack.lifetime, (float)attack.isReturning})
|
||||
hash_add(hash, value);
|
||||
for (Attack& attack : weapons.pending)
|
||||
for (float value : {attack.position.x, attack.position.y, attack.stats.damage})
|
||||
hash_add(hash, value);
|
||||
for (Dummy& dummy : world.dummies)
|
||||
for (float value : {dummy.flash, dummy.wobble, dummy.wobbleSpeed})
|
||||
|
||||
@@ -117,6 +117,7 @@ template <class Stream, class SimRef> static void state_walk(Stream& stream, Sim
|
||||
stream.raw(sim.bot.states, MAX_FIGHTERS);
|
||||
stream.one(sim.weapons.orbitClock);
|
||||
stream.list(sim.weapons.attacks);
|
||||
stream.list(sim.weapons.pending);
|
||||
World& world = sim.world;
|
||||
stream.list(world.dummies);
|
||||
stream.list(world.ledges);
|
||||
|
||||
+1
-1
@@ -209,7 +209,7 @@ inline constexpr TrinketDef TRINKET_DEFS[TRINKET_COUNT] = {
|
||||
{"The Good Stuff",
|
||||
"Increases spread on attacks.",
|
||||
{{.spread = 1.2f}, {.spread = 1.5f}, {.spread = 2}},
|
||||
"Steady hands are for\nsurgeons."},
|
||||
"Aye, me bottle o'scrumpy!"},
|
||||
{"Splitter",
|
||||
"Attacks split into smaller,\nless-damaging copies.",
|
||||
{{.copies = 2, .copyDamage = 1 / 3.0f},
|
||||
|
||||
+392
-116
@@ -35,6 +35,8 @@ constexpr float KNOCKBACK_REACH = 1.5f;
|
||||
constexpr float KNOCKBACK_SPEED = 550;
|
||||
constexpr float LANCE_LENGTH = 40;
|
||||
constexpr float MUZZLE_DISTANCE = 18;
|
||||
// The share of the way a loose attack heading home turns to face away from its fighter each tick.
|
||||
constexpr float HOME_TURN = 0.2f;
|
||||
constexpr float PROJECTILE_STEP = 8;
|
||||
constexpr float RAY_STEP = 2;
|
||||
constexpr float RECOIL_CURVE = 0.75f;
|
||||
@@ -70,7 +72,6 @@ constexpr float SPLIT_SCALE = 0.75f;
|
||||
constexpr float SPLIT_GAP = 0.06f;
|
||||
constexpr float SPLIT_GAP_MELEE = 0.5f;
|
||||
constexpr float SPLIT_GAP_THROWN = 0.3f;
|
||||
constexpr float UPPERCUT_REACH = 16;
|
||||
constexpr float VOLLEY_GAP = 6;
|
||||
constexpr float WHIP_CONE = 0.25f;
|
||||
constexpr float WHIP_HANDLE = 4;
|
||||
@@ -114,7 +115,8 @@ constexpr float VACUUM_SPIN_DOWN = 0.2f;
|
||||
constexpr float REST_SPEED = 40;
|
||||
// How fast a shot flies home, against the speed it left at.
|
||||
constexpr float RETURN_SHARE = 0.5f;
|
||||
// How hard a bouncing shot must hit a surface to bounce off it.
|
||||
// How hard a bouncing shot must hit a surface to bounce off it, and how hard one with Ricochet bounces left at least
|
||||
// comes off whatever it touches.
|
||||
constexpr float SHOT_BOUNCE_MIN = 60;
|
||||
// How much of its blast a grenade deals to whoever it clonks on the way.
|
||||
constexpr float GRENADE_CONTACT_SHARE = 0.2f;
|
||||
@@ -235,16 +237,17 @@ static void impact_play(Sim& sim, AttackKind kind)
|
||||
}
|
||||
}
|
||||
|
||||
// Spends one of an attack's ricochets on a bounce, adding to its later damage. Flames go quietly, as there are so
|
||||
// many.
|
||||
static void ricochet_count(Sim& sim, Attack& attack)
|
||||
// Spends one of an attack's ricochets on a bounce at point, adding to its later damage and giving it its hits back, so
|
||||
// it can strike anyone again straight away.
|
||||
static void ricochet_count(Sim& sim, Attack& attack, Vec2 point)
|
||||
{
|
||||
attack.bounces--;
|
||||
attack.ricochets++;
|
||||
if (attack.def()->kind == ATTACK_FLAME)
|
||||
return;
|
||||
attack.hitsLeft = std::max(attack.hitsLeft, attack.def()->hits);
|
||||
attack.struck = 0;
|
||||
attack.hitCooldown = 0;
|
||||
sim.cues->sound_play(SOUND_RICOCHET);
|
||||
sim.cues->burst_spawn(IMPACT_BURST, attack.position, 0, attack.owner);
|
||||
sim.cues->burst_spawn(IMPACT_BURST, point, 0, attack.owner);
|
||||
}
|
||||
|
||||
// Turns a projectile off the wall it's about to enter, starting its range, lifetime and damage falloff afresh from
|
||||
@@ -255,7 +258,7 @@ static void ricochet(Sim& sim, Attack& attack, Vec2 next)
|
||||
attack.angle = vec2_angle(attack.velocity);
|
||||
attack.origin = attack.position;
|
||||
attack.lifeStart = attack.time;
|
||||
ricochet_count(sim, attack);
|
||||
ricochet_count(sim, attack, attack.position);
|
||||
}
|
||||
|
||||
// What stops an attack moving through the world: walls and platform tops for most, walls alone for wisps.
|
||||
@@ -551,6 +554,89 @@ static Vec2 ray_march(Vec2 start, float angle, float range, auto isBlocked)
|
||||
return point;
|
||||
}
|
||||
|
||||
// Where a ray from start along angle sets off: start, or from inside a wall, back out where it went in (looking as far
|
||||
// back as the hand behind a muzzle), so it glances off the wall's face instead of bouncing about inside it.
|
||||
static Vec2 ray_start_get(Sim& sim, Vec2 start, float angle)
|
||||
{
|
||||
auto isOpen = [&](Vec2 point) {
|
||||
return !sim.world.is_blocking_at(point);
|
||||
};
|
||||
return ray_march(start, angle + PI, MUZZLE_DISTANCE, isOpen);
|
||||
}
|
||||
|
||||
// Turns a ray's segment off the wall its tip ran into: it sets off again from just short of the wall the way the wall
|
||||
// turns it, reaching range or the next wall.
|
||||
static void strand_turn(Sim& sim, Attack& segment, float range)
|
||||
{
|
||||
auto isBlocked = [&](Vec2 point) {
|
||||
return sim.world.is_blocking_at(point);
|
||||
};
|
||||
Vec2 before = segment.tip - vec2_polar(segment.angle, RAY_STEP);
|
||||
segment.angle = vec2_angle(reflect_get(sim, before, segment.tip, vec2_polar(segment.angle, 1)));
|
||||
segment.position = before;
|
||||
segment.tip = ray_march(before, segment.angle, range, isBlocked);
|
||||
}
|
||||
|
||||
// A cone's corners, returning how many: from its start (steps[0]) out to its tip (steps[1]), cut short where its aim
|
||||
// meets a wall for each bounce it has, the rest of its reach glancing off the wall. Each corner starts a cone, and the
|
||||
// last is its tip.
|
||||
static int cone_steps_get(Sim& sim, Vec2* steps, int bounces)
|
||||
{
|
||||
auto isBlocked = [&](Vec2 point) {
|
||||
return sim.world.is_blocking_at(point);
|
||||
};
|
||||
if (bounces > 0)
|
||||
steps[0] = ray_start_get(sim, steps[0], vec2_angle(steps[1] - steps[0]));
|
||||
int count = 2;
|
||||
for (; count - 2 < std::min(bounces, CONES_MAX - 1); count++)
|
||||
{
|
||||
Vec2 start = steps[count - 2];
|
||||
float angle = vec2_angle(steps[count - 1] - start);
|
||||
float reach = vec2_length(steps[count - 1] - start);
|
||||
Vec2 wall = ray_march(start, angle, reach, isBlocked);
|
||||
if (!isBlocked(wall) || vec2_length(wall - start) >= reach)
|
||||
break;
|
||||
Vec2 before = wall - vec2_polar(angle, RAY_STEP);
|
||||
float turned = vec2_angle(reflect_get(sim, before, wall, vec2_polar(angle, 1)));
|
||||
steps[count - 1] = before;
|
||||
steps[count] = before + vec2_polar(turned, reach - vec2_length(before - start));
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// The cones an attack's cone makes right now, returning how many: itself, and with Ricochet bounces, cut short where
|
||||
// its aim meets a wall to go on from there as another cone glancing off it, a bounce harder (see cone_steps_get). The
|
||||
// attack's own ricochets are how many walls it glanced off last tick (see cones_glance).
|
||||
int cones_get(Sim& sim, Attack& attack, Attack* cones)
|
||||
{
|
||||
Vec2 steps[CONES_MAX + 1] = {attack.position,
|
||||
attack.position + vec2_polar(attack.aimAngle, cone_reach_get(attack))};
|
||||
int count = cone_steps_get(sim, steps, attack.bounces) - 1;
|
||||
cones[0] = attack;
|
||||
cones[0].ricochets = 0;
|
||||
for (int i = 0; i < count && count > 1; i++)
|
||||
{
|
||||
cones[i] = attack;
|
||||
cones[i].position = steps[i];
|
||||
cones[i].aimAngle = vec2_angle(steps[i + 1] - steps[i]);
|
||||
cones[i].stats.range = vec2_length(steps[i + 1] - steps[i]);
|
||||
cones[i].length = 1;
|
||||
cones[i].ricochets = i;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// An attack's cones for this tick (see cones_get), ricocheting each time its cone glances off one more wall than it
|
||||
// did.
|
||||
static int cones_glance(Sim& sim, Attack& attack, Attack* cones)
|
||||
{
|
||||
int count = cones_get(sim, attack, cones);
|
||||
if (count - 1 > attack.ricochets)
|
||||
sim.cues->sound_play(SOUND_RICOCHET);
|
||||
attack.ricochets = count - 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
// The capsules an attack hits with right now, returning how many. A plain whip fans rays across its cone from the
|
||||
// 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)
|
||||
@@ -1071,6 +1157,7 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
|
||||
{
|
||||
const AttackDef& def = *attack.def();
|
||||
Fighter& fighter = sim.fighters[attack.owner];
|
||||
Vec2 lastTip = attack.tip;
|
||||
float progress = attack.progress_get();
|
||||
float range = attack.stats.range;
|
||||
attack.radius = def.shape == SHAPE_CIRCLE ? attack.stats.range : def.radius * attack.scale;
|
||||
@@ -1120,16 +1207,6 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
|
||||
}
|
||||
case ATTACK_PUNCH:
|
||||
break;
|
||||
case ATTACK_UPPERCUT:
|
||||
{
|
||||
// The fist rises beside its fighter on the aimed side, carried up with them. Splitter's smaller copies
|
||||
// keep the full reach so they fan out around the fighter instead of piling up on it.
|
||||
float reach = UPPERCUT_REACH * attack.scale;
|
||||
if (fighter.stat_get(STAT_COPIES) > 1)
|
||||
reach /= SPLIT_SCALE;
|
||||
attack.position = attack.origin + vec2_polar(attack.aimAngle, reach);
|
||||
break;
|
||||
}
|
||||
case ATTACK_POKE:
|
||||
{
|
||||
// Its path carries the tip, never closer in than the lance's own length, so the whole lance stays out in
|
||||
@@ -1168,12 +1245,15 @@ 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 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.
|
||||
// once (each Ricochet bounce a strand of its own, staying where it glanced). 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);
|
||||
attack.isDead = progress >= 1;
|
||||
if (attack.ricochets > 0)
|
||||
break;
|
||||
attack.origin = fighter.hand_get(attack.hand);
|
||||
attack.angle = vec2_angle(attack.tip - attack.origin);
|
||||
attack.position = attack.origin + vec2_polar(attack.angle, WHIP_HANDLE);
|
||||
break;
|
||||
@@ -1182,6 +1262,26 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
|
||||
return sim.world.is_blocking_at(point);
|
||||
};
|
||||
Vec2 reach = ray_march(attack.origin, attack.aimAngle, reach_at_get(attack), isBlocked);
|
||||
// As it cracks against a wall, the reach the wall cut off glances off it with each Ricochet bounce, a
|
||||
// strand like a charged crack's, hitting as it appears (see strands_spawn).
|
||||
bool isCracking = progress >= PATH_OUT && progress - TICK / attack.lifetime < PATH_OUT;
|
||||
if (isCracking && attack.bounces > 0 && isBlocked(reach))
|
||||
{
|
||||
Attack strand = attack;
|
||||
strand.variant = VARIANT_CHARGED;
|
||||
strand.stats.lifetime = 0;
|
||||
strand.stats.range = reach_at_get(attack) - vec2_length(reach - attack.origin);
|
||||
strand.duration = strand.def()->duration;
|
||||
strand.hitsLeft = def.hits;
|
||||
strand.hitCooldown = 0;
|
||||
strand.time = 0;
|
||||
strand.timePrevious = 0;
|
||||
strand.lifeStart = 0;
|
||||
strand.angle = attack.aimAngle;
|
||||
strand.tip = reach;
|
||||
strands_spawn(sim, strand, pending);
|
||||
attack.bounces = 0;
|
||||
}
|
||||
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.origin) - WHIP_HANDLE);
|
||||
@@ -1227,12 +1327,15 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
|
||||
if (!isFlying && def.kind != ATTACK_POKE && def.kind != ATTACK_BEAM && def.kind != ATTACK_WHIP)
|
||||
attack.tip = attack.position;
|
||||
attack.isHeld = false;
|
||||
if (def.anchor != ANCHOR_WORLD)
|
||||
anchor_bounce(sim, attack, lastTip);
|
||||
}
|
||||
|
||||
// Places an anchored attack along its path from its anchor (its fighter's hand or middle) for its age, ending it once
|
||||
// its lifetime is up: as far out along its heading as its speed has carried it (an eased one speeding up as it goes),
|
||||
// out and back, or as far round its arc. One moved by its own code (a Vacuum's cone) just stays at the muzzle along its
|
||||
// fighter's aim, keeping whatever turn it set off with (a Splitter copy's place in the fan), its own code ending it.
|
||||
// Places an anchored attack along its path from its anchor (its fighter's hand or middle, or once loose, the wall it
|
||||
// glanced off) for its age, ending it once its lifetime is up: as far out along its heading as its speed has carried it
|
||||
// (an eased one speeding up as it goes), out and back, or as far round its arc. One moved by its own code (a Vacuum's
|
||||
// cone) just stays at the muzzle along its fighter's aim, keeping whatever turn it set off with (a Splitter copy's
|
||||
// place in the fan), its own code ending it.
|
||||
void Weapons::path_move(Sim& sim, Attack& attack)
|
||||
{
|
||||
const AttackDef& def = *attack.def();
|
||||
@@ -1248,21 +1351,86 @@ void Weapons::path_move(Sim& sim, Attack& attack)
|
||||
return;
|
||||
}
|
||||
float age = attack.life_get();
|
||||
Vec2 anchor = def.anchor == ANCHOR_HAND ? fighter.hand_get(attack.hand) : fighter.center_get();
|
||||
// Done going out and back from the walls it glanced off, it heads home: the last wall it went from comes back to
|
||||
// its anchor as fast as it would have come back, and it turns to face away from its fighter as it goes.
|
||||
if (attack.glance == GLANCE_LOOSE && def.path == PATH_RETURN && age >= attack.lifetime)
|
||||
attack.glance = GLANCE_HOME;
|
||||
if (attack.glance == GLANCE_HOME)
|
||||
{
|
||||
Vec2 away = attack.origin - anchor;
|
||||
float left = vec2_length(away) - attack.stats.speed * PATH_OUT / (1 - PATH_OUT) * TICK;
|
||||
attack.angle = angle_lerp(attack.angle, vec2_angle(away), HOME_TURN);
|
||||
attack.aimAngle = attack.angle;
|
||||
attack.origin = anchor + vec2_normalize(away) * std::max(0.0f, left);
|
||||
attack.position = attack.origin;
|
||||
attack.isDead = left <= 0;
|
||||
return;
|
||||
}
|
||||
float out = PATH_OUT * attack.lifetime;
|
||||
float travel = def.isEased ? age * age / attack.lifetime : age;
|
||||
if (def.path == PATH_RETURN && age > out)
|
||||
travel = std::max(0.0f, out * (attack.lifetime - age) / (attack.lifetime - out));
|
||||
// Splitter's smaller copies keep the full offset, so they fan out around the anchor instead of piling up on it.
|
||||
float offset = def.offset * attack.scale / (fighter.stat_get(STAT_COPIES) > 1 ? SPLIT_SCALE : 1);
|
||||
float distance = attack.stats.speed * travel;
|
||||
if (def.path == PATH_ARC)
|
||||
{
|
||||
attack.angle = attack.aimAngle + (attack.isLeft ? -1 : 1) * (ARC_START + distance / attack.stats.range);
|
||||
distance = attack.stats.range;
|
||||
}
|
||||
attack.origin = def.anchor == ANCHOR_HAND ? fighter.hand_get(attack.hand) : fighter.center_get();
|
||||
attack.position = attack.origin + vec2_polar(attack.angle, distance);
|
||||
if (attack.glance == GLANCE_NONE)
|
||||
attack.origin = anchor;
|
||||
attack.position = attack.origin + vec2_polar(attack.aimAngle, offset) + vec2_polar(attack.angle, distance);
|
||||
attack.isDead = age >= attack.lifetime;
|
||||
}
|
||||
|
||||
// A bouncy anchored attack whose reach (from its anchor out to its tip) runs into a wall glances off it while it has
|
||||
// Ricochet bounces, hitting harder for each: an arc (a swing, a spin) sweeps back the other way from where its head was
|
||||
// last (last), and anything else (a punch, a poke, a lash) comes loose to go again from the wall the way it turns it,
|
||||
// the wall standing in for its anchor (see Glance). A riding one leaves its bounces to its fighter (see ride_bounce), a
|
||||
// cone glances as it goes (see cones_get), and a lash as it cracks (see attack_move).
|
||||
void Weapons::anchor_bounce(Sim& sim, Attack& attack, Vec2 last)
|
||||
{
|
||||
const AttackDef& def = *attack.def();
|
||||
// An arc needs where its head was last, so it can't glance until it's been somewhere.
|
||||
bool isFresh = attack.time == 0 && def.path == PATH_ARC;
|
||||
bool isSpent = attack.bounces <= 0;
|
||||
bool isOwnGlance = def.isRiding || is_cone(def) || def.kind == ATTACK_WHIP;
|
||||
if (!def.isBouncy || isOwnGlance || isSpent || isFresh)
|
||||
return;
|
||||
auto isBlocked = [&](Vec2 point) {
|
||||
return sim.world.is_blocking_at(point);
|
||||
};
|
||||
float angle = vec2_angle(attack.tip - attack.origin);
|
||||
Vec2 wall = ray_march(attack.origin, angle, vec2_length(attack.tip - attack.origin) + RAY_STEP, isBlocked);
|
||||
if (!isBlocked(wall) || isBlocked(attack.origin))
|
||||
return;
|
||||
Vec2 before = wall - vec2_polar(angle, RAY_STEP);
|
||||
float turned = vec2_angle(reflect_get(sim, before, wall, vec2_polar(angle, 1)));
|
||||
attack.isDead = false;
|
||||
if (def.path == PATH_ARC)
|
||||
{
|
||||
attack.isLeft = !attack.isLeft;
|
||||
attack.angle = vec2_angle(last - attack.origin);
|
||||
attack.aimAngle = attack.angle - (attack.isLeft ? -1 : 1) * ARC_START;
|
||||
attack.position = last;
|
||||
attack.tip = last;
|
||||
attack.lifeStart = attack.time;
|
||||
}
|
||||
else
|
||||
{
|
||||
attack.glance = GLANCE_LOOSE;
|
||||
attack.angle = turned;
|
||||
attack.aimAngle = turned;
|
||||
attack.origin = before;
|
||||
attack.position = before;
|
||||
attack.tip = before;
|
||||
attack.lifeStart = attack.time;
|
||||
}
|
||||
ricochet_count(sim, attack, before);
|
||||
}
|
||||
|
||||
// Which way is off the surface a bolt landed on at wall, coming from where it is: a slope's own normal, or else the
|
||||
// side of the wall it ran into.
|
||||
static Vec2 surface_normal_get(Sim& sim, Vec2 from, Vec2 wall)
|
||||
@@ -1304,6 +1472,23 @@ void Weapons::flight_move(Sim& sim, Attack& attack)
|
||||
sim.cues->sound_play(SOUND_WISP);
|
||||
}
|
||||
attack.isReturning |= def.path == PATH_RETURN && attack.time >= attack.restTime + def.hold;
|
||||
// Heading home with Ricochet bounces left, it glances off a wall in its way like any shot, flying out from it
|
||||
// afresh until it heads home again.
|
||||
if (attack.isReturning && attack.bounces > 0)
|
||||
{
|
||||
float pace = attack.stats.speed * (def.isTethered ? 1 : RETURN_SHARE);
|
||||
Vec2 home = vec2_normalize(fighter.hand_get(attack.hand) - attack.position) * pace;
|
||||
Vec2 wall;
|
||||
if (is_wall_reached(sim, attack, attack.position + home * TICK, wall))
|
||||
{
|
||||
attack.velocity = home;
|
||||
attack.isReturning = false;
|
||||
attack.restTime = LIFETIME_INFINITE;
|
||||
ricochet(sim, attack, wall);
|
||||
}
|
||||
else
|
||||
attack.position = start;
|
||||
}
|
||||
if (attack.isReturning && def.isTethered)
|
||||
attack.isDead = is_caught(fighter, attack);
|
||||
else if (attack.isReturning)
|
||||
@@ -1483,29 +1668,34 @@ void Weapons::body_bounce(Sim& sim, Attack& attack)
|
||||
body.velocity.x += def.gravity * body.slope / (1 + body.slope * body.slope) * TICK;
|
||||
Vec2 before = body.velocity;
|
||||
sim.world.body_move(body);
|
||||
// With Ricochet bounces left, any touch bounces it, coming off at a hop's speed at least, so even one at rest keeps
|
||||
// ricocheting until they're spent.
|
||||
bool isRicochet = attack.bounces > 0;
|
||||
float least = isRicochet ? 0 : SHOT_BOUNCE_MIN;
|
||||
float hop = isRicochet ? SHOT_BOUNCE_MIN : 0;
|
||||
bool isBounced = false;
|
||||
bool isRamp = body.normal.x != 0 || body.normal.y != 0;
|
||||
float into = vec2_dot(before, body.normal);
|
||||
if (isRamp && into < -SHOT_BOUNCE_MIN)
|
||||
if (isRamp && into < -least)
|
||||
{
|
||||
body.velocity = before - body.normal * (into * (1 + def.bounce));
|
||||
body.velocity = before - body.normal * into + body.normal * std::max(-into * def.bounce, hop);
|
||||
body.isGrounded = false;
|
||||
isBounced = true;
|
||||
}
|
||||
if (!isRamp && body.velocity.x == 0 && std::abs(before.x) > SHOT_BOUNCE_MIN)
|
||||
if (!isRamp && body.velocity.x == 0 && std::abs(before.x) > least)
|
||||
{
|
||||
body.velocity.x = -before.x * def.bounce;
|
||||
body.velocity.x = -std::copysign(std::max(std::abs(before.x) * def.bounce, hop), before.x);
|
||||
isBounced = true;
|
||||
}
|
||||
if (!isRamp && body.velocity.y == 0 && std::abs(before.y) > SHOT_BOUNCE_MIN)
|
||||
if (!isRamp && body.velocity.y == 0 && std::abs(before.y) > least)
|
||||
{
|
||||
body.velocity.y = -before.y * def.bounce;
|
||||
body.velocity.y = -std::copysign(std::max(std::abs(before.y) * def.bounce, hop), before.y);
|
||||
body.isGrounded = false;
|
||||
isBounced = true;
|
||||
}
|
||||
if (isBounced && attack.bounces > 0)
|
||||
{
|
||||
ricochet_count(sim, attack);
|
||||
ricochet_count(sim, attack, attack.position);
|
||||
attack.lifeStart = attack.time;
|
||||
}
|
||||
else if (isBounced)
|
||||
@@ -1756,6 +1946,24 @@ void Weapons::orbits_release(Fighter& fighter)
|
||||
}
|
||||
}
|
||||
|
||||
// A riding attack takes its Ricochet bounces with its fighter: when the fighter runs into something (before is the
|
||||
// speed it had), the first of its riding attacks with a bounce left glances it off, keeping its speed, and takes the
|
||||
// bounce, its damage raised and its time renewed. Returns whether one did.
|
||||
bool Weapons::ride_bounce(Sim& sim, Fighter& fighter, Vec2 before)
|
||||
{
|
||||
for (Attack& attack : attacks)
|
||||
{
|
||||
if (attack.owner != fighter.index || !attack.def()->isRiding || attack.bounces <= 0 || attack.isDead)
|
||||
continue;
|
||||
if (fighter.surface_bounce(before, 1) <= 0)
|
||||
return false;
|
||||
attack.lifeStart = attack.time;
|
||||
ricochet_count(sim, attack, attack.position);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Grimoire teleport: sparkles round the fighter while it gathers, then moves them to the spot marked when it was cast
|
||||
// (backed off again if their box no longer fits), keeping all their momentum, with a blast there that hurts without
|
||||
// knocking anyone about. A Doubler's echoes teleport again, each to where the cursor pointed as it was cast. An
|
||||
@@ -1868,10 +2076,19 @@ void Weapons::tesla_update(Sim& sim)
|
||||
}
|
||||
|
||||
// The paths the held weapon's attack would take if fired now (one per Splitter copy), out to the first wall it can't
|
||||
// ricochet off (Ricochet bounces it off as many as the shot would) or the attack's reach: curving under gravity for
|
||||
// arrows, straight for other shots, throws, beams, lashes, mines and hooks, and a teleport's line to where it lands.
|
||||
// Anything that explodes rings the blast it would make where the path ends (its reach counts from the hand). Melee,
|
||||
// flames and flails have none, as their paths aren't lines.
|
||||
// ricochet off (Ricochet bounces it off as many as the shot would) or the attack's reach: curving under gravity (and
|
||||
// slowing with drag) for anything that falls (arrows, bolts, grenades, a thrown flail ball), straight for other shots,
|
||||
// throws, beams, lashes, mines, hooks and a flying slash, and a teleport's line to where it lands. Anything that
|
||||
// explodes where it stops rings the blast it would make there (its reach counts from the hand). A bouncing shot (a
|
||||
// grenade) ends its path where it first lands. Melee, flames and wisps have none, as their paths aren't lines.
|
||||
// Adds a straight stretch of a path, from one point up to (not including) another, a step at a time.
|
||||
static void path_line_add(Path& path, Vec2 from, Vec2 to)
|
||||
{
|
||||
float length = vec2_length(to - from);
|
||||
for (float traveled = 0; traveled < length; traveled += TRAJECTORY_STEP)
|
||||
path.steps.push_back(from + (to - from) * (traveled / length));
|
||||
}
|
||||
|
||||
std::vector<Path> Weapons::paths_get(Sim& sim, Fighter& fighter)
|
||||
{
|
||||
std::vector<Path> paths;
|
||||
@@ -1891,9 +2108,7 @@ std::vector<Path> Weapons::paths_get(Sim& sim, Fighter& fighter)
|
||||
Vec2 spot = teleport_spot(sim, fighter, fighter.controls.cursor, stats.range);
|
||||
Vec2 from = fighter.center_get();
|
||||
Path path = {.area = stats.area * def.scale * fighter.stat_get(STAT_ATTACK_SIZE)};
|
||||
float length = vec2_length(spot - from);
|
||||
for (float traveled = 0; traveled < length; traveled += TRAJECTORY_STEP)
|
||||
path.steps.push_back(from + (spot - from) * (traveled / length));
|
||||
path_line_add(path, from, spot);
|
||||
path.steps.push_back(spot);
|
||||
paths.push_back(path);
|
||||
return paths;
|
||||
@@ -1907,36 +2122,62 @@ std::vector<Path> Weapons::paths_get(Sim& sim, Fighter& fighter)
|
||||
for (int split = 0; split < splits; split++)
|
||||
{
|
||||
float offset = splits > 1 ? split_offset(kind, split, splits, fighter.stat_get(STAT_SPREAD)) : 0;
|
||||
Vec2 tip = muzzle + vec2_polar(vec2_angle(fighter.aim) + offset, reach);
|
||||
paths.push_back({.steps = {muzzle, tip}, .cone = VACUUM_CONE * fighter.stat_get(STAT_SPREAD)});
|
||||
Vec2 steps[CONES_MAX + 1] = {muzzle, muzzle + vec2_polar(vec2_angle(fighter.aim) + offset, reach)};
|
||||
int count = cone_steps_get(sim, steps, def.isBouncy ? fighter.stat_get(STAT_RICOCHETS) : 0);
|
||||
for (int i = 0; i + 1 < count; i++)
|
||||
paths.push_back(
|
||||
{.steps = {steps[i], steps[i + 1]}, .cone = VACUUM_CONE * fighter.stat_get(STAT_SPREAD)});
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
bool isArrow = kind == ATTACK_ARROW;
|
||||
auto isBlocked = [&](Vec2 point) {
|
||||
return sim.world.is_blocking_at(point);
|
||||
};
|
||||
bool isFalling = def.gravity > 0;
|
||||
bool isStraight = kind == ATTACK_PROJECTILE || kind == ATTACK_BULLET || kind == ATTACK_ROCKET ||
|
||||
kind == ATTACK_FIREBALL || kind == ATTACK_BEAM || kind == ATTACK_WHIP || kind == ATTACK_MINE ||
|
||||
kind == ATTACK_BOOMERANG || (kind == ATTACK_HOOK && variant != VARIANT_CHARGED);
|
||||
if (!isArrow && !isStraight)
|
||||
kind == ATTACK_BOOMERANG || kind == ATTACK_HOOK ||
|
||||
(kind == ATTACK_SLASH && variant == VARIANT_CHARGED);
|
||||
if (!isFalling && !isStraight)
|
||||
return paths;
|
||||
|
||||
// Each Splitter copy gets its own path, fanned like the copies. Straight paths step along the aim one unit at a
|
||||
// time. Arrows fly at their real speed so gravity bends them.
|
||||
// time. Falling ones fly at their real speed so gravity bends them.
|
||||
bool isBouncing = def.wall == WALL_BOUNCE;
|
||||
float area = stats.area * def.scale * fighter.stat_get(STAT_ATTACK_SIZE) * (splits > 1 ? SPLIT_SCALE : 1);
|
||||
float reach = std::min(stats.range, TRAJECTORY_REACH);
|
||||
float range = reach_get(def.path, stats);
|
||||
float reach = std::min(range, TRAJECTORY_REACH);
|
||||
for (int split = 0; split < splits; split++)
|
||||
{
|
||||
float offset = splits > 1 ? split_offset(kind, split, splits, fighter.stat_get(STAT_SPREAD)) : 0;
|
||||
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 velocity = isFalling ? aim * speed : aim;
|
||||
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;
|
||||
int bounces = def.isBouncy && !isBouncing ? (int)fighter.stat_get(STAT_RICOCHETS) : 0;
|
||||
Path path;
|
||||
bool isEnded = stats.range <= TRAJECTORY_REACH;
|
||||
// An instant one reaches its whole range at once from where it sets off (backed out of any wall), and its whole
|
||||
// range again off each wall it glances off, as instant_launch has it.
|
||||
if (def.isInstant)
|
||||
{
|
||||
Vec2 start = ray_start_get(sim, kind == ATTACK_WHIP ? probe.origin : point, probe.angle);
|
||||
probe.tip = ray_march(start, probe.angle, range, isBlocked);
|
||||
path_line_add(path, start, probe.tip);
|
||||
for (; bounces > 0 && isBlocked(probe.tip); bounces--)
|
||||
{
|
||||
strand_turn(sim, probe, range);
|
||||
path_line_add(path, probe.position, probe.tip);
|
||||
}
|
||||
path.steps.push_back(probe.tip);
|
||||
path.area = area;
|
||||
paths.push_back(path);
|
||||
continue;
|
||||
}
|
||||
bool isEnded = range <= TRAJECTORY_REACH;
|
||||
for (float traveled = area > 0 ? MUZZLE_DISTANCE : 0; traveled < reach; traveled += TRAJECTORY_STEP)
|
||||
{
|
||||
float time = TRAJECTORY_STEP / std::max(1.0f, vec2_length(velocity));
|
||||
@@ -1950,11 +2191,12 @@ std::vector<Path> Weapons::paths_get(Sim& sim, Fighter& fighter)
|
||||
continue;
|
||||
}
|
||||
velocity.y += def.gravity * time;
|
||||
velocity = isFalling ? velocity * math_pow(def.drag, time / TICK) : velocity;
|
||||
path.steps.push_back(point);
|
||||
point = next;
|
||||
}
|
||||
path.steps.push_back(point);
|
||||
path.area = isEnded ? area : 0;
|
||||
path.area = isEnded && !isBouncing ? area : 0;
|
||||
paths.push_back(path);
|
||||
}
|
||||
return paths;
|
||||
@@ -1977,7 +2219,7 @@ void Weapons::hook_attach(Sim& sim, Attack& attack)
|
||||
|
||||
// An instant attack reaches its full range at once along its aim, stopping at the first wall (or, for a latching one,
|
||||
// the first target), where a latching one latches. Each Ricochet bounce marches its full range again as a segment of
|
||||
// its own, its falloff starting afresh. A bursting one blows up at its tip, without a push.
|
||||
// its own, its falloff starting afresh. A bursting one blows up at its tip, without a push, harder for each ricochet.
|
||||
void Weapons::instant_launch(Sim& sim, Attack& piece)
|
||||
{
|
||||
const AttackDef& def = *piece.def();
|
||||
@@ -1988,33 +2230,32 @@ void Weapons::instant_launch(Sim& sim, Attack& piece)
|
||||
return true;
|
||||
return sim.world.is_blocking_at(point);
|
||||
};
|
||||
Vec2 start = def.kind == ATTACK_WHIP ? piece.origin : piece.position;
|
||||
Vec2 start = ray_start_get(sim, def.kind == ATTACK_WHIP ? piece.origin : piece.position, piece.angle);
|
||||
piece.velocity = {};
|
||||
piece.tip = ray_march(start, piece.angle, piece.stats.range, isBlocked);
|
||||
// A latching one glances off walls for its Ricochet bounces before it latches where it ends.
|
||||
if (isLatching)
|
||||
{
|
||||
while (piece.bounces > 0 && sim.world.is_blocking_at(piece.tip))
|
||||
{
|
||||
piece.position = piece.tip - vec2_polar(piece.angle, RAY_STEP);
|
||||
piece.angle = vec2_angle(reflect_get(sim, piece.position, piece.tip, vec2_polar(piece.angle, 1)));
|
||||
piece.tip = ray_march(piece.position, piece.angle, piece.stats.range, isBlocked);
|
||||
ricochet_count(sim, piece, piece.position);
|
||||
}
|
||||
piece.position = piece.tip;
|
||||
if (sim.world.is_blocking_at(piece.position))
|
||||
hook_attach(sim, piece);
|
||||
return;
|
||||
}
|
||||
sim.cues->burst_spawn(SPARK_BURST, piece.tip, piece.angle + PI, piece.owner);
|
||||
Attack segment = piece;
|
||||
for (int i = 0; i < piece.bounces; i++)
|
||||
{
|
||||
Vec2 before = segment.tip - vec2_polar(segment.angle, RAY_STEP);
|
||||
segment.angle = vec2_angle(reflect_get(sim, before, segment.tip, vec2_polar(segment.angle, 1)));
|
||||
segment.position = before;
|
||||
segment.tip = ray_march(segment.position, segment.angle, piece.stats.range, isBlocked);
|
||||
segment.ricochets++;
|
||||
attacks.push_back(segment);
|
||||
sim.cues->burst_spawn(SPARK_BURST, segment.tip, segment.angle + PI, piece.owner);
|
||||
}
|
||||
Attack segment = strands_spawn(sim, piece, attacks);
|
||||
if (def.isBursting)
|
||||
blast_spawn(sim, {
|
||||
.position = piece.tip,
|
||||
.position = segment.tip,
|
||||
.area = piece.stats.area * piece.scale / piece.stats.scale,
|
||||
.damage = piece.stats.damage,
|
||||
.bonus = piece.bonus * (1 + segment.ricochets * RICOCHET_BONUS),
|
||||
.damage = piece.stats.damage * (1 + segment.ricochets * RICOCHET_BONUS),
|
||||
.owner = piece.owner,
|
||||
.sound = SOUND_NONE, // the attack's own sound already carries it
|
||||
.weapon = piece.weapon,
|
||||
@@ -2022,6 +2263,25 @@ void Weapons::instant_launch(Sim& sim, Attack& piece)
|
||||
});
|
||||
}
|
||||
|
||||
// Glances a ray (piece, its tip where it ran into a wall) off walls for each of its Ricochet bounces, each bounce a
|
||||
// strand of its own as long as its range, staying where it glanced, into the given attacks. Returns the last strand (or
|
||||
// the piece, with none).
|
||||
Attack Weapons::strands_spawn(Sim& sim, Attack& piece, std::vector<Attack>& into)
|
||||
{
|
||||
auto isBlocked = [&](Vec2 point) {
|
||||
return sim.world.is_blocking_at(point);
|
||||
};
|
||||
Attack segment = piece;
|
||||
while (segment.bounces > 0 && isBlocked(segment.tip))
|
||||
{
|
||||
strand_turn(sim, segment, piece.stats.range);
|
||||
ricochet_count(sim, segment, segment.position);
|
||||
into.push_back(segment);
|
||||
sim.cues->burst_spawn(SPARK_BURST, segment.tip, segment.angle + PI, piece.owner);
|
||||
}
|
||||
return segment;
|
||||
}
|
||||
|
||||
// Pulls a whip's wielder toward a wall its lash (reaching as far as it reaches when fired) meets short of full length.
|
||||
void Weapons::whip_pull(Sim& sim, Attack& piece)
|
||||
{
|
||||
@@ -2311,28 +2571,6 @@ void Weapons::vacuum_move(Sim& sim, Attack& attack)
|
||||
attack.isReturning = !isHeld;
|
||||
attack.length = std::clamp(attack.length + (isHeld ? TICK / VACUUM_SPIN_UP : -TICK / VACUUM_SPIN_DOWN), 0.0f, 1.0f);
|
||||
attack.isDead = !isHeld && attack.length <= 0;
|
||||
for (int i = 0; i < sim.fighterCount; i++)
|
||||
{
|
||||
Fighter& target = sim.fighters[i];
|
||||
Vec2 center = target.center_get();
|
||||
if (!is_target(sim, i, attack.owner) || !is_in_cone(attack, center))
|
||||
continue;
|
||||
// Boxing Glove pulls harder, as it knocks harder.
|
||||
float boxing = fighter.stat_get(STAT_KNOCKBACK);
|
||||
Vec2 toward = vec2_normalize(attack.position - center);
|
||||
float room = std::max(0.0f, attack.stats.speed * boxing - vec2_dot(target.body.velocity, toward));
|
||||
float pull = std::min(room, VACUUM_PULL_RATE * boxing * TICK) * target.stat_get(STAT_KNOCKBACK_TAKEN);
|
||||
target.body.velocity = target.body.velocity + toward * pull;
|
||||
}
|
||||
for (int i = 0; i < (int)sim.world.dummies.size(); i++)
|
||||
{
|
||||
Dummy& dummy = sim.world.dummies[i];
|
||||
Vec2 center = {dummy.box.x + dummy.box.w / 2, dummy.box.y + dummy.box.h / 2};
|
||||
if (dummy.tile >= 0 || !is_target(sim, MAX_FIGHTERS + i, attack.owner) || !is_in_cone(attack, center))
|
||||
continue;
|
||||
Vec2 toward = vec2_normalize(attack.position - center);
|
||||
dummy.wobbleSpeed += toward.x * VACUUM_PULL_RATE * fighter.stat_get(STAT_KNOCKBACK) * WOBBLE_PER_PULL * TICK;
|
||||
}
|
||||
// Each interval (the hit cooldown counting it down), everyone in the cone can be nibbled again.
|
||||
float interval = fighter.level_get(attack.key).cooldown / fighter.stat_get(STAT_ATTACK_RATE);
|
||||
bool isPulse = attack.hitCooldown <= 0;
|
||||
@@ -2341,19 +2579,49 @@ void Weapons::vacuum_move(Sim& sim, Attack& attack)
|
||||
attack.struck = 0;
|
||||
attack.hitCooldown = interval;
|
||||
}
|
||||
int targets = std::min(MAX_FIGHTERS + (int)sim.world.dummies.size(), STRUCK_MAX);
|
||||
for (int target = 0; target < targets; target++)
|
||||
{
|
||||
Rect box = target_box(sim, target);
|
||||
Vec2 center = {box.x + box.w / 2, box.y + box.h / 2};
|
||||
if ((attack.struck >> target & 1) || !is_target(sim, target, attack.owner) || !is_in_cone(attack, center))
|
||||
continue;
|
||||
attack.struck |= 1u << target;
|
||||
attack_hit(sim, attack, target, center);
|
||||
}
|
||||
// Only the first of a fan of copies (or repeats) lengthens lives, so they don't add up.
|
||||
bool isLengthening = isPulse && !attack.isCopy;
|
||||
shots_drift(sim, attack, 0, VACUUM_PULL_RATE * TICK, isLengthening ? VACUUM_RESTORE : 0);
|
||||
// Boxing Glove pulls harder, as it knocks harder.
|
||||
float boxing = fighter.stat_get(STAT_KNOCKBACK);
|
||||
int targets = std::min(MAX_FIGHTERS + (int)sim.world.dummies.size(), STRUCK_MAX);
|
||||
// Bounced off walls by Ricochet, each cone pulls toward its own start, so whatever it catches goes around the bend.
|
||||
Attack cones[CONES_MAX];
|
||||
int count = cones_glance(sim, attack, cones);
|
||||
for (int c = 0; c < count; c++)
|
||||
{
|
||||
Attack& cone = cones[c];
|
||||
for (int i = 0; i < sim.fighterCount; i++)
|
||||
{
|
||||
Fighter& target = sim.fighters[i];
|
||||
Vec2 center = target.center_get();
|
||||
if (!is_target(sim, i, attack.owner) || !is_in_cone(cone, center))
|
||||
continue;
|
||||
Vec2 toward = vec2_normalize(cone.position - center);
|
||||
float room = std::max(0.0f, attack.stats.speed * boxing - vec2_dot(target.body.velocity, toward));
|
||||
float pull = std::min(room, VACUUM_PULL_RATE * boxing * TICK) * target.stat_get(STAT_KNOCKBACK_TAKEN);
|
||||
target.body.velocity = target.body.velocity + toward * pull;
|
||||
}
|
||||
for (int i = 0; i < (int)sim.world.dummies.size(); i++)
|
||||
{
|
||||
Dummy& dummy = sim.world.dummies[i];
|
||||
Vec2 center = {dummy.box.x + dummy.box.w / 2, dummy.box.y + dummy.box.h / 2};
|
||||
if (dummy.tile >= 0 || !is_target(sim, MAX_FIGHTERS + i, attack.owner) || !is_in_cone(cone, center))
|
||||
continue;
|
||||
Vec2 toward = vec2_normalize(cone.position - center);
|
||||
dummy.wobbleSpeed += toward.x * VACUUM_PULL_RATE * boxing * WOBBLE_PER_PULL * TICK;
|
||||
}
|
||||
for (int target = 0; target < targets; target++)
|
||||
{
|
||||
Rect box = target_box(sim, target);
|
||||
Vec2 center = {box.x + box.w / 2, box.y + box.h / 2};
|
||||
if ((attack.struck >> target & 1) || !is_target(sim, target, attack.owner) || !is_in_cone(cone, center))
|
||||
continue;
|
||||
attack.struck |= 1u << target;
|
||||
attack_hit(sim, cone, target, center);
|
||||
attack.hitCooldown = cone.hitCooldown;
|
||||
}
|
||||
shots_drift(sim, cone, 0, VACUUM_PULL_RATE * TICK, isLengthening ? VACUUM_RESTORE : 0);
|
||||
}
|
||||
}
|
||||
|
||||
// The Vacuum's airblast: once, as it goes off, its cone hits everyone in it for a little damage and knocks every
|
||||
@@ -2365,26 +2633,34 @@ void Weapons::gust_move(Sim& sim, Attack& attack)
|
||||
attack.isDead = attack.life_get() >= attack.lifetime;
|
||||
if (attack.time > 0)
|
||||
return;
|
||||
// Bounced off walls by Ricochet, each cone blows out from its own start, hitting anyone once.
|
||||
int targets = std::min(MAX_FIGHTERS + (int)sim.world.dummies.size(), STRUCK_MAX);
|
||||
for (int target = 0; target < targets; target++)
|
||||
Attack cones[CONES_MAX];
|
||||
int count = cones_glance(sim, attack, cones);
|
||||
for (int c = 0; c < count; c++)
|
||||
{
|
||||
Rect box = target_box(sim, target);
|
||||
Vec2 center = {box.x + box.w / 2, box.y + box.h / 2};
|
||||
if (!is_target(sim, target, attack.owner) || !is_in_cone(attack, center))
|
||||
continue;
|
||||
attack_hit(sim, attack, target, center);
|
||||
// Boxing Glove adds to it as to any knock.
|
||||
Vec2 push = vec2_normalize(center - attack.position) * attack.stats.speed * cone_strength_get(attack, center) *
|
||||
sim.fighters[attack.owner].stat_get(STAT_KNOCKBACK);
|
||||
if (target >= MAX_FIGHTERS)
|
||||
Attack& cone = cones[c];
|
||||
for (int target = 0; target < targets; target++)
|
||||
{
|
||||
dummy_push(sim.world.dummies[target - MAX_FIGHTERS], push);
|
||||
continue;
|
||||
Rect box = target_box(sim, target);
|
||||
Vec2 center = {box.x + box.w / 2, box.y + box.h / 2};
|
||||
if ((attack.struck >> target & 1) || !is_target(sim, target, attack.owner) || !is_in_cone(cone, center))
|
||||
continue;
|
||||
attack.struck |= 1u << target;
|
||||
attack_hit(sim, cone, target, center);
|
||||
// Boxing Glove adds to it as to any knock.
|
||||
Vec2 push = vec2_normalize(center - cone.position) * attack.stats.speed * cone_strength_get(cone, center) *
|
||||
sim.fighters[attack.owner].stat_get(STAT_KNOCKBACK);
|
||||
if (target >= MAX_FIGHTERS)
|
||||
{
|
||||
dummy_push(sim.world.dummies[target - MAX_FIGHTERS], push);
|
||||
continue;
|
||||
}
|
||||
Fighter& fighter = sim.fighters[target];
|
||||
fighter.knock_take(push * fighter.stat_get(STAT_KNOCKBACK_TAKEN), attack.owner);
|
||||
}
|
||||
Fighter& fighter = sim.fighters[target];
|
||||
fighter.knock_take(push * fighter.stat_get(STAT_KNOCKBACK_TAKEN), attack.owner);
|
||||
shots_drift(sim, cone, attack.stats.speed * GUST_SHOT_SHARE, 0, attack.isCopy ? 0 : GUST_RESTORE);
|
||||
}
|
||||
shots_drift(sim, attack, attack.stats.speed * GUST_SHOT_SHARE, 0, attack.isCopy ? 0 : GUST_RESTORE);
|
||||
}
|
||||
|
||||
// A weapon (by its key, see Fighter::weapon_key) is busy once as many of it are out of the hand (wielded attacks) as
|
||||
|
||||
+43
-3
@@ -10,6 +10,8 @@ constexpr float RICOCHET_BONUS = 0.25f;
|
||||
// Hits for an attack that keeps hitting (a hit interval apart) as long as it's out.
|
||||
constexpr int HITS_ENDLESS = 1 << 30;
|
||||
constexpr int WHIP_RAYS = 9;
|
||||
// The most cones a Vacuum's cone can glance into off walls, itself included.
|
||||
constexpr int CONES_MAX = 8;
|
||||
// How long a thrown flail ball stays out before it reels itself back in.
|
||||
constexpr float FLAIL_LIFETIME = 15;
|
||||
|
||||
@@ -71,6 +73,16 @@ enum AttackKind
|
||||
ATTACK_WISP
|
||||
};
|
||||
|
||||
// Where an anchored attack is with Ricochet (see Weapons::anchor_bounce): following its anchor, loose (going again from
|
||||
// the wall it glanced off as if the wall were its anchor), or heading home to its anchor from the last wall once done
|
||||
// going out and back.
|
||||
enum Glance
|
||||
{
|
||||
GLANCE_NONE,
|
||||
GLANCE_LOOSE,
|
||||
GLANCE_HOME
|
||||
};
|
||||
|
||||
// What an attack moves with: nothing (flying free in the world), its fighter's hand, or its fighter's middle.
|
||||
enum AttackAnchor
|
||||
{
|
||||
@@ -131,6 +143,7 @@ constexpr float KNOCK_SLIGHT = 0.1f;
|
||||
// degrees either side, and scatter how far each piece of a volley may stray on top of that. Knock is the share of a
|
||||
// hit's knockback it delivers, and radius its hitbox's (grown with its scale).
|
||||
// - Anchor and path say how it moves (see AttackAnchor and AttackPath), and shape what it hits with (see AttackShape).
|
||||
// An anchored one holds offset out along its aim from its anchor, past where its speed carries it.
|
||||
// - A flying shot's physics: accelerate (u/s², up to speedup times its speed), drag (the share of its speed it keeps
|
||||
// each tick), gravity (u/s², rising when negative), homing (how fast it turns toward its fighter's cursor, in
|
||||
// radians a second) and wall (see WallHit). One that bounces moves as a box body across, keeping bounce of its speed
|
||||
@@ -150,6 +163,8 @@ constexpr float KNOCK_SLIGHT = 0.1f;
|
||||
// isOrbiting: it circles its fighter in a shared orbit until launched (see Weapons::orbits_update).
|
||||
// isPassingPlatforms: it flies through platforms.
|
||||
// isProjectile: the Accelerator speeds it up.
|
||||
// isRiding: it moves with its fighter, so its Ricochet bounces glance the fighter off what they run into instead (see
|
||||
// Weapons::ride_bounce).
|
||||
// isRigid: its reach grows with its size as one rigid piece (a hammer, a Vacuum's cone).
|
||||
// isShot: it ends once out of hits.
|
||||
// isTethered: it's kept within its range of its fighter's hand (on a chain or rope).
|
||||
@@ -174,6 +189,7 @@ struct AttackDef
|
||||
float hold = 0;
|
||||
float homing = 0;
|
||||
float knock = 1;
|
||||
float offset = 0;
|
||||
float radius = 0;
|
||||
float scale = 1;
|
||||
float shake = 0;
|
||||
@@ -195,6 +211,7 @@ struct AttackDef
|
||||
bool isOrbiting = false;
|
||||
bool isPassingPlatforms = false;
|
||||
bool isProjectile = false;
|
||||
bool isRiding = false;
|
||||
bool isRigid = false;
|
||||
bool isShot = false;
|
||||
bool isTethered = false;
|
||||
@@ -380,14 +397,18 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
||||
.radius = 6,
|
||||
.shake = 0.025f,
|
||||
.spread = 18,
|
||||
.isBouncy = true,
|
||||
.isProjectile = true},
|
||||
.charged = {.label = "uppercut",
|
||||
.kind = ATTACK_UPPERCUT,
|
||||
.sound = SOUND_UPPERCUT,
|
||||
.anchor = ANCHOR_BODY,
|
||||
.offset = 16,
|
||||
.radius = 14,
|
||||
.shake = 0.25f,
|
||||
.spread = 5},
|
||||
.spread = 5,
|
||||
.isBouncy = true,
|
||||
.isRiding = true},
|
||||
.levels =
|
||||
{
|
||||
{.normal = {.damage = 16, .lifetime = 0.12f, .speed = 283},
|
||||
@@ -459,6 +480,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
||||
.radius = 8,
|
||||
.shake = 0.15f,
|
||||
.spread = 6,
|
||||
.isBouncy = true,
|
||||
.isEased = true,
|
||||
.isProjectile = true,
|
||||
.isRigid = true,
|
||||
@@ -471,6 +493,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
||||
.radius = 8,
|
||||
.shake = 0.35f,
|
||||
.spread = 6,
|
||||
.isBouncy = true,
|
||||
.isEased = true,
|
||||
.isProjectile = true,
|
||||
.isRigid = true,
|
||||
@@ -502,6 +525,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
||||
.radius = 3,
|
||||
.shake = 0.075f,
|
||||
.spread = 4,
|
||||
.isBouncy = true,
|
||||
.isProjectile = true,
|
||||
.isWielded = true},
|
||||
.charged = {.label = "lunging poke",
|
||||
@@ -512,6 +536,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
||||
.radius = 3,
|
||||
.shake = 0.2f,
|
||||
.spread = 3,
|
||||
.isBouncy = true,
|
||||
.isProjectile = true,
|
||||
.isWielded = true},
|
||||
.levels =
|
||||
@@ -544,6 +569,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
||||
.radius = 4,
|
||||
.shake = 0.05f,
|
||||
.spread = 7.5f,
|
||||
.isBouncy = true,
|
||||
.isProjectile = true},
|
||||
.charged = {.label = "lash + blast",
|
||||
.kind = ATTACK_WHIP,
|
||||
@@ -554,6 +580,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
||||
.shake = 0.15f,
|
||||
.spread = 0.5f,
|
||||
.hits = 0,
|
||||
.isBouncy = true,
|
||||
.isBursting = true,
|
||||
.isInstant = true},
|
||||
.levels =
|
||||
@@ -585,6 +612,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
||||
.shake = 0.075f,
|
||||
.spread = 8,
|
||||
.hits = HITS_ENDLESS,
|
||||
.isBouncy = true,
|
||||
.isProjectile = true,
|
||||
.isWielded = true},
|
||||
.charged = {.label = "ball throw",
|
||||
@@ -687,6 +715,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
||||
.shake = 0.1f,
|
||||
.spread = 2,
|
||||
.isAligned = true,
|
||||
.isBouncy = true,
|
||||
.isDrifting = true,
|
||||
.isFalloff = true,
|
||||
.isInheriting = true,
|
||||
@@ -702,6 +731,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
||||
.shake = 0.2f,
|
||||
.spread = 1,
|
||||
.isAligned = true,
|
||||
.isBouncy = true,
|
||||
.isDrifting = true,
|
||||
.isFalloff = true,
|
||||
.isInstant = true,
|
||||
@@ -969,6 +999,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
||||
.spread = 4,
|
||||
.hits = HITS_ENDLESS,
|
||||
.isAligned = true,
|
||||
.isBouncy = true,
|
||||
.isDrifting = true,
|
||||
.isFalloff = true,
|
||||
.isInheriting = true,
|
||||
@@ -985,6 +1016,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
||||
.spread = 2,
|
||||
.hits = HITS_ENDLESS,
|
||||
.isAligned = true,
|
||||
.isBouncy = true,
|
||||
.isDrifting = true,
|
||||
.isFalloff = true,
|
||||
.isInheriting = true,
|
||||
@@ -1222,6 +1254,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
||||
.shape = SHAPE_CONE,
|
||||
.knock = 0,
|
||||
.hits = 0,
|
||||
.isBouncy = true,
|
||||
.isRigid = true},
|
||||
.charged = {.label = "airblast",
|
||||
.kind = ATTACK_GUST,
|
||||
@@ -1232,6 +1265,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
||||
.knock = 0,
|
||||
.shake = 0.15f,
|
||||
.hits = 0,
|
||||
.isBouncy = true,
|
||||
.isClaiming = true,
|
||||
.isRigid = true},
|
||||
.levels =
|
||||
@@ -1393,6 +1427,7 @@ struct Attack
|
||||
int hitsLeft = 0;
|
||||
int owner = 0;
|
||||
int ownerPrevious = -1;
|
||||
// How many times it's ricocheted (for a cone, how many walls it glanced off last tick, see cones_get).
|
||||
int ricochets = 0;
|
||||
// Targets already hit, a bit each: by a grenade's clonks, or by a Vacuum's nibble this interval.
|
||||
Uint32 struck = 0;
|
||||
@@ -1404,6 +1439,7 @@ struct Attack
|
||||
// The hand it came from: the main hand (0) or a Dual Wield off-hand (1 up).
|
||||
int hand = 0;
|
||||
AttackVariant variant = VARIANT_NORMAL;
|
||||
Glance glance = GLANCE_NONE;
|
||||
bool isAttached = false;
|
||||
bool isCopy = false;
|
||||
bool isDead = false;
|
||||
@@ -1461,6 +1497,7 @@ float fill_get(float strength);
|
||||
std::vector<Vec2> lash_get(Attack& attack);
|
||||
Vec2 muzzle_get(Attack& attack);
|
||||
int shapes_get(Sim& sim, Attack& attack, Capsule* shapes);
|
||||
int cones_get(Sim& sim, Attack& attack, Attack* cones);
|
||||
|
||||
// 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
|
||||
@@ -1583,6 +1620,8 @@ class Weapons
|
||||
|
||||
public:
|
||||
std::vector<Attack> attacks;
|
||||
// Explosions queued to hit next tick (see blast_spawn).
|
||||
std::vector<Attack> pending;
|
||||
float orbitClock = 0;
|
||||
|
||||
void fire(Sim& sim, Fighter& fighter, float charge);
|
||||
@@ -1601,14 +1640,14 @@ public:
|
||||
void contact_update(Sim& sim, Fighter& fighter);
|
||||
void orbits_release(Fighter& fighter);
|
||||
void teleport_move(Sim& sim, Attack& attack);
|
||||
bool ride_bounce(Sim& sim, Fighter& fighter, Vec2 before);
|
||||
void solids_add(Fighter& fighter, World& world);
|
||||
|
||||
private:
|
||||
std::vector<Attack> pending;
|
||||
|
||||
bool is_spawned(Sim& sim, Fighter& fighter, Shot shot);
|
||||
void attack_move(Sim& sim, Attack& attack);
|
||||
void path_move(Sim& sim, Attack& attack);
|
||||
void anchor_bounce(Sim& sim, Attack& attack, Vec2 last);
|
||||
void flight_move(Sim& sim, Attack& attack);
|
||||
void wall_hit(Sim& sim, Attack& attack, Vec2 wall, bool isHeld);
|
||||
void attack_remember(Attack& attack);
|
||||
@@ -1618,6 +1657,7 @@ private:
|
||||
Attack* tether_get(Fighter& fighter, int key, int hand);
|
||||
void hook_attach(Sim& sim, Attack& attack);
|
||||
void instant_launch(Sim& sim, Attack& piece);
|
||||
Attack strands_spawn(Sim& sim, Attack& piece, std::vector<Attack>& into);
|
||||
void whip_pull(Sim& sim, Attack& piece);
|
||||
void attack_explode(Sim& sim, Attack& attack);
|
||||
void vacuum_move(Sim& sim, Attack& attack);
|
||||
|
||||
Reference in new issue
Block a user