4881 results

global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Twitch Poop" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "twitchpoop" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "garry" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "garry.twitchpoop" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "6/6/2026 7:39:31 PM" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "25" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.128.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.128.0")]
using System;
using Sandbox;

namespace Goo.Animation;

public record struct SmoothVector2
{
    public Vector2 Current;
    public Vector2 Target;
    public Vector2 Velocity;
    public float SmoothTime;

    public SmoothVector2(Vector2 initial, float smoothTime)
    {
        Current = initial;
        Target = initial;
        Velocity = default;
        SmoothTime = smoothTime;
    }

    public void Update(float dt)
    {
        float vx = Velocity.x, vy = Velocity.y;
        Current = new Vector2(
            MathX.SmoothDamp(Current.x, Target.x, ref vx, SmoothTime, dt),
            MathX.SmoothDamp(Current.y, Target.y, ref vy, SmoothTime, dt));
        Velocity = new Vector2(vx, vy);
    }

    public bool IsSettled =>
        MathF.Abs(Target.x - Current.x) < 0.0001f &&
        MathF.Abs(Target.y - Current.y) < 0.0001f &&
        MathF.Abs(Velocity.x) < 0.0001f &&
        MathF.Abs(Velocity.y) < 0.0001f;

    /// <summary>Advances by dt and returns true while still moving; chain calls with | (not ||) so every damper advances each frame.</summary>
    public bool Tick(float dt) { Update(dt); return !IsSettled; }
}
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;

namespace Goo;

// Style helpers hoisted so generated Blob facades share them. Keep the early-return form: an engine-type ternary that resolves bare null via an implicit string operator silently produces magenta (Color.Parse fallback) instead of the intended absent-property. See engine-fact memories.

internal static class StyleAccumulator
{
    static StyleList Rent(StyleList current)
        => ReferenceEquals(current, StyleList.Empty)
            ? BuildContext.Current.RentStyleList()
            : current;

    public static StyleList Add(StyleList current, StyleField field, Length? value)
    {
        if (!value.HasValue) return current;
        var list = Rent(current);
        list.Add(field, StyleValue.FromLength(value.Value));
        return list;
    }

    public static StyleList Add<TEnum>(StyleList current, StyleField field, TEnum? value, System.Func<TEnum, StyleValue> wrap) where TEnum : struct
    {
        if (!value.HasValue) return current;
        var list = Rent(current);
        list.Add(field, wrap(value.Value));
        return list;
    }

    public static StyleList Add(StyleList current, StyleField field, Color? value, System.Func<Color, StyleValue> wrap)
    {
        if (!value.HasValue) return current;
        var list = Rent(current);
        list.Add(field, wrap(value.Value));
        return list;
    }

    public static StyleList Add(StyleList current, StyleField field, string? value)
    {
        if (value is null) return current;
        var list = Rent(current);
        list.Add(field, StyleValue.FromString(value));
        return list;
    }

    public static StyleList Add(StyleList current, StyleField field, float? value)
    {
        if (!value.HasValue) return current;
        var list = Rent(current);
        list.Add(field, StyleValue.FromSingle(value.Value));
        return list;
    }

    public static StyleList Add(StyleList current, StyleField field, bool? value)
    {
        if (!value.HasValue) return current;
        var list = Rent(current);
        list.Add(field, StyleValue.FromBoolean(value.Value));
        return list;
    }

    public static StyleList Add(StyleList current, StyleField field, int? value)
    {
        if (!value.HasValue) return current;
        var list = Rent(current);
        list.Add(field, StyleValue.FromInt32(value.Value));
        return list;
    }

    public static StyleList Add(StyleList current, StyleField field, Texture? value)
    {
        if (value is null) return current;
        var list = Rent(current);
        list.Add(field, StyleValue.FromTexture(value));
        return list;
    }
}
using System;
using Sandbox;

namespace Goo.FpsUI;

// ============================================================================
//  ALL demo / self-preview behaviour for the FPS UI pack lives in this ONE file.
//
//  DELETE THIS FILE to remove demo functionality.
//  deleting this file makes every Demo inspector toggle disappear
// ============================================================================
static class FpsDemo
{
    static readonly string[] Names = { "Vex", "Korr", "Juno", "Rhys", "Mara", "Dane", "Iko", "Sol" };
    static readonly (int Amount, string Action)[] Grants =
    {
        (100, "Kill"), (125, "Headshot"), (50, "Assist"), (200, "Double Kill"), (25, "Hitmarker"),
    };

    public static void Health( HealthModel m, ref float t, float dt )  // bleed in chunks, big heal when low
    {
        t += dt;
        if ( t < 2.4f ) return;
        t = 0f;
        if ( m.Health <= m.MaxHealth * 0.3f ) m.Heal( m.MaxHealth * 0.65f );
        else m.Damage( m.MaxHealth * 0.22f );
    }

    public static void Stamina( StaminaModel m, ref float t, float dt )  // sprint on/off cycle
    {
        t += dt;
        if ( t >= 3.1f ) { t = 0f; m.SetSprinting( !m.Sprinting ); }
        if ( m.Stamina <= 0f ) m.SetSprinting( false );
    }

    public static void Ammo( AmmoModel m, ref float t, float dt )  // steady fire, auto-reload on empty
    {
        t += dt;
        if ( m.Reloading ) return;
        if ( m.Mag <= 0 ) { m.Reload(); return; }
        if ( t >= 0.18f ) { t = 0f; m.Fire(); }
    }

    public static void Crosshair( CrosshairModel m, ref float t, float dt )  // steady fire cadence
    {
        t += dt;
        if ( t >= 0.18f ) { t = 0f; m.Fire(); }
    }

    public static void Hitmarker( HitmarkerModel m, ref float t, float dt )  // periodic pop
    {
        t += dt;
        if ( t >= 0.5f ) { t = 0f; m.Pop( false ); }
    }

    public static void Killfeed( KillfeedModel m, ref float t, ref int pick, float dt )  // periodic fake kills
    {
        t += dt;
        if ( t < 2.2f ) return;
        t = 0f;
        string a = Names[pick % Names.Length];
        string v = Names[(pick + 3) % Names.Length];
        // Alternate teams, occasionally make the local player the killer so the preview shows both.
        bool youKill = pick % 3 == 0;
        KillTeam at = youKill || pick % 2 == 0 ? KillTeam.Friendly : KillTeam.Enemy;
        KillTeam vt = at == KillTeam.Friendly ? KillTeam.Enemy : KillTeam.Friendly;
        pick++;
        m.Add( a, at, v, vt, attackerLocal: youKill );
    }

    public static void Xp( XpModel m, ref float t, ref int pick, float dt )  // periodic grants
    {
        t += dt;
        if ( t < 1.6f ) return;
        t = 0f;
        var g = Grants[pick % Grants.Length];
        pick++;
        m.Add( g.Amount, g.Action );
    }

    public static void Scoreboard( ScoreboardModel m, ref float t, float dt )  // loop the clock, nudge scores, cycle variant
    {
        if ( m.TimeRemaining <= 0f ) m.TimeRemaining = 600f;
        t += dt;
        if ( t < 4f ) return;
        t = 0f;
        m.Mode = m.Mode switch
        {
            ScoreboardMode.Tdm        => ScoreboardMode.Domination,
            ScoreboardMode.Domination => ScoreboardMode.Ffa,
            _                         => ScoreboardMode.Tdm,
        };
        m.FriendlyScore = (m.FriendlyScore + 7) % (m.ScoreLimit + 1);
        m.EnemyScore    = (m.EnemyScore + 5) % (m.ScoreLimit + 1);
        m.PlayerScore  += 150;
        m.LeaderScore   = Math.Max( m.LeaderScore, m.PlayerScore ) + 50;
        for ( int i = 0; i < m.Points.Length; i++ )  // rotate cap ownership
            m.Points[i] = (CapOwner)(((int)m.Points[i] + 1) % 3);
    }
}

// ---- per-widget demo hooks (the [Property] Demo toggle + timers + StepDemo body) ----

public sealed partial class HealthWidget
{
    [Property] public bool Demo { get; set; } = true;  // self-animate in editor
    float _demoT;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Health( _m, ref _demoT, dt );
    }
}

public sealed partial class StaminaWidget
{
    [Property] public bool Demo { get; set; } = true;  // self-animate in editor
    float _demoT;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Stamina( _m, ref _demoT, dt );
    }
}

public sealed partial class AmmoWidget
{
    [Property] public bool Demo { get; set; } = true;  // self-fire in editor
    float _demoT;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Ammo( _m, ref _demoT, dt );
    }
}

public sealed partial class CrosshairWidget
{
    [Property] public bool Demo { get; set; } = true;  // self-fire in editor
    float _demoT;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Crosshair( _m, ref _demoT, dt );
    }
}

public sealed partial class HitmarkerWidget
{
    [Property] public bool Demo { get; set; } = true;  // periodic pop in editor
    float _demoT;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Hitmarker( _m, ref _demoT, dt );
    }
}

public sealed partial class KillfeedWidget
{
    [Property] public bool Demo { get; set; } = true;  // fake kills in editor
    float _demoT;
    int _demoPick;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Killfeed( _m, ref _demoT, ref _demoPick, dt );
    }
}

public sealed partial class XpWidget
{
    [Property] public bool Demo { get; set; } = true;  // fake grants in editor
    float _demoT;
    int _demoPick;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Xp( _m, ref _demoT, ref _demoPick, dt );
    }
}

public sealed partial class ScoreboardWidget
{
    [Property] public bool Demo { get; set; } = true;  // self-animate in editor
    float _demoT;
    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;
        FpsDemo.Scoreboard( _m, ref _demoT, dt );
    }
}

// ---- the assembled HUD: one coordinated synthetic firefight ----

public sealed partial class FpsHud
{
    [Property, Group( "General" )] public bool Demo { get; set; } = true;  // self-run a synthetic firefight in editor

    readonly Random _demoRng = new();
    float _hpT, _stT, _kfT, _xpT, _sbT;  // per-subsystem demo timers
    int _kfPick, _xpPick;                // demo name / grant cursors
    float _triggerT, _modeT;             // synthetic trigger square-wave + fire-mode cycle

    partial void StepDemo( float dt, ref bool active )
    {
        if ( !Demo ) return;
        active = true;

        // Cycle the fire mode every few seconds, then drive a synthetic trigger square-wave (held ~1s,
        // released ~0.4s) through the real fire control so each mode visibly fires differently.
        _modeT -= dt;
        if ( _modeT <= 0f ) { _modeT = 3.0f; CycleFireMode(); }
        _fire.Mode = FireMode; _fire.BurstCount = BurstCount; _fire.Rpm = RoundsPerMinute;
        _triggerT += dt;
        bool trigger = _triggerT % 1.4f < 1.0f;
        if ( !_ammo.Reloading )
        {
            if ( _ammo.Mag <= 0 ) _ammo.Reload();
            else if ( _fire.Tick( trigger, dt ) )
            {
                _ammo.Fire(); _crosshair.Fire();
                if ( _demoRng.NextSingle() < 0.5f ) _hitmarker.Pop( _demoRng.NextSingle() < 0.15f );
            }
        }

        FpsDemo.Health( _health, ref _hpT, dt );
        FpsDemo.Stamina( _stamina, ref _stT, dt );
        FpsDemo.Killfeed( _killfeed, ref _kfT, ref _kfPick, dt );
        FpsDemo.Xp( _xp, ref _xpT, ref _xpPick, dt );
        if ( ShowScoreboard ) FpsDemo.Scoreboard( _scoreboard, ref _sbT, dt );
    }
}
using System;
using Sandbox;

namespace Goo.FpsUI;

// A drop-in shootable target for the FPS pack. Attach to any GameObject that has a Collider; the HUD's
// hitscan damages it through the engine's Component.IDamageable interface (the same path TriggerHurt uses).
// Exposes Health / IsDead / Name so FpsHud can drive the hitmarker, killfeed, and XP on a hit or kill.
// Engine-side component, not a Goo panel.
[Title( "FPS Target" ), Category( "FPS UI" ), Icon( "my_location" )]
public sealed class FpsTarget : Component, Component.IDamageable
{
    [Property] public string Name { get; set; } = "Target";                          // shown as the victim in the killfeed
    [Property, Range( 1f, 1000f )] public float MaxHealth { get; set; } = 100f;       // health restored on enable / respawn
    [Property] public bool RespawnOnDeath { get; set; } = true;                       // refill health a moment after dying
    [Property, Range( 0.5f, 10f )] public float RespawnDelay { get; set; } = 3f;      // seconds dead before respawning

    public float Health { get; private set; }
    public bool IsDead => Health <= 0f;

    float _respawnAt;

    protected override void OnEnabled() => Health = MaxHealth;

