diff --git a/imagenes/tower_sentry-0.png b/imagenes/tower_sentry-0.png new file mode 100644 index 0000000..a0bd39a Binary files /dev/null and b/imagenes/tower_sentry-0.png differ diff --git a/imagenes/tower_sentry-1.png b/imagenes/tower_sentry-1.png new file mode 100644 index 0000000..041da44 Binary files /dev/null and b/imagenes/tower_sentry-1.png differ diff --git a/imagenes/tower_sentry-2.png b/imagenes/tower_sentry-2.png new file mode 100644 index 0000000..008aa97 Binary files /dev/null and b/imagenes/tower_sentry-2.png differ diff --git a/tower_sentry.py b/tower_sentry.py new file mode 100755 index 0000000..45f9129 --- /dev/null +++ b/tower_sentry.py @@ -0,0 +1,566 @@ +#!/usr/bin/env python3 +import math +import random +import pygame + +# ----------------------------------------------------------------------------- +# CONFIGURACIÓN Y CONSTANTES +# ----------------------------------------------------------------------------- +WIDTH, HEIGHT = 1024, 768 +GRID_SIZE = 36 # Base amplia +TILE_SIZE = 22.0 # Proporción para encajar todo el mapa + +# Reglas de alcance y línea de visión +MAX_REACH_DISTANCE = 12.0 # Distancia máxima en casillas para interactuar + +pygame.init() +pygame.font.init() +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Tower Sentry - Sistema de Niveles Infinitos") +clock = pygame.time.Clock() + +# Paleta de Colores +COLOR_SKY = (125, 165, 205) + +COLOR_WATER = (45, 85, 125) +COLOR_SAND = (195, 180, 140) +COLOR_GRASS_LOW = (80, 140, 65) +COLOR_GRASS_HIGH = (110, 155, 75) +COLOR_ROCK = (125, 120, 110) +COLOR_SNOW = (240, 245, 250) + +COLOR_TARGET = (255, 220, 80) +COLOR_TARGET_INVALID = (220, 80, 80) +COLOR_BLOCK_TOP = (205, 175, 130) +COLOR_BLOCK_SIDE = (130, 105, 75) +COLOR_CLONE_MAUVE = (175, 140, 195) +COLOR_CLONE_DARK = (130, 100, 150) + +# Costes y Valores de Energía +COST_BLOCK = 2 +COST_CLONE = 3 +VAL_TREE = 1 +VAL_SENTINEL = 10 + +# Estado de pantalla: "MENU" o "GAME" +game_state = "MENU" + +# ----------------------------------------------------------------------------- +# GENERACIÓN DE TERRENO Y LÓGICA DE NIVELES +# ----------------------------------------------------------------------------- +def noise(x, y): + xi, yi = int(x), int(y) + xf, yf = x - xi, y - yi + + u = xf * xf * (3 - 2 * xf) + v = yf * yf * (3 - 2 * yf) + + def hash_pt(px, py): + n = px * 157 + py * 113 + return math.sin(n) * 43758.5453123 - math.floor(math.sin(n) * 43758.5453123) + + g00 = hash_pt(xi, yi) + g10 = hash_pt(xi + 1, yi) + g01 = hash_pt(xi, yi + 1) + g11 = hash_pt(xi + 1, yi + 1) + + x1 = g00 + u * (g10 - g00) + x2 = g01 + u * (g11 - g01) + return x1 + v * (x2 - x1) + +def generate_realistic_heightmap(size): + grid = [[0.0 for _ in range(size)] for _ in range(size)] + seed_x, seed_y = random.uniform(10, 100), random.uniform(10, 100) + + for x in range(size): + for y in range(size): + nx, ny = (x + seed_x) * 0.07, (y + seed_y) * 0.07 + + h_base = noise(nx, ny) * 6.0 + h_detail = noise(nx * 2.0, ny * 2.0) * 2.0 + + total_h = h_base + h_detail + total_h = math.pow(max(0.0, total_h) / 5.0, 1.3) * 10.0 + + if 3.5 < total_h < 5.0: + total_h = 3.5 + elif 5.0 <= total_h < 6.5: + total_h = 6.5 + + grid[x][y] = round(total_h, 1) + + return grid + +# Variables Globales de Estado del Juego +current_level = 1 +player_energy = 10 + +base_grid = [] +has_block_grid = [] +sentinels_pos = set() +main_sentinel_pos = (0, 0) +player_start_pos = (0, 0) +player_x, player_y = (0, 0) +trees = set() +clones = set() +map_angle = 0.0 + +def load_level(level): + global base_grid, has_block_grid, sentinels_pos, main_sentinel_pos + global player_start_pos, player_x, player_y, trees, clones, map_angle + + base_grid = generate_realistic_heightmap(GRID_SIZE) + has_block_grid = [[0 for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)] + clones = set() + trees = set() + map_angle = 0.0 + + # Ordenar casillas por altura + height_list = [] + for x in range(GRID_SIZE): + for y in range(GRID_SIZE): + height_list.append((base_grid[x][y], (x, y))) + + height_list.sort(key=lambda item: item[0], reverse=True) + + # El Centinela Principal es la cima más alta + main_sentinel_pos = height_list[0][1] + + # Número de centinelas según el nivel actual + num_sentinels = level + sentinels_pos = set() + sentinels_pos.add(main_sentinel_pos) + + # Añadir centinelas adicionales en las siguientes cimas elevadas + idx = 1 + while len(sentinels_pos) < num_sentinels and idx < len(height_list): + pos = height_list[idx][1] + if base_grid[pos[0]][pos[1]] > 3.0: + sentinels_pos.add(pos) + idx += 1 + + # El Jugador empieza en la zona más baja + player_start_pos = height_list[-1][1] + player_x, player_y = player_start_pos + + # Generación de Árboles + while len(trees) < 32: + tx, ty = random.randint(0, GRID_SIZE - 1), random.randint(0, GRID_SIZE - 1) + if (tx, ty) not in sentinels_pos and (tx, ty) != player_start_pos and base_grid[tx][ty] > 1.8: + trees.add((tx, ty)) + +load_level(current_level) + +def get_effective_height(gx, gy): + if 0 <= gx < GRID_SIZE and 0 <= gy < GRID_SIZE: + return base_grid[gx][gy] + (1.2 if has_block_grid[gx][gy] == 1 else 0.0) + return 0.0 + +# ----------------------------------------------------------------------------- +# COMPROBACIÓN DE DISTANCIA Y LÍNEA DE VISIÓN +# ----------------------------------------------------------------------------- +def is_in_range_and_visible(px, py, tx, ty): + dist = math.hypot(tx - px, ty - py) + if dist > MAX_REACH_DISTANCE: + return False, "Fuera de alcance (demasiado lejos)" + + steps = int(max(abs(tx - px), abs(ty - py))) + if steps > 1: + p_height = get_effective_height(px, py) + 1.5 + t_height = get_effective_height(tx, ty) + + for i in range(1, steps): + cx = px + (tx - px) * (i / steps) + cy = py + (ty - py) * (i / steps) + + los_h = p_height + (t_height - p_height) * (i / steps) + terrain_h = get_effective_height(int(cx), int(cy)) + + if terrain_h > los_h: + return False, "Sin línea de visión (terreno bloquea la vista)" + + return True, "En alcance" + +# ----------------------------------------------------------------------------- +# PROYECCIÓN +# ----------------------------------------------------------------------------- +CENTER_GRID = GRID_SIZE / 2.0 + +def project(gx, gy, gz, angle): + rx = gx - CENTER_GRID + ry = gy - CENTER_GRID + + rot_x = rx * math.cos(angle) - ry * math.sin(angle) + rot_y = rx * math.sin(angle) + ry * math.cos(angle) + + screen_x = (rot_x - rot_y) * (TILE_SIZE / 2.0) + (WIDTH / 2.0) + screen_y = (rot_x + rot_y) * (TILE_SIZE / 4.0) - (gz * 6.5) + (HEIGHT / 2.0) + 30 + + return int(screen_x), int(screen_y), rot_y + +def get_realistic_color(height, normal_shade=1.0): + if height < 1.8: + c = COLOR_WATER + elif height < 3.0: + c = COLOR_SAND + elif height < 6.5: + t = (height - 3.0) / 3.5 + c = ( + int(COLOR_GRASS_LOW[0] * (1 - t) + COLOR_GRASS_HIGH[0] * t), + int(COLOR_GRASS_LOW[1] * (1 - t) + COLOR_GRASS_HIGH[1] * t), + int(COLOR_GRASS_LOW[2] * (1 - t) + COLOR_GRASS_HIGH[2] * t), + ) + elif height < 9.0: + c = COLOR_ROCK + else: + c = COLOR_SNOW + + r = max(0, min(255, int(c[0] * normal_shade))) + g = max(0, min(255, int(c[1] * normal_shade))) + b = max(0, min(255, int(c[2] * normal_shade))) + return (r, g, b) + +# ----------------------------------------------------------------------------- +# DIBUJO DE ENTIDADES +# ----------------------------------------------------------------------------- +def draw_humanoid(surface, cx, cy, color_main, color_shadow, is_player=False): + pygame.draw.ellipse(surface, (40, 50, 40, 100), (cx - 6, cy - 2, 12, 4)) + + pygame.draw.line(surface, color_shadow, (cx - 2, cy - 2), (cx - 1, cy - 7), 2) + pygame.draw.line(surface, color_shadow, (cx + 2, cy - 2), (cx + 1, cy - 7), 2) + + pygame.draw.polygon(surface, color_main, [ + (cx - 4, cy - 7), (cx + 4, cy - 7), + (cx + 3, cy - 15), (cx - 3, cy - 15) + ]) + + pygame.draw.line(surface, color_shadow, (cx - 3, cy - 14), (cx - 5, cy - 8), 2) + pygame.draw.line(surface, color_shadow, (cx + 3, cy - 14), (cx + 5, cy - 8), 2) + + pygame.draw.circle(surface, color_main, (cx, cy - 18), 3) + + if is_player: + pygame.draw.circle(surface, (100, 255, 200), (cx + 1, cy - 18), 2) + +def draw_sentinel(surface, cx, cy, time_ticks, is_main=True): + pygame.draw.ellipse(surface, (60, 60, 70), (cx - 8, cy - 2, 16, 5)) + + pygame.draw.polygon(surface, (40, 45, 55), [(cx - 6, cy - 3), (cx + 6, cy - 3), (cx + 4, cy - 22), (cx - 4, cy - 22)]) + pygame.draw.polygon(surface, (70, 75, 90), [(cx - 3, cy - 3), (cx + 3, cy - 3), (cx + 2, cy - 22), (cx - 2, cy - 22)]) + + pygame.draw.rect(surface, (30, 35, 45), (cx - 5, cy - 27, 10, 6)) + + eye_offset = int(math.sin(time_ticks * 0.05) * 2) + eye_color = (255, 40, 40) if is_main else (255, 140, 0) + pygame.draw.circle(surface, eye_color, (cx + eye_offset, cy - 24), 2) + +def draw_realistic_tree(surface, cx, cy): + pygame.draw.ellipse(surface, (30, 40, 30), (cx - 5, cy - 1, 10, 3)) + pygame.draw.polygon(surface, (65, 45, 25), [(cx - 1, cy - 1), (cx + 1, cy - 1), (cx + 1, cy - 6), (cx - 1, cy - 6)]) + pygame.draw.polygon(surface, (35, 85, 45), [(cx, cy - 18), (cx - 5, cy - 9), (cx + 5, cy - 9)]) + pygame.draw.polygon(surface, (45, 105, 55), [(cx, cy - 14), (cx - 4, cy - 5), (cx + 4, cy - 5)]) + +# ----------------------------------------------------------------------------- +# DIBUJO DE LA PANTALLA INICIAL (MENÚ) +# ----------------------------------------------------------------------------- +font_title = pygame.font.SysFont("Courier", 32, bold=True) +font_subtitle = pygame.font.SysFont("Courier", 16, bold=True) +font_text = pygame.font.SysFont("Courier", 13, bold=True) + +def draw_menu_screen(surface, time_ticks): + surface.fill((25, 30, 42)) + + # Título Principal + txt_title = font_title.render("TOWER SENTRY", True, (255, 215, 0)) + txt_sub = font_subtitle.render("Estrategia y Dominio Vertical Iso-3D", True, (180, 200, 220)) + surface.blit(txt_title, (WIDTH // 2 - txt_title.get_width() // 2, 45)) + surface.blit(txt_sub, (WIDTH // 2 - txt_sub.get_width() // 2, 85)) + + # Panel de Objetivos + panel_obj = pygame.Rect(50, 125, WIDTH - 100, 110) + pygame.draw.rect(surface, (35, 42, 58), panel_obj, border_radius=8) + pygame.draw.rect(surface, (70, 85, 110), panel_obj, 2, border_radius=8) + + txt_obj_title = font_subtitle.render("OBJETIVO DEL JUEGO", True, (255, 230, 120)) + surface.blit(txt_obj_title, (70, 135)) + + objs = [ + "1. Escala el terreno absorbiendo recursos y creando bloques y clones.", + "2. Elimina a los Centinelas absorbiéndolos (debes estar a su misma altura o superior).", + "3. Una vez eliminados, alcanza la Cima Principal y ocupa su peana para superar el nivel." + ] + for i, line in enumerate(objs): + t = font_text.render(line, True, (220, 225, 235)) + surface.blit(t, (70, 165 + i * 22)) + + # Panel de Elementos y Entidades + panel_entities = pygame.Rect(50, 250, WIDTH - 100, 230) + pygame.draw.rect(surface, (35, 42, 58), panel_entities, border_radius=8) + pygame.draw.rect(surface, (70, 85, 110), panel_entities, 2, border_radius=8) + + txt_ent_title = font_subtitle.render("ELEMENTOS DEL JUEGO", True, (255, 230, 120)) + surface.blit(txt_ent_title, (70, 260)) + + # Entidad: Jugador + draw_humanoid(surface, 90, 320, (60, 140, 210), (30, 70, 120), is_player=True) + t_player = font_text.render("JUGADOR: Tu proyección física en el mapa. Desplázate mediante clones o bloques.", True, (220, 225, 235)) + surface.blit(t_player, (120, 305)) + + # Entidad: Árbol + draw_realistic_tree(surface, 90, 365) + t_tree = font_text.render("ÁRBOLES: Fuente de energía básica. Absorberlos otorga +1 de energía.", True, (220, 225, 235)) + surface.blit(t_tree, (120, 350)) + + # Entidad: Clon + draw_humanoid(surface, 90, 410, COLOR_CLONE_MAUVE, COLOR_CLONE_DARK, is_player=False) + t_clone = font_text.render("CLON: Cuerpo sintético. Cuesta 3 de energía. Permite el teletransporte a su casilla.", True, (220, 225, 235)) + surface.blit(t_clone, (120, 395)) + + # Entidad: Centinela + draw_sentinel(surface, 90, 455, time_ticks, is_main=True) + t_sentry = font_text.render("CENTINELA: Guarda de las cimas. Debes absorberlos (+10 energía) para despejar la cima.", True, (220, 225, 235)) + surface.blit(t_sentry, (120, 440)) + + # Panel de Controles / Teclas + panel_controls = pygame.Rect(50, 495, WIDTH - 100, 180) + pygame.draw.rect(surface, (35, 42, 58), panel_controls, border_radius=8) + pygame.draw.rect(surface, (70, 85, 110), panel_controls, 2, border_radius=8) + + txt_ctrl_title = font_subtitle.render("TECLAS Y CONTROLES", True, (255, 230, 120)) + surface.blit(txt_ctrl_title, (70, 505)) + + controls = [ + "FLECHAS IZQ / DER : Rotar la cámara alrededor del mapa.", + "RATÓN : Apuntar e inspeccionar casillas en rango de visión.", + "TECLA 'A' : ABSORBER objetivo seleccionado (Árbol +1 | Centinela +10 | Clon +3 | Bloque +2).", + "TECLA 'B' : CREAR BLOQUE de elevación en la casilla seleccionada (Coste: 2 energía).", + "TECLA 'C' : CREAR CLON en la casilla seleccionada (Coste: 3 energía).", + "TECLA ESPACIO : TELETRANSPORTARSE al clon seleccionado o subir a un bloque." + ] + for i, line in enumerate(controls): + t = font_text.render(line, True, (200, 230, 255)) + surface.blit(t, (70, 532 + i * 22)) + + # Prompt de Inicio Parpadeante + if (time_ticks // 500) % 2 == 0: + txt_start = font_subtitle.render(">> PULSA CUALQUIER TECLA O ESPACIO PARA COMENZAR <<", True, (50, 255, 150)) + surface.blit(txt_start, (WIDTH // 2 - txt_start.get_width() // 2, 695)) + +# ----------------------------------------------------------------------------- +# BUCLE PRINCIPAL +# ----------------------------------------------------------------------------- +font = pygame.font.SysFont("Courier", 13, bold=True) +selected_tile = None +level_transition_timer = 0 + +running = True +while running: + clock.tick(60) + time_ticks = pygame.time.get_ticks() + + if game_state == "MENU": + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + elif event.type == pygame.KEYDOWN or event.type == pygame.MOUSEBUTTONDOWN: + game_state = "GAME" + + draw_menu_screen(screen, time_ticks) + pygame.display.flip() + + elif game_state == "GAME": + screen.fill(COLOR_SKY) + mouse_x, mouse_y = pygame.mouse.get_pos() + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + elif event.type == pygame.KEYDOWN and level_transition_timer == 0: + if selected_tile: + gx, gy = selected_tile + can_reach, _ = is_in_range_and_visible(player_x, player_y, gx, gy) + + if can_reach: + # Absorber ('A') + if event.key == pygame.K_a: + if (gx, gy) in trees: + trees.remove((gx, gy)) + player_energy += VAL_TREE + elif (gx, gy) in sentinels_pos: + if get_effective_height(player_x, player_y) >= get_effective_height(gx, gy): + sentinels_pos.remove((gx, gy)) + player_energy += VAL_SENTINEL + elif (gx, gy) in clones and (gx, gy) != (player_x, player_y): + clones.remove((gx, gy)) + player_energy += COST_CLONE + elif has_block_grid[gx][gy] == 1 and (gx, gy) != (player_x, player_y): + has_block_grid[gx][gy] = 0 + player_energy += COST_BLOCK + + # Crear Bloque ('B') + elif event.key == pygame.K_b: + if player_energy >= COST_BLOCK and has_block_grid[gx][gy] == 0: + if (gx, gy) != (player_x, player_y) and (gx, gy) not in trees and (gx, gy) not in sentinels_pos: + has_block_grid[gx][gy] = 1 + player_energy -= COST_BLOCK + + # Crear Clon ('C') + elif event.key == pygame.K_c: + if player_energy >= COST_CLONE and (gx, gy) != (player_x, player_y): + if (gx, gy) not in trees and (gx, gy) not in sentinels_pos: + clones.add((gx, gy)) + player_energy -= COST_CLONE + + # Teletransporte (ESPACIO) + elif event.key == pygame.K_SPACE: + target_h = get_effective_height(gx, gy) + curr_h = get_effective_height(player_x, player_y) + + if (gx, gy) in clones: + player_x, player_y = gx, gy + + # Condición de Victoria: Estar en la peana del Centinela Principal y que todos los centinelas hayan sido eliminados + if (gx, gy) == main_sentinel_pos and len(sentinels_pos) == 0: + level_transition_timer = time_ticks + + elif has_block_grid[gx][gy] == 1 and target_h <= curr_h: + player_x, player_y = gx, gy + + # Transición al siguiente nivel + if level_transition_timer > 0 and time_ticks - level_transition_timer > 2000: + current_level += 1 + load_level(current_level) + level_transition_timer = 0 + + # Rotación + keys = pygame.key.get_pressed() + if keys[pygame.K_LEFT]: + map_angle -= 0.025 + if keys[pygame.K_RIGHT]: + map_angle += 0.025 + + polygons_to_draw = [] + closest_tile = None + min_dist = float('inf') + + for gy in range(GRID_SIZE): + for gx in range(GRID_SIZE): + z0 = get_effective_height(gx, gy) + + p0, y0 = project(gx, gy, z0, map_angle)[:2], project(gx, gy, z0, map_angle)[2] + p1 = project(gx + 1, gy, get_effective_height(min(gx + 1, GRID_SIZE - 1), gy), map_angle)[:2] + p2 = project(gx + 1, gy + 1, get_effective_height(min(gx + 1, GRID_SIZE - 1), min(gy + 1, GRID_SIZE - 1)), map_angle)[:2] + p3 = project(gx, gy + 1, get_effective_height(gx, min(gy + 1, GRID_SIZE - 1)), map_angle)[:2] + + cx = (p0[0] + p1[0] + p2[0] + p3[0]) // 4 + cy = (p0[1] + p1[1] + p2[1] + p3[1]) // 4 + + dist = math.hypot(mouse_x - cx, mouse_y - cy) + if dist < min_dist and dist < 18: + min_dist = dist + closest_tile = (gx, gy) + + shade = max(0.8, min(1.1, 1.0 + (z0 - 5.0) * 0.03)) + polygons_to_draw.append((y0, [p0, p1, p2, p3], z0, gx, gy, (cx, cy), shade)) + + polygons_to_draw.sort(key=lambda item: item[0]) + selected_tile = closest_tile + + # Renderizado + for depth, poly, avg_z, gx, gy, center_pt, shade in polygons_to_draw: + is_selected = (selected_tile == (gx, gy)) + + if is_selected: + can_reach, _ = is_in_range_and_visible(player_x, player_y, gx, gy) + base_col = COLOR_TARGET if can_reach else COLOR_TARGET_INVALID + else: + base_col = get_realistic_color(avg_z, shade) + + pygame.draw.polygon(screen, base_col, poly) + pygame.draw.polygon(screen, (35, 45, 40), poly, 1) + + cx, cy = center_pt + + if has_block_grid[gx][gy] == 1: + top_b = [(cx - 9, cy - 4), (cx + 9, cy - 4), (cx + 9, cy - 1), (cx - 9, cy - 1)] + side_b = [(cx - 9, cy - 1), (cx + 9, cy - 1), (cx + 9, cy + 3), (cx - 9, cy + 3)] + pygame.draw.polygon(screen, COLOR_BLOCK_SIDE, side_b) + pygame.draw.polygon(screen, COLOR_BLOCK_TOP, top_b) + pygame.draw.polygon(screen, (40, 30, 20), top_b, 1) + + if (gx, gy) in trees: + draw_realistic_tree(screen, cx, cy) + + if (gx, gy) in clones and (gx, gy) != (player_x, player_y): + draw_humanoid(screen, cx, cy, COLOR_CLONE_MAUVE, COLOR_CLONE_DARK, is_player=False) + + if (gx, gy) in sentinels_pos: + is_main = ((gx, gy) == main_sentinel_pos) + draw_sentinel(screen, cx, cy, time_ticks, is_main) + + if (gx, gy) == (player_x, player_y): + draw_humanoid(screen, cx, cy, (60, 140, 210), (30, 70, 120), is_player=True) + + # ----------------------------------------------------------------------------- + # INTERFAZ HUD + # ----------------------------------------------------------------------------- + pygame.draw.circle(screen, (255, 255, 255), (mouse_x, mouse_y), 3) + + p_h = get_effective_height(player_x, player_y) + action_msg = "Apunta a una casilla" + msg_color = (20, 20, 20) + + if selected_tile: + sgx, sgy = selected_tile + sh = get_effective_height(sgx, sgy) + can_reach, reach_reason = is_in_range_and_visible(player_x, player_y, sgx, sgy) + + if not can_reach: + action_msg = f"CASILLA INALCANZABLE: {reach_reason}" + msg_color = (180, 40, 40) + else: + if (sgx, sgy) in sentinels_pos: + if p_h >= sh: + action_msg = "¡CENTINELA EN RANGO! Pulsa 'A' para absorberlo (+10 energía)" + msg_color = (0, 140, 0) + else: + action_msg = f"Centinela elevado ({sh:.1f}m > {p_h:.1f}m). Iguala su altura con bloques." + msg_color = (180, 30, 30) + elif (sgx, sgy) == main_sentinel_pos and (sgx, sgy) not in sentinels_pos: + if len(sentinels_pos) > 0: + action_msg = f"Cima Principal despejada. Elimina los {len(sentinels_pos)} centinelas restantes." + msg_color = (180, 110, 0) + else: + if (sgx, sgy) in clones: + action_msg = "¡PEANA LISTA! Pulsa ESPACIO para conquistar el nivel." + msg_color = (0, 150, 220) + else: + action_msg = "Cima lista. Crea un clon ('C') y desplázate a él para ganar." + msg_color = (0, 140, 0) + elif (sgx, sgy) in trees: + action_msg = "Árbol en alcance. Pulsa 'A' para absorber (+1 energía)" + msg_color = (0, 120, 0) + elif (sgx, sgy) in clones: + action_msg = "Clon en alcance. Pulsa ESPACIO para teletransportarte | 'A' para absorber" + msg_color = (110, 70, 150) + elif has_block_grid[sgx][sgy] == 1: + action_msg = "Bloque en alcance. Pulsa ESPACIO para subir | 'C' para Clon | 'A' absorber" + msg_color = (130, 90, 0) + + pygame.draw.rect(screen, (235, 238, 242), (10, 10, WIDTH - 20, 45)) + + txt_hud = font.render(f"TOWER SENTRY | NIVEL: {current_level} | ENERGIA: {player_energy} | CENTINELAS: {len(sentinels_pos)}", True, (20, 20, 20)) + txt_action = font.render(action_msg, True, msg_color) + + screen.blit(txt_hud, (15, 14)) + screen.blit(txt_action, (15, 32)) + + if level_transition_timer > 0: + pygame.draw.rect(screen, (15, 20, 30), (WIDTH // 2 - 250, HEIGHT // 2 - 30, 500, 60)) + w_surf = font.render(f"¡NIVEL {current_level} SUPERADO! CARGANDO NIVEL {current_level + 1}...", True, (255, 215, 0)) + screen.blit(w_surf, (WIDTH // 2 - 230, HEIGHT // 2 - 8)) + + pygame.display.flip() + +pygame.quit() diff --git a/tower_sentry_version-sin-intro.py b/tower_sentry_version-sin-intro.py new file mode 100644 index 0000000..a6dea91 --- /dev/null +++ b/tower_sentry_version-sin-intro.py @@ -0,0 +1,465 @@ +import math +import random +import pygame + +# ----------------------------------------------------------------------------- +# CONFIGURACIÓN Y CONSTANTES +# ----------------------------------------------------------------------------- +WIDTH, HEIGHT = 1024, 768 +GRID_SIZE = 36 # Base amplia +TILE_SIZE = 22.0 # Proporción para encajar todo el mapa + +# Reglas de alcance y línea de visión +MAX_REACH_DISTANCE = 12.0 # Distancia máxima en casillas para interactuar + +pygame.init() +pygame.font.init() +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Tower Sentry - Sistema de Niveles Infinitos") +clock = pygame.time.Clock() + +# Paleta de Colores +COLOR_SKY = (125, 165, 205) + +COLOR_WATER = (45, 85, 125) +COLOR_SAND = (195, 180, 140) +COLOR_GRASS_LOW = (80, 140, 65) +COLOR_GRASS_HIGH = (110, 155, 75) +COLOR_ROCK = (125, 120, 110) +COLOR_SNOW = (240, 245, 250) + +COLOR_TARGET = (255, 220, 80) +COLOR_TARGET_INVALID = (220, 80, 80) +COLOR_BLOCK_TOP = (205, 175, 130) +COLOR_BLOCK_SIDE = (130, 105, 75) +COLOR_CLONE_MAUVE = (175, 140, 195) +COLOR_CLONE_DARK = (130, 100, 150) + +# Costes y Valores de Energía +COST_BLOCK = 2 +COST_CLONE = 3 +VAL_TREE = 1 +VAL_SENTINEL = 10 + +# ----------------------------------------------------------------------------- +# GENERACIÓN DE TERRENO Y LÓGICA DE NIVELES +# ----------------------------------------------------------------------------- +def noise(x, y): + xi, yi = int(x), int(y) + xf, yf = x - xi, y - yi + + u = xf * xf * (3 - 2 * xf) + v = yf * yf * (3 - 2 * yf) + + def hash_pt(px, py): + n = px * 157 + py * 113 + return math.sin(n) * 43758.5453123 - math.floor(math.sin(n) * 43758.5453123) + + g00 = hash_pt(xi, yi) + g10 = hash_pt(xi + 1, yi) + g01 = hash_pt(xi, yi + 1) + g11 = hash_pt(xi + 1, yi + 1) + + x1 = g00 + u * (g10 - g00) + x2 = g01 + u * (g11 - g01) + return x1 + v * (x2 - x1) + +def generate_realistic_heightmap(size): + grid = [[0.0 for _ in range(size)] for _ in range(size)] + seed_x, seed_y = random.uniform(10, 100), random.uniform(10, 100) + + for x in range(size): + for y in range(size): + nx, ny = (x + seed_x) * 0.07, (y + seed_y) * 0.07 + + h_base = noise(nx, ny) * 6.0 + h_detail = noise(nx * 2.0, ny * 2.0) * 2.0 + + total_h = h_base + h_detail + total_h = math.pow(max(0.0, total_h) / 5.0, 1.3) * 10.0 + + if 3.5 < total_h < 5.0: + total_h = 3.5 + elif 5.0 <= total_h < 6.5: + total_h = 6.5 + + grid[x][y] = round(total_h, 1) + + return grid + +# Variables Globales de Estado del Juego +current_level = 1 +player_energy = 10 + +base_grid = [] +has_block_grid = [] +sentinels_pos = set() +main_sentinel_pos = (0, 0) +player_start_pos = (0, 0) +player_x, player_y = (0, 0) +trees = set() +clones = set() +map_angle = 0.0 + +def load_level(level): + global base_grid, has_block_grid, sentinels_pos, main_sentinel_pos + global player_start_pos, player_x, player_y, trees, clones, map_angle + + base_grid = generate_realistic_heightmap(GRID_SIZE) + has_block_grid = [[0 for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)] + clones = set() + trees = set() + map_angle = 0.0 + + # Ordenar casillas por altura + height_list = [] + for x in range(GRID_SIZE): + for y in range(GRID_SIZE): + height_list.append((base_grid[x][y], (x, y))) + + height_list.sort(key=lambda item: item[0], reverse=True) + + # El Centinela Principal es la cima más alta + main_sentinel_pos = height_list[0][1] + + # Número de centinelas según el nivel actual + num_sentinels = level + sentinels_pos = set() + sentinels_pos.add(main_sentinel_pos) + + # Añadir centinelas adicionales en las siguientes cimas elevadas + idx = 1 + while len(sentinels_pos) < num_sentinels and idx < len(height_list): + pos = height_list[idx][1] + if base_grid[pos[0]][pos[1]] > 3.0: + sentinels_pos.add(pos) + idx += 1 + + # El Jugador empieza en la zona más baja + player_start_pos = height_list[-1][1] + player_x, player_y = player_start_pos + + # Generación de Árboles + while len(trees) < 32: + tx, ty = random.randint(0, GRID_SIZE - 1), random.randint(0, GRID_SIZE - 1) + if (tx, ty) not in sentinels_pos and (tx, ty) != player_start_pos and base_grid[tx][ty] > 1.8: + trees.add((tx, ty)) + +load_level(current_level) + +def get_effective_height(gx, gy): + if 0 <= gx < GRID_SIZE and 0 <= gy < GRID_SIZE: + return base_grid[gx][gy] + (1.2 if has_block_grid[gx][gy] == 1 else 0.0) + return 0.0 + +# ----------------------------------------------------------------------------- +# COMPROBACIÓN DE DISTANCIA Y LÍNEA DE VISIÓN +# ----------------------------------------------------------------------------- +def is_in_range_and_visible(px, py, tx, ty): + dist = math.hypot(tx - px, ty - py) + if dist > MAX_REACH_DISTANCE: + return False, "Fuera de alcance (demasiado lejos)" + + steps = int(max(abs(tx - px), abs(ty - py))) + if steps > 1: + p_height = get_effective_height(px, py) + 1.5 + t_height = get_effective_height(tx, ty) + + for i in range(1, steps): + cx = px + (tx - px) * (i / steps) + cy = py + (ty - py) * (i / steps) + + los_h = p_height + (t_height - p_height) * (i / steps) + terrain_h = get_effective_height(int(cx), int(cy)) + + if terrain_h > los_h: + return False, "Sin línea de visión (terreno bloquea la vista)" + + return True, "En alcance" + +# ----------------------------------------------------------------------------- +# PROYECCIÓN +# ----------------------------------------------------------------------------- +CENTER_GRID = GRID_SIZE / 2.0 + +def project(gx, gy, gz, angle): + rx = gx - CENTER_GRID + ry = gy - CENTER_GRID + + rot_x = rx * math.cos(angle) - ry * math.sin(angle) + rot_y = rx * math.sin(angle) + ry * math.cos(angle) + + screen_x = (rot_x - rot_y) * (TILE_SIZE / 2.0) + (WIDTH / 2.0) + screen_y = (rot_x + rot_y) * (TILE_SIZE / 4.0) - (gz * 6.5) + (HEIGHT / 2.0) + 30 + + return int(screen_x), int(screen_y), rot_y + +def get_realistic_color(height, normal_shade=1.0): + if height < 1.8: + c = COLOR_WATER + elif height < 3.0: + c = COLOR_SAND + elif height < 6.5: + t = (height - 3.0) / 3.5 + c = ( + int(COLOR_GRASS_LOW[0] * (1 - t) + COLOR_GRASS_HIGH[0] * t), + int(COLOR_GRASS_LOW[1] * (1 - t) + COLOR_GRASS_HIGH[1] * t), + int(COLOR_GRASS_LOW[2] * (1 - t) + COLOR_GRASS_HIGH[2] * t), + ) + elif height < 9.0: + c = COLOR_ROCK + else: + c = COLOR_SNOW + + r = max(0, min(255, int(c[0] * normal_shade))) + g = max(0, min(255, int(c[1] * normal_shade))) + b = max(0, min(255, int(c[2] * normal_shade))) + return (r, g, b) + +# ----------------------------------------------------------------------------- +# DIBUJO DE ENTIDADES +# ----------------------------------------------------------------------------- +def draw_humanoid(surface, cx, cy, color_main, color_shadow, is_player=False): + pygame.draw.ellipse(surface, (40, 50, 40, 100), (cx - 6, cy - 2, 12, 4)) + + pygame.draw.line(surface, color_shadow, (cx - 2, cy - 2), (cx - 1, cy - 7), 2) + pygame.draw.line(surface, color_shadow, (cx + 2, cy - 2), (cx + 1, cy - 7), 2) + + pygame.draw.polygon(surface, color_main, [ + (cx - 4, cy - 7), (cx + 4, cy - 7), + (cx + 3, cy - 15), (cx - 3, cy - 15) + ]) + + pygame.draw.line(surface, color_shadow, (cx - 3, cy - 14), (cx - 5, cy - 8), 2) + pygame.draw.line(surface, color_shadow, (cx + 3, cy - 14), (cx + 5, cy - 8), 2) + + pygame.draw.circle(surface, color_main, (cx, cy - 18), 3) + + if is_player: + pygame.draw.circle(surface, (100, 255, 200), (cx + 1, cy - 18), 2) + +def draw_sentinel(surface, cx, cy, time_ticks, is_main=True): + pygame.draw.ellipse(surface, (60, 60, 70), (cx - 8, cy - 2, 16, 5)) + + pygame.draw.polygon(surface, (40, 45, 55), [(cx - 6, cy - 3), (cx + 6, cy - 3), (cx + 4, cy - 22), (cx - 4, cy - 22)]) + pygame.draw.polygon(surface, (70, 75, 90), [(cx - 3, cy - 3), (cx + 3, cy - 3), (cx + 2, cy - 22), (cx - 2, cy - 22)]) + + pygame.draw.rect(surface, (30, 35, 45), (cx - 5, cy - 27, 10, 6)) + + eye_offset = int(math.sin(time_ticks * 0.05) * 2) + eye_color = (255, 40, 40) if is_main else (255, 140, 0) + pygame.draw.circle(surface, eye_color, (cx + eye_offset, cy - 24), 2) + +def draw_realistic_tree(surface, cx, cy): + pygame.draw.ellipse(surface, (30, 40, 30), (cx - 5, cy - 1, 10, 3)) + pygame.draw.polygon(surface, (65, 45, 25), [(cx - 1, cy - 1), (cx + 1, cy - 1), (cx + 1, cy - 6), (cx - 1, cy - 6)]) + pygame.draw.polygon(surface, (35, 85, 45), [(cx, cy - 18), (cx - 5, cy - 9), (cx + 5, cy - 9)]) + pygame.draw.polygon(surface, (45, 105, 55), [(cx, cy - 14), (cx - 4, cy - 5), (cx + 4, cy - 5)]) + +# ----------------------------------------------------------------------------- +# BUCLE PRINCIPAL +# ----------------------------------------------------------------------------- +font = pygame.font.SysFont("Courier", 13, bold=True) +selected_tile = None +level_transition_timer = 0 + +running = True +while running: + clock.tick(60) + time_ticks = pygame.time.get_ticks() + screen.fill(COLOR_SKY) + + mouse_x, mouse_y = pygame.mouse.get_pos() + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + elif event.type == pygame.KEYDOWN and level_transition_timer == 0: + if selected_tile: + gx, gy = selected_tile + can_reach, _ = is_in_range_and_visible(player_x, player_y, gx, gy) + + if can_reach: + # Absorber ('A') + if event.key == pygame.K_a: + if (gx, gy) in trees: + trees.remove((gx, gy)) + player_energy += VAL_TREE + elif (gx, gy) in sentinels_pos: + if get_effective_height(player_x, player_y) >= get_effective_height(gx, gy): + sentinels_pos.remove((gx, gy)) + player_energy += VAL_SENTINEL + elif (gx, gy) in clones and (gx, gy) != (player_x, player_y): + clones.remove((gx, gy)) + player_energy += COST_CLONE + elif has_block_grid[gx][gy] == 1 and (gx, gy) != (player_x, player_y): + has_block_grid[gx][gy] = 0 + player_energy += COST_BLOCK + + # Crear Bloque ('B') + elif event.key == pygame.K_b: + if player_energy >= COST_BLOCK and has_block_grid[gx][gy] == 0: + if (gx, gy) != (player_x, player_y) and (gx, gy) not in trees and (gx, gy) not in sentinels_pos: + has_block_grid[gx][gy] = 1 + player_energy -= COST_BLOCK + + # Crear Clon ('C') + elif event.key == pygame.K_c: + if player_energy >= COST_CLONE and (gx, gy) != (player_x, player_y): + if (gx, gy) not in trees and (gx, gy) not in sentinels_pos: + clones.add((gx, gy)) + player_energy -= COST_CLONE + + # Teletransporte (ESPACIO) + elif event.key == pygame.K_SPACE: + target_h = get_effective_height(gx, gy) + curr_h = get_effective_height(player_x, player_y) + + if (gx, gy) in clones: + player_x, player_y = gx, gy + + # Condición de Victoria: Estar en la peana del Centinela Principal y que todos los centinelas hayan sido eliminados + if (gx, gy) == main_sentinel_pos and len(sentinels_pos) == 0: + level_transition_timer = time_ticks + + elif has_block_grid[gx][gy] == 1 and target_h <= curr_h: + player_x, player_y = gx, gy + + # Transición al siguiente nivel + if level_transition_timer > 0 and time_ticks - level_transition_timer > 2000: + current_level += 1 + load_level(current_level) + level_transition_timer = 0 + + # Rotación + keys = pygame.key.get_pressed() + if keys[pygame.K_LEFT]: + map_angle -= 0.025 + if keys[pygame.K_RIGHT]: + map_angle += 0.025 + + polygons_to_draw = [] + closest_tile = None + min_dist = float('inf') + + for gy in range(GRID_SIZE): + for gx in range(GRID_SIZE): + z0 = get_effective_height(gx, gy) + + p0, y0 = project(gx, gy, z0, map_angle)[:2], project(gx, gy, z0, map_angle)[2] + p1 = project(gx + 1, gy, get_effective_height(min(gx + 1, GRID_SIZE - 1), gy), map_angle)[:2] + p2 = project(gx + 1, gy + 1, get_effective_height(min(gx + 1, GRID_SIZE - 1), min(gy + 1, GRID_SIZE - 1)), map_angle)[:2] + p3 = project(gx, gy + 1, get_effective_height(gx, min(gy + 1, GRID_SIZE - 1)), map_angle)[:2] + + cx = (p0[0] + p1[0] + p2[0] + p3[0]) // 4 + cy = (p0[1] + p1[1] + p2[1] + p3[1]) // 4 + + dist = math.hypot(mouse_x - cx, mouse_y - cy) + if dist < min_dist and dist < 18: + min_dist = dist + closest_tile = (gx, gy) + + shade = max(0.8, min(1.1, 1.0 + (z0 - 5.0) * 0.03)) + polygons_to_draw.append((y0, [p0, p1, p2, p3], z0, gx, gy, (cx, cy), shade)) + + polygons_to_draw.sort(key=lambda item: item[0]) + selected_tile = closest_tile + + # Renderizado + for depth, poly, avg_z, gx, gy, center_pt, shade in polygons_to_draw: + is_selected = (selected_tile == (gx, gy)) + + if is_selected: + can_reach, _ = is_in_range_and_visible(player_x, player_y, gx, gy) + base_col = COLOR_TARGET if can_reach else COLOR_TARGET_INVALID + else: + base_col = get_realistic_color(avg_z, shade) + + pygame.draw.polygon(screen, base_col, poly) + pygame.draw.polygon(screen, (35, 45, 40), poly, 1) + + cx, cy = center_pt + + if has_block_grid[gx][gy] == 1: + top_b = [(cx - 9, cy - 4), (cx + 9, cy - 4), (cx + 9, cy - 1), (cx - 9, cy - 1)] + side_b = [(cx - 9, cy - 1), (cx + 9, cy - 1), (cx + 9, cy + 3), (cx - 9, cy + 3)] + pygame.draw.polygon(screen, COLOR_BLOCK_SIDE, side_b) + pygame.draw.polygon(screen, COLOR_BLOCK_TOP, top_b) + pygame.draw.polygon(screen, (40, 30, 20), top_b, 1) + + if (gx, gy) in trees: + draw_realistic_tree(screen, cx, cy) + + if (gx, gy) in clones and (gx, gy) != (player_x, player_y): + draw_humanoid(screen, cx, cy, COLOR_CLONE_MAUVE, COLOR_CLONE_DARK, is_player=False) + + if (gx, gy) in sentinels_pos: + is_main = ((gx, gy) == main_sentinel_pos) + draw_sentinel(screen, cx, cy, time_ticks, is_main) + + if (gx, gy) == (player_x, player_y): + draw_humanoid(screen, cx, cy, (60, 140, 210), (30, 70, 120), is_player=True) + + # ----------------------------------------------------------------------------- + # INTERFAZ HUD + # ----------------------------------------------------------------------------- + pygame.draw.circle(screen, (255, 255, 255), (mouse_x, mouse_y), 3) + + p_h = get_effective_height(player_x, player_y) + action_msg = "Apunta a una casilla" + msg_color = (20, 20, 20) + + if selected_tile: + sgx, sgy = selected_tile + sh = get_effective_height(sgx, sgy) + can_reach, reach_reason = is_in_range_and_visible(player_x, player_y, sgx, sgy) + + if not can_reach: + action_msg = f"CASILLA INALCANZABLE: {reach_reason}" + msg_color = (180, 40, 40) + else: + if (sgx, sgy) in sentinels_pos: + if p_h >= sh: + action_msg = "¡CENTINELA EN RANGO! Pulsa 'A' para absorberlo (+10 energía)" + msg_color = (0, 140, 0) + else: + action_msg = f"Centinela elevado ({sh:.1f}m > {p_h:.1f}m). Iguala su altura con bloques." + msg_color = (180, 30, 30) + elif (sgx, sgy) == main_sentinel_pos and (sgx, sgy) not in sentinels_pos: + if len(sentinels_pos) > 0: + action_msg = f"Cima Principal despejada. Elimina los {len(sentinels_pos)} centinelas restantes." + msg_color = (180, 110, 0) + else: + if (sgx, sgy) in clones: + action_msg = "¡PEANA LISTA! Pulsa ESPACIO para conquistar el nivel." + msg_color = (0, 150, 220) + else: + action_msg = "Cima lista. Crea un clon ('C') y desplázate a él para ganar." + msg_color = (0, 140, 0) + elif (sgx, sgy) in trees: + action_msg = "Árbol en alcance. Pulsa 'A' para absorber (+1 energía)" + msg_color = (0, 120, 0) + elif (sgx, sgy) in clones: + action_msg = "Clon en alcance. Pulsa ESPACIO para teletransportarte | 'A' para absorber" + msg_color = (110, 70, 150) + elif has_block_grid[sgx][sgy] == 1: + action_msg = "Bloque en alcance. Pulsa ESPACIO para subir | 'C' para Clon | 'A' absorber" + msg_color = (130, 90, 0) + + pygame.draw.rect(screen, (235, 238, 242), (10, 10, WIDTH - 20, 45)) + + txt_hud = font.render(f"TOWER SENTRY | NIVEL: {current_level} | ENERGIA: {player_energy} | CENTINELAS: {len(sentinels_pos)}", True, (20, 20, 20)) + txt_action = font.render(action_msg, True, msg_color) + + screen.blit(txt_hud, (15, 14)) + screen.blit(txt_action, (15, 32)) + + if level_transition_timer > 0: + pygame.draw.rect(screen, (15, 20, 30), (WIDTH // 2 - 250, HEIGHT // 2 - 30, 500, 60)) + w_surf = font.render(f"¡NIVEL {current_level} SUPERADO! CARGANDO NIVEL {current_level + 1}...", True, (255, 215, 0)) + screen.blit(w_surf, (WIDTH // 2 - 230, HEIGHT // 2 - 8)) + + pygame.display.flip() + +pygame.quit()