diff --git a/modules/npc_events.py b/modules/npc_events.py new file mode 100644 index 0000000..49f8d20 --- /dev/null +++ b/modules/npc_events.py @@ -0,0 +1,236 @@ +""" +NPC Events Module +Handles player commands for NPC events system +""" + +from modules.base_module import BaseModule +from src.npc_events import NPCEventsManager +from datetime import datetime + +class NPCEventsModule(BaseModule): + """Module for NPC events system commands""" + + def __init__(self, bot, database, game_engine): + super().__init__(bot, database, game_engine) + self.events_manager = NPCEventsManager(database) + + def get_commands(self): + return ['events', 'event', 'help', 'contribute', 'eventhelp'] + + async def handle_command(self, command, channel, nickname, args): + """Handle NPC events commands""" + + # Normalize command + command = self.normalize_input(command) + + if command == 'events': + await self.cmd_events(channel, nickname) + elif command == 'event': + await self.cmd_event(channel, nickname, args) + elif command == 'help' and len(args) > 0 and args[0].lower() == 'events': + await self.cmd_event_help(channel, nickname) + elif command == 'contribute': + await self.cmd_contribute(channel, nickname, args) + elif command == 'eventhelp': + await self.cmd_event_help(channel, nickname) + + async def cmd_events(self, channel, nickname): + """Show all active NPC events""" + try: + active_events = await self.events_manager.get_active_events() + + if not active_events: + self.send_message(channel, "šŸ“… No active community events at the moment. Check back later!") + return + + message = "šŸŽÆ **Active Community Events:**\n" + + for event in active_events: + progress = self.events_manager.get_progress_bar( + event['current_contributions'], + event['target_contributions'] + ) + + # Calculate time remaining + expires_at = datetime.fromisoformat(event['expires_at']) + time_left = expires_at - datetime.now() + + if time_left.total_seconds() > 0: + hours_left = int(time_left.total_seconds() / 3600) + minutes_left = int((time_left.total_seconds() % 3600) / 60) + time_str = f"{hours_left}h {minutes_left}m" + else: + time_str = "Expired" + + difficulty_stars = "⭐" * event['difficulty'] + + message += f"\n**#{event['id']} - {event['title']}** {difficulty_stars}\n" + message += f"šŸ“ {event['description']}\n" + message += f"šŸ“Š Progress: {progress}\n" + message += f"ā° Time left: {time_str}\n" + message += f"šŸ’° Reward: {event['reward_experience']} XP, ${event['reward_money']}\n" + message += f"šŸ¤ Use `!contribute {event['id']}` to help!\n" + + self.send_message(channel, message) + + except Exception as e: + print(f"Error in cmd_events: {e}") + self.send_message(channel, f"āŒ Error fetching events: {str(e)}") + + async def cmd_event(self, channel, nickname, args): + """Show details for a specific event""" + if not args: + self.send_message(channel, "āŒ Usage: !event ") + return + + try: + event_id = int(args[0]) + event = await self.events_manager.get_event_details(event_id) + + if not event: + self.send_message(channel, f"āŒ Event #{event_id} not found or expired.") + return + + # Calculate time remaining + expires_at = datetime.fromisoformat(event['expires_at']) + time_left = expires_at - datetime.now() + + if time_left.total_seconds() > 0: + hours_left = int(time_left.total_seconds() / 3600) + minutes_left = int((time_left.total_seconds() % 3600) / 60) + time_str = f"{hours_left}h {minutes_left}m" + else: + time_str = "Expired" + + progress = self.events_manager.get_progress_bar( + event['current_contributions'], + event['target_contributions'] + ) + + difficulty_stars = "⭐" * event['difficulty'] + status_emoji = "šŸ”„" if event['status'] == 'active' else "āœ…" if event['status'] == 'completed' else "āŒ" + + message = f"{status_emoji} **Event #{event['id']}: {event['title']}** {difficulty_stars}\n" + message += f"šŸ“ {event['description']}\n" + message += f"šŸ“Š Progress: {progress}\n" + message += f"ā° Time left: {time_str}\n" + message += f"šŸ’° Reward: {event['reward_experience']} XP, ${event['reward_money']}\n" + message += f"šŸ† Status: {event['status'].title()}\n" + + # Show leaderboard if there are contributors + if event['leaderboard']: + message += "\nšŸ… **Top Contributors:**\n" + for i, contributor in enumerate(event['leaderboard'][:5]): # Top 5 + rank_emoji = ["šŸ„‡", "🄈", "šŸ„‰", "4ļøāƒ£", "5ļøāƒ£"][i] + message += f"{rank_emoji} {contributor['nickname']}: {contributor['contributions']} contributions\n" + + if event['status'] == 'active': + message += f"\nšŸ¤ Use `!contribute {event['id']}` to help!" + + self.send_message(channel, message) + + except ValueError: + self.send_message(channel, "āŒ Invalid event ID. Please use a number.") + except Exception as e: + print(f"Error in cmd_event: {e}") + self.send_message(channel, f"āŒ Error fetching event details: {str(e)}") + + async def cmd_contribute(self, channel, nickname, args): + """Allow player to contribute to an event""" + if not args: + self.send_message(channel, "āŒ Usage: !contribute ") + return + + player = await self.require_player(channel, nickname) + if not player: + return + + try: + event_id = int(args[0]) + + # Check if event exists and is active + event = await self.events_manager.get_event_details(event_id) + if not event: + self.send_message(channel, f"āŒ Event #{event_id} not found or expired.") + return + + if event['status'] != 'active': + self.send_message(channel, f"āŒ Event #{event_id} is not active.") + return + + # Check if event has expired + expires_at = datetime.fromisoformat(event['expires_at']) + if datetime.now() >= expires_at: + self.send_message(channel, f"āŒ Event #{event_id} has expired.") + return + + # Add contribution + result = await self.events_manager.contribute_to_event(event_id, player['id']) + + if not result['success']: + self.send_message(channel, f"āŒ Failed to contribute: {result.get('error', 'Unknown error')}") + return + + # Get updated event details + updated_event = await self.events_manager.get_event_details(event_id) + player_contributions = await self.events_manager.get_player_contributions(player['id'], event_id) + + progress = self.events_manager.get_progress_bar( + updated_event['current_contributions'], + updated_event['target_contributions'] + ) + + if result['event_completed']: + self.send_message(channel, f"šŸŽ‰ **EVENT COMPLETED!** {updated_event['completion_message']}") + self.send_message(channel, f"šŸ† Thanks to everyone who participated! Rewards will be distributed shortly.") + else: + self.send_message(channel, f"āœ… {nickname} contributed to '{updated_event['title']}'!") + self.send_message(channel, f"šŸ“Š Progress: {progress}") + self.send_message(channel, f"šŸ¤ Your total contributions: {player_contributions}") + + # Show encouragement based on progress + progress_percent = (updated_event['current_contributions'] / updated_event['target_contributions']) * 100 + if progress_percent >= 75: + self.send_message(channel, "šŸ”„ Almost there! Keep it up!") + elif progress_percent >= 50: + self.send_message(channel, "šŸ’Ŗ Great progress! We're halfway there!") + elif progress_percent >= 25: + self.send_message(channel, "🌟 Good start! Keep contributing!") + + except ValueError: + self.send_message(channel, "āŒ Invalid event ID. Please use a number.") + except Exception as e: + print(f"Error in cmd_contribute: {e}") + self.send_message(channel, f"āŒ Error contributing to event: {str(e)}") + + async def cmd_event_help(self, channel, nickname): + """Show help for NPC events system""" + message = """šŸŽÆ **Community Events System Help** + +**Available Commands:** +• `!events` - Show all active community events +• `!event ` - Show details for a specific event +• `!contribute ` - Contribute to an event +• `!eventhelp` - Show this help message + +**How Events Work:** +🌟 Random community events spawn regularly +šŸ¤ All players can contribute to the same events +šŸ“Š Events have progress bars and time limits +šŸ† Everyone who contributes gets rewards when completed +⭐ Events have different difficulty levels (1-3 stars) + +**Event Types:** +• šŸŖ Resource Gathering - Help collect supplies +• 🐾 Pet Rescue - Search for missing pets +• šŸŽŖ Community Projects - Work together on town projects +• 🚨 Emergency Response - Help during crises +• šŸ”¬ Research - Assist with scientific discoveries + +**Tips:** +• Check `!events` regularly for new opportunities +• Higher difficulty events give better rewards +• Contributing more increases your reward multiplier +• Events expire after their time limit""" + + self.send_message(channel, message) \ No newline at end of file diff --git a/src/npc_events.py b/src/npc_events.py new file mode 100644 index 0000000..a8324f6 --- /dev/null +++ b/src/npc_events.py @@ -0,0 +1,293 @@ +""" +NPC Events System +Manages random collaborative events that all players can participate in +""" + +import asyncio +import random +from datetime import datetime, timedelta +from typing import Dict, List, Optional +from src.database import Database + +class NPCEventsManager: + def __init__(self, database: Database): + self.database = database + self.active_events = {} + self.event_templates = { + 1: [ # Difficulty 1 - Easy + { + "event_type": "resource_gathering", + "title": "Village Supply Run", + "description": "The village needs supplies! Help gather resources by exploring and finding items.", + "target_contributions": 25, + "reward_experience": 50, + "reward_money": 100, + "completion_message": "šŸŽ‰ The village has enough supplies! Everyone who helped gets rewarded!", + "duration_hours": 4 + }, + { + "event_type": "pet_rescue", + "title": "Lost Pet Search", + "description": "A pet has gone missing! Help search different locations to find clues.", + "target_contributions": 20, + "reward_experience": 40, + "reward_money": 80, + "completion_message": "🐾 The lost pet has been found safe! Thanks to everyone who helped search!", + "duration_hours": 3 + }, + { + "event_type": "community_project", + "title": "Park Cleanup", + "description": "The local park needs cleaning! Help by contributing your time and effort.", + "target_contributions": 30, + "reward_experience": 35, + "reward_money": 75, + "completion_message": "🌳 The park is clean and beautiful again! Great teamwork everyone!", + "duration_hours": 5 + } + ], + 2: [ # Difficulty 2 - Medium + { + "event_type": "emergency_response", + "title": "Storm Recovery", + "description": "A storm has damaged the town! Help with recovery efforts by contributing resources and time.", + "target_contributions": 50, + "reward_experience": 100, + "reward_money": 200, + "completion_message": "ā›ˆļø The town has recovered from the storm! Everyone's hard work paid off!", + "duration_hours": 6 + }, + { + "event_type": "festival_preparation", + "title": "Annual Festival Setup", + "description": "The annual pet festival is coming! Help set up decorations and prepare activities.", + "target_contributions": 40, + "reward_experience": 80, + "reward_money": 150, + "completion_message": "šŸŽŖ The festival is ready! Thanks to everyone who helped prepare!", + "duration_hours": 8 + }, + { + "event_type": "research_expedition", + "title": "Scientific Discovery", + "description": "Researchers need help documenting rare pets! Contribute by exploring and reporting findings.", + "target_contributions": 35, + "reward_experience": 90, + "reward_money": 180, + "completion_message": "šŸ”¬ The research is complete! Your discoveries will help future generations!", + "duration_hours": 7 + } + ], + 3: [ # Difficulty 3 - Hard + { + "event_type": "crisis_response", + "title": "Regional Emergency", + "description": "A regional crisis requires immediate community response! All trainers needed!", + "target_contributions": 75, + "reward_experience": 150, + "reward_money": 300, + "completion_message": "🚨 Crisis averted! The entire region is safe thanks to your heroic efforts!", + "duration_hours": 12 + }, + { + "event_type": "legendary_encounter", + "title": "Legendary Pet Sighting", + "description": "A legendary pet has been spotted! Help researchers track and document this rare encounter.", + "target_contributions": 60, + "reward_experience": 200, + "reward_money": 400, + "completion_message": "✨ The legendary pet has been successfully documented! History has been made!", + "duration_hours": 10 + }, + { + "event_type": "ancient_mystery", + "title": "Ancient Ruins Discovery", + "description": "Ancient ruins have been discovered! Help archaeologists uncover the secrets within.", + "target_contributions": 80, + "reward_experience": 180, + "reward_money": 350, + "completion_message": "šŸ›ļø The ancient secrets have been revealed! Your efforts uncovered lost knowledge!", + "duration_hours": 14 + } + ] + } + + async def start_background_task(self): + """Start the background task that manages NPC events""" + while True: + try: + # Check for expired events + await self.expire_events() + + # Distribute rewards for completed events + await self.distribute_completed_rewards() + + # Maybe spawn a new event + await self.maybe_spawn_event() + + # Wait 30 minutes before next check + await asyncio.sleep(30 * 60) + + except Exception as e: + print(f"Error in NPC events background task: {e}") + await asyncio.sleep(5 * 60) # Wait 5 minutes on error + + async def expire_events(self): + """Mark expired events as expired""" + try: + expired_count = await self.database.expire_npc_events() + if expired_count > 0: + print(f"šŸ• {expired_count} NPC events expired") + except Exception as e: + print(f"Error expiring NPC events: {e}") + + async def distribute_completed_rewards(self): + """Distribute rewards for completed events""" + try: + # Get completed events that haven't distributed rewards yet + completed_events = await self.database.get_active_npc_events() + + for event in completed_events: + if event['status'] == 'completed': + result = await self.database.distribute_event_rewards(event['id']) + if result['success']: + print(f"šŸŽ Distributed rewards for event '{event['title']}' to {result['participants_rewarded']} players") + + except Exception as e: + print(f"Error distributing event rewards: {e}") + + async def maybe_spawn_event(self): + """Maybe spawn a new event based on conditions""" + try: + # Check if we have any active events + active_events = await self.database.get_active_npc_events() + + # Don't spawn if we already have 2 or more active events + if len(active_events) >= 2: + return + + # 20% chance to spawn a new event each check (every 30 minutes) + if random.random() < 0.2: + await self.spawn_random_event() + + except Exception as e: + print(f"Error in maybe_spawn_event: {e}") + + async def spawn_random_event(self): + """Spawn a random event based on difficulty""" + try: + # Choose difficulty (weighted towards easier events) + difficulty_weights = {1: 0.6, 2: 0.3, 3: 0.1} + difficulty = random.choices(list(difficulty_weights.keys()), + weights=list(difficulty_weights.values()))[0] + + # Choose random event template + templates = self.event_templates[difficulty] + template = random.choice(templates) + + # Create event data + event_data = { + 'event_type': template['event_type'], + 'title': template['title'], + 'description': template['description'], + 'difficulty': difficulty, + 'target_contributions': template['target_contributions'], + 'reward_experience': template['reward_experience'], + 'reward_money': template['reward_money'], + 'completion_message': template['completion_message'], + 'expires_at': (datetime.now() + timedelta(hours=template['duration_hours'])).isoformat() + } + + # Create the event + event_id = await self.database.create_npc_event(event_data) + + print(f"šŸŽÆ New NPC event spawned: '{template['title']}' (ID: {event_id}, Difficulty: {difficulty})") + + return event_id + + except Exception as e: + print(f"Error spawning random event: {e}") + return None + + async def get_active_events(self) -> List[Dict]: + """Get all active events""" + return await self.database.get_active_npc_events() + + async def contribute_to_event(self, event_id: int, player_id: int, contribution: int = 1) -> Dict: + """Add a player's contribution to an event""" + return await self.database.contribute_to_npc_event(event_id, player_id, contribution) + + async def get_event_details(self, event_id: int) -> Optional[Dict]: + """Get detailed information about an event""" + event = await self.database.get_npc_event_by_id(event_id) + if not event: + return None + + # Add leaderboard + leaderboard = await self.database.get_event_leaderboard(event_id) + event['leaderboard'] = leaderboard + + return event + + async def get_player_contributions(self, player_id: int, event_id: int) -> int: + """Get player's contributions to a specific event""" + return await self.database.get_player_event_contributions(player_id, event_id) + + async def force_spawn_event(self, difficulty: int = 1) -> Optional[int]: + """Force spawn an event (admin command)""" + if difficulty not in self.event_templates: + return None + + templates = self.event_templates[difficulty] + template = random.choice(templates) + + event_data = { + 'event_type': template['event_type'], + 'title': template['title'], + 'description': template['description'], + 'difficulty': difficulty, + 'target_contributions': template['target_contributions'], + 'reward_experience': template['reward_experience'], + 'reward_money': template['reward_money'], + 'completion_message': template['completion_message'], + 'expires_at': (datetime.now() + timedelta(hours=template['duration_hours'])).isoformat() + } + + return await self.database.create_npc_event(event_data) + + async def force_spawn_specific_event(self, event_type: str, difficulty: int = 1) -> Optional[int]: + """Force spawn a specific event type (admin command)""" + if difficulty not in self.event_templates: + return None + + # Find template matching the event type + templates = self.event_templates[difficulty] + template = None + for t in templates: + if t['event_type'] == event_type: + template = t + break + + if not template: + return None + + event_data = { + 'event_type': template['event_type'], + 'title': template['title'], + 'description': template['description'], + 'difficulty': difficulty, + 'target_contributions': template['target_contributions'], + 'reward_experience': template['reward_experience'], + 'reward_money': template['reward_money'], + 'completion_message': template['completion_message'], + 'expires_at': (datetime.now() + timedelta(hours=template['duration_hours'])).isoformat() + } + + return await self.database.create_npc_event(event_data) + + def get_progress_bar(self, current: int, target: int, width: int = 20) -> str: + """Generate a progress bar for event progress""" + filled = int((current / target) * width) + bar = "ā–ˆ" * filled + "ā–‘" * (width - filled) + percentage = min(100, int((current / target) * 100)) + return f"[{bar}] {percentage}% ({current}/{target})" \ No newline at end of file