Initial commit: Complete PetBot IRC Game
🎮 Features implemented: - Pokemon-style pet collection and battles - Multi-location exploration system - Dynamic weather with background updates - Achievement system with location unlocks - Web dashboard for player stats - Modular command system - Async database with SQLite - PM flood prevention - Persistent player data 🌤️ Weather System: - 6 weather types with spawn modifiers - 30min-3hour dynamic durations - Background task for automatic updates - Location-specific weather patterns 🐛 Recent Bug Fixes: - Database persistence on restart - Player page SQLite row conversion - Achievement count calculations - Travel requirement messages - Battle move color coding - Locations page display 🔧 Generated with Claude Code 🤖 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
47f160a295
31 changed files with 6235 additions and 0 deletions
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
350
src/battle_engine.py
Normal file
350
src/battle_engine.py
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
import json
|
||||
import random
|
||||
import aiosqlite
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from .database import Database
|
||||
|
||||
class BattleEngine:
|
||||
def __init__(self, database: Database):
|
||||
self.database = database
|
||||
self.type_effectiveness = {}
|
||||
self.moves_data = {}
|
||||
|
||||
async def load_battle_data(self):
|
||||
"""Load moves and type effectiveness data"""
|
||||
await self.load_moves()
|
||||
await self.load_type_effectiveness()
|
||||
|
||||
async def load_moves(self):
|
||||
"""Load moves from config"""
|
||||
try:
|
||||
with open("config/moves.json", "r") as f:
|
||||
moves_data = json.load(f)
|
||||
|
||||
# Store moves by name for quick lookup
|
||||
for move in moves_data:
|
||||
self.moves_data[move["name"]] = move
|
||||
|
||||
# Load into database
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
for move in moves_data:
|
||||
await db.execute("""
|
||||
INSERT OR REPLACE INTO moves
|
||||
(name, type, category, power, accuracy, pp, description)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
move["name"], move["type"], move["category"],
|
||||
move.get("power"), move.get("accuracy", 100),
|
||||
move.get("pp", 10), move.get("description", "")
|
||||
))
|
||||
await db.commit()
|
||||
|
||||
except FileNotFoundError:
|
||||
print("No moves.json found, using basic moves")
|
||||
self.moves_data = {
|
||||
"Tackle": {"name": "Tackle", "type": "Normal", "category": "Physical", "power": 40, "accuracy": 100}
|
||||
}
|
||||
|
||||
async def load_type_effectiveness(self):
|
||||
"""Load type effectiveness chart"""
|
||||
try:
|
||||
with open("config/types.json", "r") as f:
|
||||
types_data = json.load(f)
|
||||
self.type_effectiveness = types_data.get("effectiveness", {})
|
||||
except FileNotFoundError:
|
||||
# Basic type chart
|
||||
self.type_effectiveness = {
|
||||
"Fire": {"strong_against": ["Grass"], "weak_against": ["Water", "Rock"]},
|
||||
"Water": {"strong_against": ["Fire", "Rock"], "weak_against": ["Electric", "Grass"]},
|
||||
"Grass": {"strong_against": ["Water", "Rock"], "weak_against": ["Fire"]},
|
||||
"Electric": {"strong_against": ["Water"], "weak_against": ["Rock"]},
|
||||
"Rock": {"strong_against": ["Fire", "Electric"], "weak_against": ["Water", "Grass"]},
|
||||
"Normal": {"strong_against": [], "weak_against": ["Rock"]}
|
||||
}
|
||||
|
||||
def get_type_multiplier(self, attack_type: str, defending_types: List[str]) -> float:
|
||||
"""Calculate type effectiveness multiplier"""
|
||||
multiplier = 1.0
|
||||
|
||||
for defending_type in defending_types:
|
||||
if attack_type in self.type_effectiveness:
|
||||
effectiveness = self.type_effectiveness[attack_type]
|
||||
|
||||
if defending_type in effectiveness.get("strong_against", []):
|
||||
multiplier *= 2.0 # Super effective
|
||||
elif defending_type in effectiveness.get("weak_against", []):
|
||||
multiplier *= 0.5 # Not very effective
|
||||
elif defending_type in effectiveness.get("immune_to", []):
|
||||
multiplier *= 0.0 # No effect
|
||||
|
||||
return multiplier
|
||||
|
||||
def calculate_damage(self, attacker: Dict, defender: Dict, move: Dict) -> Tuple[int, float, str]:
|
||||
"""Calculate damage from an attack"""
|
||||
if move.get("category") == "Status":
|
||||
return 0, 1.0, "status"
|
||||
|
||||
# Base damage calculation (simplified Pokémon formula)
|
||||
level = attacker.get("level", 1)
|
||||
attack_stat = attacker.get("attack", 50)
|
||||
defense_stat = defender.get("defense", 50)
|
||||
move_power = move.get("power", 40)
|
||||
|
||||
# Get defending types
|
||||
defending_types = [defender.get("type1")]
|
||||
if defender.get("type2"):
|
||||
defending_types.append(defender["type2"])
|
||||
|
||||
# Type effectiveness
|
||||
type_multiplier = self.get_type_multiplier(move["type"], defending_types)
|
||||
|
||||
# Critical hit chance (6.25%)
|
||||
critical = random.random() < 0.0625
|
||||
crit_multiplier = 1.5 if critical else 1.0
|
||||
|
||||
# Random factor (85-100%)
|
||||
random_factor = random.uniform(0.85, 1.0)
|
||||
|
||||
# STAB (Same Type Attack Bonus)
|
||||
stab = 1.5 if move["type"] in [attacker.get("type1"), attacker.get("type2")] else 1.0
|
||||
|
||||
# Calculate final damage
|
||||
damage = ((((2 * level + 10) / 250) * (attack_stat / defense_stat) * move_power + 2)
|
||||
* type_multiplier * stab * crit_multiplier * random_factor)
|
||||
|
||||
damage = max(1, int(damage)) # Minimum 1 damage
|
||||
|
||||
# Determine effectiveness message
|
||||
effectiveness_msg = ""
|
||||
if type_multiplier > 1.0:
|
||||
effectiveness_msg = "super_effective"
|
||||
elif type_multiplier < 1.0:
|
||||
effectiveness_msg = "not_very_effective"
|
||||
elif type_multiplier == 0.0:
|
||||
effectiveness_msg = "no_effect"
|
||||
else:
|
||||
effectiveness_msg = "normal"
|
||||
|
||||
if critical:
|
||||
effectiveness_msg += "_critical"
|
||||
|
||||
return damage, type_multiplier, effectiveness_msg
|
||||
|
||||
def get_available_moves(self, pet: Dict) -> List[Dict]:
|
||||
"""Get available moves for a pet (simplified)"""
|
||||
# For now, all pets have basic moves based on their type
|
||||
pet_type = pet.get("type1", "Normal")
|
||||
|
||||
basic_moves = {
|
||||
"Fire": ["Tackle", "Ember"],
|
||||
"Water": ["Tackle", "Water Gun"],
|
||||
"Grass": ["Tackle", "Vine Whip"],
|
||||
"Electric": ["Tackle", "Thunder Shock"],
|
||||
"Rock": ["Tackle", "Rock Throw"],
|
||||
"Normal": ["Tackle"],
|
||||
"Ice": ["Tackle", "Water Gun"] # Add Ice type support
|
||||
}
|
||||
|
||||
move_names = basic_moves.get(pet_type, ["Tackle"])
|
||||
available_moves = []
|
||||
for name in move_names:
|
||||
if name in self.moves_data:
|
||||
available_moves.append(self.moves_data[name])
|
||||
else:
|
||||
# Fallback to Tackle if move not found
|
||||
available_moves.append(self.moves_data.get("Tackle", {"name": "Tackle", "type": "Normal", "category": "Physical", "power": 40}))
|
||||
|
||||
return available_moves
|
||||
|
||||
async def start_battle(self, player_id: int, player_pet: Dict, wild_pet: Dict) -> Dict:
|
||||
"""Start a new battle"""
|
||||
wild_pet_json = json.dumps(wild_pet)
|
||||
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
cursor = await db.execute("""
|
||||
INSERT INTO active_battles
|
||||
(player_id, wild_pet_data, player_pet_id, player_hp, wild_hp)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", (
|
||||
player_id, wild_pet_json, player_pet["id"],
|
||||
player_pet["hp"], wild_pet["stats"]["hp"]
|
||||
))
|
||||
|
||||
battle_id = cursor.lastrowid
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"battle_id": battle_id,
|
||||
"player_pet": player_pet,
|
||||
"wild_pet": wild_pet,
|
||||
"player_hp": player_pet["hp"],
|
||||
"wild_hp": wild_pet["stats"]["hp"],
|
||||
"turn": "player",
|
||||
"available_moves": self.get_available_moves(player_pet)
|
||||
}
|
||||
|
||||
async def get_active_battle(self, player_id: int) -> Optional[Dict]:
|
||||
"""Get player's active battle"""
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM active_battles
|
||||
WHERE player_id = ? AND battle_status = 'active'
|
||||
ORDER BY id DESC LIMIT 1
|
||||
""", (player_id,))
|
||||
|
||||
battle = await cursor.fetchone()
|
||||
if not battle:
|
||||
return None
|
||||
|
||||
# Get player pet data
|
||||
cursor = await db.execute("""
|
||||
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.id = ?
|
||||
""", (battle["player_pet_id"],))
|
||||
|
||||
player_pet = await cursor.fetchone()
|
||||
if not player_pet:
|
||||
return None
|
||||
|
||||
return {
|
||||
"battle_id": battle["id"],
|
||||
"player_pet": dict(player_pet),
|
||||
"wild_pet": json.loads(battle["wild_pet_data"]),
|
||||
"player_hp": battle["player_hp"],
|
||||
"wild_hp": battle["wild_hp"],
|
||||
"turn": battle["current_turn"],
|
||||
"turn_count": battle["turn_count"],
|
||||
"available_moves": self.get_available_moves(dict(player_pet))
|
||||
}
|
||||
|
||||
async def execute_battle_turn(self, player_id: int, player_move: str) -> Dict:
|
||||
"""Execute a battle turn"""
|
||||
battle = await self.get_active_battle(player_id)
|
||||
if not battle:
|
||||
return {"error": "No active battle"}
|
||||
|
||||
if battle["turn"] != "player":
|
||||
return {"error": "Not your turn"}
|
||||
|
||||
# Get move data
|
||||
move = self.moves_data.get(player_move)
|
||||
if not move:
|
||||
return {"error": "Invalid move"}
|
||||
|
||||
results = []
|
||||
battle_over = False
|
||||
winner = None
|
||||
|
||||
# Player's turn
|
||||
damage, multiplier, effectiveness = self.calculate_damage(
|
||||
battle["player_pet"], battle["wild_pet"], move
|
||||
)
|
||||
|
||||
new_wild_hp = max(0, battle["wild_hp"] - damage)
|
||||
|
||||
results.append({
|
||||
"attacker": battle["player_pet"]["species_name"],
|
||||
"move": move["name"],
|
||||
"damage": damage,
|
||||
"effectiveness": effectiveness,
|
||||
"target_hp": new_wild_hp
|
||||
})
|
||||
|
||||
# Check if wild pet fainted
|
||||
if new_wild_hp <= 0:
|
||||
battle_over = True
|
||||
winner = "player"
|
||||
else:
|
||||
# Wild pet's turn (AI)
|
||||
wild_moves = self.get_available_moves(battle["wild_pet"])
|
||||
wild_move = random.choice(wild_moves)
|
||||
|
||||
wild_damage, wild_multiplier, wild_effectiveness = self.calculate_damage(
|
||||
battle["wild_pet"], battle["player_pet"], wild_move
|
||||
)
|
||||
|
||||
new_player_hp = max(0, battle["player_hp"] - wild_damage)
|
||||
|
||||
results.append({
|
||||
"attacker": battle["wild_pet"]["species_name"],
|
||||
"move": wild_move["name"],
|
||||
"damage": wild_damage,
|
||||
"effectiveness": wild_effectiveness,
|
||||
"target_hp": new_player_hp
|
||||
})
|
||||
|
||||
# Check if player pet fainted
|
||||
if new_player_hp <= 0:
|
||||
battle_over = True
|
||||
winner = "wild"
|
||||
|
||||
battle["player_hp"] = new_player_hp
|
||||
|
||||
battle["wild_hp"] = new_wild_hp
|
||||
battle["turn_count"] += 1
|
||||
|
||||
# Update battle in database
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
if battle_over:
|
||||
await db.execute("""
|
||||
UPDATE active_battles
|
||||
SET player_hp = ?, wild_hp = ?, turn_count = ?, battle_status = 'finished'
|
||||
WHERE id = ?
|
||||
""", (battle["player_hp"], battle["wild_hp"], battle["turn_count"], battle["battle_id"]))
|
||||
else:
|
||||
await db.execute("""
|
||||
UPDATE active_battles
|
||||
SET player_hp = ?, wild_hp = ?, turn_count = ?
|
||||
WHERE id = ?
|
||||
""", (battle["player_hp"], battle["wild_hp"], battle["turn_count"], battle["battle_id"]))
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"results": results,
|
||||
"battle_over": battle_over,
|
||||
"winner": winner,
|
||||
"player_hp": battle["player_hp"],
|
||||
"wild_hp": battle["wild_hp"],
|
||||
"available_moves": battle["available_moves"] if not battle_over else []
|
||||
}
|
||||
|
||||
async def flee_battle(self, player_id: int) -> bool:
|
||||
"""Attempt to flee from battle"""
|
||||
battle = await self.get_active_battle(player_id)
|
||||
if not battle:
|
||||
return False
|
||||
|
||||
# 50% base flee chance, higher if player pet is faster
|
||||
player_speed = battle["player_pet"].get("speed", 50)
|
||||
wild_speed = battle["wild_pet"]["stats"].get("speed", 50)
|
||||
|
||||
flee_chance = 0.5 + (player_speed - wild_speed) * 0.01
|
||||
flee_chance = max(0.1, min(0.9, flee_chance)) # Clamp between 10% and 90%
|
||||
|
||||
if random.random() < flee_chance:
|
||||
# Successful flee
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
await db.execute("""
|
||||
UPDATE active_battles
|
||||
SET battle_status = 'fled'
|
||||
WHERE id = ?
|
||||
""", (battle["battle_id"],))
|
||||
await db.commit()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def end_battle(self, player_id: int, reason: str = "manual"):
|
||||
"""End an active battle"""
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
await db.execute("""
|
||||
UPDATE active_battles
|
||||
SET battle_status = ?
|
||||
WHERE player_id = ? AND battle_status = 'active'
|
||||
""", (reason, player_id))
|
||||
await db.commit()
|
||||
return True
|
||||
191
src/bot.py
Normal file
191
src/bot.py
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import asyncio
|
||||
import irc.client_aio
|
||||
import irc.connection
|
||||
import threading
|
||||
from typing import Dict, List, Optional
|
||||
import json
|
||||
import random
|
||||
from datetime import datetime
|
||||
from .database import Database
|
||||
from .game_engine import GameEngine
|
||||
|
||||
class PetBot:
|
||||
def __init__(self, config_path: str = "config/settings.json"):
|
||||
self.config = self.load_config(config_path)
|
||||
self.database = Database()
|
||||
self.game_engine = GameEngine(self.database)
|
||||
self.client = None
|
||||
self.connection = None
|
||||
|
||||
def load_config(self, config_path: str) -> Dict:
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
return {
|
||||
"server": "irc.libera.chat",
|
||||
"port": 6667,
|
||||
"nickname": "PetBot",
|
||||
"channel": "#petz",
|
||||
"command_prefix": "!"
|
||||
}
|
||||
|
||||
async def start(self):
|
||||
await self.database.init_database()
|
||||
await self.game_engine.load_game_data()
|
||||
|
||||
self.client = irc.client_aio.AioReactor()
|
||||
|
||||
try:
|
||||
self.connection = await self.client.server().connect(
|
||||
self.config["server"],
|
||||
self.config["port"],
|
||||
self.config["nickname"]
|
||||
)
|
||||
except irc.client.ServerConnectionError:
|
||||
print(f"Could not connect to {self.config['server']}:{self.config['port']}")
|
||||
return
|
||||
|
||||
self.connection.add_global_handler("welcome", self.on_connect)
|
||||
self.connection.add_global_handler("pubmsg", self.on_message)
|
||||
self.connection.add_global_handler("privmsg", self.on_private_message)
|
||||
|
||||
await self.client.process_forever(timeout=0.2)
|
||||
|
||||
def on_connect(self, connection, event):
|
||||
connection.join(self.config["channel"])
|
||||
print(f"Connected to {self.config['channel']}")
|
||||
|
||||
async def on_message(self, connection, event):
|
||||
message = event.arguments[0]
|
||||
nickname = event.source.nick
|
||||
channel = event.target
|
||||
|
||||
if message.startswith(self.config["command_prefix"]):
|
||||
await self.handle_command(connection, channel, nickname, message)
|
||||
|
||||
async def on_private_message(self, connection, event):
|
||||
message = event.arguments[0]
|
||||
nickname = event.source.nick
|
||||
|
||||
if message.startswith(self.config["command_prefix"]):
|
||||
await self.handle_command(connection, nickname, nickname, message)
|
||||
|
||||
async def handle_command(self, connection, target, nickname, message):
|
||||
command_parts = message[1:].split()
|
||||
if not command_parts:
|
||||
return
|
||||
|
||||
command = command_parts[0].lower()
|
||||
args = command_parts[1:]
|
||||
|
||||
try:
|
||||
if command == "help":
|
||||
await self.cmd_help(connection, target, nickname)
|
||||
elif command == "start":
|
||||
await self.cmd_start(connection, target, nickname)
|
||||
elif command == "catch":
|
||||
await self.cmd_catch(connection, target, nickname, args)
|
||||
elif command == "team":
|
||||
await self.cmd_team(connection, target, nickname)
|
||||
elif command == "wild":
|
||||
await self.cmd_wild(connection, target, nickname, args)
|
||||
elif command == "battle":
|
||||
await self.cmd_battle(connection, target, nickname, args)
|
||||
elif command == "stats":
|
||||
await self.cmd_stats(connection, target, nickname, args)
|
||||
else:
|
||||
await self.send_message(connection, target, f"{nickname}: Unknown command. Use !help for available commands.")
|
||||
except Exception as e:
|
||||
await self.send_message(connection, target, f"{nickname}: Error processing command: {str(e)}")
|
||||
|
||||
async def send_message(self, connection, target, message):
|
||||
connection.privmsg(target, message)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
async def cmd_help(self, connection, target, nickname):
|
||||
help_text = [
|
||||
"Available commands:",
|
||||
"!start - Begin your pet journey",
|
||||
"!catch <location> - Try to catch a pet in a location",
|
||||
"!team - View your active pets",
|
||||
"!wild <location> - See what pets are in an area",
|
||||
"!battle <player> - Challenge another player",
|
||||
"!stats [pet_name] - View pet or player stats"
|
||||
]
|
||||
for line in help_text:
|
||||
await self.send_message(connection, target, line)
|
||||
|
||||
async def cmd_start(self, connection, target, nickname):
|
||||
player = await self.database.get_player(nickname)
|
||||
if player:
|
||||
await self.send_message(connection, target, f"{nickname}: You already have an account! Use !team to see your pets.")
|
||||
return
|
||||
|
||||
player_id = await self.database.create_player(nickname)
|
||||
starter_pet = await self.game_engine.give_starter_pet(player_id)
|
||||
|
||||
await self.send_message(connection, target,
|
||||
f"{nickname}: Welcome to the world of pets! You received a {starter_pet['species_name']}!")
|
||||
|
||||
async def cmd_catch(self, connection, target, nickname, args):
|
||||
if not args:
|
||||
await self.send_message(connection, target, f"{nickname}: Specify a location to catch pets in!")
|
||||
return
|
||||
|
||||
location_name = " ".join(args)
|
||||
player = await self.database.get_player(nickname)
|
||||
if not player:
|
||||
await self.send_message(connection, target, f"{nickname}: Use !start to begin your journey first!")
|
||||
return
|
||||
|
||||
result = await self.game_engine.attempt_catch(player["id"], location_name)
|
||||
await self.send_message(connection, target, f"{nickname}: {result}")
|
||||
|
||||
async def cmd_team(self, connection, target, nickname):
|
||||
player = await self.database.get_player(nickname)
|
||||
if not player:
|
||||
await self.send_message(connection, target, f"{nickname}: Use !start to begin your journey first!")
|
||||
return
|
||||
|
||||
pets = await self.database.get_player_pets(player["id"], active_only=True)
|
||||
if not pets:
|
||||
await self.send_message(connection, target, f"{nickname}: You don't have any active pets! Use !catch to find some.")
|
||||
return
|
||||
|
||||
team_info = []
|
||||
for pet in pets:
|
||||
name = pet["nickname"] or pet["species_name"]
|
||||
team_info.append(f"{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP")
|
||||
|
||||
await self.send_message(connection, target, f"{nickname}'s team: " + " | ".join(team_info))
|
||||
|
||||
async def cmd_wild(self, connection, target, nickname, args):
|
||||
if not args:
|
||||
await self.send_message(connection, target, f"{nickname}: Specify a location to explore!")
|
||||
return
|
||||
|
||||
location_name = " ".join(args)
|
||||
wild_pets = await self.game_engine.get_location_spawns(location_name)
|
||||
|
||||
if wild_pets:
|
||||
pet_list = ", ".join([pet["name"] for pet in wild_pets])
|
||||
await self.send_message(connection, target, f"Wild pets in {location_name}: {pet_list}")
|
||||
else:
|
||||
await self.send_message(connection, target, f"{nickname}: No location found called '{location_name}'")
|
||||
|
||||
async def cmd_battle(self, connection, target, nickname, args):
|
||||
await self.send_message(connection, target, f"{nickname}: Battle system coming soon!")
|
||||
|
||||
async def cmd_stats(self, connection, target, nickname, args):
|
||||
player = await self.database.get_player(nickname)
|
||||
if not player:
|
||||
await self.send_message(connection, target, f"{nickname}: Use !start to begin your journey first!")
|
||||
return
|
||||
|
||||
await self.send_message(connection, target,
|
||||
f"{nickname}: Level {player['level']} | {player['experience']} XP | ${player['money']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
bot = PetBot()
|
||||
asyncio.run(bot.start())
|
||||
522
src/database.py
Normal file
522
src/database.py
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
import aiosqlite
|
||||
import json
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from datetime import datetime
|
||||
|
||||
class Database:
|
||||
def __init__(self, db_path: str = "data/petbot.db"):
|
||||
self.db_path = db_path
|
||||
|
||||
async def init_database(self):
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS players (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
nickname TEXT UNIQUE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
level INTEGER DEFAULT 1,
|
||||
experience INTEGER DEFAULT 0,
|
||||
money INTEGER DEFAULT 100,
|
||||
current_location_id INTEGER DEFAULT NULL,
|
||||
FOREIGN KEY (current_location_id) REFERENCES locations (id)
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS pet_species (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
type1 TEXT NOT NULL,
|
||||
type2 TEXT,
|
||||
base_hp INTEGER NOT NULL,
|
||||
base_attack INTEGER NOT NULL,
|
||||
base_defense INTEGER NOT NULL,
|
||||
base_speed INTEGER NOT NULL,
|
||||
evolution_level INTEGER,
|
||||
evolution_species_id INTEGER,
|
||||
rarity INTEGER DEFAULT 1,
|
||||
FOREIGN KEY (evolution_species_id) REFERENCES pet_species (id)
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS pets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
player_id INTEGER NOT NULL,
|
||||
species_id INTEGER NOT NULL,
|
||||
nickname TEXT,
|
||||
level INTEGER DEFAULT 1,
|
||||
experience INTEGER DEFAULT 0,
|
||||
hp INTEGER NOT NULL,
|
||||
max_hp INTEGER NOT NULL,
|
||||
attack INTEGER NOT NULL,
|
||||
defense INTEGER NOT NULL,
|
||||
speed INTEGER NOT NULL,
|
||||
happiness INTEGER DEFAULT 50,
|
||||
caught_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT FALSE,
|
||||
FOREIGN KEY (player_id) REFERENCES players (id),
|
||||
FOREIGN KEY (species_id) REFERENCES pet_species (id)
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS moves (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
power INTEGER,
|
||||
accuracy INTEGER DEFAULT 100,
|
||||
pp INTEGER DEFAULT 10,
|
||||
description TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS pet_moves (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
pet_id INTEGER NOT NULL,
|
||||
move_id INTEGER NOT NULL,
|
||||
current_pp INTEGER,
|
||||
FOREIGN KEY (pet_id) REFERENCES pets (id),
|
||||
FOREIGN KEY (move_id) REFERENCES moves (id),
|
||||
UNIQUE(pet_id, move_id)
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS battles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
player1_id INTEGER NOT NULL,
|
||||
player2_id INTEGER,
|
||||
battle_type TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'active',
|
||||
winner_id INTEGER,
|
||||
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
ended_at TIMESTAMP,
|
||||
FOREIGN KEY (player1_id) REFERENCES players (id),
|
||||
FOREIGN KEY (player2_id) REFERENCES players (id),
|
||||
FOREIGN KEY (winner_id) REFERENCES players (id)
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS locations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
level_min INTEGER DEFAULT 1,
|
||||
level_max INTEGER DEFAULT 5
|
||||
)
|
||||
""")
|
||||
|
||||
# Add current_location_id column if it doesn't exist
|
||||
try:
|
||||
await db.execute("ALTER TABLE players ADD COLUMN current_location_id INTEGER DEFAULT 1")
|
||||
await db.commit()
|
||||
print("Added current_location_id column to players table")
|
||||
except:
|
||||
pass # Column already exists
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS location_spawns (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
location_id INTEGER NOT NULL,
|
||||
species_id INTEGER NOT NULL,
|
||||
spawn_rate REAL DEFAULT 0.1,
|
||||
min_level INTEGER DEFAULT 1,
|
||||
max_level INTEGER DEFAULT 5,
|
||||
FOREIGN KEY (location_id) REFERENCES locations (id),
|
||||
FOREIGN KEY (species_id) REFERENCES pet_species (id)
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS achievements (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
requirement_type TEXT NOT NULL,
|
||||
requirement_data TEXT,
|
||||
unlock_location_id INTEGER,
|
||||
FOREIGN KEY (unlock_location_id) REFERENCES locations (id)
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS player_achievements (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
player_id INTEGER NOT NULL,
|
||||
achievement_id INTEGER NOT NULL,
|
||||
completed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (player_id) REFERENCES players (id),
|
||||
FOREIGN KEY (achievement_id) REFERENCES achievements (id),
|
||||
UNIQUE(player_id, achievement_id)
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS location_weather (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
location_id INTEGER NOT NULL,
|
||||
weather_type TEXT NOT NULL,
|
||||
active_until TIMESTAMP,
|
||||
spawn_modifier REAL DEFAULT 1.0,
|
||||
affected_types TEXT,
|
||||
FOREIGN KEY (location_id) REFERENCES locations (id)
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS active_battles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
player_id INTEGER NOT NULL,
|
||||
wild_pet_data TEXT NOT NULL,
|
||||
player_pet_id INTEGER NOT NULL,
|
||||
player_hp INTEGER NOT NULL,
|
||||
wild_hp INTEGER NOT NULL,
|
||||
turn_count INTEGER DEFAULT 1,
|
||||
current_turn TEXT DEFAULT 'player',
|
||||
battle_status TEXT DEFAULT 'active',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (player_id) REFERENCES players (id),
|
||||
FOREIGN KEY (player_pet_id) REFERENCES pets (id)
|
||||
)
|
||||
""")
|
||||
|
||||
await db.commit()
|
||||
|
||||
async def get_player(self, nickname: str) -> Optional[Dict]:
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute(
|
||||
"SELECT * FROM players WHERE nickname = ?", (nickname,)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
async def create_player(self, nickname: str) -> int:
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
# Get Starter Town ID
|
||||
cursor = await db.execute("SELECT id FROM locations WHERE name = 'Starter Town'")
|
||||
starter_town = await cursor.fetchone()
|
||||
if not starter_town:
|
||||
raise Exception("Starter Town location not found in database - ensure game data is loaded first")
|
||||
starter_town_id = starter_town[0]
|
||||
|
||||
cursor = await db.execute(
|
||||
"INSERT INTO players (nickname, current_location_id) VALUES (?, ?)",
|
||||
(nickname, starter_town_id)
|
||||
)
|
||||
await db.commit()
|
||||
return cursor.lastrowid
|
||||
|
||||
async def get_player_pets(self, player_id: int, active_only: bool = False) -> List[Dict]:
|
||||
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
|
||||
FROM pets p
|
||||
JOIN pet_species ps ON p.species_id = ps.id
|
||||
WHERE p.player_id = ?
|
||||
"""
|
||||
params = [player_id]
|
||||
|
||||
if active_only:
|
||||
query += " AND p.is_active = TRUE"
|
||||
|
||||
cursor = await db.execute(query, params)
|
||||
rows = await cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
async def get_player_location(self, player_id: int) -> Optional[Dict]:
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT l.* FROM locations l
|
||||
JOIN players p ON p.current_location_id = l.id
|
||||
WHERE p.id = ?
|
||||
""", (player_id,))
|
||||
row = await cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
async def update_player_location(self, player_id: int, location_id: int):
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute(
|
||||
"UPDATE players SET current_location_id = ? WHERE id = ?",
|
||||
(location_id, player_id)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
async def get_location_by_name(self, location_name: str) -> Optional[Dict]:
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute(
|
||||
"SELECT * FROM locations WHERE name = ?", (location_name,)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
async def check_player_achievements(self, player_id: int, achievement_type: str, data: str):
|
||||
"""Check and award achievements based on player actions"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
|
||||
# Get relevant achievements not yet earned
|
||||
cursor = await db.execute("""
|
||||
SELECT a.* FROM achievements a
|
||||
WHERE a.requirement_type = ?
|
||||
AND a.id NOT IN (
|
||||
SELECT pa.achievement_id FROM player_achievements pa
|
||||
WHERE pa.player_id = ?
|
||||
)
|
||||
""", (achievement_type, player_id))
|
||||
|
||||
achievements = await cursor.fetchall()
|
||||
newly_earned = []
|
||||
|
||||
for achievement in achievements:
|
||||
if await self._check_achievement_requirement(player_id, achievement, data):
|
||||
# Award achievement
|
||||
await db.execute("""
|
||||
INSERT INTO player_achievements (player_id, achievement_id)
|
||||
VALUES (?, ?)
|
||||
""", (player_id, achievement["id"]))
|
||||
newly_earned.append(dict(achievement))
|
||||
|
||||
await db.commit()
|
||||
return newly_earned
|
||||
|
||||
async def _check_achievement_requirement(self, player_id: int, achievement: Dict, data: str) -> bool:
|
||||
"""Check if player meets achievement requirement"""
|
||||
req_type = achievement["requirement_type"]
|
||||
req_data = achievement["requirement_data"]
|
||||
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
|
||||
if req_type == "catch_type":
|
||||
# Count unique species of a specific type caught
|
||||
required_count, pet_type = req_data.split(":")
|
||||
cursor = await db.execute("""
|
||||
SELECT COUNT(DISTINCT ps.id) as count
|
||||
FROM pets p
|
||||
JOIN pet_species ps ON p.species_id = ps.id
|
||||
WHERE p.player_id = ? AND (ps.type1 = ? OR ps.type2 = ?)
|
||||
""", (player_id, pet_type, pet_type))
|
||||
|
||||
result = await cursor.fetchone()
|
||||
return result["count"] >= int(required_count)
|
||||
|
||||
elif req_type == "catch_total":
|
||||
# Count total pets caught
|
||||
required_count = int(req_data)
|
||||
cursor = await db.execute("""
|
||||
SELECT COUNT(*) as count FROM pets WHERE player_id = ?
|
||||
""", (player_id,))
|
||||
|
||||
result = await cursor.fetchone()
|
||||
return result["count"] >= required_count
|
||||
|
||||
elif req_type == "explore_count":
|
||||
# This would need exploration tracking - placeholder for now
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
async def get_player_achievements(self, player_id: int) -> List[Dict]:
|
||||
"""Get all achievements earned by player"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT a.*, pa.completed_at
|
||||
FROM achievements a
|
||||
JOIN player_achievements pa ON a.id = pa.achievement_id
|
||||
WHERE pa.player_id = ?
|
||||
ORDER BY pa.completed_at DESC
|
||||
""", (player_id,))
|
||||
|
||||
rows = await cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
async def can_access_location(self, player_id: int, location_id: int) -> bool:
|
||||
"""Check if player can access a location based on achievements"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
|
||||
# Check if location requires any achievements
|
||||
cursor = await db.execute("""
|
||||
SELECT a.* FROM achievements a
|
||||
WHERE a.unlock_location_id = ?
|
||||
""", (location_id,))
|
||||
|
||||
required_achievements = await cursor.fetchall()
|
||||
|
||||
if not required_achievements:
|
||||
return True # No requirements
|
||||
|
||||
# Check if player has ALL required achievements
|
||||
for achievement in required_achievements:
|
||||
cursor = await db.execute("""
|
||||
SELECT 1 FROM player_achievements
|
||||
WHERE player_id = ? AND achievement_id = ?
|
||||
""", (player_id, achievement["id"]))
|
||||
|
||||
if not await cursor.fetchone():
|
||||
return False # Missing required achievement
|
||||
|
||||
return True
|
||||
|
||||
async def get_missing_location_requirements(self, player_id: int, location_id: int) -> List[Dict]:
|
||||
"""Get list of achievements required to access a location that the player doesn't have"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
|
||||
# Get all achievements required for this location
|
||||
cursor = await db.execute("""
|
||||
SELECT a.* FROM achievements a
|
||||
WHERE a.unlock_location_id = ?
|
||||
""", (location_id,))
|
||||
|
||||
required_achievements = await cursor.fetchall()
|
||||
missing_achievements = []
|
||||
|
||||
# Check which ones the player doesn't have
|
||||
for achievement in required_achievements:
|
||||
cursor = await db.execute("""
|
||||
SELECT 1 FROM player_achievements
|
||||
WHERE player_id = ? AND achievement_id = ?
|
||||
""", (player_id, achievement["id"]))
|
||||
|
||||
if not await cursor.fetchone():
|
||||
# Player doesn't have this achievement - convert to dict manually
|
||||
achievement_dict = {
|
||||
'id': achievement[0],
|
||||
'name': achievement[1],
|
||||
'description': achievement[2],
|
||||
'requirement_type': achievement[3],
|
||||
'requirement_data': achievement[4],
|
||||
'unlock_location_id': achievement[5]
|
||||
}
|
||||
missing_achievements.append(achievement_dict)
|
||||
|
||||
return missing_achievements
|
||||
|
||||
async def get_location_weather(self, location_id: int) -> Optional[Dict]:
|
||||
"""Get current weather for a location"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM location_weather
|
||||
WHERE location_id = ? AND active_until > datetime('now')
|
||||
ORDER BY id DESC LIMIT 1
|
||||
""", (location_id,))
|
||||
|
||||
row = await cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
async def activate_pet(self, player_id: int, pet_identifier: str) -> Dict:
|
||||
"""Activate a pet by name or species name. Returns result dict."""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
|
||||
# Find pet by nickname or species name
|
||||
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.is_active = FALSE
|
||||
AND (p.nickname = ? OR ps.name = ?)
|
||||
LIMIT 1
|
||||
""", (player_id, pet_identifier, pet_identifier))
|
||||
|
||||
pet = await cursor.fetchone()
|
||||
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"],))
|
||||
await db.commit()
|
||||
|
||||
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."""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
|
||||
# Find pet by nickname or species name
|
||||
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.is_active = TRUE
|
||||
AND (p.nickname = ? OR ps.name = ?)
|
||||
LIMIT 1
|
||||
""", (player_id, pet_identifier, pet_identifier))
|
||||
|
||||
pet = await cursor.fetchone()
|
||||
if not pet:
|
||||
return {"success": False, "error": f"No active pet found named '{pet_identifier}'"}
|
||||
|
||||
# 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": "You must have at least one active pet!"}
|
||||
|
||||
# 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)}
|
||||
|
||||
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 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()
|
||||
|
||||
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()
|
||||
|
||||
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 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,
|
||||
"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"
|
||||
}
|
||||
596
src/game_engine.py
Normal file
596
src/game_engine.py
Normal file
|
|
@ -0,0 +1,596 @@
|
|||
import json
|
||||
import random
|
||||
import aiosqlite
|
||||
import asyncio
|
||||
from typing import Dict, List, Optional
|
||||
from .database import Database
|
||||
from .battle_engine import BattleEngine
|
||||
|
||||
class GameEngine:
|
||||
def __init__(self, database: Database):
|
||||
self.database = database
|
||||
self.battle_engine = BattleEngine(database)
|
||||
self.pet_species = {}
|
||||
self.locations = {}
|
||||
self.moves = {}
|
||||
self.type_chart = {}
|
||||
self.weather_patterns = {}
|
||||
self.weather_task = None
|
||||
self.shutdown_event = asyncio.Event()
|
||||
|
||||
async def load_game_data(self):
|
||||
await self.load_pet_species()
|
||||
await self.load_locations()
|
||||
await self.load_moves()
|
||||
await self.load_type_chart()
|
||||
await self.load_achievements()
|
||||
await self.init_weather_system()
|
||||
await self.battle_engine.load_battle_data()
|
||||
|
||||
async def load_pet_species(self):
|
||||
try:
|
||||
with open("config/pets.json", "r") as f:
|
||||
species_data = json.load(f)
|
||||
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
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()
|
||||
|
||||
async def create_default_species(self):
|
||||
default_species = [
|
||||
{
|
||||
"name": "Flamey",
|
||||
"type1": "Fire",
|
||||
"base_hp": 45,
|
||||
"base_attack": 52,
|
||||
"base_defense": 43,
|
||||
"base_speed": 65,
|
||||
"rarity": 1
|
||||
},
|
||||
{
|
||||
"name": "Aqua",
|
||||
"type1": "Water",
|
||||
"base_hp": 44,
|
||||
"base_attack": 48,
|
||||
"base_defense": 65,
|
||||
"base_speed": 43,
|
||||
"rarity": 1
|
||||
},
|
||||
{
|
||||
"name": "Leafy",
|
||||
"type1": "Grass",
|
||||
"base_hp": 45,
|
||||
"base_attack": 49,
|
||||
"base_defense": 49,
|
||||
"base_speed": 45,
|
||||
"rarity": 1
|
||||
},
|
||||
{
|
||||
"name": "Sparky",
|
||||
"type1": "Electric",
|
||||
"base_hp": 35,
|
||||
"base_attack": 55,
|
||||
"base_defense": 40,
|
||||
"base_speed": 90,
|
||||
"rarity": 2
|
||||
},
|
||||
{
|
||||
"name": "Rocky",
|
||||
"type1": "Rock",
|
||||
"base_hp": 40,
|
||||
"base_attack": 80,
|
||||
"base_defense": 100,
|
||||
"base_speed": 25,
|
||||
"rarity": 2
|
||||
}
|
||||
]
|
||||
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
for species in default_species:
|
||||
await db.execute("""
|
||||
INSERT OR IGNORE INTO pet_species
|
||||
(name, type1, type2, base_hp, base_attack, base_defense, base_speed, rarity)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
species["name"], species["type1"], species.get("type2"),
|
||||
species["base_hp"], species["base_attack"], species["base_defense"],
|
||||
species["base_speed"], species["rarity"]
|
||||
))
|
||||
await db.commit()
|
||||
|
||||
async def load_locations(self):
|
||||
try:
|
||||
with open("config/locations.json", "r") as f:
|
||||
locations_data = json.load(f)
|
||||
except FileNotFoundError:
|
||||
locations_data = self.get_default_locations()
|
||||
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
for location in locations_data:
|
||||
await db.execute("""
|
||||
INSERT OR IGNORE INTO locations (name, description, level_min, level_max)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (location["name"], location["description"],
|
||||
location["level_min"], location["level_max"]))
|
||||
|
||||
cursor = await db.execute(
|
||||
"SELECT id FROM locations WHERE name = ?", (location["name"],)
|
||||
)
|
||||
location_row = await cursor.fetchone()
|
||||
location_id = location_row[0]
|
||||
|
||||
for spawn in location["spawns"]:
|
||||
species_cursor = await db.execute(
|
||||
"SELECT id FROM pet_species WHERE name = ?", (spawn["species"],)
|
||||
)
|
||||
species_id = await species_cursor.fetchone()
|
||||
|
||||
if species_id:
|
||||
await db.execute("""
|
||||
INSERT OR IGNORE INTO location_spawns
|
||||
(location_id, species_id, spawn_rate, min_level, max_level)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", (location_id, species_id[0], spawn["spawn_rate"],
|
||||
spawn["min_level"], spawn["max_level"]))
|
||||
await db.commit()
|
||||
|
||||
def get_default_locations(self):
|
||||
return [
|
||||
{
|
||||
"name": "Starter Woods",
|
||||
"description": "A peaceful forest perfect for beginners",
|
||||
"level_min": 1,
|
||||
"level_max": 5,
|
||||
"spawns": [
|
||||
{"species": "Leafy", "spawn_rate": 0.4, "min_level": 1, "max_level": 3},
|
||||
{"species": "Flamey", "spawn_rate": 0.3, "min_level": 1, "max_level": 3},
|
||||
{"species": "Aqua", "spawn_rate": 0.3, "min_level": 1, "max_level": 3}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Electric Canyon",
|
||||
"description": "A charged valley crackling with energy",
|
||||
"level_min": 3,
|
||||
"level_max": 8,
|
||||
"spawns": [
|
||||
{"species": "Sparky", "spawn_rate": 0.6, "min_level": 3, "max_level": 6},
|
||||
{"species": "Rocky", "spawn_rate": 0.4, "min_level": 4, "max_level": 7}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
async def load_moves(self):
|
||||
pass
|
||||
|
||||
async def load_type_chart(self):
|
||||
pass
|
||||
|
||||
async def give_starter_pet(self, player_id: int) -> Dict:
|
||||
starters = ["Flamey", "Aqua", "Leafy"]
|
||||
chosen_starter = random.choice(starters)
|
||||
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute(
|
||||
"SELECT * FROM pet_species WHERE name = ?", (chosen_starter,)
|
||||
)
|
||||
species = await cursor.fetchone()
|
||||
|
||||
pet_data = self.generate_pet_stats(dict(species), level=5)
|
||||
|
||||
cursor = await db.execute("""
|
||||
INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp,
|
||||
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))
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {"species_name": chosen_starter, **pet_data}
|
||||
|
||||
def generate_pet_stats(self, species: Dict, level: int = 1) -> Dict:
|
||||
iv_bonus = random.randint(0, 31)
|
||||
|
||||
hp = int((2 * species["base_hp"] + iv_bonus) * level / 100) + level + 10
|
||||
attack = int((2 * species["base_attack"] + iv_bonus) * level / 100) + 5
|
||||
defense = int((2 * species["base_defense"] + iv_bonus) * level / 100) + 5
|
||||
speed = int((2 * species["base_speed"] + iv_bonus) * level / 100) + 5
|
||||
|
||||
return {
|
||||
"level": level,
|
||||
"hp": hp,
|
||||
"attack": attack,
|
||||
"defense": defense,
|
||||
"speed": speed
|
||||
}
|
||||
|
||||
async def attempt_catch(self, player_id: int, location_name: str) -> str:
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute(
|
||||
"SELECT * FROM locations WHERE name = ?", (location_name,)
|
||||
)
|
||||
location = await cursor.fetchone()
|
||||
|
||||
if not location:
|
||||
return f"Location '{location_name}' not found!"
|
||||
|
||||
cursor = await db.execute("""
|
||||
SELECT ls.*, ps.name as species_name, ps.*
|
||||
FROM location_spawns ls
|
||||
JOIN pet_species ps ON ls.species_id = ps.id
|
||||
WHERE ls.location_id = ?
|
||||
""", (location["id"],))
|
||||
spawns = await cursor.fetchall()
|
||||
|
||||
if not spawns:
|
||||
return f"No pets found in {location_name}!"
|
||||
|
||||
if random.random() > 0.7:
|
||||
return f"You searched {location_name} but found nothing..."
|
||||
|
||||
chosen_spawn = random.choices(
|
||||
spawns,
|
||||
weights=[spawn["spawn_rate"] for spawn in spawns]
|
||||
)[0]
|
||||
|
||||
pet_level = random.randint(chosen_spawn["min_level"], chosen_spawn["max_level"])
|
||||
pet_stats = self.generate_pet_stats(dict(chosen_spawn), pet_level)
|
||||
|
||||
catch_rate = 0.5 + (0.3 / chosen_spawn["rarity"])
|
||||
if random.random() < catch_rate:
|
||||
cursor = await db.execute("""
|
||||
INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp,
|
||||
attack, defense, speed, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (player_id, chosen_spawn["species_id"], pet_level, 0,
|
||||
pet_stats["hp"], pet_stats["hp"], pet_stats["attack"],
|
||||
pet_stats["defense"], pet_stats["speed"], False))
|
||||
|
||||
await db.commit()
|
||||
return f"Caught a level {pet_level} {chosen_spawn['species_name']}!"
|
||||
else:
|
||||
return f"A wild {chosen_spawn['species_name']} appeared but escaped!"
|
||||
|
||||
async def get_location_spawns(self, location_name: str) -> List[Dict]:
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute(
|
||||
"SELECT * FROM locations WHERE name = ?", (location_name,)
|
||||
)
|
||||
location = await cursor.fetchone()
|
||||
|
||||
if not location:
|
||||
return []
|
||||
|
||||
cursor = await db.execute("""
|
||||
SELECT ps.name, ps.type1, ps.type2, ls.spawn_rate
|
||||
FROM location_spawns ls
|
||||
JOIN pet_species ps ON ls.species_id = ps.id
|
||||
WHERE ls.location_id = ?
|
||||
""", (location["id"],))
|
||||
spawns = await cursor.fetchall()
|
||||
|
||||
return [dict(spawn) for spawn in spawns]
|
||||
|
||||
async def explore_location(self, player_id: int) -> Dict:
|
||||
"""Explore the player's current location and return encounter results"""
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
|
||||
# Get player's current location
|
||||
location = await self.database.get_player_location(player_id)
|
||||
if not location:
|
||||
return {"type": "error", "message": "You are not in a valid location!"}
|
||||
|
||||
# Get spawns for current location
|
||||
cursor = await db.execute("""
|
||||
SELECT ls.*, ps.name as species_name, ps.*
|
||||
FROM location_spawns ls
|
||||
JOIN pet_species ps ON ls.species_id = ps.id
|
||||
WHERE ls.location_id = ?
|
||||
""", (location["id"],))
|
||||
spawns = await cursor.fetchall()
|
||||
|
||||
if not spawns:
|
||||
return {"type": "empty", "message": f"You explore {location['name']} but find nothing interesting..."}
|
||||
|
||||
# Apply weather modifiers to spawns
|
||||
modified_spawns = await self.get_weather_modified_spawns(location["id"], spawns)
|
||||
|
||||
# Random encounter chance (70% chance of finding something)
|
||||
if random.random() > 0.7:
|
||||
return {"type": "empty", "message": f"You explore {location['name']} but find nothing this time..."}
|
||||
|
||||
# Choose random spawn with weather-modified rates
|
||||
chosen_spawn = random.choices(
|
||||
modified_spawns,
|
||||
weights=[spawn["spawn_rate"] for spawn in modified_spawns]
|
||||
)[0]
|
||||
|
||||
pet_level = random.randint(chosen_spawn["min_level"], chosen_spawn["max_level"])
|
||||
|
||||
pet_stats = self.generate_pet_stats(dict(chosen_spawn), pet_level)
|
||||
|
||||
return {
|
||||
"type": "encounter",
|
||||
"location": location["name"],
|
||||
"pet": {
|
||||
"species_id": chosen_spawn["species_id"],
|
||||
"species_name": chosen_spawn["species_name"],
|
||||
"level": pet_level,
|
||||
"type1": chosen_spawn["type1"],
|
||||
"type2": chosen_spawn["type2"],
|
||||
"stats": pet_stats,
|
||||
# Additional battle-ready stats
|
||||
"hp": pet_stats["hp"],
|
||||
"max_hp": pet_stats["hp"],
|
||||
"attack": pet_stats["attack"],
|
||||
"defense": pet_stats["defense"],
|
||||
"speed": pet_stats["speed"]
|
||||
}
|
||||
}
|
||||
|
||||
async def attempt_catch_current_location(self, player_id: int, target_pet: Dict) -> str:
|
||||
"""Attempt to catch a pet during exploration"""
|
||||
catch_rate = 0.5 + (0.3 / target_pet.get("rarity", 1))
|
||||
|
||||
if random.random() < catch_rate:
|
||||
# Successfully caught the pet
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
cursor = await db.execute("""
|
||||
INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp,
|
||||
attack, defense, speed, is_active)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (player_id, target_pet["species_id"], target_pet["level"], 0,
|
||||
target_pet["stats"]["hp"], target_pet["stats"]["hp"],
|
||||
target_pet["stats"]["attack"], target_pet["stats"]["defense"],
|
||||
target_pet["stats"]["speed"], False))
|
||||
|
||||
await db.commit()
|
||||
return f"Success! You caught the Level {target_pet['level']} {target_pet['species_name']}!"
|
||||
else:
|
||||
return f"The wild {target_pet['species_name']} escaped!"
|
||||
|
||||
async def load_achievements(self):
|
||||
"""Load achievements from config and populate database"""
|
||||
try:
|
||||
with open("config/achievements.json", "r") as f:
|
||||
achievements_data = json.load(f)
|
||||
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
for achievement in achievements_data:
|
||||
# Get location ID if specified
|
||||
location_id = None
|
||||
if achievement.get("unlock_location"):
|
||||
cursor = await db.execute(
|
||||
"SELECT id FROM locations WHERE name = ?",
|
||||
(achievement["unlock_location"],)
|
||||
)
|
||||
location_row = await cursor.fetchone()
|
||||
if location_row:
|
||||
location_id = location_row[0]
|
||||
|
||||
# Insert or update achievement
|
||||
await db.execute("""
|
||||
INSERT OR IGNORE INTO achievements
|
||||
(name, description, requirement_type, requirement_data, unlock_location_id)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", (
|
||||
achievement["name"], achievement["description"],
|
||||
achievement["requirement_type"], achievement["requirement_data"],
|
||||
location_id
|
||||
))
|
||||
|
||||
await db.commit()
|
||||
|
||||
except FileNotFoundError:
|
||||
print("No achievements.json found, skipping achievement loading")
|
||||
|
||||
async def init_weather_system(self):
|
||||
"""Initialize random weather for all locations"""
|
||||
try:
|
||||
with open("config/weather_patterns.json", "r") as f:
|
||||
self.weather_patterns = json.load(f)
|
||||
|
||||
# Set initial weather for all locations
|
||||
await self.update_all_weather()
|
||||
|
||||
# Start background weather update task
|
||||
await self.start_weather_system()
|
||||
|
||||
except FileNotFoundError:
|
||||
print("No weather_patterns.json found, skipping weather system")
|
||||
self.weather_patterns = {"weather_types": {}, "location_weather_chances": {}}
|
||||
|
||||
async def update_all_weather(self):
|
||||
"""Update weather for all locations"""
|
||||
import datetime
|
||||
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
# Get all locations
|
||||
cursor = await db.execute("SELECT * FROM locations")
|
||||
locations = await cursor.fetchall()
|
||||
|
||||
for location in locations:
|
||||
location_name = location[1] # name is second column
|
||||
|
||||
# Get possible weather for this location
|
||||
possible_weather = self.weather_patterns.get("location_weather_chances", {}).get(location_name, ["Calm"])
|
||||
|
||||
# Choose random weather
|
||||
weather_type = random.choice(possible_weather)
|
||||
weather_config = self.weather_patterns.get("weather_types", {}).get(weather_type, {
|
||||
"spawn_modifier": 1.0,
|
||||
"affected_types": [],
|
||||
"duration_minutes": [90, 180]
|
||||
})
|
||||
|
||||
# Calculate end time with random duration
|
||||
duration_range = weather_config.get("duration_minutes", [90, 180])
|
||||
duration_minutes = random.randint(duration_range[0], duration_range[1])
|
||||
end_time = datetime.datetime.now() + datetime.timedelta(minutes=duration_minutes)
|
||||
|
||||
# Clear old weather and set new
|
||||
await db.execute("DELETE FROM location_weather WHERE location_id = ?", (location[0],))
|
||||
await db.execute("""
|
||||
INSERT INTO location_weather
|
||||
(location_id, weather_type, active_until, spawn_modifier, affected_types)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", (
|
||||
location[0], weather_type, end_time.isoformat(),
|
||||
weather_config.get("spawn_modifier", 1.0),
|
||||
",".join(weather_config.get("affected_types", []))
|
||||
))
|
||||
|
||||
await db.commit()
|
||||
|
||||
async def check_and_award_achievements(self, player_id: int, action_type: str, data: str = ""):
|
||||
"""Check for new achievements after player actions"""
|
||||
return await self.database.check_player_achievements(player_id, action_type, data)
|
||||
|
||||
async def get_weather_modified_spawns(self, location_id: int, spawns: list) -> list:
|
||||
"""Apply weather modifiers to spawn rates"""
|
||||
weather = await self.database.get_location_weather(location_id)
|
||||
|
||||
if not weather or not weather.get("affected_types"):
|
||||
return spawns # No weather effects
|
||||
|
||||
affected_types = weather["affected_types"].split(",") if weather["affected_types"] else []
|
||||
modifier = weather.get("spawn_modifier", 1.0)
|
||||
|
||||
# Apply weather modifier to matching pet types
|
||||
modified_spawns = []
|
||||
for spawn in spawns:
|
||||
spawn_dict = dict(spawn)
|
||||
pet_types = [spawn_dict.get("type1")]
|
||||
if spawn_dict.get("type2"):
|
||||
pet_types.append(spawn_dict["type2"])
|
||||
|
||||
# Check if this pet type is affected by weather
|
||||
if any(pet_type in affected_types for pet_type in pet_types):
|
||||
spawn_dict["spawn_rate"] *= modifier
|
||||
|
||||
modified_spawns.append(spawn_dict)
|
||||
|
||||
return modified_spawns
|
||||
|
||||
async def start_weather_system(self):
|
||||
"""Start the background weather update task"""
|
||||
if self.weather_task is None or self.weather_task.done():
|
||||
print("🌤️ Starting weather update background task...")
|
||||
self.weather_task = asyncio.create_task(self._weather_update_loop())
|
||||
|
||||
async def stop_weather_system(self):
|
||||
"""Stop the background weather update task"""
|
||||
print("🌤️ Stopping weather update background task...")
|
||||
self.shutdown_event.set()
|
||||
if self.weather_task and not self.weather_task.done():
|
||||
self.weather_task.cancel()
|
||||
try:
|
||||
await self.weather_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
async def _weather_update_loop(self):
|
||||
"""Background task that checks and updates expired weather"""
|
||||
try:
|
||||
while not self.shutdown_event.is_set():
|
||||
try:
|
||||
# Check every 5 minutes for expired weather
|
||||
await asyncio.sleep(300) # 5 minutes
|
||||
|
||||
if self.shutdown_event.is_set():
|
||||
break
|
||||
|
||||
# Check for locations with expired weather
|
||||
await self._check_and_update_expired_weather()
|
||||
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error in weather update loop: {e}")
|
||||
# Continue the loop even if there's an error
|
||||
await asyncio.sleep(60) # Wait a minute before retrying
|
||||
|
||||
except asyncio.CancelledError:
|
||||
print("Weather update task cancelled")
|
||||
|
||||
async def _check_and_update_expired_weather(self):
|
||||
"""Check for expired weather and update it"""
|
||||
try:
|
||||
async with aiosqlite.connect(self.database.db_path) as db:
|
||||
# Find locations with expired weather
|
||||
cursor = await db.execute("""
|
||||
SELECT l.id, l.name
|
||||
FROM locations l
|
||||
WHERE l.id NOT IN (
|
||||
SELECT location_id FROM location_weather
|
||||
WHERE active_until > datetime('now')
|
||||
)
|
||||
""")
|
||||
expired_locations = await cursor.fetchall()
|
||||
|
||||
if expired_locations:
|
||||
print(f"🌤️ Updating weather for {len(expired_locations)} locations with expired weather")
|
||||
|
||||
for location in expired_locations:
|
||||
location_id = location[0]
|
||||
location_name = location[1]
|
||||
|
||||
# Get possible weather for this location
|
||||
possible_weather = self.weather_patterns.get("location_weather_chances", {}).get(location_name, ["Calm"])
|
||||
|
||||
# Choose random weather
|
||||
weather_type = random.choice(possible_weather)
|
||||
weather_config = self.weather_patterns.get("weather_types", {}).get(weather_type, {
|
||||
"spawn_modifier": 1.0,
|
||||
"affected_types": [],
|
||||
"duration_minutes": [90, 180]
|
||||
})
|
||||
|
||||
# Calculate end time with random duration
|
||||
duration_range = weather_config.get("duration_minutes", [90, 180])
|
||||
duration_minutes = random.randint(duration_range[0], duration_range[1])
|
||||
import datetime
|
||||
end_time = datetime.datetime.now() + datetime.timedelta(minutes=duration_minutes)
|
||||
|
||||
# Clear old weather and set new
|
||||
await db.execute("DELETE FROM location_weather WHERE location_id = ?", (location_id,))
|
||||
await db.execute("""
|
||||
INSERT INTO location_weather
|
||||
(location_id, weather_type, active_until, spawn_modifier, affected_types)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", (
|
||||
location_id, weather_type, end_time.isoformat(),
|
||||
weather_config.get("spawn_modifier", 1.0),
|
||||
",".join(weather_config.get("affected_types", []))
|
||||
))
|
||||
|
||||
print(f" 🌤️ {location_name}: New {weather_type} weather for {duration_minutes} minutes")
|
||||
|
||||
await db.commit()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error checking expired weather: {e}")
|
||||
|
||||
async def shutdown(self):
|
||||
"""Gracefully shutdown the game engine"""
|
||||
print("🔄 Shutting down game engine...")
|
||||
await self.stop_weather_system()
|
||||
Loading…
Add table
Add a link
Reference in a new issue