From 8b4a529ab42b068c5e68be8cb118b367f3d12907 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:19:12 +0000 Subject: [PATCH 1/6] Initial plan From 64ae402deadd8f80ecfecd629f24a8354dd6f1ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:22:11 +0000 Subject: [PATCH 2/6] Add hat preview image to .hat list command Co-authored-by: psykzz <1134201+psykzz@users.noreply.github.com> --- hat/hat.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/hat/hat.py b/hat/hat.py index 55305b8..8c4dcdd 100644 --- a/hat/hat.py +++ b/hat/hat.py @@ -286,7 +286,7 @@ async def _hat(self, ctx): @_hat.command(name="list") async def _hat_list(self, ctx): - """List all available hats.""" + """List all available hats with a preview of the default hat.""" await self._delete_command_after_delay(ctx) hats = await self.config.hats() @@ -311,7 +311,19 @@ async def _hat_list(self, ctx): embed.add_field(name="Hats", value="\n".join(hat_list) or "None", inline=False) embed.set_footer(text="⭐ = Default hat") - msg = await ctx.send(embed=embed) + # Add a preview image of the default hat (or first available hat) + preview_hat = default_hat if default_hat else next(iter(hats.keys()), None) + file = None + if preview_hat: + hat_path = await self._get_hat_path(preview_hat) + if hat_path: + file = discord.File(hat_path, filename="hat_preview.png") + embed.set_image(url="attachment://hat_preview.png") + + if file: + msg = await ctx.send(embed=embed, file=file) + else: + msg = await ctx.send(embed=embed) self._create_cleanup_task(msg, CLEANUP_DELAY * 3) # Keep list longer @_hat.command(name="select") From ce44f2af7261fe4f3cad765443533670e74367f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:15:18 +0000 Subject: [PATCH 3/6] Show preview image for every hat in .hat list command Co-authored-by: psykzz <1134201+psykzz@users.noreply.github.com> --- hat/hat.py | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/hat/hat.py b/hat/hat.py index 8c4dcdd..7357fcc 100644 --- a/hat/hat.py +++ b/hat/hat.py @@ -286,7 +286,7 @@ async def _hat(self, ctx): @_hat.command(name="list") async def _hat_list(self, ctx): - """List all available hats with a preview of the default hat.""" + """List all available hats with preview images.""" await self._delete_command_after_delay(ctx) hats = await self.config.hats() @@ -297,34 +297,30 @@ async def _hat_list(self, ctx): self._create_cleanup_task(msg, CLEANUP_DELAY) return - embed = discord.Embed( - title="🎅 Available Hats", - description="Use `.hat select ` to choose a hat!", - color=discord.Color.red(), - ) + # Send a preview for each hat + hat_names = list(hats.keys()) + for idx, name in enumerate(hat_names): + is_default = " ⭐" if name == default_hat else "" + is_last = idx == len(hat_names) - 1 - hat_list = [] - for name, data in hats.items(): - is_default = "⭐ " if name == default_hat else "" - hat_list.append(f"{is_default}**{name}**") + embed = discord.Embed( + title=f"🎅 {name}{is_default}", + description="Use `.hat select ` to choose a hat!" if idx == 0 else None, + color=discord.Color.red(), + ) - embed.add_field(name="Hats", value="\n".join(hat_list) or "None", inline=False) - embed.set_footer(text="⭐ = Default hat") + if is_last: + embed.set_footer(text="⭐ = Default hat") - # Add a preview image of the default hat (or first available hat) - preview_hat = default_hat if default_hat else next(iter(hats.keys()), None) - file = None - if preview_hat: - hat_path = await self._get_hat_path(preview_hat) + hat_path = await self._get_hat_path(name) if hat_path: file = discord.File(hat_path, filename="hat_preview.png") embed.set_image(url="attachment://hat_preview.png") + msg = await ctx.send(embed=embed, file=file) + else: + msg = await ctx.send(embed=embed) - if file: - msg = await ctx.send(embed=embed, file=file) - else: - msg = await ctx.send(embed=embed) - self._create_cleanup_task(msg, CLEANUP_DELAY * 3) # Keep list longer + self._create_cleanup_task(msg, CLEANUP_DELAY * 3) # Keep list longer @_hat.command(name="select") async def _hat_select(self, ctx, hat_name: str): From bea4f16ffbf44bca39accc20d4bb74794a77a4b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 12:22:30 +0000 Subject: [PATCH 4/6] Add flip command and allow negative position values for hat cog Co-authored-by: psykzz <1134201+psykzz@users.noreply.github.com> --- hat/hat.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/hat/hat.py b/hat/hat.py index 7357fcc..01ed665 100644 --- a/hat/hat.py +++ b/hat/hat.py @@ -17,6 +17,8 @@ DEFAULT_ROTATION = 0 DEFAULT_X_OFFSET = 0.5 # Center horizontally (0.0 = left, 1.0 = right) DEFAULT_Y_OFFSET = 0.0 # Top of image (0.0 = top, 1.0 = bottom) +DEFAULT_FLIP_X = False +DEFAULT_FLIP_Y = False # Limits MIN_SCALE = 0.1 @@ -45,6 +47,8 @@ def __init__(self, bot): "rotation": DEFAULT_ROTATION, "x_offset": DEFAULT_X_OFFSET, "y_offset": DEFAULT_Y_OFFSET, + "flip_x": DEFAULT_FLIP_X, + "flip_y": DEFAULT_FLIP_Y, } self.config.register_global(**default_global) self.config.register_user(**default_user) @@ -146,6 +150,8 @@ async def _apply_hat_to_avatar( rotation: float, x_offset: float, y_offset: float, + flip_x: bool = False, + flip_y: bool = False, ) -> bytes: """Apply a hat overlay to an avatar image.""" @@ -161,6 +167,12 @@ def process_image(): if hat.width == 0 or hat.height == 0: raise ValueError("Invalid hat image dimensions") + # Flip the hat if requested + if flip_x: + hat = hat.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + if flip_y: + hat = hat.transpose(Image.Transpose.FLIP_TOP_BOTTOM) + # Scale the hat relative to avatar width hat_width = int(avatar_width * scale) hat_height = int(hat.height * (hat_width / hat.width)) @@ -173,6 +185,7 @@ def process_image(): # Calculate position # x_offset: 0.0 = left edge, 0.5 = center, 1.0 = right edge # y_offset: 0.0 = top edge, 0.5 = center, 1.0 = bottom edge + # Negative values or values > 1.0 will position the hat partially off-screen x = int((avatar_width - hat.width) * x_offset) y = int((avatar_height - hat.height) * y_offset) @@ -250,6 +263,8 @@ async def _send_live_preview(self, ctx, error_msg: Optional[str] = None): user_data["rotation"], user_data["x_offset"], user_data["y_offset"], + user_data.get("flip_x", DEFAULT_FLIP_X), + user_data.get("flip_y", DEFAULT_FLIP_Y), ) except Exception as e: log.exception("Error applying hat to avatar") @@ -258,6 +273,13 @@ async def _send_live_preview(self, ctx, error_msg: Optional[str] = None): return # Create embed with preview + flip_status = [] + if user_data.get("flip_x", DEFAULT_FLIP_X): + flip_status.append("X") + if user_data.get("flip_y", DEFAULT_FLIP_Y): + flip_status.append("Y") + flip_text = ", ".join(flip_status) if flip_status else "None" + embed = discord.Embed( title="🎅 Hat Preview", description="Right-click the image to save it!", @@ -267,7 +289,8 @@ async def _send_live_preview(self, ctx, error_msg: Optional[str] = None): embed.add_field(name="Scale", value=f"{user_data['scale']}", inline=True) embed.add_field(name="Rotation", value=f"{user_data['rotation']}°", inline=True) embed.add_field(name="Position", value=f"({user_data['x_offset']}, {user_data['y_offset']})", inline=True) - embed.set_footer(text="Adjust: .hat scale, .hat rotate, .hat position | Refresh: .hat show") + embed.add_field(name="Flip", value=flip_text, inline=True) + embed.set_footer(text="Adjust: .hat scale, .hat rotate, .hat position, .hat flip | Refresh: .hat show") file = discord.File(io.BytesIO(result), filename="hat_preview.png") embed.set_image(url="attachment://hat_preview.png") @@ -385,18 +408,45 @@ async def _hat_position(self, ctx, x: float, y: float): x: 0.0 = left, 0.5 = center, 1.0 = right y: 0.0 = top, 0.5 = center, 1.0 = bottom + Negative values or values > 1.0 will position the hat partially off-screen. + Example: `.hat position 0.5 0.1` + Example: `.hat position -0.2 0.0` (hat partially off left side) """ - if x < 0.0 or x > 1.0 or y < 0.0 or y > 1.0: - await self._send_live_preview(ctx, "❌ Position values must be between 0.0 and 1.0.") - return - await self.config.user(ctx.author).x_offset.set(x) await self.config.user(ctx.author).y_offset.set(y) # Show live preview await self._send_live_preview(ctx) + @_hat.command(name="flip") + async def _hat_flip(self, ctx, axis: str): + """Flip the hat on the X or Y axis and see a live preview. + + axis: 'x' for horizontal flip, 'y' for vertical flip, 'none' to reset + + Example: `.hat flip x` (flip horizontally) + Example: `.hat flip y` (flip vertically) + Example: `.hat flip none` (reset flips) + """ + axis_lower = axis.lower() + if axis_lower not in ("x", "y", "none"): + await self._send_live_preview(ctx, "❌ Axis must be 'x', 'y', or 'none'.") + return + + if axis_lower == "x": + current = await self.config.user(ctx.author).flip_x() + await self.config.user(ctx.author).flip_x.set(not current) + elif axis_lower == "y": + current = await self.config.user(ctx.author).flip_y() + await self.config.user(ctx.author).flip_y.set(not current) + else: # none + await self.config.user(ctx.author).flip_x.set(DEFAULT_FLIP_X) + await self.config.user(ctx.author).flip_y.set(DEFAULT_FLIP_Y) + + # Show live preview + await self._send_live_preview(ctx) + @_hat.command(name="reset") async def _hat_reset(self, ctx): """Reset hat settings to defaults and see a live preview.""" @@ -404,6 +454,8 @@ async def _hat_reset(self, ctx): await self.config.user(ctx.author).rotation.set(DEFAULT_ROTATION) await self.config.user(ctx.author).x_offset.set(DEFAULT_X_OFFSET) await self.config.user(ctx.author).y_offset.set(DEFAULT_Y_OFFSET) + await self.config.user(ctx.author).flip_x.set(DEFAULT_FLIP_X) + await self.config.user(ctx.author).flip_y.set(DEFAULT_FLIP_Y) # Show live preview await self._send_live_preview(ctx) From 5fed240b183baa78201ce571fdc9bea17b615578 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 5 Dec 2025 12:22:45 +0000 Subject: [PATCH 5/6] Update hat/hat.py --- hat/hat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/hat/hat.py b/hat/hat.py index 01ed665..211f708 100644 --- a/hat/hat.py +++ b/hat/hat.py @@ -343,7 +343,6 @@ async def _hat_list(self, ctx): else: msg = await ctx.send(embed=embed) - self._create_cleanup_task(msg, CLEANUP_DELAY * 3) # Keep list longer @_hat.command(name="select") async def _hat_select(self, ctx, hat_name: str): From fd20196ea5fab13f9fcb2488aa8c11f0c52c2706 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 5 Dec 2025 12:24:19 +0000 Subject: [PATCH 6/6] Remove unnecessary blank line in hat.py --- hat/hat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/hat/hat.py b/hat/hat.py index 211f708..d151c7d 100644 --- a/hat/hat.py +++ b/hat/hat.py @@ -343,7 +343,6 @@ async def _hat_list(self, ctx): else: msg = await ctx.send(embed=embed) - @_hat.command(name="select") async def _hat_select(self, ctx, hat_name: str): """Select a hat and see a live preview.