Impactos de hacha
Lección 13 de 25 del curso Proyecto con Pygame: Defiende la fortaleza de Coddy.
El hacha atraviesa a un monstruo durante muchos fotogramas, unos 13. Hacerle daño en cada uno de esos fotogramas derretiría cualquier cosa que tocara. Para golpear una sola vez por pasada, el monstruo que recibe un golpe obtiene una espera, la cuenta en su propio update y el hacha solo hace daño a los monstruos cuyo tiempo de espera es 0. Una trampa que se activa una vez por segundo funciona de la misma manera:
if player.trap_wait == 0 and spikes.colliderect(player.rect):
player.hearts -= 1
player.trap_wait = 60La hoja es un Rect propio, de 36 por 36, centrado en cada punto de axe_spots(). rect.colliderect(other) es True cuando dos Rect se superponen.
Desafío
IntermedioHaz que el hacha golpee a cada monstruo una vez por pasada.
- Asigna
Monsteraxe_wait = 0; suupdateresta 1 mientras el valor sea mayor que 0. - En
hurt_monsters, después de los proyectiles: para cada posición, unabladeRect de 36 por 36 centrada en ella golpea a cada monstruo que toque y cuyoaxe_waitsea 0, conmonster.hit(1)ymonster.axe_wait = 20.
Pulsa «Jugar»: los slimes que toca el hacha caen, y un ghost recibe cuatro golpes. Después, pulsa «Comprobar».
Pruébalo tú mismo
import random
import pygame
WIDTH, HEIGHT = 720, 480
TILE = 48
NIGHT = 60 * 60 * 4
# Los tipos de monstruo que envía cada oleada, desde la primera hasta la última
WAVES = [
["slime"],
["slime", "bat"],
["slime", "bat", "spider"],
["bat", "spider"],
["slime", "spider", "ghost"],
["bat", "spider", "ghost"],
["spider", "ghost"],
["bat", "ghost"],
]
WHITE = (240, 240, 240)
BOLT = (120, 200, 255)
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)
big_font = pygame.font.Font("assets/fonts/pixel.ttf", 48)
def load(name):
return pygame.image.load("assets/images/" + name).convert_alpha()
floor_img = load("floor.png")
wall_img = load("wall.png")
knight_img = load("knight.png")
axe_img = load("axe.png")
heart_img = load("heart.png")
empty_img = load("heart_empty.png")
MONSTERS = {
"slime": {"image": load("slime.png"), "speed": 1.0, "hp": 1},
"bat": {"image": load("dungeon_bat.png"), "speed": 1.6, "hp": 1},
"spider": {"image": load("spider.png"), "speed": 1.2, "hp": 3},
"ghost": {"image": load("ghost.png"), "speed": 0.8, "hp": 4},
}
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))
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
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.pos = pygame.Vector2(pos)
self.rect = pygame.Rect(0, 0, 28, 28)
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
def hit(self, damage):
self.hp -= damage
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 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.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()))
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 monster in list(self.monsters):
if monster.hp <= 0:
monster.kill()
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
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.shoot()
self.hurt_monsters()
self.take_hits()
night = Night()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if night.state == "playing":
night.update(pygame.key.get_pressed())
screen.blit(background, (0, 0))
knight = night.knight
for monster in night.monsters:
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)))
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