file and folder organization, removal of monoliths

This commit is contained in:
2025-11-25 14:34:32 -05:00
parent 93a5da75eb
commit f5fa1cd703
12 changed files with 188 additions and 301 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

64
cogs/assignments.py Normal file
View File

@@ -0,0 +1,64 @@
import asyncio
from discord.ext import commands
class Assignments(commands.Cog):
def __init__(self, bot, enemy_queue, active_assignments, enrolled_attackers, hit_check, reassign_delay):
self.bot = bot
self.enemy_queue = enemy_queue
self.active_assignments = active_assignments
self.enrolled_attackers = enrolled_attackers
self.HIT_CHECK_INTERVAL = hit_check
self.REASSIGN_DELAY = reassign_delay
# Start background task
bot.loop.create_task(self.monitor_assignments_loop())
def get_next_attacker(self):
if not self.enrolled_attackers:
return None
attacker = self.enrolled_attackers[0]
self.enrolled_attackers.append(self.enrolled_attackers.pop(0)) # round-robin
return attacker
async def monitor_assignments_loop(self):
await self.bot.wait_until_ready()
while not self.bot.is_closed():
now = asyncio.get_event_loop().time()
reassign_list = []
for enemy_id, data in list(self.active_assignments.items()):
elapsed = now - data["time_assigned"]
if elapsed >= self.HIT_CHECK_INTERVAL and elapsed < self.HIT_CHECK_INTERVAL + 5:
attacker_user = self.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 >= self.REASSIGN_DELAY:
reassign_list.append(enemy_id)
for enemy_id in reassign_list:
info = self.active_assignments.pop(enemy_id)
await self.reassign_target(info["enemy"])
await asyncio.sleep(5)
async def reassign_target(self, enemy):
attacker = self.get_next_attacker()
if attacker is None:
return
self.active_assignments[enemy["id"]] = {
"enemy": enemy,
"attacker": attacker,
"time_assigned": asyncio.get_event_loop().time()
}
attacker_user = self.bot.get_user(attacker)
if attacker_user:
try:
await attacker_user.send(f"Target reassigned to you: **{enemy['name']}**!")
except:
pass

54
cogs/commands.py Normal file
View File

@@ -0,0 +1,54 @@
from discord.ext import commands
from services.torn_api import fetch_enemy_members
from services.ffscouter import fetch_batch_stats
class HitCommands(commands.Cog):
def __init__(self, bot, enrolled_attackers, enemy_queue):
self.bot = bot
self.enrolled_attackers = enrolled_attackers
self.enemy_queue = enemy_queue
@commands.command()
async def enroll(self, ctx):
user_id = ctx.author.id
if user_id in self.enrolled_attackers:
await ctx.send("You are already enrolled.")
return
self.enrolled_attackers.append(user_id)
await ctx.send(f"{ctx.author.mention} has been enrolled in the hit rotation!")
@commands.command()
async def drop(self, ctx):
user_id = ctx.author.id
if user_id not in self.enrolled_attackers:
await ctx.send("You are not enrolled.")
return
self.enrolled_attackers.remove(user_id)
await ctx.send(f"{ctx.author.mention} has been removed from the rotation.")
@commands.command()
async def stats(self, ctx):
members = await fetch_enemy_members()
if not members:
await ctx.send("No active members found.")
return
ids = [m["id"] for m in members if m.get("status", {}).get("state") in ("Okay", "Idle")]
ff_map = await fetch_batch_stats(ids)
lines = []
for m in members:
pid = str(m["id"])
est = ff_map.get(pid, {}).get("bs_estimate_human", "?")
if m.get("status", {}).get("state") not in ("Okay", "Idle"):
continue
lines.append(f"**{m['name']}** (ID:{pid}) | Lv {m['level']} | Estimated BS: {est}")
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)

13
config.py Normal file
View File

@@ -0,0 +1,13 @@
# Torn API
TORN_API_KEY = "9VLK0Wte1BwXOheB"
ENEMY_FACTION_ID = 52935
YOUR_FACTION_ID = 654321
ALLOWED_CHANNEL_ID = 1442876328536707316
# FFScouter API
FFSCOUTER_KEY = "XYmWPO9ZYkLqnv3v"
# Intervals
POLL_INTERVAL = 30
HIT_CHECK_INTERVAL = 60
REASSIGN_DELAY = 120

324
main.py
View File

@@ -1,324 +1,46 @@
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
from discord.ext import commands
from config import ALLOWED_CHANNEL_ID, HIT_CHECK_INTERVAL, REASSIGN_DELAY
from cogs.assignments import Assignments
from cogs.commands import HitCommands
intents = discord.Intents.default()
intents.message_content = True
# ==============================
# STATE STORAGE
# ==============================
# Global state
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()))
# Load cogs with injected state
await self.add_cog(
Assignments(
self,
enemy_queue=enemy_queue,
active_assignments=active_assignments,
enrolled_attackers=enrolled_attackers,
hit_check=HIT_CHECK_INTERVAL,
reassign_delay=REASSIGN_DELAY
)
)
await self.add_cog(
HitCommands(
self,
enrolled_attackers=enrolled_attackers,
enemy_queue=enemy_queue
)
)
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": {"<id>": {...}}}
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")

Binary file not shown.

Binary file not shown.

22
services/ffscouter.py Normal file
View File

@@ -0,0 +1,22 @@
import aiohttp
from config import FFSCOUTER_KEY
async def fetch_batch_stats(ids: list[int]):
"""
Fetches predicted stats for a list of Torn IDs in a single FFScouter request.
Returns dict keyed by player_id.
"""
if not ids:
return {}
ids_str = ",".join(map(str, ids))
url = f"https://ffscouter.com/api/v1/get-stats?key={FFSCOUTER_KEY}&targets={ids_str}"
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
if resp.status != 200:
print("FFScouter batch error:", resp.status)
return {}
data = await resp.json()
return {str(d["player_id"]): d for d in data}

12
services/torn_api.py Normal file
View File

@@ -0,0 +1,12 @@
import aiohttp
from config import TORN_API_KEY, ENEMY_FACTION_ID
async def fetch_enemy_members():
url = f"https://api.torn.com/v2/faction/{ENEMY_FACTION_ID}?selections=members&key={TORN_API_KEY}"
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
if resp.status != 200:
print("Torn faction fetch error:", resp.status)
return []
data = await resp.json()
return data.get("members", [])