    // Engine damage entry point: TriggerHurt volumes and the HUD's hitscan both call this.
    public void OnDamage( in DamageInfo damage )
    {
        if ( IsDead ) return;                                       // already down, ignore extra hits
        Health = Math.Max( 0f, Health - damage.Damage );
        if ( IsDead && RespawnOnDeath ) _respawnAt = Time.Now + RespawnDelay;
    }

    protected override void OnUpdate()
    {
        if ( IsDead && RespawnOnDeath && Time.Now >= _respawnAt ) Health = MaxHealth;
    }
}
using System;

namespace Goo.FpsUI;

// Compass logic: a 0..360 heading plus the shortest signed angular delta the view uses to
// place cardinal ticks. Engine-free.
public sealed class CompassModel
{
    public float Heading { get; private set; }  // current yaw, normalized 0..360

    public void SetHeading( float yawDeg )       // set heading from game code (any range)
        => Heading = ((yawDeg % 360f) + 360f) % 360f;

    // Signed delta from `from` to `to`, in (-180, 180]. Used to position ticks around center.
    public static float ShortestDelta( float from, float to ) => ((to - from + 540f) % 360f) - 180f;

    public bool Tick( float dt ) => false;  // heading changes drive rebuilds, not the damper
}
using System;
using Goo;
using Sandbox;
using Sandbox.UI;
using PanelTransform = Goo.PanelTransform;   // rule 18: ambiguous with Sandbox.UI otherwise

namespace Goo.FpsUI;

// Shared, stateless view helpers. Each returns a fresh Container (never cache a Container).
static class Parts
{
    public enum Corner { TopLeft, TopCenter, TopRight, BottomLeft, BottomRight, Center }

    // Absolute fill spanning `frac` (0..1) of its Relative parent's width, full height.
    public static Container FillRect( string key, float frac, Color color, float radius ) => new()
    {
        Key = key, Position = PositionMode.Absolute, Top = 0, Left = 0,
        Height = Length.Percent( 100 ),
        Width = Length.Percent( Math.Clamp( frac, 0f, 1f ) * 100f ),
        BorderRadius = radius, BackgroundColor = color,
    };

    // Absolute full-cover tint at a given opacity (flashes, warnings).
    public static Container Overlay( string key, Color color, float opacity, float radius ) => new()
    {
        Key = key, Position = PositionMode.Absolute, Top = 0, Left = 0,
        Width = Length.Percent( 100 ), Height = Length.Percent( 100 ),
        BorderRadius = radius, BackgroundColor = color, Opacity = opacity,
    };

    // Pin `child` to a screen corner/center inside a Relative, full-size root. `m` is the edge inset.
    // Container properties are init-only (record struct), so we use `with` expressions per corner.
    public static Container Anchor( string key, Corner c, float m, Container child )
    {
        var box = new Container
        {
            Key = key, Position = PositionMode.Absolute, PointerEvents = PointerEvents.None,
            Children = { child },
        };
        return c switch
        {
            Corner.TopLeft     => box with { Top = m, Left = m },
            Corner.TopRight    => box with { Top = m, Right = m },
            Corner.BottomLeft  => box with { Bottom = m, Left = m },
            Corner.BottomRight => box with { Bottom = m, Right = m },
            Corner.TopCenter   => box with
            {
                Top = m, Left = Length.Percent( 50 ),
                Transform = PanelTransform.Translate( Length.Percent( -50 ) ?? default, Length.Percent( 0 ) ?? default ),
            },
            _ => box with  // Center
            {
                Top = Length.Percent( 50 ), Left = Length.Percent( 50 ),
                Transform = PanelTransform.Translate( Length.Percent( -50 ) ?? default, Length.Percent( -50 ) ?? default ),
            },
        };
    }

    // Pin `child` horizontally centered, offset `dy` px from the screen center (positive = down, negative = up).
    public static Container CenterOffset( string key, float dy, Container child ) => new()
    {
        Key = key, Position = PositionMode.Absolute, PointerEvents = PointerEvents.None,
        Top = Length.Percent( 50 ), Left = Length.Percent( 50 ),
        Transform = PanelTransform.Translate( Length.Percent( -50 ) ?? default, Px.Of( dy ) ),
        Children = { child },
    };

    // Pin `child`'s top-left corner at a pixel offset from the screen center (positive dx = right, positive dy = down).
    public static Container Offset( string key, float dx, float dy, Container child ) => new()
    {
        Key = key, Position = PositionMode.Absolute, PointerEvents = PointerEvents.None,
        Top = Length.Percent( 50 ), Left = Length.Percent( 50 ),
        Transform = PanelTransform.Translate( Px.Of( dx ), Px.Of( dy ) ),
        Children = { child },
    };

    // Semi-opaque rounded backing behind a HUD readout, so text/bars stay legible over the world.
    // Wraps one child and sizes to it, all card styling comes from the theme.
    public static Container Panel( string key, FpsTheme t, Container child ) => new()
    {
        Key = key, Position = PositionMode.Relative,
        Padding = t.PanelPad, BorderRadius = t.PanelRadius,
        BackgroundColor = t.BackingBg,
        Children = { child },
    };

    // A full-size, pointer-through, Relative root used by every standalone widget and the FpsHud.
    public static Container Root( string key ) => new()
    {
        Key = key, Width = Length.Percent( 100 ), Height = Length.Percent( 100 ),
        Position = PositionMode.Relative, PointerEvents = PointerEvents.None,
    };
}
using System;
using Goo;
using Sandbox;
using Sandbox.UI;

namespace Goo.FpsUI;

// Stateless presenter: the secondary (stamina/armor) bar.
static class StaminaView
{
    public const float TrackW = 320f, TrackH = 10f;

    public static Container Build( StaminaModel m, FpsTheme t )
    {
        float frac = m.ShownFraction;
        Color fill = Color.Lerp( t.Warn, t.Good, frac );

        var track = new Container
        {
            Key = "stTrack", Position = PositionMode.Relative,
            Width = TrackW, Height = TrackH, BackgroundColor = t.TrackBg,
            BorderRadius = t.Radius, Overflow = OverflowMode.Hidden,
            Children = { Parts.FillRect( "fill", frac, fill, t.Radius ) },
        };
        if ( m.Flash > 0.001f ) track.Children.Add( Parts.Overlay( "flash", Color.White, m.Flash * 0.8f, t.Radius ) );
        return track;
    }
}

// Standalone stamina bar. Call Sprinting(bool) from your movement code each frame.
public sealed partial class StaminaWidget : GooPanel<Container>
{
    [Property, Range( 1f, 1000f )] public float MaxStamina { get; set; } = 100f; // full-bar value
    [Property, Range( 0.05f, 2f )] public float DrainRate { get; set; } = 0.55f;  // fraction of max drained per second while sprinting
    [Property] public PlayerController? Player { get; set; }                      // player to gate sprint on when stamina is empty (null = skip gating)

    readonly StaminaModel _m = new();
    readonly FpsTheme _t = new();
    bool _booted;
    bool _sprintBlocked;
    float _cachedRunSpeed;

    void Boot() { _m.MaxStamina = MaxStamina; _m.Reset(); _booted = true; }

    public void Sprinting( bool on ) => _m.SetSprinting( on ); // toggle sprint drain

    // Demo-only seam: implemented in FpsDemo.cs, compiles out when that file is deleted.
    partial void StepDemo( float dt, ref bool active );

    protected override bool Tick( float dt )
    {
        if ( !_booted ) Boot();
        _m.DrainRate = DrainRate;  // live-tunable
        bool demo = false;
        StepDemo( dt, ref demo );
        if ( !demo )
        {
            Sprinting( Sandbox.Input.Down( "run" ) );
            FpsInput.ApplySprintGate( Player, _m.Stamina, ref _sprintBlocked, ref _cachedRunSpeed );
        }
        bool moving = _m.Tick( dt );
        return demo || moving;
    }

    protected override Container Build()
    {
        if ( !_booted ) Boot();
        var root = Parts.Root( "fpsStamina" );
        // Sits just above where the health bar would be (health height + gap).
        root.Children.Add( Parts.Anchor( "a", Parts.Corner.BottomLeft, _t.Margin + HealthView.TrackH + 10f, Parts.Panel( "bg", _t, StaminaView.Build( _m, _t ) ) ) );
        return root;
    }
}
using System;
using Sandbox;
using Sandbox.UI;

namespace Goo.Internal;

internal sealed class StatefulShapePanel : Panel, IStatefulEventHost
{
    internal Action<MousePanelEvent>? _onClick;
    internal Action<MousePanelEvent>? _onRightClick;
    internal Action<MousePanelEvent>? _onMiddleClick;
    internal Action<MousePanelEvent>? _onMouseEnter;
    internal Action<MousePanelEvent>? _onMouseLeave;
    internal Action<MousePanelEvent>? _onMouseDown;
    internal Action<MousePanelEvent>? _onMouseUp;
    internal Action<MousePanelEvent>? _onMouseMove;
    internal bool    _userSetPointerEvents;
    internal Action? _requestRebuild;
    public Action? RequestRebuild { set => _requestRebuild = value; }

    public void ApplyEvents(in BlobEvents events)
    {
        _onClick      = events.OnClick;
        _onRightClick = events.OnRightClick;
        _onMiddleClick = events.OnMiddleClick;
        _onMouseEnter = events.OnMouseEnter;
        _onMouseLeave = events.OnMouseLeave;
        _onMouseDown  = events.OnMouseDown;
        _onMouseUp    = events.OnMouseUp;
        _onMouseMove  = events.OnMouseMove;
    }

    public bool HasEventHandlers =>
        _onClick != null || _onRightClick != null || _onMiddleClick != null || _onMouseEnter != null || _onMouseLeave != null ||
        _onMouseDown != null || _onMouseUp != null || _onMouseMove != null;

    public bool UserSetPointerEvents
    {
        get => _userSetPointerEvents;
        set => _userSetPointerEvents = value;
    }

    // Apply a baked alpha texture as BackgroundImage. The shape color comes from
    // BackgroundColor flowing through the engine's BgTint multiplier; this method
    // is only responsible for the alpha mask.
    public void ApplyShape(BlobKind kind, in ShapeParams shape)
    {
        Style.BackgroundImage = ShapeTextureCache.GetOrBake(kind, in shape);
        Style.BackgroundSizeX = Length.Percent(100);
        Style.BackgroundSizeY = Length.Percent(100);
    }

    public void ApplyPolygon(Vector2[] points)
    {
        Style.BackgroundImage = ShapeTextureCache.GetOrBakePolygon(points);
        Style.BackgroundSizeX = Length.Percent(100);
        Style.BackgroundSizeY = Length.Percent(100);
    }

    protected override void OnClick(MousePanelEvent e)       { base.OnClick(e);       EventDispatch.Fire(_onClick, e, _requestRebuild); }
    protected override void OnRightClick(MousePanelEvent e)  { base.OnRightClick(e);  EventDispatch.Fire(_onRightClick, e, _requestRebuild); }
    protected override void OnMiddleClick(MousePanelEvent e) { base.OnMiddleClick(e); EventDispatch.Fire(_onMiddleClick, e, _requestRebuild); }
    protected override void OnMouseOver(MousePanelEvent e)   { base.OnMouseOver(e);   EventDispatch.Fire(_onMouseEnter, e, _requestRebuild); }
    protected override void OnMouseOut(MousePanelEvent e)    { base.OnMouseOut(e);    EventDispatch.Fire(_onMouseLeave, e, _requestRebuild); }
    protected override void OnMouseDown(MousePanelEvent e)   { base.OnMouseDown(e);   EventDispatch.Fire(_onMouseDown, e, _requestRebuild); }
    protected override void OnMouseUp(MousePanelEvent e)     { base.OnMouseUp(e);     EventDispatch.Fire(_onMouseUp, e, _requestRebuild); }
    protected override void OnMouseMove(MousePanelEvent e)   { base.OnMouseMove(e);   EventDispatch.Fire(_onMouseMove, e, _requestRebuild); }
}
using System;
using Sandbox.UI;

namespace Goo.Internal;

internal sealed class StatefulSvgPanel : Sandbox.UI.SvgPanel, IStatefulEventHost
{
    internal Action<MousePanelEvent>? _onClick;
    internal Action<MousePanelEvent>? _onRightClick;
    internal Action<MousePanelEvent>? _onMiddleClick;
    internal Action<MousePanelEvent>? _onMouseEnter;
    internal Action<MousePanelEvent>? _onMouseLeave;
    internal Action<MousePanelEvent>? _onMouseDown;
    internal Action<MousePanelEvent>? _onMouseUp;
    internal Action<MousePanelEvent>? _onMouseMove;
    internal bool    _userSetPointerEvents;
    internal Action? _requestRebuild;
    public Action? RequestRebuild { set => _requestRebuild = value; }

