From f4e7784f3c80ebd24fe82266802f63d734cd6106 Mon Sep 17 00:00:00 2001 From: shweet Date: Tue, 10 Nov 2026 18:39:56 -0500 Subject: [PATCH] Training dummy leans with knockback, blast pushes, Vacuum suction and gusts Hits tip it by their actual knockback (shared with fighters through knock_get), blasts push it, a Vacuum pulls it toward its wielder and an airblast shoves it away. Blocks stay still. Protocol 7. --- src/fighter.cpp | 16 +++++++++++----- src/fighter.h | 3 +++ src/protocol.h | 2 +- src/weapon.cpp | 44 +++++++++++++++++++++++++++++++++++++------- 4 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/fighter.cpp b/src/fighter.cpp index 199a177..7f4da55 100644 --- a/src/fighter.cpp +++ b/src/fighter.cpp @@ -127,6 +127,15 @@ void Fighter::state_reset(Sim& sim, float maxHealth) isAlive = true; } +// The knock a hit gives whatever it lands on, centred at center: away from where it came from (and a little upward), at +// a speed that grows with the damage, times the hit's own knock. +Vec2 knock_get(Damage& damage, Vec2 center) +{ + Vec2 away = center - damage.source; + Vec2 direction = vec2_normalize(vec2_normalize(vec2_length(away) > 1 ? away : Vec2{0, -1}) + Vec2{0, -KNOCK_LIFT}); + return direction * std::min(KNOCK_MAX, KNOCK_BASE + damage.amount * KNOCK_PER_DAMAGE) * damage.knock; +} + // 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 (see knock_take). Shock Absorber softens it. Running out of health kills it // in a burst, and its attacks vanish with it. @@ -138,11 +147,8 @@ void Fighter::damage_take(Sim& sim, Damage damage) flash = 1; hitstop_add(damage.hitstop); - 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 * - (isRacing ? RACE_KNOCK : 1) * stat_get(STAT_KNOCKBACK_TAKEN); - knock_take(direction * knock, damage.owner); + float taken = (isRacing ? RACE_KNOCK : 1) * stat_get(STAT_KNOCKBACK_TAKEN); + knock_take(knock_get(damage, center_get()) * taken, damage.owner); if (health > 0) return; die(sim); diff --git a/src/fighter.h b/src/fighter.h index 30f71e4..8b649e5 100644 --- a/src/fighter.h +++ b/src/fighter.h @@ -98,6 +98,9 @@ constexpr float CHARGE_VISIBLE = 0.15f; // knock it about (half as hard), and touching a pit puts it back on the last ground it stood squarely on. Slopes slow // it climbing and speed it up descending. Crouching on one slides it down, rolling, and the slide carries on over flat // ground until it slows. +struct Damage; +Vec2 knock_get(Damage& damage, Vec2 center); + class Fighter { friend class Scene; diff --git a/src/protocol.h b/src/protocol.h index 653e4bc..0e83dae 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -27,7 +27,7 @@ #include #include -constexpr int PROTOCOL_VERSION = 6; +constexpr int PROTOCOL_VERSION = 7; constexpr int LOBBY_MEMBERS_MAX = 16; constexpr int LOBBY_CODE_LENGTH = 4; // Letters only, without I and O, so codes are easy to read out and type. diff --git a/src/weapon.cpp b/src/weapon.cpp index dca43e0..6755609 100644 --- a/src/weapon.cpp +++ b/src/weapon.cpp @@ -85,7 +85,10 @@ constexpr float WHIP_WOBBLES = 1; constexpr float WHIP_DAMPING = 0.02f; constexpr float WHIP_SAG = 0.6f; constexpr float WHIP_PULL = 220; -constexpr float WOBBLE_IMPULSE = 0.15f; +// How far a push tips a dummy: its sideways speed into lean speed for a sudden push (a hit or a blast), and its +// sideways acceleration into lean acceleration for a steady one (a Vacuum's pull). +constexpr float WOBBLE_PER_SPEED = 0.005f; +constexpr float WOBBLE_PER_PULL = 0.05f; // Vacuum: how fast its pull speeds fighters and shots up toward it, how much of an airblast's knock is left at the edge // of its reach, and how much of it shoves shots. constexpr float VACUUM_PULL_RATE = 900; @@ -1607,6 +1610,14 @@ void Weapons::attack_hit(Sim& sim, Attack& attack, int target, Vec2 point) }); } +// Tips a dummy the way a sudden push goes (a block, which never leans, ignores it), by its sideways part: a dummy +// stands on its stand, so only its lean feels the push. +static void dummy_push(Dummy& dummy, Vec2 push) +{ + if (dummy.tile < 0) + dummy.wobbleSpeed += push.x * WOBBLE_PER_SPEED; +} + // Hurts a target with all the feedback: sparks, a number, shake that grows with the damage and a sound. The attacker // freezes in hitstop, its Boxing Glove adds to the knock, and its Embiggener to the damage. // Dummies flash and wobble, blocks crack and break, and fighters take the damage themselves. In a race, fighters only @@ -1627,7 +1638,7 @@ void Weapons::damage_deal(Sim& sim, Damage damage) { Dummy& dummy = sim.world.dummies[damage.target - MAX_FIGHTERS]; dummy.flash = 1; - dummy.wobbleSpeed += side * WOBBLE_IMPULSE * damage.amount; + dummy_push(dummy, knock_get(damage, {box.x + box.w / 2, box.y + box.h / 2})); dummy.health -= damage.amount; if (dummy.tile >= 0 && dummy.health <= 0) sim.world.block_break(sim, dummy, damage.owner); @@ -2052,6 +2063,13 @@ void Weapons::blast_spawn(Sim& sim, Blast blast) vec2_normalize(distance > 0 ? away : Vec2{0, -1}) * KNOCKBACK_SPEED * (1 - distance / reach) * absorb; fighter.impulse_add(push); } + for (Dummy& dummy : sim.world.dummies) + { + Vec2 away = Vec2{dummy.box.x + dummy.box.w / 2, dummy.box.y + dummy.box.h / 2} - blast.position; + float distance = vec2_length(away); + if (blast.isPushing && distance < reach) + dummy_push(dummy, vec2_normalize(away) * KNOCKBACK_SPEED * (1 - distance / reach) * boxing); + } Attack attack = { .stats = {.damage = blast.damage, .range = blast.area}, @@ -2283,6 +2301,15 @@ void Weapons::vacuum_move(Sim& sim, Attack& attack) 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. float interval = fighter.level_get(attack.key).cooldown / fighter.stat_get(STAT_ATTACK_RATE); bool isPulse = attack.hitCooldown <= 0; @@ -2323,13 +2350,16 @@ void Weapons::gust_move(Sim& sim, Attack& attack) if (!is_target(sim, target, attack.owner) || !is_in_cone(attack, center)) continue; attack_hit(sim, attack, target, center); - if (target >= MAX_FIGHTERS) - continue; // Boxing Glove adds to it as to any knock. + Vec2 push = vec2_normalize(center - attack.position) * attack.stats.speed * cone_strength_get(attack, center) * + sim.fighters[attack.owner].stat_get(STAT_KNOCKBACK); + if (target >= MAX_FIGHTERS) + { + dummy_push(sim.world.dummies[target - MAX_FIGHTERS], push); + continue; + } Fighter& fighter = sim.fighters[target]; - float strength = attack.stats.speed * cone_strength_get(attack, center) * - fighter.stat_get(STAT_KNOCKBACK_TAKEN) * sim.fighters[attack.owner].stat_get(STAT_KNOCKBACK); - fighter.knock_take(vec2_normalize(center - attack.position) * strength, 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); }