A Mythic perk class 'PerkMoveDrainHealth' named "Death Pace" that damages the player when they move a certain distance and increases overall damage multiplier. It tracks distance moved, applies periodic self-damage when thresholds are passed, updates a display cooldown and HP regen display modifier, and sets highlight/visual properties.
using System;
using Sandbox;
[Perk( Rarity.Mythic, locked: true, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.SelfDmg })]
public class PerkMoveDrainHealth : Perk
{
private enum Mod { Distance, OverallDamageMultiplier };
private float _currDamage;
private float _currDist;
static PerkMoveDrainHealth()
{
Register<PerkMoveDrainHealth>(
name: "Death Pace",
imagePath: "textures/icons/vector/move_drain_health.png",
description: level => $"+{GetValue( level, Mod.OverallDamageMultiplier, true )}% dmg\n-1 hp when you move {GetValue( level, Mod.Distance, true ).ToString( "0.#" )}m",
upgradeDescription: level => $"+{GetValue( level - 1, Mod.OverallDamageMultiplier, true )}%→{GetValue( level, Mod.OverallDamageMultiplier, true )}% dmg\n-1 hp when you move [-]{GetValue( level - 1, Mod.Distance, true ).ToString( "0.#" )}→{GetValue( level, Mod.Distance, true ).ToString( "0.#" )}m[/-]"
);
}
public override void Start()
{
base.Start();
ShouldUpdate = true;
DisplayCooldownColor = new Color( 1f, 0f, 0f, 0.5f );
HighlightColor = new Color( 1f, 0f, 0f );
HighlightDuration = 0.25f;
HighlightOpacity = 3f;
}
public override void Refresh()
{
base.Refresh();
Player.Modify( this, PlayerStat.OverallDamageMultiplier, GetValue( Level, Mod.OverallDamageMultiplier ), ModifierType.Mult );
}
private static float GetValue( int level, Mod mod, bool isPercent = false )
{
switch ( mod )
{
case Mod.Distance:
default:
return (1.5f - 0.3f * level) * (isPercent ? 1f : Utils.Meter2Unit);
case Mod.OverallDamageMultiplier:
return isPercent
? 7f + 7f * level
: 1f + (0.07f + 0.07f * level);
}
}
public override void Update( float dt )
{
base.Update( dt );
bool isCurrentlyDraining = false;
if ( Player.TotalVelocity.LengthSquared > 0.1f )
{
float vel = Player.TotalVelocity.Length;
_currDist += vel * dt;
float damage = 1;
var distReq = GetValue( Level, Mod.Distance );
if ( _currDist > distReq )
{
if ( !(Player.IsInvincible && Player.Stats[PlayerStat.IgnoreSelfDamageWhenInvuln] > 0f) )
{
_currDamage += damage;
if ( _currDamage > 0.85f )
{
Player.Damage( _currDamage, DamageType.Self, Player.Position2D, Utils.GetRandomVector(), upwardAmount: 0f, force: 0f, ragdollForce: 1f, enemySource: null, enemyType: EnemyType.None );
float percent = Utils.Map( Player.Health, Player.Stats[PlayerStat.MaxHp], 0f, 0f, 1f );
//Manager.Instance.PlaySfxNearbyRpc( "zombie.attack.player", Player.Position2D, pitch: Utils.Map( percent, 0f, 1f, 1.4f, 1.75f, EasingType.QuadIn ), volume: Utils.Map( percent, 0f, 1f, 0.25f, 0.5f, EasingType.QuadIn ), maxDist: 180f );
_currDamage = 0f;
Highlight();
}
}
_currDist -= distReq;
DisplayCooldown = 0f;
}
else
{
DisplayCooldown = Utils.Map( _currDist, 0f, distReq, 0f, 1f );
}
// todo: average this out, so the value doesn't change so rapidly
Player.Modify( this, PlayerStat.HpRegenDisplay, -damage * vel * 0.02f, ModifierType.Add );
isCurrentlyDraining = true;
}
if ( !isCurrentlyDraining || Player.TotalVelocity.LengthSquared < 0.25f )
Player.StopModifying( this, PlayerStat.HpRegenDisplay );
}
}