    public void ApplyEvents(in BlobEvents events)
    {
        _onClick      = events.OnClick;
        _onRightClick = events.OnRightClick;
        _onMiddleClick = events.OnMiddleClick;
        _onMouseEnter = events.OnMouseEnter;
        _onMouseLeave = events.OnMouseLeave;
        _onMouseDown  = events.OnMouseDown;
        _onMouseUp    = events.OnMouseUp;
        _onMouseMove  = events.OnMouseMove;
    }

    public bool HasEventHandlers =>
        _onClick != null || _onRightClick != null || _onMiddleClick != null || _onMouseEnter != null || _onMouseLeave != null ||
        _onMouseDown != null || _onMouseUp != null || _onMouseMove != null;

    public bool UserSetPointerEvents
    {
        get => _userSetPointerEvents;
        set => _userSetPointerEvents = value;
    }

    protected override void OnClick(MousePanelEvent e)       { base.OnClick(e);       EventDispatch.Fire(_onClick, e, _requestRebuild); }
    protected override void OnRightClick(MousePanelEvent e)  { base.OnRightClick(e);  EventDispatch.Fire(_onRightClick, e, _requestRebuild); }
    protected override void OnMiddleClick(MousePanelEvent e) { base.OnMiddleClick(e); EventDispatch.Fire(_onMiddleClick, e, _requestRebuild); }
    protected override void OnMouseOver(MousePanelEvent e)   { base.OnMouseOver(e);   EventDispatch.Fire(_onMouseEnter, e, _requestRebuild); }
    protected override void OnMouseOut(MousePanelEvent e)    { base.OnMouseOut(e);    EventDispatch.Fire(_onMouseLeave, e, _requestRebuild); }
    protected override void OnMouseDown(MousePanelEvent e)   { base.OnMouseDown(e);   EventDispatch.Fire(_onMouseDown, e, _requestRebuild); }
    protected override void OnMouseUp(MousePanelEvent e)     { base.OnMouseUp(e);     EventDispatch.Fire(_onMouseUp, e, _requestRebuild); }
    protected override void OnMouseMove(MousePanelEvent e)   { base.OnMouseMove(e);   EventDispatch.Fire(_onMouseMove, e, _requestRebuild); }
}
namespace LobbySystem;

/// <summary>
/// Auto-hosts a lobby so Steam friends can join, and keeps one networked pawn per connection plus optional
/// bots by cloning <see cref="PlayerPrefab"/>. The pawn only has to implement <see cref="ILobbyAgent"/>.
/// Spawning is de-duped and runs in OnUpdate so a join can't fire mid-enumeration.
/// </summary>
public sealed class LobbyNetworkManager : Component, Component.INetworkListener
{
	[Property] public GameObject PlayerPrefab { get; set; }
	[Property] public int BotCount { get; set; } = 1;

	/// <summary>When true, bots only exist during an active round.</summary>
	[Property] public bool BotsOnlyDuringRound { get; set; } = true;

	[Property] public Color BotTint { get; set; } = new Color( 1f, 0.35f, 0.3f );

	// Lobby spawn ring, used before a round map loads.
	readonly Vector3[] _spawns =
	{
		new Vector3( 0f, -300f, 40f ), new Vector3( 300f, 0f, 40f ),
		new Vector3( 0f, 300f, 40f ),  new Vector3( -300f, 0f, 40f ),
		new Vector3( 250f, 250f, 40f ), new Vector3( -250f, -250f, 40f ),
	};
	int _spawnIndex;
	readonly Dictionary<Guid, GameObject> _pawns = new();
	readonly List<GameObject> _bots = new();

	bool _reconcileNow;
	TimeUntil _nextReconcile;
	TimeUntil _nextSweep;

	protected override async Task OnLoad()
	{
		// When joining a friend the engine is mid-connect and IsActive is briefly false, so poll for a
		// moment before hosting. Otherwise a joiner would spin up its own solo lobby.
		if ( Networking.IsActive ) return;
		for ( int i = 0; i < 6 && !Networking.IsActive; i++ )
			await Task.DelayRealtimeSeconds( 0.1f );
		if ( !Networking.IsActive )
			Networking.CreateLobby( new() );
	}

	void INetworkListener.OnActive( Connection channel ) => _reconcileNow = true;

	protected override void OnUpdate()
	{
		if ( !Networking.IsHost || PlayerPrefab is null ) return;

		if ( !_reconcileNow && _nextReconcile > 0f ) return;
		_reconcileNow = false;
		_nextReconcile = 0.25f;

		try
		{
			bool wantBots = !BotsOnlyDuringRound || (LobbyDirector.Current?.State == LobbyState.Active);
			ReconcileBots( wantBots ? Math.Max( 0, BotCount ) : 0 );

			foreach ( var conn in Connection.All.ToList() )
			{
				if ( conn is null || !conn.IsActive ) continue;
				if ( _pawns.TryGetValue( conn.Id, out var pawn ) && pawn.IsValid() ) continue;
				var id = conn.Id;
				_pawns[id] = FindConnectionPawn( id ) ?? SpawnPawn( false, conn.DisplayName, conn );
			}

			Sweep();
		}
		catch
		{
			// Connection or scene list changed during the pass; retry next frame.
		}
	}

	void ReconcileBots( int target )
	{
		_bots.RemoveAll( b => !b.IsValid() );
		while ( _bots.Count > target )
		{
			var b = _bots[_bots.Count - 1];
			_bots.RemoveAt( _bots.Count - 1 );
			if ( b.IsValid() ) b.Destroy();
		}
		while ( _bots.Count < target )
			_bots.Add( SpawnPawn( true, "Bot", null ) );
	}

	GameObject FindConnectionPawn( Guid id )
	{
		foreach ( var a in Scene.GetAllComponents<ILobbyAgent>() )
		{
			if ( !a.IsValid() || a.IsBot ) continue;
			if ( a is Component c && c.Network.OwnerId == id ) return c.GameObject;
		}
		return null;
	}

	void Sweep()
	{
		if ( _nextSweep > 0f ) return;
		_nextSweep = 1f;
		foreach ( var key in _pawns.Where( kv => !kv.Value.IsValid() ).Select( kv => kv.Key ).ToList() )
			_pawns.Remove( key );
	}

	GameObject SpawnPawn( bool isBot, string displayName, Connection owner )
	{
		var go = PlayerPrefab.Clone( NextSpawn() );
		go.Name = isBot ? "Bot" : $"Player - {displayName}";
		go.Enabled = true;

		var agent = go.Components.Get<ILobbyAgent>() ?? go.Components.GetInChildren<ILobbyAgent>();
		agent?.InitAgent( isBot, displayName );

		if ( isBot )
		{
			var rend = go.Components.GetInChildren<SkinnedModelRenderer>();
			if ( rend is not null ) rend.Tint = BotTint;
		}

		if ( owner is not null ) go.NetworkSpawn( owner );
		else go.NetworkSpawn();
		return go;
	}

	Vector3 NextSpawn()
	{
		int idx = _spawnIndex++;
		var dir = LobbyDirector.Current;
		if ( dir is not null && dir.UseRoundMap && dir.MapReady )
			return dir.RoundSpawnPoint( idx );
		return _spawns[idx % _spawns.Length];
	}
}
namespace LobbySystem;

/// <summary>Lifecycle of the lobby: Lobby, then Active, then Ended before looping back.</summary>
public enum LobbyState
{
	/// <summary>Free roam before and after a round. The mode button works here.</summary>
	Lobby,
	/// <summary>A round is running.</summary>
	Active,
	/// <summary>Round finished; results show before returning to the lobby.</summary>
	Ended
}
namespace LobbySystem;

/// <summary>
/// In-world button that opens the mode menu for the host, or a local suggestion menu for a client. It needs
/// a ModelRenderer to be visible and is hidden while a round is live. When the local player is within
/// <see cref="UseRange"/> and presses Use, the menu opens.
/// </summary>
public sealed class LobbyModeButton : Component
{
	[Property] public float UseRange { get; set; } = 130f;
	[Property] public bool GlowWhenInRange { get; set; } = true;
	[Property] public Color IdleTint { get; set; } = new Color( 0.85f, 0.4f, 0.15f );
	[Property] public Color ActiveTint { get; set; } = new Color( 1f, 0.85f, 0.3f );

	ModelRenderer _renderer;
	ILobbyAgent _me;

	protected override void OnStart()
	{
		_renderer = Components.Get<ModelRenderer>() ?? Components.GetInChildren<ModelRenderer>();
		if ( _renderer is not null ) _renderer.Tint = IdleTint;
	}

	protected override void OnUpdate()
	{
		var dir = LobbyDirector.Current;
		bool inLobby = dir is null || !dir.RoundLive;
		if ( _renderer is not null && _renderer.Enabled != inLobby )
			_renderer.Enabled = inLobby;
		if ( !inLobby ) return;

		var me = LocalPlayer();
		bool inRange = me is not null && WorldPosition.Distance( me.WorldPosition ) <= UseRange;

		if ( GlowWhenInRange && _renderer is not null )
			_renderer.Tint = inRange ? ActiveTint : IdleTint;

		if ( inRange && Input.Pressed( "Use" ) )
			dir?.RequestModeMenu();
	}

	ILobbyAgent LocalPlayer()
	{
		if ( _me is not null && _me.IsValid() && !_me.IsBot && !_me.IsProxy ) return _me;
		try { _me = Scene.GetAllComponents<ILobbyAgent>().FirstOrDefault( c => c.IsValid() && !c.IsBot && !c.IsProxy ); }
		catch { _me = null; }
		return _me;
	}
}
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "MC Clouds" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "mcclouds" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "trend" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "trend.mcclouds" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "26" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-06-16T17:04:05.4666731Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.124.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.124.0")]
using Sandbox;
using Sandbox.UI;
using System;

namespace SbTween;

public static class LightExtensions
{

