Multiplayer Demo

multiplayer_demo.py
from lunaengine.core import LunaEngine, Scene
from lunaengine.backend.network import NetworkClient, NetworkHost, NetworkServer
from lunaengine.backend.network import MessageType, NetworkMessage, UserType
from lunaengine.ui import *
import pygame as pg
import time
import random

class NetworkInfo:
    def __init__(self):
        self.host = None
        self.server = None
        self.client = None
        self.is_connected = False
        self.is_host = False
        self.messages = []
        self.player_id = None

class MainScene(Scene):
    def on_enter(self, previous_scene = None):
        print("=== Multiplayer Demo ===")
        print("1. For host: Check 'Is Host', click 'Start Host'")
        print("2. For client: Uncheck 'Is Host', enter host IP, click 'Connect'")
        print("3. Click 'Play' to enter game")
        return super().on_enter(previous_scene)
    
    def on_exit(self, next_scene = None):
        return super().on_exit(next_scene)
    
    def __init__(self, engine: LunaEngine):
        super().__init__(engine)
        self.network_info = None
        self.connection_status = "Not connected"
        self.setup_ui()
        
    def setup_ui(self):
        screen_width, screen_height = self.engine.width, self.engine.height
        
        # Title
        self.add_ui_element(TextLabel(512, 30, "LunaEngine - Multiplayer Demo", 36, (255, 255, 255), root_point=(0.5, 0)))
        
        # Connection Frame
        self.frame = UiFrame(40, 150, 600, 400, root_point=(0, 0))
        self.frame.set_background_color((60, 60, 80))
        self.add_ui_element(self.frame)
        
        # Network Settings
        self.frame.add_child(TextLabel(50, 30, "Network Settings", 28, (255, 255, 200), root_point=(0, 0)))
        
        # Host/IP Input
        self.frame.add_child(TextLabel(50, 80, "Host / IP:", 20, (200, 230, 255), root_point=(0, 0)))
        self.host_input = TextBox(150, 75, 200, 35, '127.0.0.1', 18, root_point=(0, 0))
        self.host_input.set_text("127.0.0.1")
        self.frame.add_child(self.host_input)
        
        # Port Input
        self.frame.add_child(TextLabel(50, 130, "Port:", 20, (200, 230, 255), root_point=(0, 0)))
        self.port_input = NumberSelector(150, 125, 120, 35, 1024, 65535, 4723, 4, 4, root_point=(0, 0))
        self.frame.add_child(self.port_input)
        
        # Is Host Checkbox
        self.is_host_check = Checkbox(50, 180, 30, 30, False, "Run as Host (Server + Client)")
        self.frame.add_child(self.is_host_check)
        
        # Connection Status
        self.status_label = TextLabel(300, 225, "Status: Not connected", 20, (255, 100, 100), root_point=(0, 0))
        self.frame.add_child(self.status_label)
        
        # Action Buttons Frame
        button_frame = UiFrame(screen_width-40, 150, 240, 355, root_point=(1, 0))
        button_frame.set_background_color((80, 80, 100))
        self.add_ui_element(button_frame)
        
        # Start Host Button
        self.start_host_btn = Button(120, 20, 200, 30, "Start Host", 24, root_point=(0.5, 0))
        self.start_host_btn.set_on_click(self.start_host)
        button_frame.add_child(self.start_host_btn)
        
        # Connect Button
        self.connect_btn = Button(120, 65, 200, 30, "Connect", 24, root_point=(0.5, 0))
        self.connect_btn.set_on_click(self.connect_as_client)
        button_frame.add_child(self.connect_btn)
        
        self.play_btn = Button(120, 110, 200, 40, "Play", 32, root_point=(0.5, 0))
        self.play_btn.set_on_click(self.go_to_game)
        self.play_btn.set_enabled(False)
        button_frame.add_child(self.play_btn)
        
        # Exit Button
        self.exit_btn = Button(120, 160, 200, 35, "Exit", 24, root_point=(0.5, 0))
        self.exit_btn.set_on_click(lambda: setattr(self.engine, 'running', False))
        button_frame.add_child(self.exit_btn)
        
        # Instructions
        instructions = [
            "INSTRUCTIONS:",
            "1. To host: Check 'Run as Host', click 'Start Host'",
            "2. To join: Enter host IP, click 'Connect'",
            "3. When connected, click 'Play' to start game",
            "",
            "Default host IP: 127.0.0.1 (localhost)",
            "Default port: 4723"
        ]
        
        for i, text in enumerate(instructions):
            color = (255, 255, 200) if i == 0 else (180, 230, 255)
            self.add_ui_element(TextLabel(20, 600 + i * 25, text, 16, color, root_point=(0, 0)))
    
    def start_host(self):
        """Start as host (server + client)"""
        host = self.host_input.get_text()
        port = self.port_input.get_value()
        
        print(f"Starting host on {host}:{port}...")
        self.status_label.set_text("Starting host...")
        self.status_label.color = (255, 200, 100)
        
        # Create network info
        self.network_info = NetworkInfo()
        self.network_info.is_host = True
        
        try:
            # Start host
            self.network_info.host = NetworkHost(host=host, port=port)
            if self.network_info.host.start():
                self.network_info.is_connected = True
                self.network_info.player_id = self.network_info.host.client.client_id
                
                print(f"Host started successfully! Player ID: {self.network_info.player_id}")
                self.status_label.set_text(f"Host running on {host}:{port}")
                self.status_label.color = (100, 255, 100)
                
                # Setup message handler for host
                def handle_host_message(msg):
                    if msg and len(msg) > 0:
                        source = msg[0]
                        if source == "client":
                            # Message from server to our client component
                            message = msg[1]
                            if message.message_type == MessageType.DATA:
                                print(f"Host client received: {message.payload}")
                                # Forward to game scene if it exists
                                if hasattr(self, 'network_info'):
                                    self.network_info.messages.append(message)
                        elif source == "server":
                            # Message from a client to our server component
                            client_id, message = msg[1], msg[2]
                            if message.message_type == MessageType.DATA:
                                print(f"Host server received from {client_id}: {message.payload}")
                                # Broadcast to all other clients (including ourselves)
                                if self.network_info.host:
                                    # Create broadcast message
                                    broadcast_data = {
                                        "message_id": message.message_id,
                                        "message_type": MessageType.DATA.value,
                                        "sender_id": client_id,
                                        "sender_type": message.sender_type.value,
                                        "timestamp": message.timestamp,
                                        "payload": message.payload
                                    }
                                    # Broadcast to all except sender
                                    self.network_info.host.server.broadcast(broadcast_data, exclude=[client_id])
                                    # Also store for our own client
                                    if hasattr(self, 'network_info'):
                                        self.network_info.messages.append(message)
                
                # Store in engine for other scenes to access
                self.engine.network_info = self.network_info
                
                # Enable play button
                self.play_btn.set_enabled(True)
                self.start_host_btn.set_enabled(False)
                self.connect_btn.set_enabled(False)
                
                print("Host setup complete")
            else:
                self.status_label.set_text("Failed to start host")
                self.status_label.color = (255, 100, 100)
                self.network_info = None
                
        except Exception as e:
            print(f"Error starting host: {e}")
            import traceback
            traceback.print_exc()
            self.status_label.set_text(f"Error: {str(e)}")
            self.status_label.color = (255, 100, 100)
            self.network_info = None
    
    def connect_as_client(self):
        """Connect to server as client"""
        host = self.host_input.get_text()
        port = self.port_input.get_value()
        
        print(f"Connecting to {host}:{port}...")
        self.status_label.set_text(f"Connecting to {host}:{port}...")
        self.status_label.color = (255, 200, 100)
        
        # Create network info
        self.network_info = NetworkInfo()
        self.network_info.is_host = False
        
        try:
            # Connect as client
            self.network_info.client = NetworkClient()
            if self.network_info.client.connect(host, port, timeout=5):
                self.network_info.is_connected = True
                self.network_info.player_id = self.network_info.client.client_id
                
                print(f"Connected to server! Player ID: {self.network_info.player_id}")
                self.status_label.set_text(f"Connected to {host}:{port}")
                self.status_label.color = (100, 255, 100)
                
                # Setup message handler for client
                def handle_client_message(msg: NetworkMessage):
                    print(f"Client received: {msg.message_type} - {msg.payload}")
                    self.handle_network_message(msg)
                
                # Register callback for data messages
                self.network_info.client.register_callback(MessageType.DATA, handle_client_message)
                
                # Store in engine
                self.engine.network_info = self.network_info
                
                # Enable play button
                self.play_btn.set_enabled(True)
                self.start_host_btn.set_enabled(False)
                self.connect_btn.set_enabled(False)
                
                print("Client setup complete")
            else:
                self.status_label.set_text("Connection failed")
                self.status_label.color = (255, 100, 100)
                self.network_info = None
                
        except Exception as e:
            print(f"Connection error: {e}")
            self.status_label.set_text(f"Error: {str(e)}")
            self.status_label.color = (255, 100, 100)
            self.network_info = None
    
    def handle_network_message(self, message, sender_id=None):
        """Handle incoming network messages"""
        if isinstance(message, NetworkMessage):
            print(f"Received: {message.message_type} from {sender_id or message.sender_id}")
            if hasattr(self, 'network_info'):
                self.network_info.messages.append(message)
    
    def go_to_game(self):
        """Go to the game scene"""
        if self.network_info and self.network_info.is_connected:
            print("Going to game scene...")
            self.engine.set_scene("InGame")
        else:
            self.status_label.set_text("Not connected!")
            self.status_label.color = (255, 100, 100)
    
    def render(self, renderer):
        renderer.fill_screen(ThemeManager.get_color('background'))
    
    def update(self, dt):
        # Update connection status display
        if self.network_info and self.network_info.is_connected:
            if self.network_info.is_host:
                if self.network_info.host:
                    player_count = self.network_info.host.get_client_count()
                    self.status_label.set_text(f"Host: {player_count} clients connected")
                else:
                    self.status_label.set_text("Host running")
            else:
                if self.network_info.client and self.network_info.client.connected:
                    self.status_label.set_text("Connected to server")
                else:
                    self.status_label.set_text("Disconnected")
        
        return super().update(dt)

