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
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"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue