using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using MegaCrit.Sts2.Core.Combat; using MegaCrit.Sts2.Core.Entities.Players; using MegaCrit.Sts2.Core.Models; namespace VulnerabilityIndicator.Scripts; /// /// Core logic for detecting whether a player has a card in hand /// that can inflict Vulnerability on an enemy. /// public sealed class VulnerabilityChecker { private static VulnerabilityChecker? _instance; public static VulnerabilityChecker? Instance => _instance; private readonly Dictionary _playerHasVulnCard = new(); private CombatState? _currentCombat; /// /// Gets or creates the singleton instance. /// public static VulnerabilityChecker GetOrCreate() { if (_instance == null) _instance = new VulnerabilityChecker(); return _instance; } private VulnerabilityChecker() { DiscoverVulnerabilityTypes(); } // Cached types discovered via reflection private Type? _vulnerabilityPowerType; private Type? _applyStatusCmdType; private Type? _damageCmdType; private void DiscoverVulnerabilityTypes() { var assembly = Assembly.GetAssembly(typeof(CombatState)); if (assembly == null) return; var types = assembly.GetTypes(); // Look for Vulnerability power type _vulnerabilityPowerType = types.FirstOrDefault(t => t.Name.Contains("Vulnerability") && t.FullName != null && (t.FullName.Contains("Powers") || t.FullName.Contains("Power"))); _applyStatusCmdType = types.FirstOrDefault(t => t.Name.Contains("ApplyStatus") && t.Name.Contains("Cmd")); _damageCmdType = types.FirstOrDefault(t => t.Name.Contains("Damage") && t.Name.Contains("Cmd")); } /// /// Initializes the checker for a new combat. /// public void Initialize(CombatState combat) { _currentCombat = combat; _playerHasVulnCard.Clear(); foreach (var player in GetAllPlayers()) { _playerHasVulnCard[player] = false; } OnCardChanged(); } /// /// Called when cards change in hand (draw, discard, play). /// Re-evaluates all players. /// public void OnCardChanged() { if (_currentCombat == null) return; foreach (var player in GetAllPlayers()) { if (!_playerHasVulnCard.ContainsKey(player)) continue; bool hadVuln = _playerHasVulnCard.GetValueOrDefault(player, false); bool nowHasVuln = PlayerHasVulnerabilityCard(player); if (hadVuln != nowHasVuln) { _playerHasVulnCard[player] = nowHasVuln; OnPlayerVulnerabilityChanged?.Invoke(player, nowHasVuln); } } } /// /// Fired when a player's vulnerability card status changes. /// public event Action? OnPlayerVulnerabilityChanged; /// /// Returns true if the given player has at least one card in hand /// that can inflict Vulnerability. /// public bool PlayerHasVulnerabilityCard(Player player) { var hand = GetHandForPlayer(player); if (hand == null || hand.Count == 0) return false; foreach (var card in hand) { if (CardCanApplyVulnerability(card)) return true; } return false; } /// /// Gets all players in the current combat. /// public IEnumerable GetAllPlayers() { if (_currentCombat == null) return Array.Empty(); var combatType = _currentCombat.GetType(); List result = new(); // Try Players property foreach (var propName in new[] { "Players", "CombatPlayers" }) { var prop = combatType.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (prop != null) { try { var value = prop.GetValue(_currentCombat); if (value is IEnumerable players) { foreach (var player in players) result.Add(player); return result; } if (value is IEnumerable objects) { foreach (var o in objects) { if (o is Player p) result.Add(p); } return result; } } catch { } } } return result; } /// /// Checks whether a single card can apply Vulnerability to enemies. /// private bool CardCanApplyVulnerability(CardModel card) { if (CheckTemplateForVulnerability(card)) return true; if (CheckCommandsForVulnerability(card)) return true; if (CheckDescriptionForVulnerability(card)) return true; return false; } /// /// Strategy 1: Check the card type name against known vulnerability-applying patterns. /// private bool CheckTemplateForVulnerability(CardModel card) { var cardType = card.GetType(); var name = cardType.Name.ToLowerInvariant(); if (name.Contains("vulnerability") || name.Contains("vuln")) return true; // Check base types var baseType = cardType.BaseType; while (baseType != null) { var baseName = baseType.Name.ToLowerInvariant(); if (baseName.Contains("vulnerability") || baseName.Contains("vuln")) return true; baseType = baseType.BaseType; } return false; } /// /// Strategy 2: Use reflection to inspect card commands for vulnerability application. /// private bool CheckCommandsForVulnerability(CardModel card) { var cardType = card.GetType(); foreach (var prop in cardType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { if (prop.PropertyType.IsGenericType && (prop.PropertyType.GetGenericTypeDefinition() == typeof(List<>) || prop.PropertyType.GetGenericTypeDefinition() == typeof(IReadOnlyList<>))) { try { var value = prop.GetValue(card); if (value is IEnumerable commands) { foreach (var cmd in commands) { if (cmd == null) continue; var cmdName = cmd.GetType().Name.ToLowerInvariant(); if (cmdName.Contains("vulnerability")) return true; if (cmdName.Contains("applystatus") && cmd.GetType().IsGenericType) { foreach (var arg in cmd.GetType().GetGenericArguments()) { if (arg.Name.Contains("Vulnerability")) return true; } } if (cmdName.Contains("damage") && cmdName.Contains("cmd")) { if (CheckDamageCmdForVulnerability(cmd)) return true; } } } } catch { } } } return false; } /// /// Checks if a damage command has vulnerability applied as a post-hit effect. /// private bool CheckDamageCmdForVulnerability(object cmd) { var cmdType = cmd.GetType(); foreach (var prop in cmdType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { var propName = prop.Name.ToLowerInvariant(); if (propName.Contains("vulnerability") || propName.Contains("vuln") || propName.Contains("applyonhit") || propName.Contains("posthit")) { try { var value = prop.GetValue(cmd); if (value != null) { if (value.GetType().Name.Contains("Vulnerability")) return true; if (value is IEnumerable items) { foreach (var item in items) { if (item != null && item.GetType().Name.Contains("Vulnerability")) return true; } } } } catch { } } } return false; } /// /// Strategy 3: Check card description for vulnerability-related keywords. /// private bool CheckDescriptionForVulnerability(CardModel card) { try { var cardType = card.GetType(); string? text = null; foreach (var propName in new[] { "Description", "SmartDescription", "Text", "TooltipText", "LocalizedDescription" }) { var prop = cardType.GetProperty(propName, BindingFlags.Public | BindingFlags.Instance); if (prop != null) { try { var val = prop.GetValue(card); if (val is string s && !string.IsNullOrEmpty(s)) { text = s; break; } } catch { } } } if (string.IsNullOrEmpty(text)) return false; text = text.ToLowerInvariant(); return text.Contains("vulnerabilit") || text.Contains("vuln.") || text.Contains("take more") || (text.Contains("damage taken") && text.Contains("more")) || text.Contains("weakens"); } catch { return false; } } /// /// Gets the hand cards for a given player using reflection. /// private List? GetHandForPlayer(Player player) { try { var playerType = player.GetType(); // Try Player properties that might reference hand cards foreach (var propName in new[] { "Hand", "CardsInHand", "HandCards", "PlayerHand" }) { var prop = playerType.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (prop != null) { var value = prop.GetValue(player); if (TryGetCardModels(value, out var cards)) return cards; } } // Try PlayerCombatState properties foreach (var propName in new[] { "CombatState", "PlayerCombatState", "Combat" }) { var prop = playerType.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (prop != null) { var cs = prop.GetValue(player); if (cs != null) { var csType = cs.GetType(); foreach (var handPropName in new[] { "Hand", "CardsInHand", "HandCards" }) { var handProp = csType.GetProperty(handPropName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (handProp != null) { var value = handProp.GetValue(cs); if (TryGetCardModels(value, out var cards)) return cards; } } } } } // Try CardPile approach: get the player's CardPile and filter by Hand type foreach (var propName in new[] { "CardPile", "Pile", "CardPiles", "CombatPile" }) { var prop = playerType.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (prop != null) { var value = prop.GetValue(player); if (TryGetCardModels(value, out var cards)) { // Filter to only hand cards by checking each card's Pile property return FilterHandCards(cards); } } } } catch { // Ignore reflection errors } return null; } private static bool TryGetCardModels(object? value, out List cards) { cards = new List(); if (value is IEnumerable modelEnumerable) { cards = new List(modelEnumerable); return true; } if (value is IEnumerable objectEnumerable) { foreach (var obj in objectEnumerable) { if (obj is CardModel model) cards.Add(model); } return cards.Count > 0; } if (value is CardModel singleModel) { cards.Add(singleModel); return true; } return false; } private List FilterHandCards(List cards) { var result = new List(); // Try to filter by Pile property == PileType.Hand foreach (var card in cards) { try { var pileProp = card.GetType().GetProperty("Pile", BindingFlags.Public | BindingFlags.Instance); if (pileProp != null) { var pile = pileProp.GetValue(card); if (pile != null) { var pileName = pile.GetType().Name; if (pileName.Contains("Hand")) result.Add(card); else if (pile is Enum e && e.ToString().Contains("Hand")) result.Add(card); } } } catch { } } // If filtering found no cards, return all (assume they're all in hand) if (result.Count == 0) return cards; return result; } }