	public static BaseTween TweenLightColor( this Light Light, Color target, float duration )
	{
		Color start = Light.LightColor;
		var tween = new BaseTween( duration );
		tween.Target = Light.GameObject;
		return TweenManager.Instance.AddTween( tween
			.OnStart( () => start = Light.LightColor )
			.OnUpdate( p => Light.LightColor = Color.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenRadius( this SpotLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.Radius;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.Radius )
			.OnUpdate( p => light.Radius = MathX.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenConeOuter( this SpotLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.ConeOuter;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.ConeOuter )
			.OnUpdate( p => light.ConeOuter = MathX.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenInnerCone( this SpotLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.ConeInner;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.ConeInner )
			.OnUpdate( p => light.ConeInner = MathX.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenAttenuation( this PointLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.Attenuation;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.Attenuation )
			.OnUpdate( p => light.Attenuation = MathX.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenRadius( this PointLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.Radius;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.Radius )
			.OnUpdate( p => light.Radius = MathX.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenAttenuation( this SpotLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.Attenuation;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.Attenuation )
			.OnUpdate( p => light.Attenuation = MathX.Lerp( start, target, p ) ) );
	}

	//FLICKERING LIGHT
	public static BaseTween TweenFlickerLight( this PointLight light, float minBrightness, float maxBrightness, float duration, float speed = 10f )
	{
		if ( !light.IsValid() ) return null;

		float time = 0f;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnUpdate( _ =>
			{
				time += Time.Delta * speed;
				float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
				float t = (noise + 1f) * 0.5f;
				light.Attenuation = MathX.Lerp( minBrightness, maxBrightness, t );
			} ) );
	}

	public static BaseTween TweenFlickerLight( this SpotLight light, float minBrightness, float maxBrightness, float duration, float speed = 10f )
	{
		if ( !light.IsValid() ) return null;

		float time = 0f;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnUpdate( _ =>
			{
				time += Time.Delta * speed;
				float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
				float t = (noise + 1f) * 0.5f;
				light.Attenuation = MathX.Lerp( minBrightness, maxBrightness, t );
			} ) );
	}

	//FLICKERING Color

	public static BaseTween TweenFlickerColor( this PointLight light, Color colorA, Color colorB, float duration, float speed = 10f )
	{
		if ( !light.IsValid() ) return null;

		float time = 0f;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnUpdate( _ =>
			{
				time += Time.Delta * speed;
				float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
				float t = (noise + 1f) * 0.5f;
				light.LightColor = Color.Lerp( colorA, colorB, t );
			} ) );
	}

	public static BaseTween TweenFlickerColor( this SpotLight light, Color colorA, Color colorB, float duration, float speed = 10f )
	{
		if ( !light.IsValid() ) return null;

		float time = 0f;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnUpdate( _ =>
			{
				time += Time.Delta * speed;
				float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
				float t = (noise + 1f) * 0.5f;
				light.LightColor = Color.Lerp( colorA, colorB, t );
			} ) );
	}

}
using Sandbox;
using System;

namespace SbTween;

public static class AudioExtensions
{
	public static BaseTween TweenVolume( this SoundPointComponent sound, float targetVolume, float duration )
	{
		float startVolume = sound.Volume;
		var tween = new BaseTween( duration );
		tween.Target = sound.GameObject;

		return TweenManager.Instance.AddTween( tween
			.OnStart( () => startVolume = sound.Volume )
			.OnUpdate( p =>
			{
				if ( !sound.IsValid() ) return;
				sound.Volume = MathX.Lerp( startVolume, targetVolume, p );
			} )
		);
	}
	
	public static BaseTween TweenPitch( this SoundPointComponent sound, float targetPitch, float duration )
	{
		float startPitch = sound.Pitch;
		var tween = new BaseTween( duration );
		tween.Target = sound.GameObject;

		return TweenManager.Instance.AddTween( tween
			.OnStart( () => startPitch = sound.Pitch )
			.OnUpdate( p =>
			{
				if ( !sound.IsValid() ) return;
				sound.Pitch = MathX.Lerp( startPitch, targetPitch, p );
			} )
		);
	}
}
using Sandbox;
using System;

namespace SbTween;

public static class MathTweenExtensions
{
	public static BaseTween TweenInCircle( this GameObject obj, float duration, Vector3 axis, float range, float speed, bool snapping = false )
	{
		Vector3 centerPos = obj.WorldPosition;
		var tween = new BaseTween( duration );
		tween.Target = obj;

		Vector3 normal = axis.Normal;
		Vector3 v1 = Vector3.Cross( normal, MathF.Abs( normal.z ) < 0.9f ? Vector3.Up : Vector3.Forward ).Normal;
		Vector3 v2 = Vector3.Cross( normal, v1 ).Normal;

		return TweenManager.Instance.AddTween( tween
			.OnUpdate( p =>
			{
				if ( !obj.IsValid() ) return;

				float angleDegrees = p * 360f * speed;
				float angleRadians = angleDegrees * (MathF.PI / 180f);

				float cos = MathF.Cos( angleRadians ) * range;
				float sin = MathF.Sin( angleRadians ) * range;

				Vector3 rotatedOffset = (v1 * cos) + (v2 * sin);
				obj.WorldPosition = centerPos + rotatedOffset;
			} )
		);
	}

	public static BaseTween TweenSpiral( this GameObject obj, float duration, Vector3 axis, float speed, float frequency )
	{
		Vector3 startPos = obj.WorldPosition;
		var tween = new BaseTween( duration );
		tween.Target = obj;

		return TweenManager.Instance.AddTween( tween
			.OnStart( () => startPos = obj.WorldPosition )
			.OnUpdate( p =>
			{
				if ( !obj.IsValid() ) return;

				float angle = p * MathF.PI * 2f * frequency;

				float currentRadius = p * speed;

				float x = MathF.Cos( angle ) * currentRadius;
				float y = MathF.Sin( angle ) * currentRadius;

				Vector3 axisOffset = axis * p;

				Vector3 circleOffset = new Vector3( x, y, 0 );

				obj.WorldPosition = startPos + axisOffset + circleOffset;
			} )
		);
	}
	
	public static BaseTween TweenPunchFloat( this GameObject obj, float v, float amplitude, float duration, int vibrations = 5, float elasticity = 1f, Action<float> setter = null )
	{
		var tween = new BaseTween( duration );
		tween.Target = obj;

		return TweenManager.Instance.AddTween( tween
			.OnUpdate( p =>
			{
				if ( !obj.IsValid() ) return;
				if ( p >= 1.0f )
				{
					setter?.Invoke( v );
					return;
				}
				
				float decay = MathF.Pow( 1f - p, elasticity * 3f );

				float omega = vibrations * MathF.PI * 2f;
				float oscillation = MathF.Sin( p * omega );

				float currentOffset = amplitude * oscillation * decay;

				setter?.Invoke( v + currentOffset );
			} )
			.OnComplete( () => setter?.Invoke( v ) )
		);
	}
	
	public static BaseTween TweenShakeFloat( this GameObject obj, float baseline, float strength, float duration, Action<float> setter = null )
	{
		var tween = new BaseTween( duration );
		tween.Target = obj;

		return TweenManager.Instance.AddTween( tween
			.OnUpdate( p =>
			{
				if ( !obj.IsValid() ) return;
				if ( p >= 1.0f )
				{
					setter?.Invoke( baseline );
					return;
				}

				float currentStrength = strength * (1.0f - p);
             
				float randomOffset = Game.Random.Float( -currentStrength, currentStrength );

				setter?.Invoke( baseline + randomOffset );
			} )
			.OnComplete( () => setter?.Invoke( baseline ) )
		);
	}
}
namespace Skateboard.Entities;

public sealed class GrindPathNodeEntity : Component
{
}
using SWB.Base.UI;

namespace SWB.Base.Attachments;

public abstract class Scope2DAttachment : Attachment
{
	public override string Name => "2D Scope";
	public override AttachmentCategory Category => AttachmentCategory.Sight;
	public override string Description => "A high magnification scope that is utilized for firing at long ranges.";
	public override string[] Positives => new string[]
	{
		"x12 magnification",
		"100% accurate while scoped in"
	};

	public override string[] Negatives => new string[]
	{
	};

	/// <summary>The new aim position offset</summary>
	[Property, Group( "Scope" )] public AngPos AimAnimData { get; set; }
	AngPos oldAimAnimData;

	/// <summary>Scope Information</summary>
	[Property, Group( "Scope" )] public virtual ScopeInfo ScopeInfo { get; set; } = new();
	ScopeInfo oldScopeInfo = new();

	SniperScope sniperScope;

	public override void OnEquip()
	{
		oldAimAnimData = Weapon.AimAnimData;
		oldScopeInfo = Weapon.ScopeInfo;

		Weapon.Scoping = true;
		Weapon.AimAnimData = AimAnimData;
		Weapon.ScopeInfo = ScopeInfo;
	}

	public override void OnUnequip()
	{
		Weapon.Scoping = false;
		Weapon.AimAnimData = oldAimAnimData;
		Weapon.ScopeInfo = oldScopeInfo;
	}

	public override void CreateHudElements()
	{
		base.CreateHudElements();
		sniperScope = new SniperScope( Weapon, Weapon.ScopeInfo.LensTexture, Weapon.ScopeInfo.ScopeTexture );
		Weapon.RootPanel.Panel.AddChild( sniperScope );
	}

	public override void DestroyHudElements()
	{
		base.DestroyHudElements();
		sniperScope?.Delete();
	}
}
namespace SWB.Base;

public class CrosshairSettings
{
	[Property] public bool Enabled { get; set; } = true;
	[Property] public bool ShowDot { get; set; } = true;
	[Property] public bool ShowTop { get; set; } = true;
	[Property] public bool ShowBottom { get; set; } = true;
	[Property] public bool ShowLeft { get; set; } = true;
	[Property] public bool ShowRight { get; set; } = true;
}
namespace SWB.Base;

public class ScopeInfo
{
	/// <summary>2D lens texture</summary>
	[Property, ImageAssetPathAttribute] public string LensTexture { get; set; } = "/materials/swb/scopes/swb_lens_hunter.png";

	/// <summary>2D scope texture</summary>
	[Property, ImageAssetPathAttribute] public string ScopeTexture { get; set; } = "/materials/swb/scopes/swb_scope_hunter.png";

	/// <summary>Delay between ADS and scoping in ms</summary>
	[Property] public float ScopeInDelay { get; set; } = 0.2f;

	/// <summary>Sound that plays when scoping starts</summary>
	[Property] public SoundEvent ScopeInSound { get; set; }

	/// <summary>Sound that plays when scoping ends</summary>
	[Property] public SoundEvent ScopeOutSound { get; set; }

	/// <summary>Player FOV while scoping</summary>
	[Property] public float FOV { get; set; } = 8f;

	/// <summary>Mouse sensitivity while scoping (lower is slower, 0 to use AimInfo.Sensitivity while scoped)</summary>
	[Property] public float Sensitivity { get; set; } = 0.25f;

	/// <summary>Spread while scoping (lower is more accurate, 0 = perfect, 1 = normal )</summary>
	[Property] public float Spread { get; set; } = 0f;
}
namespace SWB.Base;

public partial class Weapon
{
	// Burst Fire
	public void ResetBurstFireCount( ShootInfo shootInfo, string inputButton )
	{
		if ( shootInfo is null || shootInfo.FiringType != FiringType.burst ) return;

		if ( Input.Released( inputButton ) )
		{
			burstCount = 0;
		}
	}

	// Barrel heat
	public void BarrelHeatCheck()
	{
		if ( TimeSincePrimaryShoot > 3 && TimeSinceSecondaryShoot > 0 )
		{
			barrelHeat = 0;
		}
	}

	// Tucking
	public virtual float GetTuckDist()
	{
		if ( TuckRange == -1 )
			return -1;

		if ( !Owner.IsValid ) return -1;

		// Increase tuck range around ladders
		var tuckRange = Owner.IsClimbingLadder ? TuckRange * 1.25f : TuckRange;
		var pos = Owner.EyePos;
		var forward = Owner.EyeAngles.ToRotation().Forward;
		var trace = TraceBullet( Owner.EyePos, pos + forward * tuckRange, 2, TuckingTraceIgnoreTags );

		if ( !trace.Hit )
			return -1;

		return trace.Distance;
	}

	public bool ShouldTuck( out float dist )
	{
		dist = GetTuckDist();
		return dist != -1;
	}
}
using SWB.Base.Particles;
using SWB.Shared;
using System;

namespace SWB.Base;

public partial class Weapon
{
	public static readonly string[] BulletTraceIgnoreTags =
	{
		TagsHelper.Trigger,
		TagsHelper.PlayerClip,
		TagsHelper.PassBullets,
		TagsHelper.ViewModel,
		TagsHelper.Sky
	};

	public static readonly string[] TuckingTraceIgnoreTags =
	[
		..BulletTraceIgnoreTags,
		TagsHelper.Player,
		TagsHelper.DeadPlayer
	];

	/// <summary>
	/// Checks if the weapon can do the provided attack
	/// </summary>
	/// <param name="shootInfo">Attack information</param>
	/// <param name="lastAttackTime">Time since this attack</param>
	/// <param name="inputButton">The input button for this attack</param>
	/// <returns></returns>
	public virtual bool CanShoot( ShootInfo shootInfo, TimeSince lastAttackTime, string inputButton )
	{
		if ( (IsReloading && !ShellReloading) || (IsReloading && ShellReloading && !ShellReloadingShootCancel) || InBoltBack ) return false;
		if ( shootInfo is null || !Owner.IsValid() || (!Owner.IsBot && !Input.Down( inputButton )) || ((IsRunning || TimeSinceRunning < 0.1f) && Secondary is null) ) return false;
		if ( !HasAmmo() )
		{
			if ( Input.Pressed( inputButton ) )
			{
				// Check for auto reloading
				if ( Settings.AutoReload && Owner.AmmoCount( shootInfo.AmmoType ) > 0 && lastAttackTime > GetRealRPM( shootInfo.RPM ) )
				{
					TimeSincePrimaryShoot = 999;
					TimeSinceSecondaryShoot = 999;

					if ( ShellReloading )
						OnShellReload();
					else
						Reload();

					return false;
				}

				// Dry fire
				if ( shootInfo.DryShootSound is not null )
					PlaySound( shootInfo.DryShootSound );
			}

			return false;
		}

		if ( shootInfo.FiringType == FiringType.semi && !Owner.IsBot && !Input.Pressed( inputButton ) ) return false;
		if ( shootInfo.FiringType == FiringType.burst )
		{
			if ( burstCount > 2 ) return false;

			if ( (Owner.IsBot || Input.Down( inputButton )) && lastAttackTime > GetRealRPM( shootInfo.RPM ) )
			{
				burstCount++;
				return true;
			}

			return false;
		}
		;

		if ( shootInfo.RPM <= 0 ) return true;

		return lastAttackTime > GetRealRPM( shootInfo.RPM );
	}

	/// <summary>
	/// Checks if weapon can do the primary attack
	/// </summary>
	public virtual bool CanPrimaryShoot()
	{
		return CanShoot( Primary, TimeSincePrimaryShoot, InputButtonHelper.PrimaryAttack );
	}

	/// <summary>
	/// Checks if weapon can do the secondary attack
	/// </summary>
	public virtual bool CanSecondaryShoot()
	{
		return CanShoot( Secondary, TimeSinceSecondaryShoot, InputButtonHelper.SecondaryAttack );
	}

	public virtual void Shoot( ShootInfo shootInfo, bool isPrimary )
	{
		// Ammo
		if ( shootInfo.InfiniteAmmo != InfiniteAmmoType.clip )
			shootInfo.Ammo -= 1;

		// Animations
		var shootAnim = GetShootAnimation( shootInfo );
		if ( !string.IsNullOrEmpty( shootAnim ) )
			ApplyShootAnimation( shootAnim );

		// Sound
		if ( shootInfo.ShootSound is not null )
			PlaySound( shootInfo.ShootSound );

		// Particles
		HandleShootEffects( isPrimary );

		if ( !Owner.IsBot )
		{
			// Barrel smoke
			barrelHeat += 1;

			// Recoil
			Owner.ApplyEyeAnglesOffset( GetRecoilAngles( shootInfo ) );

			// Screenshake
			if ( shootInfo.ScreenShake is not null )
				Owner.ShakeScreen( shootInfo.ScreenShake );

			// UI
			BroadcastUIEvent( "shoot", GetRealRPM( shootInfo.RPM ) );
		}

		// Bullet
		for ( int i = 0; i < shootInfo.Bullets; i++ )
		{
			var realSpread = GetRealSpread( shootInfo.Spread );
			var spreadOffset = shootInfo.BulletType.GetRandomSpread( realSpread );
			shootInfo?.BulletType?.Shoot( this, isPrimary, spreadOffset );
		}
	}

	protected virtual void ApplyShootAnimation( string anim )
	{
		ViewModelRenderer?.Set( anim, true );
	}

	/// <summary> A single bullet trace from start to end with a certain radius.</summary>
	public static SceneTraceResult TraceBullet( GameObject toIgnoreGO, Vector3 start, Vector3 end, float radius = 2.0f, string[] ignoreTags = null )
	{
		// TODO: find another solution when water becomes more available
		// var startsInWater = SurfaceUtil.IsPointWater( start );
		// if ( startsInWater )
		//	 withoutTags.Add( TagsHelper.Water );

		var tr = Game.ActiveScene.Trace.Ray( start, end )
				.UseHitboxes()
				.WithoutTags( ignoreTags ?? BulletTraceIgnoreTags )
				.Size( radius )
				.IgnoreGameObjectHierarchy( toIgnoreGO )
				.Run();

		// Log.Info( tr.GameObject );

		return tr;
	}

	/// <summary> A single bullet trace from start to end with a certain radius.</summary>
	public virtual SceneTraceResult TraceBullet( Vector3 start, Vector3 end, float radius = 2.0f, string[] ignoreTags = null )
	{
		return TraceBullet( Owner.GameObject, start, end, radius, ignoreTags );
	}

	[Rpc.Broadcast( NetFlags.Unreliable )]
	public virtual void HandleShootEffects( bool isPrimary )
	{
		if ( !IsValid || Owner is null || Application.IsDedicatedServer ) return;

		// Player
		Owner.TriggerAnimation( Shared.Animations.Attack );

		// Weapon
		var shootInfo = GetShootInfo( isPrimary );
		if ( shootInfo is null ) return;

		// Bullet eject
		if ( shootInfo.BulletEjectParticle is not null )
		{
			if ( !BoltBack )
			{
				if ( !ShellReloading || (ShellReloading && ShellEjectDelay == 0) )
				{
					CreateBulletEjectParticle( shootInfo.BulletEjectParticle, "ejection_point" );
				}
				else
				{
					var delayedEject = async () =>
					{
						await GameTask.DelaySeconds( ShellEjectDelay );
						if ( !IsValid ) return;
						CreateBulletEjectParticle( shootInfo.BulletEjectParticle, "ejection_point" );
					};
					delayedEject();
				}
			}
			else if ( shootInfo.Ammo > 0 )
			{
				AsyncBoltBack( GetRealRPM( shootInfo.RPM ) );
			}
		}

		var muzzleObj = GetMuzzleObject();
		if ( muzzleObj is null ) return;

		var muzzleScale = CanSeeViewModel ? shootInfo.VMParticleScale : shootInfo.WMMuzzleParticleScale;

		// Muzzle flash
		if ( shootInfo.MuzzleFlashParticle is not null )
			CreateParticle( shootInfo.MuzzleFlashParticle, muzzleObj, muzzleScale );

		// Barrel smoke
		if ( !IsProxy && !Owner.IsBot && shootInfo.BarrelSmokeParticle is not null && barrelHeat >= shootInfo.ClipSize * 0.75 )
			CreateParticle( shootInfo.BarrelSmokeParticle, muzzleObj, muzzleScale );
	}

	/// <summary>Create a bullet impact effect</summary>
	public static GameObject CreateBulletImpact( SceneTraceResult tr )
	{
		return CreateBulletImpact( tr.HitPosition, tr.Normal, tr.Surface?.SoundCollection.Bullet, tr.Surface?.PrefabCollection.BulletImpact );
	}

	/// <summary>Create a bullet impact effect</summary>
	public static GameObject CreateBulletImpact( Vector3 pos, Vector3 normal, SoundEvent sound, GameObject particles )
	{
		// Sound
		SoundHandle soundHandle = null;

		if ( sound is not null )
		{
			sound.Distance = 10000;
			soundHandle = Sound.Play( sound );
		}

		soundHandle ??= Sound.Play( "impact-bullet-generic" );
		soundHandle.Position = pos;

		// Decal & Particles
		if ( !particles.IsValid() ) return null;

		var cloneConfig = new CloneConfig()
		{
			Name = "bullet_decal",
			StartEnabled = true,
			Transform = new()
			{
				Position = pos,
				Rotation = Rotation.LookAt( -normal ),
			},
			//Parent = tr.GameObject,
		};
		var decalGO = particles.Clone( cloneConfig );
		decalGO.NetworkMode = NetworkMode.Never;
		decalGO.DestroyAsync( 30f );

		WeaponParticleManager.Instance?.AddDecal( decalGO );

		return decalGO;
	}

	/// <summary>Create a bullet eject particle (always world)</summary>
	public virtual GameObject CreateBulletEjectParticle( GameObject particle, string attachment, Action<GameObject> OnParticleCreated = null )
	{
		var effectRenderer = GetEffectRenderer();
		if ( effectRenderer is null || effectRenderer.SceneModel is null ) return null;

		var transform = effectRenderer.GetAttachment( attachment );
		if ( !transform.HasValue ) return null;

		// Rotate bullet with attachment yaw
		var spawnInViewSpace = CanSeeViewModel && !IsScoping;
		var pitch = spawnInViewSpace ? ViewModelHandler.WorldRotation.Pitch() : WorldRotation.Pitch();
		var yaw = transform.Value.Rotation.Yaw();
		var newRot = Rotation.From( new Angles( 0, yaw, -pitch ) );
		transform = transform.Value.WithRotation( newRot );

		if ( spawnInViewSpace )
		{
			var viewSpacePos = CameraUtil.ProjectToViewSpace( transform.Value.Position, Owner.ViewModelCamera, Owner.Camera );
			transform = transform.Value.WithPosition( viewSpacePos );
		}

		var go = CreateParticle( particle, null, transform.Value, 1, false, OnParticleCreated );
		WeaponParticleManager.Instance?.AddEject( go );

		// Attach owner
		var ejectParticle = go.GetComponentInChildren<BulletEjectParticle>();
		ejectParticle?.Owner = Owner;

		return go;
	}

	/// <summary>Create a weapon particle</summary>
	public virtual GameObject CreateParticle( GameObject particle, GameObject parent, float scale, Action<GameObject> OnParticleCreated = null )
	{
		return CreateParticle( particle, parent, new Transform(), scale, OnParticleCreated );
	}

	/// <summary>Create a weapon particle</summary>
	public virtual GameObject CreateParticle( GameObject particle, Transform transform, float scale, Action<GameObject> OnParticleCreated = null )
	{
		return CreateParticle( particle, null, transform, scale, OnParticleCreated );
	}

	/// <summary>Create a weapon particle</summary>
	public virtual GameObject CreateParticle( GameObject particle, GameObject parent, Transform transform, float scale, Action<GameObject> OnParticleCreated = null )
	{
		return CreateParticle( particle, parent, transform, scale, CanSeeViewModel, OnParticleCreated );
	}

	public virtual GameObject CreateParticle( GameObject particle, GameObject parent, Transform transform, float scale, bool forViewModel, Action<GameObject> OnParticleCreated = null )
	{
		var go = particle.Clone( transform.WithScale( scale ), parent );

		if ( forViewModel )
			go.Tags.Add( TagsHelper.ViewModel );

		if ( OnParticleCreated is not null )
		{
			var p = go.GetComponentInChildren<ParticleEffect>();
			p.OnParticleCreated += ( p ) =>
			{
				OnParticleCreated.Invoke( go );
			};
		}

		return go;
	}
}
using Sandbox.UI;
using Sandbox.UI.Construct;
using SWB.Base;
using SWB.Player;
using SWB.Shared;
using System;

namespace SWB.HUD;

public class AmmoDisplay : Panel
{
	PlayerBase player;

	Label clipLabel;
	Label reserveLabel;

	Color reserveColor = new Color32( 200, 200, 200 ).ToColor();
	Color emptyColor = new Color32( 175, 175, 175, 200 ).ToColor();

	public AmmoDisplay( PlayerBase player )
	{
		this.player = player;
		StyleSheet.Load( "/swb_hud/AmmoDisplay.cs.scss" );

		Add.Label( "ammo", "name" );
		clipLabel = Add.Label( "", "clip" );
		reserveLabel = Add.Label( "", "reserve" );
	}

	public override void Tick()
	{
		var isAlive = player.IsAlive;
		var weapon = player.Inventory.Active?.Components.Get<Weapon>();
		var hide = !isAlive || weapon is null;
		SetClass( "hide", hide );
		if ( hide ) return;

		var hasClipSize = weapon.Primary.ClipSize > 0;
		var reserveAmmo = Math.Min( player.AmmoCount( weapon.Primary.AmmoType ), 999 );
		var hasAmmo = weapon.Primary.Ammo > 0;

		if ( weapon.Primary.InfiniteAmmo != InfiniteAmmoType.clip )
		{
			var clipAmmo = hasClipSize ? weapon.Primary.Ammo : reserveAmmo;
			clipAmmo = Math.Min( clipAmmo, 999 );

			clipLabel.Text = clipAmmo.ToString();
			clipLabel.Style.FontColor = clipAmmo == 0 ? emptyColor : Color.White;
		}
		else
		{
			clipLabel.Text = "∞";
			clipLabel.Style.FontColor = Color.White;
		}

		if ( hasClipSize )
		{
			if ( weapon.Primary.InfiniteAmmo == 0 )
			{
				reserveLabel.Text = "  /  " + reserveAmmo;
				reserveLabel.Style.FontColor = reserveAmmo == 0 ? emptyColor : reserveColor;
			}
			else
			{
				reserveLabel.Text = "  /  ∞";
				reserveLabel.Style.FontColor = reserveColor;
			}
		}
		else if ( hasAmmo )
		{
			clipLabel.Text = weapon.Primary.Ammo.ToString();
		}
		else
		{
			reserveLabel.Text = "";
		}
	}
}
using System;
using System.Collections.Generic;

/* 
 * Utility class for tables
*/

namespace SWB.Shared;

class TableUtil
{
	public static T GetRandom<T>( IList<T> list )
	{
		if ( list.Count == 0 ) return default;

		var randI = Random.Shared.Next( list.Count );
		return list[randI];
	}
}
#nullable enable

using Sandbox;

public abstract class Item
{
    public Scene? Scene {get;set;} = null;
    public GameObject? GameObject {get;set;} = null;
    public string Name {get;set;} = "empty";
    
    public abstract Item Init();
    public abstract void Act();
    public abstract void Drop();
}
using Sandbox;

public class SCP_173_SFX : Component
{
    [Property] public CharacterController Character_Controller {get;set;}
    [Property] public SoundPointComponent Rattle {get;set;}
    
    protected override void OnUpdate()
    { if (IsProxy) return;
        if (Character_Controller.IsOnGround && Character_Controller.Velocity.LengthSquared > 0
        && !GameObject.Tags.Has("seen")) {Rattle.StartSound(); return;} Rattle.StopSound();
    }
}
public class CharmRerollConsecutive : Charm
{
	public const string ItemId = "charm_reroll_consecutive";

	public static string Description() => $"See +1 perk choice for\neach consecutive reroll";

	private int _consecutiveRerolls = 0;

	public override void OnRerollBefore()
	{
		base.OnRerollBefore();

		_consecutiveRerolls++;
		Player.Modify( this, PlayerStat.NumPerkChoices, _consecutiveRerolls, ModifierType.Add );
	}

	public override void OnChoosePerk( TypeDescription type )
	{
		base.OnChoosePerk( type );

		_consecutiveRerolls = 0;
		Player.StopModifying( this, PlayerStat.NumPerkChoices );
	}

	public override void OnRunStart()
	{
		base.OnRunStart();

		_consecutiveRerolls = 0;
	}
}
using System;
using Sandbox;

public class FallingObject : Component
{
	public Player Shooter { get; set; }

	public TimeSince TimeSinceSpawn { get; set; }
	public float Lifetime { get; set; }
	public float FallProgress { get; set; }

	protected float _startingHeight = 1024f;

	public bool HasHitGround { get; set; }

	protected override void OnStart()
	{
		base.OnStart();

		TimeSinceSpawn = 0f;

		if ( IsProxy )
			return;

	}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		if ( IsProxy )
			return;

		if ( TimeSinceSpawn > Lifetime )
		{
			if( !HasHitGround )
				HitGround();
		}
		else
		{
			FallProgress = Utils.Map( TimeSinceSpawn, 0f, Lifetime, 0f, 1f );
			WorldPosition = new Vector3( WorldPosition.x, WorldPosition.y, Utils.Map( FallProgress, 0f, 1f, _startingHeight, 0f ) );
		}
	}

	public virtual void HitGround()
	{
		GameObject.Destroy();
	}
}
using System;
using Sandbox;

public class Globals
{
	
}
public class GunActiveReload : Gun
{
	public const string ItemId = "gun_active_reload";

