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
+73
-57
@@ -3,7 +3,7 @@
|
||||
#include <algorithm>
|
||||
|
||||
constexpr float AIR_ACCELERATION = 400;
|
||||
constexpr float BOUNCE_SHAKE = 0.0005f;
|
||||
constexpr float BOUNCE_SHAKE = 0.00025f;
|
||||
constexpr float SLAM_DAMAGE = 0.06f;
|
||||
constexpr float KNOCK_BASE = 60;
|
||||
constexpr float KNOCK_LIFT = 0.35f;
|
||||
@@ -17,11 +17,12 @@ constexpr float CLING_SPEED = 60;
|
||||
constexpr float COYOTE_TIME = 0.1f;
|
||||
constexpr float DASH_COOLDOWN = 0.8f;
|
||||
constexpr float DASH_TIME = 0.12f;
|
||||
constexpr float DEATH_SHAKE = 1.5f;
|
||||
constexpr float DEATH_SHAKE = 0.75f;
|
||||
constexpr float FACING_DEADZONE = 4;
|
||||
constexpr float FAST_FALL_GRAVITY = 2;
|
||||
constexpr float FLASH_DECAY = 8;
|
||||
constexpr float FAST_FALL_SPEED = 700;
|
||||
constexpr float FALL_EASE = 2000;
|
||||
constexpr float GROUND_ACCELERATION = 500;
|
||||
constexpr float GROUND_FRICTION = 3000;
|
||||
constexpr float JUMP_BUFFER = 0.1f;
|
||||
@@ -34,13 +35,13 @@ constexpr float ROPE_MIN = 24;
|
||||
constexpr float SLIDE_DUST_INTERVAL = 0.08f;
|
||||
constexpr float SWING_ACCELERATION = 500;
|
||||
constexpr float SWING_SPEED_MAX = 420;
|
||||
constexpr float POUND_AREAS[LEVEL_COUNT] = {30, 40, 50};
|
||||
constexpr float POUND_AREAS[LEVEL_COUNT] = {15, 20, 25};
|
||||
constexpr float POUND_DAMAGES[LEVEL_COUNT] = {20, 35, 50};
|
||||
constexpr float POUND_SCALE_MAX = 3;
|
||||
constexpr float POUND_SCALE_MIN = 0.3f;
|
||||
constexpr float POUND_SPEED = 700;
|
||||
constexpr Vec2 HAND_OFFSET = {4, 2};
|
||||
constexpr Vec2 OFFHAND_OFFSET = {-3, 4};
|
||||
constexpr Vec2 OFFHAND_FAN = {3, 2};
|
||||
constexpr float WALL_IMPACT_SPEED = 200;
|
||||
constexpr float WALL_LIFT = 0.25f;
|
||||
constexpr float WALL_MEMORY = 0.6f;
|
||||
@@ -118,15 +119,14 @@ void Fighter::state_reset(Sim& sim, float maxHealth)
|
||||
std::fill(std::begin(cooldowns), std::end(cooldowns), 0.0f);
|
||||
flash = 0;
|
||||
health = maxHealth;
|
||||
offSlot = -1;
|
||||
std::fill(std::begin(slotSwitches), std::end(slotSwitches), 0);
|
||||
slot = 0;
|
||||
isAlive = true;
|
||||
}
|
||||
|
||||
// Loses health with a flash and hitstop, and gets knocked away from where the hit came from (and a little upward),
|
||||
// up to a speed that grows with the damage. Knocks top up rather than stack, so rapid small hits can't juggle. A hard
|
||||
// enough knock launches it. Shock Absorber softens it. Running out of health kills it in a burst, and its attacks
|
||||
// vanish with it.
|
||||
// up to a speed that grows with the damage (see knock_take). Shock Absorber softens it. Running out of health kills it
|
||||
// in a burst, and its attacks vanish with it.
|
||||
void Fighter::damage_take(Sim& sim, Damage damage)
|
||||
{
|
||||
// Races and the sandbox never cost health.
|
||||
@@ -138,11 +138,8 @@ void Fighter::damage_take(Sim& sim, Damage damage)
|
||||
Vec2 away = center_get() - damage.source;
|
||||
Vec2 direction = vec2_normalize(vec2_normalize(vec2_length(away) > 1 ? away : Vec2{0, -1}) + Vec2{0, -KNOCK_LIFT});
|
||||
float knock = std::min(KNOCK_MAX, KNOCK_BASE + damage.amount * KNOCK_PER_DAMAGE) * damage.knock *
|
||||
(isCrouching ? CROUCH_DAMP : 1) * (isRacing ? RACE_KNOCK : 1) * knock_absorb_get();
|
||||
float along = vec2_dot(body.velocity, direction);
|
||||
impulse_add(direction * std::max(0.0f, knock - along), false);
|
||||
launcher = damage.owner;
|
||||
isLaunched |= knock >= LAUNCH_START_SPEED;
|
||||
(isRacing ? RACE_KNOCK : 1) * knock_absorb_get();
|
||||
knock_take(direction * knock, damage.owner);
|
||||
if (health > 0)
|
||||
return;
|
||||
|
||||
@@ -212,12 +209,12 @@ void Fighter::update(Sim& sim)
|
||||
rope_update(*hook);
|
||||
|
||||
bool wasGrounded = body.isGrounded;
|
||||
bool isPounding = is_down(CONTROL_DOWN) && is_trinket_equipped(TRINKET_WEIGHTS);
|
||||
float fallSpeed = body.velocity.y;
|
||||
float runSpeed = body.velocity.x;
|
||||
sim.world.obstacles = sim.weapons.solids_get(*this);
|
||||
sim.weapons.solids_add(*this, sim.world);
|
||||
sim.world.body_move(body);
|
||||
sim.world.obstacles.clear();
|
||||
sim.world.ledges.clear();
|
||||
if (isLaunched)
|
||||
bounce_resolve(sim, {runSpeed, fallSpeed});
|
||||
safe_update(sim);
|
||||
@@ -240,13 +237,14 @@ void Fighter::update(Sim& sim)
|
||||
sim.cues->sound_play(SOUND_LAND);
|
||||
sim.cues->burst_spawn(LAND_DUST, feet, 0, index);
|
||||
}
|
||||
// Iron Weights slam: the harder the landing, the harder the blast. Damage follows the landing speed, and the
|
||||
// radius its square root.
|
||||
if (body.isGrounded && !wasGrounded && fallSpeed > LAND_SPEED && isPounding)
|
||||
// Iron Weights slam: landing faster than a plain fall can go (on anything, platforms too), and the harder the
|
||||
// landing, the harder the blast. Every copy adds to its damage and radius. Damage follows the landing speed, and
|
||||
// the radius its square root.
|
||||
if (body.isGrounded && !wasGrounded && fallSpeed > MAX_FALL && is_trinket_equipped(TRINKET_WEIGHTS))
|
||||
{
|
||||
float impact = std::clamp(fallSpeed / POUND_SPEED, POUND_SCALE_MIN, POUND_SCALE_MAX);
|
||||
float impact = std::min(fallSpeed / POUND_SPEED, POUND_SCALE_MAX);
|
||||
sim.weapons.blast_spawn(sim, {.position = feet,
|
||||
.area = POUND_AREAS[trinket_level_get(TRINKET_WEIGHTS)] * std::sqrt(impact),
|
||||
.area = trinket_sum_get(TRINKET_WEIGHTS, POUND_AREAS) * std::sqrt(impact),
|
||||
.damage = trinket_sum_get(TRINKET_WEIGHTS, POUND_DAMAGES) * impact,
|
||||
.owner = index,
|
||||
.sound = SOUND_POUND});
|
||||
@@ -295,9 +293,10 @@ void Fighter::update(Sim& sim)
|
||||
buttonsPrevious = controls.buttons;
|
||||
}
|
||||
|
||||
// Horizontal control and gravity. Holding S in the air falls faster, holding W floats with the Feather, and a spinning
|
||||
// flail's pull lightens gravity. Slopes change the top speed, and crouch-slides answer only to the slope. Crouching
|
||||
// on flat ground waddles along slowly.
|
||||
// Horizontal control and gravity. Holding S in the air falls faster (Iron Weights speeding up both how fast it gets
|
||||
// there and how fast it goes), and letting go eases back to a plain fall rather than stopping short. Holding W floats
|
||||
// with the Feather, and a spinning flail's pull lightens gravity. Slopes change the top speed, and crouch-slides answer
|
||||
// only to the slope. Crouching on flat ground waddles along slowly.
|
||||
// Airborne on a rope, moving left and right pumps the swing instead.
|
||||
// Above run speed in the air, launch momentum bleeds off slowly unless steered against. Launched by a hit, there's no
|
||||
// control at all, just gravity and the knock dying down.
|
||||
@@ -320,11 +319,15 @@ void Fighter::run_update(Attack* hook)
|
||||
bool isAirborne = !body.isGrounded && !hook;
|
||||
bool isFastFalling = isAirborne && is_down(CONTROL_DOWN) && !isSliding;
|
||||
bool isFloating = isAirborne && is_down(CONTROL_UP);
|
||||
float gravity = GRAVITY * (isFastFalling ? FAST_FALL_GRAVITY : 1);
|
||||
float weights = isFastFalling ? trinket_get(TRINKET_WEIGHTS, 1) : 1;
|
||||
float gravity = GRAVITY * (isFastFalling ? FAST_FALL_GRAVITY * weights : 1);
|
||||
gravity *= isFloating ? trinket_get(TRINKET_FEATHER, 1) : 1;
|
||||
gravity *= isSpinning ? SPIN_GRAVITY : 1;
|
||||
float maxFall = isFastFalling ? FAST_FALL_SPEED * trinket_get(TRINKET_WEIGHTS, 1) : MAX_FALL;
|
||||
body.velocity.y = std::min(body.velocity.y + gravity * TICK, maxFall);
|
||||
float maxFall = isFastFalling ? FAST_FALL_SPEED * weights : MAX_FALL;
|
||||
if (body.velocity.y > maxFall)
|
||||
body.velocity.y = std::max(maxFall, body.velocity.y - FALL_EASE * TICK);
|
||||
else
|
||||
body.velocity.y = std::min(body.velocity.y + gravity * TICK, maxFall);
|
||||
|
||||
// Sliding: the slope pulls it downhill and little else slows it.
|
||||
if (isSliding)
|
||||
@@ -402,7 +405,7 @@ void Fighter::slot_update(Sim& sim)
|
||||
sim.cues->sound_play(SOUND_SELECT);
|
||||
if (slot != previous && weaponLoadout[previous] >= 0 && weapon_key(previous) != weapon_key(slot))
|
||||
{
|
||||
offSlot = previous;
|
||||
slotSwitches[previous] = ++switches;
|
||||
sim.weapons.attacks_release(*this, ATTACK_FLAIL);
|
||||
}
|
||||
|
||||
@@ -654,15 +657,14 @@ float Fighter::trinket_cooldown_get(int trinket)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The radius an Iron Weights slam would blast if the fighter, pounding, landed at its current speed, or 0 if it isn't
|
||||
// pounding down fast enough to slam at all.
|
||||
// The radius an Iron Weights slam would blast if the fighter landed at its current speed, or 0 if it isn't falling
|
||||
// fast enough to slam at all.
|
||||
float Fighter::pound_area_get()
|
||||
{
|
||||
bool isPounding = is_down(CONTROL_DOWN) && is_trinket_equipped(TRINKET_WEIGHTS);
|
||||
if (!isPounding || body.isGrounded || body.velocity.y <= LAND_SPEED)
|
||||
if (!is_trinket_equipped(TRINKET_WEIGHTS) || body.isGrounded || body.velocity.y <= MAX_FALL)
|
||||
return 0;
|
||||
float impact = std::clamp(body.velocity.y / POUND_SPEED, POUND_SCALE_MIN, POUND_SCALE_MAX);
|
||||
return POUND_AREAS[trinket_level_get(TRINKET_WEIGHTS)] * std::sqrt(impact);
|
||||
float impact = std::min(body.velocity.y / POUND_SPEED, POUND_SCALE_MAX);
|
||||
return trinket_sum_get(TRINKET_WEIGHTS, POUND_AREAS) * std::sqrt(impact);
|
||||
}
|
||||
|
||||
// Moves the fighter's middle to centre, keeping its momentum, without drawing it sliding across (teleports).
|
||||
@@ -681,6 +683,19 @@ void Fighter::impulse_add(Vec2 impulse, bool isDamped)
|
||||
isJumping = false;
|
||||
}
|
||||
|
||||
// A knock from another fighter's hit or blast, softened while crouching. It tops up the speed already going its way
|
||||
// rather than stacking, so rapid small knocks can't juggle, and a hard enough one launches it (that fighter getting
|
||||
// the credit for whatever it bounces into).
|
||||
void Fighter::knock_take(Vec2 knock, int owner)
|
||||
{
|
||||
knock = knock * (isCrouching ? CROUCH_DAMP : 1);
|
||||
float strength = vec2_length(knock);
|
||||
Vec2 direction = vec2_normalize(knock);
|
||||
impulse_add(direction * std::max(0.0f, strength - vec2_dot(body.velocity, direction)), false);
|
||||
launcher = owner;
|
||||
isLaunched |= strength >= LAUNCH_START_SPEED;
|
||||
}
|
||||
|
||||
// A push held briefly without gravity or friction, so it carries even along the ground. Used by dashes and lunges.
|
||||
void Fighter::burst_add(Vec2 impulse)
|
||||
{
|
||||
@@ -827,28 +842,27 @@ bool Fighter::is_released(Control control)
|
||||
return !is_down(control) && buttonsPrevious >> control & 1;
|
||||
}
|
||||
|
||||
// Dual Wield's off-hand weapon slot: the last one switched away from if it still holds a weapon that isn't the one in
|
||||
// hand, otherwise any other slot that does, or -1 if there is none. When weapons stack, a copy of the weapon in hand
|
||||
// is another weapon; otherwise it's the same one.
|
||||
int Fighter::off_slot_get()
|
||||
// Dual Wield's off-hand weapon slots, one per copy, written to slots: the other slots holding weapons, the ones most
|
||||
// recently switched away from first, then any never switched to, in slot order. Returns how many.
|
||||
int Fighter::off_slots_get(int* slots)
|
||||
{
|
||||
int held = weapon_key(slot);
|
||||
int fallback = -1;
|
||||
int count = 0;
|
||||
for (int i = 0; i < weaponSlots; i++)
|
||||
{
|
||||
if (weaponLoadout[i] < 0 || weapon_key(i) == held)
|
||||
continue;
|
||||
if (i == offSlot)
|
||||
return i;
|
||||
fallback = fallback < 0 ? i : fallback;
|
||||
}
|
||||
return fallback;
|
||||
if (weaponLoadout[i] >= 0 && weapon_key(i) != held)
|
||||
slots[count++] = i;
|
||||
std::stable_sort(slots, slots + count, [&](int a, int b) { return slotSwitches[a] > slotSwitches[b]; });
|
||||
return std::min(count, trinket_count(TRINKET_DUAL));
|
||||
}
|
||||
|
||||
int Fighter::off_weapon_get()
|
||||
// The weapon slot a hand holds: the selected one in the main hand (0), and Dual Wield's in the off-hands (1 up), or -1
|
||||
// for an off-hand holding nothing.
|
||||
int Fighter::hand_slot_get(int hand)
|
||||
{
|
||||
int off = off_slot_get();
|
||||
return off < 0 ? -1 : weaponLoadout[off];
|
||||
if (hand == 0)
|
||||
return slot;
|
||||
int slots[SLOT_MAX];
|
||||
return hand <= off_slots_get(slots) ? slots[hand - 1] : -1;
|
||||
}
|
||||
|
||||
// Moves the box alpha of the way through the last tick. Only for drawing.
|
||||
@@ -863,15 +877,17 @@ Vec2 Fighter::center_get()
|
||||
return {body.box.x + body.box.w / 2, body.box.y + body.box.h / 2};
|
||||
}
|
||||
|
||||
Vec2 Fighter::hand_get()
|
||||
// Where a hand holds its weapon: the main hand (0) in front, and Dual Wield's off-hands (1 up) behind the body, the
|
||||
// first just behind it and the rest fanned out further back, alternating up and down.
|
||||
Vec2 Fighter::hand_get(int hand)
|
||||
{
|
||||
return center_get() + Vec2{is_left() ? -HAND_OFFSET.x : HAND_OFFSET.x, HAND_OFFSET.y} * scale;
|
||||
}
|
||||
|
||||
// The far hand, just behind the body, where Dual Wield holds the off-hand weapon.
|
||||
Vec2 Fighter::offhand_get()
|
||||
{
|
||||
return center_get() + Vec2{is_left() ? -OFFHAND_OFFSET.x : OFFHAND_OFFSET.x, OFFHAND_OFFSET.y} * scale;
|
||||
Vec2 offset = HAND_OFFSET;
|
||||
if (hand > 0)
|
||||
{
|
||||
int step = hand / 2;
|
||||
offset = OFFHAND_OFFSET + Vec2{-OFFHAND_FAN.x * step, (hand % 2 == 0 ? -1 : 1) * OFFHAND_FAN.y * step};
|
||||
}
|
||||
return center_get() + Vec2{is_left() ? -offset.x : offset.x, offset.y} * scale;
|
||||
}
|
||||
|
||||
// Full charge pulses white on and off, shared by the charge bar and the held weapon.
|
||||
|
||||
Reference in new issue
Block a user