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 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) # ------------------------------------------------------------------ # Helper – get a single member's crimes & networth # ------------------------------------------------------------------ async def _get_user_stats(session: aiohttp.ClientSession, user_id: int): url = f"https://api.torn.com/v2/user/{user_id}/personalstats?stat=criminaloffenses,networth&key={TORN_API_KEY}" async with session.get(url) as resp: data = await resp.json() crimes = 0 networth_raw = 0 for stat in data.get("personalstats", []): if stat.get("name") == "criminaloffenses": crimes = int(stat.get("value", 0)) elif stat.get("name") == "networth": networth_raw = float(stat.get("value", 0)) networth_mill = networth_raw / 1_000_000 return crimes, networth_mill # ------------------------------------------------------------------ # Updated fetch_enemy_faction – now pulls crimes & networth # ------------------------------------------------------------------ async def fetch_enemy_faction(): """ Pulls all active members from the target faction and enriches each member with level, crimes and networth (in millions). """ url = ( f"https://api.torn.com/v2/faction/{ENEMY_FACTION_ID}" "?selections=members" f"&key={TORN_API_KEY}" ) async with aiohttp.ClientSession() as session: async with session.get(url) as resp: data = await resp.json() # The response is a dict → {"members": [ {...}, ... ]} members_list = data.get("members", []) enemies: list[dict] = [] for info in members_list: status_state = info["status"]["state"] if status_state not in ["Okay", "Idle"]: continue level = int(info.get("level", 0)) # ------------------------------------------------------------------ # Pull the per‑user stats (crimes & networth) # ------------------------------------------------------------------ crimes, networth_mill = await _get_user_stats(session, info["id"]) enemies.append({ "id": int(info["id"]), "name": info["name"], "status": status_state, "level": level, "crimes": crimes, "networth": networth_mill, # millions }) 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) # ============================== # STAT SYSTEM # ============================== def estimate_battlestats(level: int, crimes: int, networth: float) -> str: """ Returns the score‑range string that matches the chart. """ # ------------------------------------------------------------------ # Bucket definitions (same as in the original code) # ------------------------------------------------------------------ level_buckets = [ (6, "2k‑25k"), (11, "20k‑25k"), (26, "200k‑250k"), (31, "2m‑2.5m"), (50, "20m‑35m"), (float('inf'), "200m‑250m") ] crimes_buckets = [ (5000, "2k‑25k"), (10000, "20k‑25k"), (20000, "200k‑250k"), (30000, "2m‑2.5m"), (50000, "20m‑35m"), (float('inf'), "200m‑250m") ] networth_buckets = [ (50, "2k‑25k"), # < 50 m (500, "20k‑25k"), # < 500 m (5_000, "200k‑250k"), # < 5 b (50_000, "2m‑2.5m"), # < 50 b (200_000, "20m‑35m"), # < 200 b (float('inf'), "200m‑250m") ] def find_bucket(value: float, buckets): for limit, label in buckets: if value < limit: return label return "Unknown" lvl_label = find_bucket(level, level_buckets) crime_label = find_bucket(crimes, crimes_buckets) net_label = find_bucket(networth, networth_buckets) # ------------------------------------------------------------------ # Convert a label like '200k‑250k' into its lower bound in points. # We only need the *first* number – everything after the dash is # ignored. The unit (k/m/b) tells us how to scale it. # ------------------------------------------------------------------ def score_value(label: str) -> float: """ Convert a label such as '200k‑250k' or '20m‑35' into the numeric lower bound in points. """ m = re.match(r"(\d+(?:\.\d*)?)([kmb]?)", label, flags=re.IGNORECASE) if not m: return float('inf') # shouldn't happen number_str, unit = m.groups() try: num = float(number_str) except ValueError: return float('inf') unit = unit.lower() if unit == 'm': return num * 1_000_000 if unit == 'k': return num * 1_000 # No unit → plain points return num chosen_label = min([lvl_label, crime_label, net_label], key=score_value) return chosen_label # ============================== # 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 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 for e in enemies: score_range = estimate_battlestats(e["level"], e["crimes"], e["networth"]) # await ctx.send( # f'{e["name"]} (ID:{e["id"]}) | ' # f'Lv {e["level"]}, Crimes {e["crimes"]}, NetWorth ${e["networth"]:,.1f}m → ' # f'Score: {score_range}' # ) print( f'{e["name"]} (ID:{e["id"]}) | ' f'Lv {e["level"]}, Crimes {e["crimes"]}, NetWorth ${e["networth"]:,.1f}m → ' f'Score: {score_range}' ) # ============================== # BOT READY # ============================== @bot.event async def on_ready(): print(f"Logged in as {bot.user.name}") # ============================== # RUN BOT # ============================== bot.run("MTQ0Mjg3NjU3NTUzMDg3NzAxMQ.GNuHPr.UreuYD1B7YYjfsbfRcEbhFyjyqvhQDepRCN4kk")