From 055abd501ffa86899d675e04efbf81eb72a53fef Mon Sep 17 00:00:00 2001 From: jerick Date: Sun, 25 Jan 2026 18:56:11 -0500 Subject: [PATCH] Basic Discord functionality with assignments --- README.md | 2 +- __pycache__/config.cpython-311.pyc | Bin 462 -> 563 bytes config.py | 3 + main.py | 111 +++++++- .../bot_assignment.cpython-311.pyc | Bin 0 -> 13360 bytes services/bot_assignment.py | 246 ++++++++++++++++++ static/dashboard.js | 37 +++ static/styles.css | 36 +++ templates/dashboard.html | 1 + 9 files changed, 431 insertions(+), 5 deletions(-) create mode 100644 services/__pycache__/bot_assignment.cpython-311.pyc create mode 100644 services/bot_assignment.py diff --git a/README.md b/README.md index 475a7f8..e38c101 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,4 @@ ToDo: - 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 \ No newline at end of file +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 \ No newline at end of file diff --git a/__pycache__/config.cpython-311.pyc b/__pycache__/config.cpython-311.pyc index 4411a3c544e808f71b3c46f285cf5013e0083f27..977635ff2ff9f21e2bd1f79f7b2b8d2f56628e04 100644 GIT binary patch delta 178 zcmX@dyqSe}IWI340}$}9Ez4}0$ScXHHBnuiBZ?!1Gnhe>YhsXXOqGXkNT7jlR=TlY zR;aOGNNAO>3y@vqSm7I}=k8(d>mHyNmJ;b<5*U#i;G=69m1$X(QK0KzViug6>0A*W z=pP5K#dn{4_ZyE)3&!@eFqM4|0hQ@%MK1oA|Y#OBg7}2*kw> UllvGonW7mdZ)B8Y5d?|=07`x^F#rGn delta 99 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 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 diff --git a/static/dashboard.js b/static/dashboard.js index 09ca1e4..c76c0cf 100644 --- a/static/dashboard.js +++ b/static/dashboard.js @@ -658,6 +658,39 @@ async function toggleEnemyStatus() { 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) // --------------------------- @@ -682,6 +715,10 @@ function wireUp() { if (enemyBtn) enemyBtn.addEventListener("click", populateEnemy); document.getElementById("friendly-status-btn").addEventListener("click", toggleFriendlyStatus); 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"); if (resetBtn) resetBtn.addEventListener("click", resetGroups); diff --git a/static/styles.css b/static/styles.css index 6237fc8..33bf8a9 100644 --- a/static/styles.css +++ b/static/styles.css @@ -198,6 +198,42 @@ button { } 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 */ .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; } diff --git a/templates/dashboard.html b/templates/dashboard.html index ca53085..a2212bd 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -13,6 +13,7 @@

War Dashboard

+