from __future__ import annotations import asyncio import random from collections.abc import Awaitable, Callable, MutableSet import discord from discord import app_commands from core import economy import strings as S def register_economy_fish_commands( tree: app_commands.CommandTree, coin: Callable[[int], str], cd_ts: Callable, check_cmd_rate: Callable[[discord.Interaction], Awaitable[bool]], maybe_remind: Callable[[int, str], Awaitable[None]], award_exp: Callable[[discord.Interaction, int], Awaitable[None]], active_games: MutableSet[int], ) -> None: class FishCatchView(discord.ui.View): """Shown after a successful pull - lets user sell or keep the fish.""" def __init__(self, user_id: int, res: dict, fish_id: str, weight: int): super().__init__(timeout=60) self.user_id = user_id self._res = res self._fish_id = fish_id self._weight = weight self._done = False def _catch_embed(self, color: int = 0x57F287) -> discord.Embed: rarity = economy.FISH_CATALOGUE[self._fish_id]["rarity"] emoji = S.FISH_RARITY_EMOJI[rarity] fish_name = S.FISH_NAMES[self._fish_id] desc = S.FISH_UI["catch_desc"].format( name=fish_name, weight=self._weight, exp=self._res["exp"], value=coin(self._res["value"]), ) if self._res.get("is_new"): desc += S.FISH_UI["new_fish"] return discord.Embed(title=f"{emoji} {fish_name}!", description=desc, color=color) @discord.ui.button(label="", style=discord.ButtonStyle.success) async def sell_btn(self, interaction: discord.Interaction, button: discord.ui.Button): if interaction.user.id != self.user_id: await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True) return if self._done: await interaction.response.defer() return self._done = True self.stop() for child in self.children: child.disabled = True sell_res = await economy.do_fish_sell(self.user_id, [-1]) rarity = economy.FISH_CATALOGUE[self._fish_id]["rarity"] emoji = S.FISH_RARITY_EMOJI[rarity] fish_name = S.FISH_NAMES[self._fish_id] desc = S.FISH_UI["catch_sold"].format( name=fish_name, weight=self._weight, coins=coin(sell_res["coins"]), exp=self._res["exp"], balance=coin(sell_res["balance"]), ) if self._res.get("is_new"): desc += S.FISH_UI["new_fish"] embed = discord.Embed(title=f"{emoji} {fish_name}!", description=desc, color=0x57F287) await interaction.response.edit_message(embed=embed, view=self) @discord.ui.button(label="", style=discord.ButtonStyle.secondary) async def keep_btn(self, interaction: discord.Interaction, button: discord.ui.Button): if interaction.user.id != self.user_id: await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True) return if self._done: await interaction.response.defer() return self._done = True self.stop() for child in self.children: child.disabled = True rarity = economy.FISH_CATALOGUE[self._fish_id]["rarity"] emoji = S.FISH_RARITY_EMOJI[rarity] fish_name = S.FISH_NAMES[self._fish_id] desc = S.FISH_UI["catch_kept"].format( name=fish_name, weight=self._weight, exp=self._res["exp"], ) if self._res.get("is_new"): desc += S.FISH_UI["new_fish"] embed = discord.Embed(title=f"{emoji} {fish_name}!", description=desc, color=0x5865F2) await interaction.response.edit_message(embed=embed, view=self) async def on_timeout(self): for child in self.children: child.disabled = True class FishingView(discord.ui.View): BITE_WINDOW = 2.0 def __init__(self, user_id: int, fish_id: str, weight: int): super().__init__(timeout=40) self.user_id = user_id self._fish_id = fish_id self._weight = weight self._clicked = False self._bite_active = False self._msg: discord.Message | None = None self.pull_btn = discord.ui.Button( label=S.FISH_UI["btn_wait"], style=discord.ButtonStyle.secondary, disabled=True, ) self.pull_btn.callback = self._pull self.add_item(self.pull_btn) async def start(self, msg: discord.Message) -> None: self._msg = msg wait = random.uniform(5, 15) await asyncio.sleep(wait) if self._clicked or self.is_finished(): return self._bite_active = True self.pull_btn.disabled = False self.pull_btn.label = S.FISH_UI["btn_bite"] self.pull_btn.style = discord.ButtonStyle.success try: await msg.edit( embed=discord.Embed( title=S.TITLE["fish_bite"], description=S.FISH_UI["bite_desc"], color=0xED4245, ), view=self, ) except Exception: pass await asyncio.sleep(self.BITE_WINDOW) if not self._clicked: self.stop() active_games.discard(self.user_id) self.pull_btn.disabled = True try: await msg.edit( embed=discord.Embed( title=S.TITLE["fish_escape"], description=S.FISH_UI["escape_desc"], color=0x99AAB5, ), view=self, ) except Exception: pass async def _pull(self, interaction: discord.Interaction) -> None: if interaction.user.id != self.user_id: await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True) return if not self._bite_active: await interaction.response.send_message(S.FISH_UI["too_early"], ephemeral=True) return self._clicked = True self.stop() active_games.discard(self.user_id) self.pull_btn.disabled = True await interaction.response.defer() if self._fish_id == "junk": junk_text = random.choice(S.FISH_JUNK_LINES) user_data = await economy.get_user(interaction.user.id) embed = discord.Embed( title=S.TITLE["fish_junk"], description=S.FISH_UI["junk_desc"].format( text=junk_text, balance=coin(user_data.get("balance", 0)), ), color=0x99AAB5, ) await self._msg.edit(embed=embed, view=self) return res = await economy.do_fish_resolve(self.user_id, self._fish_id, self._weight) if not res["ok"]: await self._msg.edit( embed=discord.Embed(title=S.ERR["generic_error"], color=0xED4245), view=self, ) return catch_view = FishCatchView(self.user_id, res, self._fish_id, self._weight) catch_view.sell_btn.label = S.FISH_UI["btn_sell"] catch_view.keep_btn.label = S.FISH_UI["btn_keep"] await self._msg.edit(embed=catch_view._catch_embed(), view=catch_view) if res.get("exp", 0) > 0: asyncio.create_task(award_exp(interaction, res["exp"])) async def on_timeout(self): for child in self.children: child.disabled = True active_games.discard(self.user_id) @tree.command(name="fish", description=S.CMD["fish"]) @app_commands.guild_only() async def cmd_fish(interaction: discord.Interaction): if await check_cmd_rate(interaction): return if interaction.user.id in active_games: await interaction.response.send_message(S.ERR["game_in_progress"], ephemeral=True) return res = await economy.do_fish_start(interaction.user.id) if not res["ok"]: if res["reason"] == "banned": await interaction.response.send_message(S.MSG_BANNED, ephemeral=True) elif res["reason"] == "cooldown": await interaction.response.send_message( S.CD_MSG["fish"].format(ts=cd_ts(res["remaining"])), ephemeral=True, ) elif res["reason"] == "jailed": await interaction.response.send_message( S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])), ephemeral=True, ) return user_data = await economy.get_user(interaction.user.id) rarity_bump = "kalavork" in user_data.get("items", []) has_echolood = "echolood" in user_data.get("items", []) fish_id, weight = economy.roll_fish(rarity_bump=rarity_bump) active_games.add(interaction.user.id) view = FishingView(interaction.user.id, fish_id, weight) if has_echolood: view.BITE_WINDOW = 3.0 embed = discord.Embed( title=S.TITLE["fish_cast"], description=S.FISH_UI["cast_desc"], color=0x5865F2, ) await interaction.response.send_message(embed=embed, view=view) msg = await interaction.original_response() asyncio.create_task(view.start(msg)) asyncio.create_task(maybe_remind(interaction.user.id, "fish")) @tree.command(name="fishbook", description=S.CMD["fishbook"]) @app_commands.describe(kasutaja=S.OPT["fishbook_kasutaja"]) async def cmd_fishbook(interaction: discord.Interaction, kasutaja: discord.Member | None = None): target = kasutaja or interaction.user res = await economy.do_fishbook(target.id) book: dict = res["book"] total = res["total_species"] caught_count = res["unique_caught"] if not book: embed = discord.Embed( title=S.TITLE["fishbook"], description=S.FISH_UI["book_empty"], color=0x5865F2, ) await interaction.response.send_message(embed=embed, ephemeral=True) return inv_counts: dict = res.get("inv_counts", {}) all_fish = list(economy.FISH_CATALOGUE.items()) lines = [S.FISH_UI["book_caught"].format(caught=caught_count, total=total)] for fish_id, fish_data in all_fish: rarity = fish_data["rarity"] emoji = S.FISH_RARITY_EMOJI[rarity] rarity_name = S.FISH_RARITY_NAMES[rarity] count = book.get(fish_id, 0) if count > 0: n_inv = inv_counts.get(fish_id, 0) inv_str = S.FISH_UI["book_inv"].format(n=n_inv) if n_inv > 0 else "" lines.append( S.FISH_UI["book_yes"].format( emoji=emoji, name=S.FISH_NAMES[fish_id], rarity=rarity_name, count=count, inv=inv_str, ) ) else: lines.append(S.FISH_UI["book_no"].format(rarity=rarity_name)) embed = discord.Embed( title=S.TITLE["fishbook"].replace("Kalakogu", f"{target.display_name} kalakogu"), description="\n".join(lines), color=0x5865F2, ) embed.set_footer( text=S.FISH_UI["book_footer"].format( page=1, total_pages=1, caught=caught_count, total=total, ) ) await interaction.response.send_message(embed=embed, ephemeral=True) @tree.command(name="fishsell", description=S.CMD["fishsell"]) @app_commands.guild_only() async def cmd_fishsell(interaction: discord.Interaction): await interaction.response.defer(ephemeral=True) user_data = await economy.get_user(interaction.user.id) inv: list = user_data.get("fish_inventory") or [] if not inv: embed = discord.Embed( title=S.TITLE["fishbook"], description=S.FISH_UI["inv_empty"], color=0x5865F2, ) await interaction.followup.send(embed=embed, ephemeral=True) return total_value = sum(e["value"] for e in inv) lines = [S.FISH_UI["inv_header"].format(count=len(inv), total_value=coin(total_value))] for entry in inv: fid = entry.get("fish_id", "") rarity = economy.FISH_CATALOGUE.get(fid, {}).get("rarity", "common") emoji = S.FISH_RARITY_EMOJI.get(rarity, "🐟") name = S.FISH_NAMES.get(fid, fid) lines.append( S.FISH_UI["inv_entry"].format( emoji=emoji, name=name, weight=entry["weight"], value=coin(entry["value"]), ) ) embed = discord.Embed( title=S.TITLE["fishbook"], description="\n".join(lines), color=0x5865F2, ) sell_all_btn = discord.ui.Button( label=S.FISH_UI["btn_sell"] + f" ({coin(total_value)})", style=discord.ButtonStyle.success, ) async def _sell_all(btn_interaction: discord.Interaction): if btn_interaction.user.id != interaction.user.id: await btn_interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True) return sell_view.stop() for child in sell_view.children: child.disabled = True res = await economy.do_fish_sell(interaction.user.id) if not res["ok"]: await btn_interaction.response.edit_message( embed=discord.Embed(description=S.FISH_UI["inv_none"], color=0x99AAB5), view=sell_view, ) return sold_embed = discord.Embed( title=S.TITLE["fishbook"], description=S.FISH_UI["inv_sold_all"].format( count=res["count"], coins=coin(res["coins"]), balance=coin(res["balance"]), ), color=0x57F287, ) await btn_interaction.response.edit_message(embed=sold_embed, view=sell_view) sell_all_btn.callback = _sell_all sell_view = discord.ui.View(timeout=60) sell_view.add_item(sell_all_btn) await interaction.followup.send(embed=embed, view=sell_view, ephemeral=True)