|
3 | 3 | from discord import app_commands |
4 | 4 | import aiosqlite |
5 | 5 | from utils.codebuddy_database import DB_PATH |
| 6 | +from utils.codebuddy_database import ( |
| 7 | + add_guild_save_units, |
| 8 | + get_guild_save_units, |
| 9 | + get_user_save_units, |
| 10 | + increment_quest_counting_count, |
| 11 | + try_use_guild_save, |
| 12 | + try_use_user_save, |
| 13 | +) |
6 | 14 | import ast |
7 | 15 | import operator |
8 | | -import random |
9 | 16 | import asyncio |
10 | 17 | import time |
11 | 18 | from typing import Optional |
@@ -454,6 +461,19 @@ async def on_message(self, message): |
454 | 461 | # Side effects after commit to avoid duplicate reactions on retries. |
455 | 462 | self._enqueue_reaction(message, "✅") |
456 | 463 |
|
| 464 | + # Daily quest progress: count 5 numbers (best-effort). |
| 465 | + try: |
| 466 | + quest_completed = await increment_quest_counting_count(message.author.id) |
| 467 | + if quest_completed: |
| 468 | + await message.channel.send( |
| 469 | + f"Daily quest completed, {message.author.mention}! " |
| 470 | + "You earned **0.2** Streak Freeze and **0.5** Save. " |
| 471 | + "Use `?inventory` to check your items.", |
| 472 | + delete_after=15, |
| 473 | + ) |
| 474 | + except Exception: |
| 475 | + pass |
| 476 | + |
457 | 477 | # Highscore marker: react ✅+🏆 when reaching/topping the record |
458 | 478 | if next_count >= high_score: |
459 | 479 | await self._mark_highscore_message(message, next_count, high_score) |
@@ -497,136 +517,136 @@ async def on_message_delete(self, message: discord.Message): |
497 | 517 | ) |
498 | 518 |
|
499 | 519 | async def fail_count(self, message, current_count, reason): |
500 | | - # 1. Send initial message |
501 | | - await message.add_reaction("❌") |
502 | | - status_msg = await message.channel.send( |
503 | | - f"{reason} {message.author.mention} messed up at {current_count}!\n" |
504 | | - "🎲 **Rolling the Dice of Fate...**\n" |
505 | | - "React with 🎲 to help roll! (Need 2 reactions in 60s)" |
506 | | - ) |
507 | | - await status_msg.add_reaction("🎲") |
| 520 | + # Replace dice mechanic with save mechanic: |
| 521 | + # 1) Use a personal save if available. |
| 522 | + # 2) Else use a guild save if available. |
| 523 | + # 3) Else the count is ruined (reset to 0). |
508 | 524 |
|
509 | | - # 2. Wait for reactions |
510 | | - reactions_collected = False |
511 | 525 | try: |
512 | | - end_time = asyncio.get_event_loop().time() + 60 |
513 | | - while True: |
514 | | - # Check current count |
515 | | - status_msg = await message.channel.fetch_message(status_msg.id) |
516 | | - reaction = discord.utils.get(status_msg.reactions, emoji="🎲") |
517 | | - |
518 | | - # If bot reacted, count is at least 1. We need 2 total. |
519 | | - if reaction and reaction.count >= 2: |
520 | | - reactions_collected = True |
521 | | - break |
522 | | - |
523 | | - timeout = end_time - asyncio.get_event_loop().time() |
524 | | - if timeout <= 0: |
525 | | - break |
526 | | - |
527 | | - try: |
528 | | - # Wait for any reaction on this message |
529 | | - await self.bot.wait_for( |
530 | | - 'reaction_add', |
531 | | - check=lambda r, u: r.message.id == status_msg.id and str(r.emoji) == "🎲", |
532 | | - timeout=timeout |
533 | | - ) |
534 | | - except asyncio.TimeoutError: |
535 | | - break |
| 526 | + await message.add_reaction("❌") |
536 | 527 | except Exception: |
537 | | - pass # Proceed if something fails |
| 528 | + pass |
538 | 529 |
|
539 | | - # 3. Determine Outcome |
540 | | - outcome_msg = "" |
541 | | - new_count = 0 |
542 | | - new_last_user_id = None |
543 | | - |
544 | | - dice_db_ops = [] # List of DB operations to perform (query, args) |
| 530 | + if not message.guild: |
| 531 | + return |
545 | 532 |
|
546 | | - if not reactions_collected: |
547 | | - # TIMEOUT / NOT ENOUGH REACTIONS -> RESET |
548 | | - new_count = 0 |
549 | | - new_last_user_id = None |
550 | | - outcome_msg = "⏳ **Time's up!** Not enough people helped roll the dice.\n💥 **Reset!** The count goes back to 0." |
551 | | - |
552 | | - dice_db_ops.append((""" |
553 | | - UPDATE counting_config |
| 533 | + guild_id = message.guild.id |
| 534 | + user_id = message.author.id |
| 535 | + |
| 536 | + # Clear this user's warnings so a saved mistake doesn't soft-lock them. |
| 537 | + try: |
| 538 | + await self._set_warning_count(guild_id, user_id, 0) |
| 539 | + except Exception: |
| 540 | + pass |
| 541 | + |
| 542 | + used_personal = False |
| 543 | + used_guild = False |
| 544 | + try: |
| 545 | + used_personal = await try_use_user_save(user_id) |
| 546 | + except Exception: |
| 547 | + used_personal = False |
| 548 | + |
| 549 | + if not used_personal: |
| 550 | + try: |
| 551 | + used_guild = await try_use_guild_save(guild_id) |
| 552 | + except Exception: |
| 553 | + used_guild = False |
| 554 | + |
| 555 | + if used_personal or used_guild: |
| 556 | + try: |
| 557 | + remaining_user_units = await get_user_save_units(user_id) |
| 558 | + except Exception: |
| 559 | + remaining_user_units = 0 |
| 560 | + try: |
| 561 | + remaining_guild_units = await get_guild_save_units(guild_id) |
| 562 | + except Exception: |
| 563 | + remaining_guild_units = 0 |
| 564 | + |
| 565 | + source = "your" if used_personal else "the server's" |
| 566 | + await message.channel.send( |
| 567 | + f"{reason} {message.author.mention} messed up at **{current_count}**, " |
| 568 | + f"but {source} save was used — the count is **saved**.\n" |
| 569 | + f"Next number is **{current_count + 1}**.\n" |
| 570 | + f"Your saves: **{remaining_user_units/10:.1f}** • Server saves: **{remaining_guild_units/10:.1f}**" |
| 571 | + ) |
| 572 | + return |
| 573 | + |
| 574 | + # No saves: ruin the count (reset to 0) |
| 575 | + db_ops = [ |
| 576 | + ( |
| 577 | + """ |
| 578 | + UPDATE counting_config |
554 | 579 | SET current_count = 0, last_user_id = NULL |
555 | 580 | WHERE guild_id = ? |
556 | | - """, (message.guild.id,))) |
557 | | - |
558 | | - dice_db_ops.append((""" |
| 581 | + """, |
| 582 | + (guild_id,), |
| 583 | + ), |
| 584 | + ( |
| 585 | + """ |
559 | 586 | INSERT INTO counting_stats (user_id, guild_id, total_counts, ruined_counts) |
560 | 587 | VALUES (?, ?, 0, 1) |
561 | 588 | ON CONFLICT(user_id, guild_id) DO UPDATE SET ruined_counts = ruined_counts + 1 |
562 | | - """, (message.author.id, message.guild.id))) |
| 589 | + """, |
| 590 | + (user_id, guild_id), |
| 591 | + ), |
| 592 | + ] |
563 | 593 |
|
564 | | - else: |
565 | | - # REACTIONS COLLECTED -> ROLL DICE |
566 | | - dice_roll = random.randint(1, 6) |
567 | | - outcome_msg = f"🎲 **Dice Roll: {dice_roll}**\n" |
568 | | - |
569 | | - if dice_roll in [2, 4, 6]: |
570 | | - # SAVE |
571 | | - new_count = current_count |
572 | | - outcome_msg += "✨ **Saved!** The count continues!" |
573 | | - # No update to config needed except maybe verifying it? |
574 | | - # Actually if saved, we do NOTHING to counting_config. |
575 | | - elif dice_roll == 3: |
576 | | - # RESET |
577 | | - new_count = 0 |
578 | | - new_last_user_id = None |
579 | | - outcome_msg += "💥 **Reset!** The count goes back to 0." |
580 | | - elif dice_roll == 1: |
581 | | - # -10 Penalty |
582 | | - new_count = max(0, current_count - 10) |
583 | | - new_last_user_id = None |
584 | | - outcome_msg += "🔻 **-10 Penalty!** The count drops by 10." |
585 | | - elif dice_roll == 5: |
586 | | - # -5 Penalty |
587 | | - new_count = max(0, current_count - 5) |
588 | | - new_last_user_id = None |
589 | | - outcome_msg += "🔻 **-5 Penalty!** The count drops by 5." |
590 | | - |
591 | | - if dice_roll not in [2, 4, 6]: |
592 | | - dice_db_ops.append((""" |
593 | | - UPDATE counting_config |
594 | | - SET current_count = ?, last_user_id = ? |
595 | | - WHERE guild_id = ? |
596 | | - """, (new_count, new_last_user_id, message.guild.id))) |
597 | | - |
598 | | - dice_db_ops.append((""" |
599 | | - INSERT INTO counting_stats (user_id, guild_id, total_counts, ruined_counts) |
600 | | - VALUES (?, ?, 0, 1) |
601 | | - ON CONFLICT(user_id, guild_id) DO UPDATE SET ruined_counts = ruined_counts + 1 |
602 | | - """, (message.author.id, message.guild.id))) |
| 594 | + retries = 3 |
| 595 | + while retries > 0: |
| 596 | + try: |
| 597 | + async with aiosqlite.connect(DB_PATH, timeout=30.0) as db: |
| 598 | + for sql, args in db_ops: |
| 599 | + await db.execute(sql, args) |
| 600 | + await db.commit() |
| 601 | + break |
| 602 | + except aiosqlite.OperationalError as e: |
| 603 | + if "locked" in str(e).lower(): |
| 604 | + retries -= 1 |
| 605 | + await asyncio.sleep(0.3) |
| 606 | + continue |
| 607 | + break |
603 | 608 |
|
604 | | - # EXECUTE DB OPS with Retry |
605 | | - if dice_db_ops: |
606 | | - retries = 3 |
607 | | - while retries > 0: |
608 | | - try: |
609 | | - async with aiosqlite.connect(DB_PATH, timeout=30.0) as db: |
610 | | - for sql, args in dice_db_ops: |
611 | | - await db.execute(sql, args) |
612 | | - await db.commit() |
613 | | - break # Success |
614 | | - except aiosqlite.OperationalError as e: |
615 | | - if "locked" in str(e): |
616 | | - retries -= 1 |
617 | | - await asyncio.sleep(0.5) |
618 | | - else: |
619 | | - print(f"Error saving count fail state: {e}") |
620 | | - break |
621 | | - |
622 | | - # 4. Edit message |
623 | | - await status_msg.edit(content=f"{reason} {message.author.mention} messed up at {current_count}!\n{outcome_msg}\nNext number is **{new_count + 1}**.") |
624 | | - |
625 | | - # If the count was actually changed (ruined/reset/penalty), clear warnings and remove highscore marker. |
626 | | - count_ruined = new_count != current_count |
627 | | - if count_ruined and message.guild and isinstance(message.channel, discord.TextChannel): |
628 | | - await self._clear_all_warnings(message.guild.id) |
629 | | - await self._clear_highscore_marker_if_any(message.guild.id, message.channel) |
| 609 | + await message.channel.send( |
| 610 | + f"{reason} {message.author.mention} messed up at **{current_count}**. " |
| 611 | + "No saves were available — the count is **ruined** and has been reset to **0**.\n" |
| 612 | + "Next number is **1**." |
| 613 | + ) |
| 614 | + |
| 615 | + if isinstance(message.channel, discord.TextChannel): |
| 616 | + await self._clear_all_warnings(guild_id) |
| 617 | + await self._clear_highscore_marker_if_any(guild_id, message.channel) |
| 618 | + |
| 619 | + |
| 620 | + @commands.command(name="donateguild", aliases=["dg"]) |
| 621 | + async def donate_guild(self, ctx: commands.Context): |
| 622 | + """Donate 1 personal save to the guild pool (guild receives 0.5 save).""" |
| 623 | + if not ctx.guild: |
| 624 | + return await ctx.send("Server only command.") |
| 625 | + |
| 626 | + user_id = ctx.author.id |
| 627 | + guild_id = ctx.guild.id |
| 628 | + |
| 629 | + # Need at least 1.0 save (10 units) to donate. |
| 630 | + user_units = await get_user_save_units(user_id) |
| 631 | + if user_units < 10: |
| 632 | + return await ctx.send( |
| 633 | + f"You need **1.0** save to donate. Your saves: **{user_units/10:.1f}**" |
| 634 | + ) |
| 635 | + |
| 636 | + # Consume 1.0 personal save |
| 637 | + used = await try_use_user_save(user_id) |
| 638 | + if not used: |
| 639 | + return await ctx.send("Couldn't donate right now (try again).") |
| 640 | + |
| 641 | + # Guild receives 0.5 save (5 units) |
| 642 | + await add_guild_save_units(guild_id, 5) |
| 643 | + |
| 644 | + new_user_units = await get_user_save_units(user_id) |
| 645 | + new_guild_units = await get_guild_save_units(guild_id) |
| 646 | + await ctx.send( |
| 647 | + f"Donated **1.0** save to the server pool. Server gained **0.5** save.\n" |
| 648 | + f"Your saves: **{new_user_units/10:.1f}** • Server saves: **{new_guild_units/10:.1f}**" |
| 649 | + ) |
630 | 650 |
|
631 | 651 | @commands.hybrid_command(name="highscoretable", aliases=["highscores"], help="Show recent counting highscores") |
632 | 652 | async def highscore_table(self, ctx: commands.Context): |
|
0 commit comments