	public static string Description() => $"Start with {Perk.GetRichTextNameToken( typeof( PerkActiveReload ) )} {Perk.GetRichTextToken( nameof(PerkActiveReload) )}";

	public override void OnRunStart()
	{
		base.OnRunStart();

		Manager.Instance.Chat.AddLocalChatMessage( $"Got {Perk.GetRichTextNameToken( typeof( PerkActiveReload ) )} {Perk.GetRichTextToken( nameof( PerkActiveReload ) )}", from: "" );

		Player.AddPerk( TypeLibrary.GetType( typeof( PerkActiveReload ) ) );
	}
}
/// <summary>
/// Marker interface for anything that can register stat modifiers on a Player via Player.Modify.
/// Implemented by Perk, Gun, Charm, and Gem.
/// </summary>
public interface IStatModifier { }
using System;
using Sandbox;

public class LightningParticleEffect : Component
{
	[Property] public ParticleEffect ParticleEffect { get; set; }
	[Property] public ParticleSpriteRenderer ParticleRenderer { get; set; }
	[Property] public ParticleRingEmitter RingEmitter { get; set; }

	private const float CHARGE_TIME = 3f;
	private const float DELAY = 0.5f;

	private TimeSince _timeSinceReset;

	protected override void OnStart()
	{
		base.OnStart();

		_timeSinceReset = 0f;
	}

	[Rpc.Broadcast]
	public void ResetEffect()
	{
		if ( !IsProxy ) // only for client effect
			return;

		if ( !ParticleRenderer.Enabled )
			return;

		_timeSinceReset = 0f;

		RingEmitter.Rate = 500;
		RingEmitter.Radius = 30f;
		ParticleEffect.Alpha = 0f;
	}

	protected override void OnUpdate()
	{
		if ( !IsProxy ) // only for client effect
			return;

		if ( _timeSinceReset < DELAY )
			return;

		float progress = Utils.Map( _timeSinceReset, 0f, DELAY + CHARGE_TIME, 0f, 1f );

		RingEmitter.Rate = 500;
		RingEmitter.Radius = Utils.Map( progress, 0f, 1f, 30f, 0f, EasingType.SineIn );
		ParticleEffect.Alpha = Utils.Map( progress, 0f, 1f, 0f, 1f, EasingType.ExpoIn );
	}

	[Rpc.Broadcast]
	public void SetVisible( bool visible )
	{
		ParticleRenderer.Enabled = visible;
	}
}
using System;
using Sandbox;

[Perk( Rarity.Unique, curse: true, alwaysOfferDebug: false )]
public class CurseObscureScreen : Perk
{
	private const float TOTAL_TIME = 5f;
	private const float FADE_IN_TIME = 0.2f;
	private const float FADE_OUT_START = 4.5f;
	private const float MAX_INTENSITY = 2f;

	private TimeSince _timeSinceHurt;
	private bool _isActive;
	private float _fadeInStartIntensity;

	static CurseObscureScreen()
	{
		Register<CurseObscureScreen>(
			name: "Blindness",
			imagePath: "textures/icons/vector/curse_obscure_screen.png",
			description: level => $"Reduce vision for {TOTAL_TIME}s when hit"
		);
	}

	public override void Start()
	{
		base.Start();
	}

	public override void Update( float dt )
	{
		base.Update( dt );

		if ( !_isActive ) 
			return;

		var obscure = Manager.Instance.OverlayEffects[OverlayEffectsType.Obscure];
		var vignette = obscure.GetComponent<Vignette>();

		if ( _timeSinceHurt > TOTAL_TIME )
		{
			obscure.Enabled = false;
			_isActive = false;
			ShouldUpdate = false;
			DisplayText = " ";
			DisplayCooldown = 0f;
		}
		else
		{
			float intensity;

			if ( _timeSinceHurt < FADE_IN_TIME )
				intensity = Utils.Map( _timeSinceHurt, 0f, FADE_IN_TIME, _fadeInStartIntensity, MAX_INTENSITY, EasingType.SineOut );
			else if ( _timeSinceHurt < FADE_OUT_START )
				intensity = MAX_INTENSITY;
			else
				intensity = Utils.Map( _timeSinceHurt, FADE_OUT_START, TOTAL_TIME, MAX_INTENSITY, 0f, EasingType.SineIn );

			vignette.Intensity = intensity;

			DisplayText = $"{MathX.CeilToInt( TOTAL_TIME - _timeSinceHurt )}";
			DisplayCooldown = Utils.Map( _timeSinceHurt, 0f, TOTAL_TIME, 1f, 0f );
		}
	}

	public override void OnHit( float amount, DamageType damageType, bool isSelfInflicted, Vector2 dir, float force, Enemy enemySource, EnemyType enemyType, float previousHealth )
	{
		base.OnHit( amount, damageType, isSelfInflicted, dir, force, enemySource, enemyType, previousHealth );

		if ( damageType == DamageType.Self ) 
			return;

		var obscure = Manager.Instance.OverlayEffects[OverlayEffectsType.Obscure];
		var vignette = obscure.GetComponent<Vignette>();

		_fadeInStartIntensity = _isActive ? vignette.Intensity : 0f;
		obscure.Enabled = true;

		_timeSinceHurt = 0f;
		_isActive = true;
		ShouldUpdate = true;

		DisplayCooldown = 1f;

		HighlightColor = new Color( 1f, 0f, 0.5f );
		HighlightDuration = 0.4f;
		HighlightOpacity = 2.5f;
		Highlight();

		IconScale = Game.Random.Float( 1.1f, 1.2f );
		IconAngleOffset = Game.Random.Float( 8f, 12f ) * (Game.Random.Int( 0, 1 ) == 0 ? -1f : 1f);
	}

	void DisableEffect()
	{
		Manager.Instance.OverlayEffects[OverlayEffectsType.Obscure].Enabled = false;
		_isActive = false;
		ShouldUpdate = false;
	}

	public override void OnDie()
	{
		base.OnDie();
		DisableEffect();
	}

	public override void Remove( bool restart = false )
	{
		base.Remove( restart );

		DisableEffect();
	}
}
using System;
using Sandbox;

[Perk( Rarity.Unique, curse: true, alwaysOfferDebug: false )]
public class CurseXpCoinHurt : Perk
{
	private enum Mod { DmgAmount };

	static CurseXpCoinHurt()
	{
		Register<CurseXpCoinHurt>(
			name: "Sharp Coins",
			imagePath: "textures/icons/vector/curse_xp_coin_hurt.png",
			description: level => $"-{GetValue( level, Mod.DmgAmount ).ToString("0.##")} hp when you get xp coins"
		);
	}

	public override void Start()
	{
		base.Start();

		HighlightColor = new Color( 1f, 0.4f, 0.4f );
		HighlightDuration = 0.4f;
		HighlightOpacity = 0.7f;
	}

	public override void Refresh()
	{
		base.Refresh();

		Player.Modify( this, PlayerStat.CoinFlatDmgAmount, -GetValue( Level, Mod.DmgAmount ), ModifierType.Add );
	}

	public override void OnGainXpCoin( float xp )
	{
		base.OnGainXpCoin( xp );

		Player.Damage( GetValue( Level, Mod.DmgAmount ), DamageType.Self, Player.Position2D, Utils.GetRandomVector(), upwardAmount: 0f, force: 0f, ragdollForce: 1f, enemySource: null, enemyType: EnemyType.None );

		Highlight();

		IconScale = Game.Random.Float( 1.1f, 1.15f );
		IconAngleOffset = Game.Random.Float( 5f, 10f ) * (Game.Random.Int( 0, 1 ) == 0 ? -1f : 1f);
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.DmgAmount:
			default:
				return 3f;
				//switch(level)
				//{
				//	case 1: default: return 1;
				//	case 2: return 3;
				//	case 3: return 6;
				//	case 4: return 10;
				//}
		}
	}
}
using System;
using Sandbox;

[Perk( Rarity.Uncommon, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.Armor })]
public class PerkArmorBuffer : Perk
{
	private enum Mod { MaxArmor };

	private float _timer;

	private const float DELAY = 3f;


	static PerkArmorBuffer()
	{
		Register<PerkArmorBuffer>(
			name: "Armor Buffer",
			imagePath: "textures/icons/vector/armor_buffer.png",
			description: level => $"+1 armor-item every {DELAY}s while\nbelow {(int)GetValue( level, Mod.MaxArmor )} armor-item",
			upgradeDescription: level => $"+1 armor-item every {DELAY}s while\nbelow {(int)GetValue( level - 1, Mod.MaxArmor )}→{(int)GetValue( level, Mod.MaxArmor )} armor-item"
		);
	}

	public override void Start()
	{
		base.Start();

		ShouldUpdate = true;

		HighlightColor = new Color( 0.9f, 0.9f, 1f );
		HighlightDuration = 0.2f;
		HighlightOpacity = 0.5f;
	}

	public override void Refresh()
	{
		base.Refresh();

	}

	public override void Update( float dt )
	{
		base.Update( dt );

		var maxArmor = (int)GetValue( Level, Mod.MaxArmor );

		if ( Player.Armor < maxArmor )
		{
			_timer += dt;
			if ( _timer >= DELAY )
			{
				Player.GainArmor( 1 );
				_timer = 0f;

				Highlight();

				IconScale = Game.Random.Float( 1.1f, 1.15f );
				IconAngleOffset = Game.Random.Float( 5f, 8f ) * (Game.Random.Int( 0, 1 ) == 0 ? -1f : 1f);
			}
		}
		else
		{
			_timer = 0f;
		}

		DisplayCooldown = Player.Armor < maxArmor ? Utils.Map( _timer, 0f, 1f, 0f, 1f ) : 0f;
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.MaxArmor:
			default:
				return level * 2;
		}
	}
}
using System;
using Sandbox;

[Perk( Rarity.Unique, alwaysOfferDebug: false )]
public class PerkAutoAim : Perk
{
	private enum Mod { Time, Radius };

	private TimeSince _timeSinceCheck;

	static PerkAutoAim()
	{
		Register<PerkAutoAim>(
			name: "Aimbot",
			imagePath: "textures/icons/vector/auto_aim.png",
			description: level => $"Auto-aim at the nearest enemy\n+{GetValue( level, Mod.Radius, true ).ToString("0.##")}m bullet homing range",
			upgradeDescription: level => $"Auto-aim at the nearest enemy\n+{GetValue( level, Mod.Radius, true ).ToString( "0.##" )}m bullet homing range"
		);
	}

	public override void Start()
	{
		base.Start();

		ShouldUpdate = true;
	}

	public override void Refresh()
	{
		base.Refresh();

		Player.Modify( this, PlayerStat.AutoAim, 1f, ModifierType.Add );
		Player.Modify( this, PlayerStat.BulletHomingRadius, GetValue( Level, Mod.Radius ), ModifierType.Add );
		Player.Modify( this, PlayerStat.BulletHomingRadiusDisplay, GetValue( Level, Mod.Radius, true ), ModifierType.Add );
	}

	public override void Update( float dt )
	{
		base.Update( dt );

		if ( Player.Stats[PlayerStat.IsBerserk] > 0f )
			return;

		if ( _timeSinceCheck > 0.2f )
		{
			var closestEnemy = Manager.Instance.GetClosestEnemy( Player.Position2D, onlyCountsAsKill: false );
			if ( closestEnemy.IsValid() )
			{
				var dir = (closestEnemy.Position2D - Player.Position2D).Normal;
				Player.AimDir = dir;
			}

			_timeSinceCheck = 0f;
		}
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.Radius:
			default:
				return 1.0f * (isPercent ? 1f : Utils.Meter2Unit);
		}
	}
}
using System;
using Sandbox;

