diff --git a/assets/texturas/texture.png b/assets/texturas/texture.png index 84392054..42ccb287 100644 Binary files a/assets/texturas/texture.png and b/assets/texturas/texture.png differ diff --git a/main.py b/main.py index d7ceb8f7..6e0157c6 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,8 @@ import math import random import time +import base64 +import io from collections import deque import pyglet @@ -17,11 +19,11 @@ # Size of sectors used to ease block loading. SECTOR_SIZE = 16 -WALKING_SPEED = 5 -FLYING_SPEED = 15 +WALKING_SPEED = 8 +FLYING_SPEED = 8 -GRAVITY = 9.8 -MAX_JUMP_HEIGHT = 1.2 # About the height of a block. +GRAVITY = 10 +MAX_JUMP_HEIGHT = 1.3 # About the height of a block. # To derive the formula for calculating jump speed, first solve # v_t = v_0 + a * t # for the time at which you achieve maximum height, where a is the acceleration @@ -85,14 +87,17 @@ def tex_coords(top, bottom, side): # Blocos Criados pelo joao MADEIRA = tex_coords((2, 2), (2, 2), (1, 2)) +TERRA = tex_coords((0, 1), (0, 1), (0, 1)) MADEIRA_COPA = tex_coords((3, 2), (3, 2), (3, 2)) CARVAO = tex_coords((3, 0), (3, 0), (3, 0)) DIAMANTE = tex_coords((3, 1), (3, 1), (3, 1)) GRAVEL = tex_coords((0, 3), (0, 3), (0, 3)) TNT = tex_coords((3, 3), (2, 3), (1, 3)) -WATER = tex_coords((4, 0), (4, 0), (4, 0)) +BEDROCK = tex_coords((4, 3), (4, 3), (4, 3)) + +CRAFTING_TABLE = tex_coords((4, 0), (4, 2), (4, 1)) # Textura para TNT ativada (piscando) - usando textura mais brilhante -TNT_ACTIVATED = tex_coords((3, 3), (2, 3), (1, 3)) # Usando GRAVEL como base para efeito visual +TNT_ACTIVATED = tex_coords((6, 3), (6, 3), (6, 3)) # Usando GRAVEL como base para efeito visual FACES = [ ( 0, 1, 0), @@ -254,11 +259,12 @@ def _generate_lake(self, center_x, center_z, base_y, radius, depth): # Adicionar água começando na altura da grama (base_y) # A superfície da água fica exatamente no mesmo nível onde estava a grama # As camadas abaixo (base_y - 1, base_y - 2, etc.) criam profundidade - for water_layer in xrange(local_depth): - water_y = base_y - water_layer - water_pos = (x, water_y, z) - # Adicionar água: primeira camada em base_y (altura da grama), outras abaixo - self.add_block(water_pos, WATER, immediate=False) + # NOTA: WATER foi removido, então este código está desabilitado + # for water_layer in xrange(local_depth): + # water_y = base_y - water_layer + # water_pos = (x, water_y, z) + # # Adicionar água: primeira camada em base_y (altura da grama), outras abaixo + # self.add_block(water_pos, WATER, immediate=False) def _initialize(self): """ Initialize the world by placing all the blocks. @@ -273,25 +279,26 @@ def _initialize(self): for z in xrange(-n, n + 1, s): # create a layer stone an grass everywhere. self.add_block((x, y - 2, z), GRASS, immediate=False) - # 30% de chance de ser GRAVEL, 70% de ser STONE na camada abaixo da grama - if random.random() < 0.30: - self.add_block((x, y - 3, z), GRAVEL, immediate=False) - else: - self.add_block((x, y - 3, z), STONE, immediate=False) + # TERRA na camada abaixo da grama (substituindo a primeira camada de pedra) + self.add_block((x, y - 3, z), TERRA, immediate=False) # Criar mais camadas de STONE em profundidade (reduzido para otimização) for depth in xrange(-4, -7, -1): # De y -4 até y -6 (3 camadas) position = (x, depth, z) - # 17.5% de chance de ser CARVÃO ou DIAMANTE (média entre 15% e 20%) - if random.random() < 0.175: - # 50% de chance de ser CARVÃO, 50% de ser DIAMANTE - rare_block = CARVAO if random.random() < 0.5 else DIAMANTE - self.add_block(position, rare_block, immediate=False) + rand = random.random() + # 5% de chance de ser DIAMANTE + if rand < 0.05: + self.add_block(position, DIAMANTE, immediate=False) + # 12.5% de chance de ser CARVÃO (dentro dos 17.5% restantes) + elif rand < 0.175: + self.add_block(position, CARVAO, immediate=False) else: self.add_block(position, STONE, immediate=False) + # Camada de BEDROCK (1 bloco abaixo do mundo, não pode ser quebrada) + self.add_block((x, y - 7, z), BEDROCK, immediate=False) if x in (-n, n) or z in (-n, n): # create outer walls. for dy in xrange(-2, 3): - self.add_block((x, y + dy, z), STONE, immediate=False) + self.add_block((x, y + dy, z), BEDROCK, immediate=False) # generate the hills randomly o = n - 10 @@ -331,8 +338,9 @@ def _initialize(self): # self._generate_lake(lake_x, lake_z, lake_base_y, lake_radius, lake_depth) # Gerar TNT aleatoriamente no mapa (4% de chance) - for x in xrange(-n + 5, n - 4, s): - for z in xrange(-n + 5, n - 4, s): + # Usar step = 1 (s foi modificado no loop de colinas) + for x in xrange(-n + 5, n - 4, 1): + for z in xrange(-n + 5, n - 4, 1): # 4% de chance de gerar TNT nesta posição if random.random() < 0.04: # Colocar TNT na superfície (y - 1), acima da grama @@ -437,6 +445,10 @@ def remove_block(self, position, immediate=True): Whether or not to immediately remove block from canvas. """ + # Verificar se é BEDROCK - não pode ser quebrada + if position in self.world and self.world[position] == BEDROCK: + return # Não permite remover BEDROCK + del self.world[position] self.sectors[sectorize(position)].remove(position) if immediate: @@ -606,8 +618,24 @@ def process_entire_queue(self): class Window(pyglet.window.Window): def __init__(self, *args, **kwargs): + # Extrair game_mode dos kwargs antes de passar para super + self.game_mode = kwargs.pop('game_mode', 'creative') # Padrão: creative super(Window, self).__init__(*args, **kwargs) + # Inicializar label como None primeiro + self.label = None + + # Criar label logo após, garantindo que height esteja disponível + try: + self.label = pyglet.text.Label('', font_name='Arial', font_size=18, + x=10, y=self.height - 10, anchor_x='left', anchor_y='top', + color=(0, 0, 0, 255)) + except: + # Se falhar, criar com valores padrão + self.label = pyglet.text.Label('', font_name='Arial', font_size=18, + x=10, y=590, anchor_x='left', anchor_y='top', + color=(0, 0, 0, 255)) + # Whether or not the window exclusively captures the mouse. self.exclusive = False @@ -673,11 +701,79 @@ def __init__(self, *args, **kwargs): # Rastrear TNTs que estão piscando/explodindo # Formato: {position: {'start_time': float, 'blink_state': bool}} self.active_tnt = {} + + # Sistema de Crafting + self.crafting_open = False # Se o menu de crafting está aberto + # Converter texturas em tuplas para usar como chaves de dicionário + def texture_to_tuple(tex): + """ Converte uma textura (lista de floats) em tupla para usar como chave de dicionário. + """ + # tex_coords retorna uma lista plana de floats, então apenas convertemos para tupla + return tuple(tex) + + self.crafting_recipes = { + # Receitas: {resultado (tupla): {'ingredients': [(texture, quantidade), ...], 'result_count': int, 'texture': lista original}} + texture_to_tuple(MADEIRA): { + 'ingredients': [(MADEIRA_COPA, 1)], + 'result_count': 4, + 'texture': MADEIRA + }, + texture_to_tuple(BRICK): { + 'ingredients': [(STONE, 4)], + 'result_count': 4, + 'texture': BRICK + }, + # Adicionar mais receitas conforme necessário + } + + # Sistema de Survival + if self.game_mode == 'survival': + self.health = 20 # Vida máxima: 20 (como Minecraft) + self.max_health = 20 + # Inventário/hotbar (9 slots) + self.inventory = { + 0: {'texture': CRAFTING_TABLE, 'count': 1}, # Crafting table no primeiro slot + 1: None, 2: None, 3: None, 4: None, + 5: None, 6: None, 7: None, 8: None + } + self.hotbar_selected = 0 # Slot selecionado na hotbar + # Drops coletados (itens no chão) + self.drops = [] # Lista de drops: [(position, texture, pickup_time)] + # Mobs + self.mobs = [] # Lista de mobs: [{'type': 'pig'|'cow', 'position': (x,y,z), 'velocity': (dx,dy,dz)}] + # Tempo do dia (0.0 = meio-dia, 0.5 = meia-noite) + self.day_time = 0.0 # Ciclo de 0 a 1 (24 horas do jogo = 20 minutos reais) + self.day_speed = 1.0 / (20 * 60 * TICKS_PER_SEC) # 20 minutos = 1 ciclo + else: + # Modo Creative: sem vida, blocos infinitos + self.health = None + self.max_health = None + self.inventory = None + self.hotbar_selected = 0 + self.drops = [] + self.mobs = [] + self.day_time = 0.0 + self.day_speed = 1.0 / (20 * 60 * TICKS_PER_SEC) + + # Sistema de Chat + self.chat_open = False + self.chat_message = "" # Mensagem atual sendo digitada + self.chat_history = [] # Histórico de mensagens (máximo de 10 mensagens) + self.player_name = "Steve" + self.max_chat_history = 10 # Número máximo de mensagens no histórico + + # Label já foi criado no início do __init__ + + # Criar reticle inicial + x, y = self.width // 2, self.height // 2 + n = 10 + self.reticle = pyglet.graphics.vertex_list(4, + ('v2i', (x - n, y, x + n, y, x, y - n, x, y + n)) + ) - # The label that is displayed in the top left of the canvas. - self.label = pyglet.text.Label('', font_name='Arial', font_size=18, - x=10, y=self.height - 10, anchor_x='left', anchor_y='top', - color=(0, 0, 0, 255)) + # Gerar mobs iniciais no modo Survival + if self.game_mode == 'survival': + self.generate_initial_mobs() # This call schedules the `update()` method to be called # TICKS_PER_SEC. This is the main game event loop. @@ -722,9 +818,11 @@ def explode_tnt(self, dt, position): distance = math.sqrt(dx**2 + dy**2 + dz**2) if distance <= radius: block_pos = (x + dx, y + dy, z + dz) - # Adicionar à lista se existir + # Adicionar à lista se existir e não for BEDROCK if block_pos in self.model.world: - blocks_to_remove.append(block_pos) + # BEDROCK não pode ser destruída por explosões + if self.model.world[block_pos] != BEDROCK: + blocks_to_remove.append(block_pos) # Remover todos os blocos coletados for block_pos in blocks_to_remove: @@ -808,6 +906,176 @@ def update_tnt_blinking(self): if pos in self.active_tnt: del self.active_tnt[pos] + def create_drop(self, position, texture): + """ Cria um drop quando um bloco é quebrado no modo Survival. + + Parameters + ---------- + position : tuple of len 3 + Posição onde o bloco foi quebrado. + texture : list + Textura do bloco quebrado (será o item do drop). + """ + if self.game_mode != 'survival': + return + + # Criar drop na posição do bloco quebrado + drop = { + 'position': position, + 'texture': texture, + 'pickup_time': time.time() + 0.5, # Pode ser coletado após 0.5 segundos + 'velocity': (random.uniform(-0.1, 0.1), 0.1, random.uniform(-0.1, 0.1)) # Pequeno impulso + } + self.drops.append(drop) + + def collect_drops(self): + """ Coleta drops próximos ao jogador no modo Survival. + """ + if self.game_mode != 'survival': + return + + player_x, player_y, player_z = self.position + pickup_radius = 2.0 # Raio de coleta + + drops_to_remove = [] + for i, drop in enumerate(self.drops): + drop_x, drop_y, drop_z = drop['position'] + distance = math.sqrt((player_x - drop_x)**2 + (player_y - drop_y)**2 + (player_z - drop_z)**2) + + # Verificar se pode ser coletado (tempo passou e está próximo) + if time.time() >= drop['pickup_time'] and distance <= pickup_radius: + # Adicionar ao inventário + self.add_to_inventory(drop['texture']) + drops_to_remove.append(i) + + # Remover drops coletados (em ordem reversa para não quebrar índices) + for i in reversed(drops_to_remove): + self.drops.pop(i) + + def add_to_inventory(self, texture): + """ Adiciona um item ao inventário no modo Survival. + + Parameters + ---------- + texture : list + Textura do item a ser adicionado. + """ + if self.game_mode != 'survival': + return + + # Procurar slot vazio ou com o mesmo item + for slot in xrange(9): + if self.inventory[slot] is None: + self.inventory[slot] = {'texture': texture, 'count': 1} + return + elif self.inventory[slot]['texture'] == texture: + self.inventory[slot]['count'] += 1 + return + + # Se inventário cheio, descartar item + + def use_inventory_item(self, slot): + """ Usa um item do inventário (coloca bloco) no modo Survival. + + Parameters + ---------- + slot : int + Slot do inventário a ser usado (0-8). + """ + if self.game_mode != 'survival': + return True # Creative: sempre pode colocar + + if slot not in self.inventory or self.inventory[slot] is None: + return False # Slot vazio + + # Usar um item do slot + self.inventory[slot]['count'] -= 1 + if self.inventory[slot]['count'] <= 0: + self.inventory[slot] = None + + return True + + def generate_initial_mobs(self): + """ Gera mobs iniciais no mundo no modo Survival. + """ + if self.game_mode != 'survival': + return + + num_mobs = 20 # Número de mobs a gerar + spawn_radius = 50 # Raio de spawn ao redor do jogador + + for _ in xrange(num_mobs): + # Posição aleatória ao redor do spawn + angle = random.uniform(0, 2 * math.pi) + distance = random.uniform(10, spawn_radius) + x = math.cos(angle) * distance + z = math.sin(angle) * distance + y = 0 # Altura inicial + + # Encontrar altura do chão + for check_y in xrange(10, -10, -1): + if (int(x), check_y - 1, int(z)) in self.model.world: + y = check_y + break + + # Tipo aleatório de mob + mob_type = random.choice(['pig', 'cow']) + + mob = { + 'type': mob_type, + 'position': (x, y, z), + 'velocity': (random.uniform(-0.02, 0.02), 0, random.uniform(-0.02, 0.02)), + 'rotation': random.uniform(0, 360), # Rotação aleatória + 'change_direction_time': time.time() + random.uniform(2, 5) # Mudar direção periodicamente + } + self.mobs.append(mob) + + def update_mobs(self, dt): + """ Atualiza posição e comportamento dos mobs. + + Parameters + ---------- + dt : float + Delta time desde a última atualização. + """ + if self.game_mode != 'survival': + return + + current_time = time.time() + + for mob in self.mobs: + x, y, z = mob['position'] + dx, dy, dz = mob['velocity'] + + # Mudar direção periodicamente + if current_time >= mob['change_direction_time']: + angle = random.uniform(0, 2 * math.pi) + speed = random.uniform(0.01, 0.03) + mob['velocity'] = (math.cos(angle) * speed, 0, math.sin(angle) * speed) + mob['change_direction_time'] = current_time + random.uniform(2, 5) + + # Atualizar posição + new_x = x + dx + new_z = z + dz + + # Verificar colisão com blocos (simplificado - apenas verificar altura) + ground_y = y + for check_y in xrange(int(y) + 2, int(y) - 3, -1): + if (int(new_x), check_y, int(new_z)) in self.model.world: + ground_y = check_y + 1 + break + + # Atualizar posição do mob + mob['position'] = (new_x, ground_y, new_z) + + # Manter mobs dentro de um raio do jogador + player_x, player_y, player_z = self.position + distance = math.sqrt((new_x - player_x)**2 + (new_z - player_z)**2) + if distance > 100: + # Teleportar mob de volta para perto do jogador + angle = random.uniform(0, 2 * math.pi) + mob['position'] = (player_x + math.cos(angle) * 20, ground_y, player_z + math.sin(angle) * 20) + def get_sight_vector(self): """ Returns the current line of sight vector indicating the direction the player is looking. @@ -877,6 +1145,20 @@ def update(self, dt): # Atualizar piscar das TNTs ativas self.update_tnt_blinking() + # No modo Survival: coletar drops, atualizar mobs e ciclo dia/noite + if self.game_mode == 'survival': + self.collect_drops() + self.update_mobs(dt) + # Atualizar tempo do dia + self.day_time += self.day_speed * dt + if self.day_time >= 1.0: + self.day_time -= 1.0 + else: + # Modo Creative também tem ciclo dia/noite + self.day_time += self.day_speed * dt + if self.day_time >= 1.0: + self.day_time -= 1.0 + self.model.process_queue() sector = sectorize(self.position) if sector != self.sector: @@ -958,10 +1240,9 @@ def collide(self, position, height): op[i] += face[i] if tuple(op) not in self.model.world: continue - # Ignorar colisão com WATER (água não é sólida) + # Verificar colisão com blocos block_texture = self.model.world.get(tuple(op)) - if block_texture == WATER: - continue + # Água não é sólida, mas como WATER foi removido, não há verificação aqui p[i] -= (d - pad) * face[i] if face == (0, -1, 0) or face == (0, 1, 0): # You are colliding with the ground or ceiling, so stop @@ -987,6 +1268,11 @@ def on_mouse_press(self, x, y, button, modifiers): mouse button was clicked. """ + # Se o menu de crafting estiver aberto, processar cliques no menu + if self.crafting_open: + self.handle_crafting_click(x, y, button) + return + if self.exclusive: vector = self.get_sight_vector() block, previous = self.model.hit_test(self.position, vector) @@ -994,14 +1280,32 @@ def on_mouse_press(self, x, y, button, modifiers): ((button == mouse.LEFT) and (modifiers & key.MOD_CTRL)): # ON OSX, control + left click = right click. if previous: - self.model.add_block(previous, self.block) + # No modo Survival, verificar se tem o bloco no inventário + if self.game_mode == 'survival': + # Verificar se há bloco selecionado e se está no inventário + if self.block is not None and self.use_inventory_item(self.hotbar_selected): + self.model.add_block(previous, self.block) + else: + # Modo Creative: blocos infinitos + if self.block is not None: + self.model.add_block(previous, self.block) elif button == pyglet.window.mouse.LEFT and block: # Verificar se é TNT - se for, ativar em vez de quebrar texture = self.model.world.get(block) if texture == TNT: # Ativar TNT (fazer piscar e explodir após 3 segundos) self.activate_tnt(block) + elif texture == CRAFTING_TABLE: + # Abrir menu de crafting + self.crafting_open = True + self.set_exclusive_mouse(False) # Liberar mouse para usar o menu + elif texture == BEDROCK: + # BEDROCK não pode ser quebrada + pass # Não fazer nada else: + # No modo Survival, gerar drop ao quebrar bloco + if self.game_mode == 'survival': + self.create_drop(block, texture) # Permitir quebrar qualquer outro bloco, incluindo STONE self.model.remove_block(block) else: @@ -1050,13 +1354,109 @@ def on_key_press(self, symbol, modifiers): if self.dy == 0: self.dy = JUMP_SPEED elif symbol == key.ESCAPE: - self.set_exclusive_mouse(False) + if self.chat_open: + # Fechar chat sem enviar mensagem + self.chat_open = False + self.chat_message = "" + if not self.crafting_open: + self.set_exclusive_mouse(True) + elif self.crafting_open: + # Fechar menu de crafting + self.crafting_open = False + self.set_exclusive_mouse(True) + else: + self.set_exclusive_mouse(False) elif symbol == key.TAB: self.flying = not self.flying + elif symbol == key.T: + # Abrir/fechar chat + if not self.chat_open: + self.chat_open = True + self.chat_message = "" + self.set_exclusive_mouse(False) # Liberar mouse para digitar + else: + # Se já estiver aberto, enviar mensagem se houver texto + if self.chat_message.strip(): + self.send_chat_message(self.chat_message.strip()) + self.chat_open = False + self.chat_message = "" + if not self.crafting_open: + self.set_exclusive_mouse(True) # Recapturar mouse + elif symbol == key.BACKSPACE and self.chat_open: + # Backspace no chat + if len(self.chat_message) > 0: + self.chat_message = self.chat_message[:-1] + elif symbol == key.ENTER and self.chat_open: + # Enter para enviar mensagem + if self.chat_message.strip(): + self.send_chat_message(self.chat_message.strip()) + self.chat_open = False + self.chat_message = "" + if not self.crafting_open: + self.set_exclusive_mouse(True) elif symbol in self.block_map: - # Selecionar bloco diretamente pelo mapeamento da tecla - self.block = self.block_map[symbol] + # No modo Survival, selecionar da hotbar + if self.game_mode == 'survival': + # Mapear tecla para slot da hotbar (1-9 -> 0-8) + slot = (symbol - self.num_keys[0]) % 9 + self.hotbar_selected = slot + # Selecionar bloco do inventário se houver + if self.inventory[slot] is not None: + self.block = self.inventory[slot]['texture'] + else: + self.block = None # Slot vazio + else: + # Modo Creative: selecionar bloco diretamente (blocos infinitos) + self.block = self.block_map[symbol] + self.hotbar_selected = (symbol - self.num_keys[0]) % 9 # Atualizar hotbar visual + def on_text(self, text): + """ Captura texto digitado quando o chat está aberto. + + Parameters + ---------- + text : str + Texto digitado pelo usuário. + """ + if not self.chat_open: + return + + # Processar caracteres especiais + if text == '\r' or text == '\n': # Enter + if self.chat_message.strip(): + self.send_chat_message(self.chat_message.strip()) + self.chat_open = False + self.chat_message = "" + if not self.crafting_open: + self.set_exclusive_mouse(True) + elif text == '\b': # Backspace + if len(self.chat_message) > 0: + self.chat_message = self.chat_message[:-1] + elif text == '\x1b': # ESC (já tratado em on_key_press, mas por segurança) + self.chat_open = False + self.chat_message = "" + if not self.crafting_open: + self.set_exclusive_mouse(True) + else: + # Adicionar caractere à mensagem (limitar tamanho) + if len(self.chat_message) < 100: # Limite de 100 caracteres + self.chat_message += text + + def send_chat_message(self, message): + """ Adiciona uma mensagem ao histórico do chat. + + Parameters + ---------- + message : str + Mensagem a ser enviada. + """ + formatted_message = f"{self.player_name}: {message}" + self.chat_history.append(formatted_message) + + # Limitar histórico ao máximo de mensagens + if len(self.chat_history) > self.max_chat_history: + self.chat_history.pop(0) # Remover mensagem mais antiga + def on_key_release(self, symbol, modifiers): """ Called when the player releases a key. See pyglet docs for key mappings. @@ -1082,10 +1482,15 @@ def on_resize(self, width, height): """ Called when the window is resized to a new `width` and `height`. """ - # label - self.label.y = height - 10 - # reticle - if self.reticle: + # label - criar se não existir, ou atualizar se existir + if not hasattr(self, 'label') or self.label is None: + self.label = pyglet.text.Label('', font_name='Arial', font_size=18, + x=10, y=height - 10, anchor_x='left', anchor_y='top', + color=(0, 0, 0, 255)) + else: + self.label.y = height - 10 + # reticle - garantir que existe antes de usar + if hasattr(self, 'reticle') and self.reticle: self.reticle.delete() x, y = self.width // 2, self.height // 2 n = 10 @@ -1126,17 +1531,348 @@ def set_3d(self): x, y, z = self.position glTranslatef(-x, -y, -z) + def draw_mobs(self): + """ Desenha os mobs no mundo (modo Survival). + """ + if self.game_mode != 'survival': + return + + for mob in self.mobs: + x, y, z = mob['position'] + + # Desenhar mob como um cubo colorido + # Pig = rosa, Cow = marrom + if mob['type'] == 'pig': + glColor3f(1.0, 0.7, 0.8) # Rosa + else: # cow + glColor3f(0.6, 0.4, 0.2) # Marrom + + # Desenhar cubo representando o mob + vertex_data = cube_vertices(x, y, z, 0.3) # Mobs são menores que blocos + pyglet.graphics.draw(24, GL_QUADS, ('v3f/static', vertex_data)) + + def texture_to_tuple(self, tex): + """ Converte uma textura (lista de floats) em tupla para comparação. + """ + # tex_coords retorna uma lista plana de floats, então apenas convertemos para tupla + return tuple(tex) + + def can_craft(self, recipe): + """ Verifica se o jogador tem os ingredientes necessários para craftar. + + Parameters + ---------- + recipe : dict + Receita com 'ingredients' e 'result_count'. + + Returns + ------- + can_craft : bool + True se pode craftar, False caso contrário. + """ + if self.game_mode != 'survival': + return True # Creative pode craftar qualquer coisa + + # Contar itens no inventário (usar tuplas como chaves) + inventory_items = {} + for slot in self.inventory.values(): + if slot is not None: + texture = slot['texture'] + texture_key = self.texture_to_tuple(texture) + count = slot['count'] + if texture_key not in inventory_items: + inventory_items[texture_key] = 0 + inventory_items[texture_key] += count + + # Verificar se tem todos os ingredientes + for ingredient_texture, required_count in recipe['ingredients']: + ingredient_key = self.texture_to_tuple(ingredient_texture) + available = inventory_items.get(ingredient_key, 0) + if available < required_count: + return False + + return True + + def craft_item(self, result_texture, recipe): + """ Crafta um item usando os ingredientes do inventário. + + Parameters + ---------- + result_texture : list + Textura do item resultante. + recipe : dict + Receita com 'ingredients' e 'result_count'. + + Returns + ------- + success : bool + True se craftou com sucesso, False caso contrário. + """ + if self.game_mode != 'survival': + # Creative: adicionar item diretamente + self.add_to_inventory(result_texture) + return True + + # Verificar se pode craftar + if not self.can_craft(recipe): + return False + + # Remover ingredientes do inventário + for ingredient_texture, required_count in recipe['ingredients']: + remaining = required_count + # Procurar e remover dos slots + for slot_id in xrange(9): + if self.inventory[slot_id] is not None: + # Comparar texturas convertendo para tupla + slot_texture_key = self.texture_to_tuple(self.inventory[slot_id]['texture']) + ingredient_key = self.texture_to_tuple(ingredient_texture) + if slot_texture_key == ingredient_key: + to_remove = min(remaining, self.inventory[slot_id]['count']) + self.inventory[slot_id]['count'] -= to_remove + remaining -= to_remove + if self.inventory[slot_id]['count'] <= 0: + self.inventory[slot_id] = None + if remaining <= 0: + break + + # Adicionar resultado ao inventário + for _ in xrange(recipe['result_count']): + self.add_to_inventory(result_texture) + + return True + + def handle_crafting_click(self, x, y, button): + """ Processa cliques no menu de crafting. + + Parameters + ---------- + x, y : int + Coordenadas do clique. + button : int + Botão do mouse pressionado. + """ + if button != mouse.LEFT: + return + + width, height = self.get_size() + recipe_y = height - 150 + recipe_spacing = 50 + craft_button_x = width // 2 + 100 + craft_button_width = 80 + craft_button_height = 30 + + # Verificar clique em cada botão de craftar + recipe_index = 0 + for result_texture_tuple, recipe in self.crafting_recipes.items(): + recipe_y_pos = recipe_y - (recipe_index * recipe_spacing) + + # Verificar se clicou no botão + if (craft_button_x <= x <= craft_button_x + craft_button_width and + recipe_y_pos - craft_button_height // 2 <= y <= recipe_y_pos + craft_button_height // 2): + # Tentar craftar (usar a textura original do dicionário) + if self.can_craft(recipe): + self.craft_item(recipe['texture'], recipe) + break + + recipe_index += 1 + + def draw_crafting_menu(self): + """ Desenha o menu de crafting. + """ + if not self.crafting_open: + return + + self.set_2d() + width, height = self.get_size() + + # Fundo semi-transparente + glColor4f(0.1, 0.1, 0.1, 0.9) + pyglet.graphics.draw(4, GL_QUADS, + ('v2f', (0, 0, width, 0, width, height, 0, height))) + + # Título + title_label = pyglet.text.Label( + 'Crafting Table', + font_name='Arial', + font_size=32, + x=width // 2, + y=height - 50, + anchor_x='center', + anchor_y='center', + color=(255, 255, 255, 255) + ) + title_label.draw() + + # Lista de receitas + recipe_y = height - 150 + recipe_spacing = 50 + recipe_index = 0 + + for result_texture_tuple, recipe in self.crafting_recipes.items(): + recipe_y_pos = recipe_y - (recipe_index * recipe_spacing) + + # Nome do item (usar a textura original do dicionário) + result_texture = recipe['texture'] + item_name = self.get_item_name(result_texture) + can_craft = self.can_craft(recipe) + item_color = (200, 200, 200, 255) if can_craft else (100, 100, 100, 255) + + recipe_label = pyglet.text.Label( + f'{item_name} x{recipe["result_count"]}', + font_name='Arial', + font_size=18, + x=width // 2 - 200, + y=recipe_y_pos, + anchor_x='left', + anchor_y='center', + color=item_color + ) + recipe_label.draw() + + # Botão de craftar + craft_button_x = width // 2 + 100 + craft_button_y = recipe_y_pos + craft_button_width = 80 + craft_button_height = 30 + + # Fundo do botão + if can_craft: + glColor4f(0.2, 0.6, 0.2, 0.8) + else: + glColor4f(0.3, 0.3, 0.3, 0.8) + + pyglet.graphics.draw(4, GL_QUADS, + ('v2f', (craft_button_x, craft_button_y - craft_button_height // 2, + craft_button_x + craft_button_width, craft_button_y - craft_button_height // 2, + craft_button_x + craft_button_width, craft_button_y + craft_button_height // 2, + craft_button_x, craft_button_y + craft_button_height // 2))) + + # Texto do botão + button_label = pyglet.text.Label( + 'Craftar' if can_craft else 'Sem itens', + font_name='Arial', + font_size=14, + x=craft_button_x + craft_button_width // 2, + y=craft_button_y, + anchor_x='center', + anchor_y='center', + color=(255, 255, 255, 255) + ) + button_label.draw() + + recipe_index += 1 + + # Instrução para fechar + close_label = pyglet.text.Label( + 'Pressione ESC para fechar', + font_name='Arial', + font_size=14, + x=width // 2, + y=50, + anchor_x='center', + anchor_y='center', + color=(150, 150, 150, 255) + ) + close_label.draw() + + def get_item_name(self, texture): + """ Retorna o nome do item baseado na textura. + + Parameters + ---------- + texture : list + Textura do item. + + Returns + ------- + name : str + Nome do item. + """ + if texture == GRASS: + return 'Grama' + elif texture == STONE: + return 'Pedra' + elif texture == MADEIRA: + return 'Madeira' + elif texture == MADEIRA_COPA: + return 'Madeira Copa' + elif texture == BRICK: + return 'Tijolo' + elif texture == CARVAO: + return 'Carvão' + elif texture == DIAMANTE: + return 'Diamante' + elif texture == GRAVEL: + return 'Cascalho' + elif texture == SAND: + return 'Areia' + elif texture == TNT: + return 'TNT' + elif texture == CRAFTING_TABLE: + return 'Mesa de Crafting' + else: + return 'Item' + + def get_sky_color(self): + """ Retorna a cor do céu baseada no tempo do dia. + + Returns + ------- + color : tuple of len 4 + Cor RGBA do céu. + """ + # day_time: 0.0 = meio-dia, 0.25 = pôr do sol, 0.5 = meia-noite, 0.75 = nascer do sol + # Calcular brilho baseado no tempo do dia + if self.day_time < 0.25: + # Dia (0.0 a 0.25) + brightness = 1.0 - (self.day_time / 0.25) * 0.3 + elif self.day_time < 0.5: + # Noite (0.25 a 0.5) + brightness = 0.2 + ((self.day_time - 0.25) / 0.25) * 0.1 + elif self.day_time < 0.75: + # Noite para dia (0.5 a 0.75) + brightness = 0.3 - ((self.day_time - 0.5) / 0.25) * 0.1 + else: + # Dia (0.75 a 1.0) + brightness = 0.2 + ((self.day_time - 0.75) / 0.25) * 0.8 + + # Cor do céu (azul durante o dia, escuro à noite) + r = 0.5 * brightness + g = 0.69 * brightness + b = 1.0 * brightness + return (r, g, b, 1.0) + def on_draw(self): """ Called by pyglet to draw the canvas. """ + # Atualizar cor do céu baseado no ciclo dia/noite + sky_color = self.get_sky_color() + glClearColor(*sky_color) + + # Se o menu de crafting estiver aberto, desenhar apenas o menu + if self.crafting_open: + self.clear() + self.draw_crafting_menu() + return + self.clear() self.set_3d() glColor3d(1, 1, 1) self.model.batch.draw() + # Desenhar mobs no modo Survival + if self.game_mode == 'survival': + self.draw_mobs() self.draw_focused_block() self.set_2d() self.draw_label() + # Desenhar elementos do modo Survival + if self.game_mode == 'survival': + self.draw_health() + # Hotbar é desenhada em ambos os modos + self.draw_hotbar() + self.draw_selected_block_name() + self.draw_chat() self.draw_reticle() def draw_focused_block(self): @@ -1158,6 +1894,8 @@ def draw_label(self): """ Draw the label in the top left of the screen. """ + if not hasattr(self, 'label') or self.label is None: + return x, y, z = self.position self.label.text = '%02d (%.2f, %.2f, %.2f) %d / %d' % ( pyglet.clock.get_fps(), x, y, z, @@ -1168,8 +1906,241 @@ def draw_reticle(self): """ Draw the crosshairs in the center of the screen. """ + if not hasattr(self, 'reticle') or self.reticle is None: + return glColor3d(0, 0, 0) self.reticle.draw(GL_LINES) + + def draw_hotbar(self): + """ Desenha a hotbar (Survival mostra inventário, Creative mostra seleção de blocos). + """ + + self.set_2d() + width, height = self.get_size() + hotbar_y = 20 + hotbar_width = 180 + hotbar_height = 22 + slot_width = 20 + slot_height = 20 + start_x = (width - hotbar_width) // 2 + + # Desenhar fundo da hotbar + glColor4f(0.2, 0.2, 0.2, 0.8) + pyglet.graphics.draw(4, GL_QUADS, + ('v2f', (start_x, hotbar_y, start_x + hotbar_width, hotbar_y, + start_x + hotbar_width, hotbar_y + hotbar_height, + start_x, hotbar_y + hotbar_height))) + + # Desenhar slots + for i in xrange(9): + slot_x = start_x + 2 + i * (slot_width + 2) + slot_y = hotbar_y + 1 + + # Destaque do slot selecionado + if i == self.hotbar_selected: + glColor4f(1.0, 1.0, 1.0, 0.5) + pyglet.graphics.draw(4, GL_QUADS, + ('v2f', (slot_x - 1, slot_y - 1, slot_x + slot_width + 1, slot_y - 1, + slot_x + slot_width + 1, slot_y + slot_height + 1, + slot_x - 1, slot_y + slot_height + 1))) + + # Borda do slot + glColor4f(0.5, 0.5, 0.5, 1.0) + pyglet.graphics.draw(4, GL_LINE_LOOP, + ('v2f', (slot_x, slot_y, slot_x + slot_width, slot_y, + slot_x + slot_width, slot_y + slot_height, + slot_x, slot_y + slot_height))) + + # Item no slot + if self.game_mode == 'survival': + if self.inventory[i] is not None: + # Mostrar contagem do item + count_label = pyglet.text.Label( + str(self.inventory[i]['count']), + font_name='Arial', + font_size=10, + x=slot_x + slot_width - 2, + y=slot_y + 2, + anchor_x='right', + anchor_y='bottom', + color=(255, 255, 255, 255) + ) + count_label.draw() + else: + # Modo Creative: mostrar bloco selecionado + if i < len(self.num_keys): + block_texture = self.block_map.get(self.num_keys[i]) + if block_texture == self.block: + # Slot selecionado - mostrar indicador + glColor4f(1.0, 1.0, 1.0, 0.3) + pyglet.graphics.draw(4, GL_QUADS, + ('v2f', (slot_x, slot_y, slot_x + slot_width, slot_y, + slot_x + slot_width, slot_y + slot_height, + slot_x, slot_y + slot_height))) + + def draw_selected_block_name(self): + """ Desenha o nome do bloco selecionado acima da hotbar. + """ + if self.block is None: + return + + self.set_2d() + width, height = self.get_size() + + # Posição acima da hotbar + hotbar_y = 20 + hotbar_height = 22 + name_y = hotbar_y + hotbar_height + 10 + + # Obter nome do bloco + block_name = self.get_item_name(self.block) + + # Desenhar fundo semi-transparente para melhor legibilidade + text_width = len(block_name) * 8 # Aproximação da largura do texto + text_height = 20 + bg_x = (width - text_width) // 2 - 10 + bg_y = name_y - 5 + + glColor4f(0.0, 0.0, 0.0, 0.6) + pyglet.graphics.draw(4, GL_QUADS, + ('v2f', (bg_x, bg_y, bg_x + text_width + 20, bg_y, + bg_x + text_width + 20, bg_y + text_height, + bg_x, bg_y + text_height))) + + # Desenhar texto do nome do bloco + name_label = pyglet.text.Label( + block_name, + font_name='Arial', + font_size=16, + x=width // 2, + y=name_y, + anchor_x='center', + anchor_y='center', + color=(255, 255, 255, 255) + ) + name_label.draw() + + def draw_chat(self): + """ Desenha o chat na tela (histórico e campo de entrada). + """ + self.set_2d() + width, height = self.get_size() + + # Posição do chat (canto inferior esquerdo) + chat_x = 10 + chat_y = 10 + chat_width = width // 3 # Largura do chat (1/3 da tela) + chat_line_height = 20 + max_visible_lines = 5 # Número máximo de linhas visíveis no histórico + + # Desenhar histórico de mensagens (últimas 5 mensagens) + if self.chat_history: + # Mostrar apenas as últimas mensagens + visible_history = self.chat_history[-max_visible_lines:] + + for i, message in enumerate(visible_history): + y_pos = chat_y + (len(visible_history) - i) * chat_line_height + + # Fundo semi-transparente para cada mensagem + text_width = len(message) * 7 # Aproximação da largura + bg_x = chat_x + bg_y = y_pos - 2 + + glColor4f(0.0, 0.0, 0.0, 0.5) + pyglet.graphics.draw(4, GL_QUADS, + ('v2f', (bg_x, bg_y, bg_x + text_width + 10, bg_y, + bg_x + text_width + 10, bg_y + chat_line_height, + bg_x, bg_y + chat_line_height))) + + # Texto da mensagem + message_label = pyglet.text.Label( + message, + font_name='Arial', + font_size=12, + x=chat_x + 5, + y=y_pos, + anchor_x='left', + anchor_y='center', + color=(255, 255, 255, 255) + ) + message_label.draw() + + # Desenhar campo de entrada quando o chat estiver aberto + if self.chat_open: + input_y = chat_y + (len(self.chat_history[-max_visible_lines:]) if self.chat_history else 0) * chat_line_height + chat_line_height + 5 + + # Fundo do campo de entrada + input_width = chat_width + input_height = chat_line_height + 4 + + glColor4f(0.0, 0.0, 0.0, 0.8) + pyglet.graphics.draw(4, GL_QUADS, + ('v2f', (chat_x, input_y, chat_x + input_width, input_y, + chat_x + input_width, input_y + input_height, + chat_x, input_y + input_height))) + + # Borda do campo de entrada + glColor4f(1.0, 1.0, 1.0, 0.8) + pyglet.graphics.draw(4, GL_LINE_LOOP, + ('v2f', (chat_x, input_y, chat_x + input_width, input_y, + chat_x + input_width, input_y + input_height, + chat_x, input_y + input_height))) + + # Texto do campo de entrada (com cursor piscante) + input_text = self.chat_message + ("|" if int(pyglet.clock.get_default().time() * 2) % 2 == 0 else "") + input_label = pyglet.text.Label( + input_text, + font_name='Arial', + font_size=14, + x=chat_x + 5, + y=input_y + input_height // 2, + anchor_x='left', + anchor_y='center', + color=(255, 255, 255, 255) + ) + input_label.draw() + + def draw_health(self): + """ Desenha a barra de vida no modo Survival. + """ + if self.game_mode != 'survival' or self.health is None: + return + + self.set_2d() + width, height = self.get_size() + health_bar_x = 10 + health_bar_y = height - 30 + health_bar_width = 200 + health_bar_height = 10 + + # Fundo da barra (vermelho) + glColor4f(0.8, 0.0, 0.0, 0.8) + pyglet.graphics.draw(4, GL_QUADS, + ('v2f', (health_bar_x, health_bar_y, health_bar_x + health_bar_width, health_bar_y, + health_bar_x + health_bar_width, health_bar_y + health_bar_height, + health_bar_x, health_bar_y + health_bar_height))) + + # Vida atual (verde) + health_percent = float(self.health) / float(self.max_health) + glColor4f(0.0, 0.8, 0.0, 0.8) + current_width = health_bar_width * health_percent + pyglet.graphics.draw(4, GL_QUADS, + ('v2f', (health_bar_x, health_bar_y, health_bar_x + current_width, health_bar_y, + health_bar_x + current_width, health_bar_y + health_bar_height, + health_bar_x, health_bar_y + health_bar_height))) + + # Texto da vida + health_label = pyglet.text.Label( + f'{self.health}/{self.max_health}', + font_name='Arial', + font_size=12, + x=health_bar_x + health_bar_width // 2, + y=health_bar_y + health_bar_height // 2, + anchor_x='center', + anchor_y='center', + color=(255, 255, 255, 255) + ) + health_label.draw() def setup_fog(): @@ -1210,11 +2181,280 @@ def setup(): setup_fog() +# Imagem de fundo do menu em base64 +MENU_BACKGROUND_BASE64 = "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" + + +class MenuWindow(pyglet.window.Window): + """ Janela do menu principal do jogo. + """ + + def __init__(self, *args, **kwargs): + super(MenuWindow, self).__init__(*args, **kwargs) + + self.state = 'menu' # 'menu', 'help', 'mode_selection', 'game' + self.game_window = None + self.should_start_game = False # Flag para iniciar jogo ao fechar + self.selected_mode = None # 'creative' ou 'survival' + + # Decodificar imagem de fundo + try: + img_data = base64.b64decode(MENU_BACKGROUND_BASE64) + img_stream = io.BytesIO(img_data) + self.background_image = image.load('', file=img_stream) + except: + # Se falhar, usar cor sólida + self.background_image = None + + # Criar labels + self.title_label = pyglet.text.Label( + 'PhytonCraft', + font_name='Arial', + font_size=48, + x=self.width // 2, + y=self.height - 150, + anchor_x='center', + anchor_y='center', + color=(255, 255, 255, 255) + ) + + # Botão Play + self.play_label = pyglet.text.Label( + 'Play', + font_name='Arial', + font_size=32, + x=self.width // 2, + y=self.height // 2 + 30, + anchor_x='center', + anchor_y='center', + color=(200, 200, 200, 255) + ) + + # Botão Help + self.help_label = pyglet.text.Label( + 'Help', + font_name='Arial', + font_size=32, + x=self.width // 2, + y=self.height // 2 - 30, + anchor_x='center', + anchor_y='center', + color=(200, 200, 200, 255) + ) + + # Versão + self.version_label = pyglet.text.Label( + 'Version: Classic 0.10', + font_name='Arial', + font_size=12, + x=10, + y=10, + anchor_x='left', + anchor_y='bottom', + color=(150, 150, 150, 255) + ) + + # Texto de ajuda + self.help_text = """CONTROLES E AÇÕES + +MOVIMENTO: +- W, A, S, D: Mover (frente, esquerda, trás, direita) +- Espaço: Pular +- Tab: Alternar modo voo +- Mouse: Olhar ao redor + +AÇÕES: +- Botão Esquerdo: Quebrar bloco +- Botão Direito: Colocar bloco +- ESC: Liberar mouse + +BLOCOS (Teclas Numéricas): +1 - Grama (Grass) +2 - Pedra (Stone) +3 - TNT +4 - Madeira +5 - Madeira Copa +6 - Tijolo (Brick) +7 - Cascalho (Gravel) +8 - Carvão +9 - Diamante +0 - Areia (Sand) + +DICAS: +- TNT explode após 3 segundos quando ativada +- Água não é sólida, você pode atravessar +- Explore o mundo para encontrar recursos raros +- Use o modo voo (Tab) para explorar mais rápido + +Pressione ESC para voltar ao menu""" + + self.help_label_text = pyglet.text.Label( + self.help_text, + font_name='Arial', + font_size=14, + x=20, + y=self.height - 20, + anchor_x='left', + anchor_y='top', + color=(255, 255, 255, 255), + width=self.width - 40, + multiline=True + ) + + # Labels para seleção de modo + self.mode_title_label = pyglet.text.Label( + 'Selecione o Modo', + font_name='Arial', + font_size=36, + x=self.width // 2, + y=self.height - 150, + anchor_x='center', + anchor_y='center', + color=(255, 255, 255, 255) + ) + + self.creative_label = pyglet.text.Label( + 'Creative', + font_name='Arial', + font_size=32, + x=self.width // 2, + y=self.height // 2 + 30, + anchor_x='center', + anchor_y='center', + color=(200, 200, 200, 255) + ) + + self.survival_label = pyglet.text.Label( + 'Survival', + font_name='Arial', + font_size=32, + x=self.width // 2, + y=self.height // 2 - 30, + anchor_x='center', + anchor_y='center', + color=(200, 200, 200, 255) + ) + + def on_draw(self): + """ Desenha a janela do menu. + """ + self.clear() + + if self.state == 'menu': + # Desenhar fundo + if self.background_image: + self.background_image.blit(0, 0, width=self.width, height=self.height) + else: + glClearColor(0.1, 0.1, 0.2, 1) + + # Desenhar título e botões + self.title_label.draw() + self.play_label.draw() + self.help_label.draw() + self.version_label.draw() + + elif self.state == 'help': + # Desenhar tela de ajuda + glClearColor(0.05, 0.05, 0.15, 1) + self.help_label_text.draw() + self.version_label.draw() + + elif self.state == 'mode_selection': + # Desenhar tela de seleção de modo + if self.background_image: + self.background_image.blit(0, 0, width=self.width, height=self.height) + else: + glClearColor(0.1, 0.1, 0.2, 1) + + self.mode_title_label.draw() + self.creative_label.draw() + self.survival_label.draw() + self.version_label.draw() + + def on_mouse_press(self, x, y, button, modifiers): + """ Detecta cliques nos botões. + """ + if self.state == 'menu': + # Verificar clique no botão Play + play_x = self.width // 2 + play_y = self.height // 2 + 30 + if abs(x - play_x) < 100 and abs(y - play_y) < 20: + self.start_game() + + # Verificar clique no botão Help + help_x = self.width // 2 + help_y = self.height // 2 - 30 + if abs(x - help_x) < 100 and abs(y - help_y) < 20: + self.state = 'help' + + elif self.state == 'help': + # Qualquer clique volta ao menu + self.state = 'menu' + + elif self.state == 'mode_selection': + # Verificar clique no botão Creative + creative_x = self.width // 2 + creative_y = self.height // 2 + 30 + if abs(x - creative_x) < 100 and abs(y - creative_y) < 20: + self.start_game_with_mode('creative') + + # Verificar clique no botão Survival + survival_x = self.width // 2 + survival_y = self.height // 2 - 30 + if abs(x - survival_x) < 100 and abs(y - survival_y) < 20: + self.start_game_with_mode('survival') + + def on_key_press(self, symbol, modifiers): + """ Detecta teclas pressionadas. + """ + if symbol == key.ESCAPE: + if self.state == 'help': + self.state = 'menu' + elif self.state == 'game' and self.game_window: + self.close_game() + elif symbol == key.ENTER and self.state == 'menu': + self.state = 'mode_selection' + elif symbol == key.ENTER and self.state == 'mode_selection': + # Enter seleciona Creative por padrão + self.start_game_with_mode('creative') + elif symbol == key.H and self.state == 'menu': + self.state = 'help' + elif symbol == key.ESCAPE and self.state == 'mode_selection': + self.state = 'menu' + + def start_game(self): + """ Mostra a tela de seleção de modo. + """ + self.state = 'mode_selection' + + def start_game_with_mode(self, mode): + """ Inicia o jogo com o modo especificado. + + Parameters + ---------- + mode : str + 'creative' ou 'survival' + """ + self.selected_mode = mode + # Criar janela do jogo com o modo especificado + game_window = Window(width=800, height=600, caption='PhytonCraft', resizable=True, game_mode=mode) + game_window.set_exclusive_mouse(True) + + # Esconder o menu (não fechar, para manter o loop ativo) + self.set_visible(False) + + def close_game(self): + """ Fecha o jogo e volta ao menu. + """ + if self.game_window: + self.game_window.close() + self.game_window = None + self.state = 'menu' + + def main(): - window = Window(width=800, height=600, caption='Pyglet', resizable=True) - # Hide the mouse cursor and prevent the mouse from leaving the window. - window.set_exclusive_mouse(True) - setup() + # Iniciar com o menu principal + menu = MenuWindow(width=800, height=600, caption='PhytonCraft', resizable=True) pyglet.app.run()