diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e1b8326..4588336 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,11 +10,7 @@ "Bash(pip3 install:*)", "Bash(apt list:*)", "Bash(curl:*)", - "Bash(git commit:*)", - "Bash(sed:*)", - "Bash(grep:*)", - "Bash(pkill:*)", - "Bash(git add:*)" + "Bash(git commit:*)" ], "deny": [] } diff --git a/config/items.json b/config/items.json index d8a039c..0912108 100644 --- a/config/items.json +++ b/config/items.json @@ -48,28 +48,6 @@ "effect_value": 15, "locations": ["mystic_forest", "enchanted_grove"], "spawn_rate": 0.03 - }, - { - "id": 18, - "name": "Revive", - "description": "Revives a fainted pet and restores 50% of its HP", - "rarity": "rare", - "category": "healing", - "effect": "revive", - "effect_value": 50, - "locations": ["all"], - "spawn_rate": 0.005 - }, - { - "id": 19, - "name": "Max Revive", - "description": "Revives a fainted pet and fully restores its HP", - "rarity": "epic", - "category": "healing", - "effect": "max_revive", - "effect_value": 100, - "locations": ["all"], - "spawn_rate": 0.002 } ], "battle_items": [ diff --git a/config/locations.json b/config/locations.json index 532f29e..0285d02 100644 --- a/config/locations.json +++ b/config/locations.json @@ -5,11 +5,9 @@ "level_min": 1, "level_max": 3, "spawns": [ - {"species": "Leafy", "spawn_rate": 0.25, "min_level": 1, "max_level": 2}, - {"species": "Flamey", "spawn_rate": 0.25, "min_level": 1, "max_level": 2}, - {"species": "Aqua", "spawn_rate": 0.25, "min_level": 1, "max_level": 2}, - {"species": "Seedling", "spawn_rate": 0.15, "min_level": 1, "max_level": 2}, - {"species": "Furry", "spawn_rate": 0.1, "min_level": 1, "max_level": 3} + {"species": "Leafy", "spawn_rate": 0.35, "min_level": 1, "max_level": 2}, + {"species": "Flamey", "spawn_rate": 0.35, "min_level": 1, "max_level": 2}, + {"species": "Aqua", "spawn_rate": 0.3, "min_level": 1, "max_level": 2} ] }, { @@ -18,13 +16,10 @@ "level_min": 2, "level_max": 6, "spawns": [ - {"species": "Leafy", "spawn_rate": 0.2, "min_level": 2, "max_level": 4}, - {"species": "Vinewrap", "spawn_rate": 0.25, "min_level": 3, "max_level": 5}, - {"species": "Bloomtail", "spawn_rate": 0.2, "min_level": 4, "max_level": 6}, - {"species": "Flamey", "spawn_rate": 0.08, "min_level": 3, "max_level": 4}, - {"species": "Fernwhisk", "spawn_rate": 0.15, "min_level": 3, "max_level": 5}, - {"species": "Furry", "spawn_rate": 0.08, "min_level": 2, "max_level": 4}, - {"species": "Mossrock", "spawn_rate": 0.04, "min_level": 5, "max_level": 6} + {"species": "Leafy", "spawn_rate": 0.3, "min_level": 2, "max_level": 4}, + {"species": "Vinewrap", "spawn_rate": 0.35, "min_level": 3, "max_level": 5}, + {"species": "Bloomtail", "spawn_rate": 0.25, "min_level": 4, "max_level": 6}, + {"species": "Flamey", "spawn_rate": 0.1, "min_level": 3, "max_level": 4} ] }, { @@ -33,11 +28,8 @@ "level_min": 4, "level_max": 9, "spawns": [ - {"species": "Sparky", "spawn_rate": 0.35, "min_level": 4, "max_level": 7}, - {"species": "Rocky", "spawn_rate": 0.25, "min_level": 5, "max_level": 8}, - {"species": "Zapper", "spawn_rate": 0.25, "min_level": 4, "max_level": 6}, - {"species": "Ember", "spawn_rate": 0.1, "min_level": 4, "max_level": 6}, - {"species": "Swiftpaw", "spawn_rate": 0.05, "min_level": 6, "max_level": 8} + {"species": "Sparky", "spawn_rate": 0.6, "min_level": 4, "max_level": 7}, + {"species": "Rocky", "spawn_rate": 0.4, "min_level": 5, "max_level": 8} ] }, { @@ -46,11 +38,8 @@ "level_min": 6, "level_max": 12, "spawns": [ - {"species": "Rocky", "spawn_rate": 0.4, "min_level": 6, "max_level": 10}, - {"species": "Sparky", "spawn_rate": 0.2, "min_level": 7, "max_level": 9}, - {"species": "Pebble", "spawn_rate": 0.25, "min_level": 6, "max_level": 8}, - {"species": "Crystalback", "spawn_rate": 0.1, "min_level": 9, "max_level": 12}, - {"species": "Voltmane", "spawn_rate": 0.05, "min_level": 10, "max_level": 12} + {"species": "Rocky", "spawn_rate": 0.7, "min_level": 6, "max_level": 10}, + {"species": "Sparky", "spawn_rate": 0.3, "min_level": 7, "max_level": 9} ] }, { @@ -59,13 +48,9 @@ "level_min": 10, "level_max": 16, "spawns": [ - {"species": "Hydrox", "spawn_rate": 0.25, "min_level": 10, "max_level": 14}, - {"species": "Rocky", "spawn_rate": 0.2, "min_level": 11, "max_level": 15}, - {"species": "Sparky", "spawn_rate": 0.15, "min_level": 12, "max_level": 14}, - {"species": "Snowball", "spawn_rate": 0.2, "min_level": 10, "max_level": 12}, - {"species": "Frostbite", "spawn_rate": 0.1, "min_level": 12, "max_level": 15}, - {"species": "Bubblin", "spawn_rate": 0.05, "min_level": 10, "max_level": 13}, - {"species": "Frostleaf", "spawn_rate": 0.05, "min_level": 14, "max_level": 16} + {"species": "Hydrox", "spawn_rate": 0.4, "min_level": 10, "max_level": 14}, + {"species": "Rocky", "spawn_rate": 0.3, "min_level": 11, "max_level": 15}, + {"species": "Sparky", "spawn_rate": 0.3, "min_level": 12, "max_level": 14} ] }, { @@ -74,19 +59,9 @@ "level_min": 15, "level_max": 25, "spawns": [ - {"species": "Blazeon", "spawn_rate": 0.22, "min_level": 15, "max_level": 20}, - {"species": "Hydrox", "spawn_rate": 0.18, "min_level": 16, "max_level": 22}, - {"species": "Rocky", "spawn_rate": 0.13, "min_level": 18, "max_level": 25}, - {"species": "Scorchclaw", "spawn_rate": 0.07, "min_level": 15, "max_level": 18}, - {"species": "Tidalfin", "spawn_rate": 0.07, "min_level": 16, "max_level": 19}, - {"species": "Infernowyrm", "spawn_rate": 0.05, "min_level": 20, "max_level": 25}, - {"species": "Abyssal", "spawn_rate": 0.05, "min_level": 20, "max_level": 25}, - {"species": "Thornking", "spawn_rate": 0.05, "min_level": 20, "max_level": 25}, - {"species": "Stormcaller", "spawn_rate": 0.05, "min_level": 20, "max_level": 25}, - {"species": "Steamvent", "spawn_rate": 0.04, "min_level": 19, "max_level": 23}, - {"species": "Mountainlord", "spawn_rate": 0.03, "min_level": 22, "max_level": 25}, - {"species": "Glaciarch", "spawn_rate": 0.03, "min_level": 22, "max_level": 25}, - {"species": "Harmonix", "spawn_rate": 0.03, "min_level": 18, "max_level": 22} + {"species": "Blazeon", "spawn_rate": 0.5, "min_level": 15, "max_level": 20}, + {"species": "Hydrox", "spawn_rate": 0.3, "min_level": 16, "max_level": 22}, + {"species": "Rocky", "spawn_rate": 0.2, "min_level": 18, "max_level": 25} ] } ] \ No newline at end of file diff --git a/config/pets.json b/config/pets.json index 0457d61..e2d40fe 100644 --- a/config/pets.json +++ b/config/pets.json @@ -8,8 +8,7 @@ "base_defense": 43, "base_speed": 65, "evolution_level": null, - "rarity": 1, - "emoji": "šŸ”„" + "rarity": 1 }, { "name": "Aqua", @@ -20,8 +19,7 @@ "base_defense": 65, "base_speed": 43, "evolution_level": null, - "rarity": 1, - "emoji": "šŸ’§" + "rarity": 1 }, { "name": "Leafy", @@ -32,8 +30,7 @@ "base_defense": 49, "base_speed": 45, "evolution_level": null, - "rarity": 1, - "emoji": "šŸƒ" + "rarity": 1 }, { "name": "Sparky", @@ -44,8 +41,7 @@ "base_defense": 40, "base_speed": 90, "evolution_level": null, - "rarity": 2, - "emoji": "⚔" + "rarity": 2 }, { "name": "Rocky", @@ -56,8 +52,7 @@ "base_defense": 100, "base_speed": 25, "evolution_level": null, - "rarity": 2, - "emoji": "šŸ—æ" + "rarity": 2 }, { "name": "Blazeon", @@ -68,8 +63,7 @@ "base_defense": 60, "base_speed": 95, "evolution_level": null, - "rarity": 3, - "emoji": "šŸŒ‹" + "rarity": 3 }, { "name": "Hydrox", @@ -80,8 +74,7 @@ "base_defense": 90, "base_speed": 60, "evolution_level": null, - "rarity": 3, - "emoji": "🌊" + "rarity": 3 }, { "name": "Vinewrap", @@ -92,8 +85,7 @@ "base_defense": 70, "base_speed": 40, "evolution_level": null, - "rarity": 2, - "emoji": "🌿" + "rarity": 2 }, { "name": "Bloomtail", @@ -104,295 +96,6 @@ "base_defense": 50, "base_speed": 80, "evolution_level": null, - "rarity": 2, - "emoji": "🌺" - }, - { - "name": "Ember", - "type1": "Fire", - "type2": null, - "base_hp": 42, - "base_attack": 50, - "base_defense": 40, - "base_speed": 68, - "evolution_level": null, - "rarity": 1, - "emoji": "✨" - }, - { - "name": "Scorchclaw", - "type1": "Fire", - "type2": null, - "base_hp": 55, - "base_attack": 75, - "base_defense": 55, - "base_speed": 70, - "evolution_level": null, - "rarity": 2, - "emoji": "🐱" - }, - { - "name": "Infernowyrm", - "type1": "Fire", - "type2": null, - "base_hp": 90, - "base_attack": 120, - "base_defense": 75, - "base_speed": 85, - "evolution_level": null, - "rarity": 4, - "emoji": "šŸ‰" - }, - { - "name": "Bubblin", - "type1": "Water", - "type2": null, - "base_hp": 48, - "base_attack": 40, - "base_defense": 60, - "base_speed": 52, - "evolution_level": null, - "rarity": 1, - "emoji": "🫧" - }, - { - "name": "Tidalfin", - "type1": "Water", - "type2": null, - "base_hp": 65, - "base_attack": 60, - "base_defense": 70, - "base_speed": 80, - "evolution_level": null, - "rarity": 2, - "emoji": "🐬" - }, - { - "name": "Abyssal", - "type1": "Water", - "type2": null, - "base_hp": 100, - "base_attack": 85, - "base_defense": 110, - "base_speed": 55, - "evolution_level": null, - "rarity": 4, - "emoji": "šŸ™" - }, - { - "name": "Seedling", - "type1": "Grass", - "type2": null, - "base_hp": 40, - "base_attack": 35, - "base_defense": 50, - "base_speed": 40, - "evolution_level": null, - "rarity": 1, - "emoji": "🌱" - }, - { - "name": "Fernwhisk", - "type1": "Grass", - "type2": null, - "base_hp": 50, - "base_attack": 55, - "base_defense": 65, - "base_speed": 75, - "evolution_level": null, - "rarity": 2, - "emoji": "🌾" - }, - { - "name": "Thornking", - "type1": "Grass", - "type2": null, - "base_hp": 85, - "base_attack": 95, - "base_defense": 120, - "base_speed": 50, - "evolution_level": null, - "rarity": 4, - "emoji": "šŸ‘‘" - }, - { - "name": "Zapper", - "type1": "Electric", - "type2": null, - "base_hp": 30, - "base_attack": 45, - "base_defense": 35, - "base_speed": 95, - "evolution_level": null, - "rarity": 1, - "emoji": "🐭" - }, - { - "name": "Voltmane", - "type1": "Electric", - "type2": null, - "base_hp": 60, - "base_attack": 85, - "base_defense": 50, - "base_speed": 110, - "evolution_level": null, - "rarity": 3, - "emoji": "šŸŽ" - }, - { - "name": "Stormcaller", - "type1": "Electric", - "type2": null, - "base_hp": 75, - "base_attack": 130, - "base_defense": 60, - "base_speed": 125, - "evolution_level": null, - "rarity": 4, - "emoji": "šŸ¦…" - }, - { - "name": "Pebble", - "type1": "Rock", - "type2": null, - "base_hp": 45, - "base_attack": 60, - "base_defense": 80, - "base_speed": 20, - "evolution_level": null, - "rarity": 1, - "emoji": "🪨" - }, - { - "name": "Crystalback", - "type1": "Rock", - "type2": null, - "base_hp": 70, - "base_attack": 90, - "base_defense": 130, - "base_speed": 35, - "evolution_level": null, - "rarity": 3, - "emoji": "🐢" - }, - { - "name": "Mountainlord", - "type1": "Rock", - "type2": null, - "base_hp": 120, - "base_attack": 110, - "base_defense": 150, - "base_speed": 20, - "evolution_level": null, - "rarity": 4, - "emoji": "ā›°ļø" - }, - { - "name": "Snowball", - "type1": "Ice", - "type2": null, - "base_hp": 40, - "base_attack": 35, - "base_defense": 55, - "base_speed": 45, - "evolution_level": null, - "rarity": 1, - "emoji": "ā˜ƒļø" - }, - { - "name": "Frostbite", - "type1": "Ice", - "type2": null, - "base_hp": 55, - "base_attack": 65, - "base_defense": 70, - "base_speed": 85, - "evolution_level": null, - "rarity": 2, - "emoji": "🦨" - }, - { - "name": "Glaciarch", - "type1": "Ice", - "type2": null, - "base_hp": 95, - "base_attack": 80, - "base_defense": 130, - "base_speed": 45, - "evolution_level": null, - "rarity": 4, - "emoji": "ā„ļø" - }, - { - "name": "Furry", - "type1": "Normal", - "type2": null, - "base_hp": 50, - "base_attack": 45, - "base_defense": 45, - "base_speed": 60, - "evolution_level": null, - "rarity": 1, - "emoji": "🐹" - }, - { - "name": "Swiftpaw", - "type1": "Normal", - "type2": null, - "base_hp": 55, - "base_attack": 70, - "base_defense": 50, - "base_speed": 100, - "evolution_level": null, - "rarity": 2, - "emoji": "🐺" - }, - { - "name": "Harmonix", - "type1": "Normal", - "type2": null, - "base_hp": 80, - "base_attack": 75, - "base_defense": 75, - "base_speed": 80, - "evolution_level": null, - "rarity": 3, - "emoji": "šŸŽµ" - }, - { - "name": "Steamvent", - "type1": "Water", - "type2": "Fire", - "base_hp": 65, - "base_attack": 80, - "base_defense": 70, - "base_speed": 75, - "evolution_level": null, - "rarity": 3, - "emoji": "šŸ’Ø" - }, - { - "name": "Mossrock", - "type1": "Grass", - "type2": "Rock", - "base_hp": 70, - "base_attack": 65, - "base_defense": 100, - "base_speed": 40, - "evolution_level": null, - "rarity": 3, - "emoji": "šŸ„" - }, - { - "name": "Frostleaf", - "type1": "Ice", - "type2": "Grass", - "base_hp": 60, - "base_attack": 55, - "base_defense": 85, - "base_speed": 65, - "evolution_level": null, - "rarity": 3, - "emoji": "🧊" + "rarity": 2 } ] \ No newline at end of file diff --git a/help.html b/help.html index 0bbcf30..0b3bb5d 100644 --- a/help.html +++ b/help.html @@ -214,112 +214,6 @@ .gym-card strong { color: var(--text-accent); } - - /* Quick Navigation */ - .quick-nav { - background: var(--bg-secondary); - border-radius: 15px; - padding: 20px; - margin-bottom: 30px; - box-shadow: var(--shadow-dark); - border: 1px solid var(--border-color); - } - - .quick-nav h3 { - color: var(--text-accent); - margin: 0 0 15px 0; - font-size: 1.2em; - text-align: center; - } - - .nav-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 10px; - } - - .nav-item { - background: var(--bg-tertiary); - padding: 12px 16px; - border-radius: 8px; - text-align: center; - transition: all 0.3s ease; - border: 1px solid var(--border-color); - } - - .nav-item:hover { - background: var(--hover-color); - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(102, 255, 102, 0.15); - } - - .nav-item a { - color: var(--text-primary); - text-decoration: none; - font-weight: 500; - font-size: 0.9em; - display: block; - } - - .nav-item a:hover { - color: var(--text-accent); - } - - /* Smooth scrolling for anchor links */ - html { - scroll-behavior: smooth; - } - - /* Section anchor targets */ - .section { - scroll-margin-top: 20px; - } - - /* Back to top button */ - .back-to-top { - position: fixed; - bottom: 30px; - right: 30px; - width: 50px; - height: 50px; - background: var(--gradient-primary); - color: white; - border: none; - border-radius: 50%; - font-size: 18px; - cursor: pointer; - opacity: 0; - visibility: hidden; - transition: all 0.3s ease; - z-index: 1000; - box-shadow: 0 4px 15px rgba(0,0,0,0.3); - } - - .back-to-top.show { - opacity: 1; - visibility: visible; - } - - .back-to-top:hover { - transform: translateY(-3px); - box-shadow: 0 6px 20px rgba(0,0,0,0.4); - } - - /* Mobile responsiveness for navigation */ - @media (max-width: 768px) { - .nav-grid { - grid-template-columns: repeat(2, 1fr); - gap: 8px; - } - - .nav-item { - padding: 10px 12px; - } - - .nav-item a { - font-size: 0.8em; - } - } @@ -330,25 +224,7 @@

