Search the source of every open source package.
6996 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
{
}
#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 );
}
}
using System;
using Sandbox;
[Perk( Rarity.Uncommon, alwaysOfferDebug: false )]
public class PerkMaxHealth : Perk
{
private enum Mod { MaxHp };
static PerkMaxHealth()
{
Register<PerkMaxHealth>(
name: "Healthy Diet",
imagePath: "textures/icons/vector/max_health.png",
description: level => $"+{(int)GetValue( level, Mod.MaxHp, true )}% max hp",
upgradeDescription: level => $"+{(int)GetValue( level - 1, Mod.MaxHp, true )}%→{(int)GetValue( level, Mod.MaxHp, true )}% max hp"
);
}
public override void Start()
{
base.Start();
}
public override void Refresh()
{
base.Refresh();
// todo: how does this interact with things that change max hp by a flat amount? should this instead add a flat amount based on current hp, instead of multiplying each time max hp stat is refreshed?
Player.Modify( this, PlayerStat.MaxHp, GetValue( Level, Mod.MaxHp ), ModifierType.Mult );
}
private static float GetValue( int level, Mod mod, bool isPercent = false )
{
switch ( mod )
{
case Mod.MaxHp:
default:
return isPercent
? (5f + 15f * level)
: 1f + (0.05f + 0.15f * level);
}
}
}
using System;
using Sandbox;
[Perk( Rarity.Mythic, includedAtStart: false, locked: true, alwaysOfferDebug: false )]
public class PerkOnly1HpMoveSpeed : Perk
{
private enum Mod { MoveSpeed };
private bool _isActive;
static PerkOnly1HpMoveSpeed()
{
Register<PerkOnly1HpMoveSpeed>(
name: "Adrenaline",
imagePath: "textures/icons/vector/only_1hp_movespeed.png",
description: level => $"+{GetValue( level, Mod.MoveSpeed, true )}% move speed while at 1 hp",
upgradeDescription: level => $"+{GetValue( level - 1, Mod.MoveSpeed, true )}%→{GetValue( level, Mod.MoveSpeed, true )}% move speed\nwhile at 1 hp"
);
}
public override void Start()
{
base.Start();
ShouldUpdate = true;
DisplayCooldownColor = new Color( 0.3f, 0.5f, 1f, 0.25f );
}
public override void Refresh()
{
base.Refresh();
if ( !(Player.Health > 1f) )
Enable();
}
public override void Update( float dt )
{
base.Update( dt );
if( _isActive )
{
if ( Player.Health > 1f )
Disable();
}
else
{
if ( !(Player.Health > 1f) )
Enable();
}
}
void Enable()
{
Player.Modify( this, PlayerStat.MoveSpeedMultiplier, GetValue( Level, Mod.MoveSpeed ), ModifierType.Mult );
_isActive = true;
DisplayCooldown = 1f;
DisplayText = $"{GetValue( Level, Mod.MoveSpeed, true )}%";
}
void Disable()
{
Player.StopModifying( this, PlayerStat.MoveSpeedMultiplier );
_isActive = false;
DisplayCooldown = 0f;
DisplayText = " ";
}
private static float GetValue( int level, Mod mod, bool isPercent = false )
{
switch ( mod )
{
case Mod.MoveSpeed:
default:
return isPercent
? 12f + 10f * level
: 1f + 0.12f + 0.10f * level;
}
}
}
using System;
using Sandbox;
[Perk( Rarity.Rare, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.Punch })]
public class PerkPunch : Perk
{
private enum Mod { DamagePercent, AttackSpeed };
public const float PUNCH_LIFETIME = 0.04f;
public const float PUNCH_SPEED = 1320f;
public override float ImportanceMultiplier => 1.4f;
static PerkPunch()
{
Register<PerkPunch>(
name: "Puncher",
imagePath: "textures/icons/vector/punch.png",
description: level => $"You no longer shoot bullets\nPunch for {(int)GetValue( level, Mod.DamagePercent, true )}% bullet dmg\n-{GetValue( level, Mod.AttackSpeed, true )}% attack speed",
upgradeDescription: level => $"You no longer shoot bullets\nPunch for {(int)GetValue( level - 1, Mod.DamagePercent, true )}%→{(int)GetValue( level, Mod.DamagePercent, true )}% bullet dmg\n-{GetValue( level - 1, Mod.AttackSpeed, true )}%→-{GetValue( level, Mod.AttackSpeed, true )}% attack speed"
);
}
public override void Start()
{
base.Start();
}
public override void Refresh()
{
base.Refresh();
// todo: add to stats screen - Punch Damage
Player.Modify( this, PlayerStat.PunchDamagePercent, GetValue( Level, Mod.DamagePercent ), ModifierType.Add );
Player.Modify( this, PlayerStat.PunchBullets, 1f, ModifierType.Add );
Player.Modify( this, PlayerStat.AttackSpeed, GetValue( Level, Mod.AttackSpeed ), ModifierType.Mult );
Player.SetGunVisible( false );
}
private static float GetValue( int level, Mod mod, bool isPercent = false )
{
switch ( mod )
{
case Mod.DamagePercent:
default:
return isPercent
? 150f + 20f * level + (level == 5 ? 10f : 0f)
: 1.50f + 0.20f * level + (level == 5 ? 0.10f : 0f);
case Mod.AttackSpeed:
return isPercent
? 30f + 5f * level
: 1f - (0.30f + 0.05f * level);
}
}
public override void Remove( bool restart = false )
{
Player.SetGunVisible( true );
}
}
using System;
using Sandbox;
[Perk( Rarity.Common, includedAtStart: false, locked: true, minUnlocksReq: 2, alwaysOfferDebug: false, IncludedCategories = new[] { PerkCategory.NumProjectiles, PerkCategory.ArcBullets })]
public class PerkPunchBullets : Perk
{
private enum Mod { NumBullets, SpreadModifier };
static PerkPunchBullets()
{
Register<PerkPunchBullets>(
name: "Backblast",
imagePath: "textures/icons/vector/punch_bullets.png",
description: level => $"Lob {(int)GetValue( level, Mod.NumBullets )} bullet-icon from enemies\nyou kill with punches",
upgradeDescription: level => $"Lob {(int)GetValue( level - 1, Mod.NumBullets )}→{(int)GetValue( level, Mod.NumBullets )} bullet-icon from enemies\nyou kill with punches"
);
}
public override void Start()
{
base.Start();
}
public override void Refresh()
{
base.Refresh();
}
public override void OnKill( Enemy enemy, DamageType damageType, bool countsAsKill )
{
base.OnKill( enemy, damageType, countsAsKill );
if ( !countsAsKill ) return;
if ( damageType != DamageType.Punch )
return;
int numBullets = (int)GetValue( Level, Mod.NumBullets );
float damage = Player.GetBulletDamage( isFromClip: false, isLastAmmo: false );
float spread = Player.Stats[PlayerStat.BulletSpread] * GetValue( Level, Mod.SpreadModifier );
var dir = Player.FacingDir;
float currAngleOffset = numBullets == 1 ? 0f : -spread * 0.5f;
float increment = numBullets == 1 ? 0f : spread / (float)(numBullets - 1);
for ( int i = 0; i < numBullets; i++ )
{
var spawnOffset = 10f;
var currDir = Utils.RotateVector( dir, currAngleOffset + increment * i );
var pos = enemy.Position2D + currDir * spawnOffset * Player.Stats[PlayerStat.Scale];
var bullet = Player.SpawnBullet( pos, currDir, damage, isFromClip: false );
bullet.SetupArc( arcHeight: Game.Random.Float( 120f, 160f ), Player.Stats[PlayerStat.ArcBulletBounces] );
bullet.Velocity *= Game.Random.Float( 0.05f, 0.45f );
bullet.Stats[BulletStat.Lifetime] *= Game.Random.Float( 1f, 1.4f );
}
}
private static float GetValue( int level, Mod mod, bool isPercent = false )
{
switch ( mod )
{
case Mod.NumBullets:
default:
switch(level)
{
case 1: default: return 2;
case 2: return 3;
case 3: return 4;
case 4: return 5;
case 5: return 6;
case 6: return 7;
case 7: return 8;
}
case Mod.SpreadModifier:
return 1.3f + 0.55f * level;
}
}
}
using System;
using Sandbox;
[Perk( Rarity.Uncommon, includedAtStart: false, alwaysOfferDebug: false )]
public class PerkRandomExisting : Perk
{
private int _perkCount;
private TimeSince _timeSincePerk;
public override float ImportanceMultiplier => 0.15f;
static PerkRandomExisting()
{
Register<PerkRandomExisting>(
name: "Specialization",
imagePath: "textures/icons/vector/random_existing_upgrade.png",
description: level => $"+1 random perk you already have",
upgradeDescription: level => $"+1 random perk you already have",
tooltipInfo: "Won't give you a curse perk"
);
}
public override void Start()
{
base.Start();
}
public override void IncreaseLevel()
{
base.IncreaseLevel();
ShouldUpdate = true;
_perkCount++;
_timeSincePerk = 0f;
}
public override void Refresh()
{
base.Refresh();
}
public override void Update( float dt )
{
base.Update( dt );
if ( _perkCount > 0 && _timeSincePerk > 0.2f )
{
if ( !Player.GiveRandomExistingPerk( ignoreType: typeof( PerkRandomExisting ) ) )
//if ( !Player.GiveRandomExistingPerk( ignoreType: typeof( PerkRandomExisting ), showMessage: true, perkType: TypeLibrary.GetType( typeof( PerkRandomExisting ) ) ) )
{
//Player.PlaySfxUI( "error2", pitch: 0.55f, volume: 0.9f );
//Manager.Instance.SpawnFloaterText( Player.WorldPosition.WithZ( 65f ), "ALREADY MAX!", new Color( 1f, 0.5f, 0.5f ), 1.3f, FloaterType.NegativeMessage );
Manager.Instance.SpawnFloaterText( Player.WorldPosition.WithZ( 65f ), "FAILED!", new Color( 1f, 0.5f, 0.5f ), 1.3f, FloaterType.NegativeMessage );
}
_perkCount--;
if ( _perkCount <= 0 )
ShouldUpdate = false;
}
}
}
using System;
using Sandbox;
[Perk( Rarity.Unique, locked: true, alwaysOfferDebug: false )]
public class PerkRarityUnique : Perk
{
private enum Mod { Chance };
static PerkRarityUnique()
{
Register<PerkRarityUnique>(
name: "Ultra Rare",
imagePath: "textures/icons/vector/rarity_unique.png",
description: level => $"+{(int)GetValue( level, Mod.Chance, true )}% chance of\nLegendary and Unique perks"
);
}
public override void Start()
{
base.Start();
}
public override void Refresh()
{
base.Refresh();
Player.Modify( this, PlayerStat.RarityIncreaseLegendary, GetValue( Level, Mod.Chance ), ModifierType.Add );
Player.Modify( this, PlayerStat.RarityIncreaseUnique, GetValue( Level, Mod.Chance ), ModifierType.Add );
}
private static float GetValue( int level, Mod mod, bool isPercent = false )
{
switch ( mod )
{
case Mod.Chance:
default:
return isPercent
? 300f * level
: 3.00f * level;
}
}
}
using System;
using Sandbox;
[Perk( Rarity.Unique, locked: true, minUnlocksReq: 2, alwaysOfferDebug: false )]
public class PerkShootOnlyWhenClick : Perk
{
private enum Mod { DamageGainSpeed, MaxDamage };
private float _accumulatedDamage;
private int _currDmgInt;
static PerkShootOnlyWhenClick()
{
Register<PerkShootOnlyWhenClick>(
name: "Trigger Discipline",
imagePath: "textures/icons/vector/shoot_only_when_click.png",
description: level => $"Shoot with left-click\nWhile not shooting or reloading,\n+{GetValue( level, Mod.DamageGainSpeed ).ToString("0.##")} dmg/s for next shot (max: [n]+{(int)GetValue( level, Mod.MaxDamage )}[/n])"
);
}
public override void Start()
{
base.Start();
ShouldUpdate = true;
}
public override void Refresh()
{
base.Refresh();
Player.Modify( this, PlayerStat.OnlyShootWithMouse1, 1f, ModifierType.Add );
DisplayCooldownColor = new Color( 0.4f, 0.4f, 1f );
}
public override void Update( float dt )
{
base.Update( dt );
if ( !Player.IsReloading )
{
var maxDmg = GetValue( Level, Mod.MaxDamage );
_accumulatedDamage = Math.Min( _accumulatedDamage + GetValue( Level, Mod.DamageGainSpeed ) * dt, maxDmg );
Player.Modify( this, PlayerStat.ShotDamageAdd, _accumulatedDamage, ModifierType.Add );
DisplayCooldown = Utils.Map( _accumulatedDamage, 0f, maxDmg, 0f, 1f );
}
DisplayText = $"+{_accumulatedDamage.ToString("0.#")}";
var targetScale = Input.Down( "click" )
? 1f
: 1.3f;
IconScale = MathX.Lerp( IconScale, targetScale, Time.Delta * 20f );
var newDmg = _accumulatedDamage.FloorToInt();
if( newDmg != _currDmgInt )
{
if(!Input.Down( "click" ) )
{
IconScale *= 1.1f;
Manager.Instance.PlaySfxUI( "click", pitch: Utils.Map( _currDmgInt, 1, 50, 1f, 3f ), volume: Utils.Map( _currDmgInt, 1, 20, 0.05f, 0.3f ) );
}
_currDmgInt = newDmg;
}
// todo: different holdtype while not shooting
}
public override void OnShoot()
{
base.OnShoot();
_accumulatedDamage = 0f;
Player.StopModifying( this, PlayerStat.ShotDamageAdd );
DisplayCooldown = 0f;
}
private static float GetValue( int level, Mod mod, bool isPercent = false )
{
switch ( mod )
{
case Mod.DamageGainSpeed:
default:
return 1f;
case Mod.MaxDamage:
return 30f;
}
}
}