[Perk( Rarity.Legendary, includedAtStart: false, locked: true, alwaysOfferDebug: false )]
public class PerkBulletBounceCopy : Perk
{
	private enum Mod { Chance };

	static PerkBulletBounceCopy()
	{
		Register<PerkBulletBounceCopy>(
			name: "Scatterbounce",
			imagePath: "textures/icons/vector/bullet_bounce_copy.png",
			description: level => $"{(int)GetValue( level, Mod.Chance, true )}% chance for bullet-icon\nto clone on first bounce",
			upgradeDescription: level => $"{(int)GetValue( level - 1, Mod.Chance, true )}%→{(int)GetValue( level, Mod.Chance, true )}% chance for bullet-icon\nto clone on first bounce"
		);
	}

	public override void Start()
	{
		base.Start();

		HighlightColor = new Color( 0.9f, 0.6f, 0.8f );
		HighlightDuration = 0.1f;
		HighlightOpacity = 2f;
	}

	public override void Refresh()
	{
		base.Refresh();

	}

	public override void OnBulletBounce( Bullet bullet, Thing other )
	{
		base.OnBulletBounce( bullet, other );

		if ( (int)bullet.Stats[BulletStat.NumBouncing] != bullet.StartingNumBounce - 1 )
			return;

		if ( Game.Random.Float( 0f, 1f ) > GetValue( Level, Mod.Chance ) )
			return;

		var dmg = bullet.Stats[BulletStat.Damage];
		var dir = Utils.RotateVector( bullet.Velocity.Normal, Game.Random.Float( 15f, 40f ) * (Game.Random.Int( 0, 1 ) == 0 ? -1f : 1f) );
		var b = Player.SpawnBullet( bullet.Position2D, dir, dmg, isFromClip: false, bulletType: bullet.BulletType );
		b.Stats[BulletStat.NumPiercing] = bullet.Stats[BulletStat.NumPiercing];
		b.Stats[BulletStat.NumBouncing] = bullet.StartingNumBounce;
		b.Velocity = dir * bullet.Velocity.Length;

		if ( bullet.Stats[BulletStat.ArcHeight] > 0f )
			b.SetupArc( bullet.Stats[BulletStat.ArcHeight], 0f );

		b.Stats[BulletStat.NumBouncing] -= 1;
		b.ShowBounce = b.Stats[BulletStat.NumBouncing] > 0f;

		// todo: cloned bullet should have same fire/freeze etc instead of random

		if ( other.IsValid() )
			b.HitThings.Add( other );

		Manager.Instance.SpawnRingRpc( bullet.Position2D, Game.Random.Float( 8f, 12f ), new Color( 0f, 1f, 0f, 0.5f ), lifetime: Game.Random.Float( 0.3f, 0.4f ), path: "ring_spiky" );

		Manager.Instance.PlaySfxNearbyRpc( "bounce_copy", bullet.Position2D, pitch: Game.Random.Float( 1f, 1.2f ), volume: 2.2f, maxDist: 350f );

		//Highlight();
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.Chance:
			default:
				return isPercent
					? 3f + 10f * level
					: 0.03f + 0.10f * level;
		}
	}
}
using System;
using Sandbox;

[Perk( Rarity.Uncommon, alwaysOfferDebug: false )]
public class PerkBulletEarlierShotDamage : Perk
{
	private enum Mod { EarlierShotDamage };

	static PerkBulletEarlierShotDamage()
	{
		Register<PerkBulletEarlierShotDamage>(
			name: "Build Up",
			imagePath: "textures/icons/vector/bullet_damage_earlier_shots.png",
			description: level => $"+{GetValue( level, Mod.EarlierShotDamage ).ToString( "0.##" )} bullet dmg each time you shoot\n(resets on reload)",
			upgradeDescription: level => $"+{GetValue( level - 1, Mod.EarlierShotDamage ).ToString("0.##")}→{GetValue( level, Mod.EarlierShotDamage ).ToString("0.##")} bullet dmg each time\nyou shoot (resets on reload)"
		);
	}

	// todo: needs a cap, so can't go infinite with PerkBulletCritReload?

	public override void Start()
	{
		base.Start();
	}

	public override void Refresh()
	{
		base.Refresh();

		Player.Modify( this, PlayerStat.DamagePerEarlierShot, GetValue( Level, Mod.EarlierShotDamage ), ModifierType.Add );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.EarlierShotDamage:
			default:
				return 0.05f + 0.06f * level;
		}
	}

	public override void OnShoot()
	{
		RefreshDisplayText();
	}

	void RefreshDisplayText()
	{
		DisplayText = Player.Stats[PlayerStat.ShotNum] > 0f ? string.Format( "{0:0.00}", Player.Stats[PlayerStat.ShotNum] * GetValue( Level, Mod.EarlierShotDamage ) ) : " ";
		DisplayTextOpacity = Utils.Map( Player.Stats[PlayerStat.ShotNum], 1, 10, 0.75f, 3f );
	}
}
using System;
using Sandbox;

[Perk( Rarity.Unique, locked: true, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.BulletBounce, PerkCategory.SelfDmg })]
public class PerkBulletHurtSelfBounce : Perk
{
	private enum Mod { NumBouncing, ReloadSpeed };


	static PerkBulletHurtSelfBounce()
	{
		Register<PerkBulletHurtSelfBounce>(
			name: "Reckless Bouncer",
			imagePath: "textures/icons/vector/bullet_hurt_self_bounce.png",
			description: level => $"+{(int)GetValue( level, Mod.NumBouncing )} bullet-icon bounces\nYour bullet-icon can hurt you"
		);
	}

	public override void Start()
	{
		base.Start();

	}

	public override void Refresh()
	{
		base.Refresh();

		Player.Modify( this, PlayerStat.BulletNumBouncing, GetValue( Level, Mod.NumBouncing ), ModifierType.Add );
		Player.Modify( this, PlayerStat.BulletCanHitShooter, 1f, ModifierType.Add );

		// todo: hurt you for X% instead of full dmg?
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.NumBouncing:
			default:
				return 3f;
		}
	}
}
using Sandbox;
using System;
using System.IO;

[Perk( Rarity.Rare, locked: true, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.Aoe })]
public class PerkBurstHeal : Perk
{
	private enum Mod { Cooldown, HpAmount };

	private bool _isReady;
	private float _stillTimer;
	private float _cooldownTimer;

	private const float HEAL_RADIUS = 180f;
	private const float DISPLAY_RADIUS = 50f;

	private ParticleEffect _particleEffect;
	private ParticleSpriteRenderer _particleRenderer;

	private ParticleEffect _particleEffectBg;
	private ParticleSpriteRenderer _particleRendererBg;

	private float _bgCurrOpacity;

	private const float STILL_TIME = 2f;


	static PerkBurstHeal()
	{
		Register<PerkBurstHeal>(
			name: "Burst Heal",
			imagePath: "textures/icons/vector/burse_heal.png",
			description: level => $"Stop moving for {STILL_TIME}s to heal nearby players for {(int)GetValue( level, Mod.HpAmount)} hp __ (cooldown: {GetValue( level, Mod.Cooldown )}s)",
			upgradeDescription: level => $"Stop moving for {STILL_TIME}s to\nheal nearby players for {(int)GetValue( level - 1, Mod.HpAmount )}→{(int)GetValue( level, Mod.HpAmount )} hp\n(cooldown: {GetValue( level - 1, Mod.Cooldown )}→{GetValue( level, Mod.Cooldown )}s)"
		);
	}

	public override void Start()
	{
		base.Start();

		ShouldUpdate = true;

		var particleGo = GameObject.Clone( "prefabs/effects/burst_heal_indicator.prefab", new CloneConfig { StartEnabled = true, Parent = Player.GameObject } );
		particleGo.LocalPosition = new Vector3( 0f, 0f, 4f );
		_particleEffect = particleGo.GetComponent<ParticleEffect>();
		_particleRenderer = particleGo.GetComponent<ParticleSpriteRenderer>();
		_particleRenderer.Scale = 0f;

		var particleBgGo = GameObject.Clone( "prefabs/effects/burst_heal_indicator_bg.prefab", new CloneConfig { StartEnabled = true, Parent = Player.GameObject } );
		particleBgGo.LocalPosition = new Vector3( 0f, 0f, 3.5f );
		_particleEffectBg = particleBgGo.GetComponent<ParticleEffect>();
		_particleRendererBg = particleBgGo.GetComponent<ParticleSpriteRenderer>();
		_particleRendererBg.Scale = DISPLAY_RADIUS * Player.LocalScale.x;

		HighlightColor = new Color( 0.7f, 0.7f, 1f );
		HighlightDuration = 0.25f;
		HighlightOpacity = 4f;
	}

	public override void IncreaseLevel()
	{
		base.IncreaseLevel();

		_isReady = true;
		_stillTimer = 0f;
		_bgCurrOpacity = 0f;
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.Cooldown:
			default:
				return 105f - 15f * level;
			case Mod.HpAmount:
				return 20f + 5f * level;
		}
	}

	public override void Update( float dt )
	{
		base.Update( dt );

		var displayRadius = DISPLAY_RADIUS * Player.LocalScale.x;

		if ( _isReady )
		{
			if ( Player.IsMoving )
			{
				_stillTimer = 0f;
			}
			else
			{
				_stillTimer += dt;
				if ( _stillTimer > STILL_TIME )
				{
					Shoot();
					_stillTimer = 0f;
				}
			}

			float progress = Utils.Map( _stillTimer, 0f, STILL_TIME, 0f, 1f );
			_particleRenderer.Scale = progress * displayRadius;
			_particleEffect.Alpha = Utils.Map( progress, 0f, 1f, 0f, 1f, EasingType.QuadOut );
		}
		else
		{
			_cooldownTimer += dt;
			if ( _cooldownTimer > GetValue( Level, Mod.Cooldown ) )
			{
				_isReady = true;
				Highlight();
			}

			_particleRenderer.Scale = 0f;
			_particleEffect.Alpha = 0f;
		}

		float bgTargetOpacity = !_isReady ? 0f : (Player.IsMoving ? 0.1f : 0.4f);
		_bgCurrOpacity = Utils.DynamicEaseTo( _bgCurrOpacity, bgTargetOpacity, 0.2f, dt );

		_particleRendererBg.Scale = displayRadius;
		_particleEffectBg.Alpha = _bgCurrOpacity;

		DisplayText = _isReady ? " " : $"{MathX.CeilToInt( GetValue( Level, Mod.Cooldown ) - _cooldownTimer )}";
		DisplayCooldown = _isReady ? 0f : Utils.Map( _cooldownTimer, 0f, GetValue( Level, Mod.Cooldown ), 1f, 0f );

		//Utils.DrawCircle(Player.Position2D, GetRadius(visual: false), 20, 0f, Color.Blue);
	}

	void Shoot()
	{
		_isReady = false;
		_cooldownTimer = 0f;

		Manager.Instance.SpawnRingRpc( Player.Position2D, GetRadius(visual: true), new Color( 0.3f, 1f, 0.3f, 0.3f ), lifetime: 0.35f, path: "ring2" );

		Manager.Instance.PlaySfxNearbyRpc( "heal", Player.Position2D, pitch: Game.Random.Float( 1f, 1.1f ), volume: 1.1f, maxDist: 400f );

		Player.Heal( amount: GetValue( Level, Mod.HpAmount ) );

		var pos = Player.Position2D;
		var radius = GetRadius( visual: false );

		var traceResults = Player.Scene.Trace.Sphere( radius, pos, pos ).WithAnyTags( "player" ).HitTriggersOnly().RunAll().ToList();
		foreach ( var tr in traceResults )
		{ 
			var gameObject = tr.GameObject;
			var player = gameObject.GetComponent<Player>();
			if ( player.IsDead || !(player.HpPercent < 1f) || player == Player )
				continue;

			player.HealRpc( amount: GetValue( Level, Mod.HpAmount ), otherPlayerHealer: Player );
		}
	}

	float GetRadius( bool visual = false )
	{
		return HEAL_RADIUS * Player.Stats[PlayerStat.RadiusMultiplier] * (visual ? 1.1f : 1f);
	}

	public override void OnDie()
	{
		base.OnDie();

		_particleEffect.Alpha = 0f;
		_particleEffectBg.Alpha = 0f;
	}

	public override void Remove( bool restart = false )
	{
		base.Remove( restart );

		if ( _particleEffect != null )
			_particleEffect.Destroy();

		if ( _particleEffectBg != null )
			_particleEffectBg.Destroy();
	}
}
using System;
using Sandbox;

[Perk( Rarity.Common, locked: true, alwaysOfferDebug: false )]
public class PerkDamageSlowerReload : Perk
{
	private enum Mod { OverallDamageMultiplier, ReloadSpeed };

	static PerkDamageSlowerReload()
	{
		Register<PerkDamageSlowerReload>(
			name: "Preparation",
			imagePath: "textures/icons/vector/overall_damage_slower_reload.png",
			description: level => $"+{GetValue( level, Mod.OverallDamageMultiplier, true )}% dmg\n-{GetValue( level, Mod.ReloadSpeed, true )}% reload speed",
			upgradeDescription: level => $"+{GetValue( level - 1, Mod.OverallDamageMultiplier, true )}%→{GetValue( level, Mod.OverallDamageMultiplier, true )}% dmg\n-{GetValue( level - 1, Mod.ReloadSpeed, true )}%→-{GetValue( level, Mod.ReloadSpeed, true )}% reload speed"
		);
	}

	public override void Start()
	{
		base.Start();

		ShouldUpdate = true;
	}

	public override void Refresh()
	{
		base.Refresh();

		// todo: change to non-bullet dmg only?
		Player.Modify( this, PlayerStat.OverallDamageMultiplier, GetValue( Level, Mod.OverallDamageMultiplier ), ModifierType.Mult );
		Player.Modify( this, PlayerStat.ReloadSpeed, GetValue( Level, Mod.ReloadSpeed ), ModifierType.Mult );
	}
	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.OverallDamageMultiplier:
			default:
				return isPercent
					? 2f + 6f * level
					: 1f + 0.02f + 0.06f * level;
			case Mod.ReloadSpeed:
				return isPercent
					? 4f + 5f * level
					: 1f - (0.04f + 0.05f * level);
		}
	}
}
using System;
using Sandbox;

[Perk( Rarity.Mythic, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.Aoe, PerkCategory.Fire })]
public class PerkDashFire : Perk
{
	private enum Mod { FireChance };


	static PerkDashFire()
	{
		Register<PerkDashFire>(
			name: "Rug Burn",
			imagePath: "textures/icons/vector/dash_fire.png",
			description: level => $"{(int)GetValue( level, Mod.FireChance, true )}% chance to\nstart a fire when you dash",
			upgradeDescription: level => $"{(int)GetValue( level - 1, Mod.FireChance, true )}%→{(int)GetValue( level, Mod.FireChance, true )}% chance to\nstart a fire when you dash"
		);
	}

	public override void Start()
	{
		base.Start();

		HighlightColor = new Color( 1f, 1f, 1f );
		HighlightDuration = 0.5f;
		HighlightOpacity = 1f;
	}

	public override void Refresh()
	{
		base.Refresh();

	}

	public override void OnDashStartedEarly( Vector2 dir )
	{
		base.OnDashStartedEarly( dir );

		if ( Game.Random.Float( 0f, 1f ) < GetValue( Level, Mod.FireChance ) )
		{
			Manager.Instance.SpawnFireGroundRpc( 
				Player.Position2D, 
				Player,
				enemySource: null,
				enemyType: EnemyType.None,
				damage: Player.Stats[PlayerStat.FireDamage], 
				lifetime: Player.Stats[PlayerStat.FireLifetime],
				spreadChance: Player.Stats[PlayerStat.FireSpreadChance], 
				canStack: Player.Stats[PlayerStat.FireDmgStack] > 0f,
				scale: Player.Stats[PlayerStat.RadiusMultiplier],
				colorA: Color.Red,
				colorB: Color.Yellow
			);

			Manager.Instance.PlaySfxNearbyRpc( "burn", Player.Position2D, pitch: Game.Random.Float( 0.95f, 1f ), volume: 0.9f, maxDist: 300f );

			Highlight();

			Player.DodgeDuckRpc( dir, time: Game.Random.Float( 0.15f, 0.2f ) );
		}
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.FireChance:
			default:
				return isPercent
					? 25f + 25f * level
					: 0.25f + 0.25f * level;
		}
	}
}
using System;
using Sandbox;

[Perk( Rarity.Rare, alwaysOfferDebug: false )]
public class PerkDashLength : Perk
{
	private enum Mod { DashLength };

	static PerkDashLength()
	{
		Register<PerkDashLength>(
			name: "Leg Day",
			imagePath: "textures/icons/vector/dash_strength.png",
			description: level => $"+{GetValue( level, Mod.DashLength, true )}% dash distance",
			upgradeDescription: level => $"+{GetValue( level - 1, Mod.DashLength, true )}%→{GetValue( level, Mod.DashLength, true )}% dash distance"
		);
	}

	public override void Start()
	{
		base.Start();
	}

	public override void Refresh()
	{
		base.Refresh();

		Player.Modify( this, PlayerStat.DashStrength, GetValue( Level, Mod.DashLength ), ModifierType.Mult );
		Player.Modify( this, PlayerStat.DashInvulnTime, GetValue( Level, Mod.DashLength ), ModifierType.Mult );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.DashLength:
			default:
				return isPercent
					? 15f + 25f * level
					: 1f + 0.15f + 0.25f * level;
		}
	}
}
using System;
using Sandbox;

[Perk( Rarity.Common, includedAtStart: false, alwaysOfferDebug: false )]
public class PerkExplosionSize : Perk
{
	private enum Mod { ExplosionSizeMultiplier };

	static PerkExplosionSize()
	{
		Register<PerkExplosionSize>(
			name: "Bigger Booms",
			imagePath: "textures/icons/vector/explosion_size.png",
			description: level => $"+{(int)GetValue( level, Mod.ExplosionSizeMultiplier, true )}% explosion size",
			upgradeDescription: level => $"+{(int)GetValue( level - 1, Mod.ExplosionSizeMultiplier, true )}%→{(int)GetValue( level, Mod.ExplosionSizeMultiplier, true )}% explosion size"
		);
	}

	public override void Start()
	{
		base.Start();
	}

	public override void Refresh()
	{
		base.Refresh();

		Player.Modify( this, PlayerStat.ExplosionSizeMultiplier, GetValue( Level, Mod.ExplosionSizeMultiplier ), ModifierType.Mult );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.ExplosionSizeMultiplier:
			default:
				return isPercent
					? 5f + 15f * level
					: 1f + (0.05f + 0.15f * level);
		}
	}
}

using System;
using Sandbox;

[Perk( Rarity.Rare, includedAtStart: false, locked: true, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.Aoe, PerkCategory.Explosion })]
public class PerkFearBomb : Perk
{
	private enum Mod { BombChance };


	static PerkFearBomb()
	{
		Register<PerkFearBomb>(
			name: "Martyrdom",
			imagePath: "textures/icons/vector/fear_drop_grenade.png",
			description: level => $"{GetValue( level, Mod.BombChance, true )}% chance for scared enemies\nyou kill to drop a bomb",
			upgradeDescription: level => $"{GetValue( level - 1, Mod.BombChance, true )}→{GetValue( level, Mod.BombChance, true )}% chance for scared enemies\nyou kill to drop a bomb"
		);
	}

	public override void Start()
	{
		base.Start();

	}

	public override void Refresh()
	{
		base.Refresh();

		Player.Modify( this, PlayerStat.FearDropGrenadeChance, GetValue( Level, Mod.BombChance ), ModifierType.Add );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.BombChance:
			default:
				return isPercent
					? 15 * level
					: 0.15f * level;
		}
	}
}
using System;
using Sandbox;

[Perk( Rarity.Common, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.Fire })]
public class PerkFireBullet : Perk
{
	private enum Mod { ShootFireIgniteChance };


	static PerkFireBullet()
	{
		Register<PerkFireBullet>(
			name: "Burning Bullets",
			imagePath: "textures/icons/vector/fire_bullet.png",
			description: level => $"{(int)GetValue( level, Mod.ShootFireIgniteChance, true )}% chance for bullet-icon to ignite",
			upgradeDescription: level => $"{(int)GetValue( level - 1, Mod.ShootFireIgniteChance, true )}%→{(int)GetValue( level, Mod.ShootFireIgniteChance, true )}% chance for\nbullet-icon to ignite"
		);
	}

	public override void Start()
	{
		base.Start();

	}

	public override void Refresh()
	{
		base.Refresh();

		Player.Modify( this, PlayerStat.ShootFireIgniteChance, GetValue( Level, Mod.ShootFireIgniteChance ), ModifierType.Add );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.ShootFireIgniteChance:
			default:
				return isPercent
					? 3f + 6f * level
					: 0.03f + 0.06f * level;
		}
	}
}
using System;
using Sandbox;

[Perk( Rarity.Rare, alwaysOfferDebug: false )]
public class PerkHealEffectiveness : Perk
{
	private enum Mod { HealEffectiveness };

	static PerkHealEffectiveness()
	{
		Register<PerkHealEffectiveness>(
			name: "Improved Healing",
			imagePath: "textures/icons/vector/heal_effectiveness.png",
			description: level => $"+{GetValue( level, Mod.HealEffectiveness, true )}% healing received",
			upgradeDescription: level => $"+{GetValue( level - 1, Mod.HealEffectiveness, true )}%→{GetValue( level, Mod.HealEffectiveness, true )}% healing received"
		);
	}

	public override void Start()
	{
		base.Start();
	}

	public override void Refresh()
	{
		base.Refresh();

		Player.Modify( this, PlayerStat.HealEffectiveness, GetValue( Level, Mod.HealEffectiveness ), ModifierType.Mult );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.HealEffectiveness:
			default:
				switch(level)
				{
					case 1: default: return isPercent ? 10f : 1f + 0.1f;
					case 2: return isPercent ? 15f : 1f + 0.15f;
					case 3: return isPercent ? 20f : 1f + 0.20f;
					case 4: return isPercent ? 25f : 1f + 0.25f;
					case 5: return isPercent ? 30f : 1f + 0.30f;
				}

				//return isPercent
				//	? 6f + 4f * level
				//	: 1f + (0.06f + 0.04f * level);
		}
	}
}
using System;
using Sandbox;

[Perk( Rarity.Mythic, locked: true, alwaysOfferDebug: false )]
public class PerkLessAmmoLongerLifetime : Perk
{
	private enum Mod { BulletLifetime, MaxAmmoCount };

	static PerkLessAmmoLongerLifetime()
	{
		Register<PerkLessAmmoLongerLifetime>(
			name: "More Propellant",
			imagePath: "textures/icons/vector/less_ammo_longer_lifetime.png",
			description: level => $"+{GetValue( level, Mod.BulletLifetime, true )}% bullet lifetime\n-{GetValue( level, Mod.MaxAmmoCount )} ammo",
			upgradeDescription: level => $"+{GetValue( level - 1, Mod.BulletLifetime, true )}%→{GetValue( level, Mod.BulletLifetime, true )}% bullet lifetime\n-{GetValue( level - 1, Mod.MaxAmmoCount )}→-{GetValue( level, Mod.MaxAmmoCount )} ammo"
		);
	}

	public override void Start()
	{
		base.Start();
	}

	public override void Refresh()
	{
		base.Refresh();

		Player.Modify( this, PlayerStat.BulletLifetime, GetValue( Level, Mod.BulletLifetime ), ModifierType.Mult );
		Player.Modify( this, PlayerStat.MaxAmmoCount, -GetValue( Level, Mod.MaxAmmoCount ), ModifierType.Add );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.BulletLifetime:
			default:
				return isPercent
					? 10f + 20f * level
					: 1f + (0.10f + 0.20f * level);
			case Mod.MaxAmmoCount:
				return level;
		}
	}
}
using System;
using System.Numerics;
using Sandbox;

[Perk( Rarity.Unique, locked: true, alwaysOfferDebug: false )]
public class PerkLoseWhenGainXp : Perk
{
	private enum Mod { BulletDamageAddition };

	static PerkLoseWhenGainXp()
	{
		Register<PerkLoseWhenGainXp>(
			name: "Anarchist",
			imagePath: "textures/icons/vector/lose_when_gain_xp.png",
			description: level => $"+{GetValue( level, Mod.BulletDamageAddition )} bullet dmg\nRemove and banish this perk\nwhen you get xp coin"
		);
	}

	public override void Start()
	{
		base.Start();
	}

	public override void Refresh()
	{
		base.Refresh();

		Player.Modify( this, PlayerStat.BulletDamage, GetValue( Level, Mod.BulletDamageAddition ), ModifierType.Add );
	}

	private static float GetValue( int level, Mod mod, bool isPercent = false )
	{
		switch ( mod )
		{
			case Mod.BulletDamageAddition:
			default:
				return 4f;
		}
	}

	public override void OnGainXpCoin( float xp )
	{
		base.OnGainXpCoin( xp );

		Player.RemovePerk( TypeLibrary.GetType( GetType() ) );
		Player.BanishPerk( TypeLibrary.GetType( GetType() ) );

		Manager.Instance.Chat.AddLocalChatMessage( $"{Perk.GetRichTextToken( GetType() )} Removed and banished because you got a coin", from: "" );

		Manager.Instance.PlaySfxNearby( "burn", Player.Position2D, pitch: 0.7f, volume: 0.8f, maxDist: 200f );
	}
}