mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-05-02 04:22:04 -04:00
Slots 2.0. Now with less Raylib
This commit is contained in:
@@ -1,3 +1,13 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using RandN;
|
||||||
|
using RandN.Compat;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Formats.Webp;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using SixLabors.ImageSharp.Drawing;
|
||||||
|
using SixLabors.ImageSharp.Drawing.Processing;
|
||||||
|
using SixLabors.Fonts;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using KfChatDotNetBot.Extensions;
|
using KfChatDotNetBot.Extensions;
|
||||||
using KfChatDotNetBot.Models;
|
using KfChatDotNetBot.Models;
|
||||||
@@ -5,15 +15,6 @@ using KfChatDotNetBot.Models.DbModels;
|
|||||||
using KfChatDotNetBot.Services;
|
using KfChatDotNetBot.Services;
|
||||||
using KfChatDotNetBot.Settings;
|
using KfChatDotNetBot.Settings;
|
||||||
using KfChatDotNetWsClient.Models.Events;
|
using KfChatDotNetWsClient.Models.Events;
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Numerics;
|
|
||||||
using RandN;
|
|
||||||
using RandN.Compat;
|
|
||||||
using Raylib_cs;
|
|
||||||
using SixLabors.ImageSharp;
|
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
|
||||||
using SixLabors.ImageSharp.Formats.Webp;
|
|
||||||
|
|
||||||
namespace KfChatDotNetBot.Commands.Kasino;
|
namespace KfChatDotNetBot.Commands.Kasino;
|
||||||
|
|
||||||
@@ -24,7 +25,10 @@ public class SlotsCommand : ICommand
|
|||||||
public List<Regex> Patterns => [
|
public List<Regex> Patterns => [
|
||||||
new Regex(@"^slots (?<amount>\d+)$", RegexOptions.IgnoreCase),
|
new Regex(@"^slots (?<amount>\d+)$", RegexOptions.IgnoreCase),
|
||||||
new Regex(@"^slots (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
|
new Regex(@"^slots (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
|
||||||
new Regex("^slots$", RegexOptions.IgnoreCase)
|
new Regex("^slots$", RegexOptions.IgnoreCase),
|
||||||
|
new Regex(@"^sluts (?<amount>\d+)$", RegexOptions.IgnoreCase),
|
||||||
|
new Regex(@"^sluts (?<amount>\d+\.\d+)$", RegexOptions.IgnoreCase),
|
||||||
|
new Regex("^sluts", RegexOptions.IgnoreCase)
|
||||||
];
|
];
|
||||||
|
|
||||||
public string? HelpText => "!slots [bet amount]";
|
public string? HelpText => "!slots [bet amount]";
|
||||||
@@ -36,7 +40,8 @@ public class SlotsCommand : ICommand
|
|||||||
Window = TimeSpan.FromSeconds(30)
|
Window = TimeSpan.FromSeconds(30)
|
||||||
};
|
};
|
||||||
|
|
||||||
public async Task RunCommand(ChatBot botInstance, MessageModel messagen, UserDbModel user, GroupCollection arguments, CancellationToken ctx)
|
public async Task RunCommand(ChatBot botInstance, MessageModel messagen, UserDbModel user,
|
||||||
|
GroupCollection arguments, CancellationToken ctx)
|
||||||
{
|
{
|
||||||
if (!arguments.TryGetValue("amount", out var amount)) //if user just enters !keno
|
if (!arguments.TryGetValue("amount", out var amount)) //if user just enters !keno
|
||||||
{
|
{
|
||||||
@@ -58,181 +63,303 @@ public class SlotsCommand : ICommand
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Raylib.SetConfigFlags(ConfigFlags.HiddenWindow);
|
|
||||||
Raylib.InitWindow(500,900,"KiwiSlot");
|
|
||||||
|
|
||||||
decimal winnings;
|
decimal winnings;
|
||||||
string featureAddOn;
|
using (var board = new KiwiSlotBoard(wager))
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var board = new KiwiSlotBoard(wager);
|
|
||||||
board.LoadAssets();
|
board.LoadAssets();
|
||||||
board.ExecuteGameLoop();
|
board.ExecuteGameLoop();
|
||||||
using var finalImage = board.GenerateAnimatedWebp(board.SlotFrames);
|
using (var finalImageStream = board.ExportAndCleanup())
|
||||||
winnings = board.RunningTotalDisplay;
|
|
||||||
featureAddOn = board.GotFeature ? "Congrats on the feature." : "";
|
|
||||||
board.Dispose();
|
|
||||||
if (finalImage == null) throw new InvalidOperationException("finalimage was null");
|
|
||||||
var imageUrl = await Zipline.Upload(finalImage, new MediaTypeHeaderValue("image/webp"), "1h", ctx);
|
|
||||||
if (imageUrl == null) throw new InvalidOperationException("Image failed to upload/failed to get URL");
|
|
||||||
await botInstance.SendChatMessageAsync($"[img]{imageUrl}[/img]", true,
|
|
||||||
autoDeleteAfter: TimeSpan.FromMinutes(3));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
{
|
||||||
Raylib.CloseWindow();
|
var imageUrl = await Zipline.Upload(finalImageStream, new MediaTypeHeaderValue("image/webp"), "1h", ctx);
|
||||||
|
await botInstance.SendChatMessageAsync($"[img]{imageUrl}[/img]", true,
|
||||||
|
autoDeleteAfter: TimeSpan.FromSeconds(150));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
winnings = (decimal)board.RunningTotalDisplay;
|
||||||
|
}
|
||||||
var colors =
|
var colors =
|
||||||
await SettingsProvider.GetMultipleValuesAsync([
|
await SettingsProvider.GetMultipleValuesAsync([
|
||||||
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
|
BuiltIn.Keys.KiwiFarmsGreenColor, BuiltIn.Keys.KiwiFarmsRedColor
|
||||||
]);
|
]);
|
||||||
decimal newBalance;
|
var newBalance = await Money.NewWagerAsync(gambler.Id, wager, -wager, WagerGame.Slots, ct: ctx);
|
||||||
if (winnings == 0) //dud spin
|
if (winnings == 0)
|
||||||
{
|
{
|
||||||
newBalance = await Money.NewWagerAsync(gambler.Id, wager, -wager, WagerGame.Slots, ct: ctx);
|
|
||||||
await botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
$"{user.FormatUsername()} you [color={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]lost[/color]. Current balance: {await newBalance.FormatKasinoCurrencyAsync()}",
|
$"{user.FormatUsername()} you [color={colors[BuiltIn.Keys.KiwiFarmsRedColor].Value}]lost[/color]. Current balance: {await newBalance.FormatKasinoCurrencyAsync()}",
|
||||||
true, autoDeleteAfter: TimeSpan.FromSeconds(30));
|
true, autoDeleteAfter: TimeSpan.FromSeconds(150));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
winnings -= wager;
|
|
||||||
newBalance = await Money.NewWagerAsync(gambler.Id, wager, winnings, WagerGame.Slots, ct: ctx);
|
newBalance = await Money.NewWagerAsync(gambler.Id, wager, winnings, WagerGame.Slots, ct: ctx);
|
||||||
await botInstance.SendChatMessageAsync(
|
await botInstance.SendChatMessageAsync(
|
||||||
$"{user.FormatUsername()}, you [color={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]won[/color] {await winnings.FormatKasinoCurrencyAsync()}! Current balance: {await newBalance.FormatKasinoCurrencyAsync()}" +
|
$"{user.FormatUsername()}, you [color={colors[BuiltIn.Keys.KiwiFarmsGreenColor].Value}]won[/color] {await winnings.FormatKasinoCurrencyAsync()}! Current balance: {await newBalance.FormatKasinoCurrencyAsync()}", true, autoDeleteAfter: TimeSpan.FromSeconds(150));
|
||||||
$"{featureAddOn}", true, autoDeleteAfter: TimeSpan.FromSeconds(30));
|
}
|
||||||
|
public class WinDetail
|
||||||
|
{
|
||||||
|
public required (int row, int col)[] Path { get; set; }
|
||||||
|
public double Amount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private class KiwiSlotBoard : IDisposable
|
private class KiwiSlotBoard : IDisposable
|
||||||
{
|
{
|
||||||
private const char WILD = 'K', FEATURE = 'L', EXPANDER = 'M';
|
private const char WILD = 'K', FEATURE = 'L', EXPANDER = 'M';
|
||||||
public readonly List<Raylib_cs.Image> SlotFrames = [];
|
private Image<Rgba32> _headerImg;
|
||||||
private readonly Dictionary<char, Texture2D> _symbolTextures = new();
|
private Dictionary<char, Image<Rgba32>> _symbolImgs = new();
|
||||||
private readonly Dictionary<int, Texture2D> _expanderTextures = new();
|
private Dictionary<int, Image<Rgba32>> _expanderImgs = new();
|
||||||
private Texture2D _headerTexture;
|
private Font _font;
|
||||||
|
|
||||||
|
// Optimized Animation Container
|
||||||
|
private Image<Rgba32> AnimatedImage { get; set; }
|
||||||
|
|
||||||
private readonly char[,] _preboard = new char[5, 5];
|
private readonly char[,] _preboard = new char[5, 5];
|
||||||
private char[,] _board = new char[5, 5];
|
private char[,] _board = new char[5, 5];
|
||||||
private readonly decimal _userBet;
|
private readonly decimal _userBet;
|
||||||
public decimal RunningTotalDisplay;
|
public double RunningTotalDisplay = 0;
|
||||||
public bool GotFeature;
|
private int _activeFeatureTier = 0, _currentFeatureSpin = 0;
|
||||||
private int _activeFeatureTier;
|
private bool _showGoldCircle = false;
|
||||||
private int _currentFeatureSpin; // Tracks progress through the feature
|
|
||||||
private bool _showGoldCircle;
|
|
||||||
|
|
||||||
private string SlotSkin = "Default";
|
|
||||||
|
|
||||||
private readonly RandomShim<StandardRng> _rand = RandomShim.Create(StandardRng.Create());
|
private readonly RandomShim<StandardRng> _rand = RandomShim.Create(StandardRng.Create());
|
||||||
private static readonly List<char> ExpanderWild =
|
private static readonly List<char> ExpanderWild =
|
||||||
['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2'];
|
['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2'];
|
||||||
private readonly Dictionary<char, double> _multiTable = new()
|
private readonly Dictionary<char, double> _multiTable = new() { { 'N', 2 }, { 'O', 3 }, { 'P', 4 }, { 'Q', 5 }, { 'R', 6 }, { 'S', 7 }, { 'T', 8 }, { 'U', 9 }, { 'V', 10 }, { 'W', 15 }, { 'X', 20 }, { 'Y', 25 }, { 'Z', 50 }, { '1', 100 }, { '2', 200 } };
|
||||||
{
|
private readonly Dictionary<string, double> _payoutTable = new() { { "A3", 0.2 }, { "A4", 1.0 }, { "A5", 5.0 }, { "B3", 0.2 }, { "B4", 1.0 }, { "B5", 5.0 }, { "C3", 0.3 }, { "C4", 1.5 }, { "C5", 7.5 }, { "D3", 0.3 }, { "D4", 1.5 }, { "D5", 7.5 }, { "E3", 0.4 }, { "E4", 2.0 }, { "E5", 10.0 }, { "F3", 1.0 }, { "F4", 5.0 }, { "F5", 15.0 }, { "G3", 1.0 }, { "G4", 5.0 }, { "G5", 15.0 }, { "H3", 1.5 }, { "H4", 7.5 }, { "H5", 17.5 }, { "I3", 1.5 }, { "I4", 7.5 }, { "I5", 17.5 }, { "J3", 2.0 }, { "J4", 10.0 }, { "J5", 20.0 }, { "K5", 25.0 }, { "L5", 25.0 }, { "M5", 25.0 } };
|
||||||
{ 'N', 2 }, { 'O', 3 }, { 'P', 4 }, { 'Q', 5 }, { 'R', 6 }, { 'S', 7 },
|
private readonly List<(int row, int col)[]> _payoutLines =
|
||||||
{ 'T', 8 }, { 'U', 9 }, { 'V', 10 }, { 'W', 15 }, { 'X', 20 }, { 'Y', 25 },
|
[
|
||||||
{ 'Z', 50 }, { '1', 100 }, { '2', 200 }
|
[(0, 0), (0, 1), (0, 2), (0, 3), (0, 4)], [(1, 0), (1, 1), (1, 2), (1, 3), (1, 4)],
|
||||||
};
|
[(2, 0), (2, 1), (2, 2), (2, 3), (2, 4)], [(3, 0), (3, 1), (3, 2), (3, 3), (3, 4)],
|
||||||
private readonly Dictionary<string, double> _payoutTable = new()
|
[(4, 0), (4, 1), (4, 2), (4, 3), (4, 4)], [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)],
|
||||||
{
|
[(4, 0), (3, 1), (2, 2), (1, 3), (0, 4)], [(1, 0), (0, 1), (1, 2), (0, 3), (1, 4)],
|
||||||
{ "A3", 0.2 }, { "A4", 1.0 }, { "A5", 5.0 }, { "B3", 0.2 }, { "B4", 1.0 }, { "B5", 5.0 },
|
[(2, 0), (1, 1), (2, 2), (1, 3), (2, 4)], [(3, 0), (2, 1), (3, 2), (2, 3), (3, 4)],
|
||||||
{ "C3", 0.3 }, { "C4", 1.5 }, { "C5", 7.5 }, { "D3", 0.3 }, { "D4", 1.5 }, { "D5", 7.5 },
|
[(4, 0), (3, 1), (4, 2), (3, 3), (4, 4)], [(0, 0), (1, 1), (0, 2), (1, 3), (0, 4)],
|
||||||
{ "E3", 0.4 }, { "E4", 2.0 }, { "E5", 10.0 }, { "F3", 1.0 }, { "F4", 5.0 }, { "F5", 15.0 },
|
[(1, 0), (2, 1), (1, 2), (2, 3), (1, 4)], [(2, 0), (3, 1), (2, 2), (3, 3), (2, 4)],
|
||||||
{ "G3", 1.0 }, { "G4", 5.0 }, { "G5", 15.0 }, { "H3", 1.5 }, { "H4", 7.5 }, { "H5", 17.5 },
|
[(3, 0), (4, 1), (3, 2), (4, 3), (3, 4)], [(2, 0), (1, 1), (0, 2), (1, 3), (2, 4)],
|
||||||
{ "I3", 1.5 }, { "I4", 7.5 }, { "I5", 17.5 }, { "J3", 2.0 }, { "J4", 10.0 }, { "J5", 20.0 },
|
[(3, 0), (2, 1), (1, 2), (2, 3), (3, 4)], [(2, 0), (3, 1), (4, 2), (3, 3), (2, 4)],
|
||||||
{ "K5", 25.0 }, { "L5", 25.0 }, { "M5", 25.0 }
|
[(1, 0), (2, 1), (3, 2), (2, 3), (1, 4)]
|
||||||
};
|
];
|
||||||
private readonly List<(int row, int col)[]> _payoutLines = new()
|
|
||||||
{
|
|
||||||
new[] { (0, 0), (0, 1), (0, 2), (0, 3), (0, 4) },
|
|
||||||
new[] { (1, 0), (1, 1), (1, 2), (1, 3), (1, 4) },
|
|
||||||
new[] { (2, 0), (2, 1), (2, 2), (2, 3), (2, 4) },
|
|
||||||
new[] { (3, 0), (3, 1), (3, 2), (3, 3), (3, 4) },
|
|
||||||
new[] { (4, 0), (4, 1), (4, 2), (4, 3), (4, 4) },
|
|
||||||
new[] { (0, 0), (1, 1), (2, 2), (3, 3), (4, 4) },
|
|
||||||
new[] { (4, 0), (3, 1), (2, 2), (1, 3), (0, 4) },
|
|
||||||
new[] { (1, 0), (0, 1), (1, 2), (0, 3), (1, 4) },
|
|
||||||
new[] { (2, 0), (1, 1), (2, 2), (1, 3), (2, 4) },
|
|
||||||
new[] { (3, 0), (2, 1), (3, 2), (2, 3), (3, 4) },
|
|
||||||
new[] { (4, 0), (3, 1), (4, 2), (3, 3), (4, 4) },
|
|
||||||
new[] { (0, 0), (1, 1), (0, 2), (1, 3), (0, 4) },
|
|
||||||
new[] { (1, 0), (2, 1), (1, 2), (2, 3), (1, 4) },
|
|
||||||
new[] { (2, 0), (3, 1), (2, 2), (3, 3), (2, 4) },
|
|
||||||
new[] { (3, 0), (4, 1), (3, 2), (4, 3), (3, 4) },
|
|
||||||
new[] { (2, 0), (1, 1), (0, 2), (1, 3), (2, 4) },
|
|
||||||
new[] { (3, 0), (2, 1), (1, 2), (2, 3), (3, 4) },
|
|
||||||
new[] { (2, 0), (3, 1), (4, 2), (3, 3), (2, 4) },
|
|
||||||
new[] { (1, 0), (2, 1), (3, 2), (2, 3), (1, 4) }
|
|
||||||
};
|
|
||||||
|
|
||||||
public KiwiSlotBoard(decimal bet) {
|
public KiwiSlotBoard(decimal bet)
|
||||||
|
{
|
||||||
_userBet = bet;
|
_userBet = bet;
|
||||||
RunningTotalDisplay = 0;
|
AnimatedImage = new Image<Rgba32>(600, 800);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadAssets()
|
public void LoadAssets()
|
||||||
{
|
{
|
||||||
var assetPath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Assets", SlotSkin);
|
var assetPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Default", "Assets");
|
||||||
_headerTexture = Raylib.LoadTexture(Path.Combine(assetPath, "header.png"));
|
|
||||||
foreach (var c in "ABCDEFGHIJKL") _symbolTextures[c] = Raylib.LoadTexture(Path.Combine(assetPath, $"{c}.png"));
|
if (!Directory.Exists(assetPath)) throw new DirectoryNotFoundException($"Assets folder missing at {assetPath}");
|
||||||
for (var i = 1; i <= 5; i++) _expanderTextures[i] = Raylib.LoadTexture(Path.Combine(assetPath, $"exp{i}.png"));
|
|
||||||
|
_headerImg = Image.Load<Rgba32>(System.IO.Path.Combine(assetPath, "header.png"));
|
||||||
|
foreach (var c in "ABCDEFGHIJKL") _symbolImgs[c] = Image.Load<Rgba32>(System.IO.Path.Combine(assetPath, $"{c}.png"));
|
||||||
|
for (var i = 1; i <= 5; i++) _expanderImgs[i] = Image.Load<Rgba32>(System.IO.Path.Combine(assetPath, $"exp{i}.png"));
|
||||||
|
_font = SystemFonts.CreateFont("Arial", 20, FontStyle.Bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderFrame(int dropOffset = 500, List<WinDetail>? activeWins = null)
|
||||||
|
{
|
||||||
|
using var frame = new Image<Rgba32>(600, 800);
|
||||||
|
frame.Mutate(ctx => {
|
||||||
|
ctx.Fill(Color.Black);
|
||||||
|
|
||||||
|
// --- SIDEBAR SECTION ---
|
||||||
|
var sidebarX = 0;
|
||||||
|
int[] tiers = [3, 4, 5];
|
||||||
|
int[] yCoords = [150, 300, 450];
|
||||||
|
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
var t = tiers[i];
|
||||||
|
var y = yCoords[i];
|
||||||
|
if (_showGoldCircle && _activeFeatureTier == t)
|
||||||
|
ctx.Fill(Color.Gold, new EllipsePolygon(sidebarX + 50, y + 50, 48));
|
||||||
|
|
||||||
|
if (_symbolImgs.TryGetValue(FEATURE, out var feat))
|
||||||
|
ctx.DrawImage(feat, new Point(sidebarX, y), 1f);
|
||||||
|
|
||||||
|
var lb = $"x{t}";
|
||||||
|
var sz = TextMeasurer.MeasureSize(lb, new TextOptions(_font));
|
||||||
|
ctx.DrawText(lb, _font, Color.White, new PointF(sidebarX + 50 - (sz.Width / 2), y + 105));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MAIN REEL SECTION ---
|
||||||
|
var mainX = 100;
|
||||||
|
ctx.DrawImage(_headerImg, new Point(mainX, 0), 1f);
|
||||||
|
|
||||||
|
var boardRect = new Rectangle(mainX, 200, 500, 500);
|
||||||
|
ctx.Clip(new RectangularPolygon(boardRect), clipCtx => {
|
||||||
|
var occupied = new bool[5, 5];
|
||||||
|
float animationY = (500 - dropOffset);
|
||||||
|
|
||||||
|
for (var j = 0; j < 5; j++) {
|
||||||
|
for (var i = 0; i < 5; i++) {
|
||||||
|
if (occupied[i, j]) continue;
|
||||||
|
var sym = _board[i, j];
|
||||||
|
var x = mainX + (j * 100);
|
||||||
|
var y = (200 + (i * 100)) - (int)animationY;
|
||||||
|
|
||||||
|
if (sym == EXPANDER || _multiTable.ContainsKey(sym)) {
|
||||||
|
var h = 0;
|
||||||
|
for (var k = i; k < 5; k++) if (_board[k, j] == sym) h++; else break;
|
||||||
|
if (_expanderImgs.TryGetValue(h, out var tex)) {
|
||||||
|
clipCtx.DrawImage(tex, new Point(x, y), 1f);
|
||||||
|
if (_multiTable.TryGetValue(sym, out var mVal))
|
||||||
|
clipCtx.DrawText($"x{mVal}", _font, Color.Yellow, new PointF(x + 50, y + (h * 50)));
|
||||||
|
}
|
||||||
|
for (var k = 0; k < h; k++) occupied[i + k, j] = true;
|
||||||
|
}
|
||||||
|
else if (_symbolImgs.TryGetValue(sym, out var tex)) clipCtx.DrawImage(tex, new Point(x, y), 1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeWins != null) {
|
||||||
|
foreach (var win in activeWins) {
|
||||||
|
var points = win.Path.Select(p => new PointF(mainX + (p.col * 100 + 50), 200 + (p.row * 100) + 50 - animationY)).ToArray();
|
||||||
|
clipCtx.Draw(new SolidPen(Color.White, 8f), new SixLabors.ImageSharp.Drawing.Path(new LinearLineSegment(points)));
|
||||||
|
|
||||||
|
var amtText = $"${win.Amount:F2}";
|
||||||
|
var midPoint = points[win.Path.Length / 2];
|
||||||
|
var size = TextMeasurer.MeasureSize(amtText, new TextOptions(_font));
|
||||||
|
var bgRect = new RectangularPolygon(midPoint.X - (size.Width / 2) - 5, midPoint.Y - (size.Height / 2) - 2, size.Width + 10, size.Height + 4);
|
||||||
|
clipCtx.Fill(Color.FromRgba(0, 0, 0, 200), bgRect);
|
||||||
|
clipCtx.DrawText(amtText, _font, Color.LimeGreen, new PointF(midPoint.X - (size.Width / 2), midPoint.Y - (size.Height / 2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- FOOTER SECTION ---
|
||||||
|
ctx.Fill(Color.FromRgb(15, 15, 15), new Rectangle(0, 700, 600, 100));
|
||||||
|
ctx.DrawLine(Color.Gold, 3f, new PointF(0, 700), new PointF(600, 700));
|
||||||
|
|
||||||
|
var largeFont = SystemFonts.CreateFont("Arial", 35, FontStyle.Bold);
|
||||||
|
|
||||||
|
void DrawAutoScaledText(string text, Font font, Color color, RectangleF targetArea) {
|
||||||
|
var textOptions = new TextOptions(font);
|
||||||
|
var size = TextMeasurer.MeasureSize(text, textOptions);
|
||||||
|
var scale = 1.0f;
|
||||||
|
if (size.Width > targetArea.Width) scale = targetArea.Width / size.Width;
|
||||||
|
|
||||||
|
// FIX: Added 'using' to prevent Font object leaks
|
||||||
|
var finalFont = new Font(font, font.Size * scale);
|
||||||
|
var finalSize = TextMeasurer.MeasureSize(text, new TextOptions(finalFont));
|
||||||
|
var yPos = targetArea.Y + (targetArea.Height - finalSize.Height) / 2;
|
||||||
|
var xPos = targetArea.X + (targetArea.Width - finalSize.Width) / 2;
|
||||||
|
ctx.DrawText(text, finalFont, color, new PointF(xPos, yPos));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawAutoScaledText($"BET: ${_userBet:F2}", largeFont, Color.White, new RectangleF(20, 700, 180, 100));
|
||||||
|
DrawAutoScaledText($"WIN: ${RunningTotalDisplay:F2}", largeFont, Color.Gold, new RectangleF(380, 700, 200, 100));
|
||||||
|
|
||||||
|
if (_currentFeatureSpin > 0) {
|
||||||
|
var total = _activeFeatureTier switch { 3 => 3, 4 => 5, 5 => 10, _ => 0 };
|
||||||
|
DrawAutoScaledText($"SPIN {_currentFeatureSpin}/{total}", largeFont, Color.SkyBlue, new RectangleF(210, 700, 160, 100));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set delay and push to master animation
|
||||||
|
frame.Frames.RootFrame.Metadata.GetWebpMetadata().FrameDelay = 2;
|
||||||
|
AnimatedImage.Frames.AddFrame(frame.Frames.RootFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MemoryStream ExportAndCleanup()
|
||||||
|
{
|
||||||
|
if (AnimatedImage.Frames.Count <= 1) return null;
|
||||||
|
|
||||||
|
var ms = new MemoryStream();
|
||||||
|
// Remove the blank placeholder frame
|
||||||
|
AnimatedImage.Frames.RemoveFrame(0);
|
||||||
|
|
||||||
|
AnimatedImage.Save(ms, new WebpEncoder { Quality = 80 });
|
||||||
|
ms.Position = 0;
|
||||||
|
|
||||||
|
// Free the animation memory now that it's encoded
|
||||||
|
ResetAnimation();
|
||||||
|
return ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetAnimation()
|
||||||
|
{
|
||||||
|
while (AnimatedImage.Frames.Count > 1)
|
||||||
|
AnimatedImage.Frames.RemoveFrame(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ExecuteGameLoop(int featureSpins = 0)
|
public void ExecuteGameLoop(int featureSpins = 0)
|
||||||
{
|
{
|
||||||
if (featureSpins is not 0) GotFeature = true;
|
|
||||||
GeneratePreBoard(featureSpins);
|
GeneratePreBoard(featureSpins);
|
||||||
|
|
||||||
var fCount = 0;
|
var fCount = 0;
|
||||||
for (var i = 0; i < 5; i++)
|
for (var i = 0; i < 5; i++) for (var j = 0; j < 5; j++) if (_preboard[i, j] == FEATURE) fCount++;
|
||||||
for (var j = 0; j < 5; j++)
|
|
||||||
if (_preboard[i, j] == FEATURE) fCount++;
|
|
||||||
|
|
||||||
if (featureSpins == 0)
|
if (featureSpins == 0) {
|
||||||
{
|
|
||||||
_showGoldCircle = false;
|
|
||||||
_activeFeatureTier = fCount >= 5 ? 5 : (fCount >= 3 ? fCount : 0);
|
_activeFeatureTier = fCount >= 5 ? 5 : (fCount >= 3 ? fCount : 0);
|
||||||
_currentFeatureSpin = 0;
|
_showGoldCircle = _activeFeatureTier >= 3; _currentFeatureSpin = 0;
|
||||||
}
|
} else {
|
||||||
else
|
_showGoldCircle = true; _currentFeatureSpin = featureSpins;
|
||||||
{
|
|
||||||
_showGoldCircle = true;
|
|
||||||
_currentFeatureSpin = featureSpins;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ConsoleDisplay();
|
ProcessReelsAndWins();
|
||||||
|
var total = _activeFeatureTier switch { 3 => 3, 4 => 5, 5 => 10, _ => 0 };
|
||||||
var totalSpins = _activeFeatureTier switch { 3 => 3, 4 => 5, 5 => 10, _ => 0 };
|
if (featureSpins == 0) for (var s = 1; s <= total; s++) ExecuteGameLoop(s);
|
||||||
if (featureSpins == 0)
|
|
||||||
for (var s = 1; s <= totalSpins; s++) ExecuteGameLoop(s);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ProcessReelsAndWins()
|
||||||
|
{
|
||||||
|
_board = (char[,])_preboard.Clone();
|
||||||
|
for (var o = 0; o <= 500; o += 50) RenderFrame(o);
|
||||||
|
List<char> multis = new(_multiTable.Keys);
|
||||||
|
for (var j = 0; j < 5; j++) {
|
||||||
|
for (var i = 0; i < 5; i++) {
|
||||||
|
if (_preboard[i, j] == EXPANDER) {
|
||||||
|
var hitWild = false;
|
||||||
|
for (var c = i; c < 5; c++) if (_preboard[c, j] == WILD) hitWild = true;
|
||||||
|
var mSym = hitWild ? multis[_rand.Next(multis.Count)] : EXPANDER;
|
||||||
|
for (var r = i; r < 5; r++) _board[r, j] = mSym;
|
||||||
|
RenderFrame(500); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var winners = GetWinningLinesCoordsWithPayouts();
|
||||||
|
var target = RunningTotalDisplay + winners.Sum(w => w.Amount);
|
||||||
|
foreach (var win in winners) {
|
||||||
|
var inc = win.Amount / 10.0;
|
||||||
|
for (var f = 0; f < 10; f++) { RunningTotalDisplay += inc; RenderFrame(500, [win]); }
|
||||||
|
}
|
||||||
|
RunningTotalDisplay = target; RenderFrame(500);
|
||||||
|
}
|
||||||
|
|
||||||
public void GeneratePreBoard(int feature = 0, char rigged = '0')
|
private List<WinDetail> GetWinningLinesCoordsWithPayouts()
|
||||||
{
|
{
|
||||||
var fCount = 0;
|
List<WinDetail> res = [];
|
||||||
var exCols = new HashSet<int>();
|
foreach (var line in _payoutLines) {
|
||||||
var riggedCounter = 0;
|
var ch = '0'; var count = 0; double m = 0; var spec = true;
|
||||||
var maxWinRiggedCounter = 0;
|
foreach (var (r, c) in line) {
|
||||||
for (var i = 0; i < 5; i++)
|
var cell = _board[r, c];
|
||||||
|
if (cell != WILD && cell != FEATURE && cell != EXPANDER && !ExpanderWild.Contains(cell)) { ch = cell; spec = false; break; }
|
||||||
|
}
|
||||||
|
if (!spec) {
|
||||||
|
foreach (var (r, c) in line) {
|
||||||
|
var cell = _board[r, c];
|
||||||
|
if (cell == ch || cell == WILD || cell == FEATURE || ExpanderWild.Contains(cell) || cell == EXPANDER) {
|
||||||
|
count++; if (ExpanderWild.Contains(cell)) m += _multiTable[cell];
|
||||||
|
} else if (count < 3) { count = 0; break; } else break;
|
||||||
|
}
|
||||||
|
} else { ch = _board[line[0].row, line[0].col]; count = 5; foreach (var (r, c) in line) if (ExpanderWild.Contains(_board[r, c])) m += _multiTable[_board[r, c]]; }
|
||||||
|
if (count >= 3) {
|
||||||
|
if (m == 0) m = 1;
|
||||||
|
if (_payoutTable.TryGetValue($"{ch}{count}", out var baseW)) {
|
||||||
|
var path = new (int, int)[count]; Array.Copy(line, path, count);
|
||||||
|
res.Add(new WinDetail { Path = path, Amount = (double)_userBet * baseW * m });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GeneratePreBoard(int f = 0, char rigged = '0')
|
||||||
{
|
{
|
||||||
for (var j = 0; j < 5; j++)
|
var fc = 0; HashSet<int> ex = [];
|
||||||
{
|
for (var i = 0; i < 5; i++) {
|
||||||
if (rigged == '0')
|
for (var j = 0; j < 5; j++) {
|
||||||
{
|
var r = _rand.NextDouble() * 100.6;
|
||||||
/*
|
if (f != 0 && j > 2) r *= 1.05;
|
||||||
* LOWEST - A, B
|
|
||||||
* LOW - C, D
|
|
||||||
* LOWMID - E
|
|
||||||
* MID - F, G
|
|
||||||
* HIGH - H, I
|
|
||||||
* HIGHEST - J
|
|
||||||
* WILD - K
|
|
||||||
* FEATURE - L
|
|
||||||
* EXPANDER - M
|
|
||||||
* EXPANDERWILD multis 2 - 10, 15, 20, 25, 50, 100, 200x - N O P Q R S T U V W X Y Z 1 2
|
|
||||||
*/
|
|
||||||
var r = _rand.NextDouble()*100.6;
|
|
||||||
if (feature!=0 && j > 2) r*=1.05;
|
|
||||||
if (r < 22) _preboard[i, j] = 'A';
|
if (r < 22) _preboard[i, j] = 'A';
|
||||||
else if (r < 44) _preboard[i, j] = 'B';
|
else if (r < 44) _preboard[i, j] = 'B';
|
||||||
else if (r < 52) _preboard[i, j] = 'C';
|
else if (r < 52) _preboard[i, j] = 'C';
|
||||||
@@ -244,306 +371,18 @@ public class SlotsCommand : ICommand
|
|||||||
else if (r < 95) _preboard[i, j] = 'I';
|
else if (r < 95) _preboard[i, j] = 'I';
|
||||||
else if (r < 97) _preboard[i, j] = 'J';
|
else if (r < 97) _preboard[i, j] = 'J';
|
||||||
else if (r < 98.5) _preboard[i, j] = WILD;
|
else if (r < 98.5) _preboard[i, j] = WILD;
|
||||||
else if (r < j switch {<=2 => 99, _ => 99.5}) { if (!exCols.Contains(j)) { _preboard[i, j] = EXPANDER; exCols.Add(j); } else _preboard[i, j] = WILD; }
|
else if (r < (j <= 2 ? 99 : 99.5)) { if (!ex.Contains(j)) { _preboard[i, j] = EXPANDER; ex.Add(j); } else _preboard[i, j] = WILD; }
|
||||||
else
|
else { if (fc < 5) { _preboard[i, j] = FEATURE; fc++; } else _preboard[i, j] = WILD; }
|
||||||
{
|
|
||||||
if (fCount < 5) { _preboard[i, j] = FEATURE; fCount++; }
|
|
||||||
else _preboard[i, j] = WILD;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
if (riggedCounter < 5 || (rigged != EXPANDER && rigged != FEATURE))
|
|
||||||
{
|
|
||||||
_preboard[i, j] = rigged;
|
|
||||||
riggedCounter++;
|
|
||||||
}
|
|
||||||
else if (rigged == EXPANDER && maxWinRiggedCounter < 5)
|
|
||||||
{
|
|
||||||
_preboard[i, j] = FEATURE;
|
|
||||||
maxWinRiggedCounter++;
|
|
||||||
}
|
|
||||||
else if (rigged == EXPANDER && maxWinRiggedCounter >= 5)
|
|
||||||
{
|
|
||||||
_preboard[i,j] = WILD;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_preboard[i, j] = 'A';
|
|
||||||
rigged = '0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RenderFrame(int dropOffset = 500, List<WinDetail>? activeWins = null)
|
|
||||||
{
|
|
||||||
var target = Raylib.LoadRenderTexture(500, 900);
|
|
||||||
Raylib.BeginTextureMode(target);
|
|
||||||
Raylib.ClearBackground(Raylib_cs.Color.Black);
|
|
||||||
|
|
||||||
Raylib.DrawTexture(_headerTexture, 0, 0, Raylib_cs.Color.White);
|
|
||||||
|
|
||||||
Raylib.BeginScissorMode(0, 200, 500, 500);
|
|
||||||
var occupied = new bool[5, 5];
|
|
||||||
for (var j = 0; j < 5; j++) {
|
|
||||||
for (var i = 0; i < 5; i++) {
|
|
||||||
if (occupied[i, j]) continue;
|
|
||||||
var sym = _board[i, j];
|
|
||||||
int x = j * 100, currentY = (200 + (i * 100)) - (500 - dropOffset);
|
|
||||||
if (sym == EXPANDER || _multiTable.ContainsKey(sym)) {
|
|
||||||
var h = 0;
|
|
||||||
for (var k = i; k < 5; k++) { if (_board[k, j] == sym) h++; else break; }
|
|
||||||
if (_expanderTextures.TryGetValue(h, out var texture)) {
|
|
||||||
Raylib.DrawTexture(texture, x, currentY, Raylib_cs.Color.White);
|
|
||||||
if (_multiTable.TryGetValue(sym, out var mVal)) {
|
|
||||||
var mText = $"x{mVal}";
|
|
||||||
const int fsM = 30; var twM = Raylib.MeasureText(mText, fsM);
|
|
||||||
Raylib.DrawText(mText, x + 50 - twM / 2 + 2, currentY + (h * 50) - fsM / 2 + 2, fsM, Raylib_cs.Color.Black);
|
|
||||||
Raylib.DrawText(mText, x + 50 - twM / 2, currentY + (h * 50) - fsM / 2, fsM, Raylib_cs.Color.Yellow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (var k = 0; k < h; k++) occupied[i + k, j] = true;
|
|
||||||
} else if (_symbolTextures.TryGetValue(sym, out var texture)) Raylib.DrawTexture(texture, x, currentY, Raylib_cs.Color.White);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeWins != null) {
|
|
||||||
foreach (var win in activeWins) {
|
|
||||||
for (var i = 0; i < win.Path.Length - 1; i++) {
|
|
||||||
var s = new Vector2(win.Path[i].col * 100 + 50, 200 + (win.Path[i].row * 100) + 50);
|
|
||||||
var e = new Vector2(win.Path[i+1].col * 100 + 50, 200 + (win.Path[i+1].row * 100) + 50);
|
|
||||||
Raylib.DrawLineEx(s, e, 8.0f, Raylib_cs.Color.White);
|
|
||||||
}
|
|
||||||
var amt = $"${win.Amount:F2}";
|
|
||||||
const int fsW = 25; var twW = Raylib.MeasureText(amt, fsW);
|
|
||||||
int tx = win.Path[win.Path.Length / 2].col * 100 + 50, ty = 200 + (win.Path[win.Path.Length / 2].row * 100) + 50;
|
|
||||||
Raylib.DrawRectangle(tx - twW / 2 - 5, ty - fsW / 2 - 2, twW + 10, fsW + 4, new Raylib_cs.Color(0, 0, 0, 200));
|
|
||||||
Raylib.DrawText(amt, tx - twW / 2, ty - fsW / 2, fsW, Raylib_cs.Color.Green);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Raylib.EndScissorMode();
|
|
||||||
|
|
||||||
// FOOTER
|
|
||||||
Raylib.DrawRectangle(0, 700, 500, 200, new Raylib_cs.Color(15, 15, 15, 255));
|
|
||||||
Raylib.DrawLineEx(new Vector2(0, 700), new Vector2(500, 700), 2, Raylib_cs.Color.Gold);
|
|
||||||
|
|
||||||
// Top Row UI - Compacted
|
|
||||||
Raylib.DrawText("BET", 20, 710, 12, Raylib_cs.Color.LightGray);
|
|
||||||
Raylib.DrawText($"${_userBet:F2}", 20, 725, 18, Raylib_cs.Color.White);
|
|
||||||
|
|
||||||
// SPIN COUNTER (Progress) - Center between Bet and Win
|
|
||||||
if (_currentFeatureSpin > 0)
|
|
||||||
{
|
|
||||||
var totalSpins = _activeFeatureTier switch { 3 => 3, 4 => 5, 5 => 10, _ => 0 };
|
|
||||||
var spinProgress = $"SPIN {_currentFeatureSpin}/{totalSpins}";
|
|
||||||
var spinFs = 20;
|
|
||||||
var spinTw = Raylib.MeasureText(spinProgress, spinFs);
|
|
||||||
Raylib.DrawText(spinProgress, 250 - (spinTw / 2), 715, spinFs, Raylib_cs.Color.SkyBlue);
|
|
||||||
}
|
|
||||||
|
|
||||||
var tallyStr = $"WIN: ${RunningTotalDisplay:F2}";
|
|
||||||
var tallySize = 26;
|
|
||||||
var tallyWidth = Raylib.MeasureText(tallyStr, tallySize);
|
|
||||||
Raylib.DrawText(tallyStr, 480 - tallyWidth, 712, tallySize, Raylib_cs.Color.Gold);
|
|
||||||
|
|
||||||
// Feature Symbol Area
|
|
||||||
var iconY = 760;
|
|
||||||
var textY = 865;
|
|
||||||
int[] xCoords = { 80, 250, 420 };
|
|
||||||
int[] tiers = { 3, 4, 5 };
|
|
||||||
|
|
||||||
for (var i = 0; i < 3; i++)
|
|
||||||
{
|
|
||||||
var tier = tiers[i];
|
|
||||||
var x = xCoords[i] - 50;
|
|
||||||
|
|
||||||
if (_showGoldCircle && _activeFeatureTier == tier)
|
|
||||||
{
|
|
||||||
Raylib.DrawCircle(x + 50, iconY + 50, 48, Raylib_cs.Color.Gold);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_symbolTextures.ContainsKey(FEATURE))
|
|
||||||
{
|
|
||||||
Raylib.DrawTexture(_symbolTextures[FEATURE], x, iconY, Raylib_cs.Color.White);
|
|
||||||
}
|
|
||||||
|
|
||||||
var tierLabel = $"x{tier}";
|
|
||||||
var fsL = 20;
|
|
||||||
var twL = Raylib.MeasureText(tierLabel, fsL);
|
|
||||||
Raylib.DrawText(tierLabel, x + 50 - twL / 2, textY, fsL, Raylib_cs.Color.White);
|
|
||||||
}
|
|
||||||
|
|
||||||
Raylib.EndTextureMode();
|
|
||||||
var finalImage = Raylib.LoadImageFromTexture(target.Texture);
|
|
||||||
Raylib.ImageFlipVertical(ref finalImage);
|
|
||||||
SlotFrames.Add(finalImage);
|
|
||||||
Raylib.UnloadRenderTexture(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ConsoleDisplay(bool riggedMaxWin = false)
|
|
||||||
{
|
|
||||||
// 1. Initial Setup and Drop Animation
|
|
||||||
_board = (char[,])_preboard.Clone();
|
|
||||||
for (var offset = 0; offset <= 500; offset += 50) RenderFrame(offset);
|
|
||||||
for (var offset = 500; offset <= 520; offset += 20) RenderFrame(offset);
|
|
||||||
for (var offset = 520; offset >= 500; offset -= 20) RenderFrame(offset);
|
|
||||||
|
|
||||||
// 2. Handle Expander Multipliers
|
|
||||||
var multis = new List<char>(_multiTable.Keys);
|
|
||||||
for (var j = 0; j < 5; j++) {
|
|
||||||
for (var i = 0; i < 5; i++)
|
|
||||||
{
|
|
||||||
if (_preboard[i, j] != EXPANDER) continue;
|
|
||||||
|
|
||||||
var hitWild = false;
|
|
||||||
for (var c = i; c < 5; c++) if (_preboard[c, j] == WILD) hitWild = true;
|
|
||||||
|
|
||||||
char mSym;
|
|
||||||
if (!riggedMaxWin)
|
|
||||||
{
|
|
||||||
mSym = hitWild ? multis[_rand.Next(multis.Count)] : EXPANDER;
|
|
||||||
}
|
|
||||||
else mSym = '2';
|
|
||||||
|
|
||||||
for (var row = i; row < 5; row++) {
|
|
||||||
_board[row, j] = mSym;
|
|
||||||
// Brief pause frames for expansion effect
|
|
||||||
for(var f = 0; f < 1; f++) RenderFrame();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Winning Line Calculations and Accurate Accumulation
|
|
||||||
var winners = GetWinningLinesCoordsWithPayouts();
|
|
||||||
|
|
||||||
// Calculate the final target to prevent rounding errors
|
|
||||||
var totalToWinThisSpin = winners.Sum(w => w.Amount);
|
|
||||||
var finalTarget = RunningTotalDisplay + totalToWinThisSpin;
|
|
||||||
|
|
||||||
// Iterate through each winning line
|
|
||||||
for (var i = 0; i < winners.Count; i++) {
|
|
||||||
var currentWin = winners[i];
|
|
||||||
var increment = currentWin.Amount / (decimal)10.0;
|
|
||||||
|
|
||||||
// Process 10 frames of animation per winning line
|
|
||||||
for (var f = 0; f < 10; f++) {
|
|
||||||
RunningTotalDisplay += increment;
|
|
||||||
|
|
||||||
// If there's a next win, we show both currently active and next for smoothness
|
|
||||||
if (i < winners.Count - 1 && f > 7) {
|
|
||||||
RenderFrame(500, [currentWin, winners[i + 1]]);
|
|
||||||
} else {
|
|
||||||
RenderFrame(500, [currentWin]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FINAL SNAP: Ensure floating point precision hasn't left us at 199.99 instead of 200.00
|
|
||||||
RunningTotalDisplay = finalTarget;
|
|
||||||
|
|
||||||
// 4. Feature Trigger Visualization
|
|
||||||
if (_activeFeatureTier >= 3)
|
|
||||||
{
|
|
||||||
_showGoldCircle = true;
|
|
||||||
// Hold on the final board state longer if a feature triggered
|
|
||||||
for (var f = 0; f < 10; f++) RenderFrame();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Short pause before the next spin/end of animation
|
|
||||||
for (var f = 0; f < 5; f++) RenderFrame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<WinDetail> GetWinningLinesCoordsWithPayouts()
|
|
||||||
{
|
|
||||||
var results = new List<WinDetail>();
|
|
||||||
foreach (var line in _payoutLines) {
|
|
||||||
var checker = '0';
|
|
||||||
var count = 0; double multi = 0; var special = true;
|
|
||||||
foreach (var (r, c) in line) {
|
|
||||||
var cell = _board[r, c];
|
|
||||||
if (cell == WILD || cell == FEATURE || cell == EXPANDER || ExpanderWild.Contains(cell)) continue;
|
|
||||||
checker = cell; special = false; break; //finds the first valid symbol in the payline
|
|
||||||
}
|
|
||||||
if (!special) {
|
|
||||||
foreach (var (r, c) in line) {
|
|
||||||
var ch = _board[r, c];
|
|
||||||
if (ch == checker || ch == WILD || ch == FEATURE || ExpanderWild.Contains(ch) || ch == EXPANDER) {
|
|
||||||
count++;
|
|
||||||
if (ExpanderWild.Contains(ch)) multi += _multiTable[ch];
|
|
||||||
} else if (count < 3) { count = 0; break; } else break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
checker = _board[line[0].row, line[0].col];
|
|
||||||
count = 5;
|
|
||||||
foreach (var (r, c) in line) if (ExpanderWild.Contains(_board[r, c])) multi += _multiTable[_board[r, c]];
|
|
||||||
}
|
|
||||||
if (count >= 3) {
|
|
||||||
if (multi == 0) multi = 1;
|
|
||||||
if (_payoutTable.TryGetValue($"{checker}{count}", out var baseWin)) {
|
|
||||||
var path = new (int, int)[count]; Array.Copy(line, path, count);
|
|
||||||
results.Add(new WinDetail { Path = path, Amount = _userBet * (decimal)baseWin * (decimal)multi });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe MemoryStream? GenerateAnimatedWebp(List<Raylib_cs.Image> frames)
|
|
||||||
{
|
|
||||||
if (frames.Count == 0) return null;
|
|
||||||
using var animated = new Image<Rgba32>(500, 900);
|
|
||||||
|
|
||||||
foreach (var rImg in frames) {
|
|
||||||
// FIX: Use Span to wrap unmanaged memory directly - NO ALLOCATION!
|
|
||||||
var span = new Span<byte>(rImg.Data, rImg.Width * rImg.Height * 4);
|
|
||||||
// LoadPixelData can work with Span directly
|
|
||||||
using var frame = SixLabors.ImageSharp.Image.LoadPixelData<Rgba32>(span, rImg.Width, rImg.Height);
|
|
||||||
frame.Frames.RootFrame.Metadata.GetWebpMetadata().FrameDelay = 2;
|
|
||||||
animated.Frames.AddFrame(frame.Frames.RootFrame);
|
|
||||||
Raylib.UnloadImage(rImg);
|
|
||||||
}
|
|
||||||
frames.Clear();
|
|
||||||
|
|
||||||
if (animated.Frames.Count > 1) animated.Frames.RemoveFrame(0);
|
|
||||||
|
|
||||||
var outputStream = new MemoryStream();
|
|
||||||
animated.Save(outputStream, new WebpEncoder { Quality = 80 });
|
|
||||||
outputStream.Position = 0;
|
|
||||||
return outputStream;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
foreach (var img in SlotFrames)
|
_headerImg?.Dispose();
|
||||||
Raylib.UnloadImage(img);
|
foreach (var img in _symbolImgs.Values) img.Dispose();
|
||||||
SlotFrames.Clear();
|
foreach (var img in _expanderImgs.Values) img.Dispose();
|
||||||
|
AnimatedImage?.Dispose();
|
||||||
foreach (var t in _expanderTextures.Values)
|
|
||||||
Raylib.UnloadTexture(t);
|
|
||||||
_expanderTextures.Clear();
|
|
||||||
|
|
||||||
foreach (var t in _symbolTextures.Values)
|
|
||||||
Raylib.UnloadTexture(t);
|
|
||||||
_symbolTextures.Clear();
|
|
||||||
|
|
||||||
if (_headerTexture.Id != 0)
|
|
||||||
{
|
|
||||||
Raylib.UnloadTexture(_headerTexture);
|
|
||||||
_headerTexture = default;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private class WinDetail
|
|
||||||
{
|
|
||||||
public required (int row, int col)[] Path { get; set; }
|
|
||||||
public decimal Amount { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,8 @@
|
|||||||
<PackageReference Include="NLog" Version="6.0.7" />
|
<PackageReference Include="NLog" Version="6.0.7" />
|
||||||
<PackageReference Include="Raffinert.FuzzySharp" Version="3.0.6" />
|
<PackageReference Include="Raffinert.FuzzySharp" Version="3.0.6" />
|
||||||
<PackageReference Include="RandN" Version="0.5.0" />
|
<PackageReference Include="RandN" Version="0.5.0" />
|
||||||
<PackageReference Include="Raylib-cs" Version="7.0.2" />
|
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.10.1" />
|
<PackageReference Include="StackExchange.Redis" Version="2.10.1" />
|
||||||
<PackageReference Include="System.Runtime.Caching" Version="10.0.1" />
|
<PackageReference Include="System.Runtime.Caching" Version="10.0.1" />
|
||||||
<PackageReference Include="Websocket.Client" Version="5.3.0" />
|
<PackageReference Include="Websocket.Client" Version="5.3.0" />
|
||||||
|
|||||||
Reference in New Issue
Block a user