الزعيم يستدعي الخفافيش
الدرس 17 من 20 في دورة مشروع Pygame: الصمود في القلعة على Coddy.
يتغير الزعيم الجيد مع استمرار القتال. عندما تنخفض صحته إلى أقل من النصف، يستدعي هذا الزعيم المساعدة: تظهر ثلاثة خفافيش حوله كل ثانيتين. تُسمّى المعارك التي تجري على مراحل كهذه أطوارًا.
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(): يوجد زعيم، ولم يسقط. يتوقف and عند أول جزء تكون قيمته خاطئة، لذا لا يُطلب أبدًا من None تنفيذ boss.alive().
جمع Vector2 مع صف يحتوي على عددين يعطي Vector2، لذا فإن boss.pos + offset تمثل نقطة قريبة من الزعيم.
التحدي
متوسط- في نهاية
spawn، ما دام الزعيم حيًا ولديه أقل من نصف صحته البالغة 120، أضف ثلاثة خفافيش كل 120 إطارًا، على ألا يبعد أيٌّ منها عن الزعيم أكثر من 50 بكسلًا في المحورين x وy.
جرّب بنفسك
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()
جميع دروس مشروع Pygame: الصمود في القلعة
تدرّب بنفسك: Playground لـ Pygame