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
+527
-132
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;
|
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)
|
if (type == PACKET_QUEUE)
|
||||||
{
|
{
|
||||||
Uint8 isOpen = 0;
|
Uint8 isOpen = 0;
|
||||||
|
|||||||
+40
-11
@@ -9,6 +9,9 @@ constexpr float KNOCK_BASE = 60;
|
|||||||
constexpr float KNOCK_LIFT = 0.35f;
|
constexpr float KNOCK_LIFT = 0.35f;
|
||||||
constexpr float KNOCK_MAX = 4500;
|
constexpr float KNOCK_MAX = 4500;
|
||||||
constexpr float KNOCK_PER_DAMAGE = 30;
|
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 LAUNCH_START_SPEED = 300;
|
||||||
|
|
||||||
constexpr float AFTERIMAGE_SPEED = 440;
|
constexpr float AFTERIMAGE_SPEED = 440;
|
||||||
@@ -241,9 +244,13 @@ void Fighter::update(Sim& sim)
|
|||||||
sim.world.body_move(body);
|
sim.world.body_move(body);
|
||||||
sim.world.obstacles.clear();
|
sim.world.obstacles.clear();
|
||||||
sim.world.ledges.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});
|
bounce_resolve(sim, {runSpeed, fallSpeed});
|
||||||
safe_update(sim);
|
safe_update(sim);
|
||||||
|
suffocation_update(sim);
|
||||||
|
|
||||||
wallMemory -= TICK;
|
wallMemory -= TICK;
|
||||||
bool isWallHit = body.velocity.x == 0 && std::abs(runSpeed) > WALL_IMPACT_SPEED;
|
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);
|
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
|
// Bounces off whatever stopped it this move (before is the speed it had), keeping keep of its speed into the surface (a
|
||||||
// grow with the impact. Hard impacts slam them for damage, credited to their launcher. The launch ends once they've
|
// slope's, or a wall's, ceiling's or floor's). Returns how hard it hit, or 0 when it didn't.
|
||||||
// slowed down.
|
float Fighter::surface_bounce(Vec2 before, float keep)
|
||||||
void Fighter::bounce_resolve(Sim& sim, Vec2 before)
|
|
||||||
{
|
{
|
||||||
float impact = 0;
|
float impact = 0;
|
||||||
Vec2 normal = body.normal;
|
Vec2 normal = body.normal;
|
||||||
float into = vec2_dot(before, normal);
|
float into = vec2_dot(before, normal);
|
||||||
if (into < -BOUNCE_MIN)
|
if (into < -BOUNCE_MIN)
|
||||||
{
|
{
|
||||||
body.velocity = before - normal * (into * (1 + BOUNCE_KEEP));
|
body.velocity = before - normal * (into * (1 + keep));
|
||||||
impact = -into;
|
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;
|
body.velocity.x = -before.x * keep;
|
||||||
impact = std::abs(before.x);
|
impact = std::abs(before.x);
|
||||||
}
|
}
|
||||||
if (body.velocity.y == 0 && std::abs(before.y) > BOUNCE_MIN)
|
if (body.velocity.y == 0 && std::abs(before.y) > BOUNCE_MIN)
|
||||||
{
|
{
|
||||||
body.velocity.y = -before.y * BOUNCE_KEEP;
|
body.velocity.y = -before.y * keep;
|
||||||
impact = std::max(impact, std::abs(before.y));
|
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)
|
if (impact > 0)
|
||||||
{
|
{
|
||||||
sim.cues->sound_play(SOUND_BOUNCE);
|
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;
|
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
|
// 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.
|
// in a puff, when it touches a pit.
|
||||||
void Fighter::safe_update(Sim& sim)
|
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.
|
// When each weapon slot was last switched away from, counting switches (0 for never), for Dual Wield.
|
||||||
int slotSwitches[SLOT_MAX] = {};
|
int slotSwitches[SLOT_MAX] = {};
|
||||||
int switches = 0;
|
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 trinketItemLevels[TRINKET_COUNT] = {};
|
||||||
int trinketSlotLevels[SLOT_MAX] = {};
|
int trinketSlotLevels[SLOT_MAX] = {};
|
||||||
int trinketLoadout[SLOT_MAX];
|
int trinketLoadout[SLOT_MAX];
|
||||||
@@ -160,6 +162,8 @@ public:
|
|||||||
void position_set(Sim& sim, Vec2 feet);
|
void position_set(Sim& sim, Vec2 feet);
|
||||||
void slots_set(int weapons, int trinkets);
|
void slots_set(int weapons, int trinkets);
|
||||||
void damage_take(Sim& sim, Damage damage);
|
void damage_take(Sim& sim, Damage damage);
|
||||||
|
void suffocation_update(Sim& sim);
|
||||||
|
float surface_bounce(Vec2 before, float keep);
|
||||||
void die(Sim& sim);
|
void die(Sim& sim);
|
||||||
void update(Sim& sim);
|
void update(Sim& sim);
|
||||||
void blend(float alpha);
|
void blend(float alpha);
|
||||||
|
|||||||
+5
-2
@@ -27,7 +27,7 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
|
||||||
constexpr int PROTOCOL_VERSION = 11;
|
constexpr int PROTOCOL_VERSION = 12;
|
||||||
constexpr int LOBBY_MEMBERS_MAX = 16;
|
constexpr int LOBBY_MEMBERS_MAX = 16;
|
||||||
constexpr int LOBBY_CODE_LENGTH = 4;
|
constexpr int LOBBY_CODE_LENGTH = 4;
|
||||||
// The longest a lobby's name and password can be.
|
// 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_PING Uint16 milliseconds this player's round trip to the server
|
||||||
// PACKET_WATCH room spectate one of the lobby's games (from its sandbox),
|
// PACKET_WATCH room spectate one of the lobby's games (from its sandbox),
|
||||||
// or 0 to go back to the 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:
|
// Server to game:
|
||||||
// PACKET_STATE room, tick, state the game this player is in now, as a tick starts
|
// 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_CHECKSUM,
|
||||||
PACKET_PING,
|
PACKET_PING,
|
||||||
PACKET_ROOMS,
|
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
|
// 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));
|
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.
|
// 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.
|
// 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;
|
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)
|
if (kind == ATTACK_VACUUM)
|
||||||
suck_draw(attack, tint);
|
suck_draw(cone, tint);
|
||||||
else
|
else
|
||||||
gust_draw(attack, tint);
|
gust_draw(cone, tint);
|
||||||
cone_outline_draw(renderer, attack.position, tip, VACUUM_CONE, color_fade(tint, alpha * CONE_ALPHA));
|
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;
|
float angle = attack.time * PIP_SPIN + 2 * PI * i / attack.bounces;
|
||||||
Vec2 pip = attack.position + vec2_polar(angle, attack.radius + PIP_GAP);
|
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;
|
Uint32 seed = ((Uint32)std::floor(phase) * SUCK_MOTES + i) * MOTE_KEYS + attack.owner;
|
||||||
float side = mote_share_get(seed) * 2 - 1;
|
float side = mote_share_get(seed) * 2 - 1;
|
||||||
float fade = std::min({1.0f, progress / SUCK_FADE_IN, (1 - progress) / SUCK_FADE_OUT});
|
float fade = std::min({1.0f, progress / SUCK_FADE_IN, (1 - progress) / SUCK_FADE_OUT});
|
||||||
Vec2 mote =
|
Vec2 mote = attack.position +
|
||||||
attack.position + vec2_polar(attack.aimAngle + side * VACUUM_CONE, cone_reach_get(attack) * (1 - progress));
|
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 particle = {.color = color, .life = fade, .lifeMax = 1, .size = SUCK_MOTE_SIZE};
|
||||||
particle_draw(game.renderer, particle, mote);
|
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 delay = GUST_STAGGER * mote_share_get(seed + 2);
|
||||||
float own = std::clamp((progress - delay) / (1 - delay), 0.0f, 1.0f);
|
float own = std::clamp((progress - delay) / (1 - delay), 0.0f, 1.0f);
|
||||||
float sweep = 1 - (1 - own) * (1 - own);
|
float sweep = 1 - (1 - own) * (1 - own);
|
||||||
Vec2 mote =
|
Vec2 mote = attack.position +
|
||||||
attack.position + vec2_polar(attack.aimAngle + side * VACUUM_CONE, cone_reach_get(attack) * reach * sweep);
|
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 particle = {.color = color, .life = 1 - own, .lifeMax = 1, .size = SUCK_MOTE_SIZE};
|
||||||
particle_draw(game.renderer, particle, mote);
|
particle_draw(game.renderer, particle, mote);
|
||||||
}
|
}
|
||||||
@@ -1055,10 +1059,16 @@ void Scene::collision_draw()
|
|||||||
capsules_fill(renderer, shapes, count, color);
|
capsules_fill(renderer, shapes, count, color);
|
||||||
if (cone_get(attack) <= 0)
|
if (cone_get(attack) <= 0)
|
||||||
continue;
|
continue;
|
||||||
float range = cone_reach_get(attack);
|
Attack cones[CONES_MAX];
|
||||||
Rect bounds = {attack.position.x - range, attack.position.y - range, attack.position.x + range,
|
int coneCount = cones_get(game.sim, attack, cones);
|
||||||
attack.position.y + range};
|
for (int c = 0; c < coneCount; c++)
|
||||||
shape_fill(renderer, bounds, [&](Vec2 point) { return is_in_cone(attack, point); }, color);
|
{
|
||||||
|
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++)
|
for (int i = 0; i < game.sim.fighterCount; i++)
|
||||||
if (game.sim.fighters[i].isAlive)
|
if (game.sim.fighters[i].isAlive)
|
||||||
|
|||||||
@@ -128,6 +128,13 @@ void Session::update(Game& game, Controls local)
|
|||||||
}
|
}
|
||||||
check = checksums.erase(check);
|
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;
|
int advance = 1;
|
||||||
if (tick - paced >= PACE_WAIT && (lead < LEAD_LOW || lead > LEAD_HIGH))
|
if (tick - paced >= PACE_WAIT && (lead < LEAD_LOW || lead > LEAD_HIGH))
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ struct Game;
|
|||||||
constexpr int SESSION_RING = 64;
|
constexpr int SESSION_RING = 64;
|
||||||
// How often (in milliseconds) this screen tells the lobby its round trip.
|
// How often (in milliseconds) this screen tells the lobby its round trip.
|
||||||
constexpr Uint64 PING_REPORT_INTERVAL = 2000;
|
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.
|
// A member's connection has no ping to show.
|
||||||
constexpr float PING_NONE = -2;
|
constexpr float PING_NONE = -2;
|
||||||
|
|
||||||
@@ -118,6 +120,8 @@ private:
|
|||||||
Uint64 nextRename = 0;
|
Uint64 nextRename = 0;
|
||||||
bool isRenameWaiting = false;
|
bool isRenameWaiting = false;
|
||||||
Uint64 nextPingReport = 0;
|
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 start(Game& game);
|
||||||
void packets_take(Game& game);
|
void packets_take(Game& game);
|
||||||
|
|||||||
+13
-1
@@ -136,12 +136,20 @@ Uint32 Sim::checksum_get()
|
|||||||
{
|
{
|
||||||
Uint32 hash = FNV_OFFSET;
|
Uint32 hash = FNV_OFFSET;
|
||||||
hash_add(hash, (float)match.state + match.timer);
|
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++)
|
for (int i = 0; i < fighterCount; i++)
|
||||||
{
|
{
|
||||||
Fighter& fighter = fighters[i];
|
Fighter& fighter = fighters[i];
|
||||||
for (float value : {fighter.body.box.x, fighter.body.box.y, fighter.body.velocity.x, fighter.body.velocity.y,
|
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})
|
fighter.aim.x, fighter.aim.y, fighter.charge, fighter.health, fighter.hitstop})
|
||||||
hash_add(hash, value);
|
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)
|
for (float cooldown : fighter.cooldowns)
|
||||||
hash_add(hash, cooldown);
|
hash_add(hash, cooldown);
|
||||||
// What the menu's edits change: loadouts, levels, slot counts, stacking and PvP.
|
// 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 (Attack& attack : weapons.attacks)
|
||||||
for (float value : {attack.position.x, attack.position.y, attack.velocity.x, attack.velocity.y, attack.angle,
|
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);
|
hash_add(hash, value);
|
||||||
for (Dummy& dummy : world.dummies)
|
for (Dummy& dummy : world.dummies)
|
||||||
for (float value : {dummy.flash, dummy.wobble, dummy.wobbleSpeed})
|
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.raw(sim.bot.states, MAX_FIGHTERS);
|
||||||
stream.one(sim.weapons.orbitClock);
|
stream.one(sim.weapons.orbitClock);
|
||||||
stream.list(sim.weapons.attacks);
|
stream.list(sim.weapons.attacks);
|
||||||
|
stream.list(sim.weapons.pending);
|
||||||
World& world = sim.world;
|
World& world = sim.world;
|
||||||
stream.list(world.dummies);
|
stream.list(world.dummies);
|
||||||
stream.list(world.ledges);
|
stream.list(world.ledges);
|
||||||
|
|||||||
+1
-1
@@ -209,7 +209,7 @@ inline constexpr TrinketDef TRINKET_DEFS[TRINKET_COUNT] = {
|
|||||||
{"The Good Stuff",
|
{"The Good Stuff",
|
||||||
"Increases spread on attacks.",
|
"Increases spread on attacks.",
|
||||||
{{.spread = 1.2f}, {.spread = 1.5f}, {.spread = 2}},
|
{{.spread = 1.2f}, {.spread = 1.5f}, {.spread = 2}},
|
||||||
"Steady hands are for\nsurgeons."},
|
"Aye, me bottle o'scrumpy!"},
|
||||||
{"Splitter",
|
{"Splitter",
|
||||||
"Attacks split into smaller,\nless-damaging copies.",
|
"Attacks split into smaller,\nless-damaging copies.",
|
||||||
{{.copies = 2, .copyDamage = 1 / 3.0f},
|
{{.copies = 2, .copyDamage = 1 / 3.0f},
|
||||||
|
|||||||
+375
-99
@@ -35,6 +35,8 @@ constexpr float KNOCKBACK_REACH = 1.5f;
|
|||||||
constexpr float KNOCKBACK_SPEED = 550;
|
constexpr float KNOCKBACK_SPEED = 550;
|
||||||
constexpr float LANCE_LENGTH = 40;
|
constexpr float LANCE_LENGTH = 40;
|
||||||
constexpr float MUZZLE_DISTANCE = 18;
|
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 PROJECTILE_STEP = 8;
|
||||||
constexpr float RAY_STEP = 2;
|
constexpr float RAY_STEP = 2;
|
||||||
constexpr float RECOIL_CURVE = 0.75f;
|
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 = 0.06f;
|
||||||
constexpr float SPLIT_GAP_MELEE = 0.5f;
|
constexpr float SPLIT_GAP_MELEE = 0.5f;
|
||||||
constexpr float SPLIT_GAP_THROWN = 0.3f;
|
constexpr float SPLIT_GAP_THROWN = 0.3f;
|
||||||
constexpr float UPPERCUT_REACH = 16;
|
|
||||||
constexpr float VOLLEY_GAP = 6;
|
constexpr float VOLLEY_GAP = 6;
|
||||||
constexpr float WHIP_CONE = 0.25f;
|
constexpr float WHIP_CONE = 0.25f;
|
||||||
constexpr float WHIP_HANDLE = 4;
|
constexpr float WHIP_HANDLE = 4;
|
||||||
@@ -114,7 +115,8 @@ constexpr float VACUUM_SPIN_DOWN = 0.2f;
|
|||||||
constexpr float REST_SPEED = 40;
|
constexpr float REST_SPEED = 40;
|
||||||
// How fast a shot flies home, against the speed it left at.
|
// How fast a shot flies home, against the speed it left at.
|
||||||
constexpr float RETURN_SHARE = 0.5f;
|
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;
|
constexpr float SHOT_BOUNCE_MIN = 60;
|
||||||
// How much of its blast a grenade deals to whoever it clonks on the way.
|
// How much of its blast a grenade deals to whoever it clonks on the way.
|
||||||
constexpr float GRENADE_CONTACT_SHARE = 0.2f;
|
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
|
// Spends one of an attack's ricochets on a bounce at point, adding to its later damage and giving it its hits back, so
|
||||||
// many.
|
// it can strike anyone again straight away.
|
||||||
static void ricochet_count(Sim& sim, Attack& attack)
|
static void ricochet_count(Sim& sim, Attack& attack, Vec2 point)
|
||||||
{
|
{
|
||||||
attack.bounces--;
|
attack.bounces--;
|
||||||
attack.ricochets++;
|
attack.ricochets++;
|
||||||
if (attack.def()->kind == ATTACK_FLAME)
|
attack.hitsLeft = std::max(attack.hitsLeft, attack.def()->hits);
|
||||||
return;
|
attack.struck = 0;
|
||||||
|
attack.hitCooldown = 0;
|
||||||
sim.cues->sound_play(SOUND_RICOCHET);
|
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
|
// 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.angle = vec2_angle(attack.velocity);
|
||||||
attack.origin = attack.position;
|
attack.origin = attack.position;
|
||||||
attack.lifeStart = attack.time;
|
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.
|
// 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;
|
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
|
// 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.
|
// 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)
|
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();
|
const AttackDef& def = *attack.def();
|
||||||
Fighter& fighter = sim.fighters[attack.owner];
|
Fighter& fighter = sim.fighters[attack.owner];
|
||||||
|
Vec2 lastTip = attack.tip;
|
||||||
float progress = attack.progress_get();
|
float progress = attack.progress_get();
|
||||||
float range = attack.stats.range;
|
float range = attack.stats.range;
|
||||||
attack.radius = def.shape == SHAPE_CIRCLE ? attack.stats.range : def.radius * attack.scale;
|
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:
|
case ATTACK_PUNCH:
|
||||||
break;
|
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:
|
case ATTACK_POKE:
|
||||||
{
|
{
|
||||||
// Its path carries the tip, never closer in than the lance's own length, so the whole lance stays out in
|
// 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:
|
case ATTACK_WHIP:
|
||||||
{
|
{
|
||||||
// A charged crack stays strung from the end of the handle in the hand to where it cracked, hitting at
|
// 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
|
// once (each Ricochet bounce a strand of its own, staying where it glanced). A plain lash goes with the
|
||||||
// and links moving as lash_get says. It hits anywhere in its cone as far as it reaches until it cracks.
|
// 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())
|
if (attack.is_charged())
|
||||||
{
|
{
|
||||||
attack.origin = fighter.hand_get(attack.hand);
|
|
||||||
attack.isDead = progress >= 1;
|
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.angle = vec2_angle(attack.tip - attack.origin);
|
||||||
attack.position = attack.origin + vec2_polar(attack.angle, WHIP_HANDLE);
|
attack.position = attack.origin + vec2_polar(attack.angle, WHIP_HANDLE);
|
||||||
break;
|
break;
|
||||||
@@ -1182,6 +1262,26 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
|
|||||||
return sim.world.is_blocking_at(point);
|
return sim.world.is_blocking_at(point);
|
||||||
};
|
};
|
||||||
Vec2 reach = ray_march(attack.origin, attack.aimAngle, reach_at_get(attack), isBlocked);
|
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.angle = vec2_angle(lash_direction_get(attack, lash_heading_get(progress)));
|
||||||
attack.position = attack.origin + vec2_polar(attack.angle, WHIP_HANDLE);
|
attack.position = attack.origin + vec2_polar(attack.angle, WHIP_HANDLE);
|
||||||
attack.length = std::max(0.0f, vec2_length(reach - attack.origin) - 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)
|
if (!isFlying && def.kind != ATTACK_POKE && def.kind != ATTACK_BEAM && def.kind != ATTACK_WHIP)
|
||||||
attack.tip = attack.position;
|
attack.tip = attack.position;
|
||||||
attack.isHeld = false;
|
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
|
// Places an anchored attack along its path from its anchor (its fighter's hand or middle, or once loose, the wall it
|
||||||
// its lifetime is up: as far out along its heading as its speed has carried it (an eased one speeding up as it goes),
|
// glanced off) for its age, ending it once its lifetime is up: as far out along its heading as its speed has carried it
|
||||||
// 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
|
// (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
|
||||||
// fighter's aim, keeping whatever turn it set off with (a Splitter copy's place in the fan), its own code ending it.
|
// 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)
|
void Weapons::path_move(Sim& sim, Attack& attack)
|
||||||
{
|
{
|
||||||
const AttackDef& def = *attack.def();
|
const AttackDef& def = *attack.def();
|
||||||
@@ -1248,21 +1351,86 @@ void Weapons::path_move(Sim& sim, Attack& attack)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
float age = attack.life_get();
|
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 out = PATH_OUT * attack.lifetime;
|
||||||
float travel = def.isEased ? age * age / attack.lifetime : age;
|
float travel = def.isEased ? age * age / attack.lifetime : age;
|
||||||
if (def.path == PATH_RETURN && age > out)
|
if (def.path == PATH_RETURN && age > out)
|
||||||
travel = std::max(0.0f, out * (attack.lifetime - age) / (attack.lifetime - 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;
|
float distance = attack.stats.speed * travel;
|
||||||
if (def.path == PATH_ARC)
|
if (def.path == PATH_ARC)
|
||||||
{
|
{
|
||||||
attack.angle = attack.aimAngle + (attack.isLeft ? -1 : 1) * (ARC_START + distance / attack.stats.range);
|
attack.angle = attack.aimAngle + (attack.isLeft ? -1 : 1) * (ARC_START + distance / attack.stats.range);
|
||||||
distance = attack.stats.range;
|
distance = attack.stats.range;
|
||||||
}
|
}
|
||||||
attack.origin = def.anchor == ANCHOR_HAND ? fighter.hand_get(attack.hand) : fighter.center_get();
|
if (attack.glance == GLANCE_NONE)
|
||||||
attack.position = attack.origin + vec2_polar(attack.angle, distance);
|
attack.origin = anchor;
|
||||||
|
attack.position = attack.origin + vec2_polar(attack.aimAngle, offset) + vec2_polar(attack.angle, distance);
|
||||||
attack.isDead = age >= attack.lifetime;
|
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
|
// 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.
|
// side of the wall it ran into.
|
||||||
static Vec2 surface_normal_get(Sim& sim, Vec2 from, Vec2 wall)
|
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);
|
sim.cues->sound_play(SOUND_WISP);
|
||||||
}
|
}
|
||||||
attack.isReturning |= def.path == PATH_RETURN && attack.time >= attack.restTime + def.hold;
|
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)
|
if (attack.isReturning && def.isTethered)
|
||||||
attack.isDead = is_caught(fighter, attack);
|
attack.isDead = is_caught(fighter, attack);
|
||||||
else if (attack.isReturning)
|
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;
|
body.velocity.x += def.gravity * body.slope / (1 + body.slope * body.slope) * TICK;
|
||||||
Vec2 before = body.velocity;
|
Vec2 before = body.velocity;
|
||||||
sim.world.body_move(body);
|
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 isBounced = false;
|
||||||
bool isRamp = body.normal.x != 0 || body.normal.y != 0;
|
bool isRamp = body.normal.x != 0 || body.normal.y != 0;
|
||||||
float into = vec2_dot(before, body.normal);
|
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;
|
body.isGrounded = false;
|
||||||
isBounced = true;
|
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;
|
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;
|
body.isGrounded = false;
|
||||||
isBounced = true;
|
isBounced = true;
|
||||||
}
|
}
|
||||||
if (isBounced && attack.bounces > 0)
|
if (isBounced && attack.bounces > 0)
|
||||||
{
|
{
|
||||||
ricochet_count(sim, attack);
|
ricochet_count(sim, attack, attack.position);
|
||||||
attack.lifeStart = attack.time;
|
attack.lifeStart = attack.time;
|
||||||
}
|
}
|
||||||
else if (isBounced)
|
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
|
// 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
|
// (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
|
// 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
|
// 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
|
// ricochet off (Ricochet bounces it off as many as the shot would) or the attack's reach: curving under gravity (and
|
||||||
// arrows, straight for other shots, throws, beams, lashes, mines and hooks, and a teleport's line to where it lands.
|
// slowing with drag) for anything that falls (arrows, bolts, grenades, a thrown flail ball), straight for other shots,
|
||||||
// Anything that explodes rings the blast it would make where the path ends (its reach counts from the hand). Melee,
|
// throws, beams, lashes, mines, hooks and a flying slash, and a teleport's line to where it lands. Anything that
|
||||||
// flames and flails have none, as their paths aren't lines.
|
// 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> Weapons::paths_get(Sim& sim, Fighter& fighter)
|
||||||
{
|
{
|
||||||
std::vector<Path> paths;
|
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 spot = teleport_spot(sim, fighter, fighter.controls.cursor, stats.range);
|
||||||
Vec2 from = fighter.center_get();
|
Vec2 from = fighter.center_get();
|
||||||
Path path = {.area = stats.area * def.scale * fighter.stat_get(STAT_ATTACK_SIZE)};
|
Path path = {.area = stats.area * def.scale * fighter.stat_get(STAT_ATTACK_SIZE)};
|
||||||
float length = vec2_length(spot - from);
|
path_line_add(path, from, spot);
|
||||||
for (float traveled = 0; traveled < length; traveled += TRAJECTORY_STEP)
|
|
||||||
path.steps.push_back(from + (spot - from) * (traveled / length));
|
|
||||||
path.steps.push_back(spot);
|
path.steps.push_back(spot);
|
||||||
paths.push_back(path);
|
paths.push_back(path);
|
||||||
return paths;
|
return paths;
|
||||||
@@ -1907,36 +2122,62 @@ std::vector<Path> Weapons::paths_get(Sim& sim, Fighter& fighter)
|
|||||||
for (int split = 0; split < splits; split++)
|
for (int split = 0; split < splits; split++)
|
||||||
{
|
{
|
||||||
float offset = splits > 1 ? split_offset(kind, split, splits, fighter.stat_get(STAT_SPREAD)) : 0;
|
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);
|
Vec2 steps[CONES_MAX + 1] = {muzzle, muzzle + vec2_polar(vec2_angle(fighter.aim) + offset, reach)};
|
||||||
paths.push_back({.steps = {muzzle, tip}, .cone = VACUUM_CONE * fighter.stat_get(STAT_SPREAD)});
|
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;
|
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 ||
|
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_FIREBALL || kind == ATTACK_BEAM || kind == ATTACK_WHIP || kind == ATTACK_MINE ||
|
||||||
kind == ATTACK_BOOMERANG || (kind == ATTACK_HOOK && variant != VARIANT_CHARGED);
|
kind == ATTACK_BOOMERANG || kind == ATTACK_HOOK ||
|
||||||
if (!isArrow && !isStraight)
|
(kind == ATTACK_SLASH && variant == VARIANT_CHARGED);
|
||||||
|
if (!isFalling && !isStraight)
|
||||||
return paths;
|
return paths;
|
||||||
|
|
||||||
// Each Splitter copy gets its own path, fanned like the copies. Straight paths step along the aim one unit at a
|
// 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 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++)
|
for (int split = 0; split < splits; split++)
|
||||||
{
|
{
|
||||||
float offset = splits > 1 ? split_offset(kind, split, splits, fighter.stat_get(STAT_SPREAD)) : 0;
|
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);
|
Vec2 aim = vec2_polar(vec2_angle(fighter.aim) + offset, 1);
|
||||||
float speed = stats.speed * fighter.stat_get(STAT_PROJECTILE_SPEED) + push_get(fighter, aim);
|
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(),
|
Attack probe = {.origin = fighter.hand_get(),
|
||||||
.angle = vec2_angle(aim),
|
.angle = vec2_angle(aim),
|
||||||
.weapon = fighter.weaponLoadout[key],
|
.weapon = fighter.weaponLoadout[key],
|
||||||
.isLeft = fighter.is_left()};
|
.isLeft = fighter.is_left()};
|
||||||
Vec2 point = muzzle_get(probe);
|
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;
|
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)
|
for (float traveled = area > 0 ? MUZZLE_DISTANCE : 0; traveled < reach; traveled += TRAJECTORY_STEP)
|
||||||
{
|
{
|
||||||
float time = TRAJECTORY_STEP / std::max(1.0f, vec2_length(velocity));
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
velocity.y += def.gravity * time;
|
velocity.y += def.gravity * time;
|
||||||
|
velocity = isFalling ? velocity * math_pow(def.drag, time / TICK) : velocity;
|
||||||
path.steps.push_back(point);
|
path.steps.push_back(point);
|
||||||
point = next;
|
point = next;
|
||||||
}
|
}
|
||||||
path.steps.push_back(point);
|
path.steps.push_back(point);
|
||||||
path.area = isEnded ? area : 0;
|
path.area = isEnded && !isBouncing ? area : 0;
|
||||||
paths.push_back(path);
|
paths.push_back(path);
|
||||||
}
|
}
|
||||||
return paths;
|
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,
|
// 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
|
// 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)
|
void Weapons::instant_launch(Sim& sim, Attack& piece)
|
||||||
{
|
{
|
||||||
const AttackDef& def = *piece.def();
|
const AttackDef& def = *piece.def();
|
||||||
@@ -1988,33 +2230,32 @@ void Weapons::instant_launch(Sim& sim, Attack& piece)
|
|||||||
return true;
|
return true;
|
||||||
return sim.world.is_blocking_at(point);
|
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.velocity = {};
|
||||||
piece.tip = ray_march(start, piece.angle, piece.stats.range, isBlocked);
|
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)
|
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;
|
piece.position = piece.tip;
|
||||||
if (sim.world.is_blocking_at(piece.position))
|
if (sim.world.is_blocking_at(piece.position))
|
||||||
hook_attach(sim, piece);
|
hook_attach(sim, piece);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sim.cues->burst_spawn(SPARK_BURST, piece.tip, piece.angle + PI, piece.owner);
|
sim.cues->burst_spawn(SPARK_BURST, piece.tip, piece.angle + PI, piece.owner);
|
||||||
Attack segment = piece;
|
Attack segment = strands_spawn(sim, piece, attacks);
|
||||||
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);
|
|
||||||
}
|
|
||||||
if (def.isBursting)
|
if (def.isBursting)
|
||||||
blast_spawn(sim, {
|
blast_spawn(sim, {
|
||||||
.position = piece.tip,
|
.position = segment.tip,
|
||||||
.area = piece.stats.area * piece.scale / piece.stats.scale,
|
.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,
|
.owner = piece.owner,
|
||||||
.sound = SOUND_NONE, // the attack's own sound already carries it
|
.sound = SOUND_NONE, // the attack's own sound already carries it
|
||||||
.weapon = piece.weapon,
|
.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.
|
// 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)
|
void Weapons::whip_pull(Sim& sim, Attack& piece)
|
||||||
{
|
{
|
||||||
@@ -2311,28 +2571,6 @@ void Weapons::vacuum_move(Sim& sim, Attack& attack)
|
|||||||
attack.isReturning = !isHeld;
|
attack.isReturning = !isHeld;
|
||||||
attack.length = std::clamp(attack.length + (isHeld ? TICK / VACUUM_SPIN_UP : -TICK / VACUUM_SPIN_DOWN), 0.0f, 1.0f);
|
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;
|
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.
|
// 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);
|
float interval = fighter.level_get(attack.key).cooldown / fighter.stat_get(STAT_ATTACK_RATE);
|
||||||
bool isPulse = attack.hitCooldown <= 0;
|
bool isPulse = attack.hitCooldown <= 0;
|
||||||
@@ -2341,19 +2579,49 @@ void Weapons::vacuum_move(Sim& sim, Attack& attack)
|
|||||||
attack.struck = 0;
|
attack.struck = 0;
|
||||||
attack.hitCooldown = interval;
|
attack.hitCooldown = interval;
|
||||||
}
|
}
|
||||||
|
// Only the first of a fan of copies (or repeats) lengthens lives, so they don't add up.
|
||||||
|
bool isLengthening = isPulse && !attack.isCopy;
|
||||||
|
// 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);
|
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++)
|
for (int target = 0; target < targets; target++)
|
||||||
{
|
{
|
||||||
Rect box = target_box(sim, target);
|
Rect box = target_box(sim, target);
|
||||||
Vec2 center = {box.x + box.w / 2, box.y + box.h / 2};
|
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))
|
if ((attack.struck >> target & 1) || !is_target(sim, target, attack.owner) || !is_in_cone(cone, center))
|
||||||
continue;
|
continue;
|
||||||
attack.struck |= 1u << target;
|
attack.struck |= 1u << target;
|
||||||
attack_hit(sim, attack, target, center);
|
attack_hit(sim, cone, target, center);
|
||||||
|
attack.hitCooldown = cone.hitCooldown;
|
||||||
|
}
|
||||||
|
shots_drift(sim, cone, 0, VACUUM_PULL_RATE * TICK, isLengthening ? VACUUM_RESTORE : 0);
|
||||||
}
|
}
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Vacuum's airblast: once, as it goes off, its cone hits everyone in it for a little damage and knocks every
|
// The Vacuum's airblast: once, as it goes off, its cone hits everyone in it for a little damage and knocks every
|
||||||
@@ -2365,16 +2633,23 @@ void Weapons::gust_move(Sim& sim, Attack& attack)
|
|||||||
attack.isDead = attack.life_get() >= attack.lifetime;
|
attack.isDead = attack.life_get() >= attack.lifetime;
|
||||||
if (attack.time > 0)
|
if (attack.time > 0)
|
||||||
return;
|
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);
|
int targets = std::min(MAX_FIGHTERS + (int)sim.world.dummies.size(), STRUCK_MAX);
|
||||||
|
Attack cones[CONES_MAX];
|
||||||
|
int count = cones_glance(sim, attack, cones);
|
||||||
|
for (int c = 0; c < count; c++)
|
||||||
|
{
|
||||||
|
Attack& cone = cones[c];
|
||||||
for (int target = 0; target < targets; target++)
|
for (int target = 0; target < targets; target++)
|
||||||
{
|
{
|
||||||
Rect box = target_box(sim, target);
|
Rect box = target_box(sim, target);
|
||||||
Vec2 center = {box.x + box.w / 2, box.y + box.h / 2};
|
Vec2 center = {box.x + box.w / 2, box.y + box.h / 2};
|
||||||
if (!is_target(sim, target, attack.owner) || !is_in_cone(attack, center))
|
if ((attack.struck >> target & 1) || !is_target(sim, target, attack.owner) || !is_in_cone(cone, center))
|
||||||
continue;
|
continue;
|
||||||
attack_hit(sim, attack, target, center);
|
attack.struck |= 1u << target;
|
||||||
|
attack_hit(sim, cone, target, center);
|
||||||
// Boxing Glove adds to it as to any knock.
|
// Boxing Glove adds to it as to any knock.
|
||||||
Vec2 push = vec2_normalize(center - attack.position) * attack.stats.speed * cone_strength_get(attack, center) *
|
Vec2 push = vec2_normalize(center - cone.position) * attack.stats.speed * cone_strength_get(cone, center) *
|
||||||
sim.fighters[attack.owner].stat_get(STAT_KNOCKBACK);
|
sim.fighters[attack.owner].stat_get(STAT_KNOCKBACK);
|
||||||
if (target >= MAX_FIGHTERS)
|
if (target >= MAX_FIGHTERS)
|
||||||
{
|
{
|
||||||
@@ -2384,7 +2659,8 @@ void Weapons::gust_move(Sim& sim, Attack& attack)
|
|||||||
Fighter& fighter = sim.fighters[target];
|
Fighter& fighter = sim.fighters[target];
|
||||||
fighter.knock_take(push * fighter.stat_get(STAT_KNOCKBACK_TAKEN), attack.owner);
|
fighter.knock_take(push * fighter.stat_get(STAT_KNOCKBACK_TAKEN), attack.owner);
|
||||||
}
|
}
|
||||||
shots_drift(sim, attack, attack.stats.speed * GUST_SHOT_SHARE, 0, attack.isCopy ? 0 : GUST_RESTORE);
|
shots_drift(sim, cone, 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
|
// 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.
|
// 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 HITS_ENDLESS = 1 << 30;
|
||||||
constexpr int WHIP_RAYS = 9;
|
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.
|
// How long a thrown flail ball stays out before it reels itself back in.
|
||||||
constexpr float FLAIL_LIFETIME = 15;
|
constexpr float FLAIL_LIFETIME = 15;
|
||||||
|
|
||||||
@@ -71,6 +73,16 @@ enum AttackKind
|
|||||||
ATTACK_WISP
|
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.
|
// What an attack moves with: nothing (flying free in the world), its fighter's hand, or its fighter's middle.
|
||||||
enum AttackAnchor
|
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
|
// 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).
|
// 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).
|
// - 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
|
// - 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
|
// 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
|
// 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).
|
// isOrbiting: it circles its fighter in a shared orbit until launched (see Weapons::orbits_update).
|
||||||
// isPassingPlatforms: it flies through platforms.
|
// isPassingPlatforms: it flies through platforms.
|
||||||
// isProjectile: the Accelerator speeds it up.
|
// 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).
|
// isRigid: its reach grows with its size as one rigid piece (a hammer, a Vacuum's cone).
|
||||||
// isShot: it ends once out of hits.
|
// isShot: it ends once out of hits.
|
||||||
// isTethered: it's kept within its range of its fighter's hand (on a chain or rope).
|
// 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 hold = 0;
|
||||||
float homing = 0;
|
float homing = 0;
|
||||||
float knock = 1;
|
float knock = 1;
|
||||||
|
float offset = 0;
|
||||||
float radius = 0;
|
float radius = 0;
|
||||||
float scale = 1;
|
float scale = 1;
|
||||||
float shake = 0;
|
float shake = 0;
|
||||||
@@ -195,6 +211,7 @@ struct AttackDef
|
|||||||
bool isOrbiting = false;
|
bool isOrbiting = false;
|
||||||
bool isPassingPlatforms = false;
|
bool isPassingPlatforms = false;
|
||||||
bool isProjectile = false;
|
bool isProjectile = false;
|
||||||
|
bool isRiding = false;
|
||||||
bool isRigid = false;
|
bool isRigid = false;
|
||||||
bool isShot = false;
|
bool isShot = false;
|
||||||
bool isTethered = false;
|
bool isTethered = false;
|
||||||
@@ -380,14 +397,18 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.radius = 6,
|
.radius = 6,
|
||||||
.shake = 0.025f,
|
.shake = 0.025f,
|
||||||
.spread = 18,
|
.spread = 18,
|
||||||
|
.isBouncy = true,
|
||||||
.isProjectile = true},
|
.isProjectile = true},
|
||||||
.charged = {.label = "uppercut",
|
.charged = {.label = "uppercut",
|
||||||
.kind = ATTACK_UPPERCUT,
|
.kind = ATTACK_UPPERCUT,
|
||||||
.sound = SOUND_UPPERCUT,
|
.sound = SOUND_UPPERCUT,
|
||||||
.anchor = ANCHOR_BODY,
|
.anchor = ANCHOR_BODY,
|
||||||
|
.offset = 16,
|
||||||
.radius = 14,
|
.radius = 14,
|
||||||
.shake = 0.25f,
|
.shake = 0.25f,
|
||||||
.spread = 5},
|
.spread = 5,
|
||||||
|
.isBouncy = true,
|
||||||
|
.isRiding = true},
|
||||||
.levels =
|
.levels =
|
||||||
{
|
{
|
||||||
{.normal = {.damage = 16, .lifetime = 0.12f, .speed = 283},
|
{.normal = {.damage = 16, .lifetime = 0.12f, .speed = 283},
|
||||||
@@ -459,6 +480,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.radius = 8,
|
.radius = 8,
|
||||||
.shake = 0.15f,
|
.shake = 0.15f,
|
||||||
.spread = 6,
|
.spread = 6,
|
||||||
|
.isBouncy = true,
|
||||||
.isEased = true,
|
.isEased = true,
|
||||||
.isProjectile = true,
|
.isProjectile = true,
|
||||||
.isRigid = true,
|
.isRigid = true,
|
||||||
@@ -471,6 +493,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.radius = 8,
|
.radius = 8,
|
||||||
.shake = 0.35f,
|
.shake = 0.35f,
|
||||||
.spread = 6,
|
.spread = 6,
|
||||||
|
.isBouncy = true,
|
||||||
.isEased = true,
|
.isEased = true,
|
||||||
.isProjectile = true,
|
.isProjectile = true,
|
||||||
.isRigid = true,
|
.isRigid = true,
|
||||||
@@ -502,6 +525,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.radius = 3,
|
.radius = 3,
|
||||||
.shake = 0.075f,
|
.shake = 0.075f,
|
||||||
.spread = 4,
|
.spread = 4,
|
||||||
|
.isBouncy = true,
|
||||||
.isProjectile = true,
|
.isProjectile = true,
|
||||||
.isWielded = true},
|
.isWielded = true},
|
||||||
.charged = {.label = "lunging poke",
|
.charged = {.label = "lunging poke",
|
||||||
@@ -512,6 +536,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.radius = 3,
|
.radius = 3,
|
||||||
.shake = 0.2f,
|
.shake = 0.2f,
|
||||||
.spread = 3,
|
.spread = 3,
|
||||||
|
.isBouncy = true,
|
||||||
.isProjectile = true,
|
.isProjectile = true,
|
||||||
.isWielded = true},
|
.isWielded = true},
|
||||||
.levels =
|
.levels =
|
||||||
@@ -544,6 +569,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.radius = 4,
|
.radius = 4,
|
||||||
.shake = 0.05f,
|
.shake = 0.05f,
|
||||||
.spread = 7.5f,
|
.spread = 7.5f,
|
||||||
|
.isBouncy = true,
|
||||||
.isProjectile = true},
|
.isProjectile = true},
|
||||||
.charged = {.label = "lash + blast",
|
.charged = {.label = "lash + blast",
|
||||||
.kind = ATTACK_WHIP,
|
.kind = ATTACK_WHIP,
|
||||||
@@ -554,6 +580,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.shake = 0.15f,
|
.shake = 0.15f,
|
||||||
.spread = 0.5f,
|
.spread = 0.5f,
|
||||||
.hits = 0,
|
.hits = 0,
|
||||||
|
.isBouncy = true,
|
||||||
.isBursting = true,
|
.isBursting = true,
|
||||||
.isInstant = true},
|
.isInstant = true},
|
||||||
.levels =
|
.levels =
|
||||||
@@ -585,6 +612,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.shake = 0.075f,
|
.shake = 0.075f,
|
||||||
.spread = 8,
|
.spread = 8,
|
||||||
.hits = HITS_ENDLESS,
|
.hits = HITS_ENDLESS,
|
||||||
|
.isBouncy = true,
|
||||||
.isProjectile = true,
|
.isProjectile = true,
|
||||||
.isWielded = true},
|
.isWielded = true},
|
||||||
.charged = {.label = "ball throw",
|
.charged = {.label = "ball throw",
|
||||||
@@ -687,6 +715,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.shake = 0.1f,
|
.shake = 0.1f,
|
||||||
.spread = 2,
|
.spread = 2,
|
||||||
.isAligned = true,
|
.isAligned = true,
|
||||||
|
.isBouncy = true,
|
||||||
.isDrifting = true,
|
.isDrifting = true,
|
||||||
.isFalloff = true,
|
.isFalloff = true,
|
||||||
.isInheriting = true,
|
.isInheriting = true,
|
||||||
@@ -702,6 +731,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.shake = 0.2f,
|
.shake = 0.2f,
|
||||||
.spread = 1,
|
.spread = 1,
|
||||||
.isAligned = true,
|
.isAligned = true,
|
||||||
|
.isBouncy = true,
|
||||||
.isDrifting = true,
|
.isDrifting = true,
|
||||||
.isFalloff = true,
|
.isFalloff = true,
|
||||||
.isInstant = true,
|
.isInstant = true,
|
||||||
@@ -969,6 +999,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.spread = 4,
|
.spread = 4,
|
||||||
.hits = HITS_ENDLESS,
|
.hits = HITS_ENDLESS,
|
||||||
.isAligned = true,
|
.isAligned = true,
|
||||||
|
.isBouncy = true,
|
||||||
.isDrifting = true,
|
.isDrifting = true,
|
||||||
.isFalloff = true,
|
.isFalloff = true,
|
||||||
.isInheriting = true,
|
.isInheriting = true,
|
||||||
@@ -985,6 +1016,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.spread = 2,
|
.spread = 2,
|
||||||
.hits = HITS_ENDLESS,
|
.hits = HITS_ENDLESS,
|
||||||
.isAligned = true,
|
.isAligned = true,
|
||||||
|
.isBouncy = true,
|
||||||
.isDrifting = true,
|
.isDrifting = true,
|
||||||
.isFalloff = true,
|
.isFalloff = true,
|
||||||
.isInheriting = true,
|
.isInheriting = true,
|
||||||
@@ -1222,6 +1254,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.shape = SHAPE_CONE,
|
.shape = SHAPE_CONE,
|
||||||
.knock = 0,
|
.knock = 0,
|
||||||
.hits = 0,
|
.hits = 0,
|
||||||
|
.isBouncy = true,
|
||||||
.isRigid = true},
|
.isRigid = true},
|
||||||
.charged = {.label = "airblast",
|
.charged = {.label = "airblast",
|
||||||
.kind = ATTACK_GUST,
|
.kind = ATTACK_GUST,
|
||||||
@@ -1232,6 +1265,7 @@ inline constexpr WeaponDef WEAPON_DEFS[WEAPON_COUNT] = {
|
|||||||
.knock = 0,
|
.knock = 0,
|
||||||
.shake = 0.15f,
|
.shake = 0.15f,
|
||||||
.hits = 0,
|
.hits = 0,
|
||||||
|
.isBouncy = true,
|
||||||
.isClaiming = true,
|
.isClaiming = true,
|
||||||
.isRigid = true},
|
.isRigid = true},
|
||||||
.levels =
|
.levels =
|
||||||
@@ -1393,6 +1427,7 @@ struct Attack
|
|||||||
int hitsLeft = 0;
|
int hitsLeft = 0;
|
||||||
int owner = 0;
|
int owner = 0;
|
||||||
int ownerPrevious = -1;
|
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;
|
int ricochets = 0;
|
||||||
// Targets already hit, a bit each: by a grenade's clonks, or by a Vacuum's nibble this interval.
|
// Targets already hit, a bit each: by a grenade's clonks, or by a Vacuum's nibble this interval.
|
||||||
Uint32 struck = 0;
|
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).
|
// The hand it came from: the main hand (0) or a Dual Wield off-hand (1 up).
|
||||||
int hand = 0;
|
int hand = 0;
|
||||||
AttackVariant variant = VARIANT_NORMAL;
|
AttackVariant variant = VARIANT_NORMAL;
|
||||||
|
Glance glance = GLANCE_NONE;
|
||||||
bool isAttached = false;
|
bool isAttached = false;
|
||||||
bool isCopy = false;
|
bool isCopy = false;
|
||||||
bool isDead = false;
|
bool isDead = false;
|
||||||
@@ -1461,6 +1497,7 @@ float fill_get(float strength);
|
|||||||
std::vector<Vec2> lash_get(Attack& attack);
|
std::vector<Vec2> lash_get(Attack& attack);
|
||||||
Vec2 muzzle_get(Attack& attack);
|
Vec2 muzzle_get(Attack& attack);
|
||||||
int shapes_get(Sim& sim, Attack& attack, Capsule* shapes);
|
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
|
// 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
|
// knocked away from, and knock scales how hard. Targets are numbered fighters first, then dummies
|
||||||
@@ -1583,6 +1620,8 @@ class Weapons
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
std::vector<Attack> attacks;
|
std::vector<Attack> attacks;
|
||||||
|
// Explosions queued to hit next tick (see blast_spawn).
|
||||||
|
std::vector<Attack> pending;
|
||||||
float orbitClock = 0;
|
float orbitClock = 0;
|
||||||
|
|
||||||
void fire(Sim& sim, Fighter& fighter, float charge);
|
void fire(Sim& sim, Fighter& fighter, float charge);
|
||||||
@@ -1601,14 +1640,14 @@ public:
|
|||||||
void contact_update(Sim& sim, Fighter& fighter);
|
void contact_update(Sim& sim, Fighter& fighter);
|
||||||
void orbits_release(Fighter& fighter);
|
void orbits_release(Fighter& fighter);
|
||||||
void teleport_move(Sim& sim, Attack& attack);
|
void teleport_move(Sim& sim, Attack& attack);
|
||||||
|
bool ride_bounce(Sim& sim, Fighter& fighter, Vec2 before);
|
||||||
void solids_add(Fighter& fighter, World& world);
|
void solids_add(Fighter& fighter, World& world);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<Attack> pending;
|
|
||||||
|
|
||||||
bool is_spawned(Sim& sim, Fighter& fighter, Shot shot);
|
bool is_spawned(Sim& sim, Fighter& fighter, Shot shot);
|
||||||
void attack_move(Sim& sim, Attack& attack);
|
void attack_move(Sim& sim, Attack& attack);
|
||||||
void path_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 flight_move(Sim& sim, Attack& attack);
|
||||||
void wall_hit(Sim& sim, Attack& attack, Vec2 wall, bool isHeld);
|
void wall_hit(Sim& sim, Attack& attack, Vec2 wall, bool isHeld);
|
||||||
void attack_remember(Attack& attack);
|
void attack_remember(Attack& attack);
|
||||||
@@ -1618,6 +1657,7 @@ private:
|
|||||||
Attack* tether_get(Fighter& fighter, int key, int hand);
|
Attack* tether_get(Fighter& fighter, int key, int hand);
|
||||||
void hook_attach(Sim& sim, Attack& attack);
|
void hook_attach(Sim& sim, Attack& attack);
|
||||||
void instant_launch(Sim& sim, Attack& piece);
|
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 whip_pull(Sim& sim, Attack& piece);
|
||||||
void attack_explode(Sim& sim, Attack& attack);
|
void attack_explode(Sim& sim, Attack& attack);
|
||||||
void vacuum_move(Sim& sim, Attack& attack);
|
void vacuum_move(Sim& sim, Attack& attack);
|
||||||
|
|||||||
Reference in new issue
Block a user