diff --git a/KfChatDotNetBot/Assets/bossmancoin-heads-jacky.webp b/KfChatDotNetBot/Assets/bossmancoin-heads-jacky.webp new file mode 100644 index 0000000..28dce45 Binary files /dev/null and b/KfChatDotNetBot/Assets/bossmancoin-heads-jacky.webp differ diff --git a/KfChatDotNetBot/Assets/bossmancoin-heads.webp b/KfChatDotNetBot/Assets/bossmancoin-heads.webp new file mode 100644 index 0000000..1918f1d Binary files /dev/null and b/KfChatDotNetBot/Assets/bossmancoin-heads.webp differ diff --git a/KfChatDotNetBot/Assets/bossmancoin-tails-jacky.webp b/KfChatDotNetBot/Assets/bossmancoin-tails-jacky.webp new file mode 100644 index 0000000..8e4b2d9 Binary files /dev/null and b/KfChatDotNetBot/Assets/bossmancoin-tails-jacky.webp differ diff --git a/KfChatDotNetBot/Assets/bossmancoin-tails.webp b/KfChatDotNetBot/Assets/bossmancoin-tails.webp new file mode 100644 index 0000000..5cbab9e Binary files /dev/null and b/KfChatDotNetBot/Assets/bossmancoin-tails.webp differ diff --git a/KfChatDotNetBot/Commands/Kasino/CoinflipCommand.cs b/KfChatDotNetBot/Commands/Kasino/CoinflipCommand.cs new file mode 100644 index 0000000..1ba9a53 --- /dev/null +++ b/KfChatDotNetBot/Commands/Kasino/CoinflipCommand.cs @@ -0,0 +1,148 @@ + +using System.Net.Http.Headers; +using System.Text.RegularExpressions; +using KfChatDotNetBot.Extensions; +using KfChatDotNetBot.Models; +using KfChatDotNetBot.Models.DbModels; +using KfChatDotNetBot.Services; +using KfChatDotNetBot.Settings; +using KfChatDotNetWsClient.Models.Events; +using RandN; +using RandN.Compat; + +namespace KfChatDotNetBot.Commands.Kasino; + +[KasinoCommand] +[WagerCommand] +public class CoinflipCommand : ICommand +{ + public List Patterns => [ + new Regex("^coinflip$", RegexOptions.IgnoreCase), + new Regex(@"^coinflip (?\d+) (?heads|tails)$", RegexOptions.IgnoreCase), + new Regex(@"^coinflip (?\d+\.\d+) (?heads|tails)$", RegexOptions.IgnoreCase), + ]; + + public string? HelpText => "!coinflip , flip a coin"; + public UserRight RequiredRight => UserRight.Loser; + public TimeSpan Timeout => TimeSpan.FromSeconds(5); + public RateLimitOptionsModel? RateLimitOptions => new() + { + MaxInvocations = 3, + Window = TimeSpan.FromSeconds(15) + }; + private static double _houseEdge = 0.015; // house edge hack? + + public async Task RunCommand(ChatBot botInstance, MessageModel messagen, UserDbModel user, GroupCollection arguments, + CancellationToken ctx) + { + var settings = await SettingsProvider.GetMultipleValuesAsync([ + BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay, BuiltIn.Keys.KasinoCoinflipCleanupDelay, + BuiltIn.Keys.KasinoCoinflipEnabled + ]); + + var coinflipEnabled = settings[BuiltIn.Keys.KasinoCoinflipEnabled].ToBoolean(); + if (!coinflipEnabled) + { + var gameDisabledCleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoGameDisabledMessageCleanupDelay].ToType()); + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, coinflip is currently disabled.", + true, autoDeleteAfter: gameDisabledCleanupDelay); + return; + } + + var cleanupDelay = TimeSpan.FromMilliseconds(settings[BuiltIn.Keys.KasinoCoinflipCleanupDelay].ToType()); + + if (!arguments.TryGetValue("amount", out var amount)) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, not enough arguments. !coinflip ", + true, autoDeleteAfter: cleanupDelay); + return; + } + + if (!arguments.TryGetValue("choice", out var choice)) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, not enough arguments. !coinflip ", + true, autoDeleteAfter: cleanupDelay); + return; + } + + var choiceStr = choice.Value.ToLowerInvariant(); + var wager = Convert.ToDecimal(amount.Value); + if (wager <= 0) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, your wager must be greater than zero.", + true, autoDeleteAfter: cleanupDelay); + return; + } + + var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx); + + if (gambler == null) + throw new InvalidOperationException($"Caught a null when retrieving gambler for {user.KfUsername}"); + if (gambler.Balance < wager) + { + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, your balance of {await gambler.Balance.FormatKasinoCurrencyAsync()} isn't enough for this wager.", + true, autoDeleteAfter: cleanupDelay); + return; + } + var rolled = Money.GetRandomDouble(gambler); + var colors = + await SettingsProvider.GetMultipleValuesAsync([ + BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor + ]); + + + decimal newBalance; + if (rolled > 0.5 + _houseEdge) + { + // won + var coinflipAnimation = await GetCoinFlipAnimationUrl(choiceStr); + + await botInstance.SendChatMessageAsync($"[IMG]{coinflipAnimation}[/IMG]", true, autoDeleteAfter: cleanupDelay); + await Task.Delay(1200, ctx); + + var effect = wager; + newBalance = await Money.NewWagerAsync(gambler.Id, wager, effect, WagerGame.CoinFlip, ct: ctx); + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, you [B][COLOR={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]WON![/COLOR][/B] " + + $"You won {await effect.FormatKasinoCurrencyAsync()} and your balance is now {await newBalance.FormatKasinoCurrencyAsync()}", + true, autoDeleteAfter: cleanupDelay); + } + else + { + // lost + bool isJacky = rolled > 0.5; // would've won without house edge + var coinflipAnimationURL = await GetCoinFlipAnimationUrl("heads" == choiceStr ? "tails" : "heads", isJacky); + + await botInstance.SendChatMessageAsync($"[IMG]{coinflipAnimationURL}[/IMG]", true, autoDeleteAfter: cleanupDelay); + await Task.Delay(1200, ctx); + + newBalance = await Money.NewWagerAsync(gambler.Id, wager, -wager, WagerGame.CoinFlip, ct: ctx); + await botInstance.SendChatMessageAsync( + $"{user.FormatUsername()}, you [B][COLOR={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]LOST![/COLOR][/B] " + + $"Your balance is now {await newBalance.FormatKasinoCurrencyAsync()}", + true, autoDeleteAfter: cleanupDelay); + } + } + + private static async Task GetCoinFlipAnimationUrl(string choiceStr, bool isJacky = false) + { + string animationPath; + if (isJacky) + { + animationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets", $"bossmancoin-{choiceStr}-jacky.webp"); + } + else + { + animationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets", $"bossmancoin-{choiceStr}.webp"); + } + if (!File.Exists(animationPath)) throw new DirectoryNotFoundException($"Coinflip animation missing at {animationPath}"); + + using var imageStream = File.OpenRead(animationPath); + return await Zipline.Upload(imageStream, new MediaTypeHeaderValue("image/webp"), "1h"); + } +} \ No newline at end of file diff --git a/KfChatDotNetBot/Settings/BuiltIn.cs b/KfChatDotNetBot/Settings/BuiltIn.cs index 6b2276c..f6f3c62 100644 --- a/KfChatDotNetBot/Settings/BuiltIn.cs +++ b/KfChatDotNetBot/Settings/BuiltIn.cs @@ -453,6 +453,8 @@ public static class BuiltIn public static string KasinoLambchopCleanupDelay = "Kasino.Lambchop.CleanupDelay"; [BuiltInSetting("Delay in milliseconds before cleaning up dice", SettingValueType.Text, "15000", WholeNumberRegex)] public static string KasinoDiceCleanupDelay = "Kasino.Dice.CleanupDelay"; + [BuiltInSetting("Delay in milliseconds before cleaning up coinflip", SettingValueType.Text, "15000", WholeNumberRegex)] + public static string KasinoCoinflipCleanupDelay = "Kasino.Coinflip.CleanupDelay"; [BuiltInSetting("Delay in milliseconds before cleaning up wheel", SettingValueType.Text, "30000", WholeNumberRegex)] public static string KasinoWheelCleanupDelay = "Kasino.Wheel.CleanupDelay"; [BuiltInSetting("Whether the YouTube PubSub Redis client is enabled", SettingValueType.Boolean, "true", BooleanRegex)]