class Player:
    def __init__(self, x, y, player_id, name="Player", color=(255, 255, 255)):
        self.x = x
        self.y = y
        self.id = player_id
        self.name = name
        self.width = 40
        self.height = 40
        self.color = color
        self.velocity = [0, 0]
        self.speed = 250
        self.last_update = 0

class InGameScene(Scene):
    def __init__(self, engine: LunaEngine):
        super().__init__(engine)
        self.players = {}  # player_id: Player
        self.local_player = None
        self.chat_messages = []  # Store chat messages
        self.setup_ui()
        self.network_update_timer = 0
        self.network_update_interval = 0.05  # 20 updates per second
        self.last_player_update = 0
        
    def on_enter(self, previous_scene=None):
        print("Entering game scene...")
        
        # Get network info from engine
        if hasattr(self.engine, 'network_info'):
            self.network_info = self.engine.network_info
            
            if self.network_info and self.network_info.is_connected:
                # Create local player
                player_name = f"Player_{self.network_info.player_id[:4]}"
                player_color = (
                    random.randint(100, 255),
                    random.randint(100, 255),
                    random.randint(100, 255)
                )
                
                self.local_player = Player(
                    x=512, y=360,
                    player_id=self.network_info.player_id,
                    name=player_name,
                    color=player_color
                )
                self.players[self.network_info.player_id] = self.local_player
                
                # Send player join message
                self.send_player_join()
                print(f"Local player created: {player_name}")
                
                # If host, send existing players to new clients
                if self.network_info.is_host and self.network_info.host:
                    print("Host: Will broadcast player list to new clients")
            else:
                print("Not connected to network!")
                self.engine.set_scene("main")
        else:
            print("No network info found!")
            self.engine.set_scene("main")
    
    def on_exit(self, next_scene=None):
        # Send player leave message
        if hasattr(self, 'local_player') and self.local_player:
            self.send_player_leave()
        
        # Clear players
        self.players.clear()
        return super().on_exit(next_scene)
    
    def setup_ui(self):
        # Game UI (hidden by default)
        self.ui_frame = UiFrame(512, 300, 400, 375,root_point=(0.5, 0))
        self.ui_frame.set_background_color((60, 60, 80, 200))
        self.ui_frame.add_group('game_ui')
        self.add_ui_element(self.ui_frame)
        
        # UI Title
        self.ui_frame.add_child(TextLabel(200, 20, "Multiplayer Game", 28, (255, 255, 200), root_point=(0.5, 0)))
        
        # Player List
        self.player_list_label = TextLabel(20, 60, "Players:", 22, (200, 230, 255))
        self.ui_frame.add_child(self.player_list_label)
        
        # Chat Display Label
        self.chat_display_label = TextLabel(20, 120, "Chat:", 22, (200, 230, 255))
        self.ui_frame.add_child(self.chat_display_label)
        
        # Chat Scrolling Frame - FIXED: Proper ScrollingFrame setup
        self.chat_scrolling = ScrollingFrame(200, 150, 340, 100, 340, 200, root_point=(0.5, 0))
        self.chat_scrolling.set_background_color((40, 40, 50, 200))
        self.ui_frame.add_child(self.chat_scrolling)
        
        # Chat Input
        self.chat_input = TextBox(20, 280, 250, 35, "Type message...", 16)
        self.ui_frame.add_child(self.chat_input)
        
        # Send Chat Button
        self.send_chat_btn = Button(280, 280, 80, 35, "Send", 16)
        self.send_chat_btn.set_on_click(self.send_chat)
        self.ui_frame.add_child(self.send_chat_btn)
        
        # Disconnect Button
        self.disconnect_btn = Button(200, 320, 120, 40, "Disconnect", 20, root_point=(0.5, 0))
        self.disconnect_btn.set_on_click(self.disconnect)
        self.ui_frame.add_child(self.disconnect_btn)
        
        # Hide UI by default
        self.ui_visible = False
        self.toggle_element_group('game_ui', self.ui_visible)
        
        # Add ESC key handler to toggle UI
        @self.engine.on_event(pg.KEYDOWN)
        def on_keydown(event):
            if event.key == pg.K_ESCAPE:
                self.ui_visible = not self.ui_visible
                self.toggle_element_group('game_ui', self.ui_visible)
            elif event.key == pg.K_RETURN and self.chat_input.has_focus():
                self.send_chat()
    
    def send_player_join(self):
        """Send player join message"""
        if not self.local_player or not hasattr(self, 'network_info'):
            return
        
        join_data = {
            'type': 'player_join',
            'player_id': self.local_player.id,
            'name': self.local_player.name,
            'position': [self.local_player.x, self.local_player.y],
            'color': self.local_player.color
        }
        
        self.send_network_message(join_data)
        print(f"Sent player join: {self.local_player.name}")
    
    def send_player_leave(self):
        """Send player leave message"""
        if not self.local_player or not hasattr(self, 'network_info'):
            return
        
        leave_data = {
            'type': 'player_leave',
            'player_id': self.local_player.id
        }
        
        self.send_network_message(leave_data)
        print(f"Sent player leave: {self.local_player.name}")
    
    def send_player_update(self):
        """Send player position update"""
        if not self.local_player or not hasattr(self, 'network_info'):
            return
        
        # Only send update if position changed significantly
        current_time = time.time()
        if current_time - self.last_player_update < 0.1:  # Limit to 10 updates per second
            return
        
        update_data = {
            'type': 'player_update',
            'player_id': self.local_player.id,
            'position': [self.local_player.x, self.local_player.y],
            'velocity': self.local_player.velocity
        }
        
        self.send_network_message(update_data)
        self.last_player_update = current_time
    
    def send_chat(self):
        """Send chat message"""
        message = self.chat_input.get_text().strip()
        if not message or not hasattr(self, 'network_info'):
            return
        
        chat_data = {
            'type': 'chat',
            'player_id': self.local_player.id,
            'player_name': self.local_player.name,
            'message': message,
            'timestamp': time.time()
        }
        
        self.send_network_message(chat_data)
        
        # Add to local chat immediately
        self.add_chat_message(self.local_player.name, message)
        
        # Clear input
        self.chat_input.set_text("")
    
    def send_network_message(self, data):
        """Send network message based on connection type"""
        if not hasattr(self, 'network_info') or not self.network_info.is_connected:
            return False
        
        try:
            if self.network_info.is_host:
                # As host, send through our client component
                if self.network_info.host and self.network_info.host.client:
                    return self.network_info.host.send_as_host(MessageType.DATA, data)
            else:
                # As client, send directly
                if self.network_info.client and self.network_info.client.connected:
                    return self.network_info.client.send(MessageType.DATA, data)
        except Exception as e:
            print(f"Error sending message: {e}")
            return False
        
        return False
    
    def process_network_messages(self):
        """Process incoming network messages"""
        if not hasattr(self, 'network_info') or not self.network_info.is_connected:
            return
        
        try:
            if self.network_info.is_host:
                # Process host messages
                if self.network_info.host:
                    msg = self.network_info.host.get_message(timeout=0)
                    while msg:
                        source = msg[0]
                        if source == "client":
                            # Message from server to our client
                            message = msg[1]
                            self.handle_network_message(message)
                        elif source == "server":
                            # Message from a client to our server
                            client_id, message = msg[1], msg[2]
                            print(f"Host server received from {client_id}: {message.payload}")
                            
                            # Handle the message locally
                            self.handle_network_message(message, client_id)
                            
                            # If it's a player join/update/chat, broadcast to all except sender
                            if message.message_type == MessageType.DATA:
                                data = message.payload
                                if isinstance(data, dict) and data.get('type') in ['player_join', 'player_update', 'chat']:
                                    # Don't broadcast our own messages back to ourselves
                                    if client_id != self.network_info.player_id:
                                        # Broadcast to all other clients
                                        broadcast_msg = {
                                            "message_id": message.message_id,
                                            "message_type": MessageType.DATA.value,
                                            "sender_id": client_id,
                                            "sender_type": message.sender_type.value,
                                            "timestamp": message.timestamp,
                                            "payload": data
                                        }
                                        self.network_info.host.server.broadcast(broadcast_msg, exclude=[client_id])
                        
                        msg = self.network_info.host.get_message(timeout=0)
            else:
                # Process client messages
                if self.network_info.client:
                    msg = self.network_info.client.get_message(timeout=0)
                    while msg:
                        self.handle_network_message(msg)
                        msg = self.network_info.client.get_message(timeout=0)
        except Exception as e:
            print(f"Error processing messages: {e}")
    
    def handle_network_message(self, message, sender_id=None):
        """Handle a network message"""
        if not isinstance(message, NetworkMessage) or message.message_type != MessageType.DATA:
            return
        
        data = message.payload
        if not isinstance(data, dict):
            return
        
        msg_type = data.get('type')
        player_id = data.get('player_id')
        
        # Handle different message types
        if msg_type == 'player_join':
            # New player joined
            if player_id and player_id != self.local_player.id:
                player_name = data.get('name', f'Player_{player_id[:4]}')
                position = data.get('position', [512, 360])
                color = data.get('color', (255, 255, 255))
                
                if player_id not in self.players:
                    self.players[player_id] = Player(
                        x=position[0], y=position[1],
                        player_id=player_id,
                        name=player_name,
                        color=color
                    )
                    
                    self.add_chat_message("System", f"{player_name} joined the game")
                    print(f"Player joined: {player_name}")
        
        elif msg_type == 'player_leave':
            # Player left
            if player_id in self.players:
                player_name = self.players[player_id].name
                del self.players[player_id]
                self.add_chat_message("System", f"{player_name} left the game")
                print(f"Player left: {player_name}")
        
        elif msg_type == 'player_update':
            # Player position update
            if player_id in self.players and player_id != self.local_player.id:
                position = data.get('position')
                if position:
                    self.players[player_id].x = position[0]
                    self.players[player_id].y = position[1]
                    # Update velocity if provided
                    velocity = data.get('velocity')
                    if velocity:
                        self.players[player_id].velocity = velocity
        
        elif msg_type == 'chat':
            # Chat message
            player_name = data.get('player_name', f'Player_{player_id[:4]}')
            message_text = data.get('message', '')
            self.add_chat_message(player_name, message_text)
            print(f"Chat from {player_name}: {message_text}")
    
    def add_chat_message(self, sender, message):
        """Add chat message to ScrollingFrame"""
        timestamp = time.strftime("%H:%M:%S")
        full_message = f"[{timestamp}] {sender}: {message}"
        
        # Add to our list
        self.chat_messages.append(full_message)
        
        # Keep only last 10 messages
        if len(self.chat_messages) > 10:
            self.chat_messages.pop(0)
        
        # Update the ScrollingFrame
        self.update_chat_scrolling()
    
    def update_chat_scrolling(self):
        """Update the chat ScrollingFrame with current messages"""
        # Clear existing chat labels
        self.chat_scrolling.clear_children()
        
        # Add each message as a TextLabel
        for i, msg in enumerate(self.chat_messages):
            # Calculate position (starting from bottom)
            y_pos = 5 + (len(self.chat_messages) - i - 1) * 25
            
            # Create text label
            chat_label = TextLabel(5, y_pos, msg, 16, (230, 230, 255))
            self.chat_scrolling.add_child(chat_label)
    
    def update_player_movement(self, dt):
        """Update local player movement"""
        if not self.local_player:
            return
        
        keys = pg.key.get_pressed()
        old_velocity = self.local_player.velocity.copy()
        self.local_player.velocity = [0, 0]
        
        if keys[pg.K_w] or keys[pg.K_UP]:
            self.local_player.velocity[1] = -1
        if keys[pg.K_s] or keys[pg.K_DOWN]:
            self.local_player.velocity[1] = 1
        if keys[pg.K_a] or keys[pg.K_LEFT]:
            self.local_player.velocity[0] = -1
        if keys[pg.K_d] or keys[pg.K_RIGHT]:
            self.local_player.velocity[0] = 1
        
        # Normalize diagonal movement
        if self.local_player.velocity[0] != 0 and self.local_player.velocity[1] != 0:
            self.local_player.velocity[0] *= 0.7071
            self.local_player.velocity[1] *= 0.7071
        
        # Only send update if velocity changed significantly
        velocity_changed = (
            abs(old_velocity[0] - self.local_player.velocity[0]) > 0.1 or
            abs(old_velocity[1] - self.local_player.velocity[1]) > 0.1
        )
        
        # Update position
        self.local_player.x += self.local_player.velocity[0] * self.local_player.speed * dt
        self.local_player.y += self.local_player.velocity[1] * self.local_player.speed * dt
        
        # Keep player in bounds
        self.local_player.x = max(20, min(1004, self.local_player.x))
        self.local_player.y = max(20, min(700, self.local_player.y))
        
        # Send update if moving or velocity changed
        if velocity_changed or self.local_player.velocity != [0, 0]:
            self.send_player_update()
    
    def update_player_list(self):
        """Update player list UI"""
        player_text = f"Players ({len(self.players)}):\n"
        for player_id, player in self.players.items():
            is_local = "★ " if player_id == self.local_player.id else "  "
            player_text += f"{is_local}{player.name}\n"
        
        self.player_list_label.set_text(player_text)
    
    def disconnect(self):
        """Disconnect from network and return to main menu"""
        print("Disconnecting...")
        
        # Clean up network connections
        if hasattr(self, 'network_info'):
            if self.network_info.is_host and self.network_info.host:
                self.network_info.host.stop()
            elif self.network_info.client:
                self.network_info.client.disconnect()
        
        # Clear engine network info
        if hasattr(self.engine, 'network_info'):
            self.engine.network_info = None
        
        # Go back to main menu
        self.engine.set_scene("main")
    
    def update(self, dt):
        # Process network messages
        self.process_network_messages()
        
        # Update player movement
        self.update_player_movement(dt)
        
        # Update UI
        self.update_player_list()
        
        return super().update(dt)
    
    def render(self, renderer):
        # Draw background
        renderer.fill_screen((30, 30, 40))
        
        # Draw grid
        grid_color = (50, 50, 60)
        for x in range(0, 1024, 50):
            renderer.draw_line(x, 0, x, 720, grid_color, 1)
        for y in range(0, 720, 50):
            renderer.draw_line(0, y, 1024, y, grid_color, 1)
        
        # Draw center point
        renderer.draw_circle(512, 360, 5, (100, 100, 150))
        
        # Draw all players
        for player_id, player in self.players.items():
            # Draw player body
            renderer.draw_rect(
                player.x - player.width/2,
                player.y - player.height/2,
                player.width,
                player.height,
                player.color
            )
            
            # Draw player name
            renderer.draw_text(str(player.name), player.x, player.y - 30,(255, 255, 255) if player_id != self.local_player.id else (255, 255, 0), FontManager.get_font(None, 16), anchor_point=(0.5, 0.5))
            
            # Draw local player indicator
            if player_id == self.local_player.id:
                renderer.draw_circle(player.x, player.y - 40, 5, (255, 255, 0))
                
                # Draw velocity indicator
                if player.velocity != [0, 0]:
                    end_x = player.x + player.velocity[0] * 30
                    end_y = player.y + player.velocity[1] * 30
                    renderer.draw_line(player.x, player.y, end_x, end_y, (255, 255, 0, 150), 2)
        
        # Draw instructions
        if not self.ui_visible:
            font = pg.font.SysFont("Arial", 18)
            instructions = [
                "WASD or Arrow Keys - Move",
                "ESC - Toggle UI",
                "ENTER - Send chat message",
                "Click Disconnect to leave"
            ]
            
            for i, text in enumerate(instructions):
                text_surface = font.render(text, True, (200, 230, 255))
                renderer.blit(text_surface, (20, 20 + i * 30))
            
            # Draw connection info
            if hasattr(self, 'network_info') and self.network_info:
                mode = "Host" if self.network_info.is_host else "Client"
                info = f"Connected as {mode} | Players: {len(self.players)}"
                info_surface = font.render(info, True, (100, 255, 100))
                renderer.blit(info_surface, (20, 680))
        
        # Draw network status
        font = pg.font.SysFont("Arial", 14)
        if hasattr(self, 'network_info') and self.network_info:
            if self.network_info.is_host:
                if self.network_info.host:
                    client_count = self.network_info.host.get_client_count()
                    status_text = f"Hosting | Clients: {client_count}"
                else:
                    status_text = "Host (disconnected)"
            else:
                if self.network_info.client and self.network_info.client.connected:
                    status_text = "Connected to server"
                else:
                    status_text = "Disconnected"
        else:
            status_text = "No connection"
        
        status_surface = font.render(status_text, True, (200, 200, 200))
        renderer.blit(status_surface, (1024 - status_surface.get_width() - 20, 680))

def main():
    engine = LunaEngine('LunaEngine - Multiplayer Demo', 1024, 720, False)
    
    engine.add_scene('main', MainScene)
    engine.add_scene('InGame', InGameScene)
    engine.set_scene('main')
    
    print("=== LunaEngine Multiplayer Demo ===")
    print("Instructions:")
    print("1. For HOST: Check 'Run as Host', click 'Start Host'")
    print("2. For CLIENT: Enter host IP (127.0.0.1 for local), click 'Connect'")
    print("3. Click 'Play' to enter game")
    print("4. In game: WASD to move, ESC to toggle UI, ENTER to chat")
    
    engine.run()

if __name__ == "__main__":
    main()
About This Example