375 lines
11 KiB
Python
375 lines
11 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
|
||
|
||
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")
|