source-sdk-2013 icon indicating copy to clipboard operation
source-sdk-2013 copied to clipboard

[TF2] Fix extended melee range while charging not working

Open Kajek777 opened this issue 1 month ago • 17 comments

This pull request attempts to fix the broken intended mechanic of extending the range of melee attacks while charging with one of Demoman's shields not applying to attacks that begun in the middle of the charge's duration.

Within the GetSwingRange() function, there is a check whether the player is charging or not that extends the range of the melee attack to 128 hammer units if they are. However, before this function is called, the start of the attack ends any ongoing charge, so this check almost always fails unless the attack was started before the charge was. I tried adding an additional condition to that check that activates when a charge was ended 250ms ago or sooner. I understand the implementation might not be ideal, so any criticism and/or improvements are welcome.

Kajek777 avatar Nov 04 '25 22:11 Kajek777

Rather than this kind of implementation, I think there are two options to take for this kind of fix:

  1. In CTFWearableDemoShield::EndSpecialAction, CalcChargeCrit is setting some value on the player so that their next hit is a crit, and you can co-opt that value to also use for the extended melee range (so MELEE_CRIT, MELEE_MINICRIT, or MELEE_RANGE, where all 3 grant the bonus range in GetSwingRange). Arguably, based on the (seemingly unused?) variable m_bCurrentAttackIsDuringDemoCharge in CTFWeaponBaseMelee::PrimaryAttack, you probably don't even have to add MELEE_RANGE and just make it extend range if it's able to minicrit or crit. If you want, I can make a commit (or a PR) for this implementation.
  2. In CTFWeaponBaseMelee::PrimaryAttack you can save the charging status before it calls EndClassSpecialSkill, then add a parameter to Swing() that can increase the swing size. That seems a bit more involved than the other option, though.

In either case, a cvar to enable or disable this for balance purposes would probably be useful, since it does affect balance.

FlaminSarge avatar Nov 04 '25 23:11 FlaminSarge

