Implement gym battle system with location-based challenges
🏛️ Gym Battle System - Phase 1: - Complete database schema with 4 new tables (gyms, gym_teams, player_gym_battles) - 6 unique gyms across all locations with themed leaders and badges - Location-based challenges (must travel to gym location) - Difficulty scaling system (gets harder with each victory) - Badge tracking and progress system 🎯 Features Added: - \!gym - List gyms in current location with progress - \!gym list - Show all gyms across all locations - \!gym challenge "<name>" - Challenge a gym (location validation) - \!gym info "<name>" - Get detailed gym information - \!gym status - Quick status overview 🏆 Gym Roster: - Starter Town: Forest Guardian (Grass) - Trainer Verde - Whispering Woods: Nature's Haven (Grass) - Elder Sage - Electric Canyon: Storm Master (Electric) - Captain Volt - Crystal Caves: Stone Crusher (Rock) - Miner Magnus - Frozen Tundra: Ice Breaker (Ice/Water) - Arctic Queen - Dragon's Peak: Dragon Slayer (Fire) - Champion Drake ⚔️ Battle Mechanics: - Teams scale with victory count (20% stat increase per win) - First victory awards badge + bonus rewards - Repeat challenges give scaling rewards - Simulated battles (full battle system integration pending) 🔧 Technical Implementation: - Config-driven gym system (config/gyms.json) - Scalable database design for easy expansion - Location validation prevents remote challenges - Automatic gym data loading on startup Next phase: Integrate with full battle system and add web interface badges. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d74c6f2897
commit
87eff2a336
7 changed files with 703 additions and 6 deletions
256
src/database.py
256
src/database.py
|
|
@ -213,6 +213,49 @@ class Database:
|
|||
)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS gyms (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
location_id INTEGER NOT NULL,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
leader_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
theme TEXT NOT NULL,
|
||||
badge_name TEXT NOT NULL,
|
||||
badge_icon TEXT NOT NULL,
|
||||
badge_description TEXT,
|
||||
FOREIGN KEY (location_id) REFERENCES locations (id)
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS gym_teams (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
gym_id INTEGER NOT NULL,
|
||||
species_id INTEGER NOT NULL,
|
||||
base_level INTEGER NOT NULL,
|
||||
move_set TEXT,
|
||||
team_position INTEGER NOT NULL,
|
||||
FOREIGN KEY (gym_id) REFERENCES gyms (id),
|
||||
FOREIGN KEY (species_id) REFERENCES pet_species (id)
|
||||
)
|
||||
""")
|
||||
|
||||
await db.execute("""
|
||||
CREATE TABLE IF NOT EXISTS player_gym_battles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
player_id INTEGER NOT NULL,
|
||||
gym_id INTEGER NOT NULL,
|
||||
victories INTEGER DEFAULT 0,
|
||||
highest_difficulty INTEGER DEFAULT 0,
|
||||
first_victory_date TIMESTAMP,
|
||||
last_battle_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (player_id) REFERENCES players (id),
|
||||
FOREIGN KEY (gym_id) REFERENCES gyms (id),
|
||||
UNIQUE(player_id, gym_id)
|
||||
)
|
||||
""")
|
||||
|
||||
await db.commit()
|
||||
|
||||
async def get_player(self, nickname: str) -> Optional[Dict]:
|
||||
|
|
@ -286,6 +329,14 @@ class Database:
|
|||
row = await cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
async def get_all_locations(self) -> List[Dict]:
|
||||
"""Get all locations"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("SELECT * FROM locations ORDER BY id")
|
||||
rows = await cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
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:
|
||||
|
|
@ -686,4 +737,207 @@ class Database:
|
|||
async with aiosqlite.connect(self.db_path) as db:
|
||||
await db.execute("UPDATE pets SET hp = ? WHERE id = ?", (new_hp, pet_id))
|
||||
await db.commit()
|
||||
return True
|
||||
return True
|
||||
|
||||
# Gym Battle System Methods
|
||||
async def get_gyms_in_location(self, location_id: int, player_id: int = None) -> List[Dict]:
|
||||
"""Get all gyms in a specific location with optional player progress"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
|
||||
if player_id:
|
||||
cursor = await db.execute("""
|
||||
SELECT g.*, l.name as location_name,
|
||||
COALESCE(pgb.victories, 0) as victories,
|
||||
COALESCE(pgb.highest_difficulty, 0) as highest_difficulty,
|
||||
pgb.first_victory_date
|
||||
FROM gyms g
|
||||
JOIN locations l ON g.location_id = l.id
|
||||
LEFT JOIN player_gym_battles pgb ON g.id = pgb.gym_id AND pgb.player_id = ?
|
||||
WHERE g.location_id = ?
|
||||
ORDER BY g.id
|
||||
""", (player_id, location_id))
|
||||
else:
|
||||
cursor = await db.execute("""
|
||||
SELECT g.*, l.name as location_name
|
||||
FROM gyms g
|
||||
JOIN locations l ON g.location_id = l.id
|
||||
WHERE g.location_id = ?
|
||||
ORDER BY g.id
|
||||
""", (location_id,))
|
||||
|
||||
rows = await cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
async def get_gym_by_name(self, gym_name: str) -> Optional[Dict]:
|
||||
"""Get gym details by name"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT g.*, l.name as location_name
|
||||
FROM gyms g
|
||||
JOIN locations l ON g.location_id = l.id
|
||||
WHERE g.name = ?
|
||||
""", (gym_name,))
|
||||
row = await cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
async def get_gym_team(self, gym_id: int, difficulty_multiplier: float = 1.0) -> List[Dict]:
|
||||
"""Get gym team with difficulty scaling applied"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT gt.*, ps.name as species_name, ps.type1, ps.type2,
|
||||
ps.base_hp, ps.base_attack, ps.base_defense, ps.base_speed
|
||||
FROM gym_teams gt
|
||||
JOIN pet_species ps ON gt.species_id = ps.id
|
||||
WHERE gt.gym_id = ?
|
||||
ORDER BY gt.team_position
|
||||
""", (gym_id,))
|
||||
rows = await cursor.fetchall()
|
||||
|
||||
team = []
|
||||
for row in rows:
|
||||
# Apply difficulty scaling
|
||||
scaled_level = int(row["base_level"] * difficulty_multiplier)
|
||||
stat_multiplier = 1.0 + (difficulty_multiplier - 1.0) * 0.5 # Less aggressive stat scaling
|
||||
|
||||
# Calculate scaled stats
|
||||
hp = int((2 * row["base_hp"] + 31) * scaled_level / 100) + scaled_level + 10
|
||||
attack = int(((2 * row["base_attack"] + 31) * scaled_level / 100) + 5) * stat_multiplier
|
||||
defense = int(((2 * row["base_defense"] + 31) * scaled_level / 100) + 5) * stat_multiplier
|
||||
speed = int(((2 * row["base_speed"] + 31) * scaled_level / 100) + 5) * stat_multiplier
|
||||
|
||||
pet_data = {
|
||||
"species_id": row["species_id"],
|
||||
"species_name": row["species_name"],
|
||||
"level": scaled_level,
|
||||
"type1": row["type1"],
|
||||
"type2": row["type2"],
|
||||
"hp": int(hp),
|
||||
"max_hp": int(hp),
|
||||
"attack": int(attack),
|
||||
"defense": int(defense),
|
||||
"speed": int(speed),
|
||||
"moves": row["move_set"].split(",") if row["move_set"] else ["Tackle", "Growl"],
|
||||
"position": row["team_position"]
|
||||
}
|
||||
team.append(pet_data)
|
||||
|
||||
return team
|
||||
|
||||
async def get_player_gym_progress(self, player_id: int, gym_id: int) -> Optional[Dict]:
|
||||
"""Get player's progress for a specific gym"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM player_gym_battles
|
||||
WHERE player_id = ? AND gym_id = ?
|
||||
""", (player_id, gym_id))
|
||||
row = await cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
async def record_gym_victory(self, player_id: int, gym_id: int) -> Dict:
|
||||
"""Record a gym victory and update player progress"""
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
# Get current progress
|
||||
cursor = await db.execute("""
|
||||
SELECT victories, first_victory_date FROM player_gym_battles
|
||||
WHERE player_id = ? AND gym_id = ?
|
||||
""", (player_id, gym_id))
|
||||
current = await cursor.fetchone()
|
||||
|
||||
if current:
|
||||
new_victories = current[0] + 1
|
||||
await db.execute("""
|
||||
UPDATE player_gym_battles
|
||||
SET victories = ?, highest_difficulty = ?, last_battle_date = CURRENT_TIMESTAMP
|
||||
WHERE player_id = ? AND gym_id = ?
|
||||
""", (new_victories, new_victories, player_id, gym_id))
|
||||
else:
|
||||
new_victories = 1
|
||||
await db.execute("""
|
||||
INSERT INTO player_gym_battles
|
||||
(player_id, gym_id, victories, highest_difficulty, first_victory_date, last_battle_date)
|
||||
VALUES (?, ?, 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
""", (player_id, gym_id))
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"victories": new_victories,
|
||||
"is_first_victory": current is None,
|
||||
"next_difficulty": new_victories + 1
|
||||
}
|
||||
|
||||
async def initialize_gyms(self):
|
||||
"""Initialize gyms from config file"""
|
||||
import json
|
||||
|
||||
try:
|
||||
with open("config/gyms.json", "r") as f:
|
||||
gyms_data = json.load(f)
|
||||
except FileNotFoundError:
|
||||
print("Gyms config file not found")
|
||||
return
|
||||
|
||||
async with aiosqlite.connect(self.db_path) as db:
|
||||
# Clear existing gym data
|
||||
await db.execute("DELETE FROM gym_teams")
|
||||
await db.execute("DELETE FROM gyms")
|
||||
|
||||
for gym_config in gyms_data:
|
||||
# Get location ID
|
||||
cursor = await db.execute(
|
||||
"SELECT id FROM locations WHERE name = ?",
|
||||
(gym_config["location"],)
|
||||
)
|
||||
location_row = await cursor.fetchone()
|
||||
if not location_row:
|
||||
print(f"Location '{gym_config['location']}' not found for gym '{gym_config['name']}'")
|
||||
continue
|
||||
|
||||
location_id = location_row[0]
|
||||
|
||||
# Insert gym
|
||||
cursor = await db.execute("""
|
||||
INSERT OR REPLACE INTO gyms
|
||||
(location_id, name, leader_name, description, theme, badge_name, badge_icon, badge_description)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
location_id,
|
||||
gym_config["name"],
|
||||
gym_config["leader_name"],
|
||||
gym_config["description"],
|
||||
gym_config["theme"],
|
||||
gym_config["badge"]["name"],
|
||||
gym_config["badge"]["icon"],
|
||||
gym_config["badge"]["description"]
|
||||
))
|
||||
|
||||
gym_id = cursor.lastrowid
|
||||
|
||||
# Insert gym team
|
||||
for member in gym_config["team"]:
|
||||
# Get species ID
|
||||
species_cursor = await db.execute(
|
||||
"SELECT id FROM pet_species WHERE name = ?",
|
||||
(member["species"],)
|
||||
)
|
||||
species_row = await species_cursor.fetchone()
|
||||
if species_row:
|
||||
moves_str = ",".join(member["moves"]) if member["moves"] else ""
|
||||
await db.execute("""
|
||||
INSERT INTO gym_teams
|
||||
(gym_id, species_id, base_level, move_set, team_position)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", (
|
||||
gym_id,
|
||||
species_row[0],
|
||||
member["base_level"],
|
||||
moves_str,
|
||||
member["position"]
|
||||
))
|
||||
|
||||
await db.commit()
|
||||
print("Gyms initialized from config")
|
||||
|
|
@ -25,6 +25,7 @@ class GameEngine:
|
|||
await self.load_type_chart()
|
||||
await self.load_achievements()
|
||||
await self.database.initialize_items()
|
||||
await self.database.initialize_gyms()
|
||||
await self.init_weather_system()
|
||||
await self.battle_engine.load_battle_data()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue