mirror of
https://gitee.com/xiarenalofs/squad-rain-ops-mini.git
synced 2026-08-06 13:26:25 +08:00
301 lines
11 KiB
C#
301 lines
11 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 是否启用插件
|
||
/// </summary>
|
||
public bool Enabled { get; set; } = false;
|
||
|
||
/// <summary>
|
||
/// 触发功能关键字 (可配多个,中文支持首拼、缩写等模糊匹配方式)
|
||
/// </summary>
|
||
public string LikeKey { get; set; } = "刮刮乐";
|
||
|
||
/// <summary>
|
||
/// 每次需要消耗的积分数
|
||
/// </summary>
|
||
public int Integral { get; set; } = 50;
|
||
|
||
/// <summary>
|
||
/// 用户每次可抽取多少个号码
|
||
/// </summary>
|
||
public int UserWinningNumber { get; set; } = 5;
|
||
|
||
/// <summary>
|
||
/// 翻倍倍率
|
||
/// </summary>
|
||
public int DoubleCount { get; set; } = 2;
|
||
|
||
/// <summary>
|
||
/// 普通中奖号码的个数
|
||
/// </summary>
|
||
public int WinningNumber { get; set; } = 2;
|
||
|
||
/// <summary>
|
||
/// 奖池累积积分 (内部维护)
|
||
/// </summary>
|
||
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<ScratchCardConfig>(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<int> GenerateRandomNumbers(int count, int max)
|
||
{
|
||
var list = new HashSet<int>();
|
||
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: \[(?<steamId>\d+)\] (?<name>.+) : (?<content>.+)");
|
||
if (!match.Success)
|
||
{
|
||
match = Regex.Match(msg, @"\[Chat.+?\] \[Online IDs:EOS: .+? steam: (?<steamId>\d+)\]\s+(?<name>.+?) : (?<content>.+)");
|
||
}
|
||
|
||
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}");
|
||
}
|
||
}
|
||
}
|