Compare commits

..

No commits in common. "c8cb99a4d0dcfdfc0282c17c12b3f7ea30c83596" and "39ba55832dfbcf0ff83352e93a1c3f8f0508f9d6" have entirely different histories.

15 changed files with 1412 additions and 3150 deletions

View file

@ -99,6 +99,7 @@ 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

View file

@ -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, manage team composition
- **Team Management**: Activate/deactivate pets, swap team members
- **Achievement System**: Unlock new areas by completing challenges
- **Item Collection**: Discover and collect useful items during exploration
@ -78,6 +78,7 @@ 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

View file

@ -430,6 +430,11 @@
<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 &lt;pet1&gt; &lt;pet2&gt;</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>

View file

@ -19,12 +19,14 @@ class Achievements(BaseModule):
if not player:
return
# 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"📊 Quick summary: {len(achievements)} achievements earned! Check the web interface for details.")
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!")
else:
self.send_message(channel, f"💡 No achievements yet! Keep exploring and catching pets to unlock new areas!")
self.send_message(channel, f"{nickname}: No achievements yet! Keep exploring and catching pets to unlock new areas!")

View file

@ -12,15 +12,6 @@ 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"""

View file

@ -52,12 +52,6 @@ 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:
@ -93,7 +87,7 @@ class BattleSystem(BaseModule):
if not player:
return
move_name = " ".join(self.normalize_input(args)).title() # Normalize to Title Case
move_name = " ".join(args).title() # Normalize to Title Case
result = await self.game_engine.battle_engine.execute_battle_turn(player["id"], move_name)
if "error" in result:

View file

@ -40,8 +40,5 @@ 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']}")
self.send_message(channel,
f"🌐 View detailed statistics at: http://petz.rdx4.com/player/{nickname}#stats")

View file

@ -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", "flee"]
return ["explore", "travel", "location", "where", "weather", "wild", "catch", "capture"]
async def handle_command(self, channel, nickname, command, args):
if command == "explore":
@ -22,8 +22,6 @@ 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"""
@ -31,18 +29,6 @@ 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":
@ -65,7 +51,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, !catch to try catching it directly, or !flee to escape!")
self.send_message(channel, f"Choose your action: !battle to fight it, or !catch to try catching it directly!")
async def cmd_travel(self, channel, nickname, args):
"""Travel to a different location"""
@ -78,7 +64,7 @@ class Exploration(BaseModule):
return
# Handle various input formats and normalize location names
destination_input = self.normalize_input(" ".join(args))
destination_input = " ".join(args).lower()
# Map common variations to exact location names
location_mappings = {
@ -96,7 +82,7 @@ class Exploration(BaseModule):
destination = location_mappings.get(destination_input)
if not destination:
# Fall back to title case if no mapping found
destination = " ".join(self.normalize_input(args)).title()
destination = " ".join(args).title()
location = await self.database.get_location_by_name(destination)
@ -185,7 +171,7 @@ class Exploration(BaseModule):
if args:
# Specific location requested
location_name = " ".join(self.normalize_input(args)).title()
location_name = " ".join(args).title()
else:
# Default to current location
current_location = await self.database.get_player_location(player["id"])
@ -210,13 +196,6 @@ 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"]
@ -319,35 +298,3 @@ class Exploration(BaseModule):
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)
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!")

View file

