#4869·pygame

juego

Author: ghiandelcarpiom-collabCreated Jun 17, 2026Updated Jun 17, 2026

def init(self, x, y, w, h): self.rect = pygame.Rect(x, y, w, h)

def draw(self, surface, camera): x = self.rect.x - camera.x y = self.rect.y - camera.y

pygame.draw.rect(surface, YELLOW_WALL, (x, y, self.rect.w, self.rect.h))
pygame.draw.rect(surface, YELLOW_LIGHT, (x, y, self.rect.w, 5))
pygame.draw.rect(surface, (70, 60, 35), (x, y + self.rect.h - 4, self.rect.w, 4))

# Líneas sutiles para simular papel pintado / paneles.
for lx in range(x + 24, x + self.rect.w, 48):
    pygame.draw.line(surface, (130, 115, 55), (lx, y + 6), (lx, y + self.rect.h - 6), 1)
    def __init__(self, x, y):
self.rect = pygame.Rect(x, y, 22, 22)
self.collected = False
self.float_phase = random.random() * math.tau

def draw(self, surface, camera, player_center): if self.collected: return

# Las notas están "ocultas": solo se muestran claramente si estás cerca.
if distance(self.rect.center, player_center) > 430:
    return

x = self.rect.x - camera.x
y = self.rect.y - camera.y + math.sin(pygame.time.get_ticks() * 0.005 + self.float_phase) * 3

pygame.draw.rect(surface, (235, 230, 190), (x, y, self.rect.w, self.rect.h))
pygame.draw.rect(surface, (70, 55, 30), (x, y, self.rect.w, self.rect.h), 2)
pygame.draw.line(surface, (100, 80, 45), (x + 5, y + 8), (x + 17, y + 8), 1)
pygame.draw.line(surface, (100, 80, 45), (x + 5, y + 13), (x + 15, y + 13), 1)

def init(self, x, y, color, name="Actor"): self.pos = pygame.Vector2(x, y) self.rect = pygame.Rect(x, y, 30, 48)

self.vel = pygame.Vector2(0, 0)
self.color = color
self.name = name

self.health = 100
self.alive = True

self.on_ground = False
self.on_wall = 0  # -1 pared izquierda, 1 pared derecha
self.jumps_left = 1
self.facing = 1

self.control_lock = 0.0
self.hit_flash = 0.0

def center(self): return pygame.Vector2(self.rect.center)

def damage(self, amount): if not self.alive: return

self.health -= amount
self.hit_flash = 0.12

if self.health <= 0:
    self.health = 0
    self.alive = False

def jump(self): if not self.alive: return

# Salto normal
if self.on_ground:
    self.vel.y = -JUMP_POWER
    self.on_ground = False
    self.jumps_left = 1

# Wall jump: empuja en dirección contraria a la pared.
elif self.on_wall != 0:
    self.vel.y = -JUMP_POWER * 0.92
    self.vel.x = -self.on_wall * WALL_JUMP_PUSH
    self.jumps_left = 1
    self.control_lock = 0.16

# Doble salto
elif self.jumps_left > 0:
    self.vel.y = -DOUBLE_JUMP_POWER
    self.jumps_left -= 1

def detect_wall(self, platforms): self.on_wall = 0

left_probe = self.rect.move(-2, 0)
right_probe = self.rect.move(2, 0)

for platform in platforms:
    if left_probe.colliderect(platform.rect):
        self.on_wall = -1
        return
    if right_probe.colliderect(platform.rect):
        self.on_wall = 1
        return

def apply_physics(self, platforms): if not self.alive: return

self.on_ground = False

# Movimiento horizontal
self.pos.x += self.vel.x
self.rect.x = round(self.pos.x)

for platform in platforms:
    if self.rect.colliderect(platform.rect):
        if self.vel.x > 0:
            self.rect.right = platform.rect.left
            self.on_wall = 1
        elif self.vel.x < 0:
            self.rect.left = platform.rect.right
            self.on_wall = -1

        self.pos.x = self.rect.x
        self.vel.x = 0

self.detect_wall(platforms)

# Gravedad y wall slide
self.vel.y = min(MAX_FALL_SPEED, self.vel.y + GRAVITY)