Thank you for this explanation, feel free to make any changes that are better than the abomination i tried to do (i'm not too familiar with the source engine (or github for that matter))

Kajek777 avatar Nov 05 '25 06:11 Kajek777

this is a pretty strange implementation - this creates a new thread every time a demoknight swings their melee while charging - this measures real-world time, not the engine curtime - this completely bypasses prediction - the server will probably crash if the player disconnects during that 250ms interval

the implementations suggested by @FlaminSarge are a lot better

if you want to implement a timer on the player or a weapon, you should just create a (probably predicted or networked) field that gets checked in a think/update function (like ItemPostFrame)

see how that pattern is used for the melee swing-to-smack time for example:

https://github.com/ValveSoftware/source-sdk-2013/blob/fa288c4c013db2cb365fe328ec25dabccfb35208/src/game/shared/tf/tf_weaponbase_melee.cpp#L322

https://github.com/ValveSoftware/source-sdk-2013/blob/fa288c4c013db2cb365fe328ec25dabccfb35208/src/game/shared/tf/tf_weaponbase_melee.cpp#L792-L795

https://github.com/ValveSoftware/source-sdk-2013/blob/fa288c4c013db2cb365fe328ec25dabccfb35208/src/game/shared/tf/tf_weaponbase_melee.cpp#L374-L380

though i personally wouldn't use a timer for this, the bug happens because swinging ends the charge immediately, so i would probably set a predicted bool in Swing() that tracks if the swing happened while charging or not, then just check that in GetSwingRange()

and also yes, this seems like a pretty significant balance change for the demoknight

wgetJane avatar Nov 05 '25 06:11 wgetJane

I redid the implementation according to @FlaminSarge's suggestion, using the function IsCurrentAttackDuringDemoCharge(), as well as added a convar tf_shield_charge_range_extend that disables the added check when set to 0.

Kajek777 avatar Nov 05 '25 16:11 Kajek777

I redid the implementation according to @FlaminSarge's suggestion, using the function IsCurrentAttackDuringDemoCharge(), as well as added a convar tf_shield_charge_range_extend that disables the added check when set to 0.

Nice work. And ha, didn't realize they already had a helper method for the bool, nice find.

FlaminSarge avatar Nov 05 '25 18:11 FlaminSarge

this is MASSIVELY improved since last i saw this pr

only things i can think of:

  • maybe m_bCurrentAttackIsDuringDemoCharge should be a predicted networked var? (very minor, not really necessary)
  • this seems to apply the fix by default, which again is a significant balance change imo

i'm personally not opposed to this change, but it's possible that many may not like it and start complaining about demoknight's melee range buff

wgetJane avatar Nov 06 '25 11:11 wgetJane

The current iteration of the PR would theoretically make melee hits impossible to connect by default if one were to swing before charging (the if statement still passing if the player simply has condition TF_COND_SHIELD_CHARGE) due to the range now being 0 by default. This would particularly break melee weapons for MvM bots equipped with the attribute class attack_not_cancel_charge, as the attribute will allow demoknights to keep charging while swinging; most commonly used by Samurai Demoknights.

Marxvee avatar Nov 06 '25 17:11 Marxvee

Would something like this be acceptable?

if ( pOwner && ( pOwner->m_Shared.InCond( TF_COND_SHIELD_CHARGE ) || ( IsCurrentAttackDuringDemoCharge() && tf_shield_charge_melee_range.GetInt() ) ) )
{
    if (tf_shield_charge_melee_range.GetInt())
    {
	    return tf_shield_charge_melee_range.GetInt();
    }
    else
    {
	    return 128;
    }

Kajek777 avatar Nov 06 '25 17:11 Kajek777

That seems to work out!

Marxvee avatar Nov 06 '25 19:11 Marxvee

Honestly, at this point I'd split it into two conditions for clarity:

if ( pOwner && pOwner->m_Shared.InCond( TF_COND_SHIELD_CHARGE ) )
{
	// mvm bots that can attack while charging
	return 128;
} else if ( IsCurrentAttackDuringDemoCharge() && tf_shield_charge_melee_range.GetInt() )
{
	// maybe don't repeat the cvar call, somehow, but not that important
	return tf_shield_charge_melee_range.GetInt();
} else {
...
}

As for the '0' case, I'd say people can just use value '1' instead, so not that big a deal to have '0' as 'off'. They do it for m_flMaxSpeed like that anyways.

FlaminSarge avatar Nov 07 '25 01:11 FlaminSarge

( Potentially keep the codestyle the same as the original code, with all the extra spaces in the parentheses )

Also... does this break if you set the cvar to a negative value? Since mods can sometimes break cvar bounds, might be worth sanity checking, but if something funny and not-crashy happens when you set it negative, you could also leave it in as a 'feature'.

FlaminSarge avatar Nov 07 '25 07:11 FlaminSarge

Setting it to -1 made all attacks made during the charge impossible to hit.

I could change it from if ( IsCurrentAttackDuringDemoCharge() && tf_shield_charge_melee_range.GetInt() ) to if ( IsCurrentAttackDuringDemoCharge() && tf_shield_charge_melee_range.GetInt() > 0 ) to count negative values as disabling it as well.

Kajek777 avatar Nov 07 '25 07:11 Kajek777

While doing some extra testing, I've noticed that the extra range is only added when I attack after shield bashing something, and not when attacking while charging, and I can't figure out why.

Kajek777 avatar Nov 07 '25 17:11 Kajek777

Setting it to -1 made all attacks made during the charge impossible to hit.

What about -128 (or -900 or something silly)?

And yeah that way of doing a negative check is probably sane.

For the bash behavior, it's possible you have to edit the other location where m_bCurrentAttackIsDuringDemoCharge is getting set, too.

FlaminSarge avatar Nov 08 '25 01:11 FlaminSarge

Hi,

I'm not a coder, but I am concerned about gameplay ramifications.

  1. Sniping enemy players with crits could be really annoying for the victim, especially if the Demoknight player has high ping values
  2. If this fixes the "extender" bug where swings made before charging provides the range boost, this would remove a niche and balanced alternative to medium-range minicrit charges (was viewed as fine - give up crit-boost for a faster hit)
  3. Even if it doesn't fix "extender", constant access to range-boost irrespective of crit status would mostly diminish the strategy anyway, making Demoknight simpler to play and removing nuance

Could be a potential Thermal Thruster stomp controversy repeat if implemented, though mostly from non-demoknight players dying to criticals

SolarLightTF2 avatar Nov 08 '25 04:11 SolarLightTF2

I was trying to do this in a way that keeps the old extender (after all, mvm samurai bots make use of it) and that can be enabled and disabled at the server owner's will with the tf_shield_charge_melee_range console variable, so if it gets implemented, Valve can choose to keep it disabled in casual servers.

Kajek777 avatar Nov 08 '25 07:11 Kajek777

Seems to be working fine now; nothing changes when cvar set to a negative value or 0 (the default), range extends properly when set to positive number (tested with 128, 1000 and 10000), original behaviour when swinging before charging is preserved with both settings

Kajek777 avatar Nov 10 '25 14:11 Kajek777