using Godot; using MegaCrit.Sts2.Core.Combat; using MegaCrit.Sts2.Core.Entities.Players; using MegaCrit.Sts2.Core.Nodes.Combat; using System; using System.Linq; namespace VulnerabilityIndicator.Scripts.UI; /// /// UI indicator shown on the combat UI that tracks vulnerability /// card status for each player in multiplayer. /// Attaches to NCombatUi and positions indicators near each player's panel. /// public partial class VulnerabilityPlayerIndicator : Control { private VBoxContainer? _container; private VulnerabilityChecker? _checker; private NCombatUi? _combatUi; private readonly Dictionary _playerIcons = new(); private const string IconPath = "res://images/icons/vuln_indicator_icon.png"; public override void _Ready() { _container = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ShrinkBegin, SizeFlagsVertical = Control.SizeFlags.Fill, }; AddChild(_container); Visible = false; } public override void _Process(double delta) { if (_checker == null || _combatUi == null) return; bool anyVisible = false; foreach (var kvp in _playerIcons) { var icon = kvp.Value; bool hasVuln = _checker.PlayerHasVulnerabilityCard(kvp.Key); icon.Visible = hasVuln; if (hasVuln) anyVisible = true; } Visible = anyVisible; } /// /// Called by the node attachment system when the parent NCombatUi is ready. /// public void Bind(NCombatUi parent) { _combatUi = parent; _checker = VulnerabilityChecker.Instance; if (_checker == null || _container == null) { Visible = false; return; } // Create an indicator for each player foreach (var player in _checker.GetAllPlayers()) { var icon = new TextureRect { Texture = (Texture2D?)LoadIcon(), Size = new Vector2(20f, 20f), StretchMode = TextureRect.StretchModeEnum.Keep, Visible = false, CustomMinimumSize = new Vector2(22f, 22f), }; // Try to get a player name for the tooltip var playerName = GetPlayerName(player); icon.TooltipText = $"{playerName} has a vulnerability card"; _container.AddChild(icon); _playerIcons[player] = icon; } } private string GetPlayerName(Player player) { try { var playerType = player.GetType(); foreach (var propName in new[] { "Name", "DisplayName", "PlayerName", "NetName" }) { var prop = playerType.GetProperty(propName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (prop != null) { var value = prop.GetValue(player); if (value is string name && !string.IsNullOrEmpty(name)) return name; } } } catch { } return "Player"; } private CombatState? GetCombatState() { if (_combatUi == null) return null; try { var type = _combatUi.GetType(); foreach (var propName in new[] { "CombatState", "Combat", "State", "GameState" }) { var prop = type.GetProperty(propName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (prop != null) { var value = prop.GetValue(_combatUi); if (value is CombatState cs) return cs; } } } catch { } return null; } private Texture2D? LoadIcon() { try { var tex = GD.Load(IconPath); if (tex != null) return tex; } catch { } return GenerateVulnerabilityIcon(); } /// /// Generates a simple vulnerability icon programmatically. /// private static Texture2D GenerateVulnerabilityIcon() { var size = 32; var img = Image.Create(size, size, false, Image.Format.Rgba8); img.Fill(new Color(0f, 0f, 0f, 0f)); var fill = new Color(0.8f, 0.15f, 0.15f, 0.9f); // Draw shield polygon var shieldPoints = new (int X, int Y)[] { (size / 2, 1), (size - 3, size / 4), (size - 3, size / 2), (size / 2, size - 2), (3, size / 2), (3, size / 4), }; var minX = shieldPoints.Min(p => p.X); var maxX = shieldPoints.Max(p => p.X); var minY = shieldPoints.Min(p => p.Y); var maxY = shieldPoints.Max(p => p.Y); for (int y = minY; y <= maxY; y++) { for (int x = minX; x <= maxX; x++) { if (PointInPolygon(x, y, shieldPoints)) img.SetPixel(x, y, fill); } } // Lightning crack var crack = new Color(1f, 0.9f, 0.1f, 1f); var crackPoints = new (int X, int Y)[] { (size / 2 + 1, 4), (size / 2 + 3, 10), (size / 2 - 1, 12), (size / 2 + 2, 13), (size / 2 - 1, 18), (size / 2 + 1, 28), }; for (int i = 0; i < crackPoints.Length - 1; i++) DrawLineThick(img, crack, crackPoints[i], crackPoints[i + 1], 2); var texture = ImageTexture.CreateFromImage(img); return texture; } private static bool PointInPolygon(int px, int py, (int X, int Y)[] polygon) { bool inside = false; for (int i = 0, j = polygon.Length - 1; i < polygon.Length; j = i++) { var pi = polygon[i]; var pj = polygon[j]; if (((pi.Y > py) != (pj.Y > py)) && (px < (pj.X - pi.X) * (py - pi.Y) / (float)(pj.Y - pi.Y) + pi.X)) inside = !inside; } return inside; } private static void DrawLineThick(Image img, Color color, (int X, int Y) from, (int X, int Y) to, int thickness) { for (int tx = -thickness; tx <= thickness; tx++) { for (int ty = -thickness; ty <= thickness; ty++) { DrawLine(img, color, (from.X + tx, from.Y + ty), (to.X + tx, to.Y + ty)); } } } private static void DrawLine(Image img, Color color, (int X, int Y) from, (int X, int Y) to) { int dx = Math.Abs(to.X - from.X); int dy = Math.Abs(to.Y - from.Y); int sx = from.X < to.X ? 1 : -1; int sy = from.Y < to.Y ? 1 : -1; int err = dx - dy; int x = from.X; int y = from.Y; while (true) { if (x >= 0 && x < img.GetWidth() && y >= 0 && y < img.GetHeight()) img.SetPixel(x, y, color); if (x == to.X && y == to.Y) break; int e2 = 2 * err; if (e2 > -dy) { err -= dy; x += sx; } if (e2 < dx) { err += dx; y += sy; } } } }