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}");
}
}
}