HUD pass, weapon and trinket rework, and font arrows
HUD: - Hotbars fit a quarter of the screen, dropping to small then tiny slots (thinner outlines when tiny), with cooldown bars matching their size. - HUD elements are drawn on their own layer and composited at one opacity: hotbar slots are see-through until hovered, the rest fade while hovered. - Rival windows fit the right column in at most two rows, shrinking icons then font, then dropping icons for name, health and ping bars. - Tooltips number equipped items' slots and say Recoil for weapons that only push back. Previews use arrows, spread uses degrees. - Crosshair fills in and shakes with charge, and weapons shake after firing until their cooldown ends. DPS and speed meters off by default. Gameplay: - Dual Wield stacks: each copy fires another recently held weapon from a fanned off-hand, at its own level's cut. - Flamethrower: no slowing, smaller fireball with some recoil, and an airblast that knocks others away. - Iron Weights slam on any landing faster than a plain fall (platforms too), stacking radius and damage, and speeding fast falls. - Stuck arrows are one-way ledges until they fade, matching their (slightly larger) hit size. Attacks now collide with slopes. - Halved Tesla Coil, Recoil Coil, Diazepam, Shock Absorber, Feather and screenshake values. Art and tools: - Weapon and trinket icons merged into one sheet each (normal, small, tiny), cells in catalog order. Grimoire wisps stand upright. - Fonts gain the degree sign and four arrows, decoded from UTF-8. - Menu fighter preview walks at quarter speed. - Unequip from either catalog tab by right click or gamepad. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6003175173
commit
6f112ab231
54 files changed
+1150
-3046
No files matched your search
+78
-63
@@ -1,6 +1,7 @@
|
||||
#include "weapon.h"
|
||||
#include "sim.h"
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
|
||||
constexpr int ATTACKS_MAX = 255;
|
||||
constexpr int DAMAGE_STEP = 5;
|
||||
@@ -8,12 +9,11 @@ constexpr int HEAVY_DAMAGE = 40;
|
||||
constexpr int HOOK_SUBSTEPS = 4;
|
||||
constexpr int TESLA_LINKS = 5;
|
||||
constexpr float TESLA_INTERVAL = 0.2f;
|
||||
constexpr float TESLA_RADIUS = 4;
|
||||
constexpr float TESLA_RADIUS = 2;
|
||||
constexpr float TESLA_RANGES[LEVEL_COUNT] = {120, 170, 220};
|
||||
constexpr float DEGREE = PI / 180;
|
||||
constexpr float ARROW_LINGER = 2.5f;
|
||||
constexpr float ARROW_SPAN = 18;
|
||||
constexpr float ARROW_THICKNESS = 2;
|
||||
constexpr float ARROW_RADIUS = 6;
|
||||
constexpr float BEAM_RADIUS = 4;
|
||||
constexpr float BOOMERANG_LIFETIME = 6;
|
||||
constexpr float BOOMERANG_RADIUS = 9;
|
||||
@@ -30,8 +30,6 @@ constexpr float TRAJECTORY_REACH = 400;
|
||||
constexpr float TRAJECTORY_STEP = 4;
|
||||
constexpr float FIRE_SHAKE = 0.5f;
|
||||
constexpr float FIREBALL_RADIUS = 5;
|
||||
constexpr float FLAME_BRAKE = 0.9f;
|
||||
constexpr float FLAME_BRAKE_REACH = 16;
|
||||
constexpr float FLAIL_BOUNCE = 0.3f;
|
||||
constexpr float FLAIL_BOUNCE_MAX = 0.9f;
|
||||
constexpr float FLAIL_BOUNCE_PER_RICOCHET = 0.2f;
|
||||
@@ -72,7 +70,7 @@ constexpr float PUNCH_RADIUS = 6;
|
||||
constexpr float PUNCH_THRUST = 0.4f;
|
||||
constexpr float SLASH_DISTANCE = 12;
|
||||
constexpr float SLASH_GAP = 14;
|
||||
constexpr float SHAKE_PER_DAMAGE = 0.006f;
|
||||
constexpr float SHAKE_PER_DAMAGE = 0.003f;
|
||||
constexpr float SLAM_RATIO = 0.75f;
|
||||
constexpr float SPEED_DAMAGE = 0.05f;
|
||||
// A normal attack let go partway through a charge hits harder, up to this much more just short of full.
|
||||
@@ -418,9 +416,9 @@ int shapes_get(Sim& sim, Attack& attack, Capsule* shapes)
|
||||
|
||||
// Fires the weapon in hand with the given charge if it's ready. Full charge uses the charged attack.
|
||||
// With the Doubler on, the same attack repeats a moment later for a fraction of the damage (stacked Doublers each
|
||||
// add a repeat of their own, one after another). With Dual Wield on, the off-hand weapon fires too, for a fraction of
|
||||
// its damage. Every weapon (see Fighter::weapon_key) keeps its own cooldown, in either hand, so swapping to another
|
||||
// weapon never inherits the last one's wait.
|
||||
// add a repeat of their own, one after another). With Dual Wield on, an off-hand weapon per copy fires too (see
|
||||
// Fighter::off_slots_get), each for a fraction of its damage. Every weapon (see Fighter::weapon_key) keeps its own
|
||||
// cooldown, in any hand, so swapping to another weapon never inherits the last one's wait.
|
||||
void Weapons::fire(Sim& sim, Fighter& fighter, float charge)
|
||||
{
|
||||
int held = fighter.weapon_key(fighter.slot);
|
||||
@@ -439,13 +437,22 @@ void Weapons::fire(Sim& sim, Fighter& fighter, float charge)
|
||||
}
|
||||
}
|
||||
|
||||
int off = fighter.off_slot_get();
|
||||
off = off < 0 ? -1 : fighter.weapon_key(off);
|
||||
float dual = fighter.trinket_get(TRINKET_DUAL, 0);
|
||||
if (dual > 0 && off >= 0 && off != held && fighter.cooldowns[off] <= 0 && !is_busy(fighter, off))
|
||||
// Each Dual Wield copy fires one off-hand weapon at its own level's cut, the strongest cut going to the first.
|
||||
float powers[SLOT_MAX];
|
||||
int copies = 0;
|
||||
for (int i = 0; i < fighter.trinketSlots; i++)
|
||||
if (fighter.trinketLoadout[i] == TRINKET_DUAL)
|
||||
powers[copies++] = TRINKET_DEFS[TRINKET_DUAL].values[fighter.trinketSlotLevels[i]];
|
||||
std::sort(powers, powers + copies, std::greater<float>());
|
||||
int offs[SLOT_MAX];
|
||||
int count = fighter.off_slots_get(offs);
|
||||
for (int hand = 1; hand <= count; hand++)
|
||||
{
|
||||
int off = fighter.weapon_key(offs[hand - 1]);
|
||||
if (fighter.cooldowns[off] > 0 || is_busy(fighter, off))
|
||||
continue;
|
||||
is_spawned(sim, fighter,
|
||||
{.charge = charge, .power = dual, .weaponSlot = off, .isOffhand = true, .isSecondary = true});
|
||||
{.charge = charge, .power = powers[hand - 1], .weaponSlot = off, .hand = hand, .isSecondary = true});
|
||||
fighter.cooldowns[off] = fighter.level_get(off).cooldown / gloves;
|
||||
}
|
||||
}
|
||||
@@ -659,7 +666,7 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot)
|
||||
|
||||
Attack attack = {
|
||||
.stats = stats,
|
||||
.origin = fighter.hand_get(),
|
||||
.origin = fighter.hand_get(shot.hand),
|
||||
.velocity = aim * stats.speed,
|
||||
.aimAngle = vec2_angle(aim),
|
||||
.angle = vec2_angle(aim),
|
||||
@@ -671,9 +678,9 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot)
|
||||
.owner = fighter.index,
|
||||
.weapon = held,
|
||||
.key = key,
|
||||
.hand = shot.hand,
|
||||
.isCharged = isCharged,
|
||||
.isLeft = fighter.is_left(),
|
||||
.isOffhand = shot.isOffhand,
|
||||
.isSecondary = shot.isSecondary,
|
||||
};
|
||||
attack.position = attack.origin + aim * MUZZLE_DISTANCE;
|
||||
@@ -763,6 +770,12 @@ bool Weapons::is_spawned(Sim& sim, Fighter& fighter, Shot shot)
|
||||
fighter.index);
|
||||
if (isCharged)
|
||||
sim.cues->shake_add(def.shake * FIRE_SHAKE);
|
||||
// A fireball goes off with an airblast at the muzzle, as wide as the fireball's own blast.
|
||||
if (kind == ATTACK_FIREBALL)
|
||||
blast_spawn(sim, {.position = muzzle,
|
||||
.area = stats.area * fighter.trinket_get(TRINKET_ENLARGER, 1),
|
||||
.owner = fighter.index,
|
||||
.isHarmless = true});
|
||||
|
||||
// An uppercut launches its fighter the way it rises, cancelling any speed against that first. Shock Absorber
|
||||
// softens every push the shot gives its own fighter: lunges, kickback and Recoil Coil.
|
||||
@@ -855,20 +868,6 @@ void Weapons::update(Sim& sim)
|
||||
attack.time += TICK;
|
||||
}
|
||||
tesla_update(sim);
|
||||
|
||||
// Standing in your own lingering flames brakes you, launches included.
|
||||
for (int i = 0; i < sim.fighterCount; i++)
|
||||
{
|
||||
Fighter& fighter = sim.fighters[i];
|
||||
for (Attack& attack : attacks)
|
||||
{
|
||||
bool isNear = vec2_length(attack.position - fighter.center_get()) < FLAME_BRAKE_REACH;
|
||||
if (attack.owner != i || attack.def()->kind != ATTACK_FLAME || attack.isDead || !isNear)
|
||||
continue;
|
||||
fighter.body.velocity = fighter.body.velocity * FLAME_BRAKE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
attacks_cap(sim);
|
||||
std::erase_if(attacks, [](Attack& attack) { return attack.isDead; });
|
||||
attacks.insert(attacks.end(), pending.begin(), pending.end());
|
||||
@@ -1002,7 +1001,10 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
|
||||
// path so they can't skip past targets either.
|
||||
Vec2 start = attack.position;
|
||||
int steps = vec2_length(attack.velocity) * TICK / PROJECTILE_STEP + 1;
|
||||
attack.radius = (def.kind == ATTACK_BULLET ? SLUG_RADIUS : BULLET_RADIUS) * attack.scale;
|
||||
attack.radius = (def.kind == ATTACK_BULLET ? SLUG_RADIUS
|
||||
: def.kind == ATTACK_ARROW ? ARROW_RADIUS
|
||||
: BULLET_RADIUS) *
|
||||
attack.scale;
|
||||
for (int step = 0; step < steps && !attack.isDead; step++)
|
||||
{
|
||||
Vec2 next = attack.position + attack.velocity * (TICK / steps);
|
||||
@@ -1080,7 +1082,7 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
|
||||
attack.hitsLeft = attack.time > 0 ? 0 : attack.hitsLeft;
|
||||
if (attack.ricochets == 0)
|
||||
{
|
||||
attack.origin = attack.isOffhand ? fighter.offhand_get() : fighter.hand_get();
|
||||
attack.origin = fighter.hand_get(attack.hand);
|
||||
attack.angle = vec2_angle(attack.tip - attack.origin);
|
||||
attack.position = attack.origin + vec2_polar(attack.angle, WHIP_HANDLE);
|
||||
}
|
||||
@@ -1091,7 +1093,7 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
|
||||
// once. A plain lash rolls out from the handle, going with the hand, sweeping down across its cone from
|
||||
// the upper edge, and hits anywhere in the cone out to its front until it cracks at full reach (short of
|
||||
// any wall).
|
||||
attack.origin = attack.isOffhand ? fighter.offhand_get() : fighter.hand_get();
|
||||
attack.origin = fighter.hand_get(attack.hand);
|
||||
attack.radius = BEAM_RADIUS * attack.scale;
|
||||
attack.isDead = progress >= 1;
|
||||
if (attack.isCharged)
|
||||
@@ -1151,7 +1153,7 @@ void Weapons::attack_move(Sim& sim, Attack& attack)
|
||||
case ATTACK_SPIN:
|
||||
{
|
||||
// Keeps circling while left is held, finishing the current lap once let go.
|
||||
int hand = attack.isOffhand ? fighter.off_slot_get() : fighter.slot;
|
||||
int hand = fighter.hand_slot_get(attack.hand);
|
||||
bool isHeld = fighter.is_down(CONTROL_ATTACK) && hand >= 0 && fighter.weapon_key(hand) == attack.key;
|
||||
if (isHeld && attack.time >= attack.duration)
|
||||
{
|
||||
@@ -1500,7 +1502,7 @@ void Weapons::teleport_move(Sim& sim, Attack& attack)
|
||||
|
||||
attack.isDead = true;
|
||||
Vec2 spot = attack.position;
|
||||
if (!attack.isCopy && !attack.isOffhand && fighter.isAlive)
|
||||
if (!attack.isCopy && attack.hand == 0 && fighter.isAlive)
|
||||
{
|
||||
sim.cues->burst_spawn(PORTAL_BURST, fighter.center_get(), 0, attack.owner);
|
||||
spot = teleport_spot(sim, fighter, attack.position, vec2_length(attack.position - fighter.center_get()));
|
||||
@@ -1600,7 +1602,7 @@ std::vector<Path> Weapons::paths_get(Sim& sim, Fighter& fighter)
|
||||
{
|
||||
std::vector<Path> paths;
|
||||
int key = fighter.weapon_key(fighter.slot);
|
||||
if (key < 0 || (is_busy(fighter, key) && !tether_get(fighter, key, false)))
|
||||
if (key < 0 || (is_busy(fighter, key) && !tether_get(fighter, key, 0)))
|
||||
return paths;
|
||||
const WeaponDef& weapon = WEAPON_DEFS[fighter.weaponLoadout[key]];
|
||||
const LevelDef& level = fighter.level_get(key);
|
||||
@@ -1766,9 +1768,34 @@ void Weapons::attack_explode(Sim& sim, Attack& attack)
|
||||
}
|
||||
|
||||
// Queues an explosion that hits everything inside it next tick, weaker toward its edge. Pushing blasts fling every
|
||||
// fighter away from the centre, harder the closer they are.
|
||||
// fighter away from the centre, harder the closer they are. A harmless blast (an airblast) only knocks everyone but its
|
||||
// owner within its area, launching them if close enough, shown as a ring.
|
||||
void Weapons::blast_spawn(Sim& sim, Blast blast)
|
||||
{
|
||||
float reach = blast.area * (blast.isHarmless ? 1 : KNOCKBACK_REACH);
|
||||
for (int i = 0; i < sim.fighterCount; i++)
|
||||
{
|
||||
Fighter& fighter = sim.fighters[i];
|
||||
Vec2 away = fighter.center_get() - blast.position;
|
||||
float distance = vec2_length(away);
|
||||
// Shock Absorber softens its wearer's own blasts as weapon movement, and anyone else's as knockback.
|
||||
float absorb = i == blast.owner ? fighter.trinket_get(TRINKET_ABSORBER, 1) : fighter.knock_absorb_get();
|
||||
bool isPushed = blast.isHarmless ? i != blast.owner : blast.isPushing;
|
||||
if (!isPushed || !fighter.isAlive || distance >= reach)
|
||||
continue;
|
||||
Vec2 push =
|
||||
vec2_normalize(distance > 0 ? away : Vec2{0, -1}) * KNOCKBACK_SPEED * (1 - distance / reach) * absorb;
|
||||
if (blast.isHarmless)
|
||||
fighter.knock_take(push, blast.owner);
|
||||
else
|
||||
fighter.impulse_add(push);
|
||||
}
|
||||
if (blast.isHarmless)
|
||||
{
|
||||
sim.cues->ring_spawn(blast.position, reach, blast.owner);
|
||||
return;
|
||||
}
|
||||
|
||||
Attack attack = {
|
||||
.stats = {.damage = blast.damage, .range = blast.area},
|
||||
.origin = blast.position,
|
||||
@@ -1785,42 +1812,29 @@ void Weapons::blast_spawn(Sim& sim, Blast blast)
|
||||
};
|
||||
attack_remember(attack);
|
||||
pending.push_back(attack);
|
||||
|
||||
float reach = blast.area * KNOCKBACK_REACH;
|
||||
for (int i = 0; i < sim.fighterCount; i++)
|
||||
{
|
||||
Fighter& fighter = sim.fighters[i];
|
||||
Vec2 away = fighter.center_get() - blast.position;
|
||||
float distance = vec2_length(away);
|
||||
// Shock Absorber softens its wearer's own blasts as weapon movement, and anyone else's as knockback.
|
||||
float absorb = i == blast.owner ? fighter.trinket_get(TRINKET_ABSORBER, 1) : fighter.knock_absorb_get();
|
||||
if (blast.isPushing && fighter.isAlive && distance < reach)
|
||||
fighter.impulse_add(vec2_normalize(distance > 0 ? away : Vec2{0, -1}) * KNOCKBACK_SPEED *
|
||||
(1 - distance / reach) * absorb);
|
||||
}
|
||||
|
||||
sim.cues->sound_play(blast.sound);
|
||||
sim.cues->shake_add(EXPLOSION_DEF.shake);
|
||||
sim.cues->burst_spawn(BLAST_BURST, blast.position, 0, blast.owner);
|
||||
}
|
||||
|
||||
// The whip lashing or hook flying out from this hand, which leaves only the weapon's base in it.
|
||||
Attack* Weapons::tether_get(Fighter& fighter, int key, bool isOffhand)
|
||||
Attack* Weapons::tether_get(Fighter& fighter, int key, int hand)
|
||||
{
|
||||
for (Attack& attack : attacks)
|
||||
{
|
||||
AttackKind kind = attack.def()->kind;
|
||||
if ((kind == ATTACK_WHIP || kind == ATTACK_HOOK) && attack.owner == fighter.index && attack.key == key &&
|
||||
attack.isOffhand == isOffhand && !attack.isCopy && !attack.isDead)
|
||||
attack.hand == hand && !attack.isCopy && !attack.isDead)
|
||||
return &attack;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The fighter's own attacks it can stand on or bump into: stuck arrows and charged boomerangs in flight.
|
||||
std::vector<Rect> Weapons::solids_get(Fighter& fighter)
|
||||
// Adds the fighter's own attacks it can stand on to the world for its next move: charged boomerangs in flight as
|
||||
// obstacles it bumps into, and stuck arrows (until they start fading) as ledges it can only land on from above (and
|
||||
// drop through).
|
||||
void Weapons::solids_add(Fighter& fighter, World& world)
|
||||
{
|
||||
std::vector<Rect> solids;
|
||||
for (Attack& attack : attacks)
|
||||
{
|
||||
if (attack.owner != fighter.index || attack.isDead)
|
||||
@@ -1829,17 +1843,18 @@ std::vector<Rect> Weapons::solids_get(Fighter& fighter)
|
||||
if (kind == ATTACK_BOOMERANG && attack.isCharged)
|
||||
{
|
||||
float size = BOOMERANG_RADIUS * attack.scale;
|
||||
solids.push_back({attack.position.x - size, attack.position.y - size, size * 2, size * 2});
|
||||
world.obstacles.push_back({attack.position.x - size, attack.position.y - size, size * 2, size * 2});
|
||||
}
|
||||
if (kind == ATTACK_ARROW && attack.isAttached)
|
||||
// A stuck arrow's ledge is the box around its hit shape.
|
||||
if (kind == ATTACK_ARROW && attack.isAttached && attack.time < attack.duration - ARROW_FADE)
|
||||
{
|
||||
Vec2 middle = attack.position - vec2_polar(attack.angle, ARROW_SPAN / 2);
|
||||
Vec2 half = {std::abs(math_cos(attack.angle)) * ARROW_SPAN / 2 + ARROW_THICKNESS,
|
||||
std::abs(math_sin(attack.angle)) * ARROW_SPAN / 2 + ARROW_THICKNESS};
|
||||
solids.push_back({middle.x - half.x, middle.y - half.y, half.x * 2, half.y * 2});
|
||||
Vec2 low = {std::min(attack.position.x, attack.tip.x), std::min(attack.position.y, attack.tip.y)};
|
||||
Vec2 high = {std::max(attack.position.x, attack.tip.x), std::max(attack.position.y, attack.tip.y)};
|
||||
float radius = attack.radius;
|
||||
world.ledges.push_back(
|
||||
{low.x - radius, low.y - radius, high.x - low.x + radius * 2, high.y - low.y + radius * 2});
|
||||
}
|
||||
}
|
||||
return solids;
|
||||
}
|
||||
|
||||
// Whether the fighter has a flail spinning around it.
|
||||
|
||||
Reference in new issue
Block a user