@ -13,13 +13,13 @@ class GymBattles(BaseModule):
if command == "gym":
if not args:
await self.cmd_gym_list(channel, nickname)
elif self.normalize_input(args[0]) == "list":
elif args[0] == "list":
await self.cmd_gym_list_all(channel, nickname)
elif self.normalize_input(args[0]) == "challenge":
elif args[0] == "challenge":
await self.cmd_gym_challenge(channel, nickname, args[1:])
elif self.normalize_input(args[0]) == "info":
elif args[0] == "info":
await self.cmd_gym_info(channel, nickname, args[1:])
elif self.normalize_input(args[0]) == "status":
elif 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' to battle (gym name optional if only one gym in location)!")
f"💡 Use '!gym challenge \"<gym name>\"' to battle!")
async def cmd_gym_list_all(self, channel, nickname):
"""List all gyms across all locations"""
@ -97,6 +97,10 @@ 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
@ -107,37 +111,19 @@ class GymBattles(BaseModule):
self.send_message(channel, f"{nickname}: You are not in a valid location! Use !travel to go somewhere first.")
return
# 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
gym_name = " ".join(args).strip('"')
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
# 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_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}")
return
else:
self.send_message(channel, f"{nickname}: No gyms found in {location['name']}! Try traveling to a different location.")
return
# Check if player has active pets
active_pets = await self.database.get_active_pets(player["id"])
@ -280,7 +266,7 @@ class GymBattles(BaseModule):
if not player:
return
gym_name = " ".join(self.normalize_input(args)).strip('"')
gym_name = " ".join(args).strip('"')
# First try to find gym in player's current location
location = await self.database.get_player_location(player["id"])
@ -325,7 +311,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}#gym-badges")
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}")
async def cmd_forfeit(self, channel, nickname):
"""Forfeit the current gym battle"""

View file

@ -16,14 +16,51 @@ class Inventory(BaseModule):
await self.cmd_use_item(channel, nickname, args)
async def cmd_inventory(self, channel, nickname):
"""Redirect player to their web profile for inventory management"""
"""Display player's inventory"""
player = await self.require_player(channel, nickname)
if not player:
return
# 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!")
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!")
async def cmd_use_item(self, channel, nickname, args):
"""Use an item from inventory"""
@ -35,7 +72,7 @@ class Inventory(BaseModule):
if not player:
return
item_name = " ".join(self.normalize_input(args))
item_name = " ".join(args)
result = await self.database.use_item(player["id"], item_name)
if not result["success"]:

View file

@ -7,7 +7,7 @@ class PetManagement(BaseModule):
"""Handles team, pets, and future pet management commands"""
def get_commands(self):
return ["team", "pets", "activate", "deactivate", "nickname"]
return ["team", "pets", "activate", "deactivate", "swap", "nickname"]
async def handle_command(self, channel, nickname, command, args):
if command == "team":
@ -18,6 +18,8 @@ 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)
@ -38,7 +40,7 @@ class PetManagement(BaseModule):
team_info = []
# Active pets with star and team position
# Active pets with star
for pet in active_pets:
name = pet["nickname"] or pet["species_name"]
@ -53,9 +55,7 @@ class PetManagement(BaseModule):
else:
exp_display = f"{exp_needed} to next"
# 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}")
team_info.append(f"{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,7 +66,6 @@ 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"""
@ -75,7 +74,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}#pets")
self.send_message(channel, f"{nickname}: View your complete pet collection at: http://petz.rdx4.com/player/{nickname}")
async def cmd_activate(self, channel, nickname, args):
"""Activate a pet for battle (PM only)"""
@ -89,14 +88,13 @@ class PetManagement(BaseModule):
if not player:
return
pet_name = " ".join(self.normalize_input(args))
pet_name = " ".join(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"]
position = result.get("team_position", "?")
self.send_pm(nickname, f"{display_name} is now active for battle! Team position: {position}")
self.send_pm(nickname, f"{display_name} is now active for battle!")
self.send_message(channel, f"{nickname}: Pet activated successfully!")
else:
self.send_pm(nickname, f"{result['error']}")
@ -114,7 +112,7 @@ class PetManagement(BaseModule):
if not player:
return
pet_name = " ".join(self.normalize_input(args))
pet_name = " ".join(args)
result = await self.database.deactivate_pet(player["id"], pet_name)
if result["success"]:
@ -126,6 +124,44 @@ 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:
@ -138,7 +174,7 @@ class PetManagement(BaseModule):
return
# Split args into pet identifier and new nickname
pet_identifier = self.normalize_input(args[0])
pet_identifier = args[0]
new_nickname = " ".join(args[1:])
result = await self.database.set_pet_nickname(player["id"], pet_identifier, new_nickname)

View file

@ -62,7 +62,7 @@ class PetBotDebug:
print("✅ Background validation started")
print("🔄 Starting web server...")
self.web_server = PetBotWebServer(self.database, port=8080, bot=self)
self.web_server = PetBotWebServer(self.database, port=8080)
self.web_server.start_in_thread()
print("✅ Web server started")
@ -303,14 +303,12 @@ 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 = BaseModule.normalize_input(command_parts[0])
args = BaseModule.normalize_input(command_parts[1:])
command = command_parts[0].lower()
args = command_parts[1:]
try:
if command in self.command_map:

View file

@ -120,46 +120,6 @@ 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,
@ -448,9 +408,6 @@ 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]
@ -681,16 +638,11 @@ class Database:
if not pet:
return {"success": False, "error": f"No inactive pet found named '{pet_identifier}'"}
# 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"]))
# Activate the pet
await db.execute("UPDATE pets SET is_active = TRUE WHERE id = ?", (pet["id"],))
await db.commit()
return {"success": True, "pet": dict(pet), "team_position": next_slot}
return {"success": True, "pet": dict(pet)}
async def deactivate_pet(self, player_id: int, pet_identifier: str) -> Dict:
"""Deactivate a pet by name or species name. Returns result dict."""
@ -718,122 +670,58 @@ class Database:
if active_count["count"] <= 1:
return {"success": False, "error": "You must have at least one active pet!"}
# Deactivate the pet and clear team order
await db.execute("UPDATE pets SET is_active = FALSE, team_order = NULL WHERE id = ?", (pet["id"],))
# Deactivate the pet
await db.execute("UPDATE pets SET is_active = FALSE WHERE id = ?", (pet["id"],))
await db.commit()
return {"success": True, "pet": dict(pet)}
# Team Order Methods
async def get_next_available_team_slot(self, player_id: int) -> int:
"""Get the next available team slot (1-6)"""
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."""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
# Find both pets
cursor = await db.execute("""
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()]
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()
# 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 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()
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()
if existing_pet:
return {"success": False, "error": f"Position {position} is already taken"}
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"}
# 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))
if pet1["id"] == pet2["id"]:
return {"success": False, "error": "Cannot swap a pet with itself"}
# 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"}
# 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"}
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"
}
# Item and Inventory Methods
async def add_item_to_inventory(self, player_id: int, item_name: str, quantity: int = 1) -> bool:
@ -985,7 +873,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.team_order ASC, p.id ASC
ORDER BY p.id ASC
""", (player_id,))
rows = await cursor.fetchall()
return [dict(row) for row in rows]
@ -1713,18 +1601,12 @@ class Database:
# Begin transaction
await db.execute("BEGIN TRANSACTION")
# 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))
# 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))
# Mark any pending change as verified
await db.execute("""
@ -1831,28 +1713,18 @@ class Database:
# Get current pet states
cursor = await db.execute("""
SELECT id, is_active, team_order FROM pets WHERE player_id = ?
SELECT id, is_active FROM pets WHERE player_id = ?
""", (player_id,))
current_pets = {str(row["id"]): row["team_order"] if row["is_active"] else False for row in await cursor.fetchall()}
current_pets = {str(row["id"]): bool(row["is_active"]) for row in await cursor.fetchall()}
# Apply proposed changes to current state
new_state = current_pets.copy()
for pet_id, new_position in proposed_changes.items():
for pet_id, new_active_state in proposed_changes.items():
if pet_id in new_state:
new_state[pet_id] = new_position
new_state[pet_id] = new_active_state
# 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"}
# Count active pets in new state
active_count = sum(1 for is_active in new_state.values() if is_active)
# Validate constraints
if active_count < 1:

View file

@ -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, team_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
attack, defense, speed, is_active)
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, 1))
pet_data["defense"], pet_data["speed"], True))
await db.commit()

File diff suppressed because it is too large Load diff