Files
squad-rain-ops-mini/Plugins/ScratchCardPlugin.cs

301 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}");
}
}
}