if self.on_wall != 0 and self.vel.y > WALL_SLIDE_SPEED:
    self.vel.y = WALL_SLIDE_SPEED

# Movimiento vertical
self.pos.y += self.vel.y
self.rect.y = round(self.pos.y)

for platform in platforms:
    if self.rect.colliderect(platform.rect):
        if self.vel.y > 0:
            self.rect.bottom = platform.rect.top
            self.on_ground = True
            self.jumps_left = 1
        elif self.vel.y < 0:
            self.rect.top = platform.rect.bottom

        self.pos.y = self.rect.y
        self.vel.y = 0

# Límites del mundo
if self.rect.left < 0:
    self.rect.left = 0
    self.pos.x = self.rect.x
    self.vel.x = 0

if self.rect.right > WORLD_WIDTH:
    self.rect.right = WORLD_WIDTH
    self.pos.x = self.rect.x
    self.vel.x = 0

if self.rect.top < 0:
    self.rect.top = 0
    self.pos.y = self.rect.y
    self.vel.y = 0

if self.rect.bottom > WORLD_HEIGHT:
    self.rect.bottom = WORLD_HEIGHT
    self.pos.y = self.rect.y
    self.vel.y = 0
    self.on_ground = True
    self.jumps_left = 1

def draw(self, surface, camera): if not self.alive: return

x = self.rect.x - camera.x
y = self.rect.y - camera.y

color = self.color
if self.hit_flash > 0:
    color = (255, 70, 70)

pygame.draw.rect(surface, color, (x, y, self.rect.w, self.rect.h), border_radius=4)

# Ojo / dirección.
eye_x = x + 20 if self.facing > 0 else x + 7
pygame.draw.circle(surface, (10, 10, 20), (int(eye_x), int(y + 14)), 4)

# Indicador visual de wall slide.
if self.on_wall != 0 and not self.on_ground:
    spark_x = x - 5 if self.on_wall < 0 else x + self.rect.w + 2
    pygame.draw.line(surface, (255, 235, 120), (spark_x, y + 12), (spark_x, y + 32), 2)
    def update(self, keys, platforms, dt):
if not self.alive:
    return

if self.hit_flash > 0:
    self.hit_flash -= dt

if self.control_lock > 0:
    self.control_lock -= dt
else:
    direction = 0

    if keys[pygame.K_a] or keys[pygame.K_LEFT]:
        direction -= 1
    if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
        direction += 1

    running = keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]
    speed = RUN_SPEED if running else WALK_SPEED

    if direction != 0:
        self.facing = direction
        self.vel.x = direction * speed
    else:
        self.vel.x *= 0.78
        if abs(self.vel.x) < 0.1:
            self.vel.x = 0

self.apply_physics(platforms)
def __init__(self, x, y, bot_id):
super().__init__(x, y, BOT_COLOR, f"Bot {bot_id}")
self.jump_cooldown = random.uniform(0.2, 0.8)
self.wander_timer = 0
self.wander_dir = random.choice([-1, 1])

def update(self, game, dt): if not self.alive: return

if self.hit_flash > 0:
    self.hit_flash -= dt

self.jump_cooldown -= dt
self.wander_timer -= dt

center = self.center()
zone_center = game.zone_center
zone_distance = center.distance_to(zone_center)
entity_distance = center.distance_to(game.entity.center())

direction = 0

# Prioridad 1: si está fuera o cerca del borde, corre hacia la zona segura.
if zone_distance > game.zone_radius * 0.72:
    direction = sign(zone_center.x - center.x)

# Prioridad 2: si la Entidad está cerca, intenta alejarse.
if entity_distance < 330:
    direction = sign(center.x - game.entity.center().x)

# Si no hay amenaza clara, deambula.
if direction == 0:
    if self.wander_timer <= 0:
        self.wander_timer = random.uniform(1.2, 3.0)
        self.wander_dir = random.choice([-1, 1])
    direction = self.wander_dir

self.facing = direction if direction != 0 else self.facing

target_speed = RUN_SPEED * 0.92 if zone_distance > game.zone_radius else WALK_SPEED
self.vel.x += direction * 0.55
self.vel.x = clamp(self.vel.x, -target_speed, target_speed)

