Weapons built from traits, fashion, Hourglass, About tab, and Ithaca

- Every attack is now assembled from traits on its definition instead
  of per-weapon code: an anchor and path (thrusts, out-and-back, arcs),
  shared flight physics (gravity, drag, acceleration, homing, and one
  wall response: end, burst, stick, crawl, bounce, stop, pass, return,
  latch), and flags like bouncy, drifting, tethered, orbiting, instant
  and arming. Melee are projectiles too.
- Shots have a lifetime and a fade (when damage bottoms out). Range is
  speed times lifetime. Ricochets and the Vacuum wind lifetime back.
- The Vacuum pulls anyone's shots, and its airblast flips them out
  along the aim, claiming enemies' (fading into your colour).
- New trinket: Hourglass, longer projectile lifetime and range. Ropes
  and chains lengthen with it and the Accelerator.
- Fashion: 23 items worn on the head, face or torso, chosen in Profile.
- Flamethrower flames hit continuously, the whip coils and sweeps,
  bouncing shots roll over ramps, and Ricochet relights grenade fuses.
- Profile tab, colour tooltips, option hints on every setting, an
  About tab with credits and links, and the Ithaca font.
- Loadouts lock once readied, tomatoes clear with the match, and the
  round-over pause is 4 seconds.
- CC0 license. Tools and CLAUDE.md are no longer tracked, and the web
  shell moves to web/.
This commit is contained in:
shweet committed 2026-11-10 15:27:12 -05:00
1 parent e55596b276
commit e9d8cac926
101 files changed
+5787 -7148

No files matched your search

+18 -36
View File
@@ -12,8 +12,6 @@ constexpr float FLASH_CROSS = 0.5f;
constexpr float FLASH_LENGTH = 16;
constexpr float FLASH_LIFE = 0.05f;
constexpr float FLASH_WIDTH = 6;
constexpr float RING_LIFE = 0.3f;
constexpr int RING_WIDTH = 2;
constexpr float AFTERIMAGE_LIFE = 0.12f;
constexpr float NUMBER_GAP = 8;
constexpr float POPUP_FADE = 0.4f;
@@ -57,10 +55,10 @@ void Effects::burst(Burst burst, Vec2 position, float angle, Color color)
}
// Callers may ask every tick. Copies are only kept every few ticks so trails stay spaced out.
void Effects::afterimage_spawn(Pose pose)
void Effects::afterimage_spawn(Pose pose, int worn)
{
if (ticks % AFTERIMAGE_INTERVAL == 0)
effect_add(afterimages, Afterimage{pose, AFTERIMAGE_LIFE});
effect_add(afterimages, Afterimage{pose, AFTERIMAGE_LIFE, worn});
}
// Drawn before the player and attacks so trails sit behind what made them.
@@ -71,6 +69,7 @@ void Effects::afterimage_draw(Game& game)
Pose pose = afterimage.pose;
pose.placement.color = color_fade(pose.placement.color, AFTERIMAGE_ALPHA * afterimage.life / AFTERIMAGE_LIFE);
pose.draw(game.renderer);
game.sprites.fashion_draw(game.renderer, pose, afterimage.worn);
}
}
@@ -122,9 +121,21 @@ void Effects::flash_spawn(Vec2 position, float angle, float size, Color color)
effect_add(flashes, Flash{color, position, angle, FLASH_LIFE, size});
}
void Effects::ring_spawn(Vec2 position, float radius, Color color)
// A particle at position, shrinking and fading as its life runs out: an outline in its colour around a core shaded by
// its fill. The outline is four strips so the fading core never shows it through.
void particle_draw(Renderer& renderer, Particle& particle, Vec2 position)
{
effect_add(rings, Ring{color, position, RING_LIFE, radius});
float fade = particle.life / particle.lifeMax;
float size = std::ceil(particle.size * fade);
float outer = size + PARTICLE_OUTLINE * 2;
Vec2 corner = {std::round(position.x - size / 2), std::round(position.y - size / 2)};
Vec2 edge = corner - Vec2{PARTICLE_OUTLINE, PARTICLE_OUTLINE};
Color color = color_fade(particle.color, fade);
renderer.rect_draw({edge.x, edge.y, outer, PARTICLE_OUTLINE}, color);
renderer.rect_draw({edge.x, corner.y + size, outer, PARTICLE_OUTLINE}, color);
renderer.rect_draw({edge.x, corner.y, PARTICLE_OUTLINE, size}, color);
renderer.rect_draw({corner.x + size, corner.y, PARTICLE_OUTLINE, size}, color);
renderer.rect_draw({corner.x, corner.y, size, size}, color_fade(color_shade(particle.color, particle.fill), fade));
}
void Effects::dps_record(int amount)
@@ -164,10 +175,6 @@ void Effects::update()
flash.life -= TICK;
std::erase_if(flashes, [](Flash& flash) { return flash.life <= 0; });
for (Ring& ring : rings)
ring.life -= TICK;
std::erase_if(rings, [](Ring& ring) { return ring.life <= 0; });
for (Afterimage& afterimage : afterimages)
afterimage.life -= TICK;
std::erase_if(afterimages, [](Afterimage& afterimage) { return afterimage.life <= 0; });
@@ -209,32 +216,8 @@ void Effects::draw(Game& game)
renderer.line_draw(flash.position - across, flash.position + across, width * FLASH_CROSS, flash.color);
}
// Each ring eases out to its full radius as it fades.
for (Ring& ring : rings)
{
float fade = ring.life / RING_LIFE;
float radius = ring.radius * (1 - fade * fade);
for (int i = 0; i < RING_WIDTH; i++)
renderer.ring_draw(ring.position, std::max(1.0f, radius - i), color_fade(ring.color, fade));
}
// Each particle shrinks and fades out. The outline is four strips so the fading core never shows it through.
for (Particle& particle : particles)
{
float fade = particle.life / particle.lifeMax;
float size = std::ceil(particle.size * fade);
float outer = size + PARTICLE_OUTLINE * 2;
Vec2 position = particle.position - particle.velocity * behind;
Vec2 corner = {std::round(position.x - size / 2), std::round(position.y - size / 2)};
Vec2 edge = corner - Vec2{PARTICLE_OUTLINE, PARTICLE_OUTLINE};
Color color = color_fade(particle.color, fade);
renderer.rect_draw({edge.x, edge.y, outer, PARTICLE_OUTLINE}, color);
renderer.rect_draw({edge.x, corner.y + size, outer, PARTICLE_OUTLINE}, color);
renderer.rect_draw({edge.x, corner.y, PARTICLE_OUTLINE, size}, color);
renderer.rect_draw({corner.x + size, corner.y, PARTICLE_OUTLINE, size}, color);
renderer.rect_draw({corner.x, corner.y, size, size},
color_fade(color_shade(particle.color, particle.fill), fade));
}
particle_draw(renderer, particle, particle.position - particle.velocity * behind);
for (Popup& popup : popups)
{
@@ -258,6 +241,5 @@ void Effects::clear()
hits.clear();
particles.clear();
popups.clear();
rings.clear();
shake = 0;
}