file and folder organization, removal of monoliths
This commit is contained in:
BIN
__pycache__/config.cpython-313.pyc
Normal file
BIN
__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/main.cpython-313.pyc
Normal file
BIN
__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
cogs/__pycache__/assignments.cpython-313.pyc
Normal file
BIN
cogs/__pycache__/assignments.cpython-313.pyc
Normal file
Binary file not shown.
BIN
cogs/__pycache__/commands.cpython-313.pyc
Normal file
BIN
cogs/__pycache__/commands.cpython-313.pyc
Normal file
Binary file not shown.
64
cogs/assignments.py
Normal file
64
cogs/assignments.py
Normal 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
54
cogs/commands.py
Normal 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
13
config.py
Normal 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
|
||||
322
main.py
322
main.py
@@ -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 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")
|
||||
|
||||
BIN
services/__pycache__/ffscouter.cpython-313.pyc
Normal file
BIN
services/__pycache__/ffscouter.cpython-313.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/torn_api.cpython-313.pyc
Normal file
BIN
services/__pycache__/torn_api.cpython-313.pyc
Normal file
Binary file not shown.
22
services/ffscouter.py
Normal file
22
services/ffscouter.py
Normal 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
12
services/torn_api.py
Normal 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", [])
|
||||
Reference in New Issue
Block a user