# Detección básica de obstáculo / precipicio.
ahead = self.rect.move(direction * 22, 0)
obstacle_ahead = any(ahead.colliderect(p.rect) for p in game.platforms)

foot_probe = pygame.Rect(
    self.rect.centerx + direction * 38 - 5,
    self.rect.bottom + 5,
    10,
    60
)
ground_ahead = any(foot_probe.colliderect(p.rect) for p in game.platforms)

must_jump = obstacle_ahead or not ground_ahead

# Si el centro de la zona está bastante más alto, intenta subir.
if zone_center.y < center.y - 120:
    must_jump = True

if must_jump and self.jump_cooldown <= 0:
    self.jump()
    self.jump_cooldown = random.uniform(0.35, 0.75)

if self.on_wall != 0 and self.jump_cooldown <= 0:
    self.jump()
    self.jump_cooldown = random.uniform(0.45, 0.85)

self.apply_physics(game.platforms)
def __init__(self, x, y):
self.pos = pygame.Vector2(x, y)
self.rect = pygame.Rect(x, y, 42, 70)
self.target = None
self.retarget_timer = 0
self.phase = random.random() * math.tau

def center(self): return pygame.Vector2(self.rect.center)

def update(self, game, dt): alive_targets = [game.player] + [bot for bot in game.bots if bot.alive] alive_targets = [actor for actor in alive_targets if actor.alive]

if not alive_targets:
    return

self.retarget_timer -= dt

if self.target not in alive_targets or self.retarget_timer <= 0:
    self.target = random.choice(alive_targets)
    self.retarget_timer = random.uniform(2.0, 5.0)

target_center = self.target.center()
to_target = target_center - self.center()

if to_target.length() > 0:
    to_target = to_target.normalize()

# Movimiento flotante e irregular.
speed = 125 + math.sin(pygame.time.get_ticks() * 0.002 + self.phase) * 35
self.pos += to_target * speed * dt

self.rect.x = round(self.pos.x)
self.rect.y = round(self.pos.y)

# Daño por contacto.
for actor in alive_targets:
    if self.rect.colliderect(actor.rect):
        actor.damage(35 * dt)

def draw(self, surface, camera): x = self.rect.x - camera.x y = self.rect.y - camera.y

jitter = random.randint(-2, 2)

# Sombra/glitch.
pygame.draw.rect(surface, (90, 0, 90), (x + jitter, y, self.rect.w, self.rect.h), 2)
pygame.draw.rect(surface, ENTITY_COLOR, (x, y, self.rect.w, self.rect.h), border_radius=8)

# Ojos mínimos.
pygame.draw.circle(surface, (255, 40, 40), (int(x + 13), int(y + 20)), 4)
pygame.draw.circle(surface, (255, 40, 40), (int(x + 29), int(y + 20)), 4)
def follow(self, target_rect):
target_x = target_rect.centerx - SCREEN_WIDTH // 2
target_y = target_rect.centery - SCREEN_HEIGHT // 2

self.x += (target_x - self.x) * 0.10
self.y += (target_y - self.y) * 0.10

self.x = clamp(self.x, 0, WORLD_WIDTH - SCREEN_WIDTH)
self.y = clamp(self.y, 0, WORLD_HEIGHT - SCREEN_HEIGHT)
    self.camera = Camera()
self.running = True

self.flashlight_mask = self.create_flashlight_mask(300)

self.reset()

def reset(self): self.state = "menu"

self.platforms = []
self.notes = []
self.bots = []

self.zone_elapsed = 0
self.zone_start_center = pygame.Vector2(WORLD_WIDTH / 2, WORLD_HEIGHT / 2)
self.zone_center = self.zone_start_center.copy()

margin = 600
self.zone_target = pygame.Vector2(
    random.randint(margin, WORLD_WIDTH - margin),
    random.randint(margin, WORLD_HEIGHT - margin)
)
self.zone_radius = ZONE_START_RADIUS

self.generate_map()

self.player = Player(160, WORLD_HEIGHT - 160)

spawn_platforms = [p for p in self.platforms if p.rect.w > 120]
random.shuffle(spawn_platforms)

