things/Player.cs
using SpriteTools;
using static Manager;
public enum ModifierType { Set, Add, Mult }
public class ModifierData
{
public float value;
public ModifierType type;
public float priority;
public ModifierData( float _value, ModifierType _type, float _priority = 0f )
{
value = _value;
type = _type;
priority = _priority;
}
}
public enum PlayerStat
{
AttackTime, AttackSpeed, ReloadTime, ReloadSpeed, MaxAmmoCount, BulletDamage, BulletForce, Recoil, MoveSpeed, NumProjectiles, BulletSpread, BulletInaccuracy, BulletSpeed, BulletLifetime,
BulletNumPiercing, CritChance, CritMultiplier, LowHealthDamageMultiplier, NumUpgradeChoices, HealthRegen, HealthRegenStill, DamageReductionPercent, PushStrength, CoinAttractRange, CoinAttractStrength, Luck, MaxHp,
NumDashes, DashInvulnTime, DashCooldown, DashProgress, DashStrength, ThornsPercent, ShootFireIgniteChance, FireDamage, FireLifetime, FireSpreadChance, ShootFreezeChance, FreezeLifetime,
FreezeTimeScale, FreezeOnMeleeChance, FreezeFireDamageMultiplier, LastAmmoDamageMultiplier, FearLifetime, FearDamageMultiplier, FearOnMeleeChance, BulletDamageGrow, BulletDamageShrink,
BulletDistanceDamage, NumRerollsPerLevel, FullHealthDamageMultiplier, DamagePerEarlierShot, DamageForSpeed, OverallDamageMultiplier, ExplosionSizeMultiplier, GrenadeVelocity, ExplosionDamageMultiplier,
BulletDamageMultiplier, ExplosionDamageReductionPercent, NonExplosionDamageIncreasePercent, GrenadeStickyPercent, GrenadeFearChance, FearDrainPercent, FearPainPercent, CrateChanceAdditional,
AttackSpeedStill, FearDropGrenadeChance, FrozenShardsNum, NoDashInvuln, BulletFlatDamageAddition, GrenadesCanCrit, BulletHealTeammateAmount, HomingBulletChance, PauseWhileChoosing,
BulletNumBouncing, DashCharm, CharmedEnemyDmgTakenMultiplier, CharmedEnemyDmgDealtMultiplier, BossArrivalTime, RadiusMultiplier, SpecialistStatusAmount, ZoomAmount, MaxFireStacks, XpRepel, HealthPackAmount,
MaxBulletSpread, ShootRandomDirChance, DashRandomDirChance, MoveSelfDmg, MoveSelfDmgReqDist, MoveSelfDmgAmount, SelfDmgDistanceMoved, IncreasedDmgTaken, ReverseControls, SelfCritChance,
}
public enum DamageType { Melee, Ranged, Explosion, Fire, PlayerBullet, Self, Generic, LavaPuddle, FearPain, }
public enum PlayerDamageType { Enemy, Self, Grenade }
public struct CamShakeData
{
public float strength;
public float startTime;
public float time;
public EasingType easingType;
public bool useRealTime;
public CamShakeData( float _strength, float _startTime, float _time, EasingType _easingType, bool _useRealTime )
{
strength = _strength;
startTime = _startTime;
time = _time;
easingType = _easingType;
useRealTime = _useRealTime;
}
}
public class Player : Thing
{
[Property] public GameObject Body { get; set; }
[Property] public GameObject ArrowAimerPrefab { get; set; }
[Property] public GameObject BulletPrefab { get; set; }
[Property] public Sprite EasySprite { get; set; }
[Property] public Sprite Difficulty1Sprite { get; set; }
[Property] public Sprite Difficulty2Sprite { get; set; }
[Property] public Sprite Difficulty3Sprite { get; set; }
[Property] public Sprite Difficulty4Sprite { get; set; }
[Property] public Sprite Difficulty5Sprite { get; set; }
[Property] public Sprite Difficulty6Sprite { get; set; }
[Property] public Sprite Difficulty7Sprite { get; set; }
[Property] public Sprite Difficulty8Sprite { get; set; }
[Property] public Sprite Difficulty9Sprite { get; set; }
[Property] public Sprite Difficulty10Sprite { get; set; }
[Property] public Sprite Difficulty11Sprite { get; set; }
[Property] public Sprite Difficulty12Sprite { get; set; }
[Property] public Sprite Difficulty13Sprite { get; set; }
[Property] public Sprite Difficulty14Sprite { get; set; }
[Property] public Sprite Difficulty15Sprite { get; set; }
[Sync] public float Health { get; set; }
private float _regenHpAccumulated;
private float _drainHpAccumulated;
[Sync] public Vector2 InputVector { get; set; }
public GameObject ArrowAimer { get; private set; }
public SpriteRendererLayer ArrowSprite { get; private set; }
public Vector2 AimDir { get; private set; }
[Sync] public bool IsDead { get; private set; }
public float Timer { get; protected set; }
[Sync] public bool IsReloading { get; protected set; }
[Sync] public float ReloadProgress { get; protected set; }
public const float BASE_MOVE_SPEED = 15f;
private int _shotNum;
[Sync] public int Level { get; protected set; }
public int ExperienceTotal { get; protected set; }
public int ExperienceCurrent { get; protected set; }
public int ExperienceRequired { get; protected set; }
public bool IsChoosingLevelUpReward { get; protected set; }
public TimeSince TimeSinceLevelUp { get; set; }
public RealTimeSince RealTimeSinceChoseUpgrade { get; set; }
public List<Status> LevelUpChoices { get; private set; }
[Sync] public float DashTimer { get; private set; }
[Sync] public bool IsDashing { get; private set; }
[Sync] public Vector2 DashVelocity { get; private set; }
[Sync] public float DashInvulnTimer { get; private set; }
private TimeSince _dashCloudTime;
public float DashProgress { get; protected set; }
[Sync] public float DashRechargeProgress { get; protected set; }
[Sync] public int NumDashesAvailable { get; set; }
public int AmmoCount { get; protected set; }
public bool IsMoving => Velocity.LengthSquared > 0.05f && !IsDashing;
//public bool IsMoving => (Position2D - _lastPos2D).LengthSquared > 0.000001f;
public bool IsInputtingMove => Input.AnalogMove.LengthSquared > 0.01f;
public bool IsInvulnerable => IsDashing && Stats[PlayerStat.NoDashInvuln] <= 0f;
public bool IsTimePausedForChoosing => IsChoosingLevelUpReward && Stats[PlayerStat.PauseWhileChoosing] > 0f;
private float _flashTimer;
private bool _isFlashing;
public TimeSince TimeSinceHurt { get; private set; }
public TimeSince TimeSinceChangeHP { get; set; }
private GameObject _shieldVfx;
[Sync] public int NumRerollAvailable { get; set; }
[Sync] public NetDictionary<PlayerStat, float> Stats { get; private set; } = new();
public Dictionary<int, Status> Statuses { get; private set; }
private Dictionary<Status, Dictionary<PlayerStat, ModifierData>> _modifiers_stat = new Dictionary<Status, Dictionary<PlayerStat, ModifierData>>();
private Dictionary<PlayerStat, float> _original_properties_stat = new Dictionary<PlayerStat, float>();
private bool _doneFirstUpdate;
private TimeSince _timeSinceShoot;
private TimeSince _timeSinceSpawn;
private Vector2 _lastPos2D;
private TimeSince _timeSinceTouchLeftSide;
private bool _hasUnlockedSprinterAchievement;
private bool _hasUnlockedExperiencedAchievement;
public TimeSince TimeSinceInputMove { get; set; }
private bool _hasUnlockedNoMoveAchievement;
private List<CamShakeData> _camShakeDatas = new();
public float CamShakeAmount { get; set; }
public int ChoiceHash { get; set; }
public TimeSince TimeSinceHurtLava { get; set; }
public RealTimeSince RealTimeSinceDeath { get; set; }
private float _arrowDeathAlphaStart;
private bool _hasPlayedDeathSfx;
protected override void OnAwake()
{
base.OnAwake();
Scale = 1f;
ShadowOpacity = 0.8f;
ShadowScale = 1.12f;
Statuses = new Dictionary<int, Status>();
LevelUpChoices = new List<Status>();
InitializeStats();
if ( IsProxy )
return;
CollideWith.Add( typeof( Enemy ) );
CollideWith.Add( typeof( Player ) );
ArrowAimer = ArrowAimerPrefab.Clone( WorldPosition );
ArrowAimer.SetParent( GameObject );
ArrowAimer.NetworkMode = NetworkMode.Never;
ArrowSprite = ArrowAimer.Components.Get<SpriteRendererLayer>();
ArrowSprite.Tint = Color.White.WithAlpha( 0f );
_timeSinceShoot = 999f;
_timeSinceSpawn = 0f;
if ( Manager.Instance.Difficulty < 0 ) Sprite.Sprite = EasySprite;
else if ( Manager.Instance.Difficulty == 1 ) Sprite.Sprite = Difficulty1Sprite;
else if ( Manager.Instance.Difficulty == 2 ) Sprite.Sprite = Difficulty2Sprite;
else if ( Manager.Instance.Difficulty == 3 ) Sprite.Sprite = Difficulty3Sprite;
else if ( Manager.Instance.Difficulty == 4 ) Sprite.Sprite = Difficulty4Sprite;
else if ( Manager.Instance.Difficulty == 5 ) Sprite.Sprite = Difficulty5Sprite;
else if ( Manager.Instance.Difficulty == 6 ) Sprite.Sprite = Difficulty6Sprite;
else if ( Manager.Instance.Difficulty == 7 ) Sprite.Sprite = Difficulty7Sprite;
else if ( Manager.Instance.Difficulty == 8 ) Sprite.Sprite = Difficulty8Sprite;
else if ( Manager.Instance.Difficulty == 9 ) Sprite.Sprite = Difficulty9Sprite;
else if ( Manager.Instance.Difficulty == 10 ) Sprite.Sprite = Difficulty10Sprite;
else if ( Manager.Instance.Difficulty == 11 ) Sprite.Sprite = Difficulty11Sprite;
else if ( Manager.Instance.Difficulty == 12 ) Sprite.Sprite = Difficulty12Sprite;
else if ( Manager.Instance.Difficulty == 13 ) Sprite.Sprite = Difficulty13Sprite;
else if ( Manager.Instance.Difficulty == 14 ) Sprite.Sprite = Difficulty14Sprite;
else if ( Manager.Instance.Difficulty == 15 ) Sprite.Sprite = Difficulty15Sprite;
//Sprite.LocalScale = 0.5f * Globals.SPRITE_SCALE;
}
public void InitializeStats()
{
_original_properties_stat.Clear();
if ( Network.Active )
{
RemoveShieldVfx();
}
else
{
if ( _shieldVfx != null )
{
_shieldVfx.Destroy();
_shieldVfx = null;
}
}
Level = 0;
ExperienceRequired = GetExperienceReqForLevel( Level + 1 );
ExperienceTotal = 0;
ExperienceCurrent = 0;
Stats[PlayerStat.AttackTime] = 0.15f;
AmmoCount = 5;
Stats[PlayerStat.MaxAmmoCount] = AmmoCount;
Stats[PlayerStat.ReloadTime] = 1.5f;
Stats[PlayerStat.ReloadSpeed] = 1f;
Stats[PlayerStat.AttackSpeed] = 1f;
Stats[PlayerStat.BulletDamage] = 5f;
Stats[PlayerStat.BulletForce] = 0.55f;
Stats[PlayerStat.Recoil] = 0f;
Stats[PlayerStat.MoveSpeed] = 1f;
Stats[PlayerStat.NumProjectiles] = 1f;
Stats[PlayerStat.BulletSpread] = 35f;
Stats[PlayerStat.BulletInaccuracy] = 5f;
Stats[PlayerStat.BulletSpeed] = 4.5f;
Stats[PlayerStat.BulletLifetime] = 0.8f;
Stats[PlayerStat.Luck] = 1f;
Stats[PlayerStat.CritChance] = 0.05f;
Stats[PlayerStat.CritMultiplier] = 1.5f;
Stats[PlayerStat.LowHealthDamageMultiplier] = 1f;
Stats[PlayerStat.FullHealthDamageMultiplier] = 1f;
Stats[PlayerStat.ThornsPercent] = 0f;
Stats[PlayerStat.NumDashes] = 1f;
NumDashesAvailable = (int)MathF.Round( Stats[PlayerStat.NumDashes] );
Stats[PlayerStat.DashCooldown] = 3f;
Stats[PlayerStat.DashInvulnTime] = 0.25f;
Stats[PlayerStat.DashStrength] = 3f;
Stats[PlayerStat.BulletNumPiercing] = 0f;
Stats[PlayerStat.BulletNumBouncing] = 0f;
Stats[PlayerStat.DashCharm] = 0f;
Stats[PlayerStat.CharmedEnemyDmgTakenMultiplier] = 1f;
Stats[PlayerStat.CharmedEnemyDmgDealtMultiplier] = 1f;
Stats[PlayerStat.BossArrivalTime] = 15 * 60f;
//Stats[PlayerStat.BossArrivalTime] = (Manager.Instance.Difficulty >= 5 ? 10 : 15) * 60f;
Stats[PlayerStat.RadiusMultiplier] = 1f;
Stats[PlayerStat.SpecialistStatusAmount] = 0f;
Stats[PlayerStat.ZoomAmount] = 0f;
Stats[PlayerStat.MaxFireStacks] = 0f;
Stats[PlayerStat.XpRepel] = 0f;
Stats[PlayerStat.HealthPackAmount] = 20f;
Stats[PlayerStat.MaxBulletSpread] = 0f;
Stats[PlayerStat.ShootRandomDirChance] = 0f;
Stats[PlayerStat.DashRandomDirChance] = 0f;
Stats[PlayerStat.MoveSelfDmg] = 0f;
Stats[PlayerStat.MoveSelfDmgReqDist] = 0f;
Stats[PlayerStat.MoveSelfDmgAmount] = 0f;
Stats[PlayerStat.SelfDmgDistanceMoved] = 0f;
Stats[PlayerStat.IncreasedDmgTaken] = 0f;
Stats[PlayerStat.ReverseControls] = 0f;
Stats[PlayerStat.SelfCritChance] = 0f;
Health = 100f;
Stats[PlayerStat.MaxHp] = 100f;
//Health = 1f;
_regenHpAccumulated = 0f;
_drainHpAccumulated = 0f;
IsDead = false;
Radius = 0.10f;
GridPos = Manager.Instance.GetGridSquareForPos( Position2D );
AimDir = new Vector2( 0f, 1f );
NumRerollAvailable = Manager.Instance.Difficulty < 0 ? 4 : (Manager.Instance.Difficulty >= 3 ? 3 : 2);
Stats[PlayerStat.FireDamage] = 1f;
Stats[PlayerStat.FireLifetime] = 3f;
Stats[PlayerStat.ShootFireIgniteChance] = 0f;
Stats[PlayerStat.FireSpreadChance] = 0f;
Stats[PlayerStat.ShootFreezeChance] = 0f;
Stats[PlayerStat.FreezeLifetime] = 4f;
Stats[PlayerStat.FreezeTimeScale] = 0.55f;
Stats[PlayerStat.FreezeOnMeleeChance] = 0f;
Stats[PlayerStat.FreezeFireDamageMultiplier] = 1f;
Stats[PlayerStat.FearLifetime] = 4.5f;
Stats[PlayerStat.FearDamageMultiplier] = 1f;
Stats[PlayerStat.FearOnMeleeChance] = 0f;
Stats[PlayerStat.CoinAttractRange] = 1.7f;
Stats[PlayerStat.CoinAttractStrength] = 3.5f;
Stats[PlayerStat.NumUpgradeChoices] = 3f;
Stats[PlayerStat.HealthRegen] = Manager.Instance.Difficulty == -1 ? 0.4f : 0f;
Stats[PlayerStat.HealthRegenStill] = 0f;
//Stats[PlayerStat.HealthDrain] = 0f;
Stats[PlayerStat.DamageReductionPercent] = 0f;
Stats[PlayerStat.PushStrength] = 30f;
Stats[PlayerStat.LastAmmoDamageMultiplier] = 1f;
Stats[PlayerStat.BulletDamageGrow] = 0f;
Stats[PlayerStat.BulletDamageShrink] = 0f;
Stats[PlayerStat.BulletDistanceDamage] = 0f;
Stats[PlayerStat.NumRerollsPerLevel] = 1f;
Stats[PlayerStat.DamagePerEarlierShot] = 0f;
Stats[PlayerStat.DamageForSpeed] = 0f;
Stats[PlayerStat.OverallDamageMultiplier] = 1f;
Stats[PlayerStat.ExplosionSizeMultiplier] = 1f;
Stats[PlayerStat.GrenadeVelocity] = 8f;
Stats[PlayerStat.ExplosionDamageMultiplier] = 1f;
Stats[PlayerStat.BulletDamageMultiplier] = 1f;
Stats[PlayerStat.ExplosionDamageReductionPercent] = 0f;
Stats[PlayerStat.NonExplosionDamageIncreasePercent] = 0f;
Stats[PlayerStat.GrenadeStickyPercent] = 0f;
Stats[PlayerStat.GrenadeFearChance] = 0f;
Stats[PlayerStat.FearDrainPercent] = 0f;
Stats[PlayerStat.FearPainPercent] = 0f;
Stats[PlayerStat.CrateChanceAdditional] = 0f;
Stats[PlayerStat.AttackSpeedStill] = 1f;
Stats[PlayerStat.FearDropGrenadeChance] = 0f;
Stats[PlayerStat.FrozenShardsNum] = 0f;
Stats[PlayerStat.NoDashInvuln] = 0f;
Stats[PlayerStat.BulletFlatDamageAddition] = 0f;
Stats[PlayerStat.GrenadesCanCrit] = 0f;
Stats[PlayerStat.BulletHealTeammateAmount] = 0f;
Stats[PlayerStat.HomingBulletChance] = 0f;
Stats[PlayerStat.PauseWhileChoosing] = 0f;
Statuses.Clear();
_modifiers_stat.Clear();
_isFlashing = false;
Sprite.FlashTint = Color.White.WithAlpha( 0f );
Sprite.Tint = Color.White;
IsChoosingLevelUpReward = false;
IsDashing = false;
IsReloading = true;
Timer = Stats[PlayerStat.ReloadTime];
ReloadProgress = 0f;
DashProgress = 0f;
DashRechargeProgress = 1f;
TempWeight = 0f;
_shotNum = 0;
TimeSinceHurt = 999f;
TimeSinceChangeHP = 999f;
TimeSinceHurtLava = 999f;
ShadowOpacity = 0.8f;
ShadowSpriteDirty = true;
_timeSinceTouchLeftSide = 999f;
TimeSinceInputMove = 0f;
TimeSinceLevelUp = 999f;
RealTimeSinceChoseUpgrade = 999f;
_camShakeDatas.Clear();
ChoiceHash = 0;
}
public Vector2 AverageVelocity { get; private set; }
protected override void OnUpdate()
{
base.OnUpdate();
AverageVelocity += Velocity * Time.Delta * 8f;
//AverageVelocity = Utils.DynamicEaseTo( AverageVelocity, Vector2.Zero, 0.02f, Time.Delta );
AverageVelocity *= (1f - Time.Delta * 2.7f);
//Gizmo.Draw.Color = Color.Red.WithAlpha( 0.5f );
//Gizmo.Draw.Line( Position2D, Position2D + AverageVelocity );
//Gizmo.Draw.Color = Color.Red.WithAlpha( 0.5f );
//Gizmo.Draw.Line( Position2D, Position2D + AverageVelocity );
Vector2 anchor = Position2D + AverageVelocity * 0.1f;
Vector2 perp = Utils.GetPerpendicularVector( AverageVelocity ).Normal;
var perpA = anchor - perp * 10f;
var perpB = anchor + perp * 10f;
//Gizmo.Draw.Color = Color.Green.WithAlpha( 0.3f );
//Gizmo.Draw.Line( perpA, perpB );
//Gizmo.Draw.Color = Color.White.WithAlpha( 0.03f );
//Gizmo.Draw.LineSphere( (Vector3)Position2D, 2, 16 );
//Gizmo.Draw.Color = Color.White.WithAlpha( 0.5f );
//Gizmo.Draw.Text( $"{Stats[PlayerStat.FearPainPercent]}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.4f, 0f ) ) );
if ( !_doneFirstUpdate )
{
SpawnShadow( ShadowScale, ShadowOpacity );
Manager.Instance.Camera2D.SetPos( Position2D );
_doneFirstUpdate = true;
}
InputVector = new Vector2( -Input.AnalogMove.y, Input.AnalogMove.x );
if ( Stats[PlayerStat.ReverseControls] > 0f )
InputVector *= -1f;
//if ( Input.Pressed( "Menu" ) )
//{
// Manager.Instance.Restart();
// return;
//}
HandleCamShaking();
if ( IsDead )
{
Sprite.FlashTint = RealTimeSinceDeath < 0.1f
? Color.Red
: Color.White.WithAlpha( 0f );
ShadowOpacity = Utils.Map( RealTimeSinceDeath, 0f, Manager.FINAL_PANEL_WAIT_TIME, 0.8f, 0f, EasingType.Linear );
ShadowSprite.Tint = Color.Black.WithAlpha( ShadowOpacity );
ArrowSprite.Tint = Color.White.WithAlpha( Utils.Map( RealTimeSinceDeath, 0f, 0.4f, _arrowDeathAlphaStart, 0f, EasingType.SineOut ) );
if ( !_hasPlayedDeathSfx && RealTimeSinceDeath > 0.7f )
{
Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Game.Random.Float( 0.55f, 0.65f ), volume: 1f, maxDist: 5.5f );
_hasPlayedDeathSfx = true;
}
}
if ( !Manager.Instance.ShouldUpdatePlayer )
return;
float dt = Time.Delta;
if ( MathF.Abs( Velocity.x ) > 0.01f )
Sprite.SpriteFlags = Velocity.x > 0f ? SpriteFlags.HorizontalFlip : SpriteFlags.None;
bool hurting = TimeSinceHurt < 0.25f;
bool attacking = !IsReloading;
bool moving = Velocity.LengthSquared > 0.01f && InputVector.LengthSquared > 0.1f;
string stateStr = "";
if ( IsDead )
stateStr = "ghost_";
else if ( hurting && attacking )
stateStr = "hurt_attack_";
else if ( hurting )
stateStr = "hurt_";
else if ( attacking )
stateStr = "attack_";
Sprite.PlayAnimation( $"{stateStr}{(moving ? "walk" : "idle")}" );
Sprite.PlaybackSpeed = moving ? Utils.Map( Velocity.Length, 0f, 2f, 1.5f, 2f ) : 0.66f;
Sprite.LocalRotation = new Angles( 0f, -90f + (Velocity.Length * Utils.FastSin( Time.Now * MathF.PI * 6f ) * 1.6f) * (Sprite.SpriteFlags.HasFlag( SpriteFlags.HorizontalFlip ) ? -1f : 1f), 0f );
if ( !IsDead )
{
HandleFlashing( dt );
}
if ( IsProxy )
return;
// ACHIEVEMENTS
if ( Manager.Instance.Difficulty >= 0 )
{
if ( _lastPos2D.x < -15.7f && WorldPosition.x >= -15.7f )
{
_timeSinceTouchLeftSide = 0f;
}
if ( _lastPos2D.x < 15.7f && WorldPosition.x >= 15.7f )
{
Log.Info( $"_timeSinceTouchLeftSide: {_timeSinceTouchLeftSide}" );
if ( _timeSinceTouchLeftSide < 5.5f )
{
if ( !_hasUnlockedSprinterAchievement )
Sandbox.Services.Achievements.Unlock( "sprinter" );
_hasUnlockedSprinterAchievement = true;
}
}
if ( !_hasUnlockedNoMoveAchievement )
{
if ( IsInputtingMove )
{
TimeSinceInputMove = 0f;
}
else if ( TimeSinceInputMove > 60f * 10f )
{
Sandbox.Services.Achievements.Unlock( "stand_ground" );
_hasUnlockedNoMoveAchievement = true;
}
}
}
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"ArrowAimer: {ArrowAimer}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.7f, 0f ) ) );
//Gizmo.Draw.ScreenText( $"_timeSinceTouchLeftSide: {_timeSinceTouchLeftSide}", new Vector2( 50, 50 ) );
_lastPos2D = Position2D;
var velocity = Velocity + (IsDashing ? DashVelocity : Vector2.Zero);
Position2D += velocity * dt;
if ( Stats[PlayerStat.MoveSelfDmg] > 0f )
{
Stats[PlayerStat.SelfDmgDistanceMoved] += velocity.Length * dt;
if ( !IsInvulnerable && Stats[PlayerStat.SelfDmgDistanceMoved] > Stats[PlayerStat.MoveSelfDmgReqDist] )
{
Stats[PlayerStat.SelfDmgDistanceMoved] -= Stats[PlayerStat.MoveSelfDmgReqDist];
Damage( Stats[PlayerStat.MoveSelfDmgAmount], PlayerDamageType.Self );
Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Game.Random.Float( 1.25f, 1.45f ), volume: 0.9f, maxDist: 3f );
}
}
WorldPosition = WorldPosition.WithZ( Globals.GetZPos( Position2D.y ) );
//Velocity = Utils.DynamicEaseTo( Velocity, Vector2.Zero, 0.2f, dt );
Velocity *= Math.Max( 1f - dt * 12.9f, 0f );
if ( Velocity.LengthSquared < 0.001f )
Velocity = Vector2.Zero;
TempWeight *= (1f - dt * 4.7f);
if ( InputVector.LengthSquared > 0f )
{
Velocity += InputVector.Normal * Stats[PlayerStat.MoveSpeed] * BASE_MOVE_SPEED * dt;
//Log.Info( $"dt: {dt}" );
}
HandleBounds();
Manager.Instance.Camera2D.TargetPos = Position2D;
if ( Input.UsingController )
{
}
else
{
AimDir = (Manager.Instance.MouseWorldPos - (Position2D + new Vector2( 0f, 0.5f ))).Normal;
if ( Stats[PlayerStat.ReverseControls] > 0f )
AimDir *= -1f;
}
if ( ArrowAimer != null && !Manager.Instance.IsPauseMenuOpen && !IsTimePausedForChoosing )
{
ArrowAimer.LocalRotation = new Angles( 0f, MathF.Atan2( AimDir.y, AimDir.x ) * (180f / MathF.PI) - 180f, 0f );
ArrowAimer.LocalPosition = new Vector2( 0f, 0.4f ) + AimDir * Utils.Map( _timeSinceShoot, 0f, 0.25f, 0.6f, 0.55f, EasingType.QuadOut );
ArrowAimer.LocalScale = new Vector3( Utils.Map( _timeSinceShoot, 0f, 0.25f, 1.25f, 0.75f, EasingType.QuadOut ), 1f, 1f ) * 0.005f;
ArrowSprite.Tint = Color.White.WithAlpha( Utils.Map( _timeSinceShoot, 0f, 0.3f, 1f, 0.3f, EasingType.QuadOut ) * Utils.Map( _timeSinceSpawn, 0f, 1f, 0f, 1f, EasingType.Linear ) );
}
for ( int dx = -1; dx <= 1; dx++ )
{
for ( int dy = -1; dy <= 1; dy++ )
{
Manager.Instance.HandleThingCollisionForGridSquare( this, new GridSquare( GridPos.x + dx, GridPos.y + dy ), dt );
}
}
if ( !IsDead )
{
HandleDashing( dt );
HandleStatuses( dt );
HandleShooting( dt );
HandleRegen( dt );
}
if ( IsChoosingLevelUpReward && !Manager.Instance.IsPauseMenuOpen )
{
if ( Input.Pressed( "reload" ) ) UseReroll();
else if ( Input.Pressed( "Slot1" ) ) UseChoiceHotkey( 1 );
else if ( Input.Pressed( "Slot2" ) ) UseChoiceHotkey( 2 );
else if ( Input.Pressed( "Slot3" ) ) UseChoiceHotkey( 3 );
else if ( Input.Pressed( "Slot4" ) ) UseChoiceHotkey( 4 );
else if ( Input.Pressed( "Slot5" ) ) UseChoiceHotkey( 5 );
else if ( Input.Pressed( "Slot6" ) ) UseChoiceHotkey( 6 );
}
if ( Input.Pressed( "use" ) )
{
//AddExperience( 10 );
}
}
void HandleRegen( float dt )
{
if ( Math.Abs( Stats[PlayerStat.HealthRegen] ) > 0f )
RegenHealth( Stats[PlayerStat.HealthRegen] * dt );
//if ( Math.Abs( Stats[PlayerStat.HealthDrain] ) > 0f )
// RegenHealth( Stats[PlayerStat.HealthDrain] * dt );
if ( Stats[PlayerStat.HealthRegenStill] > 0f && !IsMoving )
{
RegenHealth( Stats[PlayerStat.HealthRegenStill] * dt );
if ( !IsDashing && Health < Stats[PlayerStat.MaxHp] )
TimeSinceChangeHP = 0f;
}
}
public void RegenHealth( float amount )
{
float maxHp = Stats[PlayerStat.MaxHp];
float hpMissing = maxHp - Health;
if ( amount > 0f )
{
if ( hpMissing <= 0f )
return;
_regenHpAccumulated += amount;
if ( _regenHpAccumulated < 0.85f && hpMissing > _regenHpAccumulated )
return;
float hpRecovered = Math.Min( _regenHpAccumulated, hpMissing );
Health += hpRecovered;
//var particle = DamageNumbersLegacy.Create( hpRecovered, Position2D + new Vector2( 0.2f + Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) ), color: Color.Green, sizeMultiplier: 0.8f );
//Vector3 velocity = new Vector3( Game.Random.Float(-0.5f, 0.5f), 0f, 0f );
//Vector3 gravity = new Vector3( 0f, 1.5f, 0f );
//particle.SetVector( 1, velocity );
//particle.SetNamedValue( "Gravity", gravity );
var pos = Position2D + new Vector2( Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) );
float size = 1.1f;
Manager.Instance.SpawnDamageNumber( pos, amount, Color.Green, size, FloaterType.Heal );
_regenHpAccumulated = 0f;
}
else
{
_drainHpAccumulated += amount;
if ( _drainHpAccumulated < -1f )
{
var dmgAmount = MathF.Truncate( _drainHpAccumulated );
_drainHpAccumulated -= dmgAmount;
Damage( MathF.Abs( dmgAmount ), PlayerDamageType.Self );
Manager.Instance.PlaySfxNearby( "lava_puddle_03", Position2D, pitch: Game.Random.Float( 1.7f, 1.75f ), volume: 0.15f, maxDist: 4f );
}
}
}
void HandleDashing( float dt )
{
int numDashes = (int)MathF.Round( Stats[PlayerStat.NumDashes] );
if ( NumDashesAvailable < numDashes )
{
DashTimer -= dt;
DashRechargeProgress = Utils.Map( DashTimer, Stats[PlayerStat.DashCooldown], 0f, 0f, 1f );
if ( DashTimer <= 0f )
{
DashRecharged();
}
}
if ( DashInvulnTimer > 0f )
{
DashInvulnTimer -= dt;
DashProgress = Utils.Map( DashInvulnTimer, Stats[PlayerStat.DashInvulnTime], 0f, 0f, 1f );
if ( DashInvulnTimer <= 0f )
{
IsDashing = false;
//Sprite.Tint = Color.White;
Sprite.FlashTint = Color.White.WithAlpha( 0f );
DashFinished();
}
else
{
if ( Stats[PlayerStat.DashCharm] > 0f )
{
if ( IsInvulnerable )
Sprite.FlashTint = new Color( Game.Random.Float( 0.8f, 1f ), Game.Random.Float( 0f, 0.1f ), Game.Random.Float( 0.8f, 1f ), 0.9f );
else if ( !_isFlashing )
Sprite.FlashTint = new Color( Game.Random.Float( 0.9f, 1f ), Game.Random.Float( 0.1f, 0.3f ), Game.Random.Float( 0.9f, 1f ), 0.8f );
}
else
{
if ( IsInvulnerable )
Sprite.FlashTint = new Color( Game.Random.Float( 0f, 0.25f ), Game.Random.Float( 0f, 0.25f ), Game.Random.Float( 0.8f, 1f ), 0.9f );
}
if ( _dashCloudTime > Game.Random.Float( 0.1f, 0.2f ) )
{
SpawnDashCloudClient();
_dashCloudTime = 0f;
}
}
}
if ( Input.Pressed( "Jump" ) || Input.Pressed( "attack1" ) )
{
//Position2D = Manager.Instance.MouseWorldPos;
////Manager.Instance.Camera2D.SetPos( Position2D );
//Transform.ClearInterpolation();
//return;
Dash();
}
}
public void Dash()
{
if ( NumDashesAvailable <= 0 || IsTimePausedForChoosing )
return;
Vector2 dashDir = Velocity.LengthSquared > 0f ? Velocity.Normal : AimDir;
if ( Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.DashRandomDirChance] )
dashDir = Utils.GetRandomVector();
DashVelocity = dashDir * Stats[PlayerStat.DashStrength];
TempWeight = 2f;
if ( NumDashesAvailable == (int)Stats[PlayerStat.NumDashes] )
DashTimer = Stats[PlayerStat.DashCooldown];
NumDashesAvailable--;
IsDashing = true;
DashInvulnTimer = Stats[PlayerStat.DashInvulnTime];
DashProgress = 0f;
DashRechargeProgress = 0f;
Manager.Instance.PlaySfxNearby( "player.dash", Position2D + dashDir * 0.5f, pitch: Utils.Map( NumDashesAvailable, 0, 5, 1f, 0.9f ), volume: 1f, maxDist: 4f );
SpawnDashCloudClient();
_dashCloudTime = 0f;
ForEachStatus( status => status.OnDashStarted() );
}
public void DashFinished()
{
ForEachStatus( status => status.OnDashFinished() );
}
public void DashRecharged()
{
NumDashesAvailable++;
var numDashes = (int)MathF.Round( Stats[PlayerStat.NumDashes] );
if ( NumDashesAvailable > numDashes )
NumDashesAvailable = numDashes;
if ( NumDashesAvailable < numDashes )
{
DashTimer = Stats[PlayerStat.DashCooldown];
DashRechargeProgress = 0f;
}
else
{
DashRechargeProgress = 1f;
}
ForEachStatus( status => status.OnDashRecharged() );
Manager.Instance.PlaySfxNearby( "player.dash.recharge", Position2D, pitch: Utils.Map( NumDashesAvailable, 1, numDashes, 1f, 1.2f ), volume: 0.2f, maxDist: 5f );
}
void HandleBounds()
{
var x_min = Manager.Instance.BOUNDS_MIN.x + Radius;
var x_max = Manager.Instance.BOUNDS_MAX.x - Radius;
var y_min = Manager.Instance.BOUNDS_MIN.y;
var y_max = Manager.Instance.BOUNDS_MAX.y - Radius;
if ( Position2D.x < x_min )
Position2D = new Vector2( x_min, Position2D.y );
else if ( Position2D.x > x_max )
Position2D = new Vector2( x_max, Position2D.y );
if ( Position2D.y < y_min )
Position2D = new Vector2( Position2D.x, y_min );
else if ( Position2D.y > y_max )
Position2D = new Vector2( Position2D.x, y_max );
}
public int GetExperienceReqForLevel( int level )
{
switch ( Manager.Instance.Difficulty )
{
case -1:
return (int)MathF.Round( Utils.Map( level, 1, 150, 3f, 240f, EasingType.SineIn ) );
case 0:
default:
return (int)MathF.Round( Utils.Map( level, 1, 150, 3f, 320f, EasingType.SineIn ) );
}
}
public void Flash( float time )
{
if ( _isFlashing )
return;
//Sprite.Tint = new Color( 1f, 0f, 0f );
Sprite.FlashTint = new Color( 1f, 0f, 0f, 1f );
_isFlashing = true;
_flashTimer = time;
}
public void Heal( float amount, float flashTime )
{
//Sprite.Tint = new Color( 0f, 1f, 0f );
Sprite.FlashTint = new Color( 0f, 1f, 0f, 1f );
_isFlashing = true;
_flashTimer = flashTime;
if ( IsProxy )
return;
if ( Health < Stats[PlayerStat.MaxHp] )
TimeSinceChangeHP = 0f;
Health += amount;
if ( Health > Stats[PlayerStat.MaxHp] )
Health = Stats[PlayerStat.MaxHp];
}
void HandleFlashing( float dt )
{
if ( _isFlashing )
{
_flashTimer -= dt;
if ( _flashTimer < 0f )
{
_isFlashing = false;
//Sprite.Tint = Color.White;
Sprite.FlashTint = Color.White.WithAlpha( 0f );
}
}
}
[ConCmd( "give_status" )]
public static void GiveStatus( string name )
{
// Cheat only works in the editor
if ( !Game.IsEditor )
return;
var type = TypeLibrary.GetType( name );
if ( type == null )
{
Log.Info( $"No status with name '{name}' found!" );
return;
}
Manager.Instance?.GetLocalPlayer()?.AddStatus( type );
}
public void AddStatus( TypeDescription type )
{
Status status = null;
var typeIdentity = type.Identity;
ForEachStatus( status => status.OnAddStatus( typeIdentity ) );
if ( Statuses.ContainsKey( typeIdentity ) )
{
status = Statuses[typeIdentity];
status.Level++;
}
if ( status == null )
{
status = StatusManager.CreateStatus( type );
Statuses.Add( typeIdentity, status );
status.Init( this );
}
//Sandbox.Services.Stats.Increment( Client, "status", 1, $"{type.Name.ToLowerInvariant()}", new { Status = type.Name.ToLowerInvariant(), Level = status.Level } );
status.Refresh();
Manager.Instance.PlaySfxNearby( "click", Position2D, 0.9f, 0.75f, 5f );
LevelUpChoices.Clear();
IsChoosingLevelUpReward = false;
RealTimeSinceChoseUpgrade = 0f;
CheckForLevelUp();
}
public bool HasStatus( TypeDescription type )
{
return Statuses.ContainsKey( type.Identity );
}
public Status GetStatus( TypeDescription type )
{
if ( Statuses.ContainsKey( type.Identity ) )
return Statuses[type.Identity];
return null;
}
public int GetStatusLevel( TypeDescription type )
{
if ( Statuses.ContainsKey( type.Identity ) )
return Statuses[type.Identity].Level;
return 0;
}
public void Modify( Status caller, PlayerStat statType, float value, ModifierType type, float priority = 0f, bool update = true )
{
if ( !_modifiers_stat.ContainsKey( caller ) )
_modifiers_stat.Add( caller, new Dictionary<PlayerStat, ModifierData>() );
_modifiers_stat[caller][statType] = new ModifierData( value, type, priority );
if ( update )
UpdateProperty( statType );
}
public void AdjustBaseStat( PlayerStat statType, float amount, bool update = true )
{
if ( !_original_properties_stat.ContainsKey( statType ) )
_original_properties_stat.Add( statType, Stats[statType] );
_original_properties_stat[statType] += amount;
if ( update )
UpdateProperty( statType );
}
void UpdateProperty( PlayerStat statType )
{
if ( !_original_properties_stat.ContainsKey( statType ) )
{
_original_properties_stat.Add( statType, Stats[statType] );
}
float curr_value = _original_properties_stat[statType];
float curr_set = curr_value;
bool should_set = false;
float curr_priority = 0f;
float total_add = 0f;
float total_mult = 1f;
foreach ( Status caller in _modifiers_stat.Keys )
{
var dict = _modifiers_stat[caller];
if ( dict.ContainsKey( statType ) )
{
var mod_data = dict[statType];
switch ( mod_data.type )
{
case ModifierType.Set:
if ( mod_data.priority >= curr_priority )
{
curr_set = mod_data.value;
curr_priority = mod_data.priority;
should_set = true;
}
break;
case ModifierType.Add:
total_add += mod_data.value;
break;
case ModifierType.Mult:
total_mult *= mod_data.value;
break;
}
}
}
if ( should_set )
curr_value = curr_set;
curr_value += total_add;
curr_value *= total_mult;
Stats[statType] = curr_value;
if ( statType == PlayerStat.MaxHp )
Stats[statType] = Math.Max( curr_value, 1f );
}
public void AddExperience( int xp )
{
ExperienceTotal += xp;
ExperienceCurrent += xp;
//var particle = DamageNumbersLegacy.Create( xp, Position2D + new Vector2( 0.2f + Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) ), color: new Color(0.1f, 0.1f, 1f), sizeMultiplier: 0.8f );
//Vector3 velocity = new Vector3( 0f, 0f, 0f );
//Vector3 gravity = new Vector3( 0f, 1f, 0f );
//particle.SetVector( 1, velocity );
//particle.SetNamedValue( "Gravity", gravity );
var pos = Position2D + new Vector2( Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) );
float size = Utils.Map( xp, 1f, 4f, 0.95f, 1.1f, EasingType.Linear );
var color = new Color( 0.4f, 0.4f, 1f );
Manager.Instance.SpawnDamageNumber( pos, xp, color, size, FloaterType.Xp );
ForEachStatus( status => status.OnGainExperience( xp ) );
if ( !IsChoosingLevelUpReward )
CheckForLevelUp();
}
public void LoseExperience( int amount )
{
ExperienceCurrent = Math.Max( ExperienceCurrent - amount, 0 );
}
public void CheckForLevelUp()
{
//Log.Info("CheckForLevelUp: " + ExperienceCurrent + " / " + ExperienceRequired + " IsServer: " + Sandbox.Game.IsServer + " Level: " + Level);
if ( ExperienceCurrent >= ExperienceRequired && Manager.Instance.ShouldUpdatePlayer )
LevelUp();
}
public void LevelUp()
{
ExperienceCurrent -= ExperienceRequired;
Level++;
ExperienceRequired = GetExperienceReqForLevel( Level + 1 );
if ( Manager.Instance.Difficulty < 3 )
NumRerollAvailable += (int)Stats[PlayerStat.NumRerollsPerLevel];
Manager.Instance.PlaySfxNearby( "levelup", Position2D, Game.Random.Float( 0.95f, 1.05f ), 0.5f, 5f );
ForEachStatus( status => status.OnLevelUp() );
GenerateLevelUpChoices();
IsChoosingLevelUpReward = true;
TimeSinceLevelUp = 0f;
if ( Manager.Instance.Difficulty >= 0 && !_hasUnlockedExperiencedAchievement && Level >= 85 )
{
Sandbox.Services.Achievements.Unlock( "experienced" );
_hasUnlockedExperiencedAchievement = true;
}
}
public void UseReroll()
{
if ( NumRerollAvailable <= 0 )
{
// todo: sfx
return;
}
Manager.Instance.PlaySfxNearby( "reroll", Position2D, Utils.Map( NumRerollAvailable, 0, 20, 0.9f, 1.4f, EasingType.QuadIn ), 0.6f, 5f );
NumRerollAvailable--;
GenerateLevelUpChoices();
ForEachStatus( status => status.OnReroll() );
}
public void UseChoiceHotkey( int num )
{
var index = num - 1;
if ( !IsChoosingLevelUpReward || index >= LevelUpChoices.Count )
return;
AddStatus( TypeLibrary.GetType( LevelUpChoices[index].GetType() ) );
}
public float CheckDamageAmount( float damage, DamageType damageType )
{
if ( IsInvulnerable )
{
return 0f;
}
if ( HasStatus( TypeLibrary.GetType( typeof( ShieldStatus ) ) ) && damageType != DamageType.LavaPuddle )
{
var shieldStatus = GetStatus( TypeLibrary.GetType( typeof( ShieldStatus ) ) ) as ShieldStatus;
if ( shieldStatus != null && shieldStatus.IsShielded )
{
shieldStatus.LoseShield();
return 0f;
}
}
if ( Stats[PlayerStat.DamageReductionPercent] > 0f )
damage *= (1f - MathX.Clamp( Stats[PlayerStat.DamageReductionPercent], 0f, 1f ));
if ( Stats[PlayerStat.IncreasedDmgTaken] > 0f )
damage *= (1f + MathX.Clamp( Stats[PlayerStat.IncreasedDmgTaken], 0f, 1f ));
if ( damageType == DamageType.Explosion && Stats[PlayerStat.ExplosionDamageReductionPercent] > 0f )
damage *= (1f - MathX.Clamp( Stats[PlayerStat.ExplosionDamageReductionPercent], 0f, 1f ));
if ( damageType != DamageType.Explosion && Stats[PlayerStat.NonExplosionDamageIncreasePercent] > 0f )
damage *= (1f + Stats[PlayerStat.NonExplosionDamageIncreasePercent]);
if ( Manager.Instance.Difficulty < 0 )
damage *= 0.571429f; // so zombie's 7 dmg becomes 4 dmg
return damage;
}
public void Damage( float damage, PlayerDamageType playerDamageType = PlayerDamageType.Enemy )
{
if ( !Manager.Instance.ShouldUpdatePlayer )
return;
TimeSinceHurt = 0f;
bool isCrit = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.SelfCritChance];
if ( isCrit )
damage *= 2f;
if ( damage > 0f )
{
TimeSinceChangeHP = 0f;
Flash( 0.125f );
}
var offset = new Vector2(
Game.Random.Float( -0.1f, 0.1f ),
Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) + (Health - damage < 0f ? -0.7f : 0f)
);
//DamageNumbers.Add( (int)damage, Position2D + Vector2.Up * Radius * 3f + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 0.2f, color: Color.Red );
//DamageNumbersLegacy.Create( damage, Position2D + offset, color: isCrit ? Color.Orange : Color.Red );
var pos = Position2D + offset;
float size;
if ( damage < 5f ) size = Utils.Map( damage, 1f, 5f, 1.1f, 1.25f, EasingType.QuadOut );
else if ( damage < 20f ) size = Utils.Map( damage, 5f, 20f, 1.25f, 1.6f, EasingType.Linear );
else size = Utils.Map( damage, 20f, 100f, 1.6f, 1.8f, EasingType.Linear );
var color = isCrit ? Color.Orange : Color.Red;
Manager.Instance.SpawnDamageNumber( pos, damage, color, size );
ForEachStatus( status => status.OnHurt( damage ) );
Health -= damage;
//if ( damage > 3f )
// ShakeCam( Utils.Map( damage, 3f, 25f, 0f, 0.1f ), Utils.Map( damage, 3f, 20f, 0.1f, 0.25f ), EasingType.QuadOut );
if ( Health <= 0f )
{
Die();
SpawnBlood( damage, sizeMultiplier: Game.Random.Float( 2.5f, 3f ), playbackSpeed: Game.Random.Float( 20f, 25f ), shouldUseRealTime: true );
}
else
{
SpawnBlood( damage );
}
}
public void AddVelocity( Vector2 vel )
{
if ( !Manager.Instance.ShouldUpdatePlayer )
return;
Velocity += vel;
}
public void SpawnBlood( float damage, float sizeMultiplier = 1f, float playbackSpeed = 0f, bool shouldUseRealTime = false )
{
var blood = Manager.Instance.SpawnBloodSplatter( Position2D );
if ( blood != null )
{
blood.LocalScale *= Utils.Map( damage, 1f, 20f, 0.5f, 1.2f, EasingType.QuadIn ) * Game.Random.Float( 0.8f, 1.2f ) * sizeMultiplier;
blood.Lifetime *= 0.3f;
blood.ShouldUseRealTime = shouldUseRealTime;
if ( playbackSpeed > 0f )
blood.Sprite.PlaybackSpeed = playbackSpeed;
}
}
public void Die()
{
if ( IsDead )
return;
IsDead = true;
_hasPlayedDeathSfx = false;
Sprite.Tint = new Color( 1f, 1f, 1f, 1f );
Sprite.FlashTint = new Color( 1f, 1f, 1f, 0f );
//ShadowOpacity = 0.2f;
_isFlashing = false;
IsReloading = false;
RealTimeSinceDeath = 0f;
_arrowDeathAlphaStart = ArrowSprite.Tint.a;
var pitch = Game.Random.Float( 1.25f, 1.3f ) * (Manager.Instance.Difficulty < 0 ? 2f : 1f);
Manager.Instance.PlaySfxNearby( "die", Position2D, pitch, volume: 1.5f, maxDist: 12f );
Sprite.LocalScale *= 2f;
Sprite.PlayAnimation( $"death" );
//Sprite.PlayAnimation( "ghost_idle" );
ShakeCam( Game.Random.Float( 0.025f, 0.065f ), Game.Random.Float( 0.3f, 0.5f ), EasingType.SineOut, useRealTime: true );
if ( IsProxy )
return;
Manager.Instance.PlayerDied( this );
}
public void Revive()
{
if ( !IsDead )
return;
IsDead = false;
IsChoosingLevelUpReward = false;
IsDashing = false;
IsReloading = true;
Sprite.Tint = Color.White;
ShadowOpacity = 0.8f;
if ( IsProxy )
return;
Timer = Stats[PlayerStat.ReloadTime];
ReloadProgress = 0f;
DashProgress = 0f;
ExperienceCurrent = 0;
Health = Stats[PlayerStat.MaxHp] * 0.33f;
}
public void ForEachStatus( Action<Status> action )
{
if ( IsProxy )
return;
foreach ( var (_, status) in Statuses )
{
action( status );
}
}
void HandleStatuses( float dt )
{
foreach ( KeyValuePair<int, Status> pair in Statuses )
{
Status status = pair.Value;
if ( status.ShouldUpdate )
status.Update( dt );
}
}
void HandleShooting( float dt )
{
if ( IsReloading )
{
ReloadProgress = Utils.Map( Timer, Stats[PlayerStat.ReloadTime], 0f, 0f, 1f );
Timer -= dt * Stats[PlayerStat.ReloadSpeed];
if ( Timer <= 0f )
{
Reload();
}
}
else
{
Timer -= dt * Stats[PlayerStat.AttackSpeed] * (IsMoving ? 1f : Stats[PlayerStat.AttackSpeedStill]);
if ( Timer <= 0f )
{
Shoot( isLastAmmo: AmmoCount == 1 );
AmmoCount--;
if ( AmmoCount <= 0 )
{
IsReloading = true;
Timer += Stats[PlayerStat.ReloadTime];
}
else
{
Timer += Stats[PlayerStat.AttackTime];
}
}
}
}
public void Shoot( bool isLastAmmo = false )
{
int num_bullets_int = (int)Stats[PlayerStat.NumProjectiles];
var aimDir = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.ShootRandomDirChance]
? Utils.GetRandomVector()
: AimDir;
var pos = Position2D + AimDir * 0.3f;
if ( Stats[PlayerStat.MaxBulletSpread] > 0f )
{
float increment = 360f / num_bullets_int;
for ( int i = 0; i < num_bullets_int; i++ )
{
var dir = Utils.RotateVector( aimDir, i * increment );
SpawnBullet( pos, dir, isLastAmmo );
}
}
else
{
float start_angle = MathF.Sin( -_shotNum * 2f ) * Stats[PlayerStat.BulletInaccuracy];
var spread = Stats[PlayerStat.BulletSpread] * num_bullets_int;
float currAngleOffset = num_bullets_int == 1 ? 0f : -spread * 0.5f;
float increment = num_bullets_int == 1 ? 0f : spread / (float)(num_bullets_int - 1);
for ( int i = 0; i < num_bullets_int; i++ )
{
var dir = Utils.RotateVector( aimDir, start_angle + currAngleOffset + increment * i );
SpawnBullet( pos, dir, isLastAmmo );
}
}
Manager.Instance.PlaySfxNearby( "shoot", pos, pitch: Utils.Map( _shotNum, 0f, (float)Stats[PlayerStat.MaxAmmoCount], 1f, 1.25f ), volume: 1f, maxDist: 4f );
Velocity -= aimDir * Stats[PlayerStat.Recoil];
_shotNum++;
_timeSinceShoot = 0f;
}
void SpawnBullet( Vector2 pos, Vector2 dir, bool isLastAmmo = false, float damageMult = 1f )
{
var damage = (Stats[PlayerStat.BulletDamage] * Stats[PlayerStat.BulletDamageMultiplier] + Stats[PlayerStat.BulletFlatDamageAddition]) * GetDamageMultiplier() * damageMult;
if ( isLastAmmo )
damage *= Stats[PlayerStat.LastAmmoDamageMultiplier];
if ( Stats[PlayerStat.DamagePerEarlierShot] > 0f )
damage += _shotNum * Stats[PlayerStat.DamagePerEarlierShot];
if ( Stats[PlayerStat.DamageForSpeed] > 0f )
{
damage += Stats[PlayerStat.DamageForSpeed] * Velocity.Length;
if ( IsDashing )
damage += Stats[PlayerStat.DamageForSpeed] * DashVelocity.Length;
}
var bulletObj = BulletPrefab.Clone( (Vector3)pos );
var bullet = bulletObj.Components.Get<Bullet>();
bullet.Velocity = dir * Stats[PlayerStat.BulletSpeed];
bullet.Shooter = this;
bullet.TempWeight = 3f;
bullet.Stats[BulletStat.Damage] = damage;
bullet.Stats[BulletStat.Force] = Stats[PlayerStat.BulletForce];
bullet.Stats[BulletStat.Lifetime] = Stats[PlayerStat.BulletLifetime];
bullet.Stats[BulletStat.NumPiercing] = (int)MathF.Round( Stats[PlayerStat.BulletNumPiercing] );
bullet.Stats[BulletStat.NumBouncing] = (int)MathF.Round( Stats[PlayerStat.BulletNumBouncing] );
bullet.Stats[BulletStat.WillIgnite] = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.ShootFireIgniteChance] ? 1f : 0f;
bullet.Stats[BulletStat.WillFreeze] = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.ShootFreezeChance] ? 1f : 0f;
bullet.Stats[BulletStat.GrowDamageAmount] = Stats[PlayerStat.BulletDamageGrow];
bullet.Stats[BulletStat.ShrinkDamageAmount] = Stats[PlayerStat.BulletDamageShrink];
bullet.Stats[BulletStat.DistanceDamageAmount] = Stats[PlayerStat.BulletDistanceDamage];
bullet.Stats[BulletStat.HealTeammateAmount] = Stats[PlayerStat.BulletHealTeammateAmount];
bullet.IsHoming = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.HomingBulletChance];
if ( Stats[PlayerStat.GrenadesCanCrit] <= 0f )
{
bullet.Stats[BulletStat.CriticalChance] = Stats[PlayerStat.CritChance];
bullet.Stats[BulletStat.CriticalMultiplier] = Stats[PlayerStat.CritMultiplier];
}
bullet.Init();
//bullet.GameObject.NetworkSpawn( Network.Owner );
}
void Reload()
{
AmmoCount = (int)Stats[PlayerStat.MaxAmmoCount];
IsReloading = false;
_shotNum = 0;
ReloadProgress = 0f;
ForEachStatus( status => status.OnReload() );
}
public float GetDamageMultiplier()
{
float damageMultiplier = Stats[PlayerStat.OverallDamageMultiplier];
if ( Stats[PlayerStat.LowHealthDamageMultiplier] > 1f )
damageMultiplier *= Utils.Map( Health, Stats[PlayerStat.MaxHp], 0f, 1f, Stats[PlayerStat.LowHealthDamageMultiplier] );
if ( Stats[PlayerStat.FullHealthDamageMultiplier] > 1f && !(Health < Stats[PlayerStat.MaxHp]) )
damageMultiplier *= Stats[PlayerStat.FullHealthDamageMultiplier];
return damageMultiplier;
}
public override void Colliding( Thing other, float percent, float dt )
{
base.Colliding( other, percent, dt );
if ( IsDead )
return;
ForEachStatus( status => status.Colliding( other, percent, dt ) );
if ( other is Enemy enemy && !enemy.IsDying )
{
if ( !Position2D.Equals( other.Position2D ) )
{
var spawnFactor = Utils.Map( enemy.TimeSinceSpawn, 0f, enemy.SpawnTime, 0f, 1f, EasingType.QuadIn );
Velocity += (Position2D - other.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 100f ) * (1f + other.TempWeight) * spawnFactor * dt;
}
}
else if ( other is Player player )
{
if ( !player.IsDead && !Position2D.Equals( other.Position2D ) )
{
Velocity += (Position2D - other.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 100f ) * (1f + other.TempWeight) * dt;
}
}
}
public void SpawnDashCloudClient()
{
Manager.Instance.SpawnCloud( Position2D + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 0.05f );
}
public void GenerateLevelUpChoices()
{
LevelUpChoices.Clear();
//if( Level == 1)
//{
// LevelUpChoices.Add( CreateStatus( TypeLibrary.GetType( typeof( PauseWhileChoosingStatus ) ) ) );
// LevelUpChoices.Add( CreateStatus( TypeLibrary.GetType( typeof( MysteryBoxStatus ) ) ) );
// LevelUpChoices.Add( CreateStatus( GetRandomStartingPerk() ) );
// LevelUpChoices.Shuffle();
// return;
//}
bool offerCurses = false;
if ( Manager.Instance.Difficulty >= 6 )
offerCurses = IsLevelCursed( Level );
int numChoices = Math.Clamp( (int)MathF.Round( Stats[PlayerStat.NumUpgradeChoices] ), 1, 6 );
List<TypeDescription> statusTypes = StatusManager.GetRandomStatuses( this, numChoices, offerCurses );
for ( int i = 0; i < statusTypes.Count; i++ )
LevelUpChoices.Add( CreateStatus( statusTypes[i] ) );
if ( Level == 1 )
{
bool alreadyOffered = false;
foreach ( var status in LevelUpChoices )
{
if ( status is PauseWhileChoosingStatus )
{
alreadyOffered = true;
break;
}
}
if ( !alreadyOffered )
{
LevelUpChoices.RemoveAt( 1 );
LevelUpChoices.Add( CreateStatus( TypeLibrary.GetType( typeof( PauseWhileChoosingStatus ) ) ) );
LevelUpChoices.Shuffle();
}
}
ChoiceHash++;
}
public bool IsLevelCursed( int level )
{
if ( Manager.Instance.Difficulty < 6 || level <= 1 )
return false;
switch ( Manager.Instance.Difficulty )
{
case 6: return level % 10 == 0;
case 7: return level % 9 == 0;
case 8: return level % 8 == 0;
case 9: return level % 7 == 0;
case 10: return level % 6 == 0;
case 11: return level % 5 == 0;
case 12: return level % 4 == 0;
case 13: return level % 3 == 0;
case 14: return level % 2 == 0;
case 15: return level % 3 != 1;
}
return false;
}
TypeDescription GetRandomStartingPerk()
{
List<(TypeDescription Type, float Weight)> perks = new List<(TypeDescription, float)>
{
(TypeLibrary.GetType( typeof( DamageStatus ) ), 5f),
(TypeLibrary.GetType( typeof( MovespeedStatus ) ), 3f),
(TypeLibrary.GetType( typeof( AttackSpeedStatus ) ), 2f),
(TypeLibrary.GetType( typeof( NumProjectileStatus ) ), 2f),
(TypeLibrary.GetType( typeof( PiercingStatus ) ), 3f),
(TypeLibrary.GetType( typeof( GrenadeShootReloadStatus ) ), 1f),
(TypeLibrary.GetType( typeof( NumDashesStatus ) ), 2f),
(TypeLibrary.GetType( typeof( FireIgniteStatus ) ), 2f),
(TypeLibrary.GetType( typeof( FreezeShootStatus ) ), 2f),
(TypeLibrary.GetType( typeof( FullHealthDamageStatus ) ), 2f),
(TypeLibrary.GetType( typeof( MoreRerollsStatus ) ), 4f),
(TypeLibrary.GetType( typeof( MoreChoicesStatus ) ), 3f),
(TypeLibrary.GetType( typeof( ReloadSpeedStatus ) ), 1f),
(TypeLibrary.GetType( typeof( XpDamageStatus ) ), 1f),
(TypeLibrary.GetType( typeof( HomingBulletStatus ) ), 4f),
(TypeLibrary.GetType( typeof( ThornsStatus ) ), 1f),
(TypeLibrary.GetType( typeof( BouncingBulletStatus ) ), 3f),
};
TypeDescription chosenPerk = null;
float totalWeight = perks.Sum( x => x.Weight );
var rand = Game.Random.Float( 0f, totalWeight );
for ( int i = perks.Count - 1; i >= 0; i-- )
{
var (type, weight) = perks[i];
rand -= weight;
if ( rand < 0f )
{
chosenPerk = type;
break;
}
}
return chosenPerk;
}
Status CreateStatus( TypeDescription type )
{
var status = StatusManager.CreateStatus( type );
var currLevel = GetStatusLevel( type );
status.Level = currLevel + 1;
return status;
}
public void Restart()
{
Sprite.PlayAnimation( "idle" );
Sprite.PlaybackSpeed = 0.66f;
Sprite.Tint = new Color( 1f, 1f, 1f, 1f );
Sprite.LocalScale = new Vector3( Globals.SPRITE_SCALE );
if ( IsProxy )
return;
Position2D = new Vector3( Game.Random.Float( -3f, 3f ), Game.Random.Float( -3f, 3f ) );
Manager.Instance.Camera2D.SetPos( Position2D );
InitializeStats();
Manager.Instance.PlaySfxNearby( "restart", Position2D, Game.Random.Float( 0.95f, 1.05f ), 0.66f, 4f );
}
public void SpawnBulletRing( Vector2 pos, int numBullets, Vector2 aimDir, float damageMultMin = 1f, float damageMultMax = 1f )
{
float increment = 360f / numBullets;
for ( int i = 0; i < numBullets; i++ )
{
var dir = Utils.RotateVector( aimDir, i * increment );
float damageMult = Game.Random.Float( damageMultMin, damageMultMax );
SpawnBullet( pos, dir, false, damageMult );
}
Manager.Instance.PlaySfxNearby( "shoot", pos, pitch: 1f, volume: 1f, maxDist: 3f );
}
public Grenade SpawnGrenade( Vector2 pos, Vector2 vel )
{
var grenadeObj = Manager.Instance.GrenadePrefab.Clone();
var grenade = grenadeObj.Components.Get<Grenade>();
grenade.Velocity = vel;
grenade.ExplosionSizeMultiplier = Stats[PlayerStat.ExplosionSizeMultiplier];
grenade.Player = this;
grenade.StickyPercent = Stats[PlayerStat.GrenadeStickyPercent];
grenade.FearChance = Stats[PlayerStat.GrenadeFearChance];
if ( Stats[PlayerStat.GrenadesCanCrit] > 0f )
{
grenade.CriticalChance = Stats[PlayerStat.CritChance];
grenade.CriticalMultiplier = Stats[PlayerStat.CritMultiplier];
}
//grenadeObj.NetworkSpawn( Network.Owner );
grenadeObj.WorldPosition = new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) );
Manager.Instance.AddThing( grenade );
return grenade;
}
public void CreateShieldVfx()
{
_shieldVfx = Manager.Instance.ShieldVfxPrefab.Clone( WorldPosition );
_shieldVfx.Parent = GameObject;
_shieldVfx.LocalPosition = new Vector3( 0f, 0f, 0.1f );
_shieldVfx.LocalScale = new Vector3( 1f ) * 1.8f * Globals.SPRITE_SCALE;
_shieldVfx.LocalRotation = new Angles( 0f, -90f, 0f );
}
public void RemoveShieldVfx()
{
if ( _shieldVfx != null )
{
_shieldVfx.Destroy();
_shieldVfx = null;
}
}
public void PlaySfx( string name, Vector2 pos, float pitch, float volume )
{
var sfx = Sound.Play( name, new Vector3( pos.x, pos.y, Globals.SFX_DEPTH ) );
if ( sfx != null )
{
sfx.Volume = volume;
sfx.Pitch = pitch;
}
}
protected override void OnStart()
{
base.OnStart();
Manager.Instance.AddPlayer( this );
}
protected override void OnDestroy()
{
base.OnDestroy();
Manager.Instance.RemovePlayer( this );
}
void HandleCamShaking()
{
CamShakeAmount = 0f;
for ( int i = _camShakeDatas.Count - 1; i >= 0; i-- )
{
var data = _camShakeDatas[i];
var time = data.useRealTime ? RealTime.Now : Time.Now;
if ( time > data.startTime + data.time )
{
_camShakeDatas.RemoveAt( i );
}
else
{
float amount = Utils.Map( time, data.startTime, data.startTime + data.time, data.strength, 0f, data.easingType );
CamShakeAmount = MathF.Max( amount, CamShakeAmount );
}
}
}
public void ShakeCam( float strength, float time, EasingType easingType = EasingType.Linear, bool useRealTime = false )
{
var timeNow = useRealTime ? RealTime.Now : Time.Now;
_camShakeDatas.Add( new CamShakeData( strength, timeNow, time, easingType, useRealTime ) );
}
}