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
+59
-38
@@ -26,13 +26,12 @@ constexpr const char* CRAWL_ANIMATIONS[] = {"Crawl Right", "Crawl Left"};
|
||||
constexpr float POUND_RING_ALPHA = 0.25f;
|
||||
constexpr const char* WALK_ANIMATIONS[] = {"Walk Right", "Walk Left"};
|
||||
|
||||
constexpr float ARROW_FADE = 0.6f;
|
||||
constexpr Burst AURA_BURST = {.life = 0.3f, .size = 2, .speed = 40, .spread = 0.3f, .count = 1};
|
||||
constexpr float BEAM_INNER = 4;
|
||||
constexpr float BEAM_OUTER = 10;
|
||||
constexpr float BOLT_JITTER = 4;
|
||||
constexpr float BOLT_JITTER = 2;
|
||||
constexpr float BOLT_SEGMENT = 8;
|
||||
constexpr float BOLT_WIDTH = 2;
|
||||
constexpr float BOLT_WIDTH = 1;
|
||||
constexpr float BOOMERANG_SPIN = 20;
|
||||
constexpr float CHAIN_SPACING = 8;
|
||||
// How hard a charging weapon shakes against an attack filled in as far.
|
||||
@@ -238,29 +237,43 @@ static float kick_get(Fighter& fighter)
|
||||
return fighter.kickTo * math_pow(KICK_DECAY, (fighter.kickTime - KICK_SWEEP) / TICK);
|
||||
}
|
||||
|
||||
// How much of a weapon's cooldown is left, from 1 just fired to 0 ready.
|
||||
static float cooldown_left_get(Fighter& fighter, int key)
|
||||
{
|
||||
float total = fighter.level_get(key).cooldown / fighter.trinket_get(TRINKET_GLOVES, 1);
|
||||
return total > 0 ? std::clamp(fighter.cooldowns[key] / total, 0.0f, 1.0f) : 0;
|
||||
}
|
||||
|
||||
// A reloading weapon's pose through its cooldown: once the kick has played out it tips up to stand vertical, dips
|
||||
// down and back like a pump, then swings back to the aim just as it's ready.
|
||||
static void reload_pose(Fighter& fighter, int key, float cooldown, Draw& draw)
|
||||
static void reload_pose(Fighter& fighter, int key, Draw& draw)
|
||||
{
|
||||
if (!WEAPON_DEFS[fighter.weaponLoadout[key]].isReloading || cooldown <= 0)
|
||||
if (!WEAPON_DEFS[fighter.weaponLoadout[key]].isReloading || fighter.cooldowns[key] <= 0)
|
||||
return;
|
||||
float total = fighter.level_get(key).cooldown / fighter.trinket_get(TRINKET_GLOVES, 1);
|
||||
float t = std::clamp((1 - cooldown / total - RELOAD_START) / (1 - RELOAD_START), 0.0f, 1.0f);
|
||||
float t = std::clamp((1 - cooldown_left_get(fighter, key) - RELOAD_START) / (1 - RELOAD_START), 0.0f, 1.0f);
|
||||
float weight = math_sin(PI * t);
|
||||
draw.angle = angle_lerp(draw.angle, -PI / 2, weight);
|
||||
draw.position.y += RELOAD_DROP * math_pow(weight, RELOAD_PUMP);
|
||||
}
|
||||
|
||||
// How far a fighter's weapons fill in with its colour: with the charge as it builds, or the speed bonus if that's more,
|
||||
// then once the charge is ready blinking between full and the speed bonus alone.
|
||||
static float charge_fill_get(Game& game, Fighter& fighter)
|
||||
// How far a fighter's weapons (and crosshair) fill in with its colour: with the charge as it builds, or the speed bonus
|
||||
// if that's more, then once the charge is ready blinking between full and the speed bonus alone.
|
||||
float Scene::charge_fill_get(Fighter& fighter)
|
||||
{
|
||||
float buff = game.sim.weapons.buff_get(fighter);
|
||||
if (fighter.charge >= 1)
|
||||
return game.scene.is_flashing(fighter) ? 1.0f : buff;
|
||||
return is_flashing(fighter) ? 1.0f : buff;
|
||||
return std::max(buff, fighter.charge);
|
||||
}
|
||||
|
||||
// How far a fighter's weapon (by key, or -1 for none) and crosshair shake this frame: harder the further its charge has
|
||||
// got, and from fully charged as the weapon is used, easing to still as its cooldown runs out.
|
||||
Vec2 Scene::weapon_shake_get(Fighter& fighter, int key)
|
||||
{
|
||||
float left = key < 0 ? 0 : cooldown_left_get(fighter, key);
|
||||
return jitter_get(std::max(fighter.charge, left * left) * CHARGE_SHAKE);
|
||||
}
|
||||
|
||||
static bool is_pair(const char* const (&pair)[2], const char* animation)
|
||||
{
|
||||
return animation == pair[0] || animation == pair[1];
|
||||
@@ -313,6 +326,12 @@ void Scene::flash_spawn(Vec2 position, float angle, float size, int owner)
|
||||
game.effects.flash_spawn(position, angle, size, game.color_get(owner));
|
||||
}
|
||||
|
||||
void Scene::ring_spawn(Vec2 position, float radius, int owner)
|
||||
{
|
||||
if (!isMuted)
|
||||
game.effects.ring_spawn(position, radius, game.color_get(owner));
|
||||
}
|
||||
|
||||
void Scene::fighter_afterimage_spawn(Fighter& fighter)
|
||||
{
|
||||
if (!isMuted)
|
||||
@@ -674,7 +693,7 @@ void Scene::attacks_draw()
|
||||
if (kind == ATTACK_HOOK)
|
||||
{
|
||||
// The rope runs from the end of the grapple's base in the hand.
|
||||
Vec2 hand = attack.isOffhand ? fighter.offhand_get() : fighter.hand_get();
|
||||
Vec2 hand = fighter.hand_get(attack.hand);
|
||||
Vec2 base = hand + vec2_polar(vec2_angle(attack.position - hand), GRAPPLE_HANDLE);
|
||||
strength_line_draw(game, attack, {{base, attack.position}, ROPE_OUTER, ROPE_INNER});
|
||||
}
|
||||
@@ -890,7 +909,7 @@ Pose Scene::attack_pose(Attack& attack)
|
||||
// their path, each a little out of step with the rest.
|
||||
bool isFlying = attack.velocity.x != 0 || attack.velocity.y != 0;
|
||||
draw.scale = {attack.scale, attack.scale};
|
||||
draw.angle = isFlying ? attack.angle : PI / 2;
|
||||
draw.angle = isFlying ? attack.angle - PI / 2 : 0;
|
||||
pose = {&effects, "Wisp", draw, (attack.time + attack.slot * WISP_FRAME_STAGGER) * effects.fps};
|
||||
break;
|
||||
}
|
||||
@@ -905,21 +924,21 @@ Pose Scene::attack_pose(Attack& attack)
|
||||
}
|
||||
|
||||
// Draws the selected weapon in the player's hand, aimed at the cursor, unless it is out attacking. It fills in as it
|
||||
// charges (see charge_fill_get) and shakes harder the further the charge has got. A charging mine weapon holds up the
|
||||
// mine it's about to place instead.
|
||||
// charges (see charge_fill_get) and shakes with its charge and cooldown (see weapon_shake_get). A charging mine weapon
|
||||
// holds up the mine it's about to place instead.
|
||||
void Scene::held_draw(Fighter& fighter)
|
||||
{
|
||||
int held = fighter.weapon_key(fighter.slot);
|
||||
if (held < 0 || (game.sim.weapons.is_busy(fighter, held) && !game.sim.weapons.tether_get(fighter, held, false)))
|
||||
if (held < 0 || (game.sim.weapons.is_busy(fighter, held) && !game.sim.weapons.tether_get(fighter, held, 0)))
|
||||
return;
|
||||
|
||||
float side = fighter.is_left() ? -1 : 1;
|
||||
Draw draw = {
|
||||
.color = game.color_get(fighter.index),
|
||||
.position = fighter.hand_get() + jitter_get(fighter.charge * CHARGE_SHAKE),
|
||||
.position = fighter.hand_get() + weapon_shake_get(fighter, held),
|
||||
.scale = {1, side},
|
||||
.angle = vec2_angle(fighter.aim) + kick_get(fighter) * side,
|
||||
.flash = charge_fill_get(game, fighter),
|
||||
.flash = charge_fill_get(fighter),
|
||||
};
|
||||
if (fighter.charge > 0 && WEAPON_DEFS[fighter.weaponLoadout[held]].charged.kind == ATTACK_MINE)
|
||||
{
|
||||
@@ -928,36 +947,38 @@ void Scene::held_draw(Fighter& fighter)
|
||||
game.sprites.effects.draw(game.renderer, "Mine", 0, draw);
|
||||
return;
|
||||
}
|
||||
reload_pose(fighter, held, fighter.cooldowns[held], draw);
|
||||
hand_draw(fighter, held, false, draw);
|
||||
reload_pose(fighter, held, draw);
|
||||
hand_draw(fighter, held, 0, draw);
|
||||
}
|
||||
|
||||
// Dual Wield: the off-hand weapon, a darker shade of the fighter's colour, held in the far hand behind it.
|
||||
// Dual Wield: the off-hand weapons, a darker shade of the fighter's colour, fanned out behind it (see
|
||||
// Fighter::hand_get), the furthest back first so the nearer cover them. Each shakes and reloads like the main one.
|
||||
void Scene::offhand_draw(Fighter& fighter)
|
||||
{
|
||||
int off = fighter.off_slot_get();
|
||||
off = off < 0 ? -1 : fighter.weapon_key(off);
|
||||
if (!fighter.is_trinket_equipped(TRINKET_DUAL) || off < 0 ||
|
||||
(game.sim.weapons.is_busy(fighter, off) && !game.sim.weapons.tether_get(fighter, off, true)))
|
||||
return;
|
||||
|
||||
int offs[SLOT_MAX];
|
||||
float side = fighter.is_left() ? -1 : 1;
|
||||
Draw draw = {
|
||||
.color = color_shade(game.color_get(fighter.index), SECONDARY_SHADE),
|
||||
.position = fighter.offhand_get() + jitter_get(fighter.charge * CHARGE_SHAKE),
|
||||
.scale = {1, side},
|
||||
.angle = vec2_angle(fighter.aim),
|
||||
.flash = charge_fill_get(game, fighter),
|
||||
};
|
||||
reload_pose(fighter, off, fighter.cooldowns[off], draw);
|
||||
hand_draw(fighter, off, true, draw);
|
||||
for (int hand = fighter.off_slots_get(offs); hand >= 1; hand--)
|
||||
{
|
||||
int off = fighter.weapon_key(offs[hand - 1]);
|
||||
if (game.sim.weapons.is_busy(fighter, off) && !game.sim.weapons.tether_get(fighter, off, hand))
|
||||
continue;
|
||||
Draw draw = {
|
||||
.color = color_shade(game.color_get(fighter.index), SECONDARY_SHADE),
|
||||
.position = fighter.hand_get(hand) + weapon_shake_get(fighter, off),
|
||||
.scale = {1, side},
|
||||
.angle = vec2_angle(fighter.aim),
|
||||
.flash = charge_fill_get(fighter),
|
||||
};
|
||||
reload_pose(fighter, off, draw);
|
||||
hand_draw(fighter, off, hand, draw);
|
||||
}
|
||||
}
|
||||
|
||||
// Draws a weapon in a hand. While its whip or hook is out, the hand holds only its base ("<name> Base"), pointing
|
||||
// down the lash or rope.
|
||||
void Scene::hand_draw(Fighter& fighter, int weaponSlot, bool isOffhand, Draw draw)
|
||||
void Scene::hand_draw(Fighter& fighter, int weaponSlot, int hand, Draw draw)
|
||||
{
|
||||
Attack* tether = game.sim.weapons.tether_get(fighter, weaponSlot, isOffhand);
|
||||
Attack* tether = game.sim.weapons.tether_get(fighter, weaponSlot, hand);
|
||||
std::string animation = WEAPON_DEFS[fighter.weaponLoadout[weaponSlot]].name;
|
||||
if (tether)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user