325 lines
8.7 KiB
Python
325 lines
8.7 KiB
Python
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": {"<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")
|