Bos Memanggil Kelelawar
Pelajaran 17 dari 20 dalam kursus Proyek Pygame: Pertahankan Benteng di Coddy.
Bos yang baik berubah seiring berlangsungnya pertarungan. Saat kesehatannya kurang dari setengah, bos ini meminta bantuan: setiap dua detik, tiga kelelawar muncul di sekitarnya. Pertarungan yang berlangsung dalam beberapa tahap seperti ini disebut fase.
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))boss and boss.alive() berarti: ada bos, dan bos itu belum tumbang. and berhenti pada bagian pertama yang bernilai false, jadi boss.alive() tidak pernah dipanggil pada None.
Vector2 ditambah tuple berisi dua angka akan menghasilkan Vector2, jadi boss.pos + offset adalah titik di dekat bos.
Tantangan
Sedang- Di akhir
spawn, saat boss masih hidup dengan health kurang dari setengah dari 120, tambahkan tiga kelelawar setiap 120 frame, masing-masing berjarak hingga 50 piksel dari boss pada sumbu x dan y.
Coba sendiri
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)
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()
Semua pelajaran di Proyek Pygame: Pertahankan Benteng
Latihan sendiri: Playground Pygame