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
16
.claude/settings.local.json
Normal file
16
.claude/settings.local.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(timeout:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(kill:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(pip3 install:*)",
|
||||
"Bash(apt list:*)",
|
||||
"Bash(curl:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
78
.gitignore
vendored
Normal file
78
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
data/petbot.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
webserver.log
|
||||
|
||||
# Configuration (might contain sensitive data)
|
||||
config/secrets.json
|
||||
config/private.json
|
||||
config/settings.json
|
||||
|
||||
# Test files
|
||||
test_*.py
|
||||
*_test.py
|
||||
test_output/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
*.bak
|
||||
*.backup
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# IRC bot specific
|
||||
*.pid
|
||||
*.lock
|
||||
184
README.md
Normal file
184
README.md
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
# PetBot - IRC Pokemon-Style Pet Game Bot
|
||||
|
||||
A feature-rich IRC bot that brings Pokemon-style pet collecting and battling to your IRC channel! Players can catch pets, explore locations, battle wild creatures, earn achievements, and more.
|
||||
|
||||
## 🎮 Features
|
||||
|
||||
### Core Gameplay
|
||||
- **Pet Collection**: Catch and collect different species of pets
|
||||
- **Exploration**: Travel between various themed locations
|
||||
- **Battle System**: Engage in turn-based battles with wild pets
|
||||
- **Team Management**: Activate/deactivate pets, swap team members
|
||||
- **Achievement System**: Unlock new areas by completing challenges
|
||||
|
||||
### Advanced Systems
|
||||
- **Dynamic Weather**: Real-time weather system affecting spawn rates
|
||||
- **Web Interface**: Modern web dashboard for player stats and pet collections
|
||||
- **Location-Based Spawns**: Different pets spawn in different locations
|
||||
- **Level Progression**: Pets gain experience and level up
|
||||
- **Type Effectiveness**: Strategic battle system with type advantages
|
||||
|
||||
### Technical Features
|
||||
- **Modular Architecture**: Clean, extensible codebase
|
||||
- **Async Database**: SQLite with async operations
|
||||
- **Background Tasks**: Automated weather updates
|
||||
- **PM Flood Prevention**: Web-based responses for large data sets
|
||||
- **Persistent Data**: Player progress survives bot restarts
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- Python 3.8+
|
||||
- SQLite3
|
||||
- Network access for IRC and web server
|
||||
|
||||
### Installation
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/petbot.git
|
||||
cd petbot
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
pip install aiosqlite
|
||||
```
|
||||
|
||||
3. Configure the bot:
|
||||
```bash
|
||||
cp config/settings.example.json config/settings.json
|
||||
# Edit config/settings.json with your IRC details
|
||||
```
|
||||
|
||||
4. Run the bot:
|
||||
```bash
|
||||
python3 run_bot_debug.py
|
||||
```
|
||||
|
||||
## 📋 Commands
|
||||
|
||||
### Player Commands
|
||||
- `!start` - Begin your pet collecting journey
|
||||
- `!explore` - Search for wild pets in your current location
|
||||
- `!catch <pet>` - Attempt to catch a wild pet
|
||||
- `!team` - View your active team
|
||||
- `!pets` - View your complete collection (web link)
|
||||
- `!travel <location>` - Move to a different location
|
||||
- `!weather` - Check current weather effects
|
||||
- `!achievements` - View your progress and unlocked achievements
|
||||
|
||||
### Battle Commands
|
||||
- `!battle` - Start a battle with a wild pet
|
||||
- `!attack <move>` - Use a move in battle
|
||||
- `!flee` - Attempt to escape from battle
|
||||
- `!moves` - View your active pet's moves
|
||||
|
||||
### Pet Management
|
||||
- `!activate <pet>` - Activate a pet for battle
|
||||
- `!deactivate <pet>` - Move a pet to storage
|
||||
- `!swap <pet1> <pet2>` - Swap two pets' active status
|
||||
|
||||
## 🌍 Locations
|
||||
|
||||
### Available Areas
|
||||
- **Starter Town**: Peaceful starting area (Fire/Water/Grass pets)
|
||||
- **Whispering Woods**: Ancient forest (Grass/Nature pets)
|
||||
- **Electric Canyon**: Charged valley (Electric/Rock pets)
|
||||
- **Crystal Caves**: Underground caverns (Rock/Crystal pets)
|
||||
- **Frozen Tundra**: Icy wasteland (Ice/Water pets)
|
||||
- **Dragon's Peak**: Ultimate challenge (Fire/Rock/Ice pets)
|
||||
|
||||
### Unlocking Locations
|
||||
Locations are unlocked by completing achievements:
|
||||
- **Nature Explorer**: Catch 3 different Grass-type pets → Whispering Woods
|
||||
- **Spark Collector**: Catch 2 different Electric-type pets → Electric Canyon
|
||||
- **Rock Hound**: Catch 3 different Rock-type pets → Crystal Caves
|
||||
- **Ice Breaker**: Catch 5 different Water/Ice-type pets → Frozen Tundra
|
||||
- **Dragon Tamer**: Catch 15 pets total + 3 Fire-types → Dragon's Peak
|
||||
|
||||
## 🌤️ Weather System
|
||||
|
||||
### Weather Types & Effects
|
||||
- **Sunny**: 1.5x Fire/Grass spawns (1-2 hours)
|
||||
- **Rainy**: 2.0x Water spawns (45-90 minutes)
|
||||
- **Thunderstorm**: 2.0x Electric spawns (30-60 minutes)
|
||||
- **Blizzard**: 1.7x Ice/Water spawns (1-2 hours)
|
||||
- **Earthquake**: 1.8x Rock spawns (30-90 minutes)
|
||||
- **Calm**: Normal spawns (1.5-3 hours)
|
||||
|
||||
### Background System
|
||||
- Weather updates automatically every 5 minutes
|
||||
- Dynamic durations from 30 minutes to 3 hours
|
||||
- Location-specific weather patterns
|
||||
- Real-time spawn rate modifications
|
||||
|
||||
## 🌐 Web Interface
|
||||
|
||||
Access the web dashboard at `http://localhost:8080/`:
|
||||
- **Player Profiles**: Complete stats and pet collections
|
||||
- **Leaderboard**: Top players by level and achievements
|
||||
- **Locations Guide**: All areas with spawn information
|
||||
- **Help System**: Complete command reference
|
||||
|
||||
## 🔧 Architecture
|
||||
|
||||
### Core Components
|
||||
- **Database Layer**: Async SQLite with comprehensive schema
|
||||
- **Game Engine**: Core logic for pets, battles, weather
|
||||
- **Module System**: Pluggable command handlers
|
||||
- **Web Server**: Built-in HTTP server for dashboard
|
||||
- **Battle Engine**: Turn-based combat system
|
||||
|
||||
### Key Files
|
||||
- `src/database.py` - Database operations and schema
|
||||
- `src/game_engine.py` - Core game logic and weather system
|
||||
- `src/battle_engine.py` - Combat mechanics
|
||||
- `modules/` - Command handler modules
|
||||
- `webserver.py` - Web interface server
|
||||
- `config/` - Configuration files
|
||||
|
||||
## 🐛 Recent Updates
|
||||
|
||||
### Weather System Enhancement
|
||||
- ✅ Added background task for automatic weather updates
|
||||
- ✅ Changed weather durations from 2-6 hours to 30min-3hours
|
||||
- ✅ Implemented continuous weather coverage
|
||||
- ✅ Added graceful shutdown handling
|
||||
|
||||
### Bug Fixes
|
||||
- ✅ Fixed database persistence on bot restart
|
||||
- ✅ Resolved individual player pages showing 'not found'
|
||||
- ✅ Corrected achievement count displays
|
||||
- ✅ Added specific travel requirement messages
|
||||
- ✅ Fixed locations page display
|
||||
- ✅ Implemented colored battle moves by type
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch: `git checkout -b feature-name`
|
||||
3. Make your changes
|
||||
4. Add tests if applicable
|
||||
5. Commit with descriptive messages
|
||||
6. Push and create a pull request
|
||||
|
||||
## 📝 License
|
||||
|
||||
This project is open source. Feel free to use, modify, and distribute.
|
||||
|
||||
## 🎯 Roadmap
|
||||
|
||||
- [ ] PvP battles between players
|
||||
- [ ] Pet evolution system
|
||||
- [ ] Trading between players
|
||||
- [ ] Seasonal events
|
||||
- [ ] More pet types and locations
|
||||
- [ ] Mobile-responsive web interface
|
||||
|
||||
## 🐾 Support
|
||||
|
||||
For questions, bug reports, or feature requests, please open an issue on GitHub.
|
||||
|
||||
---
|
||||
|
||||
*Built with ❤️ for IRC communities who love Pokemon-style games!*
|
||||
51
config/achievements.json
Normal file
51
config/achievements.json
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
[
|
||||
{
|
||||
"name": "Nature Explorer",
|
||||
"description": "Catch 3 different Grass-type pets",
|
||||
"requirement_type": "catch_type",
|
||||
"requirement_data": "3:Grass",
|
||||
"unlock_location": "Whispering Woods"
|
||||
},
|
||||
{
|
||||
"name": "Spark Collector",
|
||||
"description": "Catch 2 different Electric-type pets",
|
||||
"requirement_type": "catch_type",
|
||||
"requirement_data": "2:Electric",
|
||||
"unlock_location": "Electric Canyon"
|
||||
},
|
||||
{
|
||||
"name": "Rock Hound",
|
||||
"description": "Catch 3 different Rock-type pets",
|
||||
"requirement_type": "catch_type",
|
||||
"requirement_data": "3:Rock",
|
||||
"unlock_location": "Crystal Caves"
|
||||
},
|
||||
{
|
||||
"name": "Ice Breaker",
|
||||
"description": "Catch 5 different Water or Ice-type pets",
|
||||
"requirement_type": "catch_type",
|
||||
"requirement_data": "5:Water",
|
||||
"unlock_location": "Frozen Tundra"
|
||||
},
|
||||
{
|
||||
"name": "Dragon Tamer",
|
||||
"description": "Catch 15 pets total and have 3 Fire-type pets",
|
||||
"requirement_type": "catch_total",
|
||||
"requirement_data": "15",
|
||||
"unlock_location": "Dragon's Peak"
|
||||
},
|
||||
{
|
||||
"name": "Pet Collector",
|
||||
"description": "Catch your first 5 pets",
|
||||
"requirement_type": "catch_total",
|
||||
"requirement_data": "5",
|
||||
"unlock_location": null
|
||||
},
|
||||
{
|
||||
"name": "Advanced Trainer",
|
||||
"description": "Catch 10 pets total",
|
||||
"requirement_type": "catch_total",
|
||||
"requirement_data": "10",
|
||||
"unlock_location": null
|
||||
}
|
||||
]
|
||||
66
config/locations.json
Normal file
66
config/locations.json
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
[
|
||||
{
|
||||
"name": "Starter Town",
|
||||
"description": "A peaceful town where all trainers begin their journey",
|
||||
"level_min": 1,
|
||||
"level_max": 3,
|
||||
"spawns": [
|
||||
{"species": "Leafy", "spawn_rate": 0.35, "min_level": 1, "max_level": 2},
|
||||
{"species": "Flamey", "spawn_rate": 0.35, "min_level": 1, "max_level": 2},
|
||||
{"species": "Aqua", "spawn_rate": 0.3, "min_level": 1, "max_level": 2}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Whispering Woods",
|
||||
"description": "Ancient woods filled with grass and nature pets",
|
||||
"level_min": 2,
|
||||
"level_max": 6,
|
||||
"spawns": [
|
||||
{"species": "Leafy", "spawn_rate": 0.5, "min_level": 2, "max_level": 5},
|
||||
{"species": "Flamey", "spawn_rate": 0.2, "min_level": 3, "max_level": 4},
|
||||
{"species": "Aqua", "spawn_rate": 0.3, "min_level": 2, "max_level": 4}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Electric Canyon",
|
||||
"description": "A charged valley crackling with energy",
|
||||
"level_min": 4,
|
||||
"level_max": 9,
|
||||
"spawns": [
|
||||
{"species": "Sparky", "spawn_rate": 0.6, "min_level": 4, "max_level": 7},
|
||||
{"species": "Rocky", "spawn_rate": 0.4, "min_level": 5, "max_level": 8}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Crystal Caves",
|
||||
"description": "Deep underground caverns with rock and crystal pets",
|
||||
"level_min": 6,
|
||||
"level_max": 12,
|
||||
"spawns": [
|
||||
{"species": "Rocky", "spawn_rate": 0.7, "min_level": 6, "max_level": 10},
|
||||
{"species": "Sparky", "spawn_rate": 0.3, "min_level": 7, "max_level": 9}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Frozen Tundra",
|
||||
"description": "An icy wasteland where only the strongest survive",
|
||||
"level_min": 10,
|
||||
"level_max": 16,
|
||||
"spawns": [
|
||||
{"species": "Hydrox", "spawn_rate": 0.4, "min_level": 10, "max_level": 14},
|
||||
{"species": "Rocky", "spawn_rate": 0.3, "min_level": 11, "max_level": 15},
|
||||
{"species": "Sparky", "spawn_rate": 0.3, "min_level": 12, "max_level": 14}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Dragon's Peak",
|
||||
"description": "The ultimate challenge - legendary pets roam these heights",
|
||||
"level_min": 15,
|
||||
"level_max": 25,
|
||||
"spawns": [
|
||||
{"species": "Blazeon", "spawn_rate": 0.5, "min_level": 15, "max_level": 20},
|
||||
{"species": "Hydrox", "spawn_rate": 0.3, "min_level": 16, "max_level": 22},
|
||||
{"species": "Rocky", "spawn_rate": 0.2, "min_level": 18, "max_level": 25}
|
||||
]
|
||||
}
|
||||
]
|
||||
56
config/moves.json
Normal file
56
config/moves.json
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
[
|
||||
{
|
||||
"name": "Tackle",
|
||||
"type": "Normal",
|
||||
"category": "Physical",
|
||||
"power": 40,
|
||||
"accuracy": 100,
|
||||
"pp": 35,
|
||||
"description": "A physical attack in which the user charges and slams into the target."
|
||||
},
|
||||
{
|
||||
"name": "Ember",
|
||||
"type": "Fire",
|
||||
"category": "Special",
|
||||
"power": 40,
|
||||
"accuracy": 100,
|
||||
"pp": 25,
|
||||
"description": "The target is attacked with small flames."
|
||||
},
|
||||
{
|
||||
"name": "Water Gun",
|
||||
"type": "Water",
|
||||
"category": "Special",
|
||||
"power": 40,
|
||||
"accuracy": 100,
|
||||
"pp": 25,
|
||||
"description": "The target is blasted with a forceful shot of water."
|
||||
},
|
||||
{
|
||||
"name": "Vine Whip",
|
||||
"type": "Grass",
|
||||
"category": "Physical",
|
||||
"power": 45,
|
||||
"accuracy": 100,
|
||||
"pp": 25,
|
||||
"description": "The target is struck with slender, whiplike vines."
|
||||
},
|
||||
{
|
||||
"name": "Thunder Shock",
|
||||
"type": "Electric",
|
||||
"category": "Special",
|
||||
"power": 40,
|
||||
"accuracy": 100,
|
||||
"pp": 30,
|
||||
"description": "A jolt of electricity crashes down on the target."
|
||||
},
|
||||
{
|
||||
"name": "Rock Throw",
|
||||
"type": "Rock",
|
||||
"category": "Physical",
|
||||
"power": 50,
|
||||
"accuracy": 90,
|
||||
"pp": 15,
|
||||
"description": "The user picks up and hurls a rock at the target."
|
||||
}
|
||||
]
|
||||
79
config/pets.json
Normal file
79
config/pets.json
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
[
|
||||
{
|
||||
"name": "Flamey",
|
||||
"type1": "Fire",
|
||||
"type2": null,
|
||||
"base_hp": 45,
|
||||
"base_attack": 52,
|
||||
"base_defense": 43,
|
||||
"base_speed": 65,
|
||||
"evolution_level": null,
|
||||
"rarity": 1
|
||||
},
|
||||
{
|
||||
"name": "Aqua",
|
||||
"type1": "Water",
|
||||
"type2": null,
|
||||
"base_hp": 44,
|
||||
"base_attack": 48,
|
||||
"base_defense": 65,
|
||||
"base_speed": 43,
|
||||
"evolution_level": null,
|
||||
"rarity": 1
|
||||
},
|
||||
{
|
||||
"name": "Leafy",
|
||||
"type1": "Grass",
|
||||
"type2": null,
|
||||
"base_hp": 45,
|
||||
"base_attack": 49,
|
||||
"base_defense": 49,
|
||||
"base_speed": 45,
|
||||
"evolution_level": null,
|
||||
"rarity": 1
|
||||
},
|
||||
{
|
||||
"name": "Sparky",
|
||||
"type1": "Electric",
|
||||
"type2": null,
|
||||
"base_hp": 35,
|
||||
"base_attack": 55,
|
||||
"base_defense": 40,
|
||||
"base_speed": 90,
|
||||
"evolution_level": null,
|
||||
"rarity": 2
|
||||
},
|
||||
{
|
||||
"name": "Rocky",
|
||||
"type1": "Rock",
|
||||
"type2": null,
|
||||
"base_hp": 40,
|
||||
"base_attack": 80,
|
||||
"base_defense": 100,
|
||||
"base_speed": 25,
|
||||
"evolution_level": null,
|
||||
"rarity": 2
|
||||
},
|
||||
{
|
||||
"name": "Blazeon",
|
||||
"type1": "Fire",
|
||||
"type2": null,
|
||||
"base_hp": 65,
|
||||
"base_attack": 80,
|
||||
"base_defense": 60,
|
||||
"base_speed": 95,
|
||||
"evolution_level": null,
|
||||
"rarity": 3
|
||||
},
|
||||
{
|
||||
"name": "Hydrox",
|
||||
"type1": "Water",
|
||||
"type2": "Ice",
|
||||
"base_hp": 70,
|
||||
"base_attack": 65,
|
||||
"base_defense": 90,
|
||||
"base_speed": 60,
|
||||
"evolution_level": null,
|
||||
"rarity": 3
|
||||
}
|
||||
]
|
||||
10
config/settings.example.json
Normal file
10
config/settings.example.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"server": "irc.libera.chat",
|
||||
"port": 6667,
|
||||
"nickname": "PetBot",
|
||||
"channel": "#petz",
|
||||
"command_prefix": "!",
|
||||
"admin_users": ["your_nickname_here"],
|
||||
"web_port": 8080,
|
||||
"database_path": "data/petbot.db"
|
||||
}
|
||||
46
config/types.json
Normal file
46
config/types.json
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"effectiveness": {
|
||||
"Fire": {
|
||||
"strong_against": ["Grass", "Ice"],
|
||||
"weak_against": ["Water", "Rock"],
|
||||
"immune_to": [],
|
||||
"resists": ["Fire", "Grass"]
|
||||
},
|
||||
"Water": {
|
||||
"strong_against": ["Fire", "Rock"],
|
||||
"weak_against": ["Electric", "Grass"],
|
||||
"immune_to": [],
|
||||
"resists": ["Water", "Fire", "Ice"]
|
||||
},
|
||||
"Grass": {
|
||||
"strong_against": ["Water", "Rock"],
|
||||
"weak_against": ["Fire", "Ice"],
|
||||
"immune_to": [],
|
||||
"resists": ["Water", "Electric", "Grass"]
|
||||
},
|
||||
"Electric": {
|
||||
"strong_against": ["Water"],
|
||||
"weak_against": ["Rock"],
|
||||
"immune_to": [],
|
||||
"resists": ["Electric"]
|
||||
},
|
||||
"Rock": {
|
||||
"strong_against": ["Fire", "Electric"],
|
||||
"weak_against": ["Water", "Grass"],
|
||||
"immune_to": [],
|
||||
"resists": ["Fire", "Normal"]
|
||||
},
|
||||
"Ice": {
|
||||
"strong_against": ["Grass"],
|
||||
"weak_against": ["Fire", "Rock"],
|
||||
"immune_to": [],
|
||||
"resists": ["Ice"]
|
||||
},
|
||||
"Normal": {
|
||||
"strong_against": [],
|
||||
"weak_against": ["Rock"],
|
||||
"immune_to": [],
|
||||
"resists": []
|
||||
}
|
||||
}
|
||||
}
|
||||
48
config/weather_patterns.json
Normal file
48
config/weather_patterns.json
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"weather_types": {
|
||||
"Sunny": {
|
||||
"description": "Bright sunshine increases Fire and Grass-type spawns",
|
||||
"spawn_modifier": 1.5,
|
||||
"affected_types": ["Fire", "Grass"],
|
||||
"duration_minutes": [60, 120]
|
||||
},
|
||||
"Rainy": {
|
||||
"description": "Heavy rain boosts Water-type spawns significantly",
|
||||
"spawn_modifier": 2.0,
|
||||
"affected_types": ["Water"],
|
||||
"duration_minutes": [45, 90]
|
||||
},
|
||||
"Thunderstorm": {
|
||||
"description": "Electric storms double Electric-type spawn rates",
|
||||
"spawn_modifier": 2.0,
|
||||
"affected_types": ["Electric"],
|
||||
"duration_minutes": [30, 60]
|
||||
},
|
||||
"Blizzard": {
|
||||
"description": "Harsh snowstorm increases Ice and Water-type spawns",
|
||||
"spawn_modifier": 1.7,
|
||||
"affected_types": ["Ice", "Water"],
|
||||
"duration_minutes": [60, 120]
|
||||
},
|
||||
"Earthquake": {
|
||||
"description": "Ground tremors bring Rock-type pets to the surface",
|
||||
"spawn_modifier": 1.8,
|
||||
"affected_types": ["Rock"],
|
||||
"duration_minutes": [30, 90]
|
||||
},
|
||||
"Calm": {
|
||||
"description": "Perfect weather with normal spawn rates",
|
||||
"spawn_modifier": 1.0,
|
||||
"affected_types": [],
|
||||
"duration_minutes": [90, 180]
|
||||
}
|
||||
},
|
||||
"location_weather_chances": {
|
||||
"Starter Town": ["Sunny", "Calm", "Rainy"],
|
||||
"Whispering Woods": ["Sunny", "Rainy", "Calm"],
|
||||
"Electric Canyon": ["Thunderstorm", "Sunny", "Calm"],
|
||||
"Crystal Caves": ["Earthquake", "Calm"],
|
||||
"Frozen Tundra": ["Blizzard", "Calm"],
|
||||
"Dragon's Peak": ["Thunderstorm", "Sunny", "Calm"]
|
||||
}
|
||||
}
|
||||
525
help.html
Normal file
525
help.html
Normal file
|
|
@ -0,0 +1,525 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PetBot IRC Commands Reference</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-primary: #0f0f23;
|
||||
--bg-secondary: #1e1e3f;
|
||||
--bg-tertiary: #2a2a4a;
|
||||
--text-primary: #cccccc;
|
||||
--text-secondary: #999999;
|
||||
--text-accent: #66ff66;
|
||||
--border-color: #333366;
|
||||
--hover-color: #3a3a5a;
|
||||
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--gradient-secondary: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
|
||||
--gradient-tertiary: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
|
||||
--shadow-glow: 0 0 20px rgba(102, 255, 102, 0.2);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 50%, rgba(120, 119, 198, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 80%, rgba(120, 198, 119, 0.1) 0%, transparent 50%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
background: var(--gradient-primary);
|
||||
color: white;
|
||||
padding: 40px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 40px;
|
||||
box-shadow: var(--shadow-dark);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Ccircle cx='30' cy='30' r='4'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.header > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 3em;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(45deg, #fff, #66ff66);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.header p {
|
||||
margin: 15px 0 0 0;
|
||||
opacity: 0.9;
|
||||
font-size: 1.3em;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--bg-secondary);
|
||||
margin-bottom: 30px;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-dark);
|
||||
border: 1px solid var(--border-color);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.section:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
background: var(--gradient-tertiary);
|
||||
color: white;
|
||||
padding: 20px 25px;
|
||||
font-size: 1.4em;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.command-grid {
|
||||
padding: 25px;
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.command {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.command:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(102, 255, 102, 0.15);
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.command-name {
|
||||
background: var(--bg-primary);
|
||||
padding: 15px 20px;
|
||||
font-family: 'Fira Code', 'Courier New', monospace;
|
||||
font-weight: bold;
|
||||
color: var(--text-accent);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-size: 1.2em;
|
||||
text-shadow: 0 0 10px rgba(102, 255, 102, 0.3);
|
||||
}
|
||||
|
||||
.command-desc {
|
||||
padding: 20px;
|
||||
line-height: 1.7;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.command-example {
|
||||
background: var(--bg-primary);
|
||||
padding: 12px 20px;
|
||||
font-family: 'Fira Code', 'Courier New', monospace;
|
||||
color: var(--text-secondary);
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.locations-list, .status-list {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin: 20px 0;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.locations-list h4, .status-list h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: var(--text-accent);
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.locations-list ul, .status-list ul {
|
||||
margin: 0;
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
.locations-list li, .status-list li {
|
||||
margin: 8px 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.locations-list strong, .status-list strong {
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 50px;
|
||||
padding: 30px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 15px;
|
||||
color: var(--text-secondary);
|
||||
box-shadow: var(--shadow-dark);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.tip {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--text-accent);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin: 20px 0;
|
||||
box-shadow: 0 0 15px rgba(102, 255, 102, 0.1);
|
||||
}
|
||||
|
||||
.tip strong {
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: var(--hover-color);
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-accent);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Fira Code', 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.new-badge {
|
||||
background: var(--gradient-secondary);
|
||||
color: white;
|
||||
font-size: 0.7em;
|
||||
padding: 3px 8px;
|
||||
border-radius: 12px;
|
||||
margin-left: 10px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.pm-indicator {
|
||||
background: rgba(102, 255, 102, 0.2);
|
||||
color: var(--text-accent);
|
||||
font-size: 0.8em;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
margin-left: 10px;
|
||||
border: 1px solid var(--text-accent);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🐾 PetBot Command Reference</h1>
|
||||
<p>Complete guide to IRC pet collection and battle commands</p>
|
||||
<p><em>Connect to irc.libera.chat #petz to play!</em></p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">🎮 Getting Started</div>
|
||||
<div class="command-grid">
|
||||
<div class="command">
|
||||
<div class="command-name">!start</div>
|
||||
<div class="command-desc">Begin your pet journey! Creates your trainer account and gives you a starter pet in Starter Town.</div>
|
||||
<div class="command-example">Example: !start</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">!help</div>
|
||||
<div class="command-desc">Display a quick list of available commands in the IRC channel.</div>
|
||||
<div class="command-example">Example: !help</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">🔍 Exploration & Travel</div>
|
||||
<div class="command-grid">
|
||||
<div class="command">
|
||||
<div class="command-name">!explore</div>
|
||||
<div class="command-desc">Explore your current location to find wild pets. Weather affects what types of pets you'll encounter!</div>
|
||||
<div class="command-example">Example: !explore</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">!location (or !where)</div>
|
||||
<div class="command-desc">See where you currently are, including the location description.</div>
|
||||
<div class="command-example">Example: !location</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">!travel <location></div>
|
||||
<div class="command-desc">Travel to a different location. Some locations require achievements to unlock!</div>
|
||||
<div class="command-example">Example: !travel Whispering Woods</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">!wild <location></div>
|
||||
<div class="command-desc">Check what types of pets can be found in a specific location.</div>
|
||||
<div class="command-example">Example: !wild Electric Canyon</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="locations-list">
|
||||
<h4>Available Locations:</h4>
|
||||
<ul>
|
||||
<li><strong>Starter Town</strong> - Where all trainers begin (always accessible)</li>
|
||||
<li><strong>Whispering Woods</strong> - Unlocked by catching 3 different Grass-type pets</li>
|
||||
<li><strong>Electric Canyon</strong> - Unlocked by catching 2 different Electric-type pets</li>
|
||||
<li><strong>Crystal Caves</strong> - Unlocked by catching 3 different Rock-type pets</li>
|
||||
<li><strong>Frozen Tundra</strong> - Unlocked by catching 5 different Water/Ice-type pets</li>
|
||||
<li><strong>Dragon's Peak</strong> - Unlocked by catching 15 pets total and having 3 Fire-type pets</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">⚔️ Battle System</div>
|
||||
<div class="command-grid">
|
||||
<div class="command">
|
||||
<div class="command-name">!battle</div>
|
||||
<div class="command-desc">Start a battle with a wild pet you encountered during exploration. Strategic combat with type advantages!</div>
|
||||
<div class="command-example">Example: !battle</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">!attack <move></div>
|
||||
<div class="command-desc">Use a specific move during battle. Each pet has different moves based on their type.</div>
|
||||
<div class="command-example">Example: !attack Ember</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">!flee</div>
|
||||
<div class="command-desc">Attempt to escape from battle. Success depends on your pet's speed vs the wild pet's speed.</div>
|
||||
<div class="command-example">Example: !flee</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">!moves</div>
|
||||
<div class="command-desc">View your active pet's available moves (up to 4 moves). Shows move type and power for battle planning.</div>
|
||||
<div class="command-example">Example: !moves</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">!catch (or !capture)</div>
|
||||
<div class="command-desc">Try to catch a pet during exploration OR during battle. During battle, weaker pets (lower HP) have significantly higher catch rates - up to 90% for nearly defeated pets! Both !catch and !capture work identically.</div>
|
||||
<div class="command-example">Example: !catch or !capture</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tip">
|
||||
<strong>Battle Strategy:</strong> Use type advantages! Water beats Fire, Fire beats Grass, Grass beats Water, Electric beats Water, Rock beats Fire and Electric. Weaken wild pets in battle to increase catch rate!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">🐾 Pet Management</div>
|
||||
<div class="command-grid">
|
||||
<div class="command">
|
||||
<div class="command-name">!team</div>
|
||||
<div class="command-desc">View all your pets with active pets marked by ⭐. Shows levels, HP, and storage status.</div>
|
||||
<div class="command-example">Example: !team</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">!stats</div>
|
||||
<div class="command-desc">View your player statistics including level, experience, and money.</div>
|
||||
<div class="command-example">Example: !stats</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">!activate <pet> <span class="pm-indicator">PM ONLY</span></div>
|
||||
<div class="command-desc">Activate a pet for battle by nickname or species name. Only inactive pets can be activated. This command only works in private messages to prevent channel spam.</div>
|
||||
<div class="command-example">Example: /msg PetBot !activate Sparky<br>Example: /msg PetBot !activate Pikachu</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">!deactivate <pet> <span class="pm-indicator">PM ONLY</span></div>
|
||||
<div class="command-desc">Deactivate an active pet, removing it from battle readiness. This command only works in private messages to prevent channel spam.</div>
|
||||
<div class="command-example">Example: /msg PetBot !deactivate Sparky<br>Example: /msg PetBot !deactivate Pikachu</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">!swap <pet1> <pet2> <span class="pm-indicator">PM ONLY</span></div>
|
||||
<div class="command-desc">Swap activation status between two pets. The first pet becomes inactive, the second becomes active. This command only works in private messages to prevent channel spam.</div>
|
||||
<div class="command-example">Example: /msg PetBot !swap Sparky Flame<br>Example: /msg PetBot !swap Pikachu Charmander</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tip">
|
||||
<strong>Pet Management Tips:</strong> You can only have a limited number of active pets at once. Use !team to see which pets are active (⭐). Pet management commands (!activate, !deactivate, !swap) must be sent as private messages to the bot to prevent channel spam. Use <code>/msg PetBot <command></code> format.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">🏆 Achievements & Progress</div>
|
||||
<div class="command-grid">
|
||||
<div class="command">
|
||||
<div class="command-name">!achievements</div>
|
||||
<div class="command-desc">View your earned achievements and progress. Achievements unlock new locations!</div>
|
||||
<div class="command-example">Example: !achievements</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="locations-list">
|
||||
<h4>Key Achievements:</h4>
|
||||
<ul>
|
||||
<li><strong>Pet Collector</strong> - Catch your first 5 pets</li>
|
||||
<li><strong>Advanced Trainer</strong> - Catch 10 pets total</li>
|
||||
<li><strong>Nature Explorer</strong> - Catch 3 different Grass-type pets (unlocks Whispering Woods)</li>
|
||||
<li><strong>Spark Collector</strong> - Catch 2 different Electric-type pets (unlocks Electric Canyon)</li>
|
||||
<li><strong>Rock Hound</strong> - Catch 3 different Rock-type pets (unlocks Crystal Caves)</li>
|
||||
<li><strong>Ice Breaker</strong> - Catch 5 different Water/Ice-type pets (unlocks Frozen Tundra)</li>
|
||||
<li><strong>Dragon Tamer</strong> - Catch 15 pets total + 3 Fire types (unlocks Dragon's Peak)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">🌤️ Weather System</div>
|
||||
<div class="command-grid">
|
||||
<div class="command">
|
||||
<div class="command-name">!weather</div>
|
||||
<div class="command-desc">Check the current weather in your location and its effects on pet spawns.</div>
|
||||
<div class="command-example">Example: !weather</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-list">
|
||||
<h4>Weather Effects:</h4>
|
||||
<ul>
|
||||
<li><strong>Sunny</strong> - 1.5x Fire and Grass-type spawns</li>
|
||||
<li><strong>Rainy</strong> - 2.0x Water-type spawns</li>
|
||||
<li><strong>Thunderstorm</strong> - 2.0x Electric-type spawns</li>
|
||||
<li><strong>Blizzard</strong> - 1.7x Ice and Water-type spawns</li>
|
||||
<li><strong>Earthquake</strong> - 1.8x Rock-type spawns</li>
|
||||
<li><strong>Calm</strong> - Normal spawn rates for all types</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">📚 Game Mechanics</div>
|
||||
<div class="command-grid">
|
||||
<div style="grid-column: 1 / -1;">
|
||||
<div class="tip">
|
||||
<strong>How to Play:</strong>
|
||||
<ol style="margin: 10px 0 0 20px; padding: 0;">
|
||||
<li>Use <code>!start</code> to create your trainer and get a starter pet</li>
|
||||
<li>Use <code>!explore</code> to find wild pets in your current location</li>
|
||||
<li>Choose to <code>!battle</code> the wild pet (recommended) or <code>!catch</code> directly</li>
|
||||
<li>In battle, use <code>!attack <move></code> to weaken the wild pet</li>
|
||||
<li>Use <code>!catch</code> (or <code>!capture</code>) during battle for much higher success rates on damaged pets</li>
|
||||
<li>Battle-catch rates: 30% base + up to 50% bonus for low HP (90% max for nearly defeated pets)</li>
|
||||
<li>Collect different types of pets to unlock achievements and new locations</li>
|
||||
<li>Use <code>!travel</code> to explore new areas as you unlock them</li>
|
||||
<li>Check <code>!weather</code> for optimal catching conditions</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header">🔧 Type Effectiveness Chart</div>
|
||||
<div class="command-grid">
|
||||
<div style="grid-column: 1 / -1;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Attacking Type</th>
|
||||
<th>Strong Against (2x)</th>
|
||||
<th>Weak Against (0.5x)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Fire</strong></td>
|
||||
<td>Grass, Ice</td>
|
||||
<td>Water, Rock</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Water</strong></td>
|
||||
<td>Fire, Rock</td>
|
||||
<td>Electric, Grass</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Grass</strong></td>
|
||||
<td>Water, Rock</td>
|
||||
<td>Fire, Ice</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Electric</strong></td>
|
||||
<td>Water</td>
|
||||
<td>Rock</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Rock</strong></td>
|
||||
<td>Fire, Electric</td>
|
||||
<td>Water, Grass</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Normal</strong></td>
|
||||
<td>None</td>
|
||||
<td>Rock</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p><strong>PetBot v1.1</strong> - A complete IRC pet collection and battle game</p>
|
||||
<p>Created for Libera Chat #petz | Last updated: July 13, 2025</p>
|
||||
<p><em>Join the adventure and become the ultimate pet trainer!</em></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
18
modules/__init__.py
Normal file
18
modules/__init__.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env python3
|
||||
"""PetBot modules package"""
|
||||
|
||||
from .core_commands import CoreCommands
|
||||
from .exploration import Exploration
|
||||
from .battle_system import BattleSystem
|
||||
from .pet_management import PetManagement
|
||||
from .achievements import Achievements
|
||||
from .admin import Admin
|
||||
|
||||
__all__ = [
|
||||
'CoreCommands',
|
||||
'Exploration',
|
||||
'BattleSystem',
|
||||
'PetManagement',
|
||||
'Achievements',
|
||||
'Admin'
|
||||
]
|
||||
32
modules/achievements.py
Normal file
32
modules/achievements.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Achievements commands module for PetBot"""
|
||||
|
||||
from .base_module import BaseModule
|
||||
|
||||
class Achievements(BaseModule):
|
||||
"""Handles achievements display and tracking"""
|
||||
|
||||
def get_commands(self):
|
||||
return ["achievements"]
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
if command == "achievements":
|
||||
await self.cmd_achievements(channel, nickname)
|
||||
|
||||
async def cmd_achievements(self, channel, nickname):
|
||||
"""Show player achievements"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
achievements = await self.database.get_player_achievements(player["id"])
|
||||
|
||||
if achievements:
|
||||
self.send_message(channel, f"🏆 {nickname}'s Achievements:")
|
||||
for achievement in achievements[:5]: # Show last 5 achievements
|
||||
self.send_message(channel, f"• {achievement['name']}: {achievement['description']}")
|
||||
|
||||
if len(achievements) > 5:
|
||||
self.send_message(channel, f"... and {len(achievements) - 5} more!")
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: No achievements yet! Keep exploring and catching pets to unlock new areas!")
|
||||
30
modules/admin.py
Normal file
30
modules/admin.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Admin commands module for PetBot"""
|
||||
|
||||
from .base_module import BaseModule
|
||||
|
||||
class Admin(BaseModule):
|
||||
"""Handles admin-only commands like reload"""
|
||||
|
||||
def get_commands(self):
|
||||
return ["reload"]
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
if command == "reload":
|
||||
await self.cmd_reload(channel, nickname)
|
||||
|
||||
async def cmd_reload(self, channel, nickname):
|
||||
"""Reload bot modules (megasconed only)"""
|
||||
if nickname.lower() != "megasconed":
|
||||
self.send_message(channel, f"{nickname}: Access denied. Admin command.")
|
||||
return
|
||||
|
||||
try:
|
||||
# Trigger module reload in main bot
|
||||
success = await self.bot.reload_modules()
|
||||
if success:
|
||||
self.send_message(channel, f"{nickname}: ✅ Modules reloaded successfully!")
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: ❌ Module reload failed!")
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"{nickname}: ❌ Reload error: {str(e)}")
|
||||
43
modules/base_module.py
Normal file
43
modules/base_module.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Base module class for PetBot command modules"""
|
||||
|
||||
import asyncio
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class BaseModule(ABC):
|
||||
"""Base class for all PetBot modules"""
|
||||
|
||||
def __init__(self, bot, database, game_engine):
|
||||
self.bot = bot
|
||||
self.database = database
|
||||
self.game_engine = game_engine
|
||||
|
||||
@abstractmethod
|
||||
def get_commands(self):
|
||||
"""Return list of commands this module handles"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
"""Handle a command for this module"""
|
||||
pass
|
||||
|
||||
def send_message(self, target, message):
|
||||
"""Send message through the bot"""
|
||||
self.bot.send_message(target, message)
|
||||
|
||||
def send_pm(self, nickname, message):
|
||||
"""Send private message to user"""
|
||||
self.bot.send_message(nickname, message)
|
||||
|
||||
async def get_player(self, nickname):
|
||||
"""Get player from database"""
|
||||
return await self.database.get_player(nickname)
|
||||
|
||||
async def require_player(self, channel, nickname):
|
||||
"""Get player or send start message if not found"""
|
||||
player = await self.get_player(nickname)
|
||||
if not player:
|
||||
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
|
||||
return None
|
||||
return player
|
||||
192
modules/battle_system.py
Normal file
192
modules/battle_system.py
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Battle system commands module for PetBot"""
|
||||
|
||||
from .base_module import BaseModule
|
||||
|
||||
class BattleSystem(BaseModule):
|
||||
"""Handles battle, attack, flee, and moves commands"""
|
||||
|
||||
# IRC color codes for different move types
|
||||
MOVE_TYPE_COLORS = {
|
||||
'Fire': '\x0304', # Red
|
||||
'Water': '\x0312', # Light Blue
|
||||
'Grass': '\x0303', # Green
|
||||
'Electric': '\x0308', # Yellow
|
||||
'Rock': '\x0307', # Orange/Brown
|
||||
'Normal': '\x0314', # Gray
|
||||
'Physical': '\x0313', # Pink/Magenta (fallback for category)
|
||||
'Special': '\x0310' # Cyan (fallback for category)
|
||||
}
|
||||
|
||||
def get_commands(self):
|
||||
return ["battle", "attack", "flee", "moves"]
|
||||
|
||||
def get_move_color(self, move_type):
|
||||
"""Get IRC color code for a move type"""
|
||||
return self.MOVE_TYPE_COLORS.get(move_type, '\x0312') # Default to light blue
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
if command == "battle":
|
||||
await self.cmd_battle(channel, nickname)
|
||||
elif command == "attack":
|
||||
await self.cmd_attack(channel, nickname, args)
|
||||
elif command == "flee":
|
||||
await self.cmd_flee(channel, nickname)
|
||||
elif command == "moves":
|
||||
await self.cmd_moves(channel, nickname)
|
||||
|
||||
async def cmd_battle(self, channel, nickname):
|
||||
"""Start a battle with encountered wild pet"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
# Check if player has an active encounter
|
||||
if player["id"] not in self.bot.active_encounters:
|
||||
self.send_message(channel, f"{nickname}: You need to !explore first to find a pet to battle!")
|
||||
return
|
||||
|
||||
# Check if already 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 already in battle! Use !attack <move> or !flee.")
|
||||
return
|
||||
|
||||
# Get player's active pet
|
||||
pets = await self.database.get_player_pets(player["id"], active_only=True)
|
||||
if not pets:
|
||||
self.send_message(channel, f"{nickname}: You need an active pet to battle! Use !team to check your pets.")
|
||||
return
|
||||
|
||||
player_pet = pets[0] # Use first active pet
|
||||
wild_pet = self.bot.active_encounters[player["id"]]
|
||||
|
||||
# Start battle
|
||||
battle = await self.game_engine.battle_engine.start_battle(player["id"], player_pet, wild_pet)
|
||||
|
||||
# Condensed battle start message with all info
|
||||
moves_colored = " | ".join([
|
||||
f"{self.get_move_color(move['type'])}{move['name']}\x0F"
|
||||
for move in battle["available_moves"]
|
||||
])
|
||||
|
||||
battle_start_msg = (f"⚔️ {nickname}: Battle! Your {player_pet['species_name']} (Lv.{player_pet['level']}, "
|
||||
f"{battle['player_hp']}/{player_pet['max_hp']} HP) vs Wild {wild_pet['species_name']} "
|
||||
f"(Lv.{wild_pet['level']}, {battle['wild_hp']}/{wild_pet['max_hp']} HP)")
|
||||
|
||||
self.send_message(channel, battle_start_msg)
|
||||
self.send_message(channel, f"🎯 Moves: {moves_colored} | Use !attack <move>")
|
||||
|
||||
async def cmd_attack(self, channel, nickname, args):
|
||||
"""Use a move in battle"""
|
||||
if not args:
|
||||
self.send_message(channel, f"{nickname}: Specify a move to use! Example: !attack Tackle")
|
||||
return
|
||||
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
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:
|
||||
self.send_message(channel, f"{nickname}: {result['error']}")
|
||||
return
|
||||
|
||||
# Display battle results - condensed with player names for clarity
|
||||
for action in result["results"]:
|
||||
# Determine attacker and target with player context
|
||||
if "Wild" in action['attacker']:
|
||||
attacker = f"Wild {action['attacker']}"
|
||||
target_context = f"{nickname}"
|
||||
else:
|
||||
attacker = f"{nickname}'s {action['attacker']}"
|
||||
target_context = "wild pet"
|
||||
|
||||
# Build condensed message with all info on one line
|
||||
if action["damage"] > 0:
|
||||
effectiveness_msgs = {
|
||||
"super_effective": "⚡ Super effective!",
|
||||
"not_very_effective": "💫 Not very effective...",
|
||||
"no_effect": "❌ No effect!",
|
||||
"super_effective_critical": "💥 CRIT! Super effective!",
|
||||
"normal_critical": "💥 Critical hit!",
|
||||
"not_very_effective_critical": "💥 Crit, not very effective..."
|
||||
}
|
||||
|
||||
effectiveness = effectiveness_msgs.get(action["effectiveness"], "")
|
||||
battle_msg = f"⚔️ {attacker} used {action['move']} → {action['damage']} damage to {target_context}! {effectiveness} (HP: {action['target_hp']})"
|
||||
else:
|
||||
# Status move or no damage
|
||||
battle_msg = f"⚔️ {attacker} used {action['move']} on {target_context}! (HP: {action['target_hp']})"
|
||||
|
||||
self.send_message(channel, battle_msg)
|
||||
|
||||
if result["battle_over"]:
|
||||
if result["winner"] == "player":
|
||||
self.send_message(channel, f"🎉 {nickname}: You won the battle!")
|
||||
# Remove encounter since battle is over
|
||||
if player["id"] in self.bot.active_encounters:
|
||||
del self.bot.active_encounters[player["id"]]
|
||||
else:
|
||||
self.send_message(channel, f"💀 {nickname}: Your pet fainted! You lost the battle...")
|
||||
# Remove encounter
|
||||
if player["id"] in self.bot.active_encounters:
|
||||
del self.bot.active_encounters[player["id"]]
|
||||
else:
|
||||
# Battle continues - show available moves with type-based colors
|
||||
moves_colored = " | ".join([
|
||||
f"{self.get_move_color(move['type'])}{move['name']}\x0F"
|
||||
for move in result["available_moves"]
|
||||
])
|
||||
self.send_message(channel, f"🎯 {nickname}'s turn! Moves: {moves_colored}")
|
||||
|
||||
async def cmd_flee(self, channel, nickname):
|
||||
"""Attempt to flee from battle"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
success = await self.game_engine.battle_engine.flee_battle(player["id"])
|
||||
|
||||
if success:
|
||||
self.send_message(channel, f"💨 {nickname} successfully fled from battle!")
|
||||
# Remove encounter
|
||||
if player["id"] in self.bot.active_encounters:
|
||||
del self.bot.active_encounters[player["id"]]
|
||||
else:
|
||||
self.send_message(channel, f"❌ {nickname} couldn't escape! Battle continues!")
|
||||
|
||||
async def cmd_moves(self, channel, nickname):
|
||||
"""Show active pet's available moves"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
# Get player's active pets
|
||||
pets = await self.database.get_player_pets(player["id"], active_only=True)
|
||||
if not pets:
|
||||
self.send_message(channel, f"{nickname}: You don't have any active pets! Use !team to check your pets.")
|
||||
return
|
||||
|
||||
active_pet = pets[0] # Use first active pet
|
||||
available_moves = self.game_engine.battle_engine.get_available_moves(active_pet)
|
||||
|
||||
if not available_moves:
|
||||
self.send_message(channel, f"{nickname}: Your {active_pet['species_name']} has no available moves!")
|
||||
return
|
||||
|
||||
# Limit to 4 moves max and format compactly
|
||||
moves_to_show = available_moves[:4]
|
||||
|
||||
move_info = []
|
||||
for move in moves_to_show:
|
||||
power = move.get('power', 'Status')
|
||||
power_str = str(power) if power != 'Status' else 'Stat'
|
||||
move_colored = f"{self.get_move_color(move['type'])}{move['name']}\x0F"
|
||||
move_info.append(f"{move_colored} ({move['type']}, {power_str})")
|
||||
|
||||
pet_name = active_pet["nickname"] or active_pet["species_name"]
|
||||
moves_line = " | ".join(move_info)
|
||||
self.send_message(channel, f"🎯 {nickname}'s {pet_name}: {moves_line}")
|
||||
44
modules/core_commands.py
Normal file
44
modules/core_commands.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Core commands module for PetBot"""
|
||||
|
||||
from .base_module import BaseModule
|
||||
|
||||
class CoreCommands(BaseModule):
|
||||
"""Handles basic bot commands like start, help, stats"""
|
||||
|
||||
def get_commands(self):
|
||||
return ["help", "start", "stats"]
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
if command == "help":
|
||||
await self.cmd_help(channel, nickname)
|
||||
elif command == "start":
|
||||
await self.cmd_start(channel, nickname)
|
||||
elif command == "stats":
|
||||
await self.cmd_stats(channel, nickname, args)
|
||||
|
||||
async def cmd_help(self, channel, nickname):
|
||||
"""Send help URL to prevent rate limiting"""
|
||||
self.send_message(channel, f"{nickname}: Complete command reference available at: http://localhost:8080/help")
|
||||
|
||||
async def cmd_start(self, channel, nickname):
|
||||
"""Start a new player"""
|
||||
player = await self.get_player(nickname)
|
||||
if player:
|
||||
self.send_message(channel, 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)
|
||||
|
||||
self.send_message(channel,
|
||||
f"🎉 {nickname}: Welcome to the world of pets! You received a Level {starter_pet['level']} {starter_pet['species_name']}! You are now in Starter Town.")
|
||||
|
||||
async def cmd_stats(self, channel, nickname, args):
|
||||
"""Show player statistics"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
self.send_message(channel,
|
||||
f"📊 {nickname}: Level {player['level']} | {player['experience']} XP | ${player['money']}")
|
||||
225
modules/exploration.py
Normal file
225
modules/exploration.py
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Exploration commands module for PetBot"""
|
||||
|
||||
from .base_module import BaseModule
|
||||
|
||||
class Exploration(BaseModule):
|
||||
"""Handles exploration, travel, location, weather, and wild commands"""
|
||||
|
||||
def get_commands(self):
|
||||
return ["explore", "travel", "location", "where", "weather", "wild", "catch", "capture"]
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
if command == "explore":
|
||||
await self.cmd_explore(channel, nickname)
|
||||
elif command == "travel":
|
||||
await self.cmd_travel(channel, nickname, args)
|
||||
elif command in ["location", "where"]:
|
||||
await self.cmd_location(channel, nickname)
|
||||
elif command == "weather":
|
||||
await self.cmd_weather(channel, nickname)
|
||||
elif command == "wild":
|
||||
await self.cmd_wild(channel, nickname, args)
|
||||
elif command in ["catch", "capture"]:
|
||||
await self.cmd_catch(channel, nickname)
|
||||
|
||||
async def cmd_explore(self, channel, nickname):
|
||||
"""Explore current location"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
encounter = await self.game_engine.explore_location(player["id"])
|
||||
|
||||
if encounter["type"] == "error":
|
||||
self.send_message(channel, f"{nickname}: {encounter['message']}")
|
||||
elif encounter["type"] == "empty":
|
||||
self.send_message(channel, f"🔍 {nickname}: {encounter['message']}")
|
||||
elif encounter["type"] == "encounter":
|
||||
# Store the encounter for potential catching
|
||||
self.bot.active_encounters[player["id"]] = encounter["pet"]
|
||||
|
||||
pet = encounter["pet"]
|
||||
type_str = pet["type1"]
|
||||
if pet["type2"]:
|
||||
type_str += f"/{pet['type2']}"
|
||||
|
||||
self.send_message(channel,
|
||||
f"🐾 {nickname}: A wild Level {pet['level']} {pet['species_name']} ({type_str}) appeared in {encounter['location']}!")
|
||||
self.send_message(channel, f"Choose your action: !battle to fight it, or !catch to try catching it directly!")
|
||||
|
||||
async def cmd_travel(self, channel, nickname, args):
|
||||
"""Travel to a different location"""
|
||||
if not args:
|
||||
self.send_message(channel, f"{nickname}: Specify where to travel! Available: Starter Town, Whispering Woods, Electric Canyon, Crystal Caves, Frozen Tundra, Dragon's Peak")
|
||||
return
|
||||
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
destination = " ".join(args).title() # Normalize to Title Case
|
||||
location = await self.database.get_location_by_name(destination)
|
||||
|
||||
if not location:
|
||||
self.send_message(channel, f"{nickname}: '{destination}' is not a valid location!")
|
||||
return
|
||||
|
||||
# Check if player can access this location
|
||||
missing_requirements = await self.database.get_missing_location_requirements(player["id"], location["id"])
|
||||
if missing_requirements:
|
||||
# Build specific message about required achievements
|
||||
if len(missing_requirements) == 1:
|
||||
achievement = missing_requirements[0]
|
||||
self.send_message(channel, f"{nickname}: You cannot access {destination} yet! Required achievement: '{achievement['name']}' - {achievement['description']}")
|
||||
else:
|
||||
achievement_names = [f"'{req['name']}'" for req in missing_requirements]
|
||||
self.send_message(channel, f"{nickname}: You cannot access {destination} yet! Required achievements: {', '.join(achievement_names)}. Use !achievements to see progress.")
|
||||
return
|
||||
|
||||
# Clear any active encounters when traveling
|
||||
if player["id"] in self.bot.active_encounters:
|
||||
del self.bot.active_encounters[player["id"]]
|
||||
|
||||
await self.database.update_player_location(player["id"], location["id"])
|
||||
|
||||
# Show weather info
|
||||
weather = await self.database.get_location_weather(location["id"])
|
||||
weather_msg = ""
|
||||
if weather:
|
||||
weather_patterns = getattr(self.game_engine, 'weather_patterns', {})
|
||||
weather_info = weather_patterns.get("weather_types", {}).get(weather["weather_type"], {})
|
||||
weather_desc = weather_info.get("description", f"{weather['weather_type']} weather")
|
||||
weather_msg = f" Weather: {weather['weather_type']} - {weather_desc}"
|
||||
|
||||
self.send_message(channel, f"🗺️ {nickname}: You traveled to {destination}. {location['description']}{weather_msg}")
|
||||
|
||||
async def cmd_location(self, channel, nickname):
|
||||
"""Show current location"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
location = await self.database.get_player_location(player["id"])
|
||||
if location:
|
||||
self.send_message(channel, f"📍 {nickname}: You are currently in {location['name']}. {location['description']}")
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: You seem to be lost! Contact an admin.")
|
||||
|
||||
async def cmd_weather(self, channel, nickname):
|
||||
"""Show current weather"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
location = await self.database.get_player_location(player["id"])
|
||||
if not location:
|
||||
self.send_message(channel, f"{nickname}: You seem to be lost!")
|
||||
return
|
||||
|
||||
weather = await self.database.get_location_weather(location["id"])
|
||||
if weather:
|
||||
weather_patterns = getattr(self.game_engine, 'weather_patterns', {})
|
||||
weather_info = weather_patterns.get("weather_types", {}).get(weather["weather_type"], {})
|
||||
weather_desc = weather_info.get("description", f"{weather['weather_type']} weather")
|
||||
|
||||
self.send_message(channel, f"🌤️ {nickname}: Current weather in {location['name']}: {weather['weather_type']}")
|
||||
self.send_message(channel, f"Effect: {weather_desc}")
|
||||
else:
|
||||
self.send_message(channel, f"🌤️ {nickname}: The weather in {location['name']} is calm with no special effects.")
|
||||
|
||||
async def cmd_wild(self, channel, nickname, args):
|
||||
"""Show wild pets in location (defaults to current)"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
if args:
|
||||
# Specific location requested
|
||||
location_name = " ".join(args).title()
|
||||
else:
|
||||
# Default to current location
|
||||
current_location = await self.database.get_player_location(player["id"])
|
||||
if not current_location:
|
||||
self.send_message(channel, f"{nickname}: You seem to be lost!")
|
||||
return
|
||||
location_name = current_location["name"]
|
||||
|
||||
wild_pets = await self.game_engine.get_location_spawns(location_name)
|
||||
|
||||
if wild_pets:
|
||||
pet_list = ", ".join([pet["name"] for pet in wild_pets])
|
||||
self.send_message(channel, f"🌿 Wild pets in {location_name}: {pet_list}")
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: No location found called '{location_name}'")
|
||||
|
||||
async def cmd_catch(self, channel, nickname):
|
||||
"""Catch a pet during exploration or battle"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
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:
|
||||
# Catching during battle
|
||||
wild_pet = active_battle["wild_pet"]
|
||||
current_hp = active_battle["wild_hp"]
|
||||
max_hp = wild_pet["max_hp"]
|
||||
|
||||
# Calculate catch rate based on remaining HP (lower HP = higher catch rate)
|
||||
hp_percentage = current_hp / max_hp
|
||||
base_catch_rate = 0.3 # Lower base rate than direct catch
|
||||
hp_bonus = (1.0 - hp_percentage) * 0.5 # Up to 50% bonus for low HP
|
||||
final_catch_rate = min(0.9, base_catch_rate + hp_bonus) # Cap at 90%
|
||||
|
||||
import random
|
||||
if random.random() < final_catch_rate:
|
||||
# Successful catch during battle
|
||||
result = await self.game_engine.attempt_catch_current_location(player["id"], wild_pet)
|
||||
|
||||
# End the battle
|
||||
await_result = await self.game_engine.battle_engine.end_battle(player["id"], "caught")
|
||||
|
||||
# Check for achievements
|
||||
type_achievements = await self.game_engine.check_and_award_achievements(player["id"], "catch_type", "")
|
||||
total_achievements = await self.game_engine.check_and_award_achievements(player["id"], "catch_total", "")
|
||||
|
||||
all_achievements = type_achievements + total_achievements
|
||||
if all_achievements:
|
||||
for achievement in all_achievements:
|
||||
self.send_message(channel, f"🏆 {nickname}: Achievement unlocked: {achievement['name']}! {achievement['description']}")
|
||||
|
||||
# Remove encounter
|
||||
if player["id"] in self.bot.active_encounters:
|
||||
del self.bot.active_encounters[player["id"]]
|
||||
|
||||
self.send_message(channel, f"🎯 {nickname}: Caught the {wild_pet['species_name']} during battle! (HP: {current_hp}/{max_hp})")
|
||||
else:
|
||||
# Failed catch - battle continues
|
||||
catch_percentage = int(final_catch_rate * 100)
|
||||
self.send_message(channel, f"🎯 {nickname}: The {wild_pet['species_name']} broke free! ({catch_percentage}% catch rate) Battle continues!")
|
||||
return
|
||||
|
||||
# Regular exploration catch (not in battle)
|
||||
if player["id"] not in self.bot.active_encounters:
|
||||
self.send_message(channel, f"{nickname}: You need to !explore first to find a pet, or be in battle to catch!")
|
||||
return
|
||||
|
||||
target_pet = self.bot.active_encounters[player["id"]]
|
||||
result = await self.game_engine.attempt_catch_current_location(player["id"], target_pet)
|
||||
|
||||
# Check for achievements after successful catch
|
||||
if "Success!" in result:
|
||||
type_achievements = await self.game_engine.check_and_award_achievements(player["id"], "catch_type", "")
|
||||
total_achievements = await self.game_engine.check_and_award_achievements(player["id"], "catch_total", "")
|
||||
|
||||
all_achievements = type_achievements + total_achievements
|
||||
if all_achievements:
|
||||
for achievement in all_achievements:
|
||||
self.send_message(channel, f"🏆 {nickname}: Achievement unlocked: {achievement['name']}! {achievement['description']}")
|
||||
|
||||
# Remove the encounter regardless of success
|
||||
del self.bot.active_encounters[player["id"]]
|
||||
|
||||
self.send_message(channel, f"🎯 {nickname}: {result}")
|
||||
149
modules/pet_management.py
Normal file
149
modules/pet_management.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Pet management commands module for PetBot"""
|
||||
|
||||
from .base_module import BaseModule
|
||||
|
||||
class PetManagement(BaseModule):
|
||||
"""Handles team, pets, and future pet management commands"""
|
||||
|
||||
def get_commands(self):
|
||||
return ["team", "pets", "activate", "deactivate", "swap"]
|
||||
|
||||
async def handle_command(self, channel, nickname, command, args):
|
||||
if command == "team":
|
||||
await self.cmd_team(channel, nickname)
|
||||
elif command == "pets":
|
||||
await self.cmd_pets(channel, nickname)
|
||||
elif command == "activate":
|
||||
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)
|
||||
|
||||
async def cmd_team(self, channel, nickname):
|
||||
"""Show active pets (channel display)"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
pets = await self.database.get_player_pets(player["id"], active_only=False)
|
||||
if not pets:
|
||||
self.send_message(channel, f"{nickname}: You don't have any pets! Use !catch to find some.")
|
||||
return
|
||||
|
||||
# Show active pets first, then others
|
||||
active_pets = [pet for pet in pets if pet.get("is_active")]
|
||||
inactive_pets = [pet for pet in pets if not pet.get("is_active")]
|
||||
|
||||
team_info = []
|
||||
|
||||
# Active pets with star
|
||||
for pet in active_pets:
|
||||
name = pet["nickname"] or pet["species_name"]
|
||||
team_info.append(f"⭐{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP")
|
||||
|
||||
# Inactive pets
|
||||
for pet in inactive_pets[:5]: # Show max 5 inactive
|
||||
name = pet["nickname"] or pet["species_name"]
|
||||
team_info.append(f"{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP")
|
||||
|
||||
if len(inactive_pets) > 5:
|
||||
team_info.append(f"... and {len(inactive_pets) - 5} more in storage")
|
||||
|
||||
self.send_message(channel, f"🐾 {nickname}'s team: " + " | ".join(team_info))
|
||||
|
||||
async def cmd_pets(self, channel, nickname):
|
||||
"""Show link to pet collection web page"""
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
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://localhost:8080/player/{nickname}")
|
||||
|
||||
async def cmd_activate(self, channel, nickname, args):
|
||||
"""Activate a pet for battle (PM only)"""
|
||||
# Redirect to PM for privacy
|
||||
if not args:
|
||||
self.send_pm(nickname, "Usage: !activate <pet_name>")
|
||||
self.send_message(channel, f"{nickname}: Pet activation instructions sent via PM!")
|
||||
return
|
||||
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
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"]
|
||||
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']}")
|
||||
self.send_message(channel, f"{nickname}: Pet activation failed - check PM for details!")
|
||||
|
||||
async def cmd_deactivate(self, channel, nickname, args):
|
||||
"""Deactivate a pet to storage (PM only)"""
|
||||
# Redirect to PM for privacy
|
||||
if not args:
|
||||
self.send_pm(nickname, "Usage: !deactivate <pet_name>")
|
||||
self.send_message(channel, f"{nickname}: Pet deactivation instructions sent via PM!")
|
||||
return
|
||||
|
||||
player = await self.require_player(channel, nickname)
|
||||
if not player:
|
||||
return
|
||||
|
||||
pet_name = " ".join(args)
|
||||
result = await self.database.deactivate_pet(player["id"], pet_name)
|
||||
|
||||
if result["success"]:
|
||||
pet = result["pet"]
|
||||
display_name = pet["nickname"] or pet["species_name"]
|
||||
self.send_pm(nickname, f"📦 {display_name} moved to storage!")
|
||||
self.send_message(channel, f"{nickname}: Pet deactivated successfully!")
|
||||
else:
|
||||
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!")
|
||||
0
plugins/__init__.py
Normal file
0
plugins/__init__.py
Normal file
4
requirements.txt
Normal file
4
requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
irc>=20.3.0
|
||||
aiosqlite>=0.19.0
|
||||
python-dotenv>=1.0.0
|
||||
asyncio
|
||||
85
reset_players.py
Executable file
85
reset_players.py
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Reset Player Database Script
|
||||
Clears all player data for a fresh start while preserving game data (species, locations, etc.)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
|
||||
# Add the project directory to the path
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from src.database import Database
|
||||
|
||||
async def reset_player_data(force=False):
|
||||
"""Reset all player-related data in the database"""
|
||||
print("🔄 Initializing database connection...")
|
||||
database = Database()
|
||||
await database.init_database()
|
||||
|
||||
print("⚠️ WARNING: This will delete ALL player data!")
|
||||
print(" - All player accounts")
|
||||
print(" - All caught pets")
|
||||
print(" - All player achievements")
|
||||
print(" - All player progress")
|
||||
print(" - All battle history")
|
||||
print("")
|
||||
print("Game data (species, locations, moves, etc.) will be preserved.")
|
||||
print("")
|
||||
|
||||
if not force:
|
||||
confirm = input("Are you sure you want to reset all player data? (type 'yes' to confirm): ")
|
||||
if confirm.lower() != 'yes':
|
||||
print("❌ Reset cancelled.")
|
||||
return
|
||||
else:
|
||||
print("🚀 Force mode enabled - proceeding with reset...")
|
||||
|
||||
print("")
|
||||
print("🗑️ Resetting player data...")
|
||||
|
||||
try:
|
||||
# Get database connection using aiosqlite directly
|
||||
import aiosqlite
|
||||
async with aiosqlite.connect(database.db_path) as db:
|
||||
# Clear player-related tables in correct order (foreign key constraints)
|
||||
tables_to_clear = [
|
||||
'active_battles', # Battle data
|
||||
'player_achievements', # Player achievements
|
||||
'pets', # Player pets
|
||||
'players' # Player accounts
|
||||
]
|
||||
|
||||
for table in tables_to_clear:
|
||||
print(f" Clearing {table}...")
|
||||
await db.execute(f"DELETE FROM {table}")
|
||||
await db.commit()
|
||||
|
||||
# Reset auto-increment counters
|
||||
print(" Resetting ID counters...")
|
||||
for table in tables_to_clear:
|
||||
await db.execute(f"DELETE FROM sqlite_sequence WHERE name='{table}'")
|
||||
await db.commit()
|
||||
|
||||
print("✅ Player data reset complete!")
|
||||
print("")
|
||||
print("🎮 Players can now use !start to begin fresh journeys.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error resetting player data: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
async def main():
|
||||
"""Main function"""
|
||||
print("🐾 PetBot Player Data Reset Tool")
|
||||
print("=" * 40)
|
||||
|
||||
# Check for force flag
|
||||
force = len(sys.argv) > 1 and sys.argv[1] == '--force'
|
||||
await reset_player_data(force=force)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
254
run_bot.py
Normal file
254
run_bot.py
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
#!/usr/bin/env python3
|
||||
import socket
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
import importlib
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from src.database import Database
|
||||
from src.game_engine import GameEngine
|
||||
from modules import CoreCommands, Exploration, BattleSystem, PetManagement, Achievements, Admin
|
||||
|
||||
class PetBot:
|
||||
def __init__(self):
|
||||
self.database = Database()
|
||||
self.game_engine = GameEngine(self.database)
|
||||
self.config = {
|
||||
"server": "irc.libera.chat",
|
||||
"port": 6667,
|
||||
"nickname": "PetBot",
|
||||
"channel": "#petz",
|
||||
"command_prefix": "!"
|
||||
}
|
||||
self.socket = None
|
||||
self.connected = False
|
||||
self.running = True
|
||||
self.active_encounters = {} # player_id -> encounter_data
|
||||
self.modules = {}
|
||||
self.command_map = {}
|
||||
|
||||
def initialize_async_components(self):
|
||||
"""Initialize async components"""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
print("Initializing database...")
|
||||
loop.run_until_complete(self.database.init_database())
|
||||
print("Loading game data...")
|
||||
loop.run_until_complete(self.game_engine.load_game_data())
|
||||
print("Loading modules...")
|
||||
self.load_modules()
|
||||
print("✓ Bot initialized successfully!")
|
||||
|
||||
self.loop = loop
|
||||
|
||||
def load_modules(self):
|
||||
"""Load all command modules"""
|
||||
module_classes = [
|
||||
CoreCommands,
|
||||
Exploration,
|
||||
BattleSystem,
|
||||
PetManagement,
|
||||
Achievements,
|
||||
Admin
|
||||
]
|
||||
|
||||
self.modules = {}
|
||||
self.command_map = {}
|
||||
|
||||
for module_class in module_classes:
|
||||
module_name = module_class.__name__
|
||||
module_instance = module_class(self, self.database, self.game_engine)
|
||||
self.modules[module_name] = module_instance
|
||||
|
||||
# Map commands to modules
|
||||
for command in module_instance.get_commands():
|
||||
self.command_map[command] = module_instance
|
||||
|
||||
print(f"✓ Loaded {len(self.modules)} modules with {len(self.command_map)} commands")
|
||||
|
||||
async def reload_modules(self):
|
||||
"""Reload all modules (for admin use)"""
|
||||
try:
|
||||
# Reload module files
|
||||
import modules
|
||||
importlib.reload(modules.core_commands)
|
||||
importlib.reload(modules.exploration)
|
||||
importlib.reload(modules.battle_system)
|
||||
importlib.reload(modules.pet_management)
|
||||
importlib.reload(modules.achievements)
|
||||
importlib.reload(modules.admin)
|
||||
importlib.reload(modules)
|
||||
|
||||
# Reinitialize modules
|
||||
self.load_modules()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Module reload failed: {e}")
|
||||
return False
|
||||
|
||||
def test_connection(self):
|
||||
"""Test if we can connect to IRC"""
|
||||
print("Testing IRC connection...")
|
||||
try:
|
||||
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
test_sock.settimeout(5)
|
||||
test_sock.connect((self.config["server"], self.config["port"]))
|
||||
test_sock.close()
|
||||
print("✓ IRC connection test successful")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ IRC connection failed: {e}")
|
||||
return False
|
||||
|
||||
def connect(self):
|
||||
self.initialize_async_components()
|
||||
|
||||
if not self.test_connection():
|
||||
print("\n=== IRC Connection Failed ===")
|
||||
print("The bot's core functionality is working perfectly!")
|
||||
print("Run these commands to test locally:")
|
||||
print(" python3 test/test_commands.py - Test all functionality")
|
||||
print(" python3 test/test_db.py - Test database operations")
|
||||
print("\nTo connect to IRC:")
|
||||
print("1. Ensure network access to irc.libera.chat:6667")
|
||||
print("2. Or modify config in run_bot.py to use a different server")
|
||||
return
|
||||
|
||||
print(f"Connecting to {self.config['server']}:{self.config['port']}")
|
||||
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.settimeout(10)
|
||||
|
||||
try:
|
||||
self.socket.connect((self.config["server"], self.config["port"]))
|
||||
print("✓ Connected to IRC server!")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to connect: {e}")
|
||||
return
|
||||
|
||||
# Send IRC handshake
|
||||
self.send(f"NICK {self.config['nickname']}")
|
||||
self.send(f"USER {self.config['nickname']} 0 * :{self.config['nickname']}")
|
||||
print("✓ Sent IRC handshake")
|
||||
|
||||
# Start message processing
|
||||
self.socket.settimeout(1)
|
||||
self.main_loop()
|
||||
|
||||
def main_loop(self):
|
||||
print("✓ Bot is now running! Use Ctrl+C to stop.")
|
||||
print(f"✓ Bot will join {self.config['channel']} when connected")
|
||||
print(f"✓ Loaded modules: {', '.join(self.modules.keys())}")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
data = self.socket.recv(4096).decode('utf-8', errors='ignore')
|
||||
if not data:
|
||||
print("Connection closed by server")
|
||||
break
|
||||
|
||||
lines = data.strip().split('\\n')
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
self.handle_line(line.strip())
|
||||
|
||||
except socket.timeout:
|
||||
continue
|
||||
except KeyboardInterrupt:
|
||||
print("\\n✓ Shutting down bot...")
|
||||
self.running = False
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error in main loop: {e}")
|
||||
time.sleep(1)
|
||||
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
self.loop.close()
|
||||
|
||||
def send(self, message):
|
||||
if self.socket:
|
||||
full_message = f"{message}\\r\\n"
|
||||
print(f">> {message}")
|
||||
self.socket.send(full_message.encode('utf-8'))
|
||||
|
||||
def handle_line(self, line):
|
||||
print(f"<< {line}")
|
||||
|
||||
if line.startswith("PING"):
|
||||
pong_response = line.replace("PING", "PONG")
|
||||
self.send(pong_response)
|
||||
return
|
||||
|
||||
# Handle connection messages
|
||||
if "376" in line or "422" in line: # End of MOTD
|
||||
if not self.connected:
|
||||
self.send(f"JOIN {self.config['channel']}")
|
||||
self.connected = True
|
||||
print(f"✓ Joined {self.config['channel']} - Bot is ready!")
|
||||
return
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) < 4:
|
||||
return
|
||||
|
||||
if parts[1] == "PRIVMSG":
|
||||
channel = parts[2]
|
||||
message = " ".join(parts[3:])[1:]
|
||||
|
||||
hostmask = parts[0][1:]
|
||||
nickname = hostmask.split('!')[0]
|
||||
|
||||
if message.startswith(self.config["command_prefix"]):
|
||||
print(f"Processing command from {nickname}: {message}")
|
||||
self.handle_command(channel, nickname, message)
|
||||
|
||||
def handle_command(self, channel, nickname, message):
|
||||
command_parts = message[1:].split()
|
||||
if not command_parts:
|
||||
return
|
||||
|
||||
command = command_parts[0].lower()
|
||||
args = command_parts[1:]
|
||||
|
||||
try:
|
||||
if command in self.command_map:
|
||||
module = self.command_map[command]
|
||||
# Run async command handler
|
||||
self.loop.run_until_complete(
|
||||
module.handle_command(channel, nickname, command, args)
|
||||
)
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: Unknown command. Use !help for available commands.")
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"{nickname}: Error processing command: {str(e)}")
|
||||
print(f"Command error: {e}")
|
||||
|
||||
def send_message(self, target, message):
|
||||
self.send(f"PRIVMSG {target} :{message}")
|
||||
time.sleep(0.5)
|
||||
|
||||
def run_async_command(self, coro):
|
||||
return self.loop.run_until_complete(coro)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🐾 Starting Pet Bot for IRC (Modular Version)...")
|
||||
bot = PetBot()
|
||||
try:
|
||||
bot.connect()
|
||||
except KeyboardInterrupt:
|
||||
print("\\n🔄 Bot stopping...")
|
||||
# Gracefully shutdown the game engine
|
||||
try:
|
||||
bot.loop.run_until_complete(bot.game_engine.shutdown())
|
||||
except Exception as e:
|
||||
print(f"Error during shutdown: {e}")
|
||||
print("✓ Bot stopped by user")
|
||||
except Exception as e:
|
||||
print(f"✗ Bot crashed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
283
run_bot_debug.py
Normal file
283
run_bot_debug.py
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
#!/usr/bin/env python3
|
||||
import socket
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
import importlib
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from src.database import Database
|
||||
from src.game_engine import GameEngine
|
||||
from modules import CoreCommands, Exploration, BattleSystem, PetManagement, Achievements, Admin
|
||||
from webserver import PetBotWebServer
|
||||
|
||||
class PetBotDebug:
|
||||
def __init__(self):
|
||||
print("🤖 PetBot Debug Mode - Initializing...")
|
||||
self.database = Database()
|
||||
self.game_engine = GameEngine(self.database)
|
||||
self.config = {
|
||||
"server": "irc.libera.chat",
|
||||
"port": 6667,
|
||||
"nickname": "PetBot",
|
||||
"channel": "#petz",
|
||||
"command_prefix": "!"
|
||||
}
|
||||
self.socket = None
|
||||
self.connected = False
|
||||
self.running = True
|
||||
self.active_encounters = {}
|
||||
self.modules = {}
|
||||
self.command_map = {}
|
||||
self.web_server = None
|
||||
print("✅ Basic initialization complete")
|
||||
|
||||
def initialize_async_components(self):
|
||||
"""Initialize async components"""
|
||||
print("🔄 Creating event loop...")
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
print("✅ Event loop created")
|
||||
|
||||
print("🔄 Initializing database...")
|
||||
loop.run_until_complete(self.database.init_database())
|
||||
print("✅ Database initialized")
|
||||
|
||||
print("🔄 Loading game data...")
|
||||
loop.run_until_complete(self.game_engine.load_game_data())
|
||||
print("✅ Game data loaded")
|
||||
|
||||
print("🔄 Loading modules...")
|
||||
self.load_modules()
|
||||
print("✅ Modules loaded")
|
||||
|
||||
print("🔄 Starting web server...")
|
||||
self.web_server = PetBotWebServer(self.database, port=8080)
|
||||
self.web_server.start_in_thread()
|
||||
print("✅ Web server started")
|
||||
|
||||
self.loop = loop
|
||||
print("✅ Async components ready")
|
||||
|
||||
def load_modules(self):
|
||||
"""Load all command modules"""
|
||||
module_classes = [
|
||||
CoreCommands,
|
||||
Exploration,
|
||||
BattleSystem,
|
||||
PetManagement,
|
||||
Achievements,
|
||||
Admin
|
||||
]
|
||||
|
||||
self.modules = {}
|
||||
self.command_map = {}
|
||||
|
||||
for module_class in module_classes:
|
||||
module_name = module_class.__name__
|
||||
print(f" Loading {module_name}...")
|
||||
module_instance = module_class(self, self.database, self.game_engine)
|
||||
self.modules[module_name] = module_instance
|
||||
|
||||
# Map commands to modules
|
||||
commands = module_instance.get_commands()
|
||||
for command in commands:
|
||||
self.command_map[command] = module_instance
|
||||
print(f" ✅ {module_name}: {len(commands)} commands")
|
||||
|
||||
print(f"✅ Loaded {len(self.modules)} modules with {len(self.command_map)} commands")
|
||||
|
||||
async def reload_modules(self):
|
||||
"""Reload all modules (for admin use)"""
|
||||
try:
|
||||
# Reload module files
|
||||
import modules
|
||||
importlib.reload(modules.core_commands)
|
||||
importlib.reload(modules.exploration)
|
||||
importlib.reload(modules.battle_system)
|
||||
importlib.reload(modules.pet_management)
|
||||
importlib.reload(modules.achievements)
|
||||
importlib.reload(modules.admin)
|
||||
importlib.reload(modules)
|
||||
|
||||
# Reinitialize modules
|
||||
print("🔄 Reloading modules...")
|
||||
self.load_modules()
|
||||
print("✅ Modules reloaded successfully")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Module reload failed: {e}")
|
||||
return False
|
||||
|
||||
def test_connection(self):
|
||||
"""Test if we can connect to IRC"""
|
||||
print("🔄 Testing IRC connection...")
|
||||
try:
|
||||
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
test_sock.settimeout(5)
|
||||
test_sock.connect((self.config["server"], self.config["port"]))
|
||||
test_sock.close()
|
||||
print("✅ IRC connection test successful")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ IRC connection failed: {e}")
|
||||
return False
|
||||
|
||||
def connect(self):
|
||||
print("🚀 Starting bot connection process...")
|
||||
|
||||
print("🔄 Step 1: Initialize async components...")
|
||||
self.initialize_async_components()
|
||||
print("✅ Step 1 complete")
|
||||
|
||||
print("🔄 Step 2: Test IRC connectivity...")
|
||||
if not self.test_connection():
|
||||
print("❌ IRC connection test failed - would run offline mode")
|
||||
print("✅ Bot core systems are working - IRC connection unavailable")
|
||||
return
|
||||
print("✅ Step 2 complete")
|
||||
|
||||
print("🔄 Step 3: Create IRC socket...")
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.settimeout(10)
|
||||
print("✅ Socket created")
|
||||
|
||||
print("🔄 Step 4: Connect to IRC server...")
|
||||
try:
|
||||
self.socket.connect((self.config["server"], self.config["port"]))
|
||||
print("✅ Connected to IRC server!")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to connect: {e}")
|
||||
return
|
||||
|
||||
print("🔄 Step 5: Send IRC handshake...")
|
||||
self.send(f"NICK {self.config['nickname']}")
|
||||
self.send(f"USER {self.config['nickname']} 0 * :{self.config['nickname']}")
|
||||
print("✅ Handshake sent")
|
||||
|
||||
print("🔄 Step 6: Start main loop...")
|
||||
self.socket.settimeout(1)
|
||||
print("✅ Bot is now running!")
|
||||
print(f"✅ Will join {self.config['channel']} when connected")
|
||||
print(f"✅ Loaded commands: {', '.join(sorted(self.command_map.keys()))}")
|
||||
print("🎮 Ready for testing! Press Ctrl+C to stop.")
|
||||
|
||||
self.main_loop()
|
||||
|
||||
def main_loop(self):
|
||||
message_count = 0
|
||||
while self.running:
|
||||
try:
|
||||
data = self.socket.recv(4096).decode('utf-8', errors='ignore')
|
||||
if not data:
|
||||
print("💀 Connection closed by server")
|
||||
break
|
||||
|
||||
lines = data.strip().split('\n')
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
message_count += 1
|
||||
if message_count <= 10: # Show first 10 messages for debugging
|
||||
print(f"📨 {message_count}: {line.strip()}")
|
||||
self.handle_line(line.strip())
|
||||
|
||||
except socket.timeout:
|
||||
continue
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Shutting down bot...")
|
||||
self.running = False
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"❌ Error in main loop: {e}")
|
||||
time.sleep(1)
|
||||
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
self.loop.close()
|
||||
print("✅ Bot shutdown complete")
|
||||
|
||||
def send(self, message):
|
||||
if self.socket:
|
||||
full_message = f"{message}\r\n"
|
||||
print(f"📤 >> {message}")
|
||||
self.socket.send(full_message.encode('utf-8'))
|
||||
|
||||
def handle_line(self, line):
|
||||
if line.startswith("PING"):
|
||||
pong_response = line.replace("PING", "PONG")
|
||||
self.send(pong_response)
|
||||
return
|
||||
|
||||
# Handle connection messages
|
||||
if "376" in line or "422" in line: # End of MOTD
|
||||
if not self.connected:
|
||||
self.send(f"JOIN {self.config['channel']}")
|
||||
self.connected = True
|
||||
print(f"🎉 Joined {self.config['channel']} - Bot is ready for commands!")
|
||||
return
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) < 4:
|
||||
return
|
||||
|
||||
if parts[1] == "PRIVMSG":
|
||||
channel = parts[2]
|
||||
message = " ".join(parts[3:])[1:]
|
||||
|
||||
hostmask = parts[0][1:]
|
||||
nickname = hostmask.split('!')[0]
|
||||
|
||||
if message.startswith(self.config["command_prefix"]):
|
||||
print(f"🎮 Command from {nickname}: {message}")
|
||||
self.handle_command(channel, nickname, message)
|
||||
|
||||
def handle_command(self, channel, nickname, message):
|
||||
command_parts = message[1:].split()
|
||||
if not command_parts:
|
||||
return
|
||||
|
||||
command = command_parts[0].lower()
|
||||
args = command_parts[1:]
|
||||
|
||||
try:
|
||||
if command in self.command_map:
|
||||
module = self.command_map[command]
|
||||
print(f"🔧 Executing {command} via {module.__class__.__name__}")
|
||||
# Run async command handler
|
||||
self.loop.run_until_complete(
|
||||
module.handle_command(channel, nickname, command, args)
|
||||
)
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: Unknown command. Use !help for available commands.")
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"{nickname}: Error processing command: {str(e)}")
|
||||
print(f"❌ Command error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def send_message(self, target, message):
|
||||
self.send(f"PRIVMSG {target} :{message}")
|
||||
time.sleep(0.5)
|
||||
|
||||
def run_async_command(self, coro):
|
||||
return self.loop.run_until_complete(coro)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🐾 Starting Pet Bot for IRC (Debug Mode)...")
|
||||
bot = PetBotDebug()
|
||||
try:
|
||||
bot.connect()
|
||||
except KeyboardInterrupt:
|
||||
print("\n🔄 Bot stopping...")
|
||||
# Gracefully shutdown the game engine
|
||||
try:
|
||||
bot.loop.run_until_complete(bot.game_engine.shutdown())
|
||||
except Exception as e:
|
||||
print(f"Error during shutdown: {e}")
|
||||
print("✅ Bot stopped by user")
|
||||
except Exception as e:
|
||||
print(f"❌ Bot crashed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
633
run_bot_original.py
Normal file
633
run_bot_original.py
Normal file
|
|
@ -0,0 +1,633 @@
|
|||
#!/usr/bin/env python3
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from src.database import Database
|
||||
from src.game_engine import GameEngine
|
||||
|
||||
class PetBot:
|
||||
def __init__(self):
|
||||
self.database = Database()
|
||||
self.game_engine = GameEngine(self.database)
|
||||
self.config = {
|
||||
"server": "irc.libera.chat",
|
||||
"port": 6667,
|
||||
"nickname": "PetBot",
|
||||
"channel": "#petz",
|
||||
"command_prefix": "!"
|
||||
}
|
||||
self.socket = None
|
||||
self.connected = False
|
||||
self.running = True
|
||||
self.active_encounters = {} # player_id -> encounter_data
|
||||
|
||||
def initialize_async_components(self):
|
||||
"""Initialize async components"""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
print("Initializing database...")
|
||||
loop.run_until_complete(self.database.init_database())
|
||||
print("Loading game data...")
|
||||
loop.run_until_complete(self.game_engine.load_game_data())
|
||||
print("✓ Bot initialized successfully!")
|
||||
|
||||
self.loop = loop
|
||||
|
||||
def test_connection(self):
|
||||
"""Test if we can connect to IRC"""
|
||||
print("Testing IRC connection...")
|
||||
try:
|
||||
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
test_sock.settimeout(5)
|
||||
test_sock.connect((self.config["server"], self.config["port"]))
|
||||
test_sock.close()
|
||||
print("✓ IRC connection test successful")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ IRC connection failed: {e}")
|
||||
return False
|
||||
|
||||
def connect(self):
|
||||
self.initialize_async_components()
|
||||
|
||||
if not self.test_connection():
|
||||
print("\n=== IRC Connection Failed ===")
|
||||
print("The bot's core functionality is working perfectly!")
|
||||
print("Run these commands to test locally:")
|
||||
print(" python3 test_commands.py - Test all functionality")
|
||||
print(" python3 test_db.py - Test database operations")
|
||||
print("\nTo connect to IRC:")
|
||||
print("1. Ensure network access to irc.libera.chat:6667")
|
||||
print("2. Or modify config in run_bot.py to use a different server")
|
||||
return
|
||||
|
||||
print(f"Connecting to {self.config['server']}:{self.config['port']}")
|
||||
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.settimeout(10)
|
||||
|
||||
try:
|
||||
self.socket.connect((self.config["server"], self.config["port"]))
|
||||
print("✓ Connected to IRC server!")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to connect: {e}")
|
||||
return
|
||||
|
||||
# Send IRC handshake
|
||||
self.send(f"NICK {self.config['nickname']}")
|
||||
self.send(f"USER {self.config['nickname']} 0 * :{self.config['nickname']}")
|
||||
print("✓ Sent IRC handshake")
|
||||
|
||||
# Start message processing
|
||||
self.socket.settimeout(1)
|
||||
self.main_loop()
|
||||
|
||||
def main_loop(self):
|
||||
print("✓ Bot is now running! Use Ctrl+C to stop.")
|
||||
print(f"✓ Bot will join {self.config['channel']} when connected")
|
||||
print("✓ Available commands: !help !start !catch !team !wild !stats")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
data = self.socket.recv(4096).decode('utf-8', errors='ignore')
|
||||
if not data:
|
||||
print("Connection closed by server")
|
||||
break
|
||||
|
||||
lines = data.strip().split('\n')
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
self.handle_line(line.strip())
|
||||
|
||||
except socket.timeout:
|
||||
continue
|
||||
except KeyboardInterrupt:
|
||||
print("\n✓ Shutting down bot...")
|
||||
self.running = False
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error in main loop: {e}")
|
||||
time.sleep(1)
|
||||
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
self.loop.close()
|
||||
|
||||
def send(self, message):
|
||||
if self.socket:
|
||||
full_message = f"{message}\r\n"
|
||||
print(f">> {message}")
|
||||
self.socket.send(full_message.encode('utf-8'))
|
||||
|
||||
def handle_line(self, line):
|
||||
print(f"<< {line}")
|
||||
|
||||
if line.startswith("PING"):
|
||||
pong_response = line.replace("PING", "PONG")
|
||||
self.send(pong_response)
|
||||
return
|
||||
|
||||
# Handle connection messages
|
||||
if "376" in line or "422" in line: # End of MOTD
|
||||
if not self.connected:
|
||||
self.send(f"JOIN {self.config['channel']}")
|
||||
self.connected = True
|
||||
print(f"✓ Joined {self.config['channel']} - Bot is ready!")
|
||||
return
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) < 4:
|
||||
return
|
||||
|
||||
if parts[1] == "PRIVMSG":
|
||||
channel = parts[2]
|
||||
message = " ".join(parts[3:])[1:]
|
||||
|
||||
hostmask = parts[0][1:]
|
||||
nickname = hostmask.split('!')[0]
|
||||
|
||||
if message.startswith(self.config["command_prefix"]):
|
||||
print(f"Processing command from {nickname}: {message}")
|
||||
self.handle_command(channel, nickname, message)
|
||||
|
||||
def handle_command(self, channel, 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":
|
||||
self.cmd_help(channel, nickname)
|
||||
elif command == "start":
|
||||
self.cmd_start(channel, nickname)
|
||||
elif command == "explore":
|
||||
self.cmd_explore(channel, nickname)
|
||||
elif command == "catch" or command == "capture":
|
||||
self.cmd_catch(channel, nickname)
|
||||
elif command == "battle":
|
||||
self.cmd_battle(channel, nickname)
|
||||
elif command == "attack":
|
||||
self.cmd_attack(channel, nickname, args)
|
||||
elif command == "flee":
|
||||
self.cmd_flee(channel, nickname)
|
||||
elif command == "moves":
|
||||
self.cmd_moves(channel, nickname)
|
||||
elif command == "travel":
|
||||
self.cmd_travel(channel, nickname, args)
|
||||
elif command == "location" or command == "where":
|
||||
self.cmd_location(channel, nickname)
|
||||
elif command == "achievements":
|
||||
self.cmd_achievements(channel, nickname)
|
||||
elif command == "weather":
|
||||
self.cmd_weather(channel, nickname)
|
||||
elif command == "team":
|
||||
self.cmd_team(channel, nickname)
|
||||
elif command == "wild":
|
||||
self.cmd_wild(channel, nickname, args)
|
||||
elif command == "stats":
|
||||
self.cmd_stats(channel, nickname, args)
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: Unknown command. Use !help for available commands.")
|
||||
except Exception as e:
|
||||
self.send_message(channel, f"{nickname}: Error processing command: {str(e)}")
|
||||
print(f"Command error: {e}")
|
||||
|
||||
def send_message(self, target, message):
|
||||
self.send(f"PRIVMSG {target} :{message}")
|
||||
time.sleep(0.5)
|
||||
|
||||
def run_async_command(self, coro):
|
||||
return self.loop.run_until_complete(coro)
|
||||
|
||||
def cmd_help(self, channel, nickname):
|
||||
help_text = [
|
||||
"🐾 Pet Bot Commands:",
|
||||
"!start - Begin your pet journey in Starter Town",
|
||||
"!explore - Explore your current location for wild pets",
|
||||
"!catch (or !capture) - Try to catch a pet during exploration or battle",
|
||||
"!battle - Start a battle with an encountered wild pet",
|
||||
"!attack <move> - Use a move in battle",
|
||||
"!flee - Try to escape from battle",
|
||||
"!moves - View your active pet's available moves",
|
||||
"!travel <location> - Travel to a different location",
|
||||
"!location - See where you currently are",
|
||||
"!achievements - View your achievements and progress",
|
||||
"!weather - Check current weather effects",
|
||||
"!team - View your active pets",
|
||||
"!wild <location> - See what pets spawn in a location",
|
||||
"!stats - View your player stats",
|
||||
"Locations: Starter Town, Whispering Woods, Electric Canyon, Crystal Caves, Frozen Tundra, Dragon's Peak"
|
||||
]
|
||||
for line in help_text:
|
||||
self.send_message(channel, line)
|
||||
|
||||
def cmd_start(self, channel, nickname):
|
||||
player = self.run_async_command(self.database.get_player(nickname))
|
||||
if player:
|
||||
self.send_message(channel, f"{nickname}: You already have an account! Use !team to see your pets.")
|
||||
return
|
||||
|
||||
player_id = self.run_async_command(self.database.create_player(nickname))
|
||||
starter_pet = self.run_async_command(self.game_engine.give_starter_pet(player_id))
|
||||
|
||||
self.send_message(channel,
|
||||
f"🎉 {nickname}: Welcome to the world of pets! You received a Level {starter_pet['level']} {starter_pet['species_name']}! You are now in Starter Town.")
|
||||
|
||||
def cmd_explore(self, channel, nickname):
|
||||
player = self.run_async_command(self.database.get_player(nickname))
|
||||
if not player:
|
||||
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
|
||||
return
|
||||
|
||||
encounter = self.run_async_command(self.game_engine.explore_location(player["id"]))
|
||||
|
||||
if encounter["type"] == "error":
|
||||
self.send_message(channel, f"{nickname}: {encounter['message']}")
|
||||
elif encounter["type"] == "empty":
|
||||
self.send_message(channel, f"🔍 {nickname}: {encounter['message']}")
|
||||
elif encounter["type"] == "encounter":
|
||||
# Store the encounter for potential catching
|
||||
self.active_encounters[player["id"]] = encounter["pet"]
|
||||
|
||||
pet = encounter["pet"]
|
||||
type_str = pet["type1"]
|
||||
if pet["type2"]:
|
||||
type_str += f"/{pet['type2']}"
|
||||
|
||||
self.send_message(channel,
|
||||
f"🐾 {nickname}: A wild Level {pet['level']} {pet['species_name']} ({type_str}) appeared in {encounter['location']}!")
|
||||
self.send_message(channel, f"Choose your action: !battle to fight it, or !catch to try catching it directly!")
|
||||
|
||||
def cmd_battle(self, channel, nickname):
|
||||
player = self.run_async_command(self.database.get_player(nickname))
|
||||
if not player:
|
||||
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
|
||||
return
|
||||
|
||||
# Check if player has an active encounter
|
||||
if player["id"] not in self.active_encounters:
|
||||
self.send_message(channel, f"{nickname}: You need to !explore first to find a pet to battle!")
|
||||
return
|
||||
|
||||
# Check if already in battle
|
||||
active_battle = self.run_async_command(self.game_engine.battle_engine.get_active_battle(player["id"]))
|
||||
if active_battle:
|
||||
self.send_message(channel, f"{nickname}: You're already in battle! Use !attack <move> or !flee.")
|
||||
return
|
||||
|
||||
# Get player's active pet
|
||||
pets = self.run_async_command(self.database.get_player_pets(player["id"], active_only=True))
|
||||
if not pets:
|
||||
self.send_message(channel, f"{nickname}: You need an active pet to battle! Use !team to check your pets.")
|
||||
return
|
||||
|
||||
player_pet = pets[0] # Use first active pet
|
||||
wild_pet = self.active_encounters[player["id"]]
|
||||
|
||||
# Start battle
|
||||
battle = self.run_async_command(self.game_engine.battle_engine.start_battle(player["id"], player_pet, wild_pet))
|
||||
|
||||
self.send_message(channel, f"⚔️ {nickname}: Battle started!")
|
||||
self.send_message(channel, f"Your {player_pet['species_name']} (Lv.{player_pet['level']}, {battle['player_hp']}/{player_pet['max_hp']} HP) vs Wild {wild_pet['species_name']} (Lv.{wild_pet['level']}, {battle['wild_hp']}/{wild_pet['max_hp']} HP)")
|
||||
|
||||
move_list = [move["name"] for move in battle["available_moves"]]
|
||||
self.send_message(channel, f"Available moves: {', '.join(move_list)}")
|
||||
self.send_message(channel, f"Use !attack <move_name> to attack!")
|
||||
|
||||
def cmd_attack(self, channel, nickname, args):
|
||||
if not args:
|
||||
self.send_message(channel, f"{nickname}: Specify a move to use! Example: !attack Tackle")
|
||||
return
|
||||
|
||||
player = self.run_async_command(self.database.get_player(nickname))
|
||||
if not player:
|
||||
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
|
||||
return
|
||||
|
||||
move_name = " ".join(args).title() # Normalize to Title Case
|
||||
result = self.run_async_command(self.game_engine.battle_engine.execute_battle_turn(player["id"], move_name))
|
||||
|
||||
if "error" in result:
|
||||
self.send_message(channel, f"{nickname}: {result['error']}")
|
||||
return
|
||||
|
||||
# Display battle results
|
||||
for action in result["results"]:
|
||||
damage_msg = f"{action['attacker']} used {action['move']}!"
|
||||
self.send_message(channel, damage_msg)
|
||||
|
||||
if action["damage"] > 0:
|
||||
effectiveness_msgs = {
|
||||
"super_effective": "It's super effective!",
|
||||
"not_very_effective": "It's not very effective...",
|
||||
"no_effect": "It had no effect!",
|
||||
"super_effective_critical": "Critical hit! It's super effective!",
|
||||
"normal_critical": "Critical hit!",
|
||||
"not_very_effective_critical": "Critical hit! It's not very effective..."
|
||||
}
|
||||
|
||||
damage_text = f"Dealt {action['damage']} damage!"
|
||||
if action["effectiveness"] in effectiveness_msgs:
|
||||
damage_text += f" {effectiveness_msgs[action['effectiveness']]}"
|
||||
|
||||
self.send_message(channel, damage_text)
|
||||
self.send_message(channel, f"Target HP: {action['target_hp']}")
|
||||
|
||||
if result["battle_over"]:
|
||||
if result["winner"] == "player":
|
||||
self.send_message(channel, f"🎉 {nickname}: You won the battle!")
|
||||
# Remove encounter since battle is over
|
||||
if player["id"] in self.active_encounters:
|
||||
del self.active_encounters[player["id"]]
|
||||
else:
|
||||
self.send_message(channel, f"💀 {nickname}: Your pet fainted! You lost the battle...")
|
||||
# Remove encounter
|
||||
if player["id"] in self.active_encounters:
|
||||
del self.active_encounters[player["id"]]
|
||||
else:
|
||||
# Battle continues
|
||||
move_list = [move["name"] for move in result["available_moves"]]
|
||||
self.send_message(channel, f"Your turn! Available moves: {', '.join(move_list)}")
|
||||
|
||||
def cmd_flee(self, channel, nickname):
|
||||
player = self.run_async_command(self.database.get_player(nickname))
|
||||
if not player:
|
||||
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
|
||||
return
|
||||
|
||||
success = self.run_async_command(self.game_engine.battle_engine.flee_battle(player["id"]))
|
||||
|
||||
if success:
|
||||
self.send_message(channel, f"💨 {nickname}: You successfully escaped from battle!")
|
||||
# Remove encounter
|
||||
if player["id"] in self.active_encounters:
|
||||
del self.active_encounters[player["id"]]
|
||||
else:
|
||||
self.send_message(channel, f"❌ {nickname}: Couldn't escape! The battle continues!")
|
||||
|
||||
def cmd_moves(self, channel, nickname):
|
||||
player = self.run_async_command(self.database.get_player(nickname))
|
||||
if not player:
|
||||
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
|
||||
return
|
||||
|
||||
# Get player's active pets
|
||||
pets = self.run_async_command(self.database.get_player_pets(player["id"], active_only=True))
|
||||
if not pets:
|
||||
self.send_message(channel, f"{nickname}: You don't have any active pets! Use !team to check your pets.")
|
||||
return
|
||||
|
||||
active_pet = pets[0] # Use first active pet
|
||||
available_moves = self.game_engine.battle_engine.get_available_moves(active_pet)
|
||||
|
||||
if not available_moves:
|
||||
self.send_message(channel, f"{nickname}: Your {active_pet['species_name']} has no available moves!")
|
||||
return
|
||||
|
||||
# Limit to 4 moves max
|
||||
moves_to_show = available_moves[:4]
|
||||
|
||||
move_info = []
|
||||
for move in moves_to_show:
|
||||
power_info = f"Power: {move.get('power', 'N/A')}" if move.get('power') else "Status move"
|
||||
move_info.append(f"{move['name']} ({move['type']}) - {power_info}")
|
||||
|
||||
pet_name = active_pet["nickname"] or active_pet["species_name"]
|
||||
self.send_message(channel, f"🎯 {pet_name}'s moves:")
|
||||
for move_line in move_info:
|
||||
self.send_message(channel, f" • {move_line}")
|
||||
|
||||
def cmd_catch(self, channel, nickname):
|
||||
player = self.run_async_command(self.database.get_player(nickname))
|
||||
if not player:
|
||||
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
|
||||
return
|
||||
|
||||
# Check if player is in an active battle
|
||||
active_battle = self.run_async_command(self.game_engine.battle_engine.get_active_battle(player["id"]))
|
||||
if active_battle:
|
||||
# Catching during battle
|
||||
wild_pet = active_battle["wild_pet"]
|
||||
current_hp = active_battle["wild_hp"]
|
||||
max_hp = wild_pet["max_hp"]
|
||||
|
||||
# Calculate catch rate based on remaining HP (lower HP = higher catch rate)
|
||||
hp_percentage = current_hp / max_hp
|
||||
base_catch_rate = 0.3 # Lower base rate than direct catch
|
||||
hp_bonus = (1.0 - hp_percentage) * 0.5 # Up to 50% bonus for low HP
|
||||
final_catch_rate = min(0.9, base_catch_rate + hp_bonus) # Cap at 90%
|
||||
|
||||
import random
|
||||
if random.random() < final_catch_rate:
|
||||
# Successful catch during battle
|
||||
result = self.run_async_command(self.game_engine.attempt_catch_current_location(player["id"], wild_pet))
|
||||
|
||||
# End the battle
|
||||
await_result = self.run_async_command(self.game_engine.battle_engine.end_battle(player["id"], "caught"))
|
||||
|
||||
# Check for achievements
|
||||
achievements = self.run_async_command(self.game_engine.check_and_award_achievements(player["id"], "catch_type", ""))
|
||||
if achievements:
|
||||
for achievement in achievements:
|
||||
self.send_message(channel, f"🏆 {nickname}: Achievement unlocked: {achievement['name']}! {achievement['description']}")
|
||||
|
||||
# Remove encounter
|
||||
if player["id"] in self.active_encounters:
|
||||
del self.active_encounters[player["id"]]
|
||||
|
||||
self.send_message(channel, f"🎯 {nickname}: Caught the {wild_pet['species_name']} during battle! (HP: {current_hp}/{max_hp})")
|
||||
else:
|
||||
# Failed catch - battle continues
|
||||
catch_percentage = int(final_catch_rate * 100)
|
||||
self.send_message(channel, f"🎯 {nickname}: The {wild_pet['species_name']} broke free! ({catch_percentage}% catch rate) Battle continues!")
|
||||
return
|
||||
|
||||
# Regular exploration catch (not in battle)
|
||||
if player["id"] not in self.active_encounters:
|
||||
self.send_message(channel, f"{nickname}: You need to !explore first to find a pet, or be in battle to catch!")
|
||||
return
|
||||
|
||||
target_pet = self.active_encounters[player["id"]]
|
||||
result = self.run_async_command(self.game_engine.attempt_catch_current_location(player["id"], target_pet))
|
||||
|
||||
# Check for achievements after successful catch
|
||||
if "Success!" in result:
|
||||
achievements = self.run_async_command(self.game_engine.check_and_award_achievements(player["id"], "catch_type", ""))
|
||||
if achievements:
|
||||
for achievement in achievements:
|
||||
self.send_message(channel, f"🏆 {nickname}: Achievement unlocked: {achievement['name']}! {achievement['description']}")
|
||||
|
||||
# Remove the encounter regardless of success
|
||||
del self.active_encounters[player["id"]]
|
||||
|
||||
self.send_message(channel, f"🎯 {nickname}: {result}")
|
||||
|
||||
def cmd_travel(self, channel, nickname, args):
|
||||
if not args:
|
||||
self.send_message(channel, f"{nickname}: Specify where to travel! Available: Starter Town, Whispering Woods, Electric Canyon, Crystal Caves, Frozen Tundra, Dragon's Peak")
|
||||
return
|
||||
|
||||
player = self.run_async_command(self.database.get_player(nickname))
|
||||
if not player:
|
||||
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
|
||||
return
|
||||
|
||||
destination = " ".join(args).title() # Normalize to Title Case
|
||||
location = self.run_async_command(self.database.get_location_by_name(destination))
|
||||
|
||||
if not location:
|
||||
self.send_message(channel, f"{nickname}: '{destination}' is not a valid location!")
|
||||
return
|
||||
|
||||
# Check if player can access this location
|
||||
can_access = self.run_async_command(self.database.can_access_location(player["id"], location["id"]))
|
||||
if not can_access:
|
||||
self.send_message(channel, f"{nickname}: You cannot access {destination} yet! Complete achievements to unlock new areas. Use !achievements to see progress.")
|
||||
return
|
||||
|
||||
# Clear any active encounters when traveling
|
||||
if player["id"] in self.active_encounters:
|
||||
del self.active_encounters[player["id"]]
|
||||
|
||||
self.run_async_command(self.database.update_player_location(player["id"], location["id"]))
|
||||
|
||||
# Show weather info
|
||||
weather = self.run_async_command(self.database.get_location_weather(location["id"]))
|
||||
weather_msg = ""
|
||||
if weather:
|
||||
weather_patterns = getattr(self.game_engine, 'weather_patterns', {})
|
||||
weather_info = weather_patterns.get("weather_types", {}).get(weather["weather_type"], {})
|
||||
weather_desc = weather_info.get("description", f"{weather['weather_type']} weather")
|
||||
weather_msg = f" Weather: {weather['weather_type']} - {weather_desc}"
|
||||
|
||||
self.send_message(channel, f"🗺️ {nickname}: You traveled to {destination}. {location['description']}{weather_msg}")
|
||||
|
||||
def cmd_location(self, channel, nickname):
|
||||
player = self.run_async_command(self.database.get_player(nickname))
|
||||
if not player:
|
||||
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
|
||||
return
|
||||
|
||||
location = self.run_async_command(self.database.get_player_location(player["id"]))
|
||||
if location:
|
||||
self.send_message(channel, f"📍 {nickname}: You are currently in {location['name']}. {location['description']}")
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: You seem to be lost! Contact an admin.")
|
||||
|
||||
def cmd_achievements(self, channel, nickname):
|
||||
player = self.run_async_command(self.database.get_player(nickname))
|
||||
if not player:
|
||||
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
|
||||
return
|
||||
|
||||
achievements = self.run_async_command(self.database.get_player_achievements(player["id"]))
|
||||
|
||||
if achievements:
|
||||
self.send_message(channel, f"🏆 {nickname}'s Achievements:")
|
||||
for achievement in achievements[:5]: # Show last 5 achievements
|
||||
self.send_message(channel, f"• {achievement['name']}: {achievement['description']}")
|
||||
|
||||
if len(achievements) > 5:
|
||||
self.send_message(channel, f"... and {len(achievements) - 5} more!")
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: No achievements yet! Keep exploring and catching pets to unlock new areas!")
|
||||
|
||||
def cmd_weather(self, channel, nickname):
|
||||
player = self.run_async_command(self.database.get_player(nickname))
|
||||
if not player:
|
||||
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
|
||||
return
|
||||
|
||||
location = self.run_async_command(self.database.get_player_location(player["id"]))
|
||||
if not location:
|
||||
self.send_message(channel, f"{nickname}: You seem to be lost!")
|
||||
return
|
||||
|
||||
weather = self.run_async_command(self.database.get_location_weather(location["id"]))
|
||||
if weather:
|
||||
weather_patterns = getattr(self.game_engine, 'weather_patterns', {})
|
||||
weather_info = weather_patterns.get("weather_types", {}).get(weather["weather_type"], {})
|
||||
weather_desc = weather_info.get("description", f"{weather['weather_type']} weather")
|
||||
|
||||
self.send_message(channel, f"🌤️ {nickname}: Current weather in {location['name']}: {weather['weather_type']}")
|
||||
self.send_message(channel, f"Effect: {weather_desc}")
|
||||
else:
|
||||
self.send_message(channel, f"🌤️ {nickname}: The weather in {location['name']} is calm with no special effects.")
|
||||
|
||||
def cmd_team(self, channel, nickname):
|
||||
player = self.run_async_command(self.database.get_player(nickname))
|
||||
if not player:
|
||||
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
|
||||
return
|
||||
|
||||
pets = self.run_async_command(self.database.get_player_pets(player["id"], active_only=False))
|
||||
if not pets:
|
||||
self.send_message(channel, f"{nickname}: You don't have any pets! Use !catch to find some.")
|
||||
return
|
||||
|
||||
# Show active pets first, then others
|
||||
active_pets = [pet for pet in pets if pet.get("is_active")]
|
||||
inactive_pets = [pet for pet in pets if not pet.get("is_active")]
|
||||
|
||||
team_info = []
|
||||
|
||||
# Active pets with star
|
||||
for pet in active_pets:
|
||||
name = pet["nickname"] or pet["species_name"]
|
||||
team_info.append(f"⭐{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP")
|
||||
|
||||
# Inactive pets
|
||||
for pet in inactive_pets[:5]: # Show max 5 inactive
|
||||
name = pet["nickname"] or pet["species_name"]
|
||||
team_info.append(f"{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP")
|
||||
|
||||
if len(inactive_pets) > 5:
|
||||
team_info.append(f"... and {len(inactive_pets) - 5} more in storage")
|
||||
|
||||
self.send_message(channel, f"🐾 {nickname}'s team: " + " | ".join(team_info))
|
||||
|
||||
def cmd_wild(self, channel, nickname, args):
|
||||
if not args:
|
||||
self.send_message(channel, f"{nickname}: Specify a location! Available: Starter Town, Whispering Woods, Electric Canyon, Crystal Caves, Frozen Tundra, Dragon's Peak")
|
||||
return
|
||||
|
||||
location_name = " ".join(args).title() # Normalize to Title Case
|
||||
wild_pets = self.run_async_command(self.game_engine.get_location_spawns(location_name))
|
||||
|
||||
if wild_pets:
|
||||
pet_list = ", ".join([pet["name"] for pet in wild_pets])
|
||||
self.send_message(channel, f"🌿 Wild pets in {location_name}: {pet_list}")
|
||||
else:
|
||||
self.send_message(channel, f"{nickname}: No location found called '{location_name}'")
|
||||
|
||||
def cmd_stats(self, channel, nickname, args):
|
||||
player = self.run_async_command(self.database.get_player(nickname))
|
||||
if not player:
|
||||
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
|
||||
return
|
||||
|
||||
self.send_message(channel,
|
||||
f"📊 {nickname}: Level {player['level']} | {player['experience']} XP | ${player['money']}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🐾 Starting Pet Bot for IRC...")
|
||||
bot = PetBot()
|
||||
try:
|
||||
bot.connect()
|
||||
except KeyboardInterrupt:
|
||||
print("\n✓ Bot stopped by user")
|
||||
except Exception as e:
|
||||
print(f"✗ Bot crashed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
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()
|
||||
1425
webserver.py
Normal file
1425
webserver.py
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue