Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions DiscordBot/Domain/Casino/CasinoUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public enum TransactionType
Gift,
Game,
Admin,
Shop,
}

public class GameStatistics
Expand Down Expand Up @@ -72,10 +73,29 @@ public class GameLeaderboardEntry
public string? GameName { get; set; } // null for global leaderboard
}

public class ShopItem
{
public int Id { get; set; }
public required string Title { get; set; }
public required string Description { get; set; }
public ulong Price { get; set; }
public DateTime CreatedAt { get; set; }
}

public class ShopPurchase
{
public int Id { get; set; }
public required string UserID { get; set; }
public int ItemId { get; set; }
public DateTime PurchaseDate { get; set; }
}

public static class CasinoProps
{
public const string CasinoTableName = "casino_users";
public const string TransactionTableName = "token_transactions";
public const string ShopItemsTableName = "shop_items";
public const string ShopPurchasesTableName = "shop_purchases";

// CasinoUser properties
public const string Id = nameof(CasinoUser.Id);
Expand All @@ -92,4 +112,17 @@ public static class CasinoProps
public const string TransactionType = nameof(TokenTransaction.Type);
public const string Details = nameof(TokenTransaction.DetailsJson);
public const string TransactionCreatedAt = nameof(TokenTransaction.CreatedAt);

// ShopItem properties
public const string ShopItemId = nameof(ShopItem.Id);
public const string ShopItemTitle = nameof(ShopItem.Title);
public const string ShopItemDescription = nameof(ShopItem.Description);
public const string ShopItemPrice = nameof(ShopItem.Price);
public const string ShopItemCreatedAt = nameof(ShopItem.CreatedAt);

// ShopPurchase properties
public const string ShopPurchaseId = nameof(ShopPurchase.Id);
public const string ShopPurchaseUserID = nameof(ShopPurchase.UserID);
public const string ShopPurchaseItemId = nameof(ShopPurchase.ItemId);
public const string ShopPurchaseDate = nameof(ShopPurchase.PurchaseDate);
}
32 changes: 32 additions & 0 deletions DiscordBot/Extensions/CasinoRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,36 @@ public interface ICasinoRepo
// Test connection
[Sql($"SELECT COUNT(*) FROM {CasinoProps.CasinoTableName}")]
Task<long> TestCasinoConnection();