for i in range(5):
    platform = spawn_platforms[i + 1]
    x = random.randint(platform.rect.left + 20, platform.rect.right - 60)
    y = platform.rect.top - 52
    self.bots.append(Bot(x, y, i + 1))

self.entity = Entity(WORLD_WIDTH // 2, WORLD_HEIGHT // 2)

self.message = "Recoge notas, sobrevive a la niebla y evita a la Entidad."
self.game_result = ""

def generate_map(self): """ Generación semi-aleatoria de plataformas. Simula un nivel laberíntico de oficinas abandonadas. """

rng = random.Random()

# Suelo base.
self.platforms.append(Platform(0, WORLD_HEIGHT - 60, WORLD_WIDTH, 80))

# Capas de plataformas.
y = WORLD_HEIGHT - 260
while y > 320:
    x = 0

    while x < WORLD_WIDTH:
        segment_width = rng.randint(180, 520)
        gap = rng.randint(80, 230)

        if rng.random() > 0.16:
            self.platforms.append(Platform(x, y, segment_width, 24))

            # Paredes verticales para wall slide / wall jump.
            if rng.random() < 0.38:
                wall_h = rng.randint(90, 220)
                wall_x = x + rng.randint(20, max(25, segment_width - 45))
                self.platforms.append(Platform(wall_x, y - wall_h, 28, wall_h))

        x += segment_width + gap

    y -= rng.randint(185, 245)

# Algunas columnas altas para reforzar el parkour.
for _ in range(22):
    x = rng.randint(250, WORLD_WIDTH - 350)
    y = rng.randint(420, WORLD_HEIGHT - 420)
    h = rng.randint(100, 280)
    self.platforms.append(Platform(x, y, 34, h))

# Notas ocultas sobre plataformas aleatorias.
candidates = [p for p in self.platforms if p.rect.w > 100 and p.rect.y < WORLD_HEIGHT - 90]
random.shuffle(candidates)

for platform in candidates[:14]:
    nx = random.randint(platform.rect.left + 20, platform.rect.right - 40)
    ny = platform.rect.top - 30
    self.notes.append(Note(nx, ny))

# Luces de fondo parpadeantes.
self.background_lights = []
for ly in range(160, WORLD_HEIGHT - 120, 260):
    for lx in range(120, WORLD_WIDTH - 120, 520):
        if rng.random() < 0.75:
            self.background_lights.append((
                lx + rng.randint(-45, 45),
                ly + rng.randint(-18, 18),
                rng.random() * math.tau
            ))

def create_flashlight_mask(self, radius): """ Crea una máscara radial para simular linterna. En un juego final puedes cambiar esto por un shader o sprite de luz. """

size = radius * 2
mask = pygame.Surface((size, size), pygame.SRCALPHA)

for y in range(size):
    for x in range(size):
        dx = x - radius
        dy = y - radius
        dist = math.sqrt(dx * dx + dy * dy)

        if dist < radius:
            strength = 1.0 - dist / radius
            alpha_subtract = int(240 * (strength ** 1.7))
            mask.set_at((x, y), (0, 0, 0, alpha_subtract))
        else:
            mask.set_at((x, y), (0, 0, 0, 0))

return mask

def handle_events(self): for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False

    if event.type == pygame.KEYDOWN:
        if event.key == pygame.K_ESCAPE:
            self.running = False

        if self.state == "menu":
            if event.key in (pygame.K_RETURN, pygame.K_SPACE):
                self.state = "playing"

        elif self.state == "playing":
            if event.key in (pygame.K_SPACE, pygame.K_w, pygame.K_UP):
                self.player.jump()

        elif self.state == "gameover":
            if event.key == pygame.K_r:
                self.reset()

    # Soporte simple para prototipo táctil: click/tap = saltar o empezar.
    if event.type == pygame.MOUSEBUTTONDOWN:
        if self.state == "menu":
            self.state = "playing"
        elif self.state == "playing":
            self.player.jump()
        elif self.state == "gameover":
            self.reset()

def update_zone(self, dt): self.zone_elapsed += dt

t = clamp(self.zone_elapsed / ZONE_CLOSE_TIME, 0, 1)
smooth = t * t * (3 - 2 * t)

self.zone_center = self.zone_start_center.lerp(self.zone_target, smooth)
self.zone_radius = ZONE_START_RADIUS + (ZONE_FINAL_RADIUS - ZONE_START_RADIUS) * smooth

actors = [self.player] + self.bots

for actor in actors:
    if not actor.alive:
        continue

    d = actor.center().distance_to(self.zone_center)

    if d > self.zone_radius:
        extra = (d - self.zone_radius) * 0.006
        actor.damage((ZONE_DAMAGE_PER_SECOND + extra) * dt)

def update_notes(self): for note in self.notes: if not note.collected and self.player.rect.colliderect(note.rect): note.collected = True self.player.mystery_points += 1 self.message = f"Nota encontrada: misterio +1 ({self.player.mystery_points})"

def update(self, dt): if self.state != "playing": return

keys = pygame.key.get_pressed()

self.player.update(keys, self.platforms, dt)

for bot in self.bots:
    bot.update(self, dt)

self.entity.update(self, dt)
self.update_zone(dt)
self.update_notes()

self.camera.follow(self.player.rect)

alive_actors = [actor for actor in [self.player] + self.bots if actor.alive]

if not self.player.alive:
    self.state = "gameover"
    self.game_result = "Has sido eliminado en los Backrooms."

elif len(alive_actors) == 1 and alive_actors[0] == self.player:
    self.state = "gameover"
    self.game_result = "Sobreviviste. Pero la salida aún no existe."

def draw_background(self): screen.fill((142, 126, 58))

cam_x = int(self.camera.x)
cam_y = int(self.camera.y)

# Patrón de paredes amarillas.
for x in range(-cam_x % 120, SCREEN_WIDTH, 120):
    pygame.draw.line(screen, (120, 105, 50), (x, 0), (x, SCREEN_HEIGHT), 1)

for y in range(-cam_y % 90, SCREEN_HEIGHT, 90):
    pygame.draw.line(screen, (175, 155, 70), (0, y), (SCREEN_WIDTH, y), 1)

# Luces fluorescentes parpadeantes.
time = pygame.time.get_ticks() * 0.005

for lx, ly, phase in self.background_lights:
    sx = lx - self.camera.x
    sy = ly - self.camera.y

    if -120 < sx < SCREEN_WIDTH + 120 and -80 < sy < SCREEN_HEIGHT + 80:
        intensity = 90 + int(math.sin(time + phase) * 45)
        intensity = clamp(intensity, 35, 140)

        light_surface = pygame.Surface((110, 34), pygame.SRCALPHA)
        light_surface.fill((255, 245, 165, intensity))
        pygame.draw.rect(light_surface, (250, 235, 160, 230), (12, 12, 86, 6))

        screen.blit(light_surface, (sx - 55, sy - 17))

def draw_zone(self): center_screen = ( int(self.zone_center.x - self.camera.x), int(self.zone_center.y - self.camera.y) )

radius = int(self.zone_radius)

# Borde de zona segura.
if radius < 3000:
    pygame.draw.circle(screen, (130, 0, 180), center_screen, radius, 5)
    pygame.draw.circle(screen, (220, 40, 255), center_screen, radius, 1)

# Si el jugador está fuera, la pantalla se contamina.
if self.player.center().distance_to(self.zone_center) > self.zone_radius:
    fog = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
    pulse = 75 + int(math.sin(pygame.time.get_ticks() * 0.012) * 35)
    fog.fill((35, 0, 45, pulse))
    screen.blit(fog, (0, 0))

def draw_world(self): self.draw_background() self.draw_zone()

view = pygame.Rect(self.camera.x, self.camera.y, SCREEN_WIDTH, SCREEN_HEIGHT).inflate(200, 200)

for platform in self.platforms:
    if platform.rect.colliderect(view):
        platform.draw(screen, self.camera)

for note in self.notes:
    note.draw(screen, self.camera, self.player.center())

for bot in self.bots:
    bot.draw(screen, self.camera)

self.player.draw(screen, self.camera)
self.entity.draw(screen, self.camera)

def draw_flashlight(self): darkness = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)

flicker = 225 + random.randint(-8, 12)
darkness.fill((0, 0, 0, flicker))

player_screen_center = (
    int(self.player.rect.