diff --git a/config/items.json b/config/items.json index 0912108..d8a039c 100644 --- a/config/items.json +++ b/config/items.json @@ -48,6 +48,28 @@ "effect_value": 15, "locations": ["mystic_forest", "enchanted_grove"], "spawn_rate": 0.03 + }, + { + "id": 18, + "name": "Revive", + "description": "Revives a fainted pet and restores 50% of its HP", + "rarity": "rare", + "category": "healing", + "effect": "revive", + "effect_value": 50, + "locations": ["all"], + "spawn_rate": 0.005 + }, + { + "id": 19, + "name": "Max Revive", + "description": "Revives a fainted pet and fully restores its HP", + "rarity": "epic", + "category": "healing", + "effect": "max_revive", + "effect_value": 100, + "locations": ["all"], + "spawn_rate": 0.002 } ], "battle_items": [ diff --git a/modules/inventory.py b/modules/inventory.py index 227e882..df6cc1e 100644 --- a/modules/inventory.py +++ b/modules/inventory.py @@ -99,6 +99,41 @@ class Inventory(BaseModule): self.send_message(channel, f"🍀 {nickname}: Used {item['name']}! Rare pet encounter rate increased by {effect_value}% for 1 hour!") + elif effect == "revive": + # Handle revive items for fainted pets + fainted_pets = await self.database.get_fainted_pets(player["id"]) + if not fainted_pets: + self.send_message(channel, f"❌ {nickname}: You don't have any fainted pets to revive!") + return + + # Use the first fainted pet (can be expanded to choose specific pet) + pet = fainted_pets[0] + new_hp = int(pet["max_hp"] * (effect_value / 100.0)) # Convert percentage to HP + + # Revive the pet and restore HP + await self.database.revive_pet(pet["id"], new_hp) + + self.send_message(channel, + f"💫 {nickname}: Used {item['name']} on {pet['nickname'] or pet['species_name']}! " + f"Revived and restored {new_hp}/{pet['max_hp']} HP!") + + elif effect == "max_revive": + # Handle max revive items for fainted pets + fainted_pets = await self.database.get_fainted_pets(player["id"]) + if not fainted_pets: + self.send_message(channel, f"❌ {nickname}: You don't have any fainted pets to revive!") + return + + # Use the first fainted pet (can be expanded to choose specific pet) + pet = fainted_pets[0] + + # Revive the pet and fully restore HP + await self.database.revive_pet(pet["id"], pet["max_hp"]) + + self.send_message(channel, + f"✨ {nickname}: Used {item['name']} on {pet['nickname'] or pet['species_name']}! " + f"Revived and fully restored HP! ({pet['max_hp']}/{pet['max_hp']})") + elif effect == "money": # Handle money items (like Coin Pouch) import random diff --git a/src/database.py b/src/database.py index e190959..3782c86 100644 --- a/src/database.py +++ b/src/database.py @@ -169,6 +169,22 @@ class Database: print(f"Migration warning: {e}") pass # Don't fail if migration has issues + # Add fainted_at column for tracking when pets faint + try: + await db.execute("ALTER TABLE pets ADD COLUMN fainted_at TIMESTAMP DEFAULT NULL") + await db.commit() + print("Added fainted_at column to pets table") + except: + pass # Column already exists + + # Add last_heal_time column for heal command cooldown + try: + await db.execute("ALTER TABLE players ADD COLUMN last_heal_time TIMESTAMP DEFAULT NULL") + await db.commit() + print("Added last_heal_time column to players table") + except: + pass # Column already exists + await db.execute(""" CREATE TABLE IF NOT EXISTS location_spawns ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -349,6 +365,38 @@ class Database: ) """) + await db.execute(""" + CREATE TABLE IF NOT EXISTS npc_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + event_type TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL, + difficulty INTEGER DEFAULT 1, + target_contributions INTEGER NOT NULL, + current_contributions INTEGER DEFAULT 0, + reward_experience INTEGER DEFAULT 0, + reward_money INTEGER DEFAULT 0, + reward_items TEXT, + status TEXT DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP NOT NULL, + completion_message TEXT + ) + """) + + await db.execute(""" + CREATE TABLE IF NOT EXISTS npc_event_contributions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + event_id INTEGER NOT NULL, + player_id INTEGER NOT NULL, + contributions INTEGER DEFAULT 0, + last_contribution_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (event_id) REFERENCES npc_events (id), + FOREIGN KEY (player_id) REFERENCES players (id), + UNIQUE(event_id, player_id) + ) + """) + await db.execute(""" CREATE TABLE IF NOT EXISTS verification_pins ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -863,7 +911,7 @@ class Database: """Add an item to player's inventory""" async with aiosqlite.connect(self.db_path) as db: # Get item ID - cursor = await db.execute("SELECT id FROM items WHERE name = ?", (item_name,)) + cursor = await db.execute("SELECT id FROM items WHERE LOWER(name) = LOWER(?)", (item_name,)) item = await cursor.fetchone() if not item: return False @@ -918,7 +966,7 @@ class Database: SELECT i.*, pi.quantity FROM items i JOIN player_inventory pi ON i.id = pi.item_id - WHERE pi.player_id = ? AND i.name = ? + WHERE pi.player_id = ? AND LOWER(i.name) = LOWER(?) """, (player_id, item_name)) item = await cursor.fetchone() @@ -2065,4 +2113,336 @@ class Database: return cursor.rowcount > 0 except Exception as e: print(f"Error renaming team configuration: {e}") - return False \ No newline at end of file + return False + + # NPC Events System Methods + async def create_npc_event(self, event_data: Dict) -> int: + """Create a new NPC event""" + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute(""" + INSERT INTO npc_events + (event_type, title, description, difficulty, target_contributions, + reward_experience, reward_money, reward_items, expires_at, completion_message) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + event_data['event_type'], + event_data['title'], + event_data['description'], + event_data['difficulty'], + event_data['target_contributions'], + event_data['reward_experience'], + event_data['reward_money'], + event_data.get('reward_items', ''), + event_data['expires_at'], + event_data.get('completion_message', '') + )) + + await db.commit() + return cursor.lastrowid + + async def get_active_npc_events(self) -> List[Dict]: + """Get all active NPC events""" + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + cursor = await db.execute(""" + SELECT * FROM npc_events + WHERE status = 'active' AND expires_at > datetime('now') + ORDER BY created_at DESC + """) + + rows = await cursor.fetchall() + return [dict(row) for row in rows] + + async def get_npc_event_by_id(self, event_id: int) -> Optional[Dict]: + """Get a specific NPC event by ID""" + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + cursor = await db.execute(""" + SELECT * FROM npc_events WHERE id = ? + """, (event_id,)) + + row = await cursor.fetchone() + return dict(row) if row else None + + async def contribute_to_npc_event(self, event_id: int, player_id: int, contribution: int) -> Dict: + """Add player contribution to an NPC event""" + async with aiosqlite.connect(self.db_path) as db: + try: + await db.execute("BEGIN TRANSACTION") + + # Insert or update player contribution + await db.execute(""" + INSERT OR REPLACE INTO npc_event_contributions + (event_id, player_id, contributions, last_contribution_at) + VALUES (?, ?, + COALESCE((SELECT contributions FROM npc_event_contributions + WHERE event_id = ? AND player_id = ?), 0) + ?, + CURRENT_TIMESTAMP) + """, (event_id, player_id, event_id, player_id, contribution)) + + # Update total contributions for the event + await db.execute(""" + UPDATE npc_events + SET current_contributions = current_contributions + ? + WHERE id = ? + """, (contribution, event_id)) + + # Check if event is completed + cursor = await db.execute(""" + SELECT current_contributions, target_contributions + FROM npc_events WHERE id = ? + """, (event_id,)) + + row = await cursor.fetchone() + if row and row[0] >= row[1]: + # Mark event as completed + await db.execute(""" + UPDATE npc_events + SET status = 'completed' + WHERE id = ? + """, (event_id,)) + + await db.commit() + return {"success": True, "event_completed": True} + + await db.commit() + return {"success": True, "event_completed": False} + + except Exception as e: + await db.execute("ROLLBACK") + return {"success": False, "error": str(e)} + + async def get_player_event_contributions(self, player_id: int, event_id: int) -> int: + """Get player's contributions to a specific event""" + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute(""" + SELECT contributions FROM npc_event_contributions + WHERE event_id = ? AND player_id = ? + """, (event_id, player_id)) + + row = await cursor.fetchone() + return row[0] if row else 0 + + async def get_event_leaderboard(self, event_id: int) -> List[Dict]: + """Get leaderboard for an event""" + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + cursor = await db.execute(""" + SELECT p.nickname, c.contributions, c.last_contribution_at + FROM npc_event_contributions c + JOIN players p ON c.player_id = p.id + WHERE c.event_id = ? + ORDER BY c.contributions DESC + """, (event_id,)) + + rows = await cursor.fetchall() + return [dict(row) for row in rows] + + async def expire_npc_events(self) -> int: + """Mark expired events as expired and return count""" + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute(""" + UPDATE npc_events + SET status = 'expired' + WHERE status = 'active' AND expires_at <= datetime('now') + """) + + await db.commit() + return cursor.rowcount + + async def distribute_event_rewards(self, event_id: int) -> Dict: + """Distribute rewards to all participants of a completed event""" + async with aiosqlite.connect(self.db_path) as db: + try: + await db.execute("BEGIN TRANSACTION") + + # Get event details + cursor = await db.execute(""" + SELECT reward_experience, reward_money, reward_items + FROM npc_events WHERE id = ? AND status = 'completed' + """, (event_id,)) + + event_row = await cursor.fetchone() + if not event_row: + await db.execute("ROLLBACK") + return {"success": False, "error": "Event not found or not completed"} + + reward_exp, reward_money, reward_items = event_row + + # Get all participants + cursor = await db.execute(""" + SELECT player_id, contributions + FROM npc_event_contributions + WHERE event_id = ? + """, (event_id,)) + + participants = await cursor.fetchall() + + # Distribute rewards + for player_id, contributions in participants: + # Scale rewards based on contribution (minimum 50% of full reward) + contribution_multiplier = max(0.5, min(1.0, contributions / 10)) + + final_exp = int(reward_exp * contribution_multiplier) + final_money = int(reward_money * contribution_multiplier) + + # Update player rewards + await db.execute(""" + UPDATE players + SET experience = experience + ?, money = money + ? + WHERE id = ? + """, (final_exp, final_money, player_id)) + + await db.commit() + return {"success": True, "participants_rewarded": len(participants)} + + except Exception as e: + await db.execute("ROLLBACK") + return {"success": False, "error": str(e)} + + # Pet Healing System Methods + async def get_fainted_pets(self, player_id: int) -> List[Dict]: + """Get all fainted pets for a player""" + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + cursor = await db.execute(""" + SELECT p.*, ps.name as species_name, ps.emoji + FROM pets p + JOIN pet_species ps ON p.species_id = ps.id + WHERE p.player_id = ? AND p.fainted_at IS NOT NULL + ORDER BY p.fainted_at DESC + """, (player_id,)) + + rows = await cursor.fetchall() + return [dict(row) for row in rows] + + async def revive_pet(self, pet_id: int, new_hp: int) -> bool: + """Revive a fainted pet and restore HP""" + try: + async with aiosqlite.connect(self.db_path) as db: + await db.execute(""" + UPDATE pets + SET hp = ?, fainted_at = NULL + WHERE id = ? + """, (new_hp, pet_id)) + + await db.commit() + return True + except Exception as e: + print(f"Error reviving pet: {e}") + return False + + async def faint_pet(self, pet_id: int) -> bool: + """Mark a pet as fainted""" + try: + async with aiosqlite.connect(self.db_path) as db: + await db.execute(""" + UPDATE pets + SET hp = 0, fainted_at = CURRENT_TIMESTAMP + WHERE id = ? + """, (pet_id,)) + + await db.commit() + return True + except Exception as e: + print(f"Error fainting pet: {e}") + return False + + async def get_pets_for_auto_recovery(self) -> List[Dict]: + """Get pets that are eligible for auto-recovery (fainted for 30+ minutes)""" + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + 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.fainted_at IS NOT NULL + AND p.fainted_at <= datetime('now', '-30 minutes') + """) + + rows = await cursor.fetchall() + return [dict(row) for row in rows] + + async def auto_recover_pet(self, pet_id: int) -> bool: + """Auto-recover a pet to 1 HP after 30 minutes""" + try: + async with aiosqlite.connect(self.db_path) as db: + await db.execute(""" + UPDATE pets + SET hp = 1, fainted_at = NULL + WHERE id = ? + """, (pet_id,)) + + await db.commit() + return True + except Exception as e: + print(f"Error auto-recovering pet: {e}") + return False + + async def get_last_heal_time(self, player_id: int) -> Optional[datetime]: + """Get the last time a player used the heal command""" + async with aiosqlite.connect(self.db_path) as db: + cursor = await db.execute(""" + SELECT last_heal_time FROM players WHERE id = ? + """, (player_id,)) + + row = await cursor.fetchone() + if row and row[0]: + return datetime.fromisoformat(row[0]) + return None + + async def update_last_heal_time(self, player_id: int) -> bool: + """Update the last heal time for a player""" + try: + async with aiosqlite.connect(self.db_path) as db: + await db.execute(""" + UPDATE players + SET last_heal_time = CURRENT_TIMESTAMP + WHERE id = ? + """, (player_id,)) + + await db.commit() + return True + except Exception as e: + print(f"Error updating last heal time: {e}") + return False + + async def get_active_pets(self, player_id: int) -> List[Dict]: + """Get all active pets for a player (excluding fainted pets)""" + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + cursor = await db.execute(""" + SELECT p.*, ps.name as species_name, ps.emoji + FROM pets p + JOIN pet_species ps ON p.species_id = ps.id + WHERE p.player_id = ? AND p.is_active = 1 AND p.fainted_at IS NULL + ORDER BY p.team_order + """, (player_id,)) + + rows = await cursor.fetchall() + return [dict(row) for row in rows] + + async def get_player_pets(self, player_id: int, active_only: bool = False) -> List[Dict]: + """Get all pets for a player, optionally filtering to active only""" + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + + if active_only: + cursor = await db.execute(""" + SELECT p.*, ps.name as species_name, ps.emoji + FROM pets p + JOIN pet_species ps ON p.species_id = ps.id + WHERE p.player_id = ? AND p.is_active = 1 AND p.fainted_at IS NULL + ORDER BY p.team_order + """, (player_id,)) + else: + cursor = await db.execute(""" + SELECT p.*, ps.name as species_name, ps.emoji + FROM pets p + JOIN pet_species ps ON p.species_id = ps.id + WHERE p.player_id = ? + ORDER BY p.is_active DESC, p.team_order, p.level DESC + """, (player_id,)) + + rows = await cursor.fetchall() + return [dict(row) for row in rows] \ No newline at end of file