{"TotalCount":54,"Files":[{"Ident":"gamejam.dasher","Path":"Combat/BulletTracer.cs","FileName":"BulletTracer.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"namespace Prototype;\n\n/// \u003Csummary\u003ECosmetic travelling streak; damage still uses the shot\u0027s authoritative trace.\u003C/summary\u003E\npublic sealed class BulletTracer : Component\n{\n    public Vector3 Start { get; set; }\n    public Vector3 End { get; set; }\n    private float _age;\n    protected override void OnUpdate()\n    {\n        _age \u002B= Time.Delta;\n        var delta = End - Start;\n        var head = System.MathF.Min( delta.Length, _age * 14000f );\n        var tail = System.MathF.Max( 0, _age * 14000f - 160f );\n        if ( tail \u003E= delta.Length ) { DestroyGameObject(); return; }\n        WorldPosition = Start \u002B delta.Normal * ((head \u002B tail) * .5f);\n        WorldScale = new Vector3( System.MathF.Max( 1, head - tail ), .65f, .65f );\n    }\n}\n"},{"Ident":"gamejam.dasher","Path":"Combat/TransientEffect.cs","FileName":"TransientEffect.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"namespace Prototype;\n\npublic sealed class TransientEffect : Component\n{\n\tpublic float Lifetime { get; set; } = .1f;\n\tprotected override void OnUpdate()\n\t{\n\t\tLifetime -= Time.Delta;\n\t\tif ( Lifetime \u003C= 0f ) DestroyGameObject();\n\t}\n}\n"},{"Ident":"gamejam.dasher","Path":"Core/GameProfile.PendingRuns.cs","FileName":"GameProfile.PendingRuns.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\n\r\nnamespace Prototype;\r\n\r\npublic sealed class PendingRunSubmission\r\n{\r\n    public RunRecord Record { get; set; }\r\n    public bool PersonalBest { get; set; }\r\n}\r\n\r\npublic static partial class GameProfile\r\n{\r\n    private const string PendingRunsKey = \u0022dasher.pending-runs.v1\u0022;\r\n    private static List\u003CPendingRunSubmission\u003E _pendingRuns = new();\r\n    private static bool _sendingRuns;\r\n    private static DateTime _retryRunsAt;\r\n\r\n    private static void LoadPendingRuns() =\u003E _pendingRuns = Sandbox.Game.Cookies.Get\u003CList\u003CPendingRunSubmission\u003E\u003E(PendingRunsKey, null) ?? new();\r\n    private static void SavePendingRuns() =\u003E Sandbox.Game.Cookies.Set(PendingRunsKey, _pendingRuns);\r\n\r\n    private static void QueueRun(RunRecord record, bool personalBest)\r\n    {\r\n        foreach (var pending in _pendingRuns)\r\n            if (!string.IsNullOrEmpty(record.RunId) \u0026\u0026 pending.Record.RunId == record.RunId) return;\r\n        _pendingRuns.Add(new PendingRunSubmission { Record = record, PersonalBest = personalBest });\r\n        SavePendingRuns();\r\n        RetryPendingRuns();\r\n    }\r\n\r\n    public static void RetryPendingRuns()\r\n    {\r\n        if (!_loaded || _sendingRuns || _pendingRuns.Count == 0 || DateTime.UtcNow \u003C _retryRunsAt) return;\r\n        _sendingRuns = true;\r\n        _ = SendPendingRuns();\r\n    }\r\n\r\n    private static async Task SendPendingRuns()\r\n    {\r\n        try\r\n        {\r\n            // FIFO keeps last-score stats and their metadata in run order.\r\n            while (_pendingRuns.Count \u003E 0)\r\n            {\r\n                var pending = _pendingRuns[0];\r\n                var record = pending.Record;\r\n                var key = record.Category.ToLowerInvariant();\r\n                var metadata = new Dictionary\u003Cstring, object\u003E { [\u0022team\u0022] = record.PlayerName, [\u0022run\u0022] = record.RunId, [\u0022kills\u0022] = record.Kills, [\u0022levels\u0022] = record.Levels, [\u0022seconds\u0022] = record.Seconds };\r\n                Sandbox.Services.Stats.SetValue($\u0022dasher_{key}_team_last_score\u0022, record.Score, metadata);\r\n                if (pending.PersonalBest)\r\n                {\r\n                    Sandbox.Services.Stats.SetValue($\u0022prototype_{key}_score\u0022, record.Score, metadata);\r\n                    Sandbox.Services.Stats.SetValue($\u0022prototype_{key}_kills\u0022, record.Kills);\r\n                    Sandbox.Services.Stats.SetValue($\u0022prototype_{key}_levels\u0022, record.Levels);\r\n                    Sandbox.Services.Stats.SetValue($\u0022prototype_{key}_time\u0022, (long)MathF.Ceiling(record.Seconds));\r\n                }\r\n                using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15));\r\n                await Sandbox.Services.Stats.FlushAndWaitAsync(timeout.Token);\r\n                _pendingRuns.RemoveAt(0);\r\n                SavePendingRuns();\r\n            }\r\n        }\r\n        catch (Exception exception)\r\n        {\r\n            // Retry SetValue, never Increment: a lost acknowledgement must not add coins/kills.\r\n            Log.Warning($\u0022Run results saved locally; upload will retry: {exception.Message}\u0022);\r\n        }\r\n        finally\r\n        {\r\n            _retryRunsAt = DateTime.UtcNow.AddSeconds(30);\r\n            _sendingRuns = false;\r\n        }\r\n    }\r\n}\r\n"},{"Ident":"gamejam.dasher","Path":"Core/GameProfile.cs","FileName":"GameProfile.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Threading.Tasks;\r\n\r\nnamespace Prototype;\r\n\r\npublic sealed class GameSettingsData\r\n{\r\n\tpublic Dictionary\u003Cstring, float\u003E VoiceVolumes { get; set; } = new();\r\n\tpublic float FieldOfView { get; set; } = 100f;\r\n\tpublic float Brightness { get; set; } = 1f;\r\n\tpublic float SoundVolume { get; set; } = 1f;\r\n\tpublic float MouseSensitivity { get; set; } = 1f;\r\n    public float MusicVolume { get; set; } = 1f;\r\n    public float EffectsVolume { get; set; } = 1f;\r\n    public float VoiceVolume { get; set; } = 1f;\r\n    public bool MicrophoneEnabled { get; set; } = true;\r\n    public bool InvertMouse { get; set; }\r\n    public bool ShowCrosshair { get; set; } = true;\r\n    public bool ShowDamageNumbers { get; set; } = true;\r\n    public bool ShowHitmarker { get; set; } = true;\r\n    public float HitmarkerBodyHue { get; set; } = -1f;\r\n    public float HitmarkerHeadHue { get; set; } = 38f;\r\n    public bool ShowHurtEffect { get; set; } = true;\r\n    public float HurtEffectHue { get; set; } = 358f;\r\n\r\n    public bool ReduceWeaponMotion { get; set; }\r\n    public int AudioSettingsVersion { get; set; }\r\n    public bool CrosshairDot { get; set; } = true;\r\n    public bool CrosshairOutline { get; set; } = true;\r\n    public float CrosshairLength { get; set; } = 8f;\r\n    public float CrosshairGap { get; set; } = 4f;\r\n    public float CrosshairThickness { get; set; } = 2f;\r\n    public float CrosshairOpacity { get; set; } = 1f;\r\n    public float CrosshairHue { get; set; } = 180f;\r\n    public float CrosshairSaturation { get; set; } = 1f;\r\n    public float DamageSaturation { get; set; } = 1f;\r\n    public float DamageTextSize { get; set; } = 24f;\r\n    public float DamageHue { get; set; } = 45f;\r\n    public GameSettingsData Copy() =\u003E new GameSettingsData\r\n    {\r\n        CrosshairLength = CrosshairLength,\r\n        CrosshairGap = CrosshairGap,\r\n        CrosshairThickness = CrosshairThickness,\r\n        CrosshairOpacity = CrosshairOpacity,\r\n        CrosshairHue = CrosshairHue,\r\n        CrosshairSaturation = CrosshairSaturation,\r\n        DamageSaturation = DamageSaturation,\r\n        DamageTextSize = DamageTextSize,\r\n        DamageHue = DamageHue,\r\n        ShowHitmarker = ShowHitmarker,\r\n        HitmarkerBodyHue = HitmarkerBodyHue,\r\n        HitmarkerHeadHue = HitmarkerHeadHue,\r\n        ShowHurtEffect = ShowHurtEffect,\r\n        HurtEffectHue = HurtEffectHue,\r\n\r\n        AudioSettingsVersion = AudioSettingsVersion,\r\n        CrosshairDot = CrosshairDot,\r\n        CrosshairOutline = CrosshairOutline,\r\n        VoiceVolumes = new Dictionary\u003Cstring, float\u003E(VoiceVolumes),\r\n        FieldOfView = FieldOfView,\r\n        Brightness = Brightness,\r\n        SoundVolume = SoundVolume,\r\n        MouseSensitivity = MouseSensitivity,\r\n        MusicVolume = MusicVolume,\r\n        EffectsVolume = EffectsVolume,\r\n        VoiceVolume = VoiceVolume,\r\n        MicrophoneEnabled = MicrophoneEnabled,\r\n        InvertMouse = InvertMouse,\r\n        ShowCrosshair = ShowCrosshair,\r\n        ShowDamageNumbers = ShowDamageNumbers,\r\n        ReduceWeaponMotion = ReduceWeaponMotion\r\n    };\r\n}\r\n\r\npublic sealed class RunRecord\r\n{\r\n    public string RunId { get; set; } = \u0022\u0022;\r\n    public long OwnerSteamId { get; set; }\r\n    public long GlobalRank { get; set; }\r\n\tpublic string PlayerName { get; set; } = \u0022Runner\u0022;\r\n\tpublic string Category { get; set; } = \u0022Solo\u0022;\r\n\tpublic int Score { get; set; }\r\n\tpublic int Kills { get; set; }\r\n\tpublic int Levels { get; set; }\r\n\tpublic float Seconds { get; set; }\r\n\tpublic long FinishedAt { get; set; }\r\n}\r\n\r\npublic static partial class GameProfile\r\n{\r\n\tprivate const string SettingsKey = \u0022prototype.settings.v1\u0022;\r\n\tprivate const string RunsKey = \u0022prototype.runs.v1\u0022;\r\n\tprivate const string ProgressKey = \u0022prototype.progress.v1\u0022;\r\n\tpublic static GameSettingsData Settings { get; private set; } = new();\r\n\tpublic static ProgressionData Progress { get; private set; } = new();\r\n\tpublic static IReadOnlyList\u003CRunRecord\u003E Runs =\u003E _runs;\r\n\tprivate static readonly List\u003CRunRecord\u003E _runs = new();\r\n\tprivate static readonly Dictionary\u003Cstring, List\u003CRunRecord\u003E\u003E _globalRuns = new();\r\n\tprivate static bool _loaded;\r\n\r\n\tpublic static void Load()\r\n\t{\r\n\t\tif ( _loaded ) return;\r\n\t\t_loaded = true;\r\n\t\tSettings = Sandbox.Game.Cookies.Get\u003CGameSettingsData\u003E( SettingsKey, null ) ?? new GameSettingsData();\r\n\t\tProgress = Sandbox.Game.Cookies.Get\u003CProgressionData\u003E( ProgressKey, null ) ?? new ProgressionData();\r\n\t\tProgress.SkillRanks ??= new Dictionary\u003Cstring, int\u003E();\r\n        Progress.CoinReceipts ??= new();\r\n        if (!Progress.CoinsMigrated) { Progress.Coins \u002B= Math.Max(0, Progress.Insight) * 60; Progress.CoinsMigrated = true; SaveProgress(); }\r\n\t\tif (Progress.SkillTreeVersion \u003C 2)\r\n        {\r\n            foreach (var pair in Progress.SkillRanks.Where(x =\u003E !x.Key.StartsWith(\u0022tree_\u0022)).ToArray())\r\n            {\r\n                for (var r = 0; r \u003C pair.Value; r\u002B\u002B) Progress.Coins \u002B= 100 \u002B r * 110 \u002B r * r * 90;\r\n                Progress.SkillRanks.Remove(pair.Key);\r\n            }\r\n            Progress.SkillTreeVersion = 2;\r\n            SaveProgress();\r\n        }\r\n        Settings.VoiceVolumes ??= new();\r\n        if (Settings.AudioSettingsVersion \u003C 1)\r\n        {\r\n            Settings.SoundVolume = Settings.MusicVolume = Settings.EffectsVolume = Settings.VoiceVolume = 1f;\r\n            Settings.AudioSettingsVersion = 1;\r\n            SaveSettings();\r\n        }\r\n\t\tSettings.FieldOfView = Math.Clamp( Settings.FieldOfView, 75f, 120f );\r\n\t\tSettings.Brightness = Math.Clamp( Settings.Brightness, .65f, 1.5f );\r\n\t\tSettings.SoundVolume = Math.Clamp( Settings.SoundVolume, 0f, 2f );\r\n\t\tSettings.MouseSensitivity = Math.Clamp( Settings.MouseSensitivity, .25f, 3f );\r\n        ValidateSettings();\r\n\t\t_runs.AddRange( Sandbox.Game.Cookies.Get\u003CList\u003CRunRecord\u003E\u003E( RunsKey, null ) ?? new List\u003CRunRecord\u003E() );\r\n\t\tLoadPendingRuns();\r\n\t\tApplyAudio();\r\n\t}\r\n\r\n\tprivate static void ValidateSettings()\r\n    {\r\n        static float Safe(float v, float min, float max, float fallback) =\u003E float.IsFinite(v) ? Math.Clamp(v, min, max) : fallback;\r\n        Settings.CrosshairLength = Safe(Settings.CrosshairLength,0f,24f,8f);\r\n        Settings.CrosshairGap = Safe(Settings.CrosshairGap,0f,20f,4f);\r\n        Settings.CrosshairThickness = Safe(Settings.CrosshairThickness,1f,6f,2f);\r\n        Settings.CrosshairOpacity = Safe(Settings.CrosshairOpacity,0f,1f,1f);\r\n        Settings.CrosshairSaturation = Safe(Settings.CrosshairSaturation,0f,1f,1f);\r\n        Settings.DamageSaturation = Safe(Settings.DamageSaturation,0f,1f,1f);\r\n        Settings.CrosshairHue = Safe(Settings.CrosshairHue,0f,360f,180f);\r\n        Settings.DamageTextSize = Safe(Settings.DamageTextSize,14f,56f,24f);\r\n        Settings.DamageHue = Safe(Settings.DamageHue,0f,360f,45f);\r\n        Settings.HitmarkerBodyHue = Safe(Settings.HitmarkerBodyHue,-1f,360f,-1f);\r\n        Settings.HitmarkerHeadHue = Safe(Settings.HitmarkerHeadHue,-1f,360f,38f);\r\n        Settings.HurtEffectHue = Safe(Settings.HurtEffectHue,-1f,360f,358f);\r\n\r\n        Settings.FieldOfView = Safe(Settings.FieldOfView,75f,120f,100f);\r\n        Settings.Brightness = Safe(Settings.Brightness,.65f,1.5f,1f);\r\n        Settings.MouseSensitivity = Safe(Settings.MouseSensitivity,.25f,3f,1f);\r\n        Settings.SoundVolume = Safe(Settings.SoundVolume,0f,2f,1f);\r\n        Settings.MusicVolume = Safe(Settings.MusicVolume,0f,2f,1f);\r\n        Settings.EffectsVolume = Safe(Settings.EffectsVolume,0f,2f,1f);\r\n        Settings.VoiceVolume = Safe(Settings.VoiceVolume,0f,2f,1f);\r\n    }\r\n\r\n    public static void SaveSettings()\r\n    {\r\n        ValidateSettings();\r\n\t\tSandbox.Game.Cookies.Set( SettingsKey, Settings );\r\n\t\tApplyAudio();\r\n\t}\r\n\r\n\tpublic static void ApplySettings(GameSettingsData draft)\r\n    {\r\n        // Preserve per-player voice adjustments made outside this menu.\r\n        var voices = Settings.VoiceVolumes;\r\n        Settings = draft.Copy();\r\n        Settings.AudioSettingsVersion = 1;\r\n        Settings.VoiceVolumes = voices;\r\n        SaveSettings();\r\n    }\r\n\r\n    private static float _audioTarget = 1f;\r\n    private static void ApplyAudio()\r\n    {\r\n        // Settings only set the target; avoid an abrupt gain jump during a shot.\r\n        _audioTarget = Math.Clamp(Settings.SoundVolume, 0f, 2f);\r\n    }\r\n\r\n    public static void UpdateAudio(float delta)\r\n    {\r\n        var mixer = Sandbox.Audio.Mixer.Master;\r\n        if (mixer is null) return;\r\n        var step = Math.Clamp(delta, 0f, .02f) / .08f;\r\n        mixer.Volume \u002B= Math.Clamp(_audioTarget - mixer.Volume, -step, step);\r\n    }\r\n\r\n\tpublic static void AddRun( RunRecord record, bool publish = true )\r\n\t{\r\n\t\tLoad();\r\n\t\tif (!string.IsNullOrEmpty(record.RunId) \u0026\u0026 _runs.Any(r =\u003E r.RunId == record.RunId)) return;\r\n        var previousBest = _runs.Where( x =\u003E string.Equals( x.Category, record.Category, StringComparison.OrdinalIgnoreCase ) ).Select( x =\u003E x.Score ).DefaultIfEmpty( -1 ).Max();\r\n        if (publish) QueueRun(record, record.Score \u003E previousBest);\r\n\t\t_runs.Add( record );\r\n\t\tvar compact = _runs.OrderByDescending( x =\u003E x.Score ).ThenByDescending( x =\u003E x.Levels ).ThenBy( x =\u003E x.Seconds ).Take( 100 ).ToList();\r\n\t\t_runs.Clear(); _runs.AddRange( compact );\r\n\t\tSandbox.Game.Cookies.Set( RunsKey, _runs );\r\n\t\tif (!publish) return;\r\n        // Coins are saved incrementally from host-confirmed rewards, never again at results.\r\n\t\tSaveProgress();\r\n\t}\r\n\r\n\tpublic static int SkillRank( string id ) =\u003E Progress.SkillRanks.GetValueOrDefault( id );\r\n\tpublic static void SelectHero( string id ) { Progress.SelectedHero = HeroCatalog.Get( id ).Id; SaveProgress(); }\r\n\tpublic static int SkillCost(SkillSpec skill) =\u003E 150 \u002B skill.Tier * 250 \u002B SkillRank(skill.Id) * 200;\r\n    public static void BankCoins(string runId, int total)\r\n    {\r\n        Load();\r\n        if (string.IsNullOrEmpty(runId) || total \u003C 0) return;\r\n        var previous = Progress.CoinReceipts.GetValueOrDefault(runId);\r\n        if (total \u003C= previous) return;\r\n        Progress.Coins \u002B= total - previous;\r\n        Progress.CoinReceipts[runId] = total;\r\n        SaveProgress();\r\n    }\r\n    public static string SkillRequirement(SkillSpec skill) =\u003E !string.IsNullOrEmpty(skill.Parent) \u0026\u0026 SkillRank(skill.Parent) \u003C 2\r\n        ? $\u0022Requires {HeroCatalog.Skills.First(x =\u003E x.Id == skill.Parent).Name} rank 2\u0022 : \u0022\u0022;\r\n    public static bool BuySkill( SkillSpec skill )\r\n    {\r\n        Load();\r\n        if (!HeroCatalog.Skills.Contains(skill) || SkillRequirement(skill).Length \u003E 0) return false;\r\n        if (PrototypeGame.Instance is not null \u0026\u0026 PrototypeGame.Instance.LocalPhase != RunPhase.Lobby) return false;\r\n\t\tvar rank = SkillRank( skill.Id );\r\n\t\tif ( rank \u003E= skill.MaxRank ) return false;\r\n\t\tvar cost = SkillCost(skill);\r\n\t\tif ( Progress.Coins \u003C cost ) return false;\r\n\t\tProgress.Coins -= cost; Progress.SkillRanks[skill.Id] = rank \u002B 1; SaveProgress(); return true;\r\n\t}\r\n\tprivate static void SaveProgress() =\u003E Sandbox.Game.Cookies.Set( ProgressKey, Progress );\r\n\r\n\tpublic static IEnumerable\u003CRunRecord\u003E Category( string category ) =\u003E\r\n\t\t(_globalRuns.TryGetValue( category, out var global ) \u0026\u0026 global.Count \u003E 0 ? global : _runs.Where( x =\u003E string.Equals( x.Category, category, StringComparison.OrdinalIgnoreCase ) ))\r\n\t\t\t.OrderByDescending( x =\u003E x.Score ).ThenByDescending( x =\u003E x.Levels ).ThenByDescending( x =\u003E x.Kills ).ThenBy( x =\u003E x.Seconds );\r\n\r\n\tpublic static string BoardSource(string category) =\u003E _globalRuns.TryGetValue(category, out var rows) \u0026\u0026 rows.Count \u003E 0 ? \u0022Global rankings\u0022 : \u0022Local run history\u0022;\r\n\tpublic static async Task RefreshGlobal( string category )\r\n\t{\r\n\t\tvar key = category.ToLowerInvariant();\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar scoreBoard = Sandbox.Services.Leaderboards.GetFromStat( $\u0022prototype_{key}_score\u0022 );\r\n\t\t\tscoreBoard.MaxEntries = 30; scoreBoard.SetAggregationMax(); scoreBoard.SetSortDescending(); scoreBoard.FilterByNone();\r\n\t\t\tawait scoreBoard.Refresh();\r\n\t\t\tif ( scoreBoard.Entries is null || scoreBoard.Entries.Length == 0 ) { _globalRuns.Remove(category); return; }\r\n\t\t\tvar ids = scoreBoard.Entries.Select( x =\u003E x.SteamId ).ToArray();\r\n\t\t\tvar kills = await ReadValues( $\u0022prototype_{key}_kills\u0022, ids );\r\n\t\t\tvar levels = await ReadValues( $\u0022prototype_{key}_levels\u0022, ids );\r\n\t\t\tvar times = await ReadValues( $\u0022prototype_{key}_time\u0022, ids );\r\n\t\t\t_globalRuns[category] = scoreBoard.Entries.Select( entry =\u003E new RunRecord\r\n\t\t\t{\r\n\t\t\t\tOwnerSteamId = entry.SteamId, GlobalRank = entry.Rank,\r\n                PlayerName = string.IsNullOrWhiteSpace( entry.DisplayName ) ? entry.SteamId.ToString() : entry.DisplayName,\r\n\t\t\t\tCategory = category, Score = (int)Math.Clamp( entry.Value, 0, int.MaxValue ),\r\n\t\t\t\tKills = (int)kills.GetValueOrDefault( entry.SteamId ), Levels = (int)levels.GetValueOrDefault( entry.SteamId ),\r\n\t\t\t\tSeconds = (float)times.GetValueOrDefault( entry.SteamId ), FinishedAt = entry.Timestamp.ToUnixTimeSeconds()\r\n\t\t\t} ).ToList();\r\n            var metadataTasks = scoreBoard.Entries.Select(async entry =\u003E\r\n            {\r\n                if(string.IsNullOrWhiteSpace(entry.DataUrl)) return;\r\n                try\r\n                {\r\n                    using var timeout = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(3));\r\n                    var json = await Sandbox.Http.RequestStringAsync(entry.DataUrl, cancellationToken:timeout.Token);\r\n                    using var doc = System.Text.Json.JsonDocument.Parse(json);\r\n                    var row = _globalRuns[category].First(x =\u003E x.OwnerSteamId == entry.SteamId);\r\n                    if(doc.RootElement.TryGetProperty(\u0022team\u0022,out var team)) row.PlayerName = team.GetString() ?? row.PlayerName;\r\n                    if(doc.RootElement.TryGetProperty(\u0022run\u0022,out var run)) row.RunId = run.GetString() ?? \u0022\u0022;\r\n                    if(doc.RootElement.TryGetProperty(\u0022kills\u0022,out var k) \u0026\u0026 k.TryGetInt32(out var kv)) row.Kills=kv;\r\n                    if(doc.RootElement.TryGetProperty(\u0022levels\u0022,out var l) \u0026\u0026 l.TryGetInt32(out var lv)) row.Levels=lv;\r\n                    if(doc.RootElement.TryGetProperty(\u0022seconds\u0022,out var t) \u0026\u0026 t.TryGetSingle(out var tv)) row.Seconds=tv;\r\n                }\r\n                catch { /* Legacy entries keep their recorded account name. */ }\r\n            }).ToArray();\r\n            foreach (var metadataTask in metadataTasks) await metadataTask;\r\n\t\t}\r\n\t\tcatch ( Exception exception ) { Log.Warning( $\u0022Leaderboard refresh unavailable: {exception.Message}\u0022 ); }\r\n\t}\r\n\r\n\tprivate static async Task\u003CDictionary\u003Clong, double\u003E\u003E ReadValues( string stat, long[] ids )\r\n\t{\r\n\t\tvar board = Sandbox.Services.Leaderboards.GetFromStat( stat );\r\n\t\tboard.MaxEntries = 30; board.IncludeSteamIds( ids ); board.SetAggregationLast(); board.FilterByNone();\r\n\t\tawait board.Refresh();\r\n\t\treturn (board.Entries ?? Array.Empty\u003CSandbox.Services.Leaderboards.Board2.Entry\u003E()).ToDictionary( x =\u003E x.SteamId, x =\u003E x.Value );\r\n\t}\r\n}\r\n"},{"Ident":"gamejam.dasher","Path":"Core/GunnerGameplayPreview.cs","FileName":"GunnerGameplayPreview.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"namespace Prototype;\r\n/// \u003Csummary\u003EReversible visual experiment on the first Gunner of each wave.\u003C/summary\u003E\r\npublic sealed class GunnerGameplayPreview : Component\r\n{\r\n    public Vector3 MuzzlePosition =\u003E _renderer.IsValid() \u0026\u0026 _renderer.TryGetBoneTransform( \u0022attach_muzzle\u0022, out var muzzle )\r\n        ? muzzle.Position : WorldPosition \u002B Vector3.Up * 42f \u002B WorldRotation.Forward * 35f;\r\n    private GameObject _visual;\r\n    private SkinnedModelRenderer _renderer;\r\n    private ModelHitboxes _hitboxes;\r\n    private float _playedAttackAt = -100f;\r\n    private int _playedDamage;\r\n    private Vector3 _previous;\r\n    private string _clip;\r\n    private float _hitUntil;\r\n    private int _damage;\r\n    private float _originalEnd;\r\n    private bool _rejected;\r\n    protected override void OnStart()\r\n    {\r\n        _previous = WorldPosition;\r\n        _originalEnd = Components.Get\u003CCapsuleCollider\u003E()?.End.z ?? 0f;\r\n    }\r\n    protected override void OnUpdate()\r\n    {\r\n        var brain = Components.Get\u003CEnemyBrain\u003E();\r\n        if ( !brain.IsValid() ) return;\r\n        bool active = PrototypeGame.Instance?.ReviewGunnerInGame == true;\r\n        if ( active \u0026\u0026 !_visual.IsValid() )\r\n        {\r\n            if ( _rejected ) return;\r\n            var model = Model.Load( \u0022models/review/gunner_relic_v001/gunner.vmdl\u0022 );\r\n            if ( model is null || model.Bounds.Size.z \u003C 60 || model.Bounds.Size.z \u003E 90 || model.Bounds.Size.x \u003E 85 || model.Bounds.Size.y \u003E 85 || model.BoneCount != 20 )\r\n            {\r\n                _rejected = true;\r\n                Log.Error( \u0022GUNNER_GAMEPLAY: candidate rejected by bounds/bone check; keeping original visual\u0022 );\r\n                return;\r\n            }\r\n            _visual = new GameObject( true, \u0022Gunner relic v001\u0022 );\r\n            _visual.NetworkMode = NetworkMode.Never;\r\n            _visual.SetParent( GameObject, false );\r\n            _visual.LocalPosition = Vector3.Down * (27f * brain.Spec.Scale \u002B 2f);\r\n            _renderer = _visual.Components.Create\u003CSkinnedModelRenderer\u003E();\r\n            _renderer.Model = model; _renderer.UseAnimGraph = false;\r\n            _renderer.Sequence.Blending = true;\r\n            _visual.Tags.Add( \u0022enemy\u0022 );\r\n            _hitboxes = _visual.Components.Create\u003CModelHitboxes\u003E();\r\n            _hitboxes.Renderer = _renderer;\r\n            _hitboxes.Target = GameObject;\r\n        }\r\n        if ( _hitboxes.IsValid() ) _hitboxes.Enabled = active \u0026\u0026 brain.Vitality?.IsDead != true;\r\n        if ( active \u0026\u0026 _hitboxes.IsValid() ) GameObject.Tags.Add( \u0022enemy_model_proxy\u0022 );\r\n        else GameObject.Tags.Remove( \u0022enemy_model_proxy\u0022 );\r\n        if ( brain.Vitality?.IsDead == true )\r\n        {\r\n            var deadCapsule = Components.Get\u003CCapsuleCollider\u003E();\r\n            if ( deadCapsule.IsValid() ) deadCapsule.Enabled = false;\r\n        }\r\n        if ( _visual.IsValid() )\r\n        {\r\n            _visual.Enabled = active;\r\n            _visual.LocalPosition = Vector3.Down * (27f * brain.Spec.Scale \u002B 2f);\r\n        }\r\n        foreach ( var child in GameObject.Children )\r\n            if ( child.Name == \u0022Gunner Visual\u0022 )\r\n            {\r\n                var oldRenderer = child.Components.Get\u003CModelRenderer\u003E();\r\n                if ( oldRenderer is not null ) oldRenderer.Enabled = !active;\r\n                var oldAnimator = child.Components.Get\u003CEnemyVisualAnimator\u003E();\r\n                if ( oldAnimator is not null ) oldAnimator.Enabled = !active;\r\n                child.Enabled = !active;\r\n            }\r\n        var capsule = Components.Get\u003CCapsuleCollider\u003E();\r\n        if ( capsule.IsValid() \u0026\u0026 brain.Vitality?.IsDead != true ) capsule.End = Vector3.Up * (active ? 75.8071f - (27f * brain.Spec.Scale \u002B 2f) - capsule.Radius : _originalEnd);\r\n        var speed = (WorldPosition - _previous).WithZ(0).Length / System.MathF.Max(Time.Delta, .001f);\r\n        _previous = WorldPosition;\r\n        if ( !active || _renderer is null ) return;\r\n        if ( brain.DamageSequence != _damage ) { _damage = brain.DamageSequence; _hitUntil = Time.Now \u002B .6f; }\r\n        var clip = brain.Vitality?.IsDead == true ? \u0022death\u0022 : Time.Now \u003C _hitUntil ? \u0022hit\u0022 : Time.Now - brain.LastAttackAt \u003C .9f ? \u0022attack\u0022 : speed \u003E 10 ? \u0022run\u0022 : \u0022idle\u0022;\r\n        var repeatedAttack = (clip is \u0022attack\u0022 or \u0022cast\u0022) \u0026\u0026 _playedAttackAt != brain.LastAttackAt;\r\n        var repeatedHit = clip == \u0022hit\u0022 \u0026\u0026 _playedDamage != brain.DamageSequence;\r\n        if ( clip == _clip \u0026\u0026 !repeatedAttack \u0026\u0026 !repeatedHit ) return;\r\n        _playedAttackAt = brain.LastAttackAt;\r\n        _playedDamage = brain.DamageSequence;\r\n        _clip = clip; _renderer.Sequence.Name = clip; _renderer.Sequence.Time = 0;\r\n        _renderer.Sequence.PlaybackRate = clip == \u0022attack\u0022\r\n            ? System.MathF.Max( 1f, _renderer.Sequence.Duration / System.MathF.Max( .1f, System.MathF.Min( .9f, brain.Spec.AttackCooldown ) ) )\r\n            : 1f;\r\n        _renderer.Sequence.Looping = clip is \u0022idle\u0022 or \u0022run\u0022;\r\n    }\r\n}\r\n"},{"Ident":"gamejam.dasher","Path":"UI/LeaderboardPanel.razor","FileName":"LeaderboardPanel.razor","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"@using Sandbox.UI\r\n@using System\r\n@using System.Linq\r\n@using System.Threading.Tasks\r\n@namespace Prototype\r\n@inherits Panel\r\n\u003Croot\u003E\r\n    \u003Cdiv class=\u0022lb-heading\u0022\u003E\u003Cdiv\u003E\u003Clabel style=\u0022font-size:38px; font-weight:700; color:#f1f6fa; letter-spacing:0px;\u0022\u003ELeaderboard\u003C/label\u003E\u003Clabel class=\u0022lb-muted\u0022\u003EBest completed runs\u003C/label\u003E\u003C/div\u003E\u003Cbutton disabled=\u0022@_busy\u0022 onclick=\u0022@Refresh\u0022\u003E\u003Clabel style=\u0022font-size:18px; color:#ffffff; letter-spacing:0px;\u0022\u003E@(_busy ? \u0022Loading...\u0022 : \u0022Refresh\u0022)\u003C/label\u003E\u003C/button\u003E\u003C/div\u003E\r\n    \u003Cdiv class=\u0022lb-toolbar\u0022\u003E\u003Cdiv class=\u0022lb-tabs\u0022\u003E@foreach(var mode in Modes){var item=mode;\u003Cbutton class=\u0022@(_mode==item ? \u0022active\u0022 : \u0022\u0022)\u0022 onclick=\u0022@(() =\u003E Select(item))\u0022\u003E\u003Clabel style=\u0022font-size:20px; font-weight:500; color:#eef6fa; letter-spacing:0px;\u0022\u003E@item\u003C/label\u003E\u003C/button\u003E}\u003C/div\u003E\u003Clabel class=\u0022lb-muted\u0022\u003E@GameProfile.BoardSource(_mode)\u003C/label\u003E\u003C/div\u003E\r\n    \u003Cdiv class=\u0022lb-columns lb-header\u0022\u003E\u003Clabel class=\u0022lb-rank\u0022\u003E#\u003C/label\u003E\u003Clabel class=\u0022lb-team\u0022\u003E@(_mode==\u0022Solo\u0022 ? \u0022PLAYER\u0022 : \u0022TEAM\u0022)\u003C/label\u003E\u003Clabel class=\u0022lb-score\u0022\u003ESCORE\u003C/label\u003E\u003Clabel class=\u0022lb-stat\u0022\u003EKILLS\u003C/label\u003E\u003Clabel class=\u0022lb-stat\u0022\u003ELEVELS\u003C/label\u003E\u003Clabel class=\u0022lb-time\u0022\u003ETIME\u003C/label\u003E\u003C/div\u003E\r\n    \u003Cdiv class=\u0022lb-list\u0022\u003E\r\n    @{var index=0;}\r\n    @foreach(var run in GameProfile.Category(_mode).Take(30))\r\n    {\r\n        var place=run.GlobalRank\u003E0 ? run.GlobalRank : \u002B\u002Bindex;\r\n        \u003Cdiv class=\u0022lb-columns lb-entry @(Own(run) ? \u0022own\u0022 : \u0022\u0022)\u0022\u003E\r\n            \u003Clabel class=\u0022lb-rank\u0022 style=\u0022font-size:23px; color:@(place==1 ? \u0022#edce79\u0022 : place==2 ? \u0022#cedce6\u0022 : place==3 ? \u0022#d4a27a\u0022 : \u0022#b3c5d1\u0022); letter-spacing:0px;\u0022\u003E@place\u003C/label\u003E\r\n            \u003Cdiv class=\u0022lb-team lb-names\u0022\u003E\r\n                @foreach(var name in run.PlayerName.Split(\u0022 \u002B \u0022, StringSplitOptions.RemoveEmptyEntries))\r\n                {\u003Clabel style=\u0022font-size:20px; font-weight:500; color:#edf5fa; letter-spacing:0px;\u0022\u003E@name\u003C/label\u003E}\r\n                @if(Own(run)){\u003Clabel class=\u0022lb-you\u0022\u003EYOUR RUN\u003C/label\u003E}\r\n            \u003C/div\u003E\r\n            \u003Clabel class=\u0022lb-score\u0022 style=\u0022font-size:26px; font-weight:700; color:#ecd080; letter-spacing:0px;\u0022\u003E@run.Score.ToString(\u0022N0\u0022)\u003C/label\u003E\r\n            \u003Clabel class=\u0022lb-stat\u0022\u003E@run.Kills\u003C/label\u003E\u003Clabel class=\u0022lb-stat\u0022\u003E@run.Levels\u003C/label\u003E\u003Clabel class=\u0022lb-time\u0022\u003E@Clock(run.Seconds)\u003C/label\u003E\r\n        \u003C/div\u003E\r\n    }\r\n    @if(!GameProfile.Category(_mode).Any()){\u003Clabel class=\u0022lb-empty\u0022\u003E@(_busy ? \u0022Loading results...\u0022 : \u0022No completed runs in this mode yet.\u0022)\u003C/label\u003E}\r\n    \u003C/div\u003E\r\n    \u003Cdiv class=\u0022lb-best\u0022\u003E\u003Cdiv class=\u0022lb-best-title\u0022\u003E\u003Clabel class=\u0022lb-you\u0022\u003EYOUR BEST RUN\u003C/label\u003E\u003Clabel class=\u0022lb-muted\u0022\u003E@_mode\u003C/label\u003E\u003C/div\u003E\r\n    @if(Best is { } best){@if(BestRank \u003E 0){\u003Clabel style=\u0022font-size:23px; color:#70e2dc; letter-spacing:0px;\u0022\u003E#@BestRank\u003C/label\u003E}\u003Clabel class=\u0022lb-best-name\u0022 style=\u0022font-size:19px; color:#edf5fa; letter-spacing:0px;\u0022\u003E@best.PlayerName\u003C/label\u003E\u003Clabel style=\u0022font-size:26px; font-weight:700; color:#ecd080; letter-spacing:0px;\u0022\u003E@best.Score.ToString(\u0022N0\u0022)\u003C/label\u003E\u003Clabel class=\u0022lb-muted\u0022\u003Epoints\u003C/label\u003E}\r\n    else {\u003Clabel class=\u0022lb-muted\u0022\u003EComplete a run to set your first record.\u003C/label\u003E}\r\n    \u003C/div\u003E\r\n    \u003Clabel class=\u0022lb-note\u0022\u003ERanked by score. Team names are shown when saved with the result.\u003C/label\u003E\r\n\u003C/root\u003E\r\n@code {\r\n    private static readonly string[] Modes={\u0022Solo\u0022,\u0022Duo\u0022,\u0022Trio\u0022,\u0022Squad\u0022};\r\n    private string _mode=\u0022Solo\u0022;\r\n    private bool _busy;\r\n    private int _revision;\r\n    private RunRecord Best =\u003E GameProfile.Runs.Where(x=\u003Ex.Category==_mode).OrderByDescending(x=\u003Ex.Score).ThenByDescending(x=\u003Ex.Levels).ThenByDescending(x=\u003Ex.Kills).ThenBy(x=\u003Ex.Seconds).FirstOrDefault();\r\n    private long BestRank =\u003E Best is { } best ? GameProfile.Category(_mode).FirstOrDefault(x =\u003E x.GlobalRank \u003E 0 \u0026\u0026 !string.IsNullOrEmpty(best.RunId) \u0026\u0026 x.RunId == best.RunId)?.GlobalRank ?? 0 : 0;\r\n    private bool Own(RunRecord r) =\u003E r.OwnerSteamId == (long)Sandbox.Game.SteamId || (!string.IsNullOrEmpty(r.RunId) \u0026\u0026 GameProfile.Runs.Any(x=\u003Ex.RunId==r.RunId)) || r.OwnerSteamId==0;\r\n    private static string Clock(float seconds) {var t=TimeSpan.FromSeconds(Math.Max(0,seconds));return $\u0022{(int)t.TotalMinutes:00}:{t.Seconds:00}\u0022;}\r\n    protected override void OnAfterTreeRender(bool firstTime) { if(firstTime) _=Refresh(); }\r\n    private void Select(string mode) { _mode=mode; _revision\u002B\u002B; StateHasChanged(); if(!_busy) _=Refresh(); }\r\n    private async Task Refresh() { if(_busy)return; _busy=true; StateHasChanged(); var mode=_mode; await GameProfile.RefreshGlobal(mode); _busy=false; _revision\u002B\u002B; StateHasChanged(); if(mode!=_mode) await Refresh(); }\r\n    protected override int BuildHash()=\u003EHashCode.Combine(_mode,_busy,_revision);\r\n}\r\n"},{"Ident":"gamejam.dasher","Path":"Player/DasherBodyVisual.cs","FileName":"DasherBodyVisual.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"using System.Linq;\r\n\r\nnamespace Prototype;\r\n\r\n/// \u003Csummary\u003EShared rig for first-person arms and the visible multiplayer body.\u003C/summary\u003E\r\npublic sealed class DasherBodyVisual : Component\r\n{\r\n    private GameObject _body, _weapon;\r\n    private SkinnedModelRenderer _renderer;\r\n    private ModelRenderer _weaponRenderer;\r\n    private string _clip, _weaponId;\r\n    private bool _firstPerson;\r\n    public bool Ready =\u003E _renderer.IsValid() \u0026\u0026 _weaponRenderer.IsValid();\r\n    public Vector3 MuzzlePosition\r\n    {\r\n        get\r\n        {\r\n            UpdateWeaponPose();\r\n            return _weapon.IsValid() ? _weapon.WorldPosition \u002B _weapon.WorldRotation.Forward * (_weaponId == \u0022scatter\u0022 ? 24.66f : 27.64f) \u002B _weapon.WorldRotation.Up * (_weaponId == \u0022scatter\u0022 ? 4.91f : 4.72f) : WorldPosition;\r\n        }\r\n    }\r\n\r\n    protected override void OnUpdate() =\u003E PrepareView();\r\n\r\n    public void PrepareView()\r\n    {\r\n        // Networked component must be unique. Old clients/hotload can retain a\r\n        // locally-created copy from the previous OnStart setup.\r\n        var primary = Components.GetAll\u003CDasherBodyVisual\u003E().FirstOrDefault();\r\n        if (primary != this)\r\n        {\r\n            if (_body.IsValid()) _body.Enabled = false;\r\n            if (_weapon.IsValid()) _weapon.Enabled = false;\r\n            return;\r\n        }\r\n        var player = Components.Get\u003CFpsPlayer\u003E();\r\n        if ( !player.IsValid() ) return;\r\n        // Hide legacy bodies left by hot reload or older replicated hierarchies.\r\n        foreach (var child in GameObject.Children)\r\n            if (child.Name == \u0022DASHER Visual\u0022 || child.Name == \u0022THE NOMAD Visual\u0022\r\n                || (child != _body \u0026\u0026 (child.Name == \u0022Dasher View Arms\u0022 || child.Name == \u0022Dasher Skinned Body\u0022))) child.Enabled = false;\r\n        var viewer = PrototypeGame.Instance?.LocalPlayer;\r\n        var watched = viewer?.IsSpectating == true \u0026\u0026 viewer.SpectatorTarget == player;\r\n        var firstPerson = watched || (viewer == player \u0026\u0026 !player.ThirdPersonView);\r\n        player.UpdateLegacyVisualVisibility(firstPerson, Ready);\r\n        if ( !_renderer.IsValid() || _firstPerson != firstPerson )\r\n        {\r\n            var model = Model.Load( firstPerson ? \u0022models/review/dasher_v003/arms.vmdl\u0022 : \u0022models/review/dasher_v003/body.vmdl\u0022 );\r\n            if ( model is null || model.BoneCount \u003C (firstPerson ? 10 : 30) ) return;\r\n            if ( _body.IsValid() ) _body.Destroy();\r\n            _firstPerson = firstPerson;\r\n            _body = new GameObject( false, firstPerson ? \u0022Dasher View Arms\u0022 : \u0022Dasher Skinned Body\u0022 );\r\n            _body.NetworkMode = NetworkMode.Never;\r\n            _body.SetParent( GameObject, false );\r\n            _renderer = _body.Components.Create\u003CSkinnedModelRenderer\u003E();\r\n            _renderer.Model = model; _renderer.UseAnimGraph = false;\r\n            _renderer.RenderOptions.Game = !firstPerson; _renderer.RenderOptions.Overlay = firstPerson;\r\n            _weapon = new GameObject( true, \u0022Dasher Held Weapon\u0022 ); _weapon.SetParent( _body, false );\r\n            _weapon.NetworkMode = NetworkMode.Never;\r\n            _weaponRenderer = _weapon.Components.Create\u003CModelRenderer\u003E();\r\n            _weaponRenderer.RenderOptions.Game = !firstPerson; _weaponRenderer.RenderOptions.Overlay = firstPerson;\r\n            _body.Enabled = true;\r\n            _clip = null; _weaponId = null;\r\n            foreach ( var child in GameObject.Children )\r\n                if ( child.Name == \u0022DASHER Visual\u0022 ) child.Enabled = false;\r\n        }\r\n        if ( _weaponId != player.BodyWeaponId )\r\n        {\r\n            _weaponId = player.BodyWeaponId;\r\n            _weaponRenderer.Model = Model.Load( _weaponId == \u0022scatter\u0022 ? \u0022models/review/dasher_v003/scatter.vmdl\u0022 : \u0022models/review/dasher_v003/rifle.vmdl\u0022 );\r\n        }\r\n        var dead = player.Vitality?.IsDead == true || player.BodyAnimation == \u0022death\u0022;\r\n        _body.Enabled = !firstPerson || !dead;\r\n        _weapon.Enabled = !dead;\r\n        var clip = dead ? \u0022death\u0022 : player.WeaponSwitchProgress \u003E 0f ? \u0022swap\u0022 : player.BodyAnimation ?? \u0022idle\u0022;\r\n        // Disabled view arms have no scene object; do not write sequence time\r\n        // during death/model creation before the renderer has been initialized.\r\n        if ( !_body.Enabled || _renderer.SceneObject is null ) return;\r\n        if ( clip != _clip )\r\n        {\r\n            _clip = clip; _renderer.Sequence.Name = clip; _renderer.Sequence.Time = 0;\r\n            _renderer.Sequence.Looping = clip is \u0022idle\u0022 or \u0022run\u0022 or \u0022walk\u0022;\r\n            _renderer.PlaybackRate = clip.StartsWith( \u0022dash\u0022 ) ? (17f / 30f) / player.DashDuration : 1f;\r\n        }\r\n        if ( clip == \u0022reload\u0022 ) _renderer.Sequence.Time = player.BodyReloadProgress * (53f / 30f);\r\n        if ( clip == \u0022swap\u0022 ) _renderer.Sequence.Time = player.WeaponSwitchProgress * (16f / 30f);\r\n    }\r\n\r\n    protected override void OnPreRender()\r\n    {\r\n        PrepareView();\r\n        UpdateWeaponPose();\r\n    }\r\n\r\n    private void UpdateWeaponPose()\r\n    {\r\n        if ( !Ready || _renderer.SceneObject is null ) return;\r\n        var player = Components.Get\u003CFpsPlayer\u003E();\r\n        // Bone poses belong to the renderer\u0027s sampled transform, which can lag the\r\n        // camera root. Convert to model space before applying this frame\u0027s root.\r\n        var sampledRoot = _renderer.SceneObject.Transform;\r\n        var hasGrip = _renderer.TryGetBoneTransform( \u0022attach_right_grip\u0022, out var grip );\r\n        var localGrip = hasGrip ? sampledRoot.ToLocal( grip ) : _renderer.Model.GetBoneTransform( \u0022attach_right_grip\u0022 );\r\n        if ( _firstPerson )\r\n        {\r\n            var rotation = player.ViewRigRotation;\r\n            _body.WorldRotation = rotation;\r\n            _body.WorldPosition = player.EyePosition \u002B player.ViewRigOffset - rotation.Up * 59f \u002B rotation.Forward * 20f;\r\n        }\r\n        if ( _weapon.Parent != _body ) _weapon.SetParent( _body, false );\r\n        _weapon.LocalPosition = localGrip.Position;\r\n        _weapon.LocalRotation = localGrip.Rotation * _renderer.Model.GetBoneTransform( \u0022attach_right_grip\u0022 ).Rotation.Inverse;\r\n    }\r\n\r\n    protected override void OnDestroy()\r\n    {\r\n        if ( _weapon.IsValid() ) _weapon.Destroy();\r\n        if ( _body.IsValid() ) _body.Destroy();\r\n    }\r\n}\r\n"},{"Ident":"gamejam.dasher","Path":"UI/BuffIcons.cs","FileName":"BuffIcons.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nnamespace Prototype;\r\npublic static class BuffIcons\r\n{\r\n    public readonly record struct Stroke(float X,float Y,float Length,float Angle);\r\n    public static IEnumerable\u003CStroke\u003E Lines(string id)\r\n    {\r\n        var paths = id switch {\r\n            \u0022power\u0022 =\u003E new[]{\u002218,24 42,24 56,32 42,40 18,40 18,24\u0022,\u00226,26 13,26\u0022,\u00222,32 13,32\u0022,\u00226,38 13,38\u0022},\r\n            \u0022haste\u0022 =\u003E new[]{\u00228,14 31,14 40,20 31,26 8,26\u0022,\u002218,30 41,30 50,36 41,42 18,42\u0022,\u002228,46 51,46 58,52 51,58 28,58\u0022},\r\n            \u0022reload\u0022 =\u003E new[]{\u002249,20 42,12 24,12 12,24 12,42 24,52 42,52 52,42\u0022,\u002249,8 49,22 35,22\u0022,\u002228,23 38,23 38,42 28,42 28,23\u0022},\r\n            \u0022execute\u0022 =\u003E new[]{\u002218,48 46,12 53,8 52,18 24,54 18,48\u0022,\u002212,42 30,57\u0022,\u002210,57 17,49\u0022,\u00228,19 16,19\u0022,\u002212,15 12,23\u0022},\r\n            \u0022vitality\u0022 =\u003E new[]{\u002232,7 52,15 50,38 43,48 32,57 21,48 14,38 12,15 32,7\u0022,\u002232,21 32,41\u0022,\u002222,31 42,31\u0022},\r\n            \u0022armor\u0022 =\u003E new[]{\u002232,7 52,15 50,38 43,48 32,57 21,48 14,38 12,15 32,7\u0022,\u002232,17 43,22 42,35 32,46 22,35 21,22 32,17\u0022,\u002232,17 32,46\u0022},\r\n            \u0022recovery\u0022 =\u003E new[]{\u00227,33 18,33 24,20 33,46 40,28 45,33 57,33\u0022,\u00229,21 17,12 31,8 46,13 54,23\u0022,\u002254,43 45,52 31,56 17,51 9,42\u0022},\r\n            \u0022lifeline\u0022 =\u003E new[]{\u002232,15 23,8 13,11 7,22 10,33 32,56 54,33 57,22 51,11 41,8 32,15\u0022,\u00225,32 21,32 27,23 34,42 40,32 59,32\u0022},\r\n            \u0022speed\u0022 =\u003E new[]{\u00229,10 28,32 9,54\u0022,\u002231,10 50,32 31,54\u0022},\r\n            \u0022dash\u0022 =\u003E new[]{\u00229,20 21,32 9,44\u0022,\u002227,14 43,32 27,50\u0022,\u002247,10 56,10 56,54 47,54\u0022},\r\n            \u0022focus\u0022 =\u003E new[]{\u002225,14 39,14 50,25 50,39 39,50 25,50 14,39 14,25 25,14\u0022,\u002232,23 32,41\u0022,\u002223,32 41,32\u0022,\u002232,3 32,8\u0022,\u002232,56 32,61\u0022,\u00223,32 8,32\u0022,\u002256,32 61,32\u0022},\r\n            \u0022rhythm\u0022 =\u003E new[]{\u00224,32 16,32 24,14 34,50 42,25 48,32 60,32\u0022,\u002247,8 57,15 47,22\u0022},\r\n            \u0022power_extra\u0022 =\u003E new[]{\u002215,48 45,13 55,8 52,20 22,55 15,48\u0022,\u002210,42 29,59\u0022,\u00229,60 17,50\u0022,\u00228,12 20,12\u0022,\u002214,6 14,18\u0022},\r\n            _ =\u003E new[]{\u002214,14 26,14 26,37 49,45 49,53 12,53 12,45 14,14\u0022,\u002237,11 51,11\u0022,\u002242,21 57,21\u0022,\u002237,31 51,31\u0022}\r\n        };\r\n        foreach(var path in paths)\r\n        {\r\n            var points=path.Split(\u0027 \u0027);\r\n            for(int i=1;i\u003Cpoints.Length;i\u002B\u002B)\r\n            {\r\n                var a=points[i-1].Split(\u0027,\u0027);var b=points[i].Split(\u0027,\u0027);\r\n                float x=int.Parse(a[0]),y=int.Parse(a[1]),dx=int.Parse(b[0])-x,dy=int.Parse(b[1])-y;\r\n                yield return new Stroke(x,y,MathF.Sqrt(dx*dx\u002Bdy*dy),MathF.Atan2(dy,dx)*180f/MathF.PI);\r\n            }\r\n        }\r\n    }\r\n    public static string Style(Stroke s) =\u003E FormattableString.Invariant($\u0022left:{s.X}px; top:{s.Y}px; width:{s.Length}px; transform:rotate({s.Angle}deg);\u0022);\r\n}\r\n"},{"Ident":"gamejam.dasher","Path":"UI/LobbyRoomPreview.cs","FileName":"LobbyRoomPreview.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"using Sandbox;\r\nusing Sandbox.UI;\r\nusing System.Linq;\r\nnamespace Prototype;\r\npublic sealed class LobbyRoomPreview : ScenePanel\r\n{\r\n    private CameraComponent _camera;\r\n    private readonly GameObject[] _runners = new GameObject[4];\r\n    public LobbyRoomPreview()\r\n    {\r\n        RenderScene = new Scene();\r\n        using(RenderScene.Push())\r\n        {\r\n            var camera = _camera = new GameObject(true,\u0022Lobby camera\u0022).Components.Create\u003CCameraComponent\u003E();\r\n            camera.WorldPosition=new Vector3(390,0,115);\r\n            camera.WorldRotation=Rotation.LookAt(new Vector3(0,0,55)-camera.WorldPosition);\r\n            camera.Orthographic=true; camera.OrthographicHeight=230; camera.FieldOfView=65; camera.BackgroundColor=new Color(.025f,.04f,.065f);\r\n            var stone=Material.Load(\u0022materials/prototype/ruin_stone.vmat\u0022);\r\n            CreateStoneBox(new Vector3(80,0,-12),new Vector3(900,1500,24),stone);\r\n            CreateStoneBox(new Vector3(-140,0,150),new Vector3(30,1500,324),stone);\r\n            CreateStoneBox(new Vector3(-115,0,220),new Vector3(40,1500,24),stone);\r\n            CreateStoneBox(new Vector3(-115,0,12),new Vector3(40,1500,24),stone);\r\n            for(int i=0;i\u003C4;i\u002B\u002B)\r\n            {\r\n                float y=(i-1.5f)*100;\r\n                _runners[i]=new GameObject(true,\u0022Preview runner\u0022);\r\n                _runners[i].WorldPosition=new Vector3(0,y,0);\r\n                _runners[i].Components.Create\u003CSkinnedModelRenderer\u003E().Model=Model.Load(\u0022models/review/dasher_v003/body.vmdl\u0022);\r\n                CreateStoneBox(new Vector3(-115,y,105),new Vector3(25,22,210),stone);\r\n            }\r\n            foreach(var y in new[]{-180f,180f})\r\n            {\r\n                var torch=new GameObject(true,\u0022Lobby torch\u0022); torch.WorldPosition=new Vector3(-122,y,108);\r\n                torch.Components.Create\u003CModelRenderer\u003E().Model=Model.Load(\u0022models/production/torch_v001/torch.vmdl\u0022);\r\n                torch.Components.Create\u003CBrazierFire\u003E();\r\n            }\r\n            Light(new Vector3(160,0,200),new Color(.5f,.68f,1f)*.8f,700);\r\n            new GameObject(true,\u0022Ambient\u0022).Components.Create\u003CAmbientLight\u003E().Color=new Color(.2f,.23f,.3f);\r\n        }\r\n    }\r\n    private void CreateStoneBox(Vector3 position,Vector3 scale,Material material)\r\n    {\r\n        var go=new GameObject(true,\u0022Stone room\u0022); go.WorldPosition=position;\r\n        var renderer=go.Components.Create\u003CModelRenderer\u003E();renderer.Model=Model.Load(\u0022models/dev/box.vmdl\u0022);renderer.MaterialOverride=material;\r\n        var bounds=renderer.Model.Bounds; go.WorldScale=scale/bounds.Size; go.WorldPosition=position-bounds.Center*go.WorldScale;\r\n    }\r\n    private void Light(Vector3 position,Color color,float radius)\r\n    {\r\n        var go=new GameObject(true,\u0022Lobby light\u0022);go.WorldPosition=position;\r\n        var light=go.Components.Create\u003CPointLight\u003E();light.LightColor=color;light.Radius=radius;\r\n    }\r\n    public override void Tick()\r\n    {\r\n        base.Tick();\r\n        var game=PrototypeGame.Instance;\r\n        var count=game?.AllPartyRooms.FirstOrDefault(r=\u003Er.Id==game.LocalPartyId).MemberCount ?? 1;\r\n        count=System.Math.Max(1,count);\r\n        var aspect=Box.Rect.Height\u003E0 ? Box.Rect.Width/Box.Rect.Height : 1.6f;\r\n        for(int i=0;i\u003C4;i\u002B\u002B)\r\n        {\r\n            _runners[i].Enabled=i\u003Ccount;\r\n            // Match the four equal UI columns, from camera-left to camera-right.\r\n            _runners[i].WorldPosition=new Vector3(0,(i-1.5f)*.23f*_camera.OrthographicHeight*aspect,0);\r\n        }\r\n    }\r\n    public override void OnDeleted() { RenderScene?.Destroy(); base.OnDeleted(); }\r\n}\r\n"},{"Ident":"gamejam.dasher","Path":"World/RelicPropAnimator.cs","FileName":"RelicPropAnimator.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"using System;\n\nnamespace Prototype;\n\npublic enum RelicPropMode { Chest, Portal }\n\npublic sealed class RelicPropAnimator : Component\n{\n\tpublic RelicPropMode Mode { get; set; }\n\tprivate Vector3 _origin;\n\tprivate Rotation _rotation;\n\n\tprotected override void OnStart() { _origin = WorldPosition; _rotation = WorldRotation; }\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( Mode == RelicPropMode.Portal )\n\t\t{\n\t\t\tWorldRotation = _rotation * Rotation.FromYaw( Time.Now * 22f );\n\t\t\tWorldPosition = _origin \u002B Vector3.Up * (MathF.Sin( Time.Now * 2.4f ) * 5f);\n\t\t}\n\t\telse\n\t\t{\n\t\t\tWorldPosition = _origin \u002B Vector3.Up * (MathF.Sin( Time.Now * 1.8f ) * 2f);\n\t\t}\n\t}\n}\n"},{"Ident":"gamejam.dasher","Path":"Player/RunBuffs.cs","FileName":"RunBuffs.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"using System;\r\nusing System.Linq;\r\nusing System.Collections.Generic;\r\nnamespace Prototype;\r\n\r\npublic sealed record ChestBuff(string Id, string Name, string Description, int Group, int Weight, int MaxRank);\r\npublic sealed class RunBuffs : Component\r\n{\r\n    public const float ArmorPerRank = 15f;\r\n    public static readonly ChestBuff[] Catalog = {\r\n        new(\u0022power\u0022, \u0022OVERCHARGE\u0022, \u0022\u002B15% weapon damage\u0022,0,35,3),\r\n        new(\u0022haste\u0022, \u0022RAPID FIRE\u0022, \u0022\u002B12% fire rate\u0022,0,30,3),\r\n        new(\u0022reload\u0022, \u0022QUICK HANDS\u0022, \u002215% shorter reload\u0022,0,20,3),\r\n        new(\u0022execute\u0022, \u0022EXECUTIONER\u0022, \u0022\u002B25% damage to enemies below 30% HP\u0022,0,15,2),\r\n        new(\u0022vitality\u0022, \u0022FORTIFY\u0022, \u0022\u002B20 max HP. Heal 50% of new maximum\u0022,1,35,3),\r\n        new(\u0022armor\u0022, \u0022IRON SKIN\u0022, \u0022Reduces incoming damage\u0022,1,30,3),\r\n        new(\u0022recovery\u0022, \u0022SECOND WIND\u0022, \u0022Heal 8% max HP after clearing a room\u0022,1,20,2),\r\n        new(\u0022lifeline\u0022, \u0022LAST CHANCE\u0022, \u0022\u002B1 charge: survive lethal damage at 1 HP, invulnerable for 10s\u0022,1,15,int.MaxValue),\r\n        new(\u0022speed\u0022, \u0022MOMENTUM\u0022, \u0022\u002B6% movement speed\u0022,2,35,3),\r\n        new(\u0022dash\u0022, \u0022BLINK RHYTHM\u0022, \u002210% shorter dash cooldown\u0022,2,30,3),\r\n        new(\u0022focus\u0022, \u0022FOCUS\u0022, \u002210% shorter ultimate cooldown\u0022,2,20,3),\r\n        new(\u0022rhythm\u0022, \u0022COMBAT RHYTHM\u0022, \u0022\u002B15% damage for 2s after dash. Triggers every 8s\u0022,2,15,2),\r\n        new(\u0022power_extra\u0022, \u0022SHARPEN\u0022, \u0022\u002B3% weapon damage\u0022,0,100,int.MaxValue),\r\n        new(\u0022speed_extra\u0022, \u0022LIGHT STEP\u0022, \u0022\u002B1% movement speed\u0022,2,100,int.MaxValue)\r\n    };\r\n    [Sync(SyncFlags.FromHost)] public NetDictionary\u003Cstring,int\u003E Ranks {get;set;} = new();\r\n    [Sync(SyncFlags.FromHost)] public string Offers {get;set;} = \u0022\u0022;\r\n    [Sync(SyncFlags.FromHost)] public int OfferedSeed {get;set;}\r\n    [Sync(SyncFlags.FromHost)] public int ClaimedSeed {get;set;}\r\n    [Sync] public int LifeCharges {get;private set;}\r\n    [Sync] public float RhythmUntil {get;private set;}\r\n    private float _nextRhythm, _protectedUntil;\r\n    private string _lastRoom = \u0022\u0022, _lastGrant = \u0022\u0022;\r\n    public int Rank(string id) =\u003E Ranks.TryGetValue(id,out var n) ? n : 0;\r\n    public IEnumerable\u003CChestBuff\u003E Choices =\u003E Offers.Split(\u0027,\u0027).Select(id =\u003E Catalog.FirstOrDefault(b =\u003E b.Id == id)).Where(b =\u003E b is not null);\r\n    public float ReloadFactor =\u003E MathF.Max(.55f,1f-.15f*Rank(\u0022reload\u0022));\r\n    public float RateFactor =\u003E 1f\u002B.12f*Rank(\u0022haste\u0022);\r\n    public float SpeedFactor =\u003E 1f\u002B.06f*Rank(\u0022speed\u0022)\u002B.01f*Rank(\u0022speed_extra\u0022);\r\n    public float DamageFactor(float hpFraction) =\u003E 1f\u002B.15f*Rank(\u0022power\u0022)\u002B.03f*Rank(\u0022power_extra\u0022)\u002B(hpFraction \u003C .3f ? .25f*Rank(\u0022execute\u0022) : 0f)\u002B(Time.Now \u003C RhythmUntil ? .15f*Rank(\u0022rhythm\u0022) : 0f);\r\n    public void Roll(int seed)\r\n    {\r\n        if (PrototypeGame.Instance?.IsHost != true || OfferedSeed == seed) return;\r\n        var chosen = new List\u003Cstring\u003E();\r\n        for(int group=0;group\u003C3;group\u002B\u002B)\r\n        {\r\n            var pool=Catalog.Where(b=\u003Eb.Group==group \u0026\u0026 !b.Id.EndsWith(\u0022_extra\u0022) \u0026\u0026 Rank(b.Id)\u003Cb.MaxRank).ToArray();\r\n            if(pool.Length==0) pool=Catalog.Where(b=\u003Eb.Group==group \u0026\u0026 b.Id.EndsWith(\u0022_extra\u0022)).ToArray();\r\n            var roll=Game.Random.Int(1,pool.Sum(b=\u003Eb.Weight));\r\n            foreach(var buff in pool) { roll-=buff.Weight; if(roll\u003C=0){chosen.Add(buff.Id);break;} }\r\n        }\r\n        Offers=string.Join(\u0022,\u0022,chosen.OrderBy(_=\u003EGame.Random.Float(0,1))); OfferedSeed=seed;\r\n    }\r\n    public void ResetHost() { Ranks.Clear(); Offers=\u0022\u0022; OfferedSeed=ClaimedSeed=0; }\r\n    public void ResetOwner() { LifeCharges=0; RhythmUntil=0; _nextRhythm=_protectedUntil=0; _lastRoom=_lastGrant=\u0022\u0022; }\r\n    public void DashStarted() { if(Time.Now\u003C_nextRhythm || Rank(\u0022rhythm\u0022)==0)return; RhythmUntil=Time.Now\u002B2; _nextRhythm=Time.Now\u002B8; }\r\n    public bool AbsorbLethal(float damage, HealthComponent health)\r\n    {\r\n        var hp=health.Health; var charges=LifeCharges;\r\n        var prevented=BuffRules.PreventDeath(damage,Time.Now,ref hp,ref charges,ref _protectedUntil);\r\n        LifeCharges=charges; health.Health=hp; return prevented;\r\n    }\r\n    [Rpc.Broadcast]\r\n    public void Grant(string id,int seed)\r\n    {\r\n        if(Rpc.Caller is not null \u0026\u0026 !Rpc.Caller.IsHost || IsProxy)return;\r\n        var key=$\u0022{PrototypeGame.Instance.CoinRunId}:{seed}\u0022;\r\n        if(_lastGrant==key)return; _lastGrant=key;\r\n        var hp=Components.Get\u003CHealthComponent\u003E();\r\n        if(id==\u0022lifeline\u0022)LifeCharges\u002B\u002B;\r\n        if(id==\u0022vitality\u0022) { hp.MaxHealth\u002B=20; if(!hp.IsDead)hp.Health=MathF.Min(hp.MaxHealth,hp.Health\u002Bhp.MaxHealth*.5f); }\r\n    }\r\n    [Rpc.Broadcast]\r\n    public void RoomCleared(int seed,int room)\r\n    {\r\n        if(Rpc.Caller is not null \u0026\u0026 !Rpc.Caller.IsHost || IsProxy)return;\r\n        var key=$\u0022{PrototypeGame.Instance.CoinRunId}:{seed}:{room}\u0022;\r\n        if(_lastRoom==key)return; _lastRoom=key;\r\n        var hp=Components.Get\u003CHealthComponent\u003E();\r\n        if(hp?.IsDead==false) hp.Health=MathF.Min(hp.MaxHealth,hp.Health\u002Bhp.MaxHealth*.08f*Rank(\u0022recovery\u0022));\r\n    }\r\n}\r\n"},{"Ident":"gamejam.dasher","Path":"World/BrazierFire.cs","FileName":"BrazierFire.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"using System;\r\nnamespace Prototype;\r\n\r\n/// \u003Csummary\u003ELocal reusable flame meshes; no particles or lights are network-spawned.\u003C/summary\u003E\r\npublic sealed class BrazierFire : Component\r\n{\r\n    [Property] public float Phase { get; set; }\r\n    private static Model _flameModelV2;\r\n    private readonly GameObject[] _flames = new GameObject[7];\r\n    private readonly GameObject[] _sparks = new GameObject[6];\r\n    private PointLight _light;\r\n    private int _meshRevision;\r\n    private static readonly Vector3 Origin = new(46, 0, 2);\r\n\r\n    private static Vector3 Surface(int ring, int side)\r\n    {\r\n        float t = ring / 8f, a = side * MathF.PI * 2 / 10f;\r\n        float radius = MathF.Sin(MathF.PI * MathF.Pow(t, .7f)) * .48f;\r\n        return new Vector3(MathF.Cos(a)*radius \u002B t*t*.3f, MathF.Sin(a)*radius, t);\r\n    }\r\n    private static Model FlameModel()\r\n    {\r\n        if (_flameModelV2 is not null) return _flameModelV2;\r\n        var vertices = new VertexBuffer(); vertices.Init(false);\r\n        void Triangle(Vector3 a, Vector3 b, Vector3 c)\r\n        {\r\n            var normal = Vector3.Cross(b-a,c-a).Normal;\r\n            foreach(var p in new[] {a,b,c}) vertices.Add(new Vertex(p,normal,Vector3.Forward,Vector4.Zero) { Color = Color.White });\r\n        }\r\n        for(int r=0;r\u003C8;r\u002B\u002B) for(int s=0;s\u003C10;s\u002B\u002B)\r\n        {\r\n            var a=Surface(r,s); var b=Surface(r,s\u002B1);var c=Surface(r\u002B1,s\u002B1);var d=Surface(r\u002B1,s);\r\n            Triangle(a,b,c);Triangle(a,c,d);\r\n        }\r\n        var mesh = new Mesh(Material.Load(\u0022materials/production/torch_v001/flame.vmat\u0022));mesh.CreateBuffers(vertices);\r\n        return _flameModelV2 = new ModelBuilder().AddMesh(mesh).Create();\r\n    }\r\n    private GameObject Create(string name,Color color)\r\n    {\r\n        var go=new GameObject(true,name);go.NetworkMode=NetworkMode.Never;go.SetParent(GameObject,false);\r\n        var renderer=go.Components.Create\u003CModelRenderer\u003E();renderer.Model=FlameModel();renderer.Tint=color;\r\n        \r\n        return go;\r\n    }\r\n    protected override void OnStart()\r\n    {\r\n        for(int i=0;i\u003C_flames.Length;i\u002B\u002B) _flames[i]=Create(\u0022Flame tongue\u0022, i\u003C4?new Color(1,.24f,.015f):new Color(1,.72f,.18f));\r\n        for(int i=0;i\u003C_sparks.Length;i\u002B\u002B) _sparks[i]=Create(\u0022Rising ember\u0022,new Color(1,.48f,.05f));\r\n        var glow=new GameObject(true,\u0022Brazier glow\u0022);glow.NetworkMode=NetworkMode.Never;glow.SetParent(GameObject,false);glow.LocalPosition=Origin\u002BVector3.Up*12;\r\n        _light=glow.Components.Create\u003CPointLight\u003E();_light.Radius=760;_light.Shadows=false;\r\n    }\r\n    protected override void OnUpdate()\r\n    {\r\n        if (_meshRevision != 2)\r\n        {\r\n            foreach (var go in _flames) if(go.IsValid()) go.Components.Get\u003CModelRenderer\u003E().Model=FlameModel();\r\n            foreach (var go in _sparks) if(go.IsValid()) go.Components.Get\u003CModelRenderer\u003E().Model=FlameModel();\r\n            _meshRevision=2;\r\n        }\r\n        float time=Time.Now\u002BPhase;\r\n        for(int i=0;i\u003C_flames.Length;i\u002B\u002B)\r\n        {\r\n            if(!_flames[i].IsValid())continue;\r\n            float a=i*2.39996f, pulse=.82f\u002B.16f*MathF.Sin(time*7.3f\u002Bi*1.8f)\u002B.08f*MathF.Sin(time*13.1f\u002Bi);\r\n            float radius=i\u003C4?4.8f:2.2f;\r\n            _flames[i].LocalPosition=Origin\u002Bnew Vector3(MathF.Cos(a)*radius,MathF.Sin(a)*radius,0);\r\n            _flames[i].LocalRotation=Rotation.From(new Angles(MathF.Sin(time*4\u002Bi)*9,i*73\u002BMathF.Sin(time*3\u002Bi)*18,MathF.Cos(time*5\u002Bi)*7));\r\n            _flames[i].LocalScale=new Vector3(i\u003C4?12:7,i\u003C4?11:7,(i\u003C4?39:23)*pulse);\r\n        }\r\n        for(int i=0;i\u003C_sparks.Length;i\u002B\u002B)\r\n        {\r\n            if(!_sparks[i].IsValid())continue;\r\n            float age=(time*.45f\u002Bi*.173f)%1f, a=i*2.4f\u002Btime*.7f;\r\n            _sparks[i].LocalPosition=Origin\u002Bnew Vector3(MathF.Cos(a)*(3\u002Bage*7),MathF.Sin(a)*(3\u002Bage*7),8\u002Bage*56);\r\n            _sparks[i].LocalScale=new Vector3(.65f*(1-age),.65f*(1-age),2.4f*(1-age));\r\n        }\r\n        if(_light.IsValid())_light.LightColor=new Color(1,.34f,.075f)*(7.5f\u002B.7f*MathF.Sin(time*9.3f)\u002B.35f*MathF.Sin(time*17.7f));\r\n    }\r\n}\r\n"},{"Ident":"gamejam.dasher","Path":"World/TorchFlicker.cs","FileName":"TorchFlicker.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"using System;\n\nnamespace Prototype;\n\npublic sealed class TorchFlicker : Component\n{\n\tpublic float Phase { get; set; }\n\tprivate PointLight _light;\n\n\tprotected override void OnStart() =\u003E _light = Components.Get\u003CPointLight\u003E();\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( _light is null ) return;\n\t\tvar pulse = .88f \u002B MathF.Sin( Time.Now * 9.3f \u002B Phase ) * .08f \u002B MathF.Sin( Time.Now * 17.7f \u002B Phase * 2.1f ) * .04f;\n\t\t_light.LightColor = new Color( 1f, .31f, .075f ) * (8.5f * pulse);\n\t}\n}\n"},{"Ident":"gamejam.dasher","Path":"Core/ChargerReviewStage.cs","FileName":"ChargerReviewStage.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\n\nnamespace Prototype;\n\n/// \u003Csummary\u003EOnly attached to scenes/review/charger_review.scene; never spawns gameplay enemies.\u003C/summary\u003E\npublic sealed class ChargerReviewStage : Component\n{\n    [Property] public string Clip { get; set; } = \u0022idle\u0022;\n    private readonly List\u003CSkinnedModelRenderer\u003E _models = new();\n    private string _lastClip;\n\n    protected override void OnStart()\n    {\n        var model = Model.Load( \u0022models/review/charger_relic_v001/charger.vmdl\u0022 );\n        var fillObject = new GameObject( true, \u0022Review fill light\u0022 );\n        fillObject.WorldPosition = new Vector3( 120, -80, 145 );\n        var fill = fillObject.Components.Create\u003CPointLight\u003E();\n        fill.Radius = 500; fill.LightColor = new Color( 1.8f, 1.65f, 1.45f ); fill.Shadows = false;\n        var size = model.Bounds.Size;\n        Log.Info( $\u0022CHARGER_REVIEW imported size={size}, bones={model.BoneCount}, bounds={model.Bounds}\u0022 );\n        if ( size.z \u003C 60 || size.z \u003E 90 || size.x \u003E 105 || size.y \u003E 105 )\n        {\n            Log.Error( \u0022CHARGER_REVIEW: unsafe import dimensions; preview spawn rejected\u0022 );\n            return;\n        }\n        PrototypeFactory.Box( \u0022Review floor\u0022, new Vector3( 0, 0, -3 ), new Vector3( 440, 440, 6 ), new Color( .22f, .23f, .25f ), true );\n        for ( int n = -5; n \u003C= 5; n\u002B\u002B )\n        {\n            float p = n * 39.37008f;\n            PrototypeFactory.Box( \u00221 metre grid X\u0022, new Vector3( p, 0, .04f ), new Vector3( .22f, 400, .08f ), new Color( .48f, .45f, .35f ), false );\n            PrototypeFactory.Box( \u00221 metre grid Y\u0022, new Vector3( 0, p, .04f ), new Vector3( 400, .22f, .08f ), new Color( .48f, .45f, .35f ), false );\n        }\n        for ( int i = 0; i \u003C 3; i\u002B\u002B )\n        {\n            var go = new GameObject( true, $\u0022Charger {i}\u0022 );\n            go.WorldPosition = new Vector3( 0, (i - 1) * 76, 0 );\n            go.WorldRotation = Rotation.FromYaw( i == 1 ? 0 : i == 0 ? 90 : 180 );\n            var renderer = go.Components.Create\u003CSkinnedModelRenderer\u003E();\n            renderer.Model = model; renderer.UseAnimGraph = false;\n            renderer.Sequence.Name = Clip; renderer.Sequence.Looping = true;\n            _models.Add( renderer );\n            // Gameplay-sized proxy is explicit, separate from decorative mesh bounds.\n            var proxy = go.Components.Create\u003CCapsuleCollider\u003E();\n            proxy.Radius = 10; proxy.Start = new Vector3( 0, 0, 10 ); proxy.End = new Vector3( 0, 0, 59 );\n        }\n        var reference = new GameObject( true, \u0022Citizen scale reference\u0022 );\n        reference.WorldPosition = new Vector3( 0, 150, 0 );\n        var citizen = reference.Components.Create\u003CSkinnedModelRenderer\u003E();\n        citizen.Model = Model.Load( \u0022models/citizen/citizen.vmdl\u0022 ); citizen.UseAnimGraph = false;\n        PrototypeFactory.Box( \u00221.8 metre ruler\u0022, new Vector3( 0, 174, 35.433f ), new Vector3( .8f, .8f, 70.866f ), new Color( .2f, .8f, .7f ), false );\n        for ( int i = 0; i \u003C= 18; i\u002B\u002B )\n            PrototypeFactory.Box( \u002210 cm tick\u0022, new Vector3( 0, 174, i * 3.937f ), new Vector3( .8f, i % 5 == 0 ? 6 : 3, .35f ), Color.White, false );\n        _lastClip = Clip;\n    }\n\n    protected override void OnUpdate()\n    {\n        if ( _lastClip == Clip ) return;\n        foreach ( var renderer in _models )\n        {\n            renderer.Sequence.Name = Clip; renderer.Sequence.Time = 0;\n            renderer.Sequence.Looping = Clip is \u0022idle\u0022 or \u0022walk\u0022 or \u0022run\u0022;\n        }\n        _lastClip = Clip;\n    }\n}\n\r\n\r\n"},{"Ident":"gamejam.dasher","Path":"Core/ChaserGameplayPreview.cs","FileName":"ChaserGameplayPreview.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"namespace Prototype;\r\n/// \u003Csummary\u003EReversible visual experiment on the first Chaser of each wave.\u003C/summary\u003E\r\npublic sealed class ChaserGameplayPreview : Component\r\n{\r\n    private GameObject _visual;\r\n    private SkinnedModelRenderer _renderer;\r\n    private ModelHitboxes _hitboxes;\r\n    private float _playedAttackAt = -100f;\r\n    private int _playedDamage;\r\n    private Vector3 _previous;\r\n    private string _clip;\r\n    private float _hitUntil;\r\n    private int _damage;\r\n    private float _originalEnd;\r\n    private bool _rejected;\r\n    protected override void OnStart()\r\n    {\r\n        _previous = WorldPosition;\r\n        _originalEnd = Components.Get\u003CCapsuleCollider\u003E()?.End.z ?? 0f;\r\n    }\r\n    protected override void OnUpdate()\r\n    {\r\n        var brain = Components.Get\u003CEnemyBrain\u003E();\r\n        if ( !brain.IsValid() ) return;\r\n        bool active = PrototypeGame.Instance?.ReviewChaserInGame == true;\r\n        if ( active \u0026\u0026 !_visual.IsValid() )\r\n        {\r\n            if ( _rejected ) return;\r\n            var model = Model.Load( \u0022models/review/chaser_stylized_v001/chaser.vmdl\u0022 );\r\n            if ( model is null || model.Bounds.Size.z \u003C 60 || model.Bounds.Size.z \u003E 90 || model.Bounds.Size.x \u003E 85 || model.Bounds.Size.y \u003E 85 || model.BoneCount != 69 )\r\n            {\r\n                _rejected = true;\r\n                Log.Error( \u0022CHASER_GAMEPLAY: candidate rejected by bounds/bone check; keeping original visual\u0022 );\r\n                return;\r\n            }\r\n            _visual = new GameObject( true, \u0022Chaser candidate v004\u0022 );\r\n            _visual.NetworkMode = NetworkMode.Never;\r\n            _visual.SetParent( GameObject, false );\r\n            _visual.LocalPosition = Vector3.Down * (27f * brain.Spec.Scale \u002B 2f);\r\n            _renderer = _visual.Components.Create\u003CSkinnedModelRenderer\u003E();\r\n            _renderer.Model = model; _renderer.UseAnimGraph = false;\r\n            _renderer.Sequence.Blending = true;\r\n            _visual.Tags.Add( \u0022enemy\u0022 );\r\n            _hitboxes = _visual.Components.Create\u003CModelHitboxes\u003E();\r\n            _hitboxes.Renderer = _renderer;\r\n            _hitboxes.Target = GameObject;\r\n        }\r\n        if ( _hitboxes.IsValid() ) _hitboxes.Enabled = active \u0026\u0026 brain.Vitality?.IsDead != true;\r\n        if ( active \u0026\u0026 _hitboxes.IsValid() ) GameObject.Tags.Add( \u0022enemy_model_proxy\u0022 );\r\n        else GameObject.Tags.Remove( \u0022enemy_model_proxy\u0022 );\r\n        if ( brain.Vitality?.IsDead == true )\r\n        {\r\n            var deadCapsule = Components.Get\u003CCapsuleCollider\u003E();\r\n            if ( deadCapsule.IsValid() ) deadCapsule.Enabled = false;\r\n        }\r\n        if ( _visual.IsValid() )\r\n        {\r\n            _visual.Enabled = active;\r\n            _visual.LocalPosition = Vector3.Down * (27f * brain.Spec.Scale \u002B 2f);\r\n        }\r\n        foreach ( var child in GameObject.Children )\r\n            if ( child.Name == \u0022Chaser Visual\u0022 )\r\n            {\r\n                var oldRenderer = child.Components.Get\u003CModelRenderer\u003E();\r\n                if ( oldRenderer is not null ) oldRenderer.Enabled = !active;\r\n                var oldAnimator = child.Components.Get\u003CEnemyVisualAnimator\u003E();\r\n                if ( oldAnimator is not null ) oldAnimator.Enabled = !active;\r\n                child.Enabled = !active;\r\n            }\r\n        var capsule = Components.Get\u003CCapsuleCollider\u003E();\r\n        if ( capsule.IsValid() \u0026\u0026 brain.Vitality?.IsDead != true ) capsule.End = Vector3.Up * (active ? 75.4752f - (27f * brain.Spec.Scale \u002B 2f) - capsule.Radius : _originalEnd);\r\n        var speed = (WorldPosition - _previous).WithZ(0).Length / System.MathF.Max(Time.Delta, .001f);\r\n        _previous = WorldPosition;\r\n        if ( !active || _renderer is null ) return;\r\n        if ( brain.DamageSequence != _damage ) { _damage = brain.DamageSequence; _hitUntil = Time.Now \u002B .6f; }\r\n        var clip = brain.Vitality?.IsDead == true ? \u0022death\u0022 : Time.Now \u003C _hitUntil ? \u0022hit\u0022 : Time.Now - brain.LastAttackAt \u003C .9f ? \u0022attack\u0022 : speed \u003E 10 ? \u0022run\u0022 : \u0022idle\u0022;\r\n        var repeatedAttack = (clip is \u0022attack\u0022 or \u0022cast\u0022) \u0026\u0026 _playedAttackAt != brain.LastAttackAt;\r\n        var repeatedHit = clip == \u0022hit\u0022 \u0026\u0026 _playedDamage != brain.DamageSequence;\r\n        if ( clip == _clip \u0026\u0026 !repeatedAttack \u0026\u0026 !repeatedHit ) return;\r\n        _playedAttackAt = brain.LastAttackAt;\r\n        _playedDamage = brain.DamageSequence;\r\n        _clip = clip; _renderer.Sequence.Name = clip; _renderer.Sequence.Time = 0;\r\n        _renderer.Sequence.PlaybackRate = clip == \u0022attack\u0022\r\n            ? System.MathF.Max( 1f, _renderer.Sequence.Duration / System.MathF.Max( .1f, System.MathF.Min( .9f, brain.Spec.AttackCooldown ) ) )\r\n            : 1f;\r\n        _renderer.Sequence.Looping = clip is \u0022idle\u0022 or \u0022run\u0022;\r\n    }\r\n}\r\n"},{"Ident":"gamejam.dasher","Path":"Core/PartyLobby.cs","FileName":"PartyLobby.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\nnamespace Prototype;\r\n\r\npublic readonly record struct PartyLobbyView( string Id, string Name, string OwnerId, bool Locked, bool Started, IReadOnlyList\u003Cstring\u003E Members )\n{\n    public const int Capacity = 4;\n    public static bool HasConnectionSlot(IEnumerable\u003Cstring\u003E connectionIds, string incomingId) =\u003E\n        connectionIds.Where(id =\u003E id != incomingId).Distinct().Count() \u003C Capacity;\n\tpublic string ServerAddress { get; init; }\r\n    public int? AdvertisedMembers { get; init; }\r\n    public int MemberCount =\u003E AdvertisedMembers ?? Members?.Count ?? 0;\r\n\tpublic bool IsFull =\u003E MemberCount \u003E= Capacity;\n\r\n\tpublic static string Encode( PartyLobbyView room, string passwordToken )\r\n\t{\r\n\t\tvar safeName = (room.Name ?? \u0022Prototype\u0022).Replace( \u0022\\u001f\u0022, \u0022 \u0022 ).Trim();\r\n\t\treturn string.Join( \u0022\\u001f\u0022, safeName, room.OwnerId ?? \u0022\u0022, room.Locked ? \u00221\u0022 : \u00220\u0022, room.Started ? \u00221\u0022 : \u00220\u0022, passwordToken ?? \u0022\u0022, string.Join( \u0022,\u0022, room.Members ?? Array.Empty\u003Cstring\u003E() ) );\r\n\t}\r\n\r\n\tpublic static bool TryDecode( string id, string encoded, out PartyLobbyView room, out string passwordToken )\r\n\t{\r\n\t\troom = default; passwordToken = \u0022\u0022;\r\n\t\tvar fields = (encoded ?? \u0022\u0022).Split( \u0027\\u001f\u0027 );\r\n\t\tif ( fields.Length \u003C 6 ) return false;\r\n\t\tvar members = fields[5].Split( \u0027,\u0027, StringSplitOptions.RemoveEmptyEntries ).Distinct().ToArray();\r\n\t\troom = new PartyLobbyView( id, fields[0], fields[1], fields[2] == \u00221\u0022, fields[3] == \u00221\u0022, members );\r\n\t\tpasswordToken = fields[4];\r\n\t\treturn true;\r\n\t}\r\n}\r\n"},{"Ident":"gamejam.dasher","Path":"Player/BuffRules.cs","FileName":"BuffRules.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"using System;\r\nnamespace Prototype;\r\npublic static class BuffRules\r\n{\r\n    public static float ReducedDamage(float damage,float defense) =\u003E damage/(1f\u002BMathF.Max(0f,defense)/100f);\r\n    public static bool PreventDeath(float damage,float now,ref float hp,ref int charges,ref float protectedUntil)\r\n    {\r\n        if(now\u003CprotectedUntil)return true;\r\n        if(damage\u003Chp || charges\u003C=0)return false;\r\n        charges--; hp=1f; protectedUntil=now\u002B10f; return true;\r\n    }\r\n}\r\n"},{"Ident":"gamejam.dasher","Path":".obj/__compiler_extra.cs","FileName":"__compiler_extra.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonTitle\u0022, \u0022Dasher\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022dasher\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022gamejam\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022gamejam.dasher\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002229\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-09-14T08:51:14.4846040Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.110.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.110.0\u0022)]"},{"Ident":"gamejam.dasher","Path":"Core/ChaserReviewStage.cs","FileName":"ChaserReviewStage.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\n\nnamespace Prototype;\n\n/// \u003Csummary\u003EOnly attached to scenes/review/chaser_review.scene; never spawns gameplay enemies.\u003C/summary\u003E\npublic sealed class ChaserReviewStage : Component\n{\n    [Property] public string Clip { get; set; } = \u0022idle\u0022;\n    private readonly List\u003CSkinnedModelRenderer\u003E _models = new();\n    private string _lastClip;\n\n    protected override void OnStart()\n    {\n        var model = Model.Load( \u0022models/review/chaser_stylized_v001/chaser.vmdl\u0022 );\n        var fillObject = new GameObject( true, \u0022Review fill light\u0022 );\n        fillObject.WorldPosition = new Vector3( 120, -80, 145 );\n        var fill = fillObject.Components.Create\u003CPointLight\u003E();\n        fill.Radius = 500; fill.LightColor = new Color( 1.8f, 1.65f, 1.45f ); fill.Shadows = false;\n        var size = model.Bounds.Size;\n        Log.Info( $\u0022CHASER_REVIEW imported size={size}, bones={model.BoneCount}, bounds={model.Bounds}\u0022 );\n        if ( size.z \u003C 60 || size.z \u003E 90 || size.x \u003E 85 || size.y \u003E 85 )\n        {\n            Log.Error( \u0022CHASER_REVIEW: unsafe import dimensions; preview spawn rejected\u0022 );\n            return;\n        }\n        PrototypeFactory.Box( \u0022Review floor\u0022, new Vector3( 0, 0, -3 ), new Vector3( 440, 440, 6 ), new Color( .22f, .23f, .25f ), true );\n        for ( int n = -5; n \u003C= 5; n\u002B\u002B )\n        {\n            float p = n * 39.37008f;\n            PrototypeFactory.Box( \u00221 metre grid X\u0022, new Vector3( p, 0, .04f ), new Vector3( .22f, 400, .08f ), new Color( .48f, .45f, .35f ), false );\n            PrototypeFactory.Box( \u00221 metre grid Y\u0022, new Vector3( 0, p, .04f ), new Vector3( 400, .22f, .08f ), new Color( .48f, .45f, .35f ), false );\n        }\n        for ( int i = 0; i \u003C 3; i\u002B\u002B )\n        {\n            var go = new GameObject( true, $\u0022Chaser {i}\u0022 );\n            go.WorldPosition = new Vector3( 0, (i - 1) * 76, 0 );\n            go.WorldRotation = Rotation.FromYaw( i == 1 ? 0 : i == 0 ? 90 : 180 );\n            var renderer = go.Components.Create\u003CSkinnedModelRenderer\u003E();\n            renderer.Model = model; renderer.UseAnimGraph = false;\n            renderer.Sequence.Name = Clip; renderer.Sequence.Looping = true;\n            _models.Add( renderer );\n            // Gameplay-sized proxy is explicit, separate from decorative mesh bounds.\n            var proxy = go.Components.Create\u003CCapsuleCollider\u003E();\n            proxy.Radius = 10; proxy.Start = new Vector3( 0, 0, 10 ); proxy.End = new Vector3( 0, 0, 59 );\n        }\n        var reference = new GameObject( true, \u0022Citizen scale reference\u0022 );\n        reference.WorldPosition = new Vector3( 0, 150, 0 );\n        var citizen = reference.Components.Create\u003CSkinnedModelRenderer\u003E();\n        citizen.Model = Model.Load( \u0022models/citizen/citizen.vmdl\u0022 ); citizen.UseAnimGraph = false;\n        PrototypeFactory.Box( \u00221.8 metre ruler\u0022, new Vector3( 0, 174, 35.433f ), new Vector3( .8f, .8f, 70.866f ), new Color( .2f, .8f, .7f ), false );\n        for ( int i = 0; i \u003C= 18; i\u002B\u002B )\n            PrototypeFactory.Box( \u002210 cm tick\u0022, new Vector3( 0, 174, i * 3.937f ), new Vector3( .8f, i % 5 == 0 ? 6 : 3, .35f ), Color.White, false );\n        _lastClip = Clip;\n    }\n\n    protected override void OnUpdate()\n    {\n        if ( _lastClip == Clip ) return;\n        foreach ( var renderer in _models )\n        {\n            renderer.Sequence.Name = Clip; renderer.Sequence.Time = 0;\n            renderer.Sequence.Looping = Clip is \u0022idle\u0022 or \u0022walk\u0022 or \u0022run\u0022;\n        }\n        _lastClip = Clip;\n    }\n}\n"},{"Ident":"gamejam.dasher","Path":"Core/HeroCatalog.cs","FileName":"HeroCatalog.cs","PackageType":"game","CodeKind":"Game","AssetVersionId":380840,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\nnamespace Prototype;\r\n\r\npublic readonly record struct HeroSpec( string Id, string Name, string Role, string Passive, string Ultimate, Color Color );\r\npublic readonly record struct SkillSpec( string Id, string HeroId, string Name, string Description, int MaxRank, int Branch = 0, int Tier = 0, string Parent = \u0022\u0022, string Icon = \u0022shield\u0022 );\r\n\r\npublic sealed class ProgressionData\r\n{\r\n\tpublic int SkillTreeVersion { get; set; }\r\n\tpublic int Insight { get; set; }\r\n    public int Coins { get; set; }\r\n    public bool CoinsMigrated { get; set; }\r\n    public Dictionary\u003Cstring, int\u003E CoinReceipts { get; set; } = new();\r\n\tpublic string SelectedHero { get; set; } = \u0022nomad\u0022;\r\n\tpublic Dictionary\u003Cstring, int\u003E SkillRanks { get; set; } = new();\r\n}\r\n\r\npublic static class HeroCatalog\r\n{\r\n\tpublic static readonly HeroSpec Warden = new( \u0022warden\u0022, \u0022THE WARDEN\u0022, \u0022VANGUARD\u0022, \u0022Stonebound: \u002B25 maximum health and reduced contact damage.\u0022, \u0022Aegis Pulse: temporary immunity and a radial knockback.\u0022, new Color( .25f, .78f, .92f ) );\r\n\tpublic static readonly HeroSpec Seer = new( \u0022seer\u0022, \u0022THE SEER\u0022, \u0022ARCANIST\u0022, \u0022Resonance: every fifth hit deals bonus arcane damage.\u0022, \u0022Chrono Veil: greatly increases movement and fire rate for six seconds.\u0022, new Color( .72f, .38f, 1f ) );\r\n\tpublic static readonly HeroSpec Relic = new( \u0022relic\u0022, \u0022THE RELIC\u0022, \u0022MARKSMAN\u0022, \u0022Patient Hand: increased weapon damage and accuracy.\u0022, \u0022Sun Lance: empowers shots with extreme damage for five seconds.\u0022, new Color( 1f, .66f, .18f ) );\r\n\tpublic static readonly HeroSpec Nomad = new( \u0022nomad\u0022, \u0022DASHER\u0022, \u0022DASH SPECIALIST\u0022, \u0022Wayfinder: faster movement and shorter dash cooldown.\u0022, \u0022Dust Step: instantly refreshes dash and grants sustained haste.\u0022, new Color( .35f, .92f, .55f ) );\r\n\tpublic static IReadOnlyList\u003CHeroSpec\u003E All { get; } = new[] { Nomad };\r\n\tpublic static HeroSpec Get( string id ) =\u003E All.FirstOrDefault( hero =\u003E hero.Id == id ) is var found \u0026\u0026 !string.IsNullOrEmpty( found.Id ) ? found : Nomad;\r\n\r\n\tpublic static IReadOnlyList\u003CSkillSpec\u003E Skills { get; } = new[]\r\n\t{\r\n        new SkillSpec(\u0022tree_heart\u0022, \u0022nomad\u0022, \u0022Stone Heart\u0022, \u0022\u002B10 maximum HP per rank.\u0022, 3, 0, 0, \u0022\u0022, \u0022shield\u0022),\r\n        new SkillSpec(\u0022tree_armor\u0022, \u0022nomad\u0022, \u0022Iron Will\u0022, \u0022\u002B5 defense per rank. Damage uses diminishing returns.\u0022, 3, 0, 1, \u0022tree_heart\u0022, \u0022shield\u0022),\r\n        new SkillSpec(\u0022tree_vitality\u0022, \u0022nomad\u0022, \u0022Deep Vitality\u0022, \u0022\u002B5 maximum HP and \u002B2 defense per rank.\u0022, 3, 0, 2, \u0022tree_armor\u0022, \u0022plus\u0022),\r\n        new SkillSpec(\u0022tree_bastion\u0022, \u0022nomad\u0022, \u0022Bastion\u0022, \u0022\u002B10 maximum HP and \u002B3 defense per rank.\u0022, 3, 0, 3, \u0022tree_vitality\u0022, \u0022shield\u0022),\r\n        new SkillSpec(\u0022tree_power\u0022, \u0022nomad\u0022, \u0022Runic Edge\u0022, \u0022\u002B5% weapon damage per rank.\u0022, 3, 1, 0, \u0022\u0022, \u0022trophy\u0022),\r\n        new SkillSpec(\u0022tree_haste\u0022, \u0022nomad\u0022, \u0022Quick Trigger\u0022, \u0022\u002B3% fire rate per rank.\u0022, 3, 1, 1, \u0022tree_power\u0022, \u0022plus\u0022),\r\n        new SkillSpec(\u0022tree_reload\u0022, \u0022nomad\u0022, \u0022Quick Hands\u0022, \u0022\u002B5% reload speed per rank.\u0022, 3, 1, 2, \u0022tree_haste\u0022, \u0022join\u0022),\r\n        new SkillSpec(\u0022tree_precision\u0022, \u0022nomad\u0022, \u0022Weapon Mastery\u0022, \u0022\u002B3% damage and \u002B2% fire rate per rank.\u0022, 3, 1, 3, \u0022tree_reload\u0022, \u0022trophy\u0022),\r\n        new SkillSpec(\u0022tree_stride\u0022, \u0022nomad\u0022, \u0022Light Step\u0022, \u0022\u002B2% movement speed per rank.\u0022, 3, 2, 0, \u0022\u0022, \u0022join\u0022),\r\n        new SkillSpec(\u0022tree_dash\u0022, \u0022nomad\u0022, \u0022Wind Cut\u0022, \u0022\u002B5% dash recharge speed per rank.\u0022, 3, 2, 1, \u0022tree_stride\u0022, \u0022join\u0022),\r\n        new SkillSpec(\u0022tree_focus\u0022, \u0022nomad\u0022, \u0022Clear Mind\u0022, \u0022\u002B5% ultimate recharge speed per rank.\u0022, 3, 2, 2, \u0022tree_dash\u0022, \u0022skills\u0022),\r\n        new SkillSpec(\u0022tree_dust\u0022, \u0022nomad\u0022, \u0022Living Dust\u0022, \u0022\u002B0.4 seconds ultimate duration per rank.\u0022, 3, 2, 3, \u0022tree_focus\u0022, \u0022skills\u0022)\r\n\t};\r\n\tpublic static IEnumerable\u003CSkillSpec\u003E For( string heroId ) =\u003E Skills.Where( skill =\u003E skill.HeroId == heroId );\r\n}\r\n\r\n"}]}