Files
faction_war_dispatch_bot/main.py

375 lines
11 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 peruser 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 scorerange string that matches the chart.
"""
# ------------------------------------------------------------------
# Bucket definitions (same as in the original code)
# ------------------------------------------------------------------
level_buckets = [
(6, "2k25k"),
(11, "20k25k"),
(26, "200k250k"),
(31, "2m2.5m"),
(50, "20m35m"),
(float('inf'), "200m250m")
]
crimes_buckets = [
(5000, "2k25k"),
(10000, "20k25k"),
(20000, "200k250k"),
(30000, "2m2.5m"),
(50000, "20m35m"),
(float('inf'), "200m250m")
]
networth_buckets = [
(50, "2k25k"), # <50m
(500, "20k25k"), # <500m
(5_000, "200k250k"), # <5b
(50_000, "2m2.5m"), # <50b
(200_000, "20m35m"), # <200b
(float('inf'), "200m250m")
]
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 '200k250k' 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 '200k250k' or '20m35' 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")