Menu
CoddyTech

Morgendämmerung

Lektion 18 von 20 im Kurs Pygame-Projekt: Verteidige den Bergfried von Coddy.

Jetzt lässt sich die Nacht bezwingen. Wenn night.time NIGHT erreicht, geht die Sonne auf, der Zustand wird zu "won" und die Schleife wird nicht mehr aktualisiert, wie es auch bei einer Niederlage der Fall ist.

Sorge dafür, dass der Spieler die Dämmerung kommen sieht. Lege die ganze Nacht einen durchscheinenden dunkelblauen Schleier mit einer Deckkraft von 120 von 255 über den Hof und blende ihn während der letzten Minute aus. Erstelle die shade-Surface einmal vor der Schleife, wie den Schleier, und fülle sie in jedem Frame:

dark = min(120, max(0, NIGHT - night.time) * 120 // (60 * 60))
shade.fill((10, 10, 40, dark))
screen.blit(shade, (0, 0))

Bei 30 Sekunden Restzeit ergibt 1800 * 120 // 3600 den Wert 60: halb so dunkel. Zeichne den Schleier nach dem Ritter und den Äxten, aber vor den Geschossen und der oberen Leiste, damit die Magie und die Herzen durch die Dunkelheit hindurchscheinen.

challenge icon

Aufgabe

Mittel
  1. Am Ende von update: Wenn self.time >= NIGHT, während das Spiel läuft, setze self.state = "won".
  2. Erstelle vor der Schleife shade, eine fenstergroße SRCALPHA-Surface, und dark_cover, eine Surface, die mit (0, 0, 0, 210) gefüllt ist. Zeichne den Schatten wie oben nach den Äxten und vor den Bolzen.
  3. Blitte in "won" und "lost" zuerst dark_cover; zeige in "won" "Dawn! The keep holds" in großen goldenen Buchstaben bei (360, 200) an.

Probier es selbst

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()

Alle Lektionen in Pygame-Projekt: Verteidige den Bergfried

Übe selbstständig: Pygame-Playground