Complete guide to Pokemon-style pet collecting in IRC

-
-

🧭 Quick Navigation

- -
- -
+
šŸš€ Getting Started
@@ -371,7 +247,7 @@
-
+
šŸŒ Exploration & Travel
@@ -395,11 +271,6 @@
See which location you're currently in and get information about the area.
Example: !where
-
-
!wild [location]
-
Show wild pets available in your current location or a specified location. Helps you see what pets you can encounter.
-
Example: !wild or !wild crystal caves
-
@@ -428,7 +299,7 @@
-
+
āš”ļø Battle System
@@ -458,20 +329,10 @@
Example: !flee
- -
-

šŸ’€ Pet Fainting System

-
    -
  • Battle Defeat - Pets that lose battles will faint and cannot be used until healed
  • -
  • Healing Options - Use Revive items, !heal command, or wait 30 minutes for auto-recovery
  • -
  • Strategic Impact - Plan your battles carefully to avoid having all pets faint
  • -
  • Type Advantages - Use type matchups to win battles and avoid fainting
  • -
-
-
+
šŸ›ļø Gym Battles NEW!
@@ -495,11 +356,6 @@
Get detailed information about a gym including leader, theme, team, and badge details.
Example: !gym info "Storm Master"
-
-
!forfeit
-
Forfeit your current gym battle if you're losing or want to try a different strategy.
-
Example: !forfeit
-
@@ -550,7 +406,7 @@
-
+
🐾 Pet Management
@@ -574,17 +430,12 @@
Remove a pet from your active team and put it in storage.
Example: !deactivate aqua
-
-
!nickname <pet> <new_nickname>
-
Give a custom nickname to one of your pets. Use their current name or ID to reference them.
-
Example: !nickname flamey "Blazer"
-
-
-
šŸŽ’ Inventory & Healing System UPDATED!
+
+
šŸŽ’ Inventory System NEW!
@@ -594,13 +445,8 @@
!use <item name>
-
Use a consumable item from your inventory. Items can heal pets, boost stats, revive fainted pets, or provide other benefits.
-
Example: !use Small Potion, !use Revive
-
-
-
!heal
-
Heal all your active pets to full health. Has a 1-hour cooldown to prevent abuse.
-
Example: !heal
+
Use a consumable item from your inventory. Items can heal pets, boost stats, or provide other benefits.
+
Example: !use Small Potion
@@ -609,8 +455,8 @@
  • ā—‹ Common (15%) - Small Potions, basic healing items
  • ā—‡ Uncommon (8-12%) - Large Potions, battle boosters, special berries
  • -
  • ā—† Rare (3-6%) - Super Potions, Revive items, speed elixirs, location treasures
  • -
  • ā˜… Epic (2-3%) - Max Revive, evolution stones, rare crystals, ancient artifacts
  • +
  • ā—† Rare (3-6%) - Super Potions, speed elixirs, location treasures
  • +
  • ā˜… Epic (2-3%) - Evolution stones, rare crystals, ancient artifacts
  • ✦ Legendary (1%) - Lucky charms, ancient fossils, ultimate items
@@ -618,74 +464,9 @@
šŸ’” Item Discovery: Find items while exploring! Each location has unique treasures. Items stack in your inventory and can be used anytime.
- -
-

šŸ„ Pet Healing System

-
    -
  • Fainted Pets - Pets that lose battles faint and cannot be used until healed
  • -
  • Revive Items - Use Revive (50% HP) or Max Revive (100% HP) to restore fainted pets
  • -
  • !heal Command - Heals all active pets to full health (1-hour cooldown)
  • -
  • Auto-Recovery - Fainted pets automatically recover to 1 HP after 30 minutes
  • -
  • Travel Allowed - You can still travel and explore with fainted pets
  • -
-
-
-
šŸŽÆ Community Events NEW!
-
-
-
-
!events
-
View all active community events that all players can participate in together.
-
Example: !events
-
-
-
!event <id>
-
Get detailed information about a specific community event, including progress and leaderboard.
-
Example: !event 1
-
-
-
!contribute <id>
-
Contribute to a community event. Everyone who participates gets rewards when the event completes!
-
Example: !contribute 1
-
-
-
!eventhelp
-
Get detailed help about the community events system and how it works.
-
Example: !eventhelp
-
-
- -
-

šŸ¤ How Community Events Work

-
    -
  • Collaborative Goals - All players work together toward shared objectives
  • -
  • Progress Tracking - Events show progress bars and contribution leaderboards
  • -
  • Time Limited - Events have deadlines and expire if not completed
  • -
  • Difficulty Levels - ⭐ Easy, ⭐⭐ Medium, ⭐⭐⭐ Hard events with better rewards
  • -
  • Automatic Spawning - New events appear regularly for ongoing engagement
  • -
-
- -
-

šŸŽŖ Event Types

-
    -
  • šŸŖ Resource Gathering - Help collect supplies for the community
  • -
  • 🐾 Pet Rescue - Search for and rescue missing pets
  • -
  • šŸŽŖ Community Projects - Work together on town improvement projects
  • -
  • 🚨 Emergency Response - Help during natural disasters or crises
  • -
  • šŸ”¬ Research - Assist scientists with important discoveries
  • -
