Compare commits
2 Commits
79d0012d66
...
7cf882959b
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cf882959b | |||
| 317442b8ea |
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
from discord.ext import commands
|
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):
|
class HitCommands(commands.Cog):
|
||||||
|
|||||||
117
main.py
117
main.py
@@ -6,6 +6,8 @@ from cogs.commands import HitCommands
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
@@ -13,47 +15,97 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from pydantic import BaseModel
|
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()
|
app = FastAPI()
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# Dashboard page
|
# ============================================================
|
||||||
# -----------------------------
|
# Dashboard Webpage
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def dashboard(request: Request):
|
async def dashboard(request: Request):
|
||||||
print(">>> DASHBOARD ROUTE LOADED")
|
print(">>> DASHBOARD ROUTE LOADED")
|
||||||
return templates.TemplateResponse("dashboard.html", {"request": request})
|
return templates.TemplateResponse("dashboard.html", {"request": request})
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# Pydantic model for JSON payloads
|
# ============================================================
|
||||||
# -----------------------------
|
# Pydantic model for JSON POST input
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
class FactionRequest(BaseModel):
|
class FactionRequest(BaseModel):
|
||||||
faction_id: int
|
faction_id: int
|
||||||
interval: 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 = discord.Intents.default()
|
||||||
intents.message_content = True
|
intents.message_content = True
|
||||||
|
|
||||||
@@ -62,9 +114,9 @@ enemy_queue = []
|
|||||||
active_assignments = {}
|
active_assignments = {}
|
||||||
round_robin_index = 0
|
round_robin_index = 0
|
||||||
|
|
||||||
|
|
||||||
class HitDispatchBot(commands.Bot):
|
class HitDispatchBot(commands.Bot):
|
||||||
async def setup_hook(self):
|
async def setup_hook(self):
|
||||||
# Load cogs with injected state
|
|
||||||
await self.add_cog(
|
await self.add_cog(
|
||||||
Assignments(
|
Assignments(
|
||||||
self,
|
self,
|
||||||
@@ -72,9 +124,10 @@ class HitDispatchBot(commands.Bot):
|
|||||||
active_assignments=active_assignments,
|
active_assignments=active_assignments,
|
||||||
enrolled_attackers=enrolled_attackers,
|
enrolled_attackers=enrolled_attackers,
|
||||||
hit_check=HIT_CHECK_INTERVAL,
|
hit_check=HIT_CHECK_INTERVAL,
|
||||||
reassign_delay=REASSIGN_DELAY
|
reassign_delay=REASSIGN_DELAY,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.add_cog(
|
await self.add_cog(
|
||||||
HitCommands(
|
HitCommands(
|
||||||
self,
|
self,
|
||||||
@@ -86,25 +139,31 @@ class HitDispatchBot(commands.Bot):
|
|||||||
async def cog_check(self, ctx):
|
async def cog_check(self, ctx):
|
||||||
return ctx.channel.id == ALLOWED_CHANNEL_ID
|
return ctx.channel.id == ALLOWED_CHANNEL_ID
|
||||||
|
|
||||||
|
|
||||||
bot = HitDispatchBot(command_prefix="!", intents=intents)
|
bot = HitDispatchBot(command_prefix="!", intents=intents)
|
||||||
|
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_ready():
|
async def on_ready():
|
||||||
print(f"Logged in as {bot.user.name}")
|
print(f"Logged in as {bot.user.name}")
|
||||||
|
|
||||||
|
|
||||||
TOKEN = "YOUR_DISCORD_TOKEN"
|
TOKEN = "YOUR_DISCORD_TOKEN"
|
||||||
|
|
||||||
|
|
||||||
async def start_bot():
|
async def start_bot():
|
||||||
await bot.start(TOKEN)
|
await bot.start(TOKEN)
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# Main entry
|
# ============================================================
|
||||||
# -----------------------------
|
# Main Entry Point
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
# Start Discord bot in background
|
# Start Discord bot in background
|
||||||
loop.create_task(start_bot())
|
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)
|
uvicorn.run(app, host="127.0.0.1", port=8000)
|
||||||
|
|||||||
Binary file not shown.
@@ -3,31 +3,34 @@ import aiohttp
|
|||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
from pathlib import Path
|
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
|
from .ffscouter import fetch_batch_stats
|
||||||
|
|
||||||
ENEMY_FILE = Path("data/enemy_faction.json")
|
FRIENDLY_MEMBERS_FILE = Path("data/friendly_members.json")
|
||||||
FRIENDLY_FILE = Path("data/friendly_faction.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
|
# Tasks
|
||||||
enemy_task = None
|
friendly_status_task = None
|
||||||
friendly_task = None
|
enemy_status_task = None
|
||||||
|
|
||||||
current_enemy_id = None
|
# Locks
|
||||||
current_friendly_id = None
|
friendly_lock = asyncio.Lock()
|
||||||
|
enemy_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
|
||||||
async def fetch_and_save_faction(faction_id: int, file_path: Path) -> bool:
|
# -----------------------------
|
||||||
"""
|
# Static population (once)
|
||||||
Fetches faction members from Torn, fetches their estimated BS from FFScouter,
|
# -----------------------------
|
||||||
and saves everything to a JSON file.
|
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}"
|
url = f"https://api.torn.com/v2/faction/{faction_id}?selections=members&key={TORN_API_KEY}"
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url) as resp:
|
async with session.get(url) as resp:
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
print(f"Torn faction fetch error: {resp.status}")
|
print(f"Error fetching faction {faction_id}: {resp.status}")
|
||||||
return False
|
return False
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
|
|
||||||
@@ -35,91 +38,95 @@ async def fetch_and_save_faction(faction_id: int, file_path: Path) -> bool:
|
|||||||
if not members_list:
|
if not members_list:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Build list of IDs (Torn uses 'id', not 'player_id')
|
member_ids = [m.get("id") for m in members_list if "id" in m]
|
||||||
member_ids = [info.get("id") for info in members_list if "id" in info]
|
|
||||||
if not member_ids:
|
if not member_ids:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Fetch batch FFScouter stats
|
# Fetch FFScouter data once
|
||||||
ff_data = await fetch_batch_stats(member_ids) # returns dict keyed by player_id
|
ff_data = await fetch_batch_stats(member_ids)
|
||||||
|
|
||||||
# Build final faction data
|
|
||||||
faction_data = []
|
|
||||||
for info in members_list:
|
|
||||||
pid = info.get("id")
|
|
||||||
if pid is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
# 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", "?")
|
est = ff_data.get(str(pid), {}).get("bs_estimate_human", "?")
|
||||||
member = {
|
member = {
|
||||||
"id": pid,
|
"id": pid,
|
||||||
"name": info.get("name", "Unknown"),
|
"name": m.get("name", "Unknown"),
|
||||||
"level": info.get("level", 0),
|
"level": m.get("level", 0),
|
||||||
"estimate": est
|
"estimate": est,
|
||||||
}
|
}
|
||||||
faction_data.append(member)
|
members.append(member)
|
||||||
|
# initial status
|
||||||
|
status_data[pid] = {"status": m.get("status", {}).get("state", "Unknown")}
|
||||||
|
|
||||||
# Save to file
|
# Save members
|
||||||
file_path.parent.mkdir(exist_ok=True, parents=True) # ensure folder exists
|
members_file.parent.mkdir(exist_ok=True, parents=True)
|
||||||
with open(file_path, "w", encoding="utf-8") as f:
|
with open(members_file, "w", encoding="utf-8") as f:
|
||||||
json.dump(faction_data, f, indent=2)
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Status refresh loop
|
||||||
#Loop for the constant update of members and the stop function
|
# -----------------------------
|
||||||
|
async def refresh_status_loop(faction_id: int, status_file: Path, lock: asyncio.Lock, interval: int):
|
||||||
async def stop_task_if_running(task: asyncio.Task | None):
|
"""Refresh only status from Torn API periodically."""
|
||||||
"""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.
|
|
||||||
"""
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
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:
|
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)
|
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
|
async def populate_enemy(faction_id: int):
|
||||||
if new_faction_id != current_enemy_id:
|
return await populate_faction(faction_id, ENEMY_MEMBERS_FILE, ENEMY_STATUS_FILE)
|
||||||
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
|
|
||||||
|
|
||||||
# Start new loop
|
|
||||||
enemy_task = asyncio.create_task(
|
async def start_friendly_status_loop(faction_id: int, interval: int):
|
||||||
faction_loop(new_faction_id, ENEMY_FILE, interval)
|
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):
|
async def start_enemy_status_loop(faction_id: int, interval: int):
|
||||||
global friendly_task, current_friendly_id
|
global enemy_status_task
|
||||||
|
if enemy_status_task and not enemy_status_task.done():
|
||||||
if new_faction_id != current_friendly_id:
|
enemy_status_task.cancel()
|
||||||
print(f"[FRIENDLY] Changing faction from {current_friendly_id} → {new_faction_id}")
|
enemy_status_task = asyncio.create_task(
|
||||||
await stop_task_if_running(friendly_task)
|
refresh_status_loop(faction_id, ENEMY_STATUS_FILE, enemy_lock, interval)
|
||||||
current_friendly_id = new_faction_id
|
|
||||||
|
|
||||||
friendly_task = asyncio.create_task(
|
|
||||||
faction_loop(new_faction_id, FRIENDLY_FILE, interval)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,158 @@
|
|||||||
async function updateEnemy() {
|
// dashboard.js
|
||||||
const factionId = parseInt(document.getElementById("enemyId").value);
|
|
||||||
const interval = parseInt(document.getElementById("refreshInterval").value);
|
|
||||||
|
|
||||||
if (!factionId || !interval) {
|
const friendlyMembers = new Map();
|
||||||
alert("Please enter Enemy Faction ID and Refresh Interval!");
|
const enemyMembers = new Map();
|
||||||
return;
|
|
||||||
|
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}<br>
|
||||||
|
Estimate: ${member.estimate}<br>
|
||||||
|
Status: <span class="status-text">${member.status || "Unknown"}</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetch(`/api/update_enemy_faction`, {
|
function updateStatusColor(member) {
|
||||||
|
const statusSpan = member.domElement.querySelector(".status-text");
|
||||||
|
statusSpan.classList.remove("status-ok", "status-traveling", "status-hospitalized");
|
||||||
|
|
||||||
|
if (!member.status) return;
|
||||||
|
|
||||||
|
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",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ faction_id: factionId, interval: interval })
|
body: JSON.stringify({ faction_id: id, interval: 0 })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
loadMembers("friendly");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateFriendly() {
|
async function populateEnemy() {
|
||||||
const factionId = parseInt(document.getElementById("friendlyId").value);
|
const id = parseInt(document.getElementById("enemy-id").value);
|
||||||
|
if (!id) return alert("Enter a valid faction ID!");
|
||||||
|
|
||||||
if (!factionId) {
|
await fetch("/api/populate_enemy", {
|
||||||
alert("Please enter Friendly Faction ID!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const interval = parseInt(document.getElementById("refreshInterval").value);
|
|
||||||
if (!interval) {
|
|
||||||
alert("Please enter Refresh Interval!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await fetch(`/api/update_friendly_faction`, {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ faction_id: factionId, interval: interval })
|
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);
|
||||||
|
|||||||
@@ -4,43 +4,74 @@ body {
|
|||||||
color: #f0f0f0;
|
color: #f0f0f0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 800px;
|
width: 95%;
|
||||||
|
max-width: 1300px;
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
padding: 2rem;
|
}
|
||||||
|
|
||||||
|
.top-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interval-box {
|
||||||
|
background-color: #3a3a4d;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interval-box label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #ffcc66;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interval-box input {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row !important;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.faction-card {
|
||||||
|
flex: 1;
|
||||||
background-color: #2c2c3e;
|
background-color: #2c2c3e;
|
||||||
|
padding: 1.5rem;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 0 20px rgba(0,0,0,0.5);
|
box-shadow: 0 0 20px rgba(0,0,0,0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.faction-card h2 {
|
||||||
text-align: center;
|
|
||||||
color: #ffcc00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.faction-section {
|
|
||||||
background-color: #3a3a4d;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.faction-section h2 {
|
|
||||||
color: #66ccff;
|
color: #66ccff;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="number"] {
|
input[type="number"] {
|
||||||
padding: 0.5rem;
|
padding: 0.7rem;
|
||||||
margin-right: 0.5rem;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: none;
|
border: none;
|
||||||
width: 150px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.7rem 1rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: #66ccff;
|
background-color: #66ccff;
|
||||||
@@ -53,3 +84,89 @@ button {
|
|||||||
button:hover {
|
button:hover {
|
||||||
background-color: #3399ff;
|
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; }
|
||||||
|
|||||||
@@ -7,19 +7,34 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<!-- Top bar: Title + Refresh Interval -->
|
||||||
|
<div class="top-bar">
|
||||||
<h1>War Dashboard</h1>
|
<h1>War Dashboard</h1>
|
||||||
|
<div class="interval-box">
|
||||||
<div class="faction-section">
|
<label for="refresh-interval">Refresh Interval (seconds)</label>
|
||||||
<h2>Enemy Faction</h2>
|
<input type="number" id="refresh-interval" value="10" min="1">
|
||||||
<input type="number" id="enemyId" placeholder="Enemy Faction ID">
|
</div>
|
||||||
<input type="number" id="refreshInterval" placeholder="Refresh Interval (sec)">
|
|
||||||
<button onclick="updateEnemy()">Refresh Enemy</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="faction-section">
|
<!-- Faction row -->
|
||||||
|
<div class="faction-row">
|
||||||
|
<!-- Friendly Faction -->
|
||||||
|
<div class="faction-card">
|
||||||
<h2>Friendly Faction</h2>
|
<h2>Friendly Faction</h2>
|
||||||
<input type="number" id="friendlyId" placeholder="Friendly Faction ID">
|
<input type="number" id="friendly-id" placeholder="Faction ID">
|
||||||
<button onclick="updateFriendly()">Refresh Friendly</button>
|
<button id="friendly-populate-btn">Populate Friendly</button>
|
||||||
|
<button id="friendly-status-btn">Start Refresh</button>
|
||||||
|
<div id="friendly-container" class="members-container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Enemy Faction -->
|
||||||
|
<div class="faction-card">
|
||||||
|
<h2>Enemy Faction</h2>
|
||||||
|
<input type="number" id="enemy-id" placeholder="Faction ID">
|
||||||
|
<button id="enemy-populate-btn">Populate Enemy</button>
|
||||||
|
<button id="enemy-status-btn">Start Refresh</button>
|
||||||
|
<div id="enemy-container" class="members-container"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user