""" Loaded Dice - Developer Testing System HIDDEN DEV COMMAND for testing dice-dependent gameplay mechanics. Use `!loaded <2d6_total> ` to set up a loaded roll for your next /ab. Example: !loaded 3 7 15 → Next /ab will roll 3 for d6, 7 for 2d6, 15 for d20 The loaded values are consumed after one use (one-shot). Only users with Administrator permission can use this command. """ import logging from dataclasses import dataclass from typing import Optional import discord from discord.ext import commands from utils.logging import get_contextual_logger from views.embeds import EmbedColors, EmbedTemplate logger = logging.getLogger(__name__) @dataclass class LoadedRoll: """Represents a pending loaded roll for a user.""" d6_1: int # First d6 (1-6) d6_2_total: int # 2d6 total (2-12) d20: int # d20 value (1-20) user_id: int # User who set this up # In-memory storage for pending loaded rolls # Maps user_id -> LoadedRoll _pending_loaded_rolls: dict[int, LoadedRoll] = {} def set_loaded_roll(user_id: int, d6_1: int, d6_2_total: int, d20: int) -> None: """ Set up a loaded roll for a user's next /ab command. Args: user_id: Discord user ID d6_1: Value for the first d6 (1-6) d6_2_total: Total for the 2d6 roll (2-12) d20: Value for the d20 roll (1-20) """ _pending_loaded_rolls[user_id] = LoadedRoll( d6_1=d6_1, d6_2_total=d6_2_total, d20=d20, user_id=user_id ) logger.info(f"Loaded roll set for user {user_id}: d6={d6_1}, 2d6={d6_2_total}, d20={d20}") def get_and_consume_loaded_roll(user_id: int) -> Optional[LoadedRoll]: """ Get and consume a pending loaded roll for a user. Returns the loaded roll if one exists, then removes it from storage. Returns None if no loaded roll is pending. Args: user_id: Discord user ID Returns: LoadedRoll if pending, None otherwise """ loaded = _pending_loaded_rolls.pop(user_id, None) if loaded: logger.info(f"Consumed loaded roll for user {user_id}: d6={loaded.d6_1}, 2d6={loaded.d6_2_total}, d20={loaded.d20}") return loaded def has_pending_loaded_roll(user_id: int) -> bool: """Check if a user has a pending loaded roll.""" return user_id in _pending_loaded_rolls def clear_loaded_roll(user_id: int) -> bool: """ Clear a pending loaded roll without consuming it. Returns True if a roll was cleared, False if none existed. """ if user_id in _pending_loaded_rolls: del _pending_loaded_rolls[user_id] logger.info(f"Cleared loaded roll for user {user_id}") return True return False class LoadedDiceCommands(commands.Cog): """ Developer commands for loaded dice testing. These are prefix commands (!) to avoid cluttering the slash command list. Only administrators can use these commands. """ def __init__(self, bot: commands.Bot): self.bot = bot self.logger = get_contextual_logger(f'{__name__}.LoadedDiceCommands') async def cog_check(self, ctx: commands.Context) -> bool: """Only allow administrators to use dev commands.""" if not ctx.guild: return False if not isinstance(ctx.author, discord.Member): return False if ctx.author.id != 258104532423147520: # Silently fail - don't reveal command exists to non-admins return False return True @commands.command(name="loaded", aliases=["load", "ld"]) async def set_loaded_dice( self, ctx: commands.Context, d6_1: int, d6_2_total: int, d20: int, target_user_id: int | None = None ): """ Set up a loaded roll for the next /ab command. Usage: !loaded <2d6_total> [user_id] Examples: !loaded 3 7 15 → Load for yourself !loaded 5 12 1 123456789 → Load for user ID 123456789 The loaded values are consumed after one /ab use. """ # Determine target user (default to command author) target_id = target_user_id or ctx.author.id self.logger.info( f"Loaded dice command from {ctx.author}: d6={d6_1}, 2d6={d6_2_total}, d20={d20}, target_id={target_id}" ) # Validate d6_1 (1-6) if d6_1 < 1 or d6_1 > 6: await ctx.send("❌ First d6 must be 1-6", delete_after=10) return # Validate 2d6 total (2-12) if d6_2_total < 2 or d6_2_total > 12: await ctx.send("❌ 2d6 total must be 2-12", delete_after=10) return # Validate d20 (1-20) if d20 < 1 or d20 > 20: await ctx.send("❌ d20 must be 1-20", delete_after=10) return # Store the loaded roll for target user set_loaded_roll(target_id, d6_1, d6_2_total, d20) # Create confirmation embed card_type = "Batter" if d6_1 <= 3 else "Pitcher" # Build description based on target if target_id == ctx.author.id: description = "Your next `/ab` roll will use these values:" else: description = f"Next `/ab` roll for <@{target_id}> will use these values:" embed = EmbedTemplate.create_base_embed( title="🎲 Loaded Dice Set", description=description, color=EmbedColors.WARNING ) embed.add_field( name="Loaded Values", value=f"```\nd6: {d6_1} ({card_type} card)\n2d6: {d6_2_total}\nd20: {d20}\n```", inline=False ) # Special indicators specials = [] if d20 == 1: specials.append("⚠️ Wild Pitch check") elif d20 == 2: specials.append("⚠️ Passed Ball check") if d6_1 == 6 and d6_2_total in [7, 8, 9, 10, 11, 12]: injury_rating = 13 - d6_2_total specials.append(f"🩹 Injury check (rating {injury_rating})") if specials: embed.add_field( name="Special Plays", value='\n'.join(specials), inline=False ) # Footer with target info if target_id == ctx.author.id: footer = "🔧 Dev Mode • Values consumed on next /ab • Use !unload to cancel" else: footer = f"🔧 Dev Mode • Target: {target_id} • Use !unload {target_id} to cancel" embed.set_footer(text=footer) await ctx.send(embed=embed, delete_after=30) # Delete the command message to keep it hidden try: await ctx.message.delete() except discord.Forbidden: pass # Can't delete in DMs or without permissions @commands.command(name="unload", aliases=["unld", "clearload"]) async def clear_loaded_dice(self, ctx: commands.Context, target_user_id: int | None = None): """Clear any pending loaded dice without using them.""" target_id = target_user_id or ctx.author.id self.logger.info(f"Unload command from {ctx.author}, target_id={target_id}") if clear_loaded_roll(target_id): if target_id == ctx.author.id: await ctx.send("✅ Loaded dice cleared", delete_after=10) else: await ctx.send(f"✅ Loaded dice cleared for <@{target_id}>", delete_after=10) else: await ctx.send("ℹ️ No loaded dice pending", delete_after=10) # Delete the command message try: await ctx.message.delete() except discord.Forbidden: pass @commands.command(name="checkload", aliases=["chkld"]) async def check_loaded_dice(self, ctx: commands.Context, target_user_id: int | None = None): """Check if loaded dice are pending for a user.""" target_id = target_user_id or ctx.author.id self.logger.info(f"Check load command from {ctx.author}, target_id={target_id}") if target_id in _pending_loaded_rolls: loaded = _pending_loaded_rolls[target_id] card_type = "Batter" if loaded.d6_1 <= 3 else "Pitcher" target_text = "" if target_id == ctx.author.id else f" for <@{target_id}>" await ctx.send( f"🎲 Loaded{target_text}: d6={loaded.d6_1} ({card_type}), 2d6={loaded.d6_2_total}, d20={loaded.d20}", delete_after=15 ) else: await ctx.send("ℹ️ No loaded dice pending", delete_after=10) # Delete the command message try: await ctx.message.delete() except discord.Forbidden: pass async def setup(bot: commands.Bot): """Load the loaded dice commands cog.""" await bot.add_cog(LoadedDiceCommands(bot)) # Export for use by dice commands __all__ = [ 'LoadedDiceCommands', 'LoadedRoll', 'set_loaded_roll', 'get_and_consume_loaded_roll', 'has_pending_loaded_roll', 'clear_loaded_roll', ]