// Shop Item Operations
[Sql($@"
INSERT INTO {CasinoProps.ShopItemsTableName} ({CasinoProps.ShopItemTitle}, {CasinoProps.ShopItemDescription}, {CasinoProps.ShopItemPrice}, {CasinoProps.ShopItemCreatedAt})
VALUES (@{CasinoProps.ShopItemTitle}, @{CasinoProps.ShopItemDescription}, @{CasinoProps.ShopItemPrice}, @{CasinoProps.ShopItemCreatedAt});
SELECT * FROM {CasinoProps.ShopItemsTableName} WHERE {CasinoProps.ShopItemId} = LAST_INSERT_ID()")]
Task<ShopItem> InsertShopItem(ShopItem item);

[Sql($"SELECT * FROM {CasinoProps.ShopItemsTableName} ORDER BY {CasinoProps.ShopItemPrice} ASC")]
Task<IList<ShopItem>> GetAllShopItems();

[Sql($"SELECT * FROM {CasinoProps.ShopItemsTableName} WHERE {CasinoProps.ShopItemId} = @itemId")]
Task<ShopItem> GetShopItem(int itemId);

[Sql($"DELETE FROM {CasinoProps.ShopItemsTableName}")]
Task ClearAllShopItems();

// Shop Purchase Operations
[Sql($@"
INSERT INTO {CasinoProps.ShopPurchasesTableName} ({CasinoProps.ShopPurchaseUserID}, {CasinoProps.ShopPurchaseItemId}, {CasinoProps.ShopPurchaseDate})
VALUES (@{CasinoProps.ShopPurchaseUserID}, @{CasinoProps.ShopPurchaseItemId}, @{CasinoProps.ShopPurchaseDate});
SELECT * FROM {CasinoProps.ShopPurchasesTableName} WHERE {CasinoProps.ShopPurchaseId} = LAST_INSERT_ID()")]
Task<ShopPurchase> InsertShopPurchase(ShopPurchase purchase);

[Sql($"SELECT * FROM {CasinoProps.ShopPurchasesTableName} WHERE {CasinoProps.ShopPurchaseUserID} = @userId")]
Task<IList<ShopPurchase>> GetUserShopPurchases(string userId);

[Sql($"SELECT COUNT(*) FROM {CasinoProps.ShopPurchasesTableName} WHERE {CasinoProps.ShopPurchaseUserID} = @userId AND {CasinoProps.ShopPurchaseItemId} = @itemId")]
Task<long> CheckUserHasItem(string userId, int itemId);

[Sql($"DELETE FROM {CasinoProps.ShopPurchasesTableName}")]
Task ClearAllShopPurchases();
}
185 changes: 185 additions & 0 deletions DiscordBot/Modules/Casino/CasinoSlashModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ public async Task NavigateHistory(string userId, string pageStr, string requestT
TransactionType.Gift => GetGiftDisplay(transaction),
TransactionType.Game => GetGameDisplay(transaction),
TransactionType.Admin => GetAdminDisplay(transaction),
TransactionType.Shop => GetShopDisplay(transaction),
_ => ("❓", transaction.Type.ToString(), "")
};

Expand Down Expand Up @@ -416,6 +417,16 @@ public async Task NavigateHistory(string userId, string pageStr, string requestT
return ("⚙️", title, description);
}

private (string emoji, string title, string description) GetShopDisplay(TokenTransaction transaction)
{
var itemTitle = transaction.Details?.GetValueOrDefault("item_title");

string title = "Shop Purchase";
if (itemTitle != null) title = $"Bought {itemTitle}";

return ("🛒", title, "");
}

private string CapitalizeFirst(string input)
{
if (string.IsNullOrEmpty(input))
Expand Down Expand Up @@ -541,6 +552,180 @@ public async Task Daily()

#endregion

#region Shop Command

[SlashCommand("shop", "View and purchase items from the casino shop")]
public async Task Shop()
{
if (!await CheckChannelPermissions()) return;

try
{
await Context.Interaction.DeferAsync(ephemeral: true);

// Ensure shop items are initialized
await CasinoService.InitializeDefaultShopItems();

var user = await CasinoService.GetOrCreateCasinoUser(Context.User.Id.ToString());
var allItems = await CasinoService.GetAllShopItems();
var userPurchases = await CasinoService.GetUserShopPurchases(Context.User.Id.ToString());
var purchasedItemIds = userPurchases.Select(p => p.ItemId).ToHashSet();

if (allItems.Count == 0)
{
await Context.Interaction.FollowupAsync("🛒 The shop is currently empty. Please check back later!", ephemeral: true);
return;
}

var embed = new EmbedBuilder()
.WithTitle("🛒 Casino Shop")
.WithDescription($"**Your Balance:** {user.Tokens:N0} tokens\n\nSelect an item from the dropdown below to purchase it.")
.WithColor(Color.Purple);

// Add shop items to embed (strike through purchased items)
foreach (var item in allItems)
{
var isPurchased = purchasedItemIds.Contains(item.Id);
var titleDisplay = isPurchased ? $"~~{item.Title}~~" : item.Title;
var statusText = isPurchased ? " ✅ **OWNED**" : $" - **{item.Price:N0} tokens**";

embed.AddField($"{titleDisplay}{statusText}", item.Description, false);
}

// Create dropdown with available items (not purchased)
var availableItems = allItems.Where(item => !purchasedItemIds.Contains(item.Id)).ToList();

if (availableItems.Count == 0)
{
embed.WithFooter("🎉 You own all available shop items!");
await Context.Interaction.FollowupAsync(embed: embed.Build(), ephemeral: true);
return;
}

var selectMenu = new SelectMenuBuilder()
.WithPlaceholder("Select an item to purchase...")
.WithCustomId($"shop_purchase:{Context.User.Id}")
.WithMinValues(1)
.WithMaxValues(1);

foreach (var item in availableItems.Take(25)) // Discord limit of 25 options
{
var canAfford = user.Tokens >= item.Price;
var label = $"{item.Title} - {item.Price:N0} tokens";
if (!canAfford) label += " (Can't afford)";

selectMenu.AddOption(
label: label,
value: item.Id.ToString(),
description: item.Description.Length > 100 ? item.Description.Substring(0, 97) + "..." : item.Description,
isDefault: false
);
}

var components = new ComponentBuilder()
.WithSelectMenu(selectMenu)
.Build();

await Context.Interaction.FollowupAsync(embed: embed.Build(), components: components, ephemeral: true);
}
catch (Exception ex)
{
await LoggingService.LogChannelAndFile($"Casino: ERROR in Shop command for user {Context.User.Username} (ID: {Context.User.Id}): {ex.Message}", ExtendedLogSeverity.Error);
await LoggingService.LogChannelAndFile($"Casino: Shop Exception Details: {ex}");

try
{
if (!Context.Interaction.HasResponded)
{
await Context.Interaction.RespondAsync("❌ An error occurred while loading the shop. Please try again.", ephemeral: true);
}
else
{
await Context.Interaction.FollowupAsync("❌ An error occurred while loading the shop. Please try again.", ephemeral: true);
}
}
catch
{
await LoggingService.LogChannelAndFile($"Casino: Failed to send error response to user {Context.User.Username} in Shop command");
}
}
}

[ComponentInteraction("shop_purchase:*", true)]
public async Task HandleShopPurchase(string userId, string[] selectedValues)
{
try
{
await Context.Interaction.DeferAsync(ephemeral: true);

if (Context.User.Id.ToString() != userId)
{
await Context.Interaction.FollowupAsync("🚫 You are not authorized to make this purchase.", ephemeral: true);
return;
}

if (selectedValues == null || selectedValues.Length == 0)
{
await Context.Interaction.FollowupAsync("❌ No item selected.", ephemeral: true);
return;
}

if (!int.TryParse(selectedValues[0], out int itemId))
{
await Context.Interaction.FollowupAsync("❌ Invalid item selected.", ephemeral: true);
return;
}

var (success, message) = await CasinoService.PurchaseShopItem(Context.User.Id.ToString(), itemId);

if (success)
{
var embed = new EmbedBuilder()
.WithTitle("🎉 Purchase Successful!")
.WithDescription(message)
.WithColor(Color.Green)
.WithCurrentTimestamp()
.Build();

await Context.Interaction.FollowupAsync(embed: embed, ephemeral: true);
await LoggingService.LogChannelAndFile($"Casino: {Context.User.Username} successfully purchased shop item {itemId}");
}
else
{
var embed = new EmbedBuilder()
.WithTitle("❌ Purchase Failed")
.WithDescription(message)
.WithColor(Color.Red)
.Build();

await Context.Interaction.FollowupAsync(embed: embed, ephemeral: true);
}
}
catch (Exception ex)
{
await LoggingService.LogChannelAndFile($"Casino: ERROR in HandleShopPurchase for user {Context.User.Username} (ID: {Context.User.Id}): {ex.Message}", ExtendedLogSeverity.Error);
await LoggingService.LogChannelAndFile($"Casino: HandleShopPurchase Exception Details: {ex}");

try
{
if (!Context.Interaction.HasResponded)
{
await Context.Interaction.RespondAsync("❌ An error occurred while processing your purchase. Please try again.", ephemeral: true);
}
else
{
await Context.Interaction.FollowupAsync("❌ An error occurred while processing your purchase. Please try again.", ephemeral: true);
}
}
catch
{
await LoggingService.LogChannelAndFile($"Casino: Failed to send error response to user {Context.User.Username} in HandleShopPurchase");
}
}
}

#endregion

#region Admin Reset Command

[SlashCommand("reset", "Reset all casino data (Admin only) - REQUIRES CONFIRMATION")]
Expand Down
Loading
Loading