Basic Discord functionality with assignments
This commit is contained in:
@@ -18,4 +18,4 @@ ToDo:
|
|||||||
- add status description to member cards
|
- add status description to member cards
|
||||||
|
|
||||||
|
|
||||||
For now let's pivot to the Discord Bot functionality. What the bot is going to do is for each battle group it needs to assign a friendly member to an enemy member. That friendly will then get a ping in Discord by pulling the list of Discord users and matching the player id to the user in the Discord server. It will ping them and say "New target for @user , attack (link to enemy profile) in the next 60 seconds!" If the enemies status does not change to "In Hospital" in the next 60 seconds that enemy will be assigned to the next player in the group that has not received a hit yet. We will also need to keep track of how many hits a friendly has completed. That way if a new friendly enters a pool they will get a chance to attack before the ones that have not had a chance We also need a button on the webpage to start and stop the bot
|
For now let's pivot to the Discord Bot functionality. What the bot is going to do is for each battle group it needs to assign a friendly member to an enemy member. That friendly will then get a ping in Discord by pulling the list of Discord users and matching the player id to the user in the Discord server. It will ping them and say "New target for @user , attack (link to enemy profile) in the next 30 seconds!" If the enemies status does not change to "In Hospital" in the next 30 seconds that enemy will be assigned to the next player in the group that has not received a hit yet. We will also need to keep track of how many hits a friendly has completed. That way if a new friendly enters a pool they will get a chance to attack before the ones that have not had a chance. We also need a button on the webpage to start and stop the bot
|
||||||
Binary file not shown.
@@ -7,6 +7,9 @@ ALLOWED_CHANNEL_ID = 1442876328536707316
|
|||||||
# FFScouter API
|
# FFScouter API
|
||||||
FFSCOUTER_KEY = "XYmWPO9ZYkLqnv3v"
|
FFSCOUTER_KEY = "XYmWPO9ZYkLqnv3v"
|
||||||
|
|
||||||
|
# Discord Bot
|
||||||
|
DISCORD_TOKEN = "MTQ0Mjg3NjU3NTUzMDg3NzAxMQ.GH7MGP.VdYH4QXmPL-9Zi9zhp-Ot6SmiCxWQOWU3U-1dk"
|
||||||
|
|
||||||
# Intervals
|
# Intervals
|
||||||
POLL_INTERVAL = 30
|
POLL_INTERVAL = 30
|
||||||
HIT_CHECK_INTERVAL = 60
|
HIT_CHECK_INTERVAL = 60
|
||||||
|
|||||||
111
main.py
111
main.py
@@ -1,7 +1,7 @@
|
|||||||
# main.py (updated)
|
# main.py (updated)
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from config import ALLOWED_CHANNEL_ID, HIT_CHECK_INTERVAL, REASSIGN_DELAY
|
from config import ALLOWED_CHANNEL_ID, HIT_CHECK_INTERVAL, REASSIGN_DELAY, DISCORD_TOKEN
|
||||||
from cogs.assignments import Assignments
|
from cogs.assignments import Assignments
|
||||||
from cogs.commands import HitCommands
|
from cogs.commands import HitCommands
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ from pydantic import BaseModel
|
|||||||
from services.server_state import STATE, Member
|
from services.server_state import STATE, Member
|
||||||
|
|
||||||
from services.torn_api import populate_friendly, populate_enemy, start_friendly_status_loop, start_enemy_status_loop
|
from services.torn_api import populate_friendly, populate_enemy, start_friendly_status_loop, start_enemy_status_loop
|
||||||
|
from services.bot_assignment import BotAssignmentManager
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# FastAPI Setup
|
# FastAPI Setup
|
||||||
@@ -56,6 +57,10 @@ class RemoveAssignmentRequest(BaseModel):
|
|||||||
class BotControl(BaseModel):
|
class BotControl(BaseModel):
|
||||||
action: str # "start" or "stop"
|
action: str # "start" or "stop"
|
||||||
|
|
||||||
|
class DiscordMappingRequest(BaseModel):
|
||||||
|
torn_id: int
|
||||||
|
discord_id: int
|
||||||
|
|
||||||
# ================================
|
# ================================
|
||||||
# Helper: load JSON file into STATE
|
# Helper: load JSON file into STATE
|
||||||
# ================================
|
# ================================
|
||||||
@@ -201,13 +206,98 @@ async def api_clear_assignments():
|
|||||||
# =============================
|
# =============================
|
||||||
# Bot control endpoint
|
# Bot control endpoint
|
||||||
# =============================
|
# =============================
|
||||||
|
@app.get("/api/bot_status")
|
||||||
|
async def api_bot_status():
|
||||||
|
"""Get current bot status"""
|
||||||
|
active_count = len(assignment_manager.active_targets) if assignment_manager else 0
|
||||||
|
return {
|
||||||
|
"bot_running": STATE.bot_running,
|
||||||
|
"active_assignments": active_count,
|
||||||
|
"discord_mappings_count": len(assignment_manager.discord_mapping) if assignment_manager else 0
|
||||||
|
}
|
||||||
|
|
||||||
@app.post("/api/bot_control")
|
@app.post("/api/bot_control")
|
||||||
async def api_bot_control(req: BotControl):
|
async def api_bot_control(req: BotControl):
|
||||||
if req.action not in ("start", "stop"):
|
if req.action not in ("start", "stop"):
|
||||||
raise HTTPException(status_code=400, detail="invalid action")
|
raise HTTPException(status_code=400, detail="invalid action")
|
||||||
|
|
||||||
STATE.bot_running = (req.action == "start")
|
STATE.bot_running = (req.action == "start")
|
||||||
|
|
||||||
|
# Start or stop the assignment manager
|
||||||
|
if assignment_manager:
|
||||||
|
if req.action == "start":
|
||||||
|
await assignment_manager.start()
|
||||||
|
else:
|
||||||
|
await assignment_manager.stop()
|
||||||
|
|
||||||
return {"status": "ok", "bot_running": STATE.bot_running}
|
return {"status": "ok", "bot_running": STATE.bot_running}
|
||||||
|
|
||||||
|
# =============================
|
||||||
|
# Discord Mapping endpoints
|
||||||
|
# =============================
|
||||||
|
@app.get("/api/discord_mappings")
|
||||||
|
async def get_discord_mappings():
|
||||||
|
"""Get all Torn ID to Discord ID mappings"""
|
||||||
|
path = Path("data/discord_mapping.json")
|
||||||
|
if not path.exists():
|
||||||
|
return {"mappings": {}}
|
||||||
|
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return {"mappings": data.get("mappings", {})}
|
||||||
|
|
||||||
|
@app.post("/api/discord_mapping")
|
||||||
|
async def add_discord_mapping(req: DiscordMappingRequest):
|
||||||
|
"""Add or update a Torn ID to Discord ID mapping"""
|
||||||
|
path = Path("data/discord_mapping.json")
|
||||||
|
|
||||||
|
# Load existing mappings
|
||||||
|
if path.exists():
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
else:
|
||||||
|
data = {"comment": "Map Torn player IDs to Discord user IDs", "mappings": {}}
|
||||||
|
|
||||||
|
# Update mapping
|
||||||
|
data["mappings"][str(req.torn_id)] = str(req.discord_id)
|
||||||
|
|
||||||
|
# Save back
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
# Reload in assignment manager
|
||||||
|
if assignment_manager:
|
||||||
|
assignment_manager.load_discord_mapping()
|
||||||
|
|
||||||
|
return {"status": "ok", "torn_id": req.torn_id, "discord_id": req.discord_id}
|
||||||
|
|
||||||
|
@app.delete("/api/discord_mapping/{torn_id}")
|
||||||
|
async def remove_discord_mapping(torn_id: int):
|
||||||
|
"""Remove a Discord mapping"""
|
||||||
|
path = Path("data/discord_mapping.json")
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="No mappings found")
|
||||||
|
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# Remove mapping
|
||||||
|
if str(torn_id) in data.get("mappings", {}):
|
||||||
|
del data["mappings"][str(torn_id)]
|
||||||
|
|
||||||
|
# Save back
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
# Reload in assignment manager
|
||||||
|
if assignment_manager:
|
||||||
|
assignment_manager.load_discord_mapping()
|
||||||
|
|
||||||
|
return {"status": "ok", "torn_id": torn_id}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="Mapping not found")
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Reset Groups Endpoint
|
# Reset Groups Endpoint
|
||||||
@@ -257,14 +347,27 @@ class HitDispatchBot(commands.Bot):
|
|||||||
|
|
||||||
bot = HitDispatchBot(command_prefix="!", intents=intents)
|
bot = HitDispatchBot(command_prefix="!", intents=intents)
|
||||||
|
|
||||||
|
# Initialize bot assignment manager
|
||||||
|
assignment_manager = None
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_ready():
|
async def on_ready():
|
||||||
print(f"Logged in as {bot.user.name}")
|
global assignment_manager
|
||||||
|
print(f"✓ Discord bot logged in as {bot.user.name} (ID: {bot.user.id})")
|
||||||
|
print(f"✓ Bot is in {len(bot.guilds)} server(s)")
|
||||||
|
|
||||||
TOKEN = "YOUR_DISCORD_TOKEN"
|
# Initialize assignment manager
|
||||||
|
assignment_manager = BotAssignmentManager(bot)
|
||||||
|
print("✓ Bot assignment manager initialized")
|
||||||
|
|
||||||
async def start_bot():
|
async def start_bot():
|
||||||
await bot.start(TOKEN)
|
try:
|
||||||
|
print("Starting Discord bot...")
|
||||||
|
await bot.start(DISCORD_TOKEN)
|
||||||
|
except discord.LoginFailure:
|
||||||
|
print("ERROR: Invalid Discord token! Please set DISCORD_TOKEN in config.py")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR starting Discord bot: {e}")
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Main Entry Point
|
# Main Entry Point
|
||||||
|
|||||||
BIN
services/__pycache__/bot_assignment.cpython-311.pyc
Normal file
BIN
services/__pycache__/bot_assignment.cpython-311.pyc
Normal file
Binary file not shown.
246
services/bot_assignment.py
Normal file
246
services/bot_assignment.py
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
# services/bot_assignment.py
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
from services.server_state import STATE
|
||||||
|
|
||||||
|
class BotAssignmentManager:
|
||||||
|
def __init__(self, bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.discord_mapping: Dict[int, int] = {} # torn_id -> discord_id
|
||||||
|
self.active_targets: Dict[str, Dict] = {} # key: "group_id:enemy_id" -> assignment data
|
||||||
|
self.running = False
|
||||||
|
self.task = None
|
||||||
|
|
||||||
|
# Load Discord mapping
|
||||||
|
self.load_discord_mapping()
|
||||||
|
|
||||||
|
def load_discord_mapping(self):
|
||||||
|
"""Load Torn ID to Discord ID mapping from JSON file"""
|
||||||
|
path = Path("data/discord_mapping.json")
|
||||||
|
if not path.exists():
|
||||||
|
print("No discord_mapping.json found")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
# Convert string keys to int
|
||||||
|
self.discord_mapping = {int(k): int(v) for k, v in data.get("mappings", {}).items()}
|
||||||
|
print(f"Loaded {len(self.discord_mapping)} Discord mappings")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading discord mapping: {e}")
|
||||||
|
|
||||||
|
def get_discord_id(self, torn_id: int) -> Optional[int]:
|
||||||
|
"""Get Discord user ID for a Torn player ID"""
|
||||||
|
return self.discord_mapping.get(torn_id)
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
"""Start the bot assignment loop"""
|
||||||
|
if self.running:
|
||||||
|
print("⚠ Bot assignment already running")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
self.task = asyncio.create_task(self.assignment_loop())
|
||||||
|
print("✓ Bot assignment loop started")
|
||||||
|
print(f"✓ Loaded {len(self.discord_mapping)} Discord ID mappings")
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""Stop the bot assignment loop"""
|
||||||
|
if not self.running:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.running = False
|
||||||
|
if self.task:
|
||||||
|
self.task.cancel()
|
||||||
|
try:
|
||||||
|
await self.task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
print("Bot assignment stopped")
|
||||||
|
|
||||||
|
def get_next_friendly_in_group(self, group_id: str, friendly_ids: list) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Get the next friendly in the group who should receive a target.
|
||||||
|
Prioritizes members with fewer hits.
|
||||||
|
"""
|
||||||
|
if not friendly_ids:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get hit counts for all friendlies in this group
|
||||||
|
friendly_hits = []
|
||||||
|
for fid in friendly_ids:
|
||||||
|
if fid in STATE.friendly:
|
||||||
|
hits = STATE.friendly[fid].hits
|
||||||
|
friendly_hits.append((fid, hits))
|
||||||
|
|
||||||
|
if not friendly_hits:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Sort by hit count (ascending) - members with fewer hits first
|
||||||
|
friendly_hits.sort(key=lambda x: x[1])
|
||||||
|
|
||||||
|
# Return the friendly with the fewest hits
|
||||||
|
return friendly_hits[0][0]
|
||||||
|
|
||||||
|
def get_next_enemy_in_group(self, group_id: str, enemy_ids: list) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Get the next enemy in the group who needs to be assigned.
|
||||||
|
Returns None if all enemies are already assigned.
|
||||||
|
"""
|
||||||
|
for eid in enemy_ids:
|
||||||
|
key = f"{group_id}:{eid}"
|
||||||
|
# If enemy is not currently assigned, return it
|
||||||
|
if key not in self.active_targets:
|
||||||
|
return eid
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def assignment_loop(self):
|
||||||
|
"""Main loop that assigns targets and monitors status"""
|
||||||
|
await self.bot.wait_until_ready()
|
||||||
|
print("✓ Bot is ready, assignment loop running")
|
||||||
|
|
||||||
|
first_run = True
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
# Check if bot is enabled via STATE
|
||||||
|
if not STATE.bot_running:
|
||||||
|
if first_run:
|
||||||
|
print("⏸ Bot paused - waiting for Start Bot button to be clicked")
|
||||||
|
first_run = False
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if first_run:
|
||||||
|
print("▶ Bot activated - processing assignments")
|
||||||
|
first_run = False
|
||||||
|
|
||||||
|
# Process each group
|
||||||
|
has_assignments = False
|
||||||
|
async with STATE.lock:
|
||||||
|
for group_id, assignments in STATE.groups.items():
|
||||||
|
friendly_ids = assignments.get("friendly", [])
|
||||||
|
enemy_ids = assignments.get("enemy", [])
|
||||||
|
|
||||||
|
if friendly_ids and enemy_ids:
|
||||||
|
has_assignments = True
|
||||||
|
|
||||||
|
if not friendly_ids or not enemy_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Try to assign any unassigned enemies
|
||||||
|
enemy_id = self.get_next_enemy_in_group(group_id, enemy_ids)
|
||||||
|
if enemy_id:
|
||||||
|
friendly_id = self.get_next_friendly_in_group(group_id, friendly_ids)
|
||||||
|
if friendly_id:
|
||||||
|
await self.assign_target(group_id, friendly_id, enemy_id)
|
||||||
|
|
||||||
|
if not has_assignments and STATE.bot_running:
|
||||||
|
print("⚠ No group assignments found - drag members into groups first!")
|
||||||
|
|
||||||
|
# Monitor active targets for status changes or timeouts
|
||||||
|
await self.monitor_active_targets()
|
||||||
|
|
||||||
|
# Sleep before next iteration
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error in assignment loop: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
async def assign_target(self, group_id: str, friendly_id: int, enemy_id: int):
|
||||||
|
"""Assign an enemy target to a friendly player"""
|
||||||
|
# Get member data
|
||||||
|
friendly = STATE.friendly.get(friendly_id)
|
||||||
|
enemy = STATE.enemy.get(enemy_id)
|
||||||
|
|
||||||
|
if not friendly or not enemy:
|
||||||
|
print(f"Cannot assign: friendly {friendly_id} or enemy {enemy_id} not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get Discord user
|
||||||
|
discord_id = self.get_discord_id(friendly_id)
|
||||||
|
if not discord_id:
|
||||||
|
print(f"No Discord mapping for Torn ID {friendly_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
discord_user = await self.bot.fetch_user(discord_id)
|
||||||
|
if not discord_user:
|
||||||
|
print(f"Discord user {discord_id} not found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Record assignment
|
||||||
|
key = f"{group_id}:{enemy_id}"
|
||||||
|
self.active_targets[key] = {
|
||||||
|
"group_id": group_id,
|
||||||
|
"friendly_id": friendly_id,
|
||||||
|
"enemy_id": enemy_id,
|
||||||
|
"discord_id": discord_id,
|
||||||
|
"assigned_at": datetime.now(),
|
||||||
|
"reminded": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send Discord message
|
||||||
|
enemy_link = f"https://www.torn.com/profiles.php?XID={enemy_id}"
|
||||||
|
message = f"🎯 **New target for {discord_user.mention}!**\n\nAttack **{enemy.name}** (Level {enemy.level})\n{enemy_link}\n\n⏰ You have 30 seconds!"
|
||||||
|
|
||||||
|
try:
|
||||||
|
await discord_user.send(message)
|
||||||
|
print(f"Assigned {enemy.name} to {friendly.name} (Discord: {discord_user.name})")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to send Discord message to {discord_user.name}: {e}")
|
||||||
|
|
||||||
|
async def monitor_active_targets(self):
|
||||||
|
"""Monitor active targets for status changes or timeouts"""
|
||||||
|
now = datetime.now()
|
||||||
|
to_reassign = []
|
||||||
|
|
||||||
|
for key, data in list(self.active_targets.items()):
|
||||||
|
elapsed = (now - data["assigned_at"]).total_seconds()
|
||||||
|
|
||||||
|
# Check enemy status
|
||||||
|
enemy_id = data["enemy_id"]
|
||||||
|
enemy = STATE.enemy.get(enemy_id)
|
||||||
|
|
||||||
|
if enemy and "hospital" in enemy.status.lower():
|
||||||
|
# Enemy is hospitalized - success!
|
||||||
|
friendly_id = data["friendly_id"]
|
||||||
|
if friendly_id in STATE.friendly:
|
||||||
|
# Increment hit count
|
||||||
|
STATE.friendly[friendly_id].hits += 1
|
||||||
|
print(f"✓ {STATE.friendly[friendly_id].name} successfully hospitalized {enemy.name}")
|
||||||
|
|
||||||
|
# Remove from active targets
|
||||||
|
del self.active_targets[key]
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Send reminder at 15 seconds
|
||||||
|
if elapsed >= 15 and not data["reminded"]:
|
||||||
|
discord_id = data["discord_id"]
|
||||||
|
try:
|
||||||
|
discord_user = await self.bot.fetch_user(discord_id)
|
||||||
|
await discord_user.send(f"⏰ **Reminder:** Target {enemy.name} - 15 seconds left!")
|
||||||
|
data["reminded"] = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Reassign after 30 seconds
|
||||||
|
if elapsed >= 30:
|
||||||
|
to_reassign.append((data["group_id"], enemy_id))
|
||||||
|
del self.active_targets[key]
|
||||||
|
|
||||||
|
# Reassign targets that timed out
|
||||||
|
for group_id, enemy_id in to_reassign:
|
||||||
|
friendly_ids = STATE.groups[group_id].get("friendly", [])
|
||||||
|
friendly_id = self.get_next_friendly_in_group(group_id, friendly_ids)
|
||||||
|
if friendly_id:
|
||||||
|
print(f"⚠ Reassigning enemy {enemy_id} (timeout)")
|
||||||
|
await self.assign_target(group_id, friendly_id, enemy_id)
|
||||||
|
|
||||||
|
# Global instance (will be initialized with bot in main.py)
|
||||||
|
assignment_manager: Optional[BotAssignmentManager] = None
|
||||||
@@ -658,6 +658,39 @@ async function toggleEnemyStatus() {
|
|||||||
btn.textContent = "Stop Refresh";
|
btn.textContent = "Stop Refresh";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// Bot control (start/stop)
|
||||||
|
// ---------------------------
|
||||||
|
async function toggleBotControl() {
|
||||||
|
const btn = document.getElementById("bot-control-btn");
|
||||||
|
const isRunning = btn.dataset.running === "true";
|
||||||
|
const action = isRunning ? "stop" : "start";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/bot_control", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ action })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error("Bot control failed:", res.status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
// Update button state
|
||||||
|
btn.dataset.running = data.bot_running ? "true" : "false";
|
||||||
|
btn.textContent = data.bot_running ? "Stop Bot" : "Start Bot";
|
||||||
|
btn.style.backgroundColor = data.bot_running ? "#ff4444" : "#4CAF50";
|
||||||
|
|
||||||
|
console.log(`Bot ${data.bot_running ? "started" : "stopped"}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("toggleBotControl error:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Reset groups (server-side)
|
// Reset groups (server-side)
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@@ -682,6 +715,10 @@ function wireUp() {
|
|||||||
if (enemyBtn) enemyBtn.addEventListener("click", populateEnemy);
|
if (enemyBtn) enemyBtn.addEventListener("click", populateEnemy);
|
||||||
document.getElementById("friendly-status-btn").addEventListener("click", toggleFriendlyStatus);
|
document.getElementById("friendly-status-btn").addEventListener("click", toggleFriendlyStatus);
|
||||||
document.getElementById("enemy-status-btn").addEventListener("click", toggleEnemyStatus);
|
document.getElementById("enemy-status-btn").addEventListener("click", toggleEnemyStatus);
|
||||||
|
|
||||||
|
const botBtn = document.getElementById("bot-control-btn");
|
||||||
|
if (botBtn) botBtn.addEventListener("click", toggleBotControl);
|
||||||
|
|
||||||
const resetBtn = document.getElementById("reset-groups-btn");
|
const resetBtn = document.getElementById("reset-groups-btn");
|
||||||
if (resetBtn) resetBtn.addEventListener("click", resetGroups);
|
if (resetBtn) resetBtn.addEventListener("click", resetGroups);
|
||||||
|
|
||||||
|
|||||||
@@ -198,6 +198,42 @@ button {
|
|||||||
}
|
}
|
||||||
button:hover { background-color: #3399ff; }
|
button:hover { background-color: #3399ff; }
|
||||||
|
|
||||||
|
/* Bot control button */
|
||||||
|
.bot-btn {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-btn[data-running="true"] {
|
||||||
|
background-color: #ff4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset button */
|
||||||
|
.reset-btn {
|
||||||
|
background-color: #ff8800;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn:hover {
|
||||||
|
background-color: #ff6600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top controls layout */
|
||||||
|
.top-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* scrollbar niceties for drop zones and lists */
|
/* scrollbar niceties for drop zones and lists */
|
||||||
.member-list::-webkit-scrollbar, .drop-zone::-webkit-scrollbar { width: 8px; height: 8px; }
|
.member-list::-webkit-scrollbar, .drop-zone::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||||
.member-list::-webkit-scrollbar-thumb, .drop-zone::-webkit-scrollbar-thumb { background: #66ccff; border-radius: 4px; }
|
.member-list::-webkit-scrollbar-thumb, .drop-zone::-webkit-scrollbar-thumb { background: #66ccff; border-radius: 4px; }
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<h1>War Dashboard</h1>
|
<h1>War Dashboard</h1>
|
||||||
|
|
||||||
<div class="top-controls">
|
<div class="top-controls">
|
||||||
|
<button id="bot-control-btn" class="bot-btn" data-running="false">Start Bot</button>
|
||||||
<button id="reset-groups-btn" class="reset-btn">Reset Groups</button>
|
<button id="reset-groups-btn" class="reset-btn">Reset Groups</button>
|
||||||
|
|
||||||
<div class="interval-box">
|
<div class="interval-box">
|
||||||
|
|||||||
Reference in New Issue
Block a user