diff --git a/__pycache__/main.cpython-311.pyc b/__pycache__/main.cpython-311.pyc
index 21493d8..93572d5 100644
Binary files a/__pycache__/main.cpython-311.pyc and b/__pycache__/main.cpython-311.pyc differ
diff --git a/cogs/__pycache__/commands.cpython-311.pyc b/cogs/__pycache__/commands.cpython-311.pyc
index 256618d..f09a539 100644
Binary files a/cogs/__pycache__/commands.cpython-311.pyc and b/cogs/__pycache__/commands.cpython-311.pyc differ
diff --git a/cogs/commands.py b/cogs/commands.py
index 7946083..1430427 100644
--- a/cogs/commands.py
+++ b/cogs/commands.py
@@ -1,5 +1,5 @@
from discord.ext import commands
-from services.torn_api import update_enemy_faction, update_friendly_faction
+#from services.torn_api import update_enemy_faction, update_friendly_faction
class HitCommands(commands.Cog):
diff --git a/main.py b/main.py
index 32e34f9..4de8cdf 100644
--- a/main.py
+++ b/main.py
@@ -6,6 +6,8 @@ from cogs.commands import HitCommands
import asyncio
import uvicorn
+import json
+from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
@@ -13,47 +15,97 @@ from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
-from services.torn_api import update_enemy_faction, update_friendly_faction
+from services.torn_api import populate_friendly, populate_enemy, start_friendly_status_loop, start_enemy_status_loop
+
+
+
+# ============================================================
+# FastAPI Setup
+# ============================================================
-# -----------------------------
-# FastAPI setup
-# -----------------------------
app = FastAPI()
templates = Jinja2Templates(directory="templates")
app.mount("/static", StaticFiles(directory="static"), name="static")
-# -----------------------------
-# Dashboard page
-# -----------------------------
+
+# ============================================================
+# Dashboard Webpage
+# ============================================================
+
@app.get("/", response_class=HTMLResponse)
async def dashboard(request: Request):
print(">>> DASHBOARD ROUTE LOADED")
return templates.TemplateResponse("dashboard.html", {"request": request})
-# -----------------------------
-# Pydantic model for JSON payloads
-# -----------------------------
+
+# ============================================================
+# Pydantic model for JSON POST input
+# ============================================================
+
class FactionRequest(BaseModel):
faction_id: int
interval: int
-# -----------------------------
-# API Endpoints
-# -----------------------------
-@app.post("/api/update_enemy_faction")
-async def api_enemy(data: FactionRequest):
- await update_enemy_faction(data.faction_id, data.interval)
- return {"status": "enemy loop running", "id": data.faction_id, "interval": data.interval}
-
-@app.post("/api/update_friendly_faction")
-async def api_friendly(data: FactionRequest):
- await update_friendly_faction(data.faction_id, data.interval)
- return {"status": "friendly loop running", "id": data.faction_id, "interval": data.interval}
# -----------------------------
-# Discord bot setup
+# Populate endpoints (populate JSON once)
# -----------------------------
+@app.post("/api/populate_friendly")
+async def api_populate_friendly(data: FactionRequest):
+ from services.torn_api import populate_friendly
+ await populate_friendly(data.faction_id)
+ return {"status": "friendly populated", "id": data.faction_id}
+
+@app.post("/api/populate_enemy")
+async def api_populate_enemy(data: FactionRequest):
+ from services.torn_api import populate_enemy
+ await populate_enemy(data.faction_id)
+ return {"status": "enemy populated", "id": data.faction_id}
+
+
+# -----------------------------
+# Start status refresh loops
+# -----------------------------
+@app.post("/api/start_friendly_status")
+async def api_start_friendly_status(data: FactionRequest):
+ from services.torn_api import start_friendly_status_loop
+ await start_friendly_status_loop(data.faction_id, data.interval)
+ return {"status": "friendly status loop started", "id": data.faction_id, "interval": data.interval}
+
+@app.post("/api/start_enemy_status")
+async def api_start_enemy_status(data: FactionRequest):
+ from services.torn_api import start_enemy_status_loop
+ await start_enemy_status_loop(data.faction_id, data.interval)
+ return {"status": "enemy status loop started", "id": data.faction_id, "interval": data.interval}
+
+
+# =============================
+# Member JSON endpoints
+# =============================
+@app.get("/api/friendly_members")
+async def get_friendly_members():
+ path = Path("data/friendly_faction.json")
+ if not path.exists():
+ return []
+ with open(path, "r", encoding="utf-8") as f:
+ return json.load(f)
+
+@app.get("/api/enemy_members")
+async def get_enemy_members():
+ path = Path("data/enemy_faction.json")
+ if not path.exists():
+ return []
+ with open(path, "r", encoding="utf-8") as f:
+ return json.load(f)
+
+
+
+
+# ============================================================
+# Discord Bot Setup
+# ============================================================
+
intents = discord.Intents.default()
intents.message_content = True
@@ -62,9 +114,9 @@ enemy_queue = []
active_assignments = {}
round_robin_index = 0
+
class HitDispatchBot(commands.Bot):
async def setup_hook(self):
- # Load cogs with injected state
await self.add_cog(
Assignments(
self,
@@ -72,9 +124,10 @@ class HitDispatchBot(commands.Bot):
active_assignments=active_assignments,
enrolled_attackers=enrolled_attackers,
hit_check=HIT_CHECK_INTERVAL,
- reassign_delay=REASSIGN_DELAY
+ reassign_delay=REASSIGN_DELAY,
)
)
+
await self.add_cog(
HitCommands(
self,
@@ -86,25 +139,31 @@ class HitDispatchBot(commands.Bot):
async def cog_check(self, ctx):
return ctx.channel.id == ALLOWED_CHANNEL_ID
+
bot = HitDispatchBot(command_prefix="!", intents=intents)
+
@bot.event
async def on_ready():
print(f"Logged in as {bot.user.name}")
+
TOKEN = "YOUR_DISCORD_TOKEN"
+
async def start_bot():
await bot.start(TOKEN)
-# -----------------------------
-# Main entry
-# -----------------------------
+
+# ============================================================
+# Main Entry Point
+# ============================================================
+
if __name__ == "__main__":
loop = asyncio.get_event_loop()
# Start Discord bot in background
loop.create_task(start_bot())
- # Run FastAPI (this will keep the loop alive)
+ # Run FastAPI app — keeps loop alive
uvicorn.run(app, host="127.0.0.1", port=8000)
diff --git a/services/__pycache__/torn_api.cpython-311.pyc b/services/__pycache__/torn_api.cpython-311.pyc
index 37a6746..ea1cae7 100644
Binary files a/services/__pycache__/torn_api.cpython-311.pyc and b/services/__pycache__/torn_api.cpython-311.pyc differ
diff --git a/services/torn_api.py b/services/torn_api.py
index 5e85db1..a9989d8 100644
--- a/services/torn_api.py
+++ b/services/torn_api.py
@@ -3,31 +3,34 @@ import aiohttp
import json
import asyncio
from pathlib import Path
-from config import TORN_API_KEY, ENEMY_FACTION_ID, YOUR_FACTION_ID
+from config import TORN_API_KEY
from .ffscouter import fetch_batch_stats
-ENEMY_FILE = Path("data/enemy_faction.json")
-FRIENDLY_FILE = Path("data/friendly_faction.json")
+FRIENDLY_MEMBERS_FILE = Path("data/friendly_members.json")
+FRIENDLY_STATUS_FILE = Path("data/friendly_status.json")
+ENEMY_MEMBERS_FILE = Path("data/enemy_members.json")
+ENEMY_STATUS_FILE = Path("data/enemy_status.json")
-# Track running tasks + current faction IDs
-enemy_task = None
-friendly_task = None
+# Tasks
+friendly_status_task = None
+enemy_status_task = None
-current_enemy_id = None
-current_friendly_id = None
+# Locks
+friendly_lock = asyncio.Lock()
+enemy_lock = asyncio.Lock()
-async def fetch_and_save_faction(faction_id: int, file_path: Path) -> bool:
- """
- Fetches faction members from Torn, fetches their estimated BS from FFScouter,
- and saves everything to a JSON file.
- """
+# -----------------------------
+# Static population (once)
+# -----------------------------
+async def populate_faction(faction_id: int, members_file: Path, status_file: Path):
+ """Fetch members + FFScouter estimates once and save static info + initial status."""
url = f"https://api.torn.com/v2/faction/{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(f"Torn faction fetch error: {resp.status}")
+ print(f"Error fetching faction {faction_id}: {resp.status}")
return False
data = await resp.json()
@@ -35,92 +38,95 @@ async def fetch_and_save_faction(faction_id: int, file_path: Path) -> bool:
if not members_list:
return False
- # Build list of IDs (Torn uses 'id', not 'player_id')
- member_ids = [info.get("id") for info in members_list if "id" in info]
+ member_ids = [m.get("id") for m in members_list if "id" in m]
if not member_ids:
return False
- # Fetch batch FFScouter stats
- ff_data = await fetch_batch_stats(member_ids) # returns dict keyed by player_id
-
- # Build final faction data
- faction_data = []
- for info in members_list:
- pid = info.get("id")
- if pid is None:
- continue
+ # Fetch FFScouter data once
+ ff_data = await fetch_batch_stats(member_ids)
+ # Build static member list
+ members = []
+ status_data = {}
+ for m in members_list:
+ pid = m["id"]
est = ff_data.get(str(pid), {}).get("bs_estimate_human", "?")
member = {
"id": pid,
- "name": info.get("name", "Unknown"),
- "level": info.get("level", 0),
- "status": info.get("status", {}).get("state", "Unknown"),
- "estimate": est
+ "name": m.get("name", "Unknown"),
+ "level": m.get("level", 0),
+ "estimate": est,
}
- faction_data.append(member)
+ members.append(member)
+ # initial status
+ status_data[pid] = {"status": m.get("status", {}).get("state", "Unknown")}
- # Save to file
- file_path.parent.mkdir(exist_ok=True, parents=True) # ensure folder exists
- with open(file_path, "w", encoding="utf-8") as f:
- json.dump(faction_data, f, indent=2)
+ # Save members
+ members_file.parent.mkdir(exist_ok=True, parents=True)
+ with open(members_file, "w", encoding="utf-8") as f:
+ json.dump(members, f, indent=2)
+
+ # Save initial status
+ with open(status_file, "w", encoding="utf-8") as f:
+ json.dump(status_data, f, indent=2)
return True
-
-
-#Loop for the constant update of members and the stop function
-
-async def stop_task_if_running(task: asyncio.Task | None):
- """Cancel an existing running task safely."""
- if task and not task.done():
- task.cancel()
- try:
- await task
- except asyncio.CancelledError:
- pass
-
-async def faction_loop(faction_id: int, file_path: Path, interval: int):
- """
- Runs fetch_and_save_faction() in a loop forever, waiting `interval`
- seconds between iterations.
- """
+# -----------------------------
+# Status refresh loop
+# -----------------------------
+async def refresh_status_loop(faction_id: int, status_file: Path, lock: asyncio.Lock, interval: int):
+ """Refresh only status from Torn API periodically."""
while True:
try:
- await fetch_and_save_faction(faction_id, file_path)
+ url = f"https://api.torn.com/v2/faction/{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(f"Status fetch error {resp.status}")
+ await asyncio.sleep(interval)
+ continue
+ data = await resp.json()
+
+ members_list = data.get("members", [])
+ status_data = {m["id"]: {"status": m.get("status", {}).get("state", "Unknown")} for m in members_list}
+
+ # Save status safely
+ async with lock:
+ with open(status_file, "w", encoding="utf-8") as f:
+ json.dump(status_data, f, indent=2)
+
except Exception as e:
- print(f"Error during faction loop for {faction_id}: {e}")
+ print(f"Error in status loop for {faction_id}: {e}")
await asyncio.sleep(interval)
+# -----------------------------
+# Helper functions for endpoints
+# -----------------------------
+async def populate_friendly(faction_id: int):
+ return await populate_faction(faction_id, FRIENDLY_MEMBERS_FILE, FRIENDLY_STATUS_FILE)
-#Functions to call the loop, maybe add one to just call once?
-async def update_enemy_faction(new_faction_id: int, interval: int):
- global enemy_task, current_enemy_id
- # If faction ID changes → stop old loop
- if new_faction_id != current_enemy_id:
- print(f"[ENEMY] Changing faction from {current_enemy_id} → {new_faction_id}")
- await stop_task_if_running(enemy_task)
- current_enemy_id = new_faction_id
+async def populate_enemy(faction_id: int):
+ return await populate_faction(faction_id, ENEMY_MEMBERS_FILE, ENEMY_STATUS_FILE)
- # Start new loop
- enemy_task = asyncio.create_task(
- faction_loop(new_faction_id, ENEMY_FILE, interval)
+
+async def start_friendly_status_loop(faction_id: int, interval: int):
+ global friendly_status_task
+ if friendly_status_task and not friendly_status_task.done():
+ friendly_status_task.cancel()
+ friendly_status_task = asyncio.create_task(
+ refresh_status_loop(faction_id, FRIENDLY_STATUS_FILE, friendly_lock, interval)
)
-async def update_friendly_faction(new_faction_id: int, interval: int):
- global friendly_task, current_friendly_id
-
- if new_faction_id != current_friendly_id:
- print(f"[FRIENDLY] Changing faction from {current_friendly_id} → {new_faction_id}")
- await stop_task_if_running(friendly_task)
- current_friendly_id = new_faction_id
-
- friendly_task = asyncio.create_task(
- faction_loop(new_faction_id, FRIENDLY_FILE, interval)
+async def start_enemy_status_loop(faction_id: int, interval: int):
+ global enemy_status_task
+ if enemy_status_task and not enemy_status_task.done():
+ enemy_status_task.cancel()
+ enemy_status_task = asyncio.create_task(
+ refresh_status_loop(faction_id, ENEMY_STATUS_FILE, enemy_lock, interval)
)
-
diff --git a/static/dashboard.js b/static/dashboard.js
index 48fb613..b1e5011 100644
--- a/static/dashboard.js
+++ b/static/dashboard.js
@@ -1,36 +1,158 @@
-async function updateEnemy() {
- const factionId = parseInt(document.getElementById("enemyId").value);
- const interval = parseInt(document.getElementById("refreshInterval").value);
+// dashboard.js
- if (!factionId || !interval) {
- alert("Please enter Enemy Faction ID and Refresh Interval!");
- return;
- }
+const friendlyMembers = new Map();
+const enemyMembers = new Map();
- await fetch(`/api/update_enemy_faction`, {
- method: "POST",
- headers: {"Content-Type": "application/json"},
- body: JSON.stringify({ faction_id: factionId, interval: interval })
+const friendlyContainer = document.getElementById("friendly-container");
+const enemyContainer = document.getElementById("enemy-container");
+
+function createMemberCard(member) {
+ const card = document.createElement("div");
+ card.classList.add("member-card");
+ card.dataset.id = member.id;
+
+ // Clear innerHTML, create structured divs
+ const nameDiv = document.createElement("div");
+ nameDiv.classList.add("name");
+ nameDiv.textContent = member.name;
+
+ const statsDiv = document.createElement("div");
+ statsDiv.classList.add("stats");
+ statsDiv.innerHTML = `
+ Level: ${member.level}
+ Estimate: ${member.estimate}
+ Status: ${member.status || "Unknown"}
+ `;
+
+ card.appendChild(nameDiv);
+ card.appendChild(statsDiv);
+
+ // Store reference to DOM element
+ member.domElement = card;
+
+ // Make card draggable
+ card.draggable = true;
+ card.addEventListener("dragstart", e => {
+ e.dataTransfer.setData("text/plain", member.id);
});
+
+ // Set initial status color
+ updateStatusColor(member);
+
+ return card;
}
-async function updateFriendly() {
- const factionId = parseInt(document.getElementById("friendlyId").value);
+function updateStatusColor(member) {
+ const statusSpan = member.domElement.querySelector(".status-text");
+ statusSpan.classList.remove("status-ok", "status-traveling", "status-hospitalized");
- if (!factionId) {
- alert("Please enter Friendly Faction ID!");
- return;
- }
+ if (!member.status) return;
- const interval = parseInt(document.getElementById("refreshInterval").value);
- if (!interval) {
- alert("Please enter Refresh Interval!");
- return;
- }
-
- await fetch(`/api/update_friendly_faction`, {
- method: "POST",
- headers: {"Content-Type": "application/json"},
- body: JSON.stringify({ faction_id: factionId, interval: interval })
- });
+ const s = member.status.toLowerCase();
+ if (s === "okay") statusSpan.classList.add("status-ok");
+ else if (s === "traveling" || s === "abroad") statusSpan.classList.add("status-traveling");
+ else if (s === "hospitalized") statusSpan.classList.add("status-hospitalized");
}
+
+async function loadMembers(faction) {
+ const url = faction === "friendly" ? "/api/friendly_members" : "/api/enemy_members";
+ const response = await fetch(url);
+ const members = await response.json();
+
+ const container = faction === "friendly" ? friendlyContainer : enemyContainer;
+ const map = faction === "friendly" ? friendlyMembers : enemyMembers;
+
+ container.innerHTML = "";
+ map.clear();
+
+ members.forEach(m => {
+ if (!m.status) m.status = "Unknown";
+ const card = createMemberCard(m);
+ map.set(m.id, m);
+ container.appendChild(card);
+ });
+
+ refreshStatus(faction);
+}
+
+async function refreshStatus(faction) {
+ const url = faction === "friendly" ? "/api/friendly_status" : "/api/enemy_status";
+ const map = faction === "friendly" ? friendlyMembers : enemyMembers;
+
+ try {
+ const response = await fetch(url);
+ const statusData = await response.json();
+
+ Object.keys(statusData).forEach(id => {
+ const member = map.get(parseInt(id));
+ if (!member) return;
+ member.status = statusData[id].status;
+
+ // Update DOM
+ const statusSpan = member.domElement.querySelector(".status-text");
+ statusSpan.textContent = member.status;
+
+ // Apply correct color class
+ updateStatusColor(member);
+ });
+ } catch (err) {
+ console.error("Failed to refresh status:", err);
+ }
+}
+
+async function populateFriendly() {
+ const id = parseInt(document.getElementById("friendly-id").value);
+ if (!id) return alert("Enter a valid faction ID!");
+
+ await fetch("/api/populate_friendly", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ faction_id: id, interval: 0 })
+ });
+
+ loadMembers("friendly");
+}
+
+async function populateEnemy() {
+ const id = parseInt(document.getElementById("enemy-id").value);
+ if (!id) return alert("Enter a valid faction ID!");
+
+ await fetch("/api/populate_enemy", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ faction_id: id, interval: 0 })
+ });
+
+ loadMembers("enemy");
+}
+
+async function startFriendlyStatus() {
+ const id = parseInt(document.getElementById("friendly-id").value);
+ const interval = parseInt(document.getElementById("refresh-interval").value) || 10;
+
+ await fetch("/api/start_friendly_status", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ faction_id: id, interval })
+ });
+
+ setInterval(() => refreshStatus("friendly"), interval * 1000);
+}
+
+async function startEnemyStatus() {
+ const id = parseInt(document.getElementById("enemy-id").value);
+ const interval = parseInt(document.getElementById("refresh-interval").value) || 10;
+
+ await fetch("/api/start_enemy_status", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ faction_id: id, interval })
+ });
+
+ setInterval(() => refreshStatus("enemy"), interval * 1000);
+}
+
+document.getElementById("friendly-populate-btn").addEventListener("click", populateFriendly);
+document.getElementById("enemy-populate-btn").addEventListener("click", populateEnemy);
+document.getElementById("friendly-status-btn").addEventListener("click", startFriendlyStatus);
+document.getElementById("enemy-status-btn").addEventListener("click", startEnemyStatus);
diff --git a/static/styles.css b/static/styles.css
index abd8908..6c299d7 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -13,7 +13,6 @@ body {
margin: 2rem auto;
}
-/* Top Bar with Title + Interval Box */
.top-bar {
display: flex;
justify-content: space-between;
@@ -41,7 +40,6 @@ body {
border: none;
}
-/* Horizontal Faction Row */
.faction-row {
display: flex;
flex-direction: row !important;
@@ -49,7 +47,6 @@ body {
gap: 2rem;
}
-/* Each Faction Card */
.faction-card {
flex: 1;
background-color: #2c2c3e;
@@ -87,3 +84,89 @@ button {
button:hover {
background-color: #3399ff;
}
+
+.member-list {
+ margin-top: 1rem;
+ max-height: 350px;
+ overflow-y: auto;
+ background: #1a1a26;
+ padding: 0.8rem;
+ border-radius: 10px;
+}
+
+.member-card {
+ background-color: #3a3a4d;
+ padding: 1rem;
+ margin: 0.5rem 0;
+ border-radius: 10px;
+ display: flex;
+ flex-direction: row; /* horizontal layout */
+ align-items: center;
+ justify-content: flex-start;
+ gap: 2rem;
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
+ cursor: grab;
+ min-width: 200px;
+}
+
+.member-card:active {
+ cursor: grabbing;
+}
+
+/* Name section */
+.member-card .name {
+ font-weight: bold;
+ color: #66ccff;
+ min-width: 120px; /* ensures spacing */
+}
+
+/* Stats section */
+.member-card .stats {
+ font-size: 0.9rem;
+ line-height: 1.3;
+ color: #f0f0f0;
+}
+
+.member-card strong {
+ font-size: 1rem;
+ min-width: 100px;
+}
+
+.member-card span {
+ font-size: 0.9rem;
+}
+
+.member-card:hover {
+ background-color: #4a4a60;
+}
+
+#friendly-container,
+#enemy-container {
+ max-height: 400px;
+ overflow-y: auto;
+ padding: 0.5rem;
+ border: 1px solid #444;
+ border-radius: 10px;
+ background-color: #2c2c3e;
+}
+
+#friendly-container::-webkit-scrollbar,
+#enemy-container::-webkit-scrollbar {
+ width: 8px;
+}
+
+#friendly-container::-webkit-scrollbar-thumb,
+#enemy-container::-webkit-scrollbar-thumb {
+ background-color: #66ccff;
+ border-radius: 4px;
+}
+
+#friendly-container::-webkit-scrollbar-track,
+#enemy-container::-webkit-scrollbar-track {
+ background-color: #2c2c3e;
+ border-radius: 4px;
+}
+
+.status-ok { color: #28a745; font-weight: bold; }
+.status-traveling { color: #3399ff; font-weight: bold; }
+.status-hospitalized { color: #ff4d4d; font-weight: bold; }
diff --git a/templates/dashboard.html b/templates/dashboard.html
index 78f0f70..b3009b1 100644
--- a/templates/dashboard.html
+++ b/templates/dashboard.html
@@ -6,38 +6,38 @@