import discord import asyncio import aiohttp from discord.ext import commands import re # ============================== # CONFIGURATION # ============================== TORN_API_KEY = "9VLK0Wte1BwXOheB" ENEMY_FACTION_ID = 52935 YOUR_FACTION_ID = 654321 ALLOWED_CHANNEL_ID = 1442876328536707316 FFSCOUTER_KEY = "XYmWPO9ZYkLqnv3v" POLL_INTERVAL = 30 HIT_CHECK_INTERVAL = 60 REASSIGN_DELAY = 120 intents = discord.Intents.default() intents.message_content = True # ============================== # STATE STORAGE # ============================== enrolled_attackers = [] enemy_queue = [] active_assignments = {} round_robin_index = 0 # ============================== # BOT SUBCLASS # ============================== class HitDispatchBot(commands.Bot): async def setup_hook(self): # Start background loops self.bg_tasks = [] self.bg_tasks.append(asyncio.create_task(update_enemy_queue_loop())) self.bg_tasks.append(asyncio.create_task(monitor_assignments_loop())) async def cog_check(self, ctx): return ctx.channel.id == ALLOWED_CHANNEL_ID # Create bot now (ONE bot only) bot = HitDispatchBot(command_prefix="!", intents=intents) async def fetch_ffscouter_stats(session: aiohttp.ClientSession, torn_id: int): """ Calls FFScouter and returns predicted battle stats. Uses an existing aiohttp session (caller must provide). """ url = f"https://ffscouter.com/api/v1/get-stats?key={FFSCOUTER_KEY}&targets={torn_id}" #print(url) async with session.get(url) as resp: if resp.status != 200: print(f"FFScouter Error for {torn_id}:", resp.status) return None data = await resp.json() # FFScouter v1 returns: {"code":200,"message":"OK","data": {"": {...}}} if "data" not in data: return None inner = data["data"] return inner.get(str(torn_id)) async def fetch_enemy_faction(): """ Pulls faction members from Torn (selections=members), then fetches FFScouter stats in a single batch request. Returns a list of enemies with name, id, level, and estimated BS. """ url = ( f"https://api.torn.com/v2/faction/{ENEMY_FACTION_ID}" f"?selections=members&key={TORN_API_KEY}" ) enemies = [] async with aiohttp.ClientSession() as session: # --- Fetch faction members --- async with session.get(url) as resp: if resp.status != 200: print("Torn faction fetch error:", resp.status) return enemies data = await resp.json() members_list = data.get("members", []) if not members_list: return enemies # --- Build comma-separated list of IDs --- member_ids = [ str(info.get("player_id", info.get("id", 0))) for info in members_list if info.get("status", {}).get("state", "Unknown") in ("Okay", "Idle") ] if not member_ids: return enemies ids_str = ",".join(member_ids) # --- Single FFScouter batch request --- ff_url = f"https://ffscouter.com/api/v1/get-stats?key={FFSCOUTER_KEY}&targets={ids_str}" async with session.get(ff_url) as resp: if resp.status != 200: print("FFScouter batch error:", resp.status) ff_data_list = [] else: ff_data_list = await resp.json() # --- Map FFScouter data by player_id for quick lookup --- ff_map = {str(d["player_id"]): d for d in ff_data_list} # --- Build final enemies list --- for info in members_list: pid = str(info.get("player_id", info.get("id", 0))) state = info.get("status", {}).get("state", "Unknown") if state not in ("Okay", "Idle"): continue name = info.get("name", "Unknown") level = int(info.get("level", 0)) est = ff_map.get(pid, {}).get("bs_estimate_human", "?") enemies.append({ "id": int(pid), "name": name, "level": level, "estimate": est }) return enemies async def update_enemy_queue_loop(): global enemy_queue await bot.wait_until_ready() while not bot.is_closed(): print("Refreshing enemy list...") enemy_queue = await fetch_enemy_faction() print(f"Enemy queue updated: {len(enemy_queue)} valid targets.") await asyncio.sleep(POLL_INTERVAL) # ============================== # ASSIGNMENT SYSTEM # ============================== def get_next_attacker(): global round_robin_index if not enrolled_attackers: return None attacker = enrolled_attackers[round_robin_index] round_robin_index = (round_robin_index + 1) % len(enrolled_attackers) return attacker async def assign_next_target(): if not enemy_queue: return None, None # Pop the weakest enemy (already sorted by score_value) enemy = enemy_queue.pop(0) attacker = get_next_attacker() if attacker is None: return None, None active_assignments[enemy["id"]] = { "enemy": enemy, "attacker": attacker, "time_assigned": asyncio.get_event_loop().time() } return enemy, attacker async def monitor_assignments_loop(): await bot.wait_until_ready() while not bot.is_closed(): now = asyncio.get_event_loop().time() reassign_list = [] for enemy_id, data in list(active_assignments.items()): elapsed = now - data["time_assigned"] if elapsed >= HIT_CHECK_INTERVAL and elapsed < HIT_CHECK_INTERVAL + 5: attacker_user = bot.get_user(data["attacker"]) if attacker_user: try: await attacker_user.send( f"Reminder: You were assigned **{data['enemy']['name']}** and they are not down yet!" ) except: pass if elapsed >= REASSIGN_DELAY: reassign_list.append(enemy_id) for enemy_id in reassign_list: info = active_assignments.pop(enemy_id) await reassign_target(info["enemy"]) await asyncio.sleep(5) async def reassign_target(enemy): attacker = get_next_attacker() if attacker is None: return active_assignments[enemy["id"]] = { "enemy": enemy, "attacker": attacker, "time_assigned": asyncio.get_event_loop().time() } attacker_user = bot.get_user(attacker) if attacker_user: try: await attacker_user.send( f"Target reassigned to you: **{enemy['name']}**!" ) except: pass # ============================== # COMMANDS # ============================== @bot.command() async def enroll(ctx): user_id = ctx.author.id if user_id in enrolled_attackers: await ctx.send("You are already enrolled.") return enrolled_attackers.append(user_id) await ctx.send(f"{ctx.author.mention} has been enrolled in the hit rotation!") @bot.command() async def drop(ctx): user_id = ctx.author.id if user_id not in enrolled_attackers: await ctx.send("You are not enrolled.") return enrolled_attackers.remove(user_id) await ctx.send(f"{ctx.author.mention} has been removed from the rotation.") @bot.command() async def next(ctx): enemy, attacker = await assign_next_target() if enemy is None: await ctx.send("No targets available or no attackers enrolled.") return attacker_user = bot.get_user(attacker) await ctx.send(f"Assigned **{enemy['name']}** → <@{attacker}>") if attacker_user: try: await attacker_user.send( f"Hit assignment: **{enemy['name']}**" ) except: pass @bot.command() async def stats(ctx): enemies = await fetch_enemy_faction() if not enemies: await ctx.send("No active members found.") return lines = [ f"**{e['name']}** (ID:{e['id']}) | Lv {e['level']} | Estimated BS: {e['estimate']}" for e in enemies ] # Discord chunking chunk = "" for line in lines: if len(chunk) + len(line) > 1900: await ctx.send(chunk) chunk = "" chunk += line + "\n" if chunk: await ctx.send(chunk) # ============================== # BOT READY # ============================== @bot.event async def on_ready(): print(f"Logged in as {bot.user.name}") # ============================== # RUN BOT # ============================== bot.run("MTQ0Mjg3NjU3NTUzMDg3NzAxMQ.GNuHPr.UreuYD1B7YYjfsbfRcEbhFyjyqvhQDepRCN4kk")