From 6f693dc19cb74dd8dbe09e66fc7ec00d94d95693 Mon Sep 17 00:00:00 2001 From: Pengu Date: Mon, 9 Feb 2026 14:08:41 -0600 Subject: [PATCH] Eh, kinda working --- .env.example | 14 +++ bot.py | 26 +++++ commands.py | 264 +++++++++++++++++++++++++++++++++++++++++++++++ constants.py | 23 +++++ db.py | 43 ++++++++ helpers.py | 11 ++ requirements.txt | 3 + 7 files changed, 384 insertions(+) create mode 100644 .env.example create mode 100644 bot.py create mode 100644 commands.py create mode 100644 constants.py create mode 100644 db.py create mode 100644 helpers.py create mode 100644 requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4e04086 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +DISCORD_TOKEN=YOUR_DISCORD_BOT_TOKEN + +MYSQL_HOST=localhost +MYSQL_USER=root +MYSQL_PASSWORD=password +MYSQL_DATABASE=momentum + +SITUPS=3 +BICEP_CURLS=4 +PUSHUPS=5 +SQUATS=5 +LUNGES=2 +RUSSIAN_TWISTS=3 +WALL_PUSHUPS=5 diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..eaab7b8 --- /dev/null +++ b/bot.py @@ -0,0 +1,26 @@ +import discord +from discord import app_commands +from dotenv import load_dotenv +import os + +from db import init_db +from commands import register + +load_dotenv() + +class Momentum(discord.Client): + def __init__(self): + super().__init__(intents=discord.Intents.default()) + self.tree = app_commands.CommandTree(self) + + async def setup_hook(self): + register(self.tree) + await self.tree.sync() + print("🌐 Commands synced") + + async def on_ready(self): + print(f"🔥 Momentum online as {self.user}") + init_db() + +bot = Momentum() +bot.run(os.getenv("DISCORD_TOKEN")) diff --git a/commands.py b/commands.py new file mode 100644 index 0000000..fca5e48 --- /dev/null +++ b/commands.py @@ -0,0 +1,264 @@ +import discord +from discord import app_commands +from db import get_db +from constants import VALORANT_RANKS, WORKOUTS +from helpers import apply_rr +import random + +def register(tree: app_commands.CommandTree): + + # ------------------- + # /create + # ------------------- + @tree.command(name="create", description="Link your Valorant account") + @app_commands.describe( + val_tag="VALUSERNAME#TAG", + rank="Your current Valorant rank", + rr="Current RR (0-99)" + ) + @app_commands.choices( + rank=[app_commands.Choice(name=r, value=r) for r in VALORANT_RANKS] + ) + async def create( + interaction: discord.Interaction, + val_tag: str, + rank: str, + rr: int + ): + db = get_db() + cur = db.cursor() + + cur.execute(""" + INSERT INTO users (discord_id, val_tag, rank, rr) + VALUES (%s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + val_tag=VALUES(val_tag), + rank=VALUES(rank), + rr=VALUES(rr) + """, (str(interaction.user.id), val_tag, rank, rr)) + + cur.close() + db.close() + + await interaction.response.send_message("✅ Account linked", ephemeral=True) + + # ------------------- + # /match + # ------------------- + enabled_workouts = [ + app_commands.Choice(name=name, value=name) + for name, value in WORKOUTS.items() + if value > 0 + ] + + @tree.command(name="match", description="Log a Valorant match") + @app_commands.describe( + result="Match result", + rr_change="RR gained or lost", + kd="Kills/Deaths (e.g. 20/15)", + workout="Workout assigned for deaths" + ) + @app_commands.choices( + result=[ + app_commands.Choice(name="WIN", value="WIN"), + app_commands.Choice(name="LOSS", value="LOSS") + ], + workout=enabled_workouts + ) + async def match( + interaction: discord.Interaction, + result: str, + rr_change: int, + kd: str, + workout: str + ): + try: + kills, deaths = map(int, kd.split("/")) + except ValueError: + await interaction.response.send_message( + "❌ K/D must be in format `kills/deaths` (e.g. 20/15)", + ephemeral=True + ) + return + + db = get_db() + cur = db.cursor(dictionary=True) + + cur.execute( + "SELECT * FROM users WHERE discord_id=%s", + (str(interaction.user.id),) + ) + user = cur.fetchone() + + if not user: + await interaction.response.send_message( + "❌ You must run `/create` first", + ephemeral=True + ) + return + + new_rank, new_rr = apply_rr(user["rank"], user["rr"], rr_change) + + per_death = WORKOUTS[workout] + workout_total = deaths * per_death + + cur.execute(""" + UPDATE users + SET + kills = kills + %s, + deaths = deaths + %s, + rank = %s, + rr = %s + WHERE discord_id = %s + """, (kills, deaths, new_rank, new_rr, str(interaction.user.id))) + + cur.execute(""" + INSERT INTO workouts (discord_id, workout, amount) + VALUES (%s, %s, %s) + ON DUPLICATE KEY UPDATE amount = amount + %s + """, (str(interaction.user.id), workout, workout_total, workout_total)) + + cur.close() + db.close() + + await interaction.response.send_message( + f"🏆 Match logged\n" + f"**Rank:** {new_rank} ({new_rr} RR)\n" + f"**Workout:** {workout} × {workout_total}" + ) + + # ------------------- + # /stats + # ------------------- + @tree.command(name="stats", description="Show Momentum stats") + async def stats( + interaction: discord.Interaction, + user: discord.User | None = None + ): + target = user or interaction.user + + db = get_db() + cur = db.cursor(dictionary=True) + + cur.execute( + "SELECT * FROM users WHERE discord_id=%s", + (str(target.id),) + ) + u = cur.fetchone() + + if not u: + await interaction.response.send_message( + "❌ User not found", + ephemeral=True + ) + return + + cur.execute( + "SELECT workout, amount FROM workouts WHERE discord_id=%s", + (str(target.id),) + ) + workout_rows = cur.fetchall() + + embed = discord.Embed( + title=f"{target.display_name}'s Momentum Stats", + color=discord.Color.red() + ) + + embed.add_field( + name="Rank", + value=f"{u['rank']} ({u['rr']} RR)", + inline=False + ) + embed.add_field( + name="Valorant", + value=u["val_tag"], + inline=False + ) + embed.add_field( + name="Kills / Deaths", + value=f"{u['kills']} / {u['deaths']}", + inline=False + ) + + workout_text = ( + "\n".join(f"{w['workout']}: {w['amount']}" for w in workout_rows) + if workout_rows else "None" + ) + + embed.add_field( + name="Workouts Completed", + value=workout_text, + inline=False + ) + + cur.close() + db.close() + + await interaction.response.send_message(embed=embed) + + # ------------------- + # /workout + # ------------------- + @tree.command(name="workout", description="Get a random workout") + async def workout(interaction: discord.Interaction): + enabled = [w for w, v in WORKOUTS.items() if v > 0] + choice = random.choice(enabled) + + await interaction.response.send_message( + f"💪 **{choice}**\n" + f"{WORKOUTS[choice]} per death" + ) + + # ------------------- + # /edit (admin) + # ------------------- + @tree.command(name="edit", description="Edit a user's stats (Admin)") + @app_commands.choices( + rank=[app_commands.Choice(name=r, value=r) for r in VALORANT_RANKS] + ) + async def edit( + interaction: discord.Interaction, + user: discord.User, + rank: str | None = None, + rr: int | None = None, + val_tag: str | None = None + ): + if not interaction.user.guild_permissions.administrator: + await interaction.response.send_message( + "❌ Admins only", + ephemeral=True + ) + return + + fields = [] + values = [] + + if rank: + fields.append("rank=%s") + values.append(rank) + if rr is not None: + fields.append("rr=%s") + values.append(rr) + if val_tag: + fields.append("val_tag=%s") + values.append(val_tag) + + if not fields: + await interaction.response.send_message( + "Nothing to update", + ephemeral=True + ) + return + + values.append(str(user.id)) + + db = get_db() + cur = db.cursor() + cur.execute( + f"UPDATE users SET {', '.join(fields)} WHERE discord_id=%s", + values + ) + cur.close() + db.close() + + await interaction.response.send_message("✏️ User updated") diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..3f17a2b --- /dev/null +++ b/constants.py @@ -0,0 +1,23 @@ +import os + +VALORANT_RANKS = [ + "Iron 1","Iron 2","Iron 3", + "Bronze 1","Bronze 2","Bronze 3", + "Silver 1","Silver 2","Silver 3", + "Gold 1","Gold 2","Gold 3", + "Platinum 1","Platinum 2","Platinum 3", + "Diamond 1","Diamond 2","Diamond 3", + "Ascendant 1","Ascendant 2","Ascendant 3", + "Immortal 1","Immortal 2","Immortal 3", + "Radiant" +] + +WORKOUTS = { + "Sit Ups": int(os.getenv("SITUPS", 0)), + "Bicep Curls": int(os.getenv("BICEP_CURLS", 0)), + "Pushups": int(os.getenv("PUSHUPS", 0)), + "Squats": int(os.getenv("SQUATS", 0)), + "Lunges": int(os.getenv("LUNGES", 0)), + "Russian Twists": int(os.getenv("RUSSIAN_TWISTS", 0)), + "Wall Pushups": int(os.getenv("WALL_PUSHUPS", 0)), +} diff --git a/db.py b/db.py new file mode 100644 index 0000000..bead079 --- /dev/null +++ b/db.py @@ -0,0 +1,43 @@ +import mysql.connector +import os + +def get_db(): + return mysql.connector.connect( + host=os.getenv("MYSQL_HOST"), + user=os.getenv("MYSQL_USER"), + password=os.getenv("MYSQL_PASSWORD"), + database=os.getenv("MYSQL_DATABASE"), + autocommit=True + ) + +def init_db(): + db = get_db() + cur = db.cursor() + + cur.execute(""" + CREATE TABLE IF NOT EXISTS users ( + discord_id VARCHAR(32) PRIMARY KEY, + val_tag VARCHAR(32) NOT NULL, + rank VARCHAR(32) NOT NULL, + rr INT DEFAULT 0, + kills INT DEFAULT 0, + deaths INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + + cur.execute(""" + CREATE TABLE IF NOT EXISTS workouts ( + discord_id VARCHAR(32), + workout VARCHAR(32), + amount INT DEFAULT 0, + PRIMARY KEY (discord_id, workout), + FOREIGN KEY (discord_id) + REFERENCES users(discord_id) + ON DELETE CASCADE + ) + """) + + cur.close() + db.close() + print("✅ Database ready") diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000..a8c2c52 --- /dev/null +++ b/helpers.py @@ -0,0 +1,11 @@ +from constants import VALORANT_RANKS + +def apply_rr(rank: str, rr: int, change: int): + rr += change + idx = VALORANT_RANKS.index(rank) + + while rr >= 100 and idx < len(VALORANT_RANKS) - 1: + rr -= 100 + idx += 1 + + return VALORANT_RANKS[idx], rr diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a8f3dc3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +discord.py>=2.3.2 +mysql-connector-python>=8.3.0 +python-dotenv>=1.0.1