using System.Text.Json; namespace CsWeb.Services { // View helpers: tiny, dependency-free formatting shared by the pages. Content-only — no markup decisions here. public static class Fmt { public static string ModeName(string? mode) => mode?.ToLowerInvariant() switch { "tdm" => "TDM", "gungame" => "Gun Game", "survival" => "Survival", _ => mode ?? "?", }; // Matches the plugin's win-banner format (m:ss.fff). public static string RunTime(long ms) => $"{ms / 60000}:{ms / 1000 % 60:D2}.{ms % 1000:D3}"; public static string Age(Cached c) where T : class => c.Data is null ? "no data yet" : c.Online ? "live" : $"last seen {(int)c.Age.TotalSeconds}s ago"; // One-line mode summary for a server card, from the status payload's mode-specific Extra block. public static string ExtraLine(StatusPayload s) { var e = s.Extra; if (e.ValueKind != JsonValueKind.Object) return ""; return s.Mode switch { "survival" when e.TryGetProperty("RunActive", out var run) && run.GetBoolean() && e.TryGetProperty("Wave", out var w) && e.TryGetProperty("WaveCount", out var wc) => $"Wave {w.GetInt32()}/{wc.GetInt32()}", "survival" => "Between runs", "gungame" when e.TryGetProperty("Ladder", out var l) => $"{l.GetArrayLength()}-rung ladder", "tdm" when e.TryGetProperty("KillGoal", out var kg) => $"First to {kg.GetInt32()} kills", _ => "", }; } // A player's current GG weapon: their rung indexed into the ladder the GG driver ships in Extra. public static string? RungWeapon(StatusPayload s, HumanRow h) { if (s.Mode != "gungame" || s.Extra.ValueKind != JsonValueKind.Object) return null; if (!s.Extra.TryGetProperty("Ladder", out var l) || l.ValueKind != JsonValueKind.Array) return null; int n = l.GetArrayLength(); if (n == 0) return null; return l[Math.Clamp(h.Rung, 0, n - 1)].GetString(); } // Best-effort display for raw weapon designer names in balance payloads (the site has no engine name table; // status payloads carry pre-rendered display names, this is only for the guides' config-derived lists). public static string PrettyWeapon(string w) => w.StartsWith("weapon_", StringComparison.Ordinal) ? w[7..].Replace('_', ' ').ToUpperInvariant() : w; public static int IntOf(JsonElement obj, string prop, int fallback = 0) => obj.ValueKind == JsonValueKind.Object && obj.TryGetProperty(prop, out var v) && v.TryGetInt32(out int i) ? i : fallback; public static double NumOf(JsonElement obj, string prop, double fallback = 0) => obj.ValueKind == JsonValueKind.Object && obj.TryGetProperty(prop, out var v) && v.ValueKind == JsonValueKind.Number ? v.GetDouble() : fallback; public static bool BoolOf(JsonElement obj, string prop, bool fallback = false) => obj.ValueKind == JsonValueKind.Object && obj.TryGetProperty(prop, out var v) && (v.ValueKind == JsonValueKind.True || v.ValueKind == JsonValueKind.False) ? v.GetBoolean() : fallback; public static string StrOf(JsonElement obj, string prop, string fallback = "") => obj.ValueKind == JsonValueKind.Object && obj.TryGetProperty(prop, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() ?? fallback : fallback; // Survival card Detail strings are FORMAT TEMPLATES ("{0} = level x PerPick" per the card defs) that the in-game // draft fills at render time. The guide shows the per-pick line, so format with one pick's worth. public static string CardDetail(string template, double perPick) { if (string.IsNullOrEmpty(template)) return ""; try { return string.Format(System.Globalization.CultureInfo.InvariantCulture, template, perPick); } catch (FormatException) { return template; } // malformed template -> show raw rather than nothing } } }