-
- -
- šŸ’” Event Strategy: Check !events regularly for new opportunities! Higher difficulty events give better rewards, and contributing more increases your reward multiplier when the event completes. -
-
-
- -
+
šŸ† Achievements & Progress
@@ -708,7 +489,7 @@
-
+
🌐 Web Interface
@@ -718,47 +499,12 @@
  • Leaderboard - Top players by level and achievements
  • Locations Guide - All areas with spawn information
  • Gym Badges - Display your earned badges and progress
  • -
  • Interactive Map - See where all players are exploring
  • -
  • Team Builder - Drag-and-drop team management with PIN verification
  • -
    -
    šŸ¤– Bot Status & Utilities
    -
    -
    -
    -
    !status
    -
    Check the bot's current connection status and basic system information.
    -
    Example: !status
    -
    -
    -
    !uptime
    -
    See how long the bot has been running since last restart.
    -
    Example: !uptime
    -
    -
    -
    !ping
    -
    Test the bot's responsiveness with a simple ping-pong test.
    -
    Example: !ping
    -
    -
    - -
    -

    šŸ”§ System Status

    -
      -
    • Connection Monitoring - Bot automatically monitors its IRC connection
    • -
    • Auto-Reconnect - Automatically reconnects if connection is lost
    • -
    • Background Tasks - Weather updates, event spawning, and data validation
    • -
    • Rate Limiting - Built-in protection against spam and abuse
    • -
    -
    -
    -
    - -
    +
    ⚔ Rate Limiting & Fair Play
    @@ -827,92 +573,12 @@
    -
    -
    šŸ›”ļø Admin Commands
    -
    -
    -
    -
    !reload
    -
    Reload all bot modules without restarting the bot (Admin only).
    -
    Example: !reload
    -
    -
    -
    !setweather <location|all> <weather_type> [duration]
    -
    Manually set weather for a location or all locations (Admin only).
    -
    Example: !setweather all sunny 120
    -
    -
    -
    !spawnevent [difficulty]
    -
    Force spawn a community event with optional difficulty 1-3 (Admin only).
    -
    Example: !spawnevent 2
    -
    -
    -
    !startevent [type] [difficulty]
    -
    Start a specific event type. Without args, shows available types (Admin only).
    -
    Example: !startevent resource_gathering 2
    -
    -
    - -
    -

    šŸ”‘ Admin Access

    -
      -
    • Single Admin User - Only one designated admin user can use these commands
    • -
    • Module Management - Reload modules without restarting the entire bot
    • -
    • Weather Control - Force weather changes for testing or events
    • -
    • Event Management - Spawn community events on demand
    • -
    -
    - -
    - āš ļø Admin Note: These commands require admin privileges and can affect the entire bot system. Use with caution and always test changes in a development environment first. -
    -
    -
    - - - - - - \ No newline at end of file diff --git a/modules/__init__.py b/modules/__init__.py index 78738c3..3090135 100644 --- a/modules/__init__.py +++ b/modules/__init__.py @@ -10,7 +10,6 @@ from .admin import Admin from .inventory import Inventory from .gym_battles import GymBattles from .team_builder import TeamBuilder -from .npc_events import NPCEventsModule __all__ = [ 'CoreCommands', @@ -21,6 +20,5 @@ __all__ = [ 'Admin', 'Inventory', 'GymBattles', - 'TeamBuilder', - 'NPCEventsModule' + 'TeamBuilder' ] \ No newline at end of file diff --git a/modules/admin.py b/modules/admin.py index 0d9a36f..8631ade 100644 --- a/modules/admin.py +++ b/modules/admin.py @@ -21,7 +21,7 @@ class Admin(BaseModule): """Handles admin-only commands like reload""" def get_commands(self): - return ["reload", "rate_stats", "rate_user", "rate_unban", "rate_reset", "weather", "setweather", "spawnevent", "startevent", "status", "uptime", "ping", "heal"] + return ["reload", "rate_stats", "rate_user", "rate_unban", "rate_reset", "weather", "setweather"] async def handle_command(self, channel, nickname, command, args): if command == "reload": @@ -38,18 +38,6 @@ class Admin(BaseModule): await self.cmd_weather(channel, nickname, args) elif command == "setweather": await self.cmd_setweather(channel, nickname, args) - elif command == "spawnevent": - await self.cmd_spawnevent(channel, nickname, args) - elif command == "startevent": - await self.cmd_startevent(channel, nickname, args) - elif command == "status": - await self.cmd_status(channel, nickname) - elif command == "uptime": - await self.cmd_uptime(channel, nickname) - elif command == "ping": - await self.cmd_ping(channel, nickname) - elif command == "heal": - await self.cmd_heal(channel, nickname) async def cmd_reload(self, channel, nickname): """Reload bot modules (admin only)""" @@ -329,182 +317,4 @@ class Admin(BaseModule): except ValueError as e: self.send_message(channel, f"{nickname}: āŒ Invalid duration: {str(e)}") except Exception as e: - self.send_message(channel, f"{nickname}: āŒ Error setting weather: {str(e)}") - - async def cmd_spawnevent(self, channel, nickname, args): - """Force spawn an NPC event (admin only)""" - if not self.is_admin(nickname): - self.send_message(channel, f"{nickname}: Access denied. Admin command.") - return - - # Default to difficulty 1 if no args provided - difficulty = 1 - if args: - try: - difficulty = int(args[0]) - if difficulty not in [1, 2, 3]: - self.send_message(channel, f"{nickname}: āŒ Difficulty must be 1, 2, or 3.") - return - except ValueError: - self.send_message(channel, f"{nickname}: āŒ Invalid difficulty. Use 1, 2, or 3.") - return - - try: - # Get the NPC events manager from the bot - if hasattr(self.bot, 'npc_events') and self.bot.npc_events: - event_id = await self.bot.npc_events.force_spawn_event(difficulty) - if event_id: - self.send_message(channel, f"šŸŽÆ {nickname}: Spawned new NPC event! Check `!events` to see it.") - else: - self.send_message(channel, f"āŒ {nickname}: Failed to spawn NPC event.") - else: - self.send_message(channel, f"āŒ {nickname}: NPC events system not available.") - - except Exception as e: - self.send_message(channel, f"{nickname}: āŒ Error spawning event: {str(e)}") - - async def cmd_startevent(self, channel, nickname, args): - """Start a specific event type (admin only)""" - if not self.is_admin(nickname): - self.send_message(channel, f"{nickname}: Access denied. Admin command.") - return - - # If no args provided, show available event types - if not args: - self.send_message(channel, f"{nickname}: Available types: resource_gathering, pet_rescue, community_project, emergency_response, festival_preparation, research_expedition, crisis_response, legendary_encounter, ancient_mystery") - return - - event_type = args[0].lower() - valid_types = ["resource_gathering", "pet_rescue", "community_project", "emergency_response", - "festival_preparation", "research_expedition", "crisis_response", - "legendary_encounter", "ancient_mystery"] - - if event_type not in valid_types: - self.send_message(channel, f"{nickname}: āŒ Invalid type. Available: {', '.join(valid_types)}") - return - - # Optional difficulty parameter - difficulty = 1 - if len(args) > 1: - try: - difficulty = int(args[1]) - if difficulty not in [1, 2, 3]: - self.send_message(channel, f"{nickname}: āŒ Difficulty must be 1, 2, or 3.") - return - except ValueError: - self.send_message(channel, f"{nickname}: āŒ Invalid difficulty. Use 1, 2, or 3.") - return - - try: - # Get the NPC events manager from the bot - if hasattr(self.bot, 'npc_events') and self.bot.npc_events: - event_id = await self.bot.npc_events.force_spawn_specific_event(event_type, difficulty) - if event_id: - self.send_message(channel, f"šŸŽÆ {nickname}: Started {event_type} event (ID: {event_id})! Check `!events` to see it.") - else: - self.send_message(channel, f"āŒ {nickname}: Failed to start {event_type} event.") - else: - self.send_message(channel, f"āŒ {nickname}: NPC events system not available.") - - except Exception as e: - self.send_message(channel, f"{nickname}: āŒ Error starting event: {str(e)}") - - async def cmd_status(self, channel, nickname): - """Show bot connection status (available to all users)""" - try: - # Check if connection manager exists - if hasattr(self.bot, 'connection_manager') and self.bot.connection_manager: - stats = self.bot.connection_manager.get_connection_stats() - connected = stats.get('connected', False) - state = stats.get('state', 'unknown') - - status_emoji = "🟢" if connected else "šŸ”“" - self.send_message(channel, f"{status_emoji} {nickname}: Bot status - {state.upper()}") - else: - self.send_message(channel, f"🟢 {nickname}: Bot is running") - - except Exception as e: - self.send_message(channel, f"{nickname}: āŒ Error getting status: {str(e)}") - - async def cmd_uptime(self, channel, nickname): - """Show bot uptime (available to all users)""" - try: - # Check if bot has startup time - if hasattr(self.bot, 'startup_time'): - import datetime - uptime = datetime.datetime.now() - self.bot.startup_time - days = uptime.days - hours, remainder = divmod(uptime.seconds, 3600) - minutes, seconds = divmod(remainder, 60) - - if days > 0: - uptime_str = f"{days}d {hours}h {minutes}m" - elif hours > 0: - uptime_str = f"{hours}h {minutes}m" - else: - uptime_str = f"{minutes}m {seconds}s" - - self.send_message(channel, f"ā±ļø {nickname}: Bot uptime - {uptime_str}") - else: - self.send_message(channel, f"ā±ļø {nickname}: Bot is running (uptime unknown)") - - except Exception as e: - self.send_message(channel, f"{nickname}: āŒ Error getting uptime: {str(e)}") - - async def cmd_ping(self, channel, nickname): - """Test bot responsiveness (available to all users)""" - try: - import time - start_time = time.time() - - # Simple responsiveness test - response_time = (time.time() - start_time) * 1000 # Convert to milliseconds - - self.send_message(channel, f"šŸ“ {nickname}: Pong! Response time: {response_time:.1f}ms") - - except Exception as e: - self.send_message(channel, f"{nickname}: āŒ Error with ping: {str(e)}") - - async def cmd_heal(self, channel, nickname): - """Heal active pets (available to all users with 1-hour cooldown)""" - try: - player = await self.require_player(channel, nickname) - if not player: - return - - # Check cooldown - from datetime import datetime, timedelta - last_heal = await self.database.get_last_heal_time(player["id"]) - if last_heal: - time_since_heal = datetime.now() - last_heal - if time_since_heal < timedelta(hours=1): - remaining = timedelta(hours=1) - time_since_heal - minutes_remaining = int(remaining.total_seconds() / 60) - self.send_message(channel, f"ā° {nickname}: Heal command is on cooldown! {minutes_remaining} minutes remaining.") - return - - # Get active pets - active_pets = await self.database.get_active_pets(player["id"]) - if not active_pets: - self.send_message(channel, f"āŒ {nickname}: You don't have any active pets to heal!") - return - - # Count how many pets need healing - pets_healed = 0 - for pet in active_pets: - if pet["hp"] < pet["max_hp"]: - # Heal pet to full HP - await self.database.update_pet_hp(pet["id"], pet["max_hp"]) - pets_healed += 1 - - if pets_healed == 0: - self.send_message(channel, f"āœ… {nickname}: All your active pets are already at full health!") - return - - # Update cooldown - await self.database.update_last_heal_time(player["id"]) - - self.send_message(channel, f"šŸ’Š {nickname}: Healed {pets_healed} pet{'s' if pets_healed != 1 else ''} to full health! Next heal available in 1 hour.") - - except Exception as e: - self.send_message(channel, f"{nickname}: āŒ Error with heal command: {str(e)}") \ No newline at end of file + self.send_message(channel, f"{nickname}: āŒ Error setting weather: {str(e)}") \ No newline at end of file diff --git a/modules/battle_system.py b/modules/battle_system.py index 639bdd3..911392f 100644 --- a/modules/battle_system.py +++ b/modules/battle_system.py @@ -148,12 +148,6 @@ class BattleSystem(BaseModule): del self.bot.active_encounters[player["id"]] else: self.send_message(channel, f"šŸ’€ {nickname}: Your pet fainted! You lost the battle...") - - # Mark pet as fainted in database - pets = await self.database.get_player_pets(player["id"], active_only=True) - if pets: - await self.database.faint_pet(pets[0]["id"]) - # Remove encounter if player["id"] in self.bot.active_encounters: del self.bot.active_encounters[player["id"]] @@ -299,11 +293,6 @@ class BattleSystem(BaseModule): # Player lost gym battle result = await self.database.end_gym_battle(player["id"], victory=False) - # Mark pet as fainted in database - pets = await self.database.get_player_pets(player["id"], active_only=True) - if pets: - await self.database.faint_pet(pets[0]["id"]) - self.send_message(channel, f"šŸ’€ {nickname}: Your pet fainted!") self.send_message(channel, f"{gym_battle['badge_icon']} {gym_battle['leader_name']}: \"Good battle! Train more and come back stronger!\"") diff --git a/modules/inventory.py b/modules/inventory.py index df6cc1e..227e882 100644 --- a/modules/inventory.py +++ b/modules/inventory.py @@ -99,41 +99,6 @@ class Inventory(BaseModule): self.send_message(channel, f"šŸ€ {nickname}: Used {item['name']}! Rare pet encounter rate increased by {effect_value}% for 1 hour!") - elif effect == "revive": - # Handle revive items for fainted pets - fainted_pets = await self.database.get_fainted_pets(player["id"]) - if not fainted_pets: - self.send_message(channel, f"āŒ {nickname}: You don't have any fainted pets to revive!") - return - - # Use the first fainted pet (can be expanded to choose specific pet) - pet = fainted_pets[0] - new_hp = int(pet["max_hp"] * (effect_value / 100.0)) # Convert percentage to HP - - # Revive the pet and restore HP - await self.database.revive_pet(pet["id"], new_hp) - - self.send_message(channel, - f"šŸ’« {nickname}: Used {item['name']} on {pet['nickname'] or pet['species_name']}! " - f"Revived and restored {new_hp}/{pet['max_hp']} HP!") - - elif effect == "max_revive": - # Handle max revive items for fainted pets - fainted_pets = await self.database.get_fainted_pets(player["id"]) - if not fainted_pets: - self.send_message(channel, f"āŒ {nickname}: You don't have any fainted pets to revive!") - return - - # Use the first fainted pet (can be expanded to choose specific pet) - pet = fainted_pets[0] - - # Revive the pet and fully restore HP - await self.database.revive_pet(pet["id"], pet["max_hp"]) - - self.send_message(channel, - f"✨ {nickname}: Used {item['name']} on {pet['nickname'] or pet['species_name']}! " - f"Revived and fully restored HP! ({pet['max_hp']}/{pet['max_hp']})") - elif effect == "money": # Handle money items (like Coin Pouch) import random diff --git a/modules/npc_events.py b/modules/npc_events.py deleted file mode 100644 index 49f8d20..0000000 --- a/modules/npc_events.py +++ /dev/null @@ -1,236 +0,0 @@ -""" -NPC Events Module -Handles player commands for NPC events system -""" - -from modules.base_module import BaseModule -from src.npc_events import NPCEventsManager -from datetime import datetime - -class NPCEventsModule(BaseModule): - """Module for NPC events system commands""" - - def __init__(self, bot, database, game_engine): - super().__init__(bot, database, game_engine) - self.events_manager = NPCEventsManager(database) - - def get_commands(self): - return ['events', 'event', 'help', 'contribute', 'eventhelp'] - - async def handle_command(self, command, channel, nickname, args): - """Handle NPC events commands""" - - # Normalize command - command = self.normalize_input(command) - - if command == 'events': - await self.cmd_events(channel, nickname) - elif command == 'event': - await self.cmd_event(channel, nickname, args) - elif command == 'help' and len(args) > 0 and args[0].lower() == 'events': - await self.cmd_event_help(channel, nickname) - elif command == 'contribute': - await self.cmd_contribute(channel, nickname, args) - elif command == 'eventhelp': - await self.cmd_event_help(channel, nickname) - - async def cmd_events(self, channel, nickname): - """Show all active NPC events""" - try: - active_events = await self.events_manager.get_active_events() - - if not active_events: - self.send_message(channel, "šŸ“… No active community events at the moment. Check back later!") - return - - message = "šŸŽÆ **Active Community Events:**\n" - - for event in active_events: - progress = self.events_manager.get_progress_bar( - event['current_contributions'], - event['target_contributions'] - ) - - # Calculate time remaining - expires_at = datetime.fromisoformat(event['expires_at']) - time_left = expires_at - datetime.now() - - if time_left.total_seconds() > 0: - hours_left = int(time_left.total_seconds() / 3600) - minutes_left = int((time_left.total_seconds() % 3600) / 60) - time_str = f"{hours_left}h {minutes_left}m" - else: - time_str = "Expired" - - difficulty_stars = "⭐" * event['difficulty'] - - message += f"\n**#{event['id']} - {event['title']}** {difficulty_stars}\n" - message += f"šŸ“ {event['description']}\n" - message += f"šŸ“Š Progress: {progress}\n" - message += f"ā° Time left: {time_str}\n" - message += f"šŸ’° Reward: {event['reward_experience']} XP, ${event['reward_money']}\n" - message += f"šŸ¤ Use `!contribute {event['id']}` to help!\n" - - self.send_message(channel, message) - - except Exception as e: - print(f"Error in cmd_events: {e}") - self.send_message(channel, f"āŒ Error fetching events: {str(e)}") - - async def cmd_event(self, channel, nickname, args): - """Show details for a specific event""" - if not args: - self.send_message(channel, "āŒ Usage: !event ") - return - - try: - event_id = int(args[0]) - event = await self.events_manager.get_event_details(event_id) - - if not event: - self.send_message(channel, f"āŒ Event #{event_id} not found or expired.") - return - - # Calculate time remaining - expires_at = datetime.fromisoformat(event['expires_at']) - time_left = expires_at - datetime.now() - - if time_left.total_seconds() > 0: - hours_left = int(time_left.total_seconds() / 3600) - minutes_left = int((time_left.total_seconds() % 3600) / 60) - time_str = f"{hours_left}h {minutes_left}m" - else: - time_str = "Expired" - - progress = self.events_manager.get_progress_bar( - event['current_contributions'], - event['target_contributions'] - ) - - difficulty_stars = "⭐" * event['difficulty'] - status_emoji = "šŸ”„" if event['status'] == 'active' else "āœ…" if event['status'] == 'completed' else "āŒ" - - message = f"{status_emoji} **Event #{event['id']}: {event['title']}** {difficulty_stars}\n" - message += f"šŸ“ {event['description']}\n" - message += f"šŸ“Š Progress: {progress}\n" - message += f"ā° Time left: {time_str}\n" - message += f"šŸ’° Reward: {event['reward_experience']} XP, ${event['reward_money']}\n" - message += f"šŸ† Status: {event['status'].title()}\n" - - # Show leaderboard if there are contributors - if event['leaderboard']: - message += "\nšŸ… **Top Contributors:**\n" - for i, contributor in enumerate(event['leaderboard'][:5]): # Top 5 - rank_emoji = ["šŸ„‡", "🄈", "šŸ„‰", "4ļøāƒ£", "5ļøāƒ£"][i] - message += f"{rank_emoji} {contributor['nickname']}: {contributor['contributions']} contributions\n" - - if event['status'] == 'active': - message += f"\nšŸ¤ Use `!contribute {event['id']}` to help!" - - self.send_message(channel, message) - - except ValueError: - self.send_message(channel, "āŒ Invalid event ID. Please use a number.") - except Exception as e: - print(f"Error in cmd_event: {e}") - self.send_message(channel, f"āŒ Error fetching event details: {str(e)}") - - async def cmd_contribute(self, channel, nickname, args): - """Allow player to contribute to an event""" - if not args: - self.send_message(channel, "āŒ Usage: !contribute ") - return - - player = await self.require_player(channel, nickname) - if not player: - return - - try: - event_id = int(args[0]) - - # Check if event exists and is active - event = await self.events_manager.get_event_details(event_id) - if not event: - self.send_message(channel, f"āŒ Event #{event_id} not found or expired.") - return - - if event['status'] != 'active': - self.send_message(channel, f"āŒ Event #{event_id} is not active.") - return - - # Check if event has expired - expires_at = datetime.fromisoformat(event['expires_at']) - if datetime.now() >= expires_at: - self.send_message(channel, f"āŒ Event #{event_id} has expired.") - return - - # Add contribution - result = await self.events_manager.contribute_to_event(event_id, player['id']) - - if not result['success']: - self.send_message(channel, f"āŒ Failed to contribute: {result.get('error', 'Unknown error')}") - return - - # Get updated event details - updated_event = await self.events_manager.get_event_details(event_id) - player_contributions = await self.events_manager.get_player_contributions(player['id'], event_id) - - progress = self.events_manager.get_progress_bar( - updated_event['current_contributions'], - updated_event['target_contributions'] - ) - - if result['event_completed']: - self.send_message(channel, f"šŸŽ‰ **EVENT COMPLETED!** {updated_event['completion_message']}") - self.send_message(channel, f"šŸ† Thanks to everyone who participated! Rewards will be distributed shortly.") - else: - self.send_message(channel, f"āœ… {nickname} contributed to '{updated_event['title']}'!") - self.send_message(channel, f"šŸ“Š Progress: {progress}") - self.send_message(channel, f"šŸ¤ Your total contributions: {player_contributions}") - - # Show encouragement based on progress - progress_percent = (updated_event['current_contributions'] / updated_event['target_contributions']) * 100 - if progress_percent >= 75: - self.send_message(channel, "šŸ”„ Almost there! Keep it up!") - elif progress_percent >= 50: - self.send_message(channel, "šŸ’Ŗ Great progress! We're halfway there!") - elif progress_percent >= 25: - self.send_message(channel, "🌟 Good start! Keep contributing!") - - except ValueError: - self.send_message(channel, "āŒ Invalid event ID. Please use a number.") - except Exception as e: - print(f"Error in cmd_contribute: {e}") - self.send_message(channel, f"āŒ Error contributing to event: {str(e)}") - - async def cmd_event_help(self, channel, nickname): - """Show help for NPC events system""" - message = """šŸŽÆ **Community Events System Help** - -**Available Commands:** -• `!events` - Show all active community events -• `!event ` - Show details for a specific event -• `!contribute ` - Contribute to an event -• `!eventhelp` - Show this help message - -**How Events Work:** -🌟 Random community events spawn regularly -šŸ¤ All players can contribute to the same events -šŸ“Š Events have progress bars and time limits -šŸ† Everyone who contributes gets rewards when completed -⭐ Events have different difficulty levels (1-3 stars) - -**Event Types:** -• šŸŖ Resource Gathering - Help collect supplies -• 🐾 Pet Rescue - Search for missing pets -• šŸŽŖ Community Projects - Work together on town projects -• 🚨 Emergency Response - Help during crises -• šŸ”¬ Research - Assist with scientific discoveries - -**Tips:** -• Check `!events` regularly for new opportunities -• Higher difficulty events give better rewards -• Contributing more increases your reward multiplier -• Events expire after their time limit""" - - self.send_message(channel, message) \ No newline at end of file diff --git a/run_bot_debug.py b/run_bot_debug.py index 15d14e2..b245824 100644 --- a/run_bot_debug.py +++ b/run_bot_debug.py @@ -57,10 +57,6 @@ class PetBotDebug: loop.run_until_complete(self.validate_all_player_data()) print("āœ… Player data validation complete") - print("šŸ”„ Validating database integrity...") - loop.run_until_complete(self.validate_database_integrity()) - print("āœ… Database integrity validation complete") - print("šŸ”„ Starting background validation task...") self.start_background_validation(loop) print("āœ… Background validation started") @@ -146,79 +142,6 @@ class PetBotDebug: print(f"āŒ Error during player data validation: {e}") # Don't fail startup if validation fails - async def validate_database_integrity(self): - """Validate database integrity and fix common issues""" - try: - import aiosqlite - async with aiosqlite.connect(self.database.db_path) as db: - # Check for orphaned pets - cursor = await db.execute(""" - SELECT COUNT(*) FROM pets - WHERE species_id NOT IN (SELECT id FROM pet_species) - """) - orphaned_pets = (await cursor.fetchone())[0] - - if orphaned_pets > 0: - print(f"āš ļø Found {orphaned_pets} orphaned pets - fixing references...") - # This should not happen with the new startup logic, but just in case - await self.fix_orphaned_pets(db) - - # Check player data accessibility - cursor = await db.execute("SELECT id, nickname FROM players") - players = await cursor.fetchall() - - total_accessible_pets = 0 - for player_id, nickname in players: - cursor = await db.execute(""" - SELECT COUNT(*) FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? - """, (player_id,)) - accessible_pets = (await cursor.fetchone())[0] - total_accessible_pets += accessible_pets - - if accessible_pets > 0: - print(f" āœ… {nickname}: {accessible_pets} pets accessible") - else: - # Get total pets for this player - cursor = await db.execute("SELECT COUNT(*) FROM pets WHERE player_id = ?", (player_id,)) - total_pets = (await cursor.fetchone())[0] - if total_pets > 0: - print(f" āš ļø {nickname}: {total_pets} pets but 0 accessible (orphaned)") - - print(f"āœ… Database integrity check: {total_accessible_pets} total accessible pets") - - except Exception as e: - print(f"āŒ Database integrity validation failed: {e}") - - async def fix_orphaned_pets(self, db): - """Fix orphaned pet references (emergency fallback)""" - try: - # This is a simplified fix - map common species names to current IDs - common_species = ['Flamey', 'Aqua', 'Leafy', 'Vinewrap', 'Bloomtail', 'Furry'] - - for species_name in common_species: - cursor = await db.execute("SELECT id FROM pet_species WHERE name = ?", (species_name,)) - species_row = await cursor.fetchone() - if species_row: - current_id = species_row[0] - # Update any pets that might be referencing old IDs for this species - await db.execute(""" - UPDATE pets SET species_id = ? - WHERE species_id NOT IN (SELECT id FROM pet_species) - AND species_id IN ( - SELECT DISTINCT p.species_id FROM pets p - WHERE p.species_id NOT IN (SELECT id FROM pet_species) - LIMIT 1 - ) - """, (current_id,)) - - await db.commit() - print(" āœ… Orphaned pets fixed") - - except Exception as e: - print(f" āŒ Failed to fix orphaned pets: {e}") - def start_background_validation(self, loop): """Start background task to periodically validate player data""" import asyncio diff --git a/run_bot_with_reconnect.py b/run_bot_with_reconnect.py index e1ae0c4..8475d40 100644 --- a/run_bot_with_reconnect.py +++ b/run_bot_with_reconnect.py @@ -19,8 +19,7 @@ from src.database import Database from src.game_engine import GameEngine from src.irc_connection_manager import IRCConnectionManager, ConnectionState from src.rate_limiter import RateLimiter, get_command_category -from src.npc_events import NPCEventsManager -from modules import CoreCommands, Exploration, BattleSystem, PetManagement, Achievements, Admin, Inventory, GymBattles, TeamBuilder, NPCEventsModule +from modules import CoreCommands, Exploration, BattleSystem, PetManagement, Achievements, Admin, Inventory, GymBattles, TeamBuilder from webserver import PetBotWebServer from config import IRC_CONFIG, RATE_LIMIT_CONFIG @@ -43,7 +42,6 @@ class PetBotWithReconnect: # Core components self.database = Database() self.game_engine = GameEngine(self.database) - self.npc_events = None self.config = IRC_CONFIG # Connection and state management @@ -84,11 +82,6 @@ class PetBotWithReconnect: await self.game_engine.load_game_data() self.logger.info("āœ… Game data loaded") - # Initialize NPC events manager - self.logger.info("šŸ”„ Initializing NPC events manager...") - self.npc_events = NPCEventsManager(self.database) - self.logger.info("āœ… NPC events manager initialized") - # Load modules self.logger.info("šŸ”„ Loading command modules...") await self.load_modules() @@ -125,7 +118,6 @@ class PetBotWithReconnect: self.logger.info("šŸ”„ Starting background tasks...") asyncio.create_task(self.background_validation_task()) asyncio.create_task(self.connection_stats_task()) - asyncio.create_task(self.npc_events.start_background_task()) self.logger.info("āœ… Background tasks started") self.logger.info("šŸŽ‰ All components initialized successfully!") @@ -145,8 +137,7 @@ class PetBotWithReconnect: Admin, Inventory, GymBattles, - TeamBuilder, - NPCEventsModule + TeamBuilder ] self.modules = {} diff --git a/src/database.py b/src/database.py index 3782c86..f8c53ba 100644 --- a/src/database.py +++ b/src/database.py @@ -36,19 +36,10 @@ class Database: evolution_level INTEGER, evolution_species_id INTEGER, rarity INTEGER DEFAULT 1, - emoji TEXT, FOREIGN KEY (evolution_species_id) REFERENCES pet_species (id) ) """) - # Add emoji column if it doesn't exist (migration) - try: - await db.execute("ALTER TABLE pet_species ADD COLUMN emoji TEXT") - await db.commit() - except Exception: - # Column already exists or other error, which is fine - pass - await db.execute(""" CREATE TABLE IF NOT EXISTS pets ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -169,22 +160,6 @@ class Database: print(f"Migration warning: {e}") pass # Don't fail if migration has issues - # Add fainted_at column for tracking when pets faint - try: - await db.execute("ALTER TABLE pets ADD COLUMN fainted_at TIMESTAMP DEFAULT NULL") - await db.commit() - print("Added fainted_at column to pets table") - except: - pass # Column already exists - - # Add last_heal_time column for heal command cooldown - try: - await db.execute("ALTER TABLE players ADD COLUMN last_heal_time TIMESTAMP DEFAULT NULL") - await db.commit() - print("Added last_heal_time column to players table") - except: - pass # Column already exists - await db.execute(""" CREATE TABLE IF NOT EXISTS location_spawns ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -365,38 +340,6 @@ class Database: ) """) - await db.execute(""" - CREATE TABLE IF NOT EXISTS npc_events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - event_type TEXT NOT NULL, - title TEXT NOT NULL, - description TEXT NOT NULL, - difficulty INTEGER DEFAULT 1, - target_contributions INTEGER NOT NULL, - current_contributions INTEGER DEFAULT 0, - reward_experience INTEGER DEFAULT 0, - reward_money INTEGER DEFAULT 0, - reward_items TEXT, - status TEXT DEFAULT 'active', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMP NOT NULL, - completion_message TEXT - ) - """) - - await db.execute(""" - CREATE TABLE IF NOT EXISTS npc_event_contributions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - event_id INTEGER NOT NULL, - player_id INTEGER NOT NULL, - contributions INTEGER DEFAULT 0, - last_contribution_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (event_id) REFERENCES npc_events (id), - FOREIGN KEY (player_id) REFERENCES players (id), - UNIQUE(event_id, player_id) - ) - """) - await db.execute(""" CREATE TABLE IF NOT EXISTS verification_pins ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -509,7 +452,7 @@ class Database: async with aiosqlite.connect(self.db_path) as db: db.row_factory = aiosqlite.Row query = """ - SELECT p.*, ps.name as species_name, ps.type1, ps.type2, ps.emoji + SELECT p.*, ps.name as species_name, ps.type1, ps.type2 FROM pets p JOIN pet_species ps ON p.species_id = ps.id WHERE p.player_id = ? @@ -911,7 +854,7 @@ class Database: """Add an item to player's inventory""" async with aiosqlite.connect(self.db_path) as db: # Get item ID - cursor = await db.execute("SELECT id FROM items WHERE LOWER(name) = LOWER(?)", (item_name,)) + cursor = await db.execute("SELECT id FROM items WHERE name = ?", (item_name,)) item = await cursor.fetchone() if not item: return False @@ -966,7 +909,7 @@ class Database: SELECT i.*, pi.quantity FROM items i JOIN player_inventory pi ON i.id = pi.item_id - WHERE pi.player_id = ? AND LOWER(i.name) = LOWER(?) + WHERE pi.player_id = ? AND i.name = ? """, (player_id, item_name)) item = await cursor.fetchone() @@ -2113,336 +2056,4 @@ class Database: return cursor.rowcount > 0 except Exception as e: print(f"Error renaming team configuration: {e}") - return False - - # NPC Events System Methods - async def create_npc_event(self, event_data: Dict) -> int: - """Create a new NPC event""" - async with aiosqlite.connect(self.db_path) as db: - cursor = await db.execute(""" - INSERT INTO npc_events - (event_type, title, description, difficulty, target_contributions, - reward_experience, reward_money, reward_items, expires_at, completion_message) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - event_data['event_type'], - event_data['title'], - event_data['description'], - event_data['difficulty'], - event_data['target_contributions'], - event_data['reward_experience'], - event_data['reward_money'], - event_data.get('reward_items', ''), - event_data['expires_at'], - event_data.get('completion_message', '') - )) - - await db.commit() - return cursor.lastrowid - - async def get_active_npc_events(self) -> List[Dict]: - """Get all active NPC events""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT * FROM npc_events - WHERE status = 'active' AND expires_at > datetime('now') - ORDER BY created_at DESC - """) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - async def get_npc_event_by_id(self, event_id: int) -> Optional[Dict]: - """Get a specific NPC event by ID""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT * FROM npc_events WHERE id = ? - """, (event_id,)) - - row = await cursor.fetchone() - return dict(row) if row else None - - async def contribute_to_npc_event(self, event_id: int, player_id: int, contribution: int) -> Dict: - """Add player contribution to an NPC event""" - async with aiosqlite.connect(self.db_path) as db: - try: - await db.execute("BEGIN TRANSACTION") - - # Insert or update player contribution - await db.execute(""" - INSERT OR REPLACE INTO npc_event_contributions - (event_id, player_id, contributions, last_contribution_at) - VALUES (?, ?, - COALESCE((SELECT contributions FROM npc_event_contributions - WHERE event_id = ? AND player_id = ?), 0) + ?, - CURRENT_TIMESTAMP) - """, (event_id, player_id, event_id, player_id, contribution)) - - # Update total contributions for the event - await db.execute(""" - UPDATE npc_events - SET current_contributions = current_contributions + ? - WHERE id = ? - """, (contribution, event_id)) - - # Check if event is completed - cursor = await db.execute(""" - SELECT current_contributions, target_contributions - FROM npc_events WHERE id = ? - """, (event_id,)) - - row = await cursor.fetchone() - if row and row[0] >= row[1]: - # Mark event as completed - await db.execute(""" - UPDATE npc_events - SET status = 'completed' - WHERE id = ? - """, (event_id,)) - - await db.commit() - return {"success": True, "event_completed": True} - - await db.commit() - return {"success": True, "event_completed": False} - - except Exception as e: - await db.execute("ROLLBACK") - return {"success": False, "error": str(e)} - - async def get_player_event_contributions(self, player_id: int, event_id: int) -> int: - """Get player's contributions to a specific event""" - async with aiosqlite.connect(self.db_path) as db: - cursor = await db.execute(""" - SELECT contributions FROM npc_event_contributions - WHERE event_id = ? AND player_id = ? - """, (event_id, player_id)) - - row = await cursor.fetchone() - return row[0] if row else 0 - - async def get_event_leaderboard(self, event_id: int) -> List[Dict]: - """Get leaderboard for an event""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT p.nickname, c.contributions, c.last_contribution_at - FROM npc_event_contributions c - JOIN players p ON c.player_id = p.id - WHERE c.event_id = ? - ORDER BY c.contributions DESC - """, (event_id,)) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - async def expire_npc_events(self) -> int: - """Mark expired events as expired and return count""" - async with aiosqlite.connect(self.db_path) as db: - cursor = await db.execute(""" - UPDATE npc_events - SET status = 'expired' - WHERE status = 'active' AND expires_at <= datetime('now') - """) - - await db.commit() - return cursor.rowcount - - async def distribute_event_rewards(self, event_id: int) -> Dict: - """Distribute rewards to all participants of a completed event""" - async with aiosqlite.connect(self.db_path) as db: - try: - await db.execute("BEGIN TRANSACTION") - - # Get event details - cursor = await db.execute(""" - SELECT reward_experience, reward_money, reward_items - FROM npc_events WHERE id = ? AND status = 'completed' - """, (event_id,)) - - event_row = await cursor.fetchone() - if not event_row: - await db.execute("ROLLBACK") - return {"success": False, "error": "Event not found or not completed"} - - reward_exp, reward_money, reward_items = event_row - - # Get all participants - cursor = await db.execute(""" - SELECT player_id, contributions - FROM npc_event_contributions - WHERE event_id = ? - """, (event_id,)) - - participants = await cursor.fetchall() - - # Distribute rewards - for player_id, contributions in participants: - # Scale rewards based on contribution (minimum 50% of full reward) - contribution_multiplier = max(0.5, min(1.0, contributions / 10)) - - final_exp = int(reward_exp * contribution_multiplier) - final_money = int(reward_money * contribution_multiplier) - - # Update player rewards - await db.execute(""" - UPDATE players - SET experience = experience + ?, money = money + ? - WHERE id = ? - """, (final_exp, final_money, player_id)) - - await db.commit() - return {"success": True, "participants_rewarded": len(participants)} - - except Exception as e: - await db.execute("ROLLBACK") - return {"success": False, "error": str(e)} - - # Pet Healing System Methods - async def get_fainted_pets(self, player_id: int) -> List[Dict]: - """Get all fainted pets for a player""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.emoji - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? AND p.fainted_at IS NOT NULL - ORDER BY p.fainted_at DESC - """, (player_id,)) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - async def revive_pet(self, pet_id: int, new_hp: int) -> bool: - """Revive a fainted pet and restore HP""" - try: - async with aiosqlite.connect(self.db_path) as db: - await db.execute(""" - UPDATE pets - SET hp = ?, fainted_at = NULL - WHERE id = ? - """, (new_hp, pet_id)) - - await db.commit() - return True - except Exception as e: - print(f"Error reviving pet: {e}") - return False - - async def faint_pet(self, pet_id: int) -> bool: - """Mark a pet as fainted""" - try: - async with aiosqlite.connect(self.db_path) as db: - await db.execute(""" - UPDATE pets - SET hp = 0, fainted_at = CURRENT_TIMESTAMP - WHERE id = ? - """, (pet_id,)) - - await db.commit() - return True - except Exception as e: - print(f"Error fainting pet: {e}") - return False - - async def get_pets_for_auto_recovery(self) -> List[Dict]: - """Get pets that are eligible for auto-recovery (fainted for 30+ minutes)""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.fainted_at IS NOT NULL - AND p.fainted_at <= datetime('now', '-30 minutes') - """) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - async def auto_recover_pet(self, pet_id: int) -> bool: - """Auto-recover a pet to 1 HP after 30 minutes""" - try: - async with aiosqlite.connect(self.db_path) as db: - await db.execute(""" - UPDATE pets - SET hp = 1, fainted_at = NULL - WHERE id = ? - """, (pet_id,)) - - await db.commit() - return True - except Exception as e: - print(f"Error auto-recovering pet: {e}") - return False - - async def get_last_heal_time(self, player_id: int) -> Optional[datetime]: - """Get the last time a player used the heal command""" - async with aiosqlite.connect(self.db_path) as db: - cursor = await db.execute(""" - SELECT last_heal_time FROM players WHERE id = ? - """, (player_id,)) - - row = await cursor.fetchone() - if row and row[0]: - return datetime.fromisoformat(row[0]) - return None - - async def update_last_heal_time(self, player_id: int) -> bool: - """Update the last heal time for a player""" - try: - async with aiosqlite.connect(self.db_path) as db: - await db.execute(""" - UPDATE players - SET last_heal_time = CURRENT_TIMESTAMP - WHERE id = ? - """, (player_id,)) - - await db.commit() - return True - except Exception as e: - print(f"Error updating last heal time: {e}") - return False - - async def get_active_pets(self, player_id: int) -> List[Dict]: - """Get all active pets for a player (excluding fainted pets)""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.emoji - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? AND p.is_active = 1 AND p.fainted_at IS NULL - ORDER BY p.team_order - """, (player_id,)) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] - - async def get_player_pets(self, player_id: int, active_only: bool = False) -> List[Dict]: - """Get all pets for a player, optionally filtering to active only""" - async with aiosqlite.connect(self.db_path) as db: - db.row_factory = aiosqlite.Row - - if active_only: - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.emoji - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? AND p.is_active = 1 AND p.fainted_at IS NULL - ORDER BY p.team_order - """, (player_id,)) - else: - cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.emoji - FROM pets p - JOIN pet_species ps ON p.species_id = ps.id - WHERE p.player_id = ? - ORDER BY p.is_active DESC, p.team_order, p.level DESC - """, (player_id,)) - - rows = await cursor.fetchall() - return [dict(row) for row in rows] \ No newline at end of file + return False \ No newline at end of file diff --git a/src/game_engine.py b/src/game_engine.py index cc935f5..c194076 100644 --- a/src/game_engine.py +++ b/src/game_engine.py @@ -16,7 +16,6 @@ class GameEngine: self.type_chart = {} self.weather_patterns = {} self.weather_task = None - self.pet_recovery_task = None self.shutdown_event = asyncio.Event() async def load_game_data(self): @@ -29,7 +28,6 @@ class GameEngine: await self.database.initialize_gyms() await self.init_weather_system() await self.battle_engine.load_battle_data() - await self.start_pet_recovery_system() async def load_pet_species(self): try: @@ -37,28 +35,19 @@ class GameEngine: species_data = json.load(f) async with aiosqlite.connect(self.database.db_path) as db: - # Check if species already exist to avoid re-inserting and changing IDs - cursor = await db.execute("SELECT COUNT(*) FROM pet_species") - existing_count = (await cursor.fetchone())[0] - - if existing_count == 0: - # Only insert if no species exist to avoid ID conflicts - for species in species_data: - await db.execute(""" - INSERT OR IGNORE INTO pet_species - (name, type1, type2, base_hp, base_attack, base_defense, - base_speed, evolution_level, rarity, emoji) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, ( - species["name"], species["type1"], species.get("type2"), - species["base_hp"], species["base_attack"], species["base_defense"], - species["base_speed"], species.get("evolution_level"), - species.get("rarity", 1), species.get("emoji", "🐾") - )) - await db.commit() - print(f"āœ… Loaded {len(species_data)} pet species into database") - else: - print(f"āœ… Found {existing_count} existing pet species - skipping reload to preserve IDs") + for species in species_data: + await db.execute(""" + INSERT OR IGNORE INTO pet_species + (name, type1, type2, base_hp, base_attack, base_defense, + base_speed, evolution_level, rarity) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + species["name"], species["type1"], species.get("type2"), + species["base_hp"], species["base_attack"], species["base_defense"], + species["base_speed"], species.get("evolution_level"), + species.get("rarity", 1) + )) + await db.commit() except FileNotFoundError: await self.create_default_species() @@ -675,90 +664,7 @@ class GameEngine: except Exception as e: print(f"Error checking expired weather: {e}") - async def get_pet_emoji(self, species_name: str) -> str: - """Get emoji for a pet species""" - try: - async with aiosqlite.connect(self.database.db_path) as db: - cursor = await db.execute( - "SELECT emoji FROM pet_species WHERE name = ?", - (species_name,) - ) - row = await cursor.fetchone() - return row[0] if row and row[0] else "🐾" - except Exception: - return "🐾" # Default emoji if something goes wrong - - def format_pet_name_with_emoji(self, pet_name: str, emoji: str = None, species_name: str = None) -> str: - """Format pet name with emoji for display in IRC or web - - Args: - pet_name: The pet's nickname or species name - emoji: Optional emoji to use (if None, will look up by species_name) - species_name: Species name for emoji lookup if emoji not provided - - Returns: - Formatted string like "šŸ”„ Flamey" or "šŸ‰ Infernowyrm" - """ - if emoji: - return f"{emoji} {pet_name}" - elif species_name: - # This would need to be async, but for IRC we can use sync fallback - return f"🐾 {pet_name}" # Use default for now, can be enhanced later - else: - return f"🐾 {pet_name}" - - async def start_pet_recovery_system(self): - """Start the background pet recovery task""" - if self.pet_recovery_task is None or self.pet_recovery_task.done(): - print("šŸ„ Starting pet recovery background task...") - self.pet_recovery_task = asyncio.create_task(self._pet_recovery_loop()) - - async def stop_pet_recovery_system(self): - """Stop the background pet recovery task""" - print("šŸ„ Stopping pet recovery background task...") - if self.pet_recovery_task and not self.pet_recovery_task.done(): - self.pet_recovery_task.cancel() - try: - await self.pet_recovery_task - except asyncio.CancelledError: - pass - - async def _pet_recovery_loop(self): - """Background task that checks for pets eligible for auto-recovery""" - try: - while not self.shutdown_event.is_set(): - try: - # Check every 5 minutes for pets that need recovery - await asyncio.sleep(300) # 5 minutes - - if self.shutdown_event.is_set(): - break - - # Get pets eligible for auto-recovery (fainted for 30+ minutes) - eligible_pets = await self.database.get_pets_for_auto_recovery() - - if eligible_pets: - print(f"šŸ„ Auto-recovering {len(eligible_pets)} pet(s) after 30 minutes...") - - for pet in eligible_pets: - success = await self.database.auto_recover_pet(pet["id"]) - if success: - print(f" šŸ„ Auto-recovered {pet['nickname'] or pet['species_name']} (ID: {pet['id']}) to 1 HP") - else: - print(f" āŒ Failed to auto-recover pet ID: {pet['id']}") - - except asyncio.CancelledError: - break - except Exception as e: - print(f"Error in pet recovery loop: {e}") - # Continue the loop even if there's an error - await asyncio.sleep(60) # Wait a minute before retrying - - except asyncio.CancelledError: - print("Pet recovery task cancelled") - async def shutdown(self): """Gracefully shutdown the game engine""" print("šŸ”„ Shutting down game engine...") - await self.stop_weather_system() - await self.stop_pet_recovery_system() \ No newline at end of file + await self.stop_weather_system() \ No newline at end of file diff --git a/src/npc_events.py b/src/npc_events.py deleted file mode 100644 index a8324f6..0000000 --- a/src/npc_events.py +++ /dev/null @@ -1,293 +0,0 @@ -""" -NPC Events System -Manages random collaborative events that all players can participate in -""" - -import asyncio -import random -from datetime import datetime, timedelta -from typing import Dict, List, Optional -from src.database import Database - -class NPCEventsManager: - def __init__(self, database: Database): - self.database = database - self.active_events = {} - self.event_templates = { - 1: [ # Difficulty 1 - Easy - { - "event_type": "resource_gathering", - "title": "Village Supply Run", - "description": "The village needs supplies! Help gather resources by exploring and finding items.", - "target_contributions": 25, - "reward_experience": 50, - "reward_money": 100, - "completion_message": "šŸŽ‰ The village has enough supplies! Everyone who helped gets rewarded!", - "duration_hours": 4 - }, - { - "event_type": "pet_rescue", - "title": "Lost Pet Search", - "description": "A pet has gone missing! Help search different locations to find clues.", - "target_contributions": 20, - "reward_experience": 40, - "reward_money": 80, - "completion_message": "🐾 The lost pet has been found safe! Thanks to everyone who helped search!", - "duration_hours": 3 - }, - { - "event_type": "community_project", - "title": "Park Cleanup", - "description": "The local park needs cleaning! Help by contributing your time and effort.", - "target_contributions": 30, - "reward_experience": 35, - "reward_money": 75, - "completion_message": "🌳 The park is clean and beautiful again! Great teamwork everyone!", - "duration_hours": 5 - } - ], - 2: [ # Difficulty 2 - Medium - { - "event_type": "emergency_response", - "title": "Storm Recovery", - "description": "A storm has damaged the town! Help with recovery efforts by contributing resources and time.", - "target_contributions": 50, - "reward_experience": 100, - "reward_money": 200, - "completion_message": "ā›ˆļø The town has recovered from the storm! Everyone's hard work paid off!", - "duration_hours": 6 - }, - { - "event_type": "festival_preparation", - "title": "Annual Festival Setup", - "description": "The annual pet festival is coming! Help set up decorations and prepare activities.", - "target_contributions": 40, - "reward_experience": 80, - "reward_money": 150, - "completion_message": "šŸŽŖ The festival is ready! Thanks to everyone who helped prepare!", - "duration_hours": 8 - }, - { - "event_type": "research_expedition", - "title": "Scientific Discovery", - "description": "Researchers need help documenting rare pets! Contribute by exploring and reporting findings.", - "target_contributions": 35, - "reward_experience": 90, - "reward_money": 180, - "completion_message": "šŸ”¬ The research is complete! Your discoveries will help future generations!", - "duration_hours": 7 - } - ], - 3: [ # Difficulty 3 - Hard - { - "event_type": "crisis_response", - "title": "Regional Emergency", - "description": "A regional crisis requires immediate community response! All trainers needed!", - "target_contributions": 75, - "reward_experience": 150, - "reward_money": 300, - "completion_message": "🚨 Crisis averted! The entire region is safe thanks to your heroic efforts!", - "duration_hours": 12 - }, - { - "event_type": "legendary_encounter", - "title": "Legendary Pet Sighting", - "description": "A legendary pet has been spotted! Help researchers track and document this rare encounter.", - "target_contributions": 60, - "reward_experience": 200, - "reward_money": 400, - "completion_message": "✨ The legendary pet has been successfully documented! History has been made!", - "duration_hours": 10 - }, - { - "event_type": "ancient_mystery", - "title": "Ancient Ruins Discovery", - "description": "Ancient ruins have been discovered! Help archaeologists uncover the secrets within.", - "target_contributions": 80, - "reward_experience": 180, - "reward_money": 350, - "completion_message": "šŸ›ļø The ancient secrets have been revealed! Your efforts uncovered lost knowledge!", - "duration_hours": 14 - } - ] - } - - async def start_background_task(self): - """Start the background task that manages NPC events""" - while True: - try: - # Check for expired events - await self.expire_events() - - # Distribute rewards for completed events - await self.distribute_completed_rewards() - - # Maybe spawn a new event - await self.maybe_spawn_event() - - # Wait 30 minutes before next check - await asyncio.sleep(30 * 60) - - except Exception as e: - print(f"Error in NPC events background task: {e}") - await asyncio.sleep(5 * 60) # Wait 5 minutes on error - - async def expire_events(self): - """Mark expired events as expired""" - try: - expired_count = await self.database.expire_npc_events() - if expired_count > 0: - print(f"šŸ• {expired_count} NPC events expired") - except Exception as e: - print(f"Error expiring NPC events: {e}") - - async def distribute_completed_rewards(self): - """Distribute rewards for completed events""" - try: - # Get completed events that haven't distributed rewards yet - completed_events = await self.database.get_active_npc_events() - - for event in completed_events: - if event['status'] == 'completed': - result = await self.database.distribute_event_rewards(event['id']) - if result['success']: - print(f"šŸŽ Distributed rewards for event '{event['title']}' to {result['participants_rewarded']} players") - - except Exception as e: - print(f"Error distributing event rewards: {e}") - - async def maybe_spawn_event(self): - """Maybe spawn a new event based on conditions""" - try: - # Check if we have any active events - active_events = await self.database.get_active_npc_events() - - # Don't spawn if we already have 2 or more active events - if len(active_events) >= 2: - return - - # 20% chance to spawn a new event each check (every 30 minutes) - if random.random() < 0.2: - await self.spawn_random_event() - - except Exception as e: - print(f"Error in maybe_spawn_event: {e}") - - async def spawn_random_event(self): - """Spawn a random event based on difficulty""" - try: - # Choose difficulty (weighted towards easier events) - difficulty_weights = {1: 0.6, 2: 0.3, 3: 0.1} - difficulty = random.choices(list(difficulty_weights.keys()), - weights=list(difficulty_weights.values()))[0] - - # Choose random event template - templates = self.event_templates[difficulty] - template = random.choice(templates) - - # Create event data - event_data = { - 'event_type': template['event_type'], - 'title': template['title'], - 'description': template['description'], - 'difficulty': difficulty, - 'target_contributions': template['target_contributions'], - 'reward_experience': template['reward_experience'], - 'reward_money': template['reward_money'], - 'completion_message': template['completion_message'], - 'expires_at': (datetime.now() + timedelta(hours=template['duration_hours'])).isoformat() - } - - # Create the event - event_id = await self.database.create_npc_event(event_data) - - print(f"šŸŽÆ New NPC event spawned: '{template['title']}' (ID: {event_id}, Difficulty: {difficulty})") - - return event_id - - except Exception as e: - print(f"Error spawning random event: {e}") - return None - - async def get_active_events(self) -> List[Dict]: - """Get all active events""" - return await self.database.get_active_npc_events() - - async def contribute_to_event(self, event_id: int, player_id: int, contribution: int = 1) -> Dict: - """Add a player's contribution to an event""" - return await self.database.contribute_to_npc_event(event_id, player_id, contribution) - - async def get_event_details(self, event_id: int) -> Optional[Dict]: - """Get detailed information about an event""" - event = await self.database.get_npc_event_by_id(event_id) - if not event: - return None - - # Add leaderboard - leaderboard = await self.database.get_event_leaderboard(event_id) - event['leaderboard'] = leaderboard - - return event - - async def get_player_contributions(self, player_id: int, event_id: int) -> int: - """Get player's contributions to a specific event""" - return await self.database.get_player_event_contributions(player_id, event_id) - - async def force_spawn_event(self, difficulty: int = 1) -> Optional[int]: - """Force spawn an event (admin command)""" - if difficulty not in self.event_templates: - return None - - templates = self.event_templates[difficulty] - template = random.choice(templates) - - event_data = { - 'event_type': template['event_type'], - 'title': template['title'], - 'description': template['description'], - 'difficulty': difficulty, - 'target_contributions': template['target_contributions'], - 'reward_experience': template['reward_experience'], - 'reward_money': template['reward_money'], - 'completion_message': template['completion_message'], - 'expires_at': (datetime.now() + timedelta(hours=template['duration_hours'])).isoformat() - } - - return await self.database.create_npc_event(event_data) - - async def force_spawn_specific_event(self, event_type: str, difficulty: int = 1) -> Optional[int]: - """Force spawn a specific event type (admin command)""" - if difficulty not in self.event_templates: - return None - - # Find template matching the event type - templates = self.event_templates[difficulty] - template = None - for t in templates: - if t['event_type'] == event_type: - template = t - break - - if not template: - return None - - event_data = { - 'event_type': template['event_type'], - 'title': template['title'], - 'description': template['description'], - 'difficulty': difficulty, - 'target_contributions': template['target_contributions'], - 'reward_experience': template['reward_experience'], - 'reward_money': template['reward_money'], - 'completion_message': template['completion_message'], - 'expires_at': (datetime.now() + timedelta(hours=template['duration_hours'])).isoformat() - } - - return await self.database.create_npc_event(event_data) - - def get_progress_bar(self, current: int, target: int, width: int = 20) -> str: - """Generate a progress bar for event progress""" - filled = int((current / target) * width) - bar = "ā–ˆ" * filled + "ā–‘" * (width - filled) - percentage = min(100, int((current / target) * 100)) - return f"[{bar}] {percentage}% ({current}/{target})" \ No newline at end of file diff --git a/src/rate_limiter.py b/src/rate_limiter.py index 64d6a42..c9d103f 100644 --- a/src/rate_limiter.py +++ b/src/rate_limiter.py @@ -11,7 +11,7 @@ class CommandCategory(Enum): BASIC = "basic" # !help, !ping, !status GAMEPLAY = "gameplay" # !explore, !catch, !battle MANAGEMENT = "management" # !pets, !activate, !deactivate - ADMIN = "admin" # !reload, !setweather, !spawnevent + ADMIN = "admin" # !backup, !reload, !reconnect WEB = "web" # Web interface requests @@ -387,6 +387,7 @@ COMMAND_CATEGORIES = { "ping": CommandCategory.BASIC, "status": CommandCategory.BASIC, "uptime": CommandCategory.BASIC, + "connection_stats": CommandCategory.BASIC, # Gameplay commands "start": CommandCategory.GAMEPLAY, @@ -410,7 +411,13 @@ COMMAND_CATEGORIES = { "nickname": CommandCategory.MANAGEMENT, # Admin commands + "backup": CommandCategory.ADMIN, + "restore": CommandCategory.ADMIN, + "backups": CommandCategory.ADMIN, + "backup_stats": CommandCategory.ADMIN, + "backup_cleanup": CommandCategory.ADMIN, "reload": CommandCategory.ADMIN, + "reconnect": CommandCategory.ADMIN, } diff --git a/start_petbot.sh b/start_petbot.sh index b7fa441..4ba2c2d 100755 --- a/start_petbot.sh +++ b/start_petbot.sh @@ -1,7 +1,7 @@ #!/bin/bash # # PetBot Startup Script -# Complete one-command startup for PetBot with all dependencies and validation +# Complete one-command startup for PetBot with all dependencies # # Usage: ./start_petbot.sh # @@ -10,8 +10,6 @@ set -e # Exit on any error echo "🐾 Starting PetBot..." echo "====================" -echo "Version: $(date '+%Y-%m-%d %H:%M:%S') - Enhanced with startup validation" -echo "" # Get script directory (works even if called from elsewhere) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -31,22 +29,23 @@ fi echo "šŸ”„ Activating virtual environment..." source venv/bin/activate -# Upgrade pip to latest version -echo "šŸ”„ Ensuring pip is up to date..." -pip install --upgrade pip -q - # Check if requirements are installed echo "šŸ”„ Checking dependencies..." -if ! python -c "import aiosqlite, irc" 2>/dev/null; then - echo "šŸ“¦ Installing/updating dependencies from requirements.txt..." +if ! python -c "import aiosqlite, irc, dotenv" 2>/dev/null; then + echo "šŸ“¦ Installing/updating dependencies..." - # Verify requirements.txt exists + # Create requirements.txt if it doesn't exist if [ ! -f "requirements.txt" ]; then - echo "āŒ requirements.txt not found!" - echo "šŸ”§ Please run install_prerequisites.sh first" - exit 1 + echo "šŸ“ Creating requirements.txt..." + cat > requirements.txt << EOF +aiosqlite>=0.17.0 +irc>=20.3.0 +python-dotenv>=0.19.0 +aiohttp>=3.8.0 +EOF fi + pip install --upgrade pip pip install -r requirements.txt echo "āœ… Dependencies installed" else @@ -61,143 +60,44 @@ sys.path.append('.') try: from src.database import Database - from src.game_engine import GameEngine + from src.game_engine import GameEngine from src.rate_limiter import RateLimiter from src.irc_connection_manager import IRCConnectionManager from config import ADMIN_USER, IRC_CONFIG, RATE_LIMIT_CONFIG print('āœ… Core modules verified') print(f'ā„¹ļø Admin user: {ADMIN_USER}') - print(f'ā„¹ļø IRC Channel: {IRC_CONFIG[\"channel\"]}') - print(f'ā„¹ļø Rate limiting: {\"Enabled\" if RATE_LIMIT_CONFIG[\"enabled\"] else \"Disabled\"}') except ImportError as e: print(f'āŒ Module import error: {e}') - print('šŸ’” Try running: ./install_prerequisites.sh') sys.exit(1) " -# Create required directories -echo "šŸ”„ Creating required directories..." -mkdir -p data backups logs - -# Check configuration files -echo "šŸ”„ Verifying configuration files..." -config_files=("config/pets.json" "config/locations.json" "config/items.json" "config/achievements.json" "config/gyms.json") -missing_configs=() - -for config_file in "${config_files[@]}"; do - if [ ! -f "$config_file" ]; then - missing_configs+=("$config_file") - fi -done - -if [ ${#missing_configs[@]} -gt 0 ]; then - echo "āŒ Missing configuration files:" - for missing in "${missing_configs[@]}"; do - echo " - $missing" - done - echo "šŸ’” These files are required for proper bot operation" - exit 1 +# Create data directory if it doesn't exist +if [ ! -d "data" ]; then + echo "šŸ“ Creating data directory..." + mkdir -p data fi -echo "āœ… All configuration files present" +# Create backups directory if it doesn't exist +if [ ! -d "backups" ]; then + echo "šŸ“ Creating backups directory..." + mkdir -p backups +fi -# Database pre-flight check -if [ -f "data/petbot.db" ]; then - echo "šŸ”„ Validating existing database..." - - # Quick database validation - python3 -c " -import sqlite3 -import sys - -try: - conn = sqlite3.connect('data/petbot.db') - cursor = conn.cursor() - - # Check essential tables exist - tables = ['players', 'pets', 'pet_species', 'locations'] - for table in tables: - cursor.execute(f'SELECT COUNT(*) FROM {table}') - count = cursor.fetchone()[0] - print(f' āœ… {table}: {count} records') - - conn.close() - print('āœ… Database validation passed') - -except Exception as e: - print(f'āŒ Database validation failed: {e}') - print('šŸ’” Database may be corrupted - backup and recreate if needed') - sys.exit(1) -" -else +# Check if database exists, if not mention first-time setup +if [ ! -f "data/petbot.db" ]; then echo "ā„¹ļø First-time setup detected - database will be created automatically" fi -# Pre-startup system test -echo "šŸ”„ Running pre-startup system test..." -python3 -c " -import sys -sys.path.append('.') - -try: - # Test basic imports and initialization - from run_bot_debug import PetBotDebug - - # Create a test bot instance to verify everything loads - print(' šŸ”§ Testing bot initialization...') - bot = PetBotDebug() - print(' āœ… Bot instance created successfully') - print('āœ… Pre-startup test passed') - -except Exception as e: - print(f'āŒ Pre-startup test failed: {e}') - import traceback - traceback.print_exc() - sys.exit(1) -" - -# Display comprehensive startup information +# Display startup information echo "" -echo "šŸš€ Launching PetBot with Enhanced Features..." -echo "=============================================" -echo "🌐 Web interface: http://localhost:8080" -echo "šŸ“± Public access: http://petz.rdx4.com/" -echo "šŸ’¬ IRC Server: irc.libera.chat" -echo "šŸ“¢ IRC Channel: #petz" -echo "šŸ‘¤ Admin User: $(python3 -c 'from config import ADMIN_USER; print(ADMIN_USER)')" -echo "" -echo "šŸŽ® Features Available:" -echo " āœ… 33 Pet Species with Emojis" -echo " āœ… 6 Exploration Locations" -echo " āœ… Team Builder with PIN Verification" -echo " āœ… Achievement System" -echo " āœ… Gym Battles" -echo " āœ… Weather System" -echo " āœ… Rate Limiting & Anti-Abuse" -echo " āœ… Auto-Reconnection" -echo " āœ… Startup Data Validation" -echo " āœ… Background Monitoring" -echo "" -echo "šŸ”§ Technical Details:" -echo " šŸ“Š Database: SQLite with validation" -echo " 🌐 Webserver: Integrated with bot instance" -echo " šŸ›”ļø Security: Rate limiting enabled" -echo " šŸ”„ Reliability: Auto-reconnect on failure" -echo " šŸ“ˆ Monitoring: Background validation every 30min" +echo "šŸš€ Launching PetBot with Auto-Reconnect..." +echo "🌐 Web interface will be available at: http://localhost:8080" +echo "šŸ’¬ IRC: Connecting to irc.libera.chat #petz" +echo "šŸ“Š Features: Rate limiting, auto-reconnect, web interface, team builder" echo "" echo "Press Ctrl+C to stop the bot" -echo "=============================================" +echo "====================" echo "" -# Launch the appropriate bot based on what's available -if [ -f "run_bot_with_reconnect.py" ]; then - echo "šŸš€ Starting with auto-reconnect support..." - exec python run_bot_with_reconnect.py -elif [ -f "run_bot_debug.py" ]; then - echo "šŸš€ Starting in debug mode..." - exec python run_bot_debug.py -else - echo "āŒ No bot startup file found!" - echo "šŸ’” Expected: run_bot_with_reconnect.py or run_bot_debug.py" - exit 1 -fi \ No newline at end of file +# Launch the bot +exec python run_bot_with_reconnect.py \ No newline at end of file diff --git a/webserver.py b/webserver.py index 99cb358..0cc3e08 100644 --- a/webserver.py +++ b/webserver.py @@ -11,7 +11,6 @@ from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs from threading import Thread import time -import math # Add the project directory to the path sys.path.append(os.path.dirname(os.path.abspath(__file__))) @@ -178,7 +177,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): padding: 15px 20px; box-shadow: 0 2px 10px var(--shadow-color); margin-bottom: 0; - border-radius: 0 0 15px 15px; } .nav-content { @@ -578,14 +576,15 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): ("locations", "šŸ›ļø Gyms") ]), ("petdex", "šŸ“š Petdex", [ - ("petdex?sort=type", "šŸ”· by Type"), - ("petdex?sort=rarity", "⭐ by Rarity"), - ("petdex?sort=name", "šŸ”¤ by Name"), - ("petdex?sort=location", "šŸ—ŗļø by Location"), - ("petdex?sort=all", "šŸ“‹ Show All"), - ("petdex#search", "šŸ” Search") + ("petdex", "šŸ”· by Type"), + ("petdex", "⭐ by Rarity"), + ("petdex", "šŸ” Search") ]), - ("help", "šŸ“– Help", []) + ("help", "šŸ“– Help", [ + ("help", "⚔ Commands"), + ("help", "šŸ“– Web Guide"), + ("help", "ā“ FAQ") + ]) ] nav_links = "" @@ -1257,61 +1256,15 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): } """ - # Load help.html content and extract both CSS and body content - try: - with open('help.html', 'r', encoding='utf-8') as f: - help_content = f.read() - - import re - - # Extract CSS from help.html - css_match = re.search(r']*>(.*?)', help_content, re.DOTALL) - help_css = css_match.group(1) if css_match else "" - - # Extract body content (everything between tags) - body_match = re.search(r']*>(.*?)', help_content, re.DOTALL) - if body_match: - body_content = body_match.group(1) - # Remove the back link since we'll have the navigation bar - body_content = re.sub(r'.*?', '', body_content, flags=re.DOTALL) - else: - # Fallback: use original content if we can't parse it - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(help_content.encode()) - return - - # Create template with merged CSS - html_content = f""" - - - - - PetBot - Help & Commands - tag + html_content = html_content.replace("", additional_css + "") - /* Help page specific styles */ - {help_css} - - - - {self.get_navigation_bar("help")} -
    - {body_content} -
    - -""" - - self.send_response(200) - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write(html_content.encode()) - except FileNotFoundError: - self.serve_error_page("Help", "Help file not found") - except Exception as e: - self.serve_error_page("Help", f"Error loading help file: {str(e)}") + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html_content.encode()) def serve_players(self): """Serve the players page with real data""" @@ -1941,10 +1894,9 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): asyncio.set_event_loop(loop) locations_data = loop.run_until_complete(self.fetch_locations_data(database)) - player_locations = loop.run_until_complete(self.fetch_player_locations(database)) loop.close() - self.serve_locations_data(locations_data, player_locations) + self.serve_locations_data(locations_data) except Exception as e: print(f"Error fetching locations data: {e}") @@ -1986,243 +1938,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): print(f"Database error fetching locations: {e}") return [] - async def fetch_player_locations(self, database): - """Fetch player locations for the interactive map""" - try: - import aiosqlite - async with aiosqlite.connect(database.db_path) as db: - db.row_factory = aiosqlite.Row - cursor = await db.execute(""" - SELECT p.nickname, p.current_location_id, l.name as location_name - FROM players p - JOIN locations l ON p.current_location_id = l.id - ORDER BY p.nickname - """) - - rows = await cursor.fetchall() - players = [] - for row in rows: - player_dict = { - 'nickname': row['nickname'], - 'location_id': row['current_location_id'], - 'location_name': row['location_name'] - } - players.append(player_dict) - return players - - except Exception as e: - print(f"Database error fetching player locations: {e}") - return [] - - def create_interactive_map(self, locations_data, player_locations): - """Create an interactive SVG map showing player locations""" - if not locations_data: - return "" - - # Define map layout - create a unique visual design - map_positions = { - 1: {"x": 200, "y": 400, "shape": "circle", "color": "#4CAF50"}, # Starter Town - central - 2: {"x": 100, "y": 200, "shape": "hexagon", "color": "#2E7D32"}, # Whispering Woods - forest - 3: {"x": 400, "y": 150, "shape": "diamond", "color": "#FF9800"}, # Thunder Peaks - mountain - 4: {"x": 550, "y": 300, "shape": "octagon", "color": "#795548"}, # Stone Caverns - cave - 5: {"x": 300, "y": 500, "shape": "star", "color": "#2196F3"}, # Frozen Lake - ice - 6: {"x": 500, "y": 450, "shape": "triangle", "color": "#F44336"} # Volcanic Crater - fire - } - - # Create player location groups - location_players = {} - for player in player_locations or []: - loc_id = player['location_id'] - if loc_id not in location_players: - location_players[loc_id] = [] - location_players[loc_id].append(player['nickname']) - - # SVG map content - svg_content = "" - - # Add connecting paths between locations - paths = [ - (1, 2), (1, 3), (1, 5), # Starter Town connections - (2, 5), (3, 4), (4, 6), (5, 6) # Other connections - ] - - for start, end in paths: - if start in map_positions and end in map_positions: - start_pos = map_positions[start] - end_pos = map_positions[end] - svg_content += f""" - - """ - - # Add location shapes - for location in locations_data: - loc_id = location['id'] - if loc_id not in map_positions: - continue - - pos = map_positions[loc_id] - players_here = location_players.get(loc_id, []) - player_count = len(players_here) - - # Create shape based on type - shape_svg = self.create_location_shape(pos, location, player_count) - svg_content += shape_svg - - # Add location label - svg_content += f""" - - {location['name']} - - """ - - # Add player names if any - if players_here: - player_text = ", ".join(players_here) - svg_content += f""" - - {player_text} - - """ - - return f""" -
    -

    šŸ—ŗļø Interactive World Map

    -

    - Current player locations - shapes represent different terrain types -

    - -
    - - {svg_content} - -
    - -
    -
    -
    - Towns -
    -
    -
    - Forests -
    -
    -
    - Mountains -
    -
    -
    - Caves -
    -
    -
    - Ice Areas -
    -
    -
    - Volcanic -
    -
    -
    - """ - - def create_location_shape(self, pos, location, player_count): - """Create SVG shape for a location based on its type""" - x, y = pos['x'], pos['y'] - color = pos['color'] - shape = pos['shape'] - - # Add glow effect if players are present - glow = 'filter="url(#glow)"' if player_count > 0 else '' - - # Base size with scaling for player count - base_size = 25 + (player_count * 3) - - if shape == "circle": - return f""" - - - - - - - - - - - """ - elif shape == "hexagon": - points = [] - for i in range(6): - angle = i * 60 * math.pi / 180 - px = x + base_size * math.cos(angle) - py = y + base_size * math.sin(angle) - points.append(f"{px},{py}") - return f""" - - """ - elif shape == "diamond": - return f""" - - """ - elif shape == "triangle": - return f""" - - """ - elif shape == "star": - # Create 5-pointed star - points = [] - for i in range(10): - angle = i * 36 * math.pi / 180 - radius = base_size if i % 2 == 0 else base_size * 0.5 - px = x + radius * math.cos(angle) - py = y + radius * math.sin(angle) - points.append(f"{px},{py}") - return f""" - - """ - elif shape == "octagon": - points = [] - for i in range(8): - angle = i * 45 * math.pi / 180 - px = x + base_size * math.cos(angle) - py = y + base_size * math.sin(angle) - points.append(f"{px},{py}") - return f""" - - """ - else: - # Default to circle - return f""" - - """ - - def serve_locations_data(self, locations_data, player_locations=None): + def serve_locations_data(self, locations_data): """Serve locations page with real data using unified template""" # Build locations HTML @@ -2287,17 +2003,12 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
    """ - # Create interactive map HTML - map_html = self.create_interactive_map(locations_data, player_locations) - content = f"""

    šŸ—ŗļø Game Locations

    Explore all areas and discover what pets await you!

    - {map_html} -

    šŸŽÆ How Locations Work

    Travel: Use !travel <location> to move between areas

    @@ -2359,82 +2070,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): # Add locations-specific CSS additional_css = """ - .map-section { - background: var(--bg-secondary); - border-radius: 15px; - padding: 30px; - margin: 30px 0; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); - border: 1px solid var(--border-color); - } - - .map-section h2 { - color: var(--text-accent); - text-align: center; - margin-bottom: 10px; - } - - .map-container { - display: flex; - justify-content: center; - margin: 20px 0; - } - - .map-container svg { - border-radius: 10px; - box-shadow: 0 4px 15px rgba(0,0,0,0.5); - max-width: 100%; - height: auto; - } - - .map-legend { - display: flex; - justify-content: center; - flex-wrap: wrap; - gap: 20px; - margin-top: 20px; - } - - .legend-item { - display: flex; - align-items: center; - gap: 8px; - color: var(--text-primary); - font-size: 0.9em; - } - - .legend-shape { - width: 16px; - height: 16px; - border-radius: 3px; - border: 1px solid white; - } - - .legend-shape.circle { - border-radius: 50%; - } - - .legend-shape.hexagon { - border-radius: 3px; - transform: rotate(45deg); - } - - .legend-shape.diamond { - transform: rotate(45deg); - } - - .legend-shape.triangle { - clip-path: polygon(50% 0%, 0% 100%, 100% 100%); - } - - .legend-shape.star { - clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%); - } - - .legend-shape.octagon { - clip-path: polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%); - } - .locations-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); @@ -2565,12 +2200,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): self.serve_error_page("Petdex", "Database not available") return - # Parse URL parameters for sorting - parsed_url = urlparse(self.path) - query_params = parse_qs(parsed_url.query) - sort_mode = query_params.get('sort', ['rarity'])[0] # Default to rarity - search_query = query_params.get('search', [''])[0] # Default to empty search - # Fetch petdex data try: import asyncio @@ -2580,7 +2209,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): petdex_data = loop.run_until_complete(self.fetch_petdex_data(database)) loop.close() - self.serve_petdex_data(petdex_data, sort_mode, search_query) + self.serve_petdex_data(petdex_data) except Exception as e: print(f"Error fetching petdex data: {e}") @@ -2593,9 +2222,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): async with aiosqlite.connect(database.db_path) as db: # Get all pet species with evolution information (no duplicates) cursor = await db.execute(""" - SELECT DISTINCT ps.id, ps.name, ps.type1, ps.type2, ps.base_hp, ps.base_attack, - ps.base_defense, ps.base_speed, ps.evolution_level, ps.evolution_species_id, - ps.rarity, ps.emoji, + SELECT DISTINCT ps.*, evolve_to.name as evolves_to_name, (SELECT COUNT(*) FROM location_spawns ls WHERE ls.species_id = ps.id) as location_count FROM pet_species ps @@ -2610,8 +2237,8 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): 'id': row[0], 'name': row[1], 'type1': row[2], 'type2': row[3], 'base_hp': row[4], 'base_attack': row[5], 'base_defense': row[6], 'base_speed': row[7], 'evolution_level': row[8], - 'evolution_species_id': row[9], 'rarity': row[10], 'emoji': row[11], - 'evolves_to_name': row[12], 'location_count': row[13] + 'evolution_species_id': row[9], 'rarity': row[10], + 'evolves_to_name': row[11], 'location_count': row[12] } pets.append(pet_dict) @@ -2639,34 +2266,9 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): print(f"Database error fetching petdex: {e}") return [] - def remove_pet_duplicates(self, pets_list): - """Remove duplicate pets based on ID and sort by name""" - seen_ids = set() - unique_pets = [] - for pet in pets_list: - if pet['id'] not in seen_ids: - seen_ids.add(pet['id']) - unique_pets.append(pet) - return sorted(unique_pets, key=lambda x: x['name']) - - def serve_petdex_data(self, petdex_data, sort_mode='rarity', search_query=''): + def serve_petdex_data(self, petdex_data): """Serve petdex page with all pet species data""" - # Remove duplicates from input data first - petdex_data = self.remove_pet_duplicates(petdex_data) - - # Apply search filter if provided - if search_query: - search_query = search_query.lower() - filtered_data = [] - for pet in petdex_data: - # Search in name, type1, type2 - if (search_query in pet['name'].lower() or - search_query in pet['type1'].lower() or - (pet['type2'] and search_query in pet['type2'].lower())): - filtered_data.append(pet) - petdex_data = filtered_data - # Build pet cards HTML grouped by rarity rarity_names = {1: "Common", 2: "Uncommon", 3: "Rare", 4: "Epic", 5: "Legendary"} rarity_colors = {1: "#ffffff", 2: "#1eff00", 3: "#0070dd", 4: "#a335ee", 5: "#ff8000"} @@ -2705,395 +2307,20 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):
    """ - # Sort and group pets based on sort_mode + pets_by_rarity = {} + for pet in petdex_data: + rarity = pet['rarity'] + if rarity not in pets_by_rarity: + pets_by_rarity[rarity] = [] + pets_by_rarity[rarity].append(pet) + petdex_html = "" total_species = len(petdex_data) - if sort_mode == 'type': - # Group by type1 - pets_by_type = {} - for pet in petdex_data: - pet_type = pet['type1'] - if pet_type not in pets_by_type: - pets_by_type[pet_type] = [] - # Check for duplicates within this type - if pet not in pets_by_type[pet_type]: - pets_by_type[pet_type].append(pet) - - # Sort each type group by name and remove any remaining duplicates - for type_name in pets_by_type: - # Remove duplicates based on pet ID and sort by name - seen_ids = set() - unique_pets = [] - for pet in pets_by_type[type_name]: - if pet['id'] not in seen_ids: - seen_ids.add(pet['id']) - unique_pets.append(pet) - pets_by_type[type_name] = sorted(unique_pets, key=lambda x: x['name']) - - type_colors = { - 'Fire': '#F08030', 'Water': '#6890F0', 'Grass': '#78C850', 'Electric': '#F8D030', - 'Psychic': '#F85888', 'Ice': '#98D8D8', 'Dragon': '#7038F8', 'Dark': '#705848', - 'Fighting': '#C03028', 'Poison': '#A040A0', 'Ground': '#E0C068', 'Flying': '#A890F0', - 'Bug': '#A8B820', 'Rock': '#B8A038', 'Ghost': '#705898', 'Steel': '#B8B8D0', - 'Normal': '#A8A878', 'Fairy': '#EE99AC' - } - - for type_name in sorted(pets_by_type.keys()): - pets_in_type = pets_by_type[type_name] - type_color = type_colors.get(type_name, '#A8A878') - - petdex_html += f""" -
    -

    - {type_name} Type ({len(pets_in_type)} species) -

    -
    """ - - for pet in pets_in_type: - type_str = pet['type1'] - if pet['type2']: - type_str += f" / {pet['type2']}" - - petdex_html += f""" -
    -
    -

    {pet.get('emoji', '🐾')} {pet['name']}

    - {type_str} -
    -
    -
    - HP: - {pet['base_hp']} -
    -
    - Attack: - {pet['base_attack']} -
    -
    - Defense: - {pet['base_defense']} -
    -
    - Speed: - {pet['base_speed']} -
    -
    -
    - - {'ā˜…' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")} - -
    -
    """ - - petdex_html += """ -
    -
    """ - - elif sort_mode == 'name': - # Sort alphabetically by name (duplicates already removed) - sorted_pets = sorted(petdex_data, key=lambda x: x['name']) - - petdex_html += f""" -
    -

    - All Species (A-Z) ({len(sorted_pets)} total) -

    -
    """ - - for pet in sorted_pets: - type_str = pet['type1'] - if pet['type2']: - type_str += f" / {pet['type2']}" - - rarity_color = rarity_colors.get(pet['rarity'], '#ffffff') - - petdex_html += f""" -
    -
    -

    {pet.get('emoji', '🐾')} {pet['name']}

    - {type_str} -
    -
    -
    - HP: - {pet['base_hp']} -
    -
    - Attack: - {pet['base_attack']} -
    -
    - Defense: - {pet['base_defense']} -
    -
    - Speed: - {pet['base_speed']} -
    -
    -
    - - {'ā˜…' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")} - -
    -
    """ - - petdex_html += """ -
    -
    """ - - elif sort_mode == 'location': - # Group by spawn locations - pets_by_location = {} - pets_no_location = [] - - for pet in petdex_data: - if pet['spawn_locations']: - for location in pet['spawn_locations']: - loc_name = location['location_name'] - if loc_name not in pets_by_location: - pets_by_location[loc_name] = [] - # Check for duplicates within this location - if pet not in pets_by_location[loc_name]: - pets_by_location[loc_name].append(pet) - else: - pets_no_location.append(pet) - - # Sort each location group by name and remove any remaining duplicates - for location_name in pets_by_location: - # Remove duplicates based on pet ID and sort by name - seen_ids = set() - unique_pets = [] - for pet in pets_by_location[location_name]: - if pet['id'] not in seen_ids: - seen_ids.add(pet['id']) - unique_pets.append(pet) - pets_by_location[location_name] = sorted(unique_pets, key=lambda x: x['name']) - - location_colors = { - 'Starter Town': '#4CAF50', - 'Whispering Woods': '#2E7D32', - 'Thunder Peaks': '#FF9800', - 'Stone Caverns': '#795548', - 'Frozen Lake': '#2196F3', - 'Volcanic Crater': '#F44336' - } - - for location_name in sorted(pets_by_location.keys()): - pets_in_location = pets_by_location[location_name] - location_color = location_colors.get(location_name, '#A8A878') - - petdex_html += f""" -
    -

    - šŸ—ŗļø {location_name} ({len(pets_in_location)} species) -

    -
    """ - - for pet in pets_in_location: - type_str = pet['type1'] - if pet['type2']: - type_str += f" / {pet['type2']}" - - rarity_color = rarity_colors.get(pet['rarity'], '#ffffff') - - # Get level range for this location - level_range = "" - for location in pet['spawn_locations']: - if location['location_name'] == location_name: - level_range = f"Lv.{location['min_level']}-{location['max_level']}" - break - - petdex_html += f""" -
    -
    -

    {pet.get('emoji', '🐾')} {pet['name']}

    - {type_str} -
    -
    -
    - HP: - {pet['base_hp']} -
    -
    - Attack: - {pet['base_attack']} -
    -
    - Defense: - {pet['base_defense']} -
    -
    - Speed: - {pet['base_speed']} -
    -
    -
    - - šŸ“ {level_range} | {'ā˜…' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")} - -
    -
    """ - - petdex_html += """ -
    -
    """ - - # Add pets with no location at the end (remove duplicates) - if pets_no_location: - seen_ids = set() - unique_no_location = [] - for pet in pets_no_location: - if pet['id'] not in seen_ids: - seen_ids.add(pet['id']) - unique_no_location.append(pet) - pets_no_location = sorted(unique_no_location, key=lambda x: x['name']) - - if pets_no_location: - petdex_html += f""" -
    -

    - ā“ Unknown Locations ({len(pets_no_location)} species) -

    -
    """ - - for pet in pets_no_location: - type_str = pet['type1'] - if pet['type2']: - type_str += f" / {pet['type2']}" - - rarity_color = rarity_colors.get(pet['rarity'], '#ffffff') - - petdex_html += f""" -
    -
    -

    {pet.get('emoji', '🐾')} {pet['name']}

    - {type_str} -
    -
    -
    - HP: - {pet['base_hp']} -
    -
    - Attack: - {pet['base_attack']} -
    -
    - Defense: - {pet['base_defense']} -
    -
    - Speed: - {pet['base_speed']} -
    -
    -
    - - ā“ Location Unknown | {'ā˜…' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")} - -
    -
    """ - - petdex_html += """ -
    -
    """ - - elif sort_mode == 'all': - # Show all pets in a grid format without grouping (duplicates already removed) - sorted_pets = sorted(petdex_data, key=lambda x: (x['rarity'], x['name'])) - - petdex_html += f""" -
    -

    - šŸ“‹ All Pet Species ({len(sorted_pets)} total) -

    -
    """ - - for pet in sorted_pets: - type_str = pet['type1'] - if pet['type2']: - type_str += f" / {pet['type2']}" - - rarity_color = rarity_colors.get(pet['rarity'], '#ffffff') - - # Get all spawn locations - location_text = "" - if pet['spawn_locations']: - locations = [f"{loc['location_name']} (Lv.{loc['min_level']}-{loc['max_level']})" - for loc in pet['spawn_locations'][:2]] - if len(pet['spawn_locations']) > 2: - locations.append(f"+{len(pet['spawn_locations']) - 2} more") - location_text = f"šŸ“ {', '.join(locations)}" - else: - location_text = "šŸ“ Location Unknown" - - petdex_html += f""" -
    -
    -

    {pet.get('emoji', '🐾')} {pet['name']}

    - {type_str} -
    -
    -
    - HP: - {pet['base_hp']} -
    -
    - Attack: - {pet['base_attack']} -
    -
    - Defense: - {pet['base_defense']} -
    -
    - Speed: - {pet['base_speed']} -
    -
    -
    - - {location_text} - -
    -
    - - {'ā˜…' * pet['rarity']} {rarity_names.get(pet['rarity'], f"Rarity {pet['rarity']}")} - -
    -
    """ - - petdex_html += """ -
    -
    """ - - else: # Default to rarity sorting - pets_by_rarity = {} - for pet in petdex_data: - rarity = pet['rarity'] - if rarity not in pets_by_rarity: - pets_by_rarity[rarity] = [] - # Check for duplicates within this rarity - if pet not in pets_by_rarity[rarity]: - pets_by_rarity[rarity].append(pet) - - # Sort each rarity group by name and remove any remaining duplicates - for rarity in pets_by_rarity: - # Remove duplicates based on pet ID and sort by name - seen_ids = set() - unique_pets = [] - for pet in pets_by_rarity[rarity]: - if pet['id'] not in seen_ids: - seen_ids.add(pet['id']) - unique_pets.append(pet) - pets_by_rarity[rarity] = sorted(unique_pets, key=lambda x: x['name']) - - for rarity in sorted(pets_by_rarity.keys()): - pets_in_rarity = pets_by_rarity[rarity] - rarity_name = rarity_names.get(rarity, f"Rarity {rarity}") - rarity_color = rarity_colors.get(rarity, "#ffffff") + for rarity in sorted(pets_by_rarity.keys()): + pets_in_rarity = pets_by_rarity[rarity] + rarity_name = rarity_names.get(rarity, f"Rarity {rarity}") + rarity_color = rarity_colors.get(rarity, "#ffffff") petdex_html += f"""
    @@ -3132,7 +2359,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): petdex_html += f"""
    -

    {pet.get('emoji', '🐾')} {pet['name']}

    +

    {pet['name']}

    {type_str}
    @@ -3162,45 +2389,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler):

    The petdex appears to be empty. Contact an administrator.

    """ - # Create search interface - search_interface = f""" - - """ - - # Determine header text based on sort mode - if sort_mode == 'type': - header_text = "šŸ“Š Pet Collection by Type" - description = "šŸ”· Pets are organized by their primary type. Each type has different strengths and weaknesses!" - elif sort_mode == 'name': - header_text = "šŸ“Š Pet Collection (A-Z)" - description = "šŸ”¤ All pets sorted alphabetically by name. Perfect for finding specific species!" - elif sort_mode == 'location': - header_text = "šŸ“Š Pet Collection by Location" - description = "šŸ—ŗļø Pets are organized by where they can be found. Use !travel <location> to visit these areas!" - elif sort_mode == 'all': - header_text = "šŸ“Š Complete Pet Collection" - description = "šŸ“‹ All pets displayed in a comprehensive grid view with locations and stats!" - else: - header_text = "šŸ“Š Pet Collection by Rarity" - description = "🌟 Pets are organized by rarity. Use !wild <location> in #petz to see what spawns where!" - # Combine all content content = f"""
    @@ -3210,11 +2398,9 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): {stats_content} - {search_interface} -
    -

    {header_text}

    -

    {description}

    +

    šŸ“Š Pet Collection by Rarity

    +

    šŸŽÆ Pets are organized by rarity. Use !wild <location> in #petz to see what spawns where!

    {petdex_html}
    @@ -3292,7 +2478,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): # Get player pets cursor = await db.execute(""" - SELECT p.*, ps.name as species_name, ps.type1, ps.type2, ps.emoji + SELECT p.*, ps.name as species_name, ps.type1, ps.type2 FROM pets p JOIN pet_species ps ON p.species_id = ps.id WHERE p.player_id = ? @@ -3307,8 +2493,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): 'hp': row[6], 'max_hp': row[7], 'attack': row[8], 'defense': row[9], 'speed': row[10], 'happiness': row[11], 'caught_at': row[12], 'is_active': bool(row[13]), # Convert to proper boolean - 'team_order': row[14], 'species_name': row[15], 'type1': row[16], 'type2': row[17], - 'emoji': row[18] if row[18] else '🐾' # Add emoji support + 'team_order': row[14], 'species_name': row[15], 'type1': row[16], 'type2': row[17] } pets.append(pet_dict) @@ -3532,7 +2717,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): pets_html += f""" {status} - {pet.get('emoji', '🐾')} {name} + {name} {pet['species_name']} {type_str} {pet['level']} @@ -4271,9 +3456,6 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): if pet['type2']: type_str += f"/{pet['type2']}" - # Get emoji for the pet species - emoji = pet.get('emoji', '🐾') # Default to paw emoji if none specified - # Debug logging print(f"Making pet card for {name} (ID: {pet['id']}): is_active={pet['is_active']}, passed_is_active={is_active}, status_class={status_class}, team_order={pet.get('team_order', 'None')}") @@ -4284,7 +3466,7 @@ class PetBotRequestHandler(BaseHTTPRequestHandler): return f"""
    -

    {emoji} {name}

    +

    {name}

    {status}
    Level {pet['level']} {pet['species_name']}