El amanecer
Lección 18 de 20 del curso Proyecto con Pygame: Defiende la fortaleza de Coddy.
Ahora se puede ganar la noche. Cuando night.time llega a NIGHT, sale el sol, el estado pasa a ser "won" y el bucle deja de actualizar, igual que cuando se pierde.
Haz que el jugador vea venir el amanecer. Cubre el patio con una capa azul oscuro semitransparente, con una opacidad de 120 sobre 255 durante toda la noche, y desvanece la capa durante el último minuto. Crea la Surface shade una sola vez, antes del bucle, como la cubierta, y rellénala en cada fotograma:
dark = min(120, max(0, NIGHT - night.time) * 120 // (60 * 60))
shade.fill((10, 10, 40, dark))
screen.blit(shade, (0, 0))Cuando quedan 30 segundos, 1800 * 120 // 3600 da 60: la mitad de oscuro. Dibuja la sombra después del caballero y las hachas, pero antes de los rayos y la barra superior, para que la magia y los corazones brillen a través de la oscuridad.
Desafío
Intermedio- Al final de
update: cuandoself.time >= NIGHTmientras se juega, estableceself.state = "won". - Antes del bucle, crea
shade, una Surface del tamaño de la ventana conSRCALPHA, ydark_cover, una Surface rellenada con(0, 0, 0, 210). Dibuja la sombra como se indicó arriba, después de las hachas y antes de los proyectiles. - En
"won"y"lost", dibuja primerodark_cover; en"won", muestra"Dawn! The keep holds"en letras grandes doradas en (360, 200).
Pruébalo tú mismo
import random
import pygame
WIDTH, HEIGHT = 720, 480
TILE = 48
NIGHT = 60 * 60 * 4
BOSS_AT = NIGHT - 60 * 60
WAVES = [
["slime"],
["slime", "bat"],
["slime", "bat", "spider"],
["bat", "spider"],
["slime", "spider", "ghost"],
["bat", "spider", "ghost"],
["spider", "ghost"],
["bat", "ghost"],
]
UPGRADES = {
"Quick bolts": "Bolts fire more often",
"Sharp bolts": "Bolts hit harder",
"Spinning axe": "One more axe circles you",
"Swift boots": "You walk faster",
"Gem magnet": "Gems come from further",
"Hearty meal": "Get back a heart",
}
WHITE = (240, 240, 240)
BOLT = (120, 200, 255)
GOLD = (250, 200, 60)
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Hold the Keep")
clock = pygame.time.Clock()
font = pygame.font.Font("assets/fonts/pixel.ttf", 24)
small_font = pygame.font.Font("assets/fonts/pixel.ttf", 16)
big_font = pygame.font.Font("assets/fonts/pixel.ttf", 48)
def load(name):
return pygame.image.load("assets/images/" + name).convert_alpha()
def sound(name):
return pygame.mixer.Sound("assets/sounds/" + name)
def whiten(image):
white = image.copy()
white.fill((255, 255, 255), special_flags=pygame.BLEND_RGB_MAX)
return white
floor_img = load("floor.png")
wall_img = load("wall.png")
knight_img = load("knight.png")
axe_img = load("axe.png")
gem_img = load("gem.png")
heart_img = load("heart.png")
empty_img = load("heart_empty.png")
MONSTERS = {
"slime": {"image": load("slime.png"), "speed": 1.0, "hp": 1, "size": 28},
"bat": {"image": load("dungeon_bat.png"), "speed": 1.6, "hp": 1, "size": 28},
"spider": {"image": load("spider.png"), "speed": 1.2, "hp": 3, "size": 28},
"ghost": {"image": load("ghost.png"), "speed": 0.8, "hp": 4, "size": 28},
"boss": {"image": pygame.transform.scale(load("ghost.png"), (96, 96)), "speed": 0.7, "hp": 120, "size": 64},
}
hit_sound = sound("hit.ogg")
hurt_sound = sound("thud.ogg")
gem_sound = sound("coin.ogg")
level_sound = sound("powerup.ogg")
courtyard = pygame.Rect(TILE, TILE, WIDTH - 2 * TILE, HEIGHT - 2 * TILE)
background = pygame.Surface((WIDTH, HEIGHT))
for row in range(HEIGHT // TILE):
for col in range(WIDTH // TILE):
x = col * TILE
y = row * TILE
if courtyard.collidepoint(x, y):
background.blit(floor_img, (x, y))
else:
background.blit(wall_img, (x, y))
cover = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
cover.fill((0, 0, 0, 150))
def draw(image, center):
screen.blit(image, image.get_rect(center=center))
def clock_text(frames):
seconds = frames // 60
return f"{seconds // 60}:{seconds % 60:02d}"
def edge_point():
side = random.randint(0, 3)
if side == 0:
return (random.randint(0, WIDTH), -24)
if side == 1:
return (random.randint(0, WIDTH), HEIGHT + 24)
if side == 2:
return (-24, random.randint(0, HEIGHT))
return (WIDTH + 24, random.randint(0, HEIGHT))
class Knight(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = knight_img
self.rect = pygame.Rect(0, 0, 24, 24)
self.rect.center = courtyard.center
self.speed = 3
self.hearts = 5
self.max_hearts = 5
self.hurt = 0
self.bolt_wait = 30
self.bolt_damage = 1
self.axes = 1
self.magnet = 80
def move(self, keys):
if keys[pygame.K_LEFT]:
self.rect.x -= self.speed
if keys[pygame.K_RIGHT]:
self.rect.x += self.speed
if keys[pygame.K_UP]:
self.rect.y -= self.speed
if keys[pygame.K_DOWN]:
self.rect.y += self.speed
self.rect.clamp_ip(courtyard)
class Monster(pygame.sprite.Sprite):
def __init__(self, kind, pos):
super().__init__()
self.kind = kind
self.image = MONSTERS[kind]["image"]
self.speed = MONSTERS[kind]["speed"]
self.hp = MONSTERS[kind]["hp"]
self.axe_wait = 0
self.flash = 0
self.pos = pygame.Vector2(pos)
self.rect = pygame.Rect(0, 0, MONSTERS[kind]["size"], MONSTERS[kind]["size"])
self.rect.center = self.pos
def update(self, target):
step = pygame.Vector2(target) - self.pos
if step.length() > 0:
self.pos += step.normalize() * self.speed
self.rect.center = self.pos
if self.axe_wait > 0:
self.axe_wait -= 1
if self.flash > 0:
self.flash -= 1
def hit(self, damage):
self.hp -= damage
self.flash = 6
hit_sound.play()
class Bolt(pygame.sprite.Sprite):
def __init__(self, start, target, damage):
super().__init__()
self.pos = pygame.Vector2(start)
step = pygame.Vector2(target) - self.pos
if step.length() == 0:
step = pygame.Vector2(1, 0)
self.velocity = step.normalize() * 7
self.damage = damage
self.rect = pygame.Rect(0, 0, 10, 10)
self.rect.center = self.pos
def update(self):
self.pos += self.velocity
self.rect.center = self.pos
if not screen.get_rect().colliderect(self.rect):
self.kill()
class Gem(pygame.sprite.Sprite):
def __init__(self, pos):
super().__init__()
self.rect = pygame.Rect(0, 0, 24, 24)
self.rect.center = pos
self.rect.clamp_ip(courtyard)
self.pos = pygame.Vector2(self.rect.center)
def update(self, knight):
target = pygame.Vector2(knight.rect.center)
if self.pos.distance_to(target) < knight.magnet:
self.pos.move_towards_ip(target, 6)
self.rect.center = self.pos
class Night:
def __init__(self):
self.knight = Knight()
self.time = 0
self.monsters = pygame.sprite.Group()
self.bolts = pygame.sprite.Group()
self.bolt_timer = 0
self.spin = 0
self.gems = pygame.sprite.Group()
self.xp = 0
self.level = 1
self.need = 5
self.choices = []
self.pick = 0
self.boss = None
self.state = "playing"
def wave(self):
return min(len(WAVES), self.time * len(WAVES) // NIGHT + 1)
def spawn(self):
wave = self.wave()
if self.time % max(16, 46 - wave * 5) == 0:
kind = random.choice(WAVES[wave - 1])
self.monsters.add(Monster(kind, edge_point()))
if self.boss is None and self.time >= BOSS_AT:
self.boss = Monster("boss", (WIDTH // 2, -40))
self.monsters.add(self.boss)
boss = self.boss
if boss and boss.alive() and boss.hp < MONSTERS["boss"]["hp"] // 2:
if self.time % 120 == 0:
for _ in range(3):
offset = (random.randint(-50, 50), random.randint(-50, 50))
self.monsters.add(Monster("bat", boss.pos + offset))
def nearest(self):
point = pygame.Vector2(self.knight.rect.center)
best = None
for monster in self.monsters:
if best is None or point.distance_to(monster.pos) < point.distance_to(best.pos):
best = monster
return best
def shoot(self):
self.bolt_timer += 1
target = self.nearest()
if self.bolt_timer >= self.knight.bolt_wait and target is not None:
self.bolt_timer = 0
bolt = Bolt(self.knight.rect.center, target.pos, self.knight.bolt_damage)
self.bolts.add(bolt)
def axe_spots(self):
spots = []
for i in range(self.knight.axes):
angle = self.spin + i * 360 / self.knight.axes
offset = pygame.Vector2(70, 0).rotate(angle)
spots.append(pygame.Vector2(self.knight.rect.center) + offset)
return spots
def hurt_monsters(self):
for bolt in self.bolts:
target = pygame.sprite.spritecollideany(bolt, self.monsters)
if target is not None:
target.hit(bolt.damage)
bolt.kill()
for spot in self.axe_spots():
blade = pygame.Rect(0, 0, 36, 36)
blade.center = spot
for monster in self.monsters:
if monster.axe_wait == 0 and blade.colliderect(monster.rect):
monster.hit(1)
monster.axe_wait = 20
for monster in list(self.monsters):
if monster.hp <= 0:
monster.kill()
drops = 8 if monster.kind == "boss" else 1
for _ in range(drops):
self.gems.add(Gem(monster.pos))
def collect(self):
for gem in pygame.sprite.spritecollide(self.knight, self.gems, True):
self.xp += 1
gem_sound.play()
if self.xp >= self.need:
self.xp -= self.need
self.level += 1
self.need += 4
self.choices = random.sample(list(UPGRADES), 3)
self.pick = 1
self.state = "levelup"
level_sound.play()
def upgrade(self, name):
knight = self.knight
if name == "Quick bolts":
knight.bolt_wait = max(8, knight.bolt_wait - 8)
elif name == "Sharp bolts":
knight.bolt_damage += 1
elif name == "Spinning axe":
knight.axes += 1
elif name == "Swift boots":
knight.speed += 1
elif name == "Gem magnet":
knight.magnet += 40
elif name == "Hearty meal":
knight.hearts = min(knight.max_hearts, knight.hearts + 1)
self.state = "playing"
def take_hits(self):
knight = self.knight
if knight.hurt > 0:
knight.hurt -= 1
elif pygame.sprite.spritecollideany(knight, self.monsters):
knight.hearts -= 1
knight.hurt = 75
hurt_sound.play()
if knight.hearts <= 0:
self.state = "lost"
def update(self, keys):
self.time += 1
self.spin = (self.spin + 4) % 360
self.knight.move(keys)
self.spawn()
self.monsters.update(self.knight.rect.center)
self.bolts.update()
self.gems.update(self.knight)
self.shoot()
self.hurt_monsters()
self.collect()
self.take_hits()
night = Night()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if night.state == "levelup":
if event.key == pygame.K_LEFT:
night.pick = (night.pick - 1) % 3
elif event.key == pygame.K_RIGHT:
night.pick = (night.pick + 1) % 3
elif event.key == pygame.K_SPACE:
night.upgrade(night.choices[night.pick])
if night.state == "playing":
night.update(pygame.key.get_pressed())
screen.blit(background, (0, 0))
knight = night.knight
for gem in night.gems:
draw(gem_img, gem.rect.center)
for monster in night.monsters:
if monster.flash > 0:
draw(whiten(monster.image), monster.rect.center)
else:
draw(monster.image, monster.rect.center)
if knight.hurt % 10 < 5:
draw(knight.image, knight.rect.center)
for spot in night.axe_spots():
draw(pygame.transform.rotate(axe_img, -night.spin * 3), spot)
for bolt in night.bolts:
pygame.draw.circle(screen, BOLT, bolt.rect.center, 5)
for i in range(knight.max_hearts):
if i < knight.hearts:
screen.blit(heart_img, (8 + i * 34, 6))
else:
screen.blit(empty_img, (8 + i * 34, 6))
left = font.render(clock_text(max(0, NIGHT - night.time)) + " until dawn", True, WHITE)
screen.blit(left, left.get_rect(center=(WIDTH // 2, 24)))
label = font.render(f"Level {night.level}", True, WHITE)
screen.blit(label, label.get_rect(topright=(WIDTH - 10, 4)))
pygame.draw.rect(screen, (40, 40, 60), (WIDTH - 160, 30, 150, 10))
pygame.draw.rect(screen, GOLD, (WIDTH - 160, 30, 150 * night.xp // night.need, 10))
if night.state == "levelup":
screen.blit(cover, (0, 0))
title = big_font.render("Level up! Pick one", True, GOLD)
screen.blit(title, title.get_rect(center=(WIDTH // 2, 130)))
for i, name in enumerate(night.choices):
card = pygame.Rect(60 + i * 210, 190, 180, 120)
pygame.draw.rect(screen, (40, 40, 60), card)
if i == night.pick:
pygame.draw.rect(screen, GOLD, card, 4)
else:
pygame.draw.rect(screen, WHITE, card, 2)
text = font.render(name, True, WHITE)
screen.blit(text, text.get_rect(center=(card.centerx, card.y + 40)))
about = small_font.render(UPGRADES[name], True, WHITE)
screen.blit(about, about.get_rect(center=(card.centerx, card.y + 80)))
hint = font.render("Left and Right to choose, Space to take it", True, WHITE)
screen.blit(hint, hint.get_rect(center=(WIDTH // 2, 360)))
if night.state == "lost":
message = big_font.render("The keep has fallen", True, WHITE)
screen.blit(message, message.get_rect(center=(WIDTH // 2, 200)))
pygame.display.flip()
clock.tick(60)
pygame.quit()
Todas las lecciones de Proyecto con Pygame: Defiende la fortaleza
Practica por tu cuenta: Playground de Pygame