Compare commits
10 commits
39ba55832d
...
c8cb99a4d0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8cb99a4d0 | ||
|
|
5ac3e36f0c | ||
|
|
f7fe4ce034 | ||
|
|
ac655b07e6 | ||
|
|
d05b2ead53 | ||
|
|
3c628c7f51 | ||
|
|
61463267c8 | ||
|
|
30dcb7e4bc | ||
|
|
ff14710987 | ||
|
|
8e9ff2960f |
15 changed files with 3145 additions and 1407 deletions
|
|
@ -99,7 +99,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- `!weather` - Check current weather
|
||||
- `!achievements` - View progress
|
||||
- `!activate/deactivate <pet>` - Manage team
|
||||
- `!swap <pet1> <pet2>` - Reorganize team
|
||||
- `!moves` - View pet abilities
|
||||
- `!flee` - Escape battles
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ A feature-rich IRC bot that brings Pokemon-style pet collecting and battling to
|
|||
- **Pet Collection**: Catch and collect different species of pets
|
||||
- **Exploration**: Travel between various themed locations
|
||||
- **Battle System**: Engage in turn-based battles with wild pets
|
||||
- **Team Management**: Activate/deactivate pets, swap team members
|
||||
- **Team Management**: Activate/deactivate pets, manage team composition
|
||||
- **Achievement System**: Unlock new areas by completing challenges
|
||||
- **Item Collection**: Discover and collect useful items during exploration
|
||||
|
||||
|
|
@ -78,7 +78,6 @@ A feature-rich IRC bot that brings Pokemon-style pet collecting and battling to
|
|||
### Pet Management
|
||||
- `!activate <pet>` - Activate a pet for battle
|
||||
- `!deactivate <pet>` - Move a pet to storage
|
||||
- `!swap <pet1> <pet2>` - Swap two pets' active status
|
||||
|
||||
### Inventory Commands
|
||||
- `!inventory` / `!inv` / `!items` - View your collected items
|
||||
|
|
|
|||
|
|
@ -430,11 +430,6 @@
|
|||
<div class="command-desc">Remove a pet from your active team and put it in storage.</div>
|
||||
<div class="command-example">Example: !deactivate aqua</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">!swap <pet1> <pet2></div>
|
||||
<div class="command-desc">Swap the active status of two pets - one becomes active, the other goes to storage.</div>
|
||||
<div class="command-example">Example: !swap leafy flamey</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,14 +19,12 @@ class Achievements(BaseModule):
|
|||
if not player:
|
||||
return
|
||||
|
||||
achievements = await self.database.get_player_achievements(player["id"])
|
||||
# Redirect to web interface for better achievements display
|
||||
self.send_message(channel, f"🏆 {nickname}: View your complete achievements at: http://petz.rdx4.com/player/{nickname}#achievements")
|
||||
|
||||
# Show quick summary in channel
|
||||
achievements = await self.database.get_player_achievements(player["id"])
|
||||
if achievements:
|
||||
self.send_message(channel, f"🏆 {nickname}'s Achievements:")
|
||||
for achievement in achievements[:5]: # Show last 5 achievements
|
||||
self.send_message(channel, f"• {achievement['name']}: {achievement['description']}")
|
||||
|
||||
if len(achievements) > 5:
|
||||
self.send_message(channel, f"... and {len(achievements) - 5} more!")
|
||||
self.send_message(channel, f"📊 Quick summary: {len(achievements)} achievements earned! Check the web interface for details.")
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: No achievements yet! Keep exploring and catching pets to unlock new areas!")
|
||||
self.send_message(channel, f"💡 No achievements yet! Keep exploring and catching pets to unlock new areas!")
|
||||
|
|
@ -12,6 +12,15 @@ class BaseModule(ABC):
|
|||
self.database = database
|
||||
self.game_engine = game_engine
|
||||
|
||||
@staticmethod
|
||||
def normalize_input(user_input):
|
||||
"""Normalize user input by converting to lowercase for case-insensitive command processing"""
|
||||
if isinstance(user_input, str):
|
||||
return user_input.lower()
|
||||
elif isinstance(user_input, list):
|
||||
return [item.lower() if isinstance(item, str) else item for item in user_input]
|
||||
return user_input
|
||||
|
||||
@abstractmethod
|
||||
def get_commands(self):
|
||||
"""Return list of commands this module handles"""
|
||||
|
|
|
|||
|
|
@ -52,6 +52,12 @@ class BattleSystem(BaseModule):
|
|||
self.send_message(channel, f"{nickname}: You're already in battle! Use !attack <move> or !flee.")
|
||||
return
|
||||
|
||||
# Check if already in gym battle
|
||||
gym_battle = await self.database.get_active_gym_battle(player["id"])
|
||||
if gym_battle:
|
||||
self.send_message(channel, f"{nickname}: You're already in a gym battle! Finish your gym battle first.")
|
||||
return
|
||||
|
||||
# Get player's active pet
|
||||
pets = await self.database.get_player_pets(player["id"], active_only=True)
|
||||
if not pets:
|
||||
|
|
@ -87,7 +93,7 @@ class BattleSystem(BaseModule):
|
|||
if not player:
|
||||
return
|
||||
|
||||
move_name = " ".join(args).title() # Normalize to Title Case
|
||||
move_name = " ".join(self.normalize_input(args)).title() # Normalize to Title Case
|
||||
result = await self.game_engine.battle_engine.execute_battle_turn(player["id"], move_name)
|
||||
|
||||
if "error" in result:
|
||||
|
|
|
|||
|
|
@ -40,5 +40,8 @@ class CoreCommands(BaseModule):
|
|||
if not player:
|
||||
return
|
||||
|
||||
# Show quick summary and direct to web interface for detailed stats
|
||||
self.send_message(channel,
|
||||
f"📊 {nickname}: Level {player['level']} | {player['experience']} XP | ${player['money']}")
|
||||
f"📊 {nickname}: Level {player['level']} | {player['experience']} XP | ${player['money']}")
|
||||
self.send_message(channel,
|
||||
f"🌐 View detailed statistics at: http://petz.rdx4.com/player/{nickname}#stats")
|
||||
|
|
@ -7,7 +7,7 @@ class Exploration(BaseModule):
|
|||
"""Handles exploration, travel, location, weather, and wild commands"""
|
||||
|
||||
def get_commands(self):
|
||||
return ["explore", "travel", "location", "where", "weather", "wild", "catch", "capture"]
|
||||
return ["explore", "travel", "location", "where", "weather", "wild", "catch", "capture", "flee"]
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
if command == "explore":
|
||||
|
|
@ -22,6 +22,8 @@ class Exploration(BaseModule):
|
|||
await self.cmd_wild(channel, nickname, args)
|
||||
elif command in ["catch", "capture"]:
|
||||
await self.cmd_catch(channel, nickname)
|
||||
elif command == "flee":
|
||||
await self.cmd_flee_encounter(channel, nickname)
|
||||
|
||||
async def cmd_explore(self, channel, nickname):
|
||||
"""Explore current location"""
|
||||
|
|
@ -29,6 +31,18 @@ class Exploration(BaseModule):
|
|||
if not player:
|
||||
return
|
||||
|
||||
# Check if player has an active encounter that must be resolved first
|
||||
if player["id"] in self.bot.active_encounters:
|
||||
current_encounter = self.bot.active_encounters[player["id"]]
|
||||
self.send_message(channel, f"{nickname}: You already have an active encounter with a wild {current_encounter['species_name']}! You must choose to !battle, !catch, or !flee before exploring again.")
|
||||
return
|
||||
|
||||
# Check if player is in an active battle
|
||||
active_battle = await self.game_engine.battle_engine.get_active_battle(player["id"])
|
||||
if active_battle:
|
||||
self.send_message(channel, f"{nickname}: You're currently in battle! Finish your battle before exploring.")
|
||||
return
|
||||
|
||||
encounter = await self.game_engine.explore_location(player["id"])
|
||||
|
||||
if encounter["type"] == "error":
|
||||
|
|
@ -51,7 +65,7 @@ class Exploration(BaseModule):
|
|||
|
||||
self.send_message(channel,
|
||||
f"🐾 {nickname}: A wild Level {pet['level']} {pet['species_name']} ({type_str}) appeared in {encounter['location']}!")
|
||||
self.send_message(channel, f"Choose your action: !battle to fight it, or !catch to try catching it directly!")
|
||||
self.send_message(channel, f"Choose your action: !battle to fight it, !catch to try catching it directly, or !flee to escape!")
|
||||
|
||||
async def cmd_travel(self, channel, nickname, args):
|
||||
"""Travel to a different location"""
|
||||
|
|
@ -64,7 +78,7 @@ class Exploration(BaseModule):
|
|||
return
|
||||
|
||||
# Handle various input formats and normalize location names
|
||||
destination_input = " ".join(args).lower()
|
||||
destination_input = self.normalize_input(" ".join(args))
|
||||
|
||||
# Map common variations to exact location names
|
||||
location_mappings = {
|
||||
|
|
@ -82,7 +96,7 @@ class Exploration(BaseModule):
|
|||
destination = location_mappings.get(destination_input)
|
||||
if not destination:
|
||||
# Fall back to title case if no mapping found
|
||||
destination = " ".join(args).title()
|
||||
destination = " ".join(self.normalize_input(args)).title()
|
||||
|
||||
location = await self.database.get_location_by_name(destination)
|
||||
|
||||
|
|
@ -171,7 +185,7 @@ class Exploration(BaseModule):
|
|||
|
||||
if args:
|
||||
# Specific location requested
|
||||
location_name = " ".join(args).title()
|
||||
location_name = " ".join(self.normalize_input(args)).title()
|
||||
else:
|
||||
# Default to current location
|
||||
current_location = await self.database.get_player_location(player["id"])
|
||||
|
|
@ -196,6 +210,13 @@ class Exploration(BaseModule):
|
|||
|
||||
# Check if player is in an active battle
|
||||
active_battle = await self.game_engine.battle_engine.get_active_battle(player["id"])
|
||||
gym_battle = await self.database.get_active_gym_battle(player["id"])
|
||||
|
||||
if gym_battle:
|
||||
# Can't catch pets during gym battles
|
||||
self.send_message(channel, f"{nickname}: You can't catch pets during gym battles! Focus on the challenge!")
|
||||
return
|
||||
|
||||
if active_battle:
|
||||
# Catching during battle
|
||||
wild_pet = active_battle["wild_pet"]
|
||||
|
|
@ -297,4 +318,36 @@ class Exploration(BaseModule):
|
|||
"""Display level up information (shared with battle system)"""
|
||||
from .battle_system import BattleSystem
|
||||
battle_system = BattleSystem(self.bot, self.database, self.game_engine)
|
||||
await battle_system.handle_level_up_display(channel, nickname, exp_result)
|
||||
await battle_system.handle_level_up_display(channel, nickname, exp_result)
|
||||
|
||||
async def cmd_flee_encounter(self, channel, nickname):
|
||||
"""Flee from an active encounter without battling"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
# Check if player has an active encounter to flee from
|
||||
if player["id"] not in self.bot.active_encounters:
|
||||
self.send_message(channel, f"{nickname}: You don't have an active encounter to flee from!")
|
||||
return
|
||||
|
||||
# Check if player is in an active battle - can't flee from exploration if in battle
|
||||
active_battle = await self.game_engine.battle_engine.get_active_battle(player["id"])
|
||||
if active_battle:
|
||||
self.send_message(channel, f"{nickname}: You're in battle! Use the battle system's !flee command to escape combat.")
|
||||
return
|
||||
|
||||
# Check if player is in a gym battle
|
||||
gym_battle = await self.database.get_active_gym_battle(player["id"])
|
||||
if gym_battle:
|
||||
self.send_message(channel, f"{nickname}: You're in a gym battle! Use !forfeit to leave the gym challenge.")
|
||||
return
|
||||
|
||||
# Get encounter details for message
|
||||
encounter = self.bot.active_encounters[player["id"]]
|
||||
|
||||
# Remove the encounter
|
||||
del self.bot.active_encounters[player["id"]]
|
||||
|
||||
self.send_message(channel, f"💨 {nickname}: You fled from the wild {encounter['species_name']}! You can now explore again.")
|
||||
self.send_message(channel, f"💡 Use !explore to search for another encounter!")
|
||||
|
|
@ -13,13 +13,13 @@ class GymBattles(BaseModule):
|
|||
if command == "gym":
|
||||
if not args:
|
||||
await self.cmd_gym_list(channel, nickname)
|
||||
elif args[0] == "list":
|
||||
elif self.normalize_input(args[0]) == "list":
|
||||
await self.cmd_gym_list_all(channel, nickname)
|
||||
elif args[0] == "challenge":
|
||||
elif self.normalize_input(args[0]) == "challenge":
|
||||
await self.cmd_gym_challenge(channel, nickname, args[1:])
|
||||
elif args[0] == "info":
|
||||
elif self.normalize_input(args[0]) == "info":
|
||||
await self.cmd_gym_info(channel, nickname, args[1:])
|
||||
elif args[0] == "status":
|
||||
elif self.normalize_input(args[0]) == "status":
|
||||
await self.cmd_gym_status(channel, nickname)
|
||||
else:
|
||||
await self.cmd_gym_list(channel, nickname)
|
||||
|
|
@ -66,7 +66,7 @@ class GymBattles(BaseModule):
|
|||
f" Status: {status} | Next difficulty: {difficulty}")
|
||||
|
||||
self.send_message(channel,
|
||||
f"💡 Use '!gym challenge \"<gym name>\"' to battle!")
|
||||
f"💡 Use '!gym challenge' to battle (gym name optional if only one gym in location)!")
|
||||
|
||||
async def cmd_gym_list_all(self, channel, nickname):
|
||||
"""List all gyms across all locations"""
|
||||
|
|
@ -97,10 +97,6 @@ class GymBattles(BaseModule):
|
|||
|
||||
async def cmd_gym_challenge(self, channel, nickname, args):
|
||||
"""Challenge a gym"""
|
||||
if not args:
|
||||
self.send_message(channel, f"{nickname}: Specify a gym to challenge! Example: !gym challenge \"Forest Guardian\"")
|
||||
return
|
||||
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
|
@ -111,19 +107,37 @@ class GymBattles(BaseModule):
|
|||
self.send_message(channel, f"{nickname}: You are not in a valid location! Use !travel to go somewhere first.")
|
||||
return
|
||||
|
||||
gym_name = " ".join(args).strip('"')
|
||||
# Get available gyms in current location
|
||||
available_gyms = await self.database.get_gyms_in_location(location["id"])
|
||||
if not available_gyms:
|
||||
self.send_message(channel, f"{nickname}: No gyms found in {location['name']}! Try traveling to a different location.")
|
||||
return
|
||||
|
||||
# Look for gym in player's current location (case-insensitive)
|
||||
gym = await self.database.get_gym_by_name_in_location(gym_name, location["id"])
|
||||
if not gym:
|
||||
# List available gyms in current location for helpful error message
|
||||
available_gyms = await self.database.get_gyms_in_location(location["id"])
|
||||
if available_gyms:
|
||||
gym = None
|
||||
|
||||
if not args:
|
||||
# No gym name provided - auto-challenge if only one gym, otherwise list options
|
||||
if len(available_gyms) == 1:
|
||||
gym = available_gyms[0]
|
||||
self.send_message(channel, f"🏛️ {nickname}: Challenging the {gym['name']} gym in {location['name']}!")
|
||||
else:
|
||||
# Multiple gyms - show list and ask user to specify
|
||||
gym_list = ", ".join([f'"{g["name"]}"' for g in available_gyms])
|
||||
self.send_message(channel, f"{nickname}: Multiple gyms found in {location['name']}! Specify which gym to challenge:")
|
||||
self.send_message(channel, f"Available gyms: {gym_list}")
|
||||
self.send_message(channel, f"💡 Use: !gym challenge \"<gym name>\"")
|
||||
return
|
||||
else:
|
||||
# Gym name provided - find specific gym
|
||||
gym_name = " ".join(self.normalize_input(args)).strip('"')
|
||||
|
||||
# Look for gym in player's current location (case-insensitive)
|
||||
gym = await self.database.get_gym_by_name_in_location(gym_name, location["id"])
|
||||
if not gym:
|
||||
# List available gyms in current location for helpful error message
|
||||
gym_list = ", ".join([f'"{g["name"]}"' for g in available_gyms])
|
||||
self.send_message(channel, f"{nickname}: No gym named '{gym_name}' found in {location['name']}! Available gyms: {gym_list}")
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: No gyms found in {location['name']}! Try traveling to a different location.")
|
||||
return
|
||||
return
|
||||
|
||||
# Check if player has active pets
|
||||
active_pets = await self.database.get_active_pets(player["id"])
|
||||
|
|
@ -266,7 +280,7 @@ class GymBattles(BaseModule):
|
|||
if not player:
|
||||
return
|
||||
|
||||
gym_name = " ".join(args).strip('"')
|
||||
gym_name = " ".join(self.normalize_input(args)).strip('"')
|
||||
|
||||
# First try to find gym in player's current location
|
||||
location = await self.database.get_player_location(player["id"])
|
||||
|
|
@ -311,7 +325,7 @@ class GymBattles(BaseModule):
|
|||
return
|
||||
|
||||
# This will show a summary - for detailed view they can use !gym list
|
||||
self.send_message(channel, f"🏆 {nickname}: Use !gym list to see all gym progress, or check your profile at: http://petz.rdx4.com/player/{nickname}")
|
||||
self.send_message(channel, f"🏆 {nickname}: Use !gym list to see all gym progress, or check your profile at: http://petz.rdx4.com/player/{nickname}#gym-badges")
|
||||
|
||||
async def cmd_forfeit(self, channel, nickname):
|
||||
"""Forfeit the current gym battle"""
|
||||
|
|
|
|||
|
|
@ -16,51 +16,14 @@ class Inventory(BaseModule):
|
|||
await self.cmd_use_item(channel, nickname, args)
|
||||
|
||||
async def cmd_inventory(self, channel, nickname):
|
||||
"""Display player's inventory"""
|
||||
"""Redirect player to their web profile for inventory management"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
inventory = await self.database.get_player_inventory(player["id"])
|
||||
|
||||
if not inventory:
|
||||
self.send_message(channel, f"🎒 {nickname}: Your inventory is empty! Try exploring to find items.")
|
||||
return
|
||||
|
||||
# Group items by category
|
||||
categories = {}
|
||||
for item in inventory:
|
||||
category = item["category"]
|
||||
if category not in categories:
|
||||
categories[category] = []
|
||||
categories[category].append(item)
|
||||
|
||||
# Send inventory summary first
|
||||
total_items = sum(item["quantity"] for item in inventory)
|
||||
self.send_message(channel, f"🎒 {nickname}'s Inventory ({total_items} items):")
|
||||
|
||||
# Display items by category
|
||||
rarity_symbols = {
|
||||
"common": "○",
|
||||
"uncommon": "◇",
|
||||
"rare": "◆",
|
||||
"epic": "★",
|
||||
"legendary": "✦"
|
||||
}
|
||||
|
||||
for category, items in categories.items():
|
||||
category_display = category.replace("_", " ").title()
|
||||
self.send_message(channel, f"📦 {category_display}:")
|
||||
|
||||
for item in items[:5]: # Limit to 5 items per category to avoid spam
|
||||
symbol = rarity_symbols.get(item["rarity"], "○")
|
||||
quantity_str = f" x{item['quantity']}" if item["quantity"] > 1 else ""
|
||||
self.send_message(channel, f" {symbol} {item['name']}{quantity_str} - {item['description']}")
|
||||
|
||||
if len(items) > 5:
|
||||
self.send_message(channel, f" ... and {len(items) - 5} more items")
|
||||
|
||||
self.send_message(channel, f"💡 Use '!use <item name>' to use consumable items!")
|
||||
# Redirect to web interface for better inventory management
|
||||
self.send_message(channel, f"🎒 {nickname}: View your complete inventory at: http://petz.rdx4.com/player/{nickname}#inventory")
|
||||
self.send_message(channel, f"💡 The web interface shows detailed item information, categories, and usage options!")
|
||||
|
||||
async def cmd_use_item(self, channel, nickname, args):
|
||||
"""Use an item from inventory"""
|
||||
|
|
@ -72,7 +35,7 @@ class Inventory(BaseModule):
|
|||
if not player:
|
||||
return
|
||||
|
||||
item_name = " ".join(args)
|
||||
item_name = " ".join(self.normalize_input(args))
|
||||
result = await self.database.use_item(player["id"], item_name)
|
||||
|
||||
if not result["success"]:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class PetManagement(BaseModule):
|
|||
"""Handles team, pets, and future pet management commands"""
|
||||
|
||||
def get_commands(self):
|
||||
return ["team", "pets", "activate", "deactivate", "swap", "nickname"]
|
||||
return ["team", "pets", "activate", "deactivate", "nickname"]
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
if command == "team":
|
||||
|
|
@ -18,8 +18,6 @@ class PetManagement(BaseModule):
|
|||
await self.cmd_activate(channel, nickname, args)
|
||||
elif command == "deactivate":
|
||||
await self.cmd_deactivate(channel, nickname, args)
|
||||
elif command == "swap":
|
||||
await self.cmd_swap(channel, nickname, args)
|
||||
elif command == "nickname":
|
||||
await self.cmd_nickname(channel, nickname, args)
|
||||
|
||||
|
|
@ -40,7 +38,7 @@ class PetManagement(BaseModule):
|
|||
|
||||
team_info = []
|
||||
|
||||
# Active pets with star
|
||||
# Active pets with star and team position
|
||||
for pet in active_pets:
|
||||
name = pet["nickname"] or pet["species_name"]
|
||||
|
||||
|
|
@ -55,7 +53,9 @@ class PetManagement(BaseModule):
|
|||
else:
|
||||
exp_display = f"{exp_needed} to next"
|
||||
|
||||
team_info.append(f"⭐{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP | EXP: {exp_display}")
|
||||
# Show team position
|
||||
position = pet.get("team_order", "?")
|
||||
team_info.append(f"[{position}]⭐{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP | EXP: {exp_display}")
|
||||
|
||||
# Inactive pets
|
||||
for pet in inactive_pets[:5]: # Show max 5 inactive
|
||||
|
|
@ -66,6 +66,7 @@ class PetManagement(BaseModule):
|
|||
team_info.append(f"... and {len(inactive_pets) - 5} more in storage")
|
||||
|
||||
self.send_message(channel, f"🐾 {nickname}'s team: " + " | ".join(team_info))
|
||||
self.send_message(channel, f"🌐 View detailed pet collection at: http://petz.rdx4.com/player/{nickname}#pets")
|
||||
|
||||
async def cmd_pets(self, channel, nickname):
|
||||
"""Show link to pet collection web page"""
|
||||
|
|
@ -74,7 +75,7 @@ class PetManagement(BaseModule):
|
|||
return
|
||||
|
||||
# Send URL to player's profile page instead of PM spam
|
||||
self.send_message(channel, f"{nickname}: View your complete pet collection at: http://petz.rdx4.com/player/{nickname}")
|
||||
self.send_message(channel, f"{nickname}: View your complete pet collection at: http://petz.rdx4.com/player/{nickname}#pets")
|
||||
|
||||
async def cmd_activate(self, channel, nickname, args):
|
||||
"""Activate a pet for battle (PM only)"""
|
||||
|
|
@ -88,13 +89,14 @@ class PetManagement(BaseModule):
|
|||
if not player:
|
||||
return
|
||||
|
||||
pet_name = " ".join(args)
|
||||
pet_name = " ".join(self.normalize_input(args))
|
||||
result = await self.database.activate_pet(player["id"], pet_name)
|
||||
|
||||
if result["success"]:
|
||||
pet = result["pet"]
|
||||
display_name = pet["nickname"] or pet["species_name"]
|
||||
self.send_pm(nickname, f"✅ {display_name} is now active for battle!")
|
||||
position = result.get("team_position", "?")
|
||||
self.send_pm(nickname, f"✅ {display_name} is now active for battle! Team position: {position}")
|
||||
self.send_message(channel, f"{nickname}: Pet activated successfully!")
|
||||
else:
|
||||
self.send_pm(nickname, f"❌ {result['error']}")
|
||||
|
|
@ -112,7 +114,7 @@ class PetManagement(BaseModule):
|
|||
if not player:
|
||||
return
|
||||
|
||||
pet_name = " ".join(args)
|
||||
pet_name = " ".join(self.normalize_input(args))
|
||||
result = await self.database.deactivate_pet(player["id"], pet_name)
|
||||
|
||||
if result["success"]:
|
||||
|
|
@ -124,44 +126,6 @@ class PetManagement(BaseModule):
|
|||
self.send_pm(nickname, f"❌ {result['error']}")
|
||||
self.send_message(channel, f"{nickname}: Pet deactivation failed - check PM for details!")
|
||||
|
||||
async def cmd_swap(self, channel, nickname, args):
|
||||
"""Swap active/storage status of two pets (PM only)"""
|
||||
# Redirect to PM for privacy
|
||||
if len(args) < 2:
|
||||
self.send_pm(nickname, "Usage: !swap <pet1> <pet2>")
|
||||
self.send_pm(nickname, "Example: !swap Flamey Aqua")
|
||||
self.send_message(channel, f"{nickname}: Pet swap instructions sent via PM!")
|
||||
return
|
||||
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
# Handle multi-word pet names by splitting on first space vs last space
|
||||
if len(args) == 2:
|
||||
pet1_name, pet2_name = args
|
||||
else:
|
||||
# For more complex parsing, assume equal split
|
||||
mid_point = len(args) // 2
|
||||
pet1_name = " ".join(args[:mid_point])
|
||||
pet2_name = " ".join(args[mid_point:])
|
||||
|
||||
result = await self.database.swap_pets(player["id"], pet1_name, pet2_name)
|
||||
|
||||
if result["success"]:
|
||||
pet1 = result["pet1"]
|
||||
pet2 = result["pet2"]
|
||||
pet1_display = pet1["nickname"] or pet1["species_name"]
|
||||
pet2_display = pet2["nickname"] or pet2["species_name"]
|
||||
|
||||
self.send_pm(nickname, f"🔄 Swap complete!")
|
||||
self.send_pm(nickname, f" • {pet1_display} → {result['pet1_now']}")
|
||||
self.send_pm(nickname, f" • {pet2_display} → {result['pet2_now']}")
|
||||
self.send_message(channel, f"{nickname}: Pet swap completed!")
|
||||
else:
|
||||
self.send_pm(nickname, f"❌ {result['error']}")
|
||||
self.send_message(channel, f"{nickname}: Pet swap failed - check PM for details!")
|
||||
|
||||
async def cmd_nickname(self, channel, nickname, args):
|
||||
"""Set a nickname for a pet"""
|
||||
if len(args) < 2:
|
||||
|
|
@ -174,7 +138,7 @@ class PetManagement(BaseModule):
|
|||
return
|
||||
|
||||
# Split args into pet identifier and new nickname
|
||||
pet_identifier = args[0]
|
||||
pet_identifier = self.normalize_input(args[0])
|
||||
new_nickname = " ".join(args[1:])
|
||||
|
||||
result = await self.database.set_pet_nickname(player["id"], pet_identifier, new_nickname)
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class PetBotDebug:
|
|||
print("✅ Background validation started")
|
||||
|
||||
print("🔄 Starting web server...")
|
||||
self.web_server = PetBotWebServer(self.database, port=8080)
|
||||
self.web_server = PetBotWebServer(self.database, port=8080, bot=self)
|
||||
self.web_server.start_in_thread()
|
||||
print("✅ Web server started")
|
||||
|
||||
|
|
@ -303,12 +303,14 @@ class PetBotDebug:
|
|||
self.handle_command(channel, nickname, message)
|
||||
|
||||
def handle_command(self, channel, nickname, message):
|
||||
from modules.base_module import BaseModule
|
||||
|
||||
command_parts = message[1:].split()
|
||||
if not command_parts:
|
||||
return
|
||||
|
||||
command = command_parts[0].lower()
|
||||
args = command_parts[1:]
|
||||
command = BaseModule.normalize_input(command_parts[0])
|
||||
args = BaseModule.normalize_input(command_parts[1:])
|
||||
|
||||
try:
|
||||
if command in self.command_map:
|
||||
|
|
|
|||
238
src/database.py
238
src/database.py
|
|
@ -120,6 +120,46 @@ class Database:
|
|||
except:
|
||||
pass # Column already exists
|
||||
|
||||
# Add team_order column if it doesn't exist
|
||||
try:
|
||||
await db.execute("ALTER TABLE pets ADD COLUMN team_order INTEGER DEFAULT NULL")
|
||||
await db.commit()
|
||||
print("Added team_order column to pets table")
|
||||
except:
|
||||
pass # Column already exists
|
||||
|
||||
# Migrate existing active pets to have team_order values
|
||||
try:
|
||||
# Find active pets without team_order
|
||||
cursor = await db.execute("""
|
||||
SELECT id, player_id FROM pets
|
||||
WHERE is_active = TRUE AND team_order IS NULL
|
||||
ORDER BY player_id, id
|
||||
""")
|
||||
pets_to_migrate = await cursor.fetchall()
|
||||
|
||||
if pets_to_migrate:
|
||||
print(f"Migrating {len(pets_to_migrate)} active pets to have team_order values...")
|
||||
|
||||
# Group pets by player
|
||||
from collections import defaultdict
|
||||
pets_by_player = defaultdict(list)
|
||||
for pet in pets_to_migrate:
|
||||
pets_by_player[pet[1]].append(pet[0])
|
||||
|
||||
# Assign team_order values for each player
|
||||
for player_id, pet_ids in pets_by_player.items():
|
||||
for i, pet_id in enumerate(pet_ids[:6]): # Max 6 pets per team
|
||||
await db.execute("""
|
||||
UPDATE pets SET team_order = ? WHERE id = ?
|
||||
""", (i + 1, pet_id))
|
||||
|
||||
await db.commit()
|
||||
print("Migration completed successfully")
|
||||
except Exception as e:
|
||||
print(f"Migration warning: {e}")
|
||||
pass # Don't fail if migration has issues
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS location_spawns (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
|
@ -408,6 +448,9 @@ class Database:
|
|||
if active_only:
|
||||
query += " AND p.is_active = TRUE"
|
||||
|
||||
# Order by team position for active pets, then by id for storage pets
|
||||
query += " ORDER BY CASE WHEN p.is_active THEN COALESCE(p.team_order, 999) ELSE 999 END ASC, p.id ASC"
|
||||
|
||||
cursor = await db.execute(query, params)
|
||||
rows = await cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
|
@ -638,11 +681,16 @@ class Database:
|
|||
if not pet:
|
||||
return {"success": False, "error": f"No inactive pet found named '{pet_identifier}'"}
|
||||
|
||||
# Activate the pet
|
||||
await db.execute("UPDATE pets SET is_active = TRUE WHERE id = ?", (pet["id"],))
|
||||
# Get next available team slot
|
||||
next_slot = await self.get_next_available_team_slot(player_id)
|
||||
if next_slot is None:
|
||||
return {"success": False, "error": "Team is full (maximum 6 pets)"}
|
||||
|
||||
# Activate the pet and assign team position
|
||||
await db.execute("UPDATE pets SET is_active = TRUE, team_order = ? WHERE id = ?", (next_slot, pet["id"]))
|
||||
await db.commit()
|
||||
|
||||
return {"success": True, "pet": dict(pet)}
|
||||
return {"success": True, "pet": dict(pet), "team_position": next_slot}
|
||||
|
||||
async def deactivate_pet(self, player_id: int, pet_identifier: str) -> Dict:
|
||||
"""Deactivate a pet by name or species name. Returns result dict."""
|
||||
|
|
@ -670,58 +718,122 @@ class Database:
|
|||
if active_count["count"] <= 1:
|
||||
return {"success": False, "error": "You must have at least one active pet!"}
|
||||
|
||||
# Deactivate the pet
|
||||
await db.execute("UPDATE pets SET is_active = FALSE WHERE id = ?", (pet["id"],))
|
||||
# Deactivate the pet and clear team order
|
||||
await db.execute("UPDATE pets SET is_active = FALSE, team_order = NULL WHERE id = ?", (pet["id"],))
|
||||
await db.commit()
|
||||
|
||||
return {"success": True, "pet": dict(pet)}
|
||||
|
||||
async def swap_pets(self, player_id: int, pet1_identifier: str, pet2_identifier: str) -> Dict:
|
||||
"""Swap the active status of two pets. Returns result dict."""
|
||||
# Team Order Methods
|
||||
async def get_next_available_team_slot(self, player_id: int) -> int:
|
||||
"""Get the next available team slot (1-6)"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
|
||||
# Find both pets
|
||||
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.player_id = ?
|
||||
AND (p.nickname = ? OR ps.name = ?)
|
||||
LIMIT 1
|
||||
""", (player_id, pet1_identifier, pet1_identifier))
|
||||
pet1 = await cursor.fetchone()
|
||||
SELECT team_order FROM pets
|
||||
WHERE player_id = ? AND is_active = TRUE AND team_order IS NOT NULL
|
||||
ORDER BY team_order ASC
|
||||
""", (player_id,))
|
||||
used_slots = [row[0] for row in await cursor.fetchall()]
|
||||
|
||||
# Find first available slot (1-6)
|
||||
for slot in range(1, 7):
|
||||
if slot not in used_slots:
|
||||
return slot
|
||||
return None # Team is full
|
||||
|
||||
async def set_pet_team_order(self, player_id: int, pet_id: int, position: int) -> Dict:
|
||||
"""Set a pet's team order position (1-6)"""
|
||||
if position < 1 or position > 6:
|
||||
return {"success": False, "error": "Team position must be between 1-6"}
|
||||
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
# Check if pet belongs to player
|
||||
cursor = await db.execute("SELECT * FROM pets WHERE id = ? AND player_id = ?", (pet_id, player_id))
|
||||
pet = await cursor.fetchone()
|
||||
if not pet:
|
||||
return {"success": False, "error": "Pet not found"}
|
||||
|
||||
# Check if position is already taken
|
||||
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.player_id = ?
|
||||
AND (p.nickname = ? OR ps.name = ?)
|
||||
LIMIT 1
|
||||
""", (player_id, pet2_identifier, pet2_identifier))
|
||||
pet2 = await cursor.fetchone()
|
||||
SELECT id FROM pets
|
||||
WHERE player_id = ? AND team_order = ? AND is_active = TRUE AND id != ?
|
||||
""", (player_id, position, pet_id))
|
||||
existing_pet = await cursor.fetchone()
|
||||
|
||||
if not pet1:
|
||||
return {"success": False, "error": f"Pet '{pet1_identifier}' not found"}
|
||||
if not pet2:
|
||||
return {"success": False, "error": f"Pet '{pet2_identifier}' not found"}
|
||||
if existing_pet:
|
||||
return {"success": False, "error": f"Position {position} is already taken"}
|
||||
|
||||
if pet1["id"] == pet2["id"]:
|
||||
return {"success": False, "error": "Cannot swap a pet with itself"}
|
||||
# Update pet's team order and make it active
|
||||
await db.execute("""
|
||||
UPDATE pets SET team_order = ?, is_active = TRUE
|
||||
WHERE id = ? AND player_id = ?
|
||||
""", (position, pet_id, player_id))
|
||||
|
||||
# Swap their active status
|
||||
await db.execute("UPDATE pets SET is_active = ? WHERE id = ?", (not pet1["is_active"], pet1["id"]))
|
||||
await db.execute("UPDATE pets SET is_active = ? WHERE id = ?", (not pet2["is_active"], pet2["id"]))
|
||||
await db.commit()
|
||||
return {"success": True, "position": position}
|
||||
|
||||
async def reorder_team_positions(self, player_id: int, new_positions: List[Dict]) -> Dict:
|
||||
"""Reorder team positions based on new arrangement"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
try:
|
||||
# Validate all positions are 1-6 and no duplicates
|
||||
positions = [pos["position"] for pos in new_positions]
|
||||
if len(set(positions)) != len(positions):
|
||||
return {"success": False, "error": "Duplicate positions detected"}
|
||||
|
||||
for pos_data in new_positions:
|
||||
position = pos_data["position"]
|
||||
pet_id = pos_data["pet_id"]
|
||||
|
||||
if position < 1 or position > 6:
|
||||
return {"success": False, "error": f"Invalid position {position}"}
|
||||
|
||||
# Verify pet belongs to player
|
||||
cursor = await db.execute("SELECT id FROM pets WHERE id = ? AND player_id = ?", (pet_id, player_id))
|
||||
if not await cursor.fetchone():
|
||||
return {"success": False, "error": f"Pet {pet_id} not found"}
|
||||
|
||||
# Clear all team orders first
|
||||
await db.execute("UPDATE pets SET team_order = NULL WHERE player_id = ?", (player_id,))
|
||||
|
||||
# Set new positions
|
||||
for pos_data in new_positions:
|
||||
await db.execute("""
|
||||
UPDATE pets SET team_order = ?, is_active = TRUE
|
||||
WHERE id = ? AND player_id = ?
|
||||
""", (pos_data["position"], pos_data["pet_id"], player_id))
|
||||
|
||||
await db.commit()
|
||||
return {"success": True, "message": "Team order updated successfully"}
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
async def remove_from_team_position(self, player_id: int, pet_id: int) -> Dict:
|
||||
"""Remove a pet from team (set to inactive and clear team_order)"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
# Check if pet belongs to player
|
||||
cursor = await db.execute("SELECT * FROM pets WHERE id = ? AND player_id = ?", (pet_id, player_id))
|
||||
pet = await cursor.fetchone()
|
||||
if not pet:
|
||||
return {"success": False, "error": "Pet not found"}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"pet1": dict(pet1),
|
||||
"pet2": dict(pet2),
|
||||
"pet1_now": "active" if not pet1["is_active"] else "storage",
|
||||
"pet2_now": "active" if not pet2["is_active"] else "storage"
|
||||
}
|
||||
# Check if this is the only active pet
|
||||
cursor = await db.execute("SELECT COUNT(*) as count FROM pets WHERE player_id = ? AND is_active = TRUE", (player_id,))
|
||||
active_count = await cursor.fetchone()
|
||||
|
||||
if active_count["count"] <= 1:
|
||||
return {"success": False, "error": "Cannot deactivate your only active pet"}
|
||||
|
||||
# Remove from team
|
||||
await db.execute("""
|
||||
UPDATE pets SET is_active = FALSE, team_order = NULL
|
||||
WHERE id = ? AND player_id = ?
|
||||
""", (pet_id, player_id))
|
||||
|
||||
await db.commit()
|
||||
return {"success": True, "message": "Pet removed from team"}
|
||||
|
||||
# Item and Inventory Methods
|
||||
async def add_item_to_inventory(self, player_id: int, item_name: str, quantity: int = 1) -> bool:
|
||||
|
|
@ -873,7 +985,7 @@ class Database:
|
|||
FROM pets p
|
||||
JOIN pet_species ps ON p.species_id = ps.id
|
||||
WHERE p.player_id = ? AND p.is_active = 1
|
||||
ORDER BY p.id ASC
|
||||
ORDER BY p.team_order ASC, p.id ASC
|
||||
""", (player_id,))
|
||||
rows = await cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
|
@ -1601,12 +1713,18 @@ class Database:
|
|||
# Begin transaction
|
||||
await db.execute("BEGIN TRANSACTION")
|
||||
|
||||
# Update pet active status based on new team
|
||||
for pet_id, is_active in team_changes.items():
|
||||
await db.execute("""
|
||||
UPDATE pets SET is_active = ?
|
||||
WHERE id = ? AND player_id = ?
|
||||
""", (is_active, int(pet_id), player_id))
|
||||
# Update pet active status and team_order based on new team
|
||||
for pet_id, position in team_changes.items():
|
||||
if position: # If position is a number (1-6), pet is active
|
||||
await db.execute("""
|
||||
UPDATE pets SET is_active = TRUE, team_order = ?
|
||||
WHERE id = ? AND player_id = ?
|
||||
""", (position, int(pet_id), player_id))
|
||||
else: # If position is False, pet is inactive
|
||||
await db.execute("""
|
||||
UPDATE pets SET is_active = FALSE, team_order = NULL
|
||||
WHERE id = ? AND player_id = ?
|
||||
""", (int(pet_id), player_id))
|
||||
|
||||
# Mark any pending change as verified
|
||||
await db.execute("""
|
||||
|
|
@ -1713,18 +1831,28 @@ class Database:
|
|||
|
||||
# Get current pet states
|
||||
cursor = await db.execute("""
|
||||
SELECT id, is_active FROM pets WHERE player_id = ?
|
||||
SELECT id, is_active, team_order FROM pets WHERE player_id = ?
|
||||
""", (player_id,))
|
||||
current_pets = {str(row["id"]): bool(row["is_active"]) for row in await cursor.fetchall()}
|
||||
current_pets = {str(row["id"]): row["team_order"] if row["is_active"] else False for row in await cursor.fetchall()}
|
||||
|
||||
# Apply proposed changes to current state
|
||||
new_state = current_pets.copy()
|
||||
for pet_id, new_active_state in proposed_changes.items():
|
||||
for pet_id, new_position in proposed_changes.items():
|
||||
if pet_id in new_state:
|
||||
new_state[pet_id] = new_active_state
|
||||
new_state[pet_id] = new_position
|
||||
|
||||
# Count active pets in new state
|
||||
active_count = sum(1 for is_active in new_state.values() if is_active)
|
||||
# Count active pets and validate positions
|
||||
active_positions = [pos for pos in new_state.values() if pos]
|
||||
active_count = len(active_positions)
|
||||
|
||||
# Check for valid positions (1-6)
|
||||
for pos in active_positions:
|
||||
if not isinstance(pos, int) or pos < 1 or pos > 6:
|
||||
return {"valid": False, "error": f"Invalid team position: {pos}"}
|
||||
|
||||
# Check for duplicate positions
|
||||
if len(active_positions) != len(set(active_positions)):
|
||||
return {"valid": False, "error": "Duplicate team positions detected"}
|
||||
|
||||
# Validate constraints
|
||||
if active_count < 1:
|
||||
|
|
|
|||
|
|
@ -196,11 +196,11 @@ class GameEngine:
|
|||
|
||||
cursor = await db.execute("""
|
||||
INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp,
|
||||
attack, defense, speed, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
attack, defense, speed, is_active, team_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (player_id, species["id"], pet_data["level"], 0,
|
||||
pet_data["hp"], pet_data["hp"], pet_data["attack"],
|
||||
pet_data["defense"], pet_data["speed"], True))
|
||||
pet_data["defense"], pet_data["speed"], True, 1))
|
||||
|
||||
await db.commit()
|
||||
|
||||
|
|
|
|||
4027
webserver.py
4027
webserver.py
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue