using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using RainOpsMini.Helpers; using RainOpsMini.Services; namespace RainOpsMini.Plugins { public class ScratchCardConfig { /// /// 是否启用插件 /// public bool Enabled { get; set; } = false; /// /// 触发功能关键字 (可配多个,中文支持首拼、缩写等模糊匹配方式) /// public string LikeKey { get; set; } = "刮刮乐"; /// /// 每次需要消耗的积分数 /// public int Integral { get; set; } = 50; /// /// 用户每次可抽取多少个号码 /// public int UserWinningNumber { get; set; } = 5; /// /// 翻倍倍率 /// public int DoubleCount { get; set; } = 2; /// /// 普通中奖号码的个数 /// public int WinningNumber { get; set; } = 2; /// /// 奖池累积积分 (内部维护) /// public int Jackpot { get; set; } = 1000; } public class ScratchCardPlugin : IPlugin, IDisposable { public string Name => "积分刮刮乐"; public string Description => "消耗积分参与刮刮乐,奖池越大中奖概率越高:奖池越大 → 中奖区间越窄 → 命中概率越高 → 玩家越愿意玩 → 奖池继续增大 → 良性循环"; public PluginCategory Category => PluginCategory.PointsFunction; public bool IsEnabled => _config.Enabled; private ScratchCardConfig _config = new ScratchCardConfig(); private readonly Random _random = new Random(); private readonly object _lock = new object(); // 基础数字范围,奖池越大此范围越小,越容易中奖 private const int BaseRange = 100; private const int MinRange = 20; // 最小范围,保证不太容易中 public ScratchCardPlugin() { ApplyConfig(); } public object GetConfig() => _config; public void UpdateConfig(JsonElement config) { var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; try { // 注意:更新配置时需要保留原有的 Jackpot int currentJackpot = _config.Jackpot; _config = JsonSerializer.Deserialize(config.GetRawText(), options) ?? new ScratchCardConfig(); // 如果传入的配置中 Jackpot 为默认值或不合理(例如重置了),尝试保留旧值 // 这里假设前端通常不会传递 Jackpot 字段,或者传递的是最新值 // 如果为了安全,可以强制读取旧值 if (_config.Jackpot < currentJackpot && _config.Jackpot == 1000) { _config.Jackpot = currentJackpot; } ApplyConfig(); } catch (Exception ex) { RainOpsLog.Log($"[{Name}] UpdateConfig Error: {ex.Message}"); } } public void ResetConfig() { _config = new ScratchCardConfig(); ApplyConfig(); } private void ApplyConfig() { Program.OnRconMessageReceived -= OnRconMessage; if (IsEnabled) { Program.OnRconMessageReceived += OnRconMessage; } } public void Dispose() { Program.OnRconMessageReceived -= OnRconMessage; } private async void OnRconMessage(string msg) { if (!IsEnabled) return; try { var (steamId, name, content) = ParseChat(msg); if (string.IsNullOrEmpty(steamId)) return; if (IsTriggerWord(content)) { await HandleScratchCard(steamId, name); } } catch (Exception ex) { RainOpsLog.Log($"[{Name}] Error: {ex.Message}"); } } private bool IsTriggerWord(string content) { if (string.IsNullOrWhiteSpace(_config.LikeKey)) return false; var keys = _config.LikeKey.Split(new[] { ',', ',', '|' }, StringSplitOptions.RemoveEmptyEntries); foreach (var key in keys) { if (string.IsNullOrWhiteSpace(key)) continue; // 1. 精确匹配 if (content.Equals(key, StringComparison.OrdinalIgnoreCase)) return true; // 2. 拼音首字母 string initials = PinyinHelper.GetInitials(key); if (!string.IsNullOrEmpty(initials) && content.Equals(initials, StringComparison.OrdinalIgnoreCase)) return true; // 3. 全拼 string pinyin = PinyinHelper.GetPinyin(key); if (!string.IsNullOrEmpty(pinyin) && content.Equals(pinyin, StringComparison.OrdinalIgnoreCase)) return true; } return false; } private async Task HandleScratchCard(string steamId, string name) { // 1. 检查积分 int currentPoints = await PointsService.GetPointsAsync(steamId); if (currentPoints < _config.Integral) { await SendWarn(steamId, $"积分不足!刮刮乐需要 {_config.Integral} 积分,当前拥有 {currentPoints} 积分。"); return; } // 2. 扣除积分并加入奖池 int currentBalance = await PointsService.AdjustPointsAsync(steamId, -_config.Integral); lock (_lock) { _config.Jackpot += _config.Integral; } // 3. 执行刮刮乐逻辑 // 算法说明: // 奖池越大,中奖概率越大。 // 实现方式:动态调整随机数范围 (Range)。 // Range = BaseRange / (1 + log10(Jackpot / BaseJackpot)) // 例如:BaseRange=100. // Jackpot=1000 (Base) -> Range = 100. // Jackpot=10000 -> Range ≈ 100 / 2 = 50. // Jackpot=100000 -> Range ≈ 100 / 3 = 33. // 用户抽取 UserWinningNumber 个数字,系统生成 WinningNumber 个数字。 // 只要有交集即为中奖。 int dynamicRange = CalculateDynamicRange(); var userNumbers = GenerateRandomNumbers(_config.UserWinningNumber, dynamicRange); var winningNumbers = GenerateRandomNumbers(_config.WinningNumber, dynamicRange); // 检查交集 var matches = userNumbers.Intersect(winningNumbers).ToList(); bool isWin = matches.Count > 0; // 4. 处理结果 int prize = 0; if (isWin) { prize = _config.Integral * _config.DoubleCount; // 检查奖池是否足够支付 bool canPay = false; lock (_lock) { if (_config.Jackpot >= prize) { _config.Jackpot -= prize; canPay = true; } else { // 奖池不足,全额支付剩下的,或者由系统补贴 // 这里选择系统补贴模式,但奖池清空 if (_config.Jackpot < prize) _config.Jackpot = 0; else _config.Jackpot -= prize; canPay = true; // 仍然算赢,系统出分 } } if (canPay) { currentBalance = await PointsService.AdjustPointsAsync(steamId, prize); string matchStr = string.Join(",", matches); string userStr = string.Join(",", userNumbers); string winStr = string.Join(",", winningNumbers); await SendBroadcast($"恭喜玩家【{name}】在刮刮乐中赢得了 {prize} 积分!(中奖号码: {matchStr})"); await SendWarn(steamId, $"恭喜中奖!\n您的号码: {userStr}\n中奖号码: {winStr}\n获得: {prize} 积分\n当前积分: {currentBalance}"); } } else { string userStr = string.Join(",", userNumbers); string winStr = string.Join(",", winningNumbers); await SendWarn(steamId, $"很遗憾未中奖。\n您的号码: {userStr}\n中奖号码: {winStr}\n奖池已累积至: {_config.Jackpot} 积分 (概率提升中!)\n当前积分: {currentBalance}"); } // 触发配置保存以便持久化奖池 // 注意:通常插件系统需要提供SaveConfig方法,这里假设UpdateConfig会被外部调用保存,或者我们手动触发某种保存机制 // 由于 IPlugin 接口没有 SaveConfig,我们只能依赖内存状态或自行实现保存。 // 在 RainOpsMini 中,配置通常由 PluginManager 管理并定期保存或在 UpdateConfig 时保存。 // 这里的 Jackpot 变化是内存中的,如果重启会丢失增量。 // 为了完善,建议后续在 PluginManager 中增加自动保存机制,或者在此处不处理持久化 (接受重启重置)。 } private int CalculateDynamicRange() { // 基础奖池阈值 double baseJackpot = 1000.0; double currentJackpot = Math.Max(_config.Jackpot, baseJackpot); // 对数衰减因子 double factor = Math.Log10(currentJackpot / baseJackpot) + 1.0; // 计算新范围 int range = (int)(BaseRange / factor); return Math.Max(range, MinRange); } private List GenerateRandomNumbers(int count, int max) { var list = new HashSet(); while (list.Count < count) { list.Add(_random.Next(1, max + 1)); } return list.OrderBy(x => x).ToList(); } private (string steamId, string name, string content) ParseChat(string msg) { var match = Regex.Match(msg, @"ChatMessage: \[(?\d+)\] (?.+) : (?.+)"); if (!match.Success) { match = Regex.Match(msg, @"\[Chat.+?\] \[Online IDs:EOS: .+? steam: (?\d+)\]\s+(?.+?) : (?.+)"); } if (match.Success) { return (match.Groups["steamId"].Value, match.Groups["name"].Value, match.Groups["content"].Value.Trim()); } return (null, null, null); } private async Task SendWarn(string steamId, string msg) { await Program.Client.SendCommandAsync($"AdminWarn {steamId} {msg}"); } private async Task SendBroadcast(string msg) { await Program.Client.SendCommandAsync($"AdminBroadcast {msg}"); } } }