Загрузка данных
import pygame
import random
import math
import sys
pygame.init()
# Константы окна и мира
SCREEN_W, SCREEN_H = 1080, 720
WORLD_W, WORLD_H = 2800, 2200
FPS = 60
screen = pygame.display.set_mode((SCREEN_W, SCREEN_H))
pygame.display.set_caption("The Witcher 2D: Chronicles of Velen")
clock = pygame.time.Clock()
# Шрифты
f_dialog = pygame.font.SysFont("georgia", 18)
f_dialog_name = pygame.font.SysFont("georgia", 19, bold=True)
f_hud = pygame.font.SysFont("consolas", 13, bold=True)
f_quest = pygame.font.SysFont("georgia", 14, bold=True)
f_boss = pygame.font.SysFont("georgia", 20, bold=True)
f_dmg = pygame.font.SysFont("georgia", 16, bold=True)
# ----------------- КАМЕРА -----------------
class Camera:
def __init__(self):
self.x = 0
self.y = 0
self.shake = 0
def update(self, target_pos):
tx = target_pos[0] - SCREEN_W // 2
ty = target_pos[1] - SCREEN_H // 2
self.x += (tx - self.x) * 0.1
self.y += (ty - self.y) * 0.1
self.x = max(0, min(self.x, WORLD_W - SCREEN_W))
self.y = max(0, min(self.y, WORLD_H - SCREEN_H))
if self.shake > 0:
self.x += random.randint(-self.shake, self.shake)
self.y += random.randint(-self.shake, self.shake)
self.shake -= 1
def apply_rect(self, rect):
return rect.move(-int(self.x), -int(self.y))
def apply_pos(self, pos):
return (int(pos[0] - self.x), int(pos[1] - self.y))
camera = Camera()
# ----------------- ГЕНЕРАТОР ПИКСЕЛЬНЫХ МОДЕЛЕЙ -----------------
def generate_witcher_surface(step, facing, attacking):
surf = pygame.Surface((44, 52), pygame.SRCALPHA)
# Сапоги и ноги
leg_offset = int(math.sin(step * 0.25) * 4)
pygame.draw.rect(surf, (20, 18, 15), (12, 38 + leg_offset, 8, 14))
pygame.draw.rect(surf, (20, 18, 15), (24, 38 - leg_offset, 8, 14))
# Кожаный доспех Школы Волка
pygame.draw.rect(surf, (45, 38, 30), (10, 16, 24, 24), border_radius=3)
pygame.draw.rect(surf, (75, 45, 30), (14, 18, 16, 12)) # Жилет
pygame.draw.rect(surf, (150, 150, 150), (20, 22, 4, 4)) # Серебряный медальон
# Два меча за спиной (ножны)
pygame.draw.line(surf, (130, 130, 130), (8, 10), (28, 42), 3) # Стальной
pygame.draw.line(surf, (200, 225, 240), (34, 10), (14, 42), 3) # Серебряный
# Голова и седые волосы Геральта
pygame.draw.circle(surf, (215, 185, 155), (22, 12), 9) # Лицо
pygame.draw.arc(surf, (240, 240, 245), (12, 2, 20, 18), 0, 3.14, 5) # Волосы
pygame.draw.circle(surf, (240, 240, 245), (22, 5), 7)
# Глаза (янтарные)
dx = int(facing.x * 3)
dy = int(facing.y * 2)
pygame.draw.circle(surf, (255, 190, 0), (20 + dx, 12 + dy), 2)
pygame.draw.circle(surf, (255, 190, 0), (25 + dx, 12 + dy), 2)
# Клинок в руке при атаке
if attacking:
pygame.draw.line(surf, (220, 245, 255), (22, 22), (22 + int(facing.x * 24), 22 + int(facing.y * 24)), 4)
return surf
def generate_npc_surface():
surf = pygame.Surface((40, 50), pygame.SRCALPHA)
pygame.draw.rect(surf, (60, 45, 30), (12, 36, 7, 14)) # Ноги
pygame.draw.rect(surf, (60, 45, 30), (21, 36, 7, 14))
pygame.draw.rect(surf, (180, 170, 150), (10, 16, 20, 22), border_radius=2) # Рубаха
pygame.draw.rect(surf, (140, 40, 40), (10, 26, 20, 4)) # Пояс
pygame.draw.circle(surf, (215, 180, 150), (20, 12), 8) # Лицо
pygame.draw.ellipse(surf, (170, 160, 150), (14, 12, 12, 10)) # Борода
return surf
def generate_house_surface(w, h):
surf = pygame.Surface((w, h), pygame.SRCALPHA)
# Стены сруба
pygame.draw.rect(surf, (70, 48, 30), (10, 40, w - 20, h - 40))
for y in range(40, h, 14):
pygame.draw.line(surf, (45, 30, 20), (10, y), (w - 10, y), 2)
# Дверь
pygame.draw.rect(surf, (40, 25, 15), (w//2 - 14, h - 36, 28, 36))
# Соломенная крыша
pygame.draw.polygon(surf, (130, 110, 60), [(0, 40), (w//2, 0), (w, 40)])
pygame.draw.polygon(surf, (150, 130, 70), [(8, 40), (w//2, 6), (w - 8, 40)], 2)
return surf
# ----------------- ИГРОК -----------------
class Witcher:
def __init__(self, x, y):
self.rect = pygame.Rect(x, y, 32, 40)
self.hp = 180
self.max_hp = 180
self.stamina = 100
self.max_stamina = 100
self.toxicity = 0
self.coins = 50
self.potions = 3
self.speed = 4.2
self.facing = pygame.Vector2(0, 1)
self.step = 0
self.sword = "SILVER" # "SILVER" или "STEEL"
self.attacking = False
self.atk_timer = 0
self.attack_box = pygame.Rect(0, 0, 0, 0)
self.dodge_timer = 0
self.quen = False
self.witcher_sense = False
def handle_input(self, keys, mouse_pressed):
self.witcher_sense = mouse_pressed[2]
move = pygame.Vector2(0, 0)
if keys[pygame.K_w]: move.y -= 1
if keys[pygame.K_s]: move.y += 1
if keys[pygame.K_a]: move.x -= 1
if keys[pygame.K_d]: move.x += 1
if move.length() > 0:
self.facing = move.normalize()
self.step += 1
else:
self.step = 0
spd = self.speed * 2.2 if self.dodge_timer > 0 else (self.speed * 0.65 if self.witcher_sense else self.speed)
return move.normalize() * spd if move.length() > 0 else pygame.Vector2(0, 0)
def update(self, move, obstacles):
if self.stamina < self.max_stamina:
self.stamina = min(self.max_stamina, self.stamina + 0.4)
if self.toxicity > 0:
self.toxicity = max(0, self.toxicity - 0.03)
if self.dodge_timer > 0:
self.dodge_timer -= 1
if self.atk_timer > 0:
self.atk_timer -= 1
if self.atk_timer == 0:
self.attacking = False
self.attack_box = pygame.Rect(0, 0, 0, 0)
# Коллизии X
self.rect.x += int(move.x)
for obs in obstacles:
if self.rect.colliderect(obs):
if move.x > 0: self.rect.right = obs.left
if move.x < 0: self.rect.left = obs.right
# Коллизии Y
self.rect.y += int(move.y)
for obs in obstacles:
if self.rect.colliderect(obs):
if move.y > 0: self.rect.bottom = obs.top
if move.y < 0: self.rect.top = obs.bottom
self.rect.clamp_ip(pygame.Rect(0, 0, WORLD_W, WORLD_H))
def attack(self):
if not self.attacking and self.stamina >= 14:
self.attacking = True
self.stamina -= 14
self.atk_timer = 15
reach = 40
ax = self.rect.centerx + int(self.facing.x * reach) - 25
ay = self.rect.centery + int(self.facing.y * reach) - 25
self.attack_box = pygame.Rect(ax, ay, 50, 50)
def draw(self, surf):
r = camera.apply_rect(self.rect)
# Тень
pygame.draw.ellipse(surf, (15, 20, 15, 120), (r.centerx - 16, r.bottom - 6, 32, 12))
sprite = generate_witcher_surface(self.step, self.facing, self.attacking)
surf.blit(sprite, (r.centerx - 22, r.centery - 26))
if self.quen:
pygame.draw.circle(surf, (255, 220, 30), r.center, 32, 2)
# ----------------- ДИАЛОГИ И КВЕСТОВЫЙ NPC -----------------
class QuestNPC:
def __init__(self, x, y, name="Староста Богдан"):
self.rect = pygame.Rect(x, y, 32, 42)
self.name = name
self.quest_stage = 0 # 0 - не взят, 1 - в процессе, 2 - выполнен
self.sprite = generate_npc_surface()
def get_dialog(self):
if self.quest_stage == 0:
return {
"speaker": self.name,
"text": "Ведьмак! Слава Мелитэле, ты пришел. В лесу к востоку завелась Бестия. Людей на куски рвет. Поможешь?",
"options": [("1. Беру заказ. [150 крон]", 1), ("2. Я подумаю.", -1)]
}
elif self.quest_stage == 1:
return {
"speaker": self.name,
"text": "Ты еще не убил тварь? Ищи следы когтей на земле у опушки леса, включи свое чутьё!",
"options": [("1. Уже иду.", -1)]
}
elif self.quest_stage == 2:
return {
"speaker": self.name,
"text": "Деревня спасена! Вот твоя награда, мастер Ведьмак. Заходи к нам в корчму всегда!",
"options": [("1. Забрать 150 крон", 3)]
}
else:
return {
"speaker": self.name,
"text": "Спокойных тебе дорог, белый волк.",
"options": [("1. Бывай.", -1)]
}
def draw(self, surf):
r = camera.apply_rect(self.rect)
surf.blit(self.sprite, (r.x - 4, r.y - 6))
# Восклицательный знак над головой
if self.quest_stage in (0, 2):
pygame.draw.circle(surf, (240, 200, 20), (r.centerx, r.top - 16), 8)
t = f_hud.render("!", True, (10, 10, 10))
surf.blit(t, (r.centerx - 3, r.top - 23))
# ----------------- БОСС (БЕСТИЯ ВЕЛЕНА) -----------------
class VelenBeast:
def __init__(self, x, y):
self.rect = pygame.Rect(x, y, 70, 70)
self.max_hp = 600
self.hp = 600
self.alive = True
self.speed = 3.2
self.attack_cd = 0
self.roar_timer = 0
def update(self, player, dmg_texts):
if not self.alive: return
self.attack_cd = max(0, self.attack_cd - 1)
v = pygame.Vector2(player.rect.center) - pygame.Vector2(self.rect.center)
dist = v.length()
if 35 < dist < 650:
d = v.normalize()
self.rect.x += int(d.x * self.speed)
self.rect.y += int(d.y * self.speed)
# Удар лапой
if self.rect.colliderect(player.rect) and self.attack_cd == 0 and player.dodge_timer == 0:
self.attack_cd = 50
if player.quen:
player.quen = False
dmg_texts.append(FloatingText(player.rect.centerx, player.rect.top, "КВЕН РАЗБИТ", (255, 220, 50)))
else:
dmg = random.randint(28, 42)
player.hp -= dmg
camera.shake = 10
dmg_texts.append(FloatingText(player.rect.centerx, player.rect.top, f"-{dmg}", (255, 40, 40), 20))
def draw(self, surf, sense_active):
if not self.alive: return
r = camera.apply_rect(self.rect)
# Модель Бестии (Массивный темный оборотень с клыками)
pygame.draw.ellipse(surf, (15, 15, 15, 140), (r.centerx - 32, r.bottom - 10, 64, 20))
pygame.draw.rect(surf, (35, 30, 32), r, border_radius=10)
pygame.draw.polygon(surf, (60, 20, 20), [(r.centerx, r.top + 30), (r.left + 15, r.top + 5), (r.right - 15, r.top + 5)])
# Глаза бестии
pygame.draw.circle(surf, (255, 30, 20), (r.centerx - 12, r.top + 22), 4)
pygame.draw.circle(surf, (255, 30, 20), (r.centerx + 12, r.top + 22), 4)
if sense_active:
glow = pygame.Surface((r.w + 20, r.h + 20), pygame.SRCALPHA)
pygame.draw.rect(glow, (255, 50, 0, 160), glow.get_rect(), width=4, border_radius=12)
surf.blit(glow, (r.x - 10, r.y - 10))
# ----------------- СЛЕДЫ ЧУДОВИЩА (ВЕДЬМАЧЬЕ ЧУТЬЕ) -----------------
class ClueTrack:
def __init__(self, x, y):
self.x, self.y = x, y
def draw(self, surf, sense_active):
if sense_active:
pos = camera.apply_pos((self.x, self.y))
# Светящиеся красные когтистые следы
pygame.draw.circle(surf, (255, 30, 30), pos, 5)
pygame.draw.line(surf, (255, 40, 40), (pos[0]-6, pos[1]-8), (pos[0]+6, pos[1]-4), 2)
pygame.draw.line(surf, (255, 40, 40), (pos[0]-8, pos[1]), (pos[0]+8, pos[1]+2), 2)
class FloatingText:
def __init__(self, x, y, text, color, size=16):
self.x, self.y = x, y
self.text = text
self.color = color
self.life = 40
self.font = pygame.font.SysFont("georgia", size, bold=True)
def update(self):
self.y -= 0.9
self.life -= 1
def draw(self, surf):
r = self.font.render(self.text, True, self.color)
surf.blit(r, camera.apply_pos((self.x, self.y)))
# ----------------- СОЗДАНИЕ МИРА -----------------
houses = [
(pygame.Rect(400, 350, 140, 120), generate_house_surface(140, 120)),
(pygame.Rect(650, 280, 160, 130), generate_house_surface(160, 130)),
(pygame.Rect(460, 600, 150, 120), generate_house_surface(150, 120))
]
obstacles = [h[0] for h in houses]
# Деревья на окраинах
for _ in range(45):
obstacles.append(pygame.Rect(random.randint(1100, WORLD_W - 150), random.randint(100, WORLD_H - 150), 55, 55))
witcher = Witcher(520, 520)
elder = QuestNPC(580, 480)
beast = VelenBeast(2200, 1400)
# Следы от деревни в лес
clues = []
start_p = pygame.Vector2(700, 500)
end_p = pygame.Vector2(2150, 1380)
for i in range(18):
p = start_p.lerp(end_p, i / 18.0)
clues.append(ClueTrack(p.x + random.randint(-15, 15), p.y + random.randint(-15, 15)))
dmg_texts = []
active_dialog = None
# ----------------- ГЛАВНЫЙ ЦИКЛ -----------------
running = True
while running:
keys = pygame.key.get_pressed()
mouse = pygame.mouse.get_pressed()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
# Диалог
if active_dialog:
if event.key == pygame.K_1:
choice = active_dialog["options"][0][1]
if choice == 1: # Взял квест
elder.quest_stage = 1
elif choice == 3: # Сдал квест
elder.quest_stage = 3
witcher.coins += 150
dmg_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "+150 КРОН", (255, 215, 0), 20))
active_dialog = None
elif event.key == pygame.K_2 or event.key == pygame.K_ESCAPE:
active_dialog = None
continue
# Открыть диалог
if event.key == pygame.K_e and math.hypot(witcher.rect.centerx - elder.rect.centerx, witcher.rect.centery - elder.rect.centery) < 65:
active_dialog = elder.get_dialog()
# Атака
if event.key == pygame.K_SPACE:
witcher.attack()
# Кувырок
if event.key == pygame.K_LSHIFT and witcher.dodge_timer == 0 and witcher.stamina >= 25:
witcher.dodge_timer = 15
witcher.stamina -= 25
# Меч
if event.key == pygame.K_r:
witcher.sword = "STEEL" if witcher.sword == "SILVER" else "SILVER"
name = "СЕРЕБРЯНЫЙ МЕЧ" if witcher.sword == "SILVER" else "СТАЛЬНОЙ МЕЧ"
dmg_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, name, (210, 230, 255)))
# Зелье
if event.key == pygame.K_f and witcher.potions > 0 and witcher.toxicity <= 75:
witcher.potions -= 1
witcher.hp = min(witcher.max_hp, witcher.hp + 60)
witcher.toxicity += 30
dmg_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "+60 HP (Ласточка)", (50, 255, 120)))
# Знаки
if event.key == pygame.K_1 and witcher.stamina >= 35: # Игни
witcher.stamina -= 35
camera.shake = 6
if math.hypot(beast.rect.centerx - witcher.rect.centerx, beast.rect.centery - witcher.rect.centery) < 220:
beast.hp -= 45
dmg_texts.append(FloatingText(beast.rect.centerx, beast.rect.top, "ИГНИ: -45", (255, 120, 20), 18))
if event.key == pygame.K_2 and witcher.stamina >= 40: # Квен
witcher.stamina -= 40
witcher.quen = True
# Обновление
if not active_dialog:
move = witcher.handle_input(keys, mouse)
witcher.update(move, obstacles)
camera.update(witcher.rect.center)
# Проверка попаданий меча
if witcher.attacking and beast.alive and witcher.attack_box.colliderect(beast.rect):
mult = 1.0 if witcher.sword == "SILVER" else 0.25
dmg = int(random.randint(30, 42) * mult)
beast.hp -= dmg
camera.shake = 5
dmg_texts.append(FloatingText(beast.rect.centerx, beast.rect.top, f"-{dmg}", (255, 240, 200), 18))
witcher.attack_box = pygame.Rect(0, 0, 0, 0)
if beast.hp <= 0:
beast.alive = False
elder.quest_stage = 2
dmg_texts.append(FloatingText(beast.rect.centerx, beast.rect.top, "ТРОФЕЙ ПОЛУЧЕН", (255, 215, 0), 22))
beast.update(witcher, dmg_texts)
for dt in dmg_texts[:]:
dt.update()
if dt.life <= 0: dmg_texts.remove(dt)
# ----------------- ОТРИСОВКА -----------------
# Земля (Ведьмачье чутье обесцвечивает мир)
bg_col = (25, 25, 25) if witcher.witcher_sense else (42, 52, 40)
screen.fill(bg_col)
# Дорога к логову
p1 = camera.apply_pos(start_p)
p2 = camera.apply_pos(end_p)
pygame.draw.line(screen, (35, 30, 25) if not witcher.witcher_sense else (20, 20, 20), p1, p2, 34)
# Дома
for rect, surf in houses:
screen.blit(surf, camera.apply_rect(rect))
# Деревья
for obs in obstacles[len(houses):]:
r = camera.apply_rect(obs)
pygame.draw.circle(screen, (20, 30, 20) if not witcher.witcher_sense else (18, 18, 18), (r.centerx, r.centery), 38)
pygame.draw.rect(screen, (40, 30, 20), (r.centerx - 6, r.centery - 6, 12, 16))
# Следы чудовища
for c in clues:
c.draw(screen, witcher.witcher_sense)
elder.draw(screen)
beast.draw(screen, witcher.witcher_sense)
witcher.draw(screen)
for dt in dmg_texts:
dt.draw(screen)
# ----------------- КРУГЛАЯ МИНИ-КАРТА (WITCHER 3 STYLE) -----------------
mm_center = (SCREEN_W - 90, 90)
mm_radius = 65
pygame.draw.circle(screen, (20, 22, 20), mm_center, mm_radius)
pygame.draw.circle(screen, (160, 140, 100), mm_center, mm_radius, 3) # Золотая рамка
# Точки на радаре
radar_scale = 0.08
# Игрок (Белая стрелка в центре)
pygame.draw.circle(screen, (240, 240, 250), mm_center, 4)
# NPC (Желтая точка)
en_rel = (pygame.Vector2(elder.rect.center) - pygame.Vector2(witcher.rect.center)) * radar_scale
if en_rel.length() < mm_radius - 6:
pygame.draw.circle(screen, (255, 215, 0), (int(mm_center[0] + en_rel.x), int(mm_center[1] + en_rel.y)), 4)
# Босс (Красная точка)
if beast.alive:
b_rel = (pygame.Vector2(beast.rect.center) - pygame.Vector2(witcher.rect.center)) * radar_scale
if b_rel.length() < mm_radius - 6:
pygame.draw.circle(screen, (255, 40, 40), (int(mm_center[0] + b_rel.x), int(mm_center[1] + b_rel.y)), 5)
# ----------------- HUD И КВЕСТ-ТРЕКЕР -----------------
# Медальон и полоски здоровья/энергии
pygame.draw.circle(screen, (35, 35, 35), (55, 55), 30)
pygame.draw.polygon(screen, (190, 190, 200), [(55, 36), (40, 58), (55, 54), (70, 58)])
# HP
pygame.draw.rect(screen, (30, 20, 20), (95, 35, 200, 14), border_radius=2)
pygame.draw.rect(screen, (200, 30, 30), (95, 35, int(200 * (witcher.hp / witcher.max_hp)), 14), border_radius=2)
# Выносливость
pygame.draw.rect(screen, (30, 30, 20), (95, 52, 160, 8), border_radius=2)
pygame.draw.rect(screen, (230, 180, 20), (95, 52, int(160 * (witcher.stamina / witcher.max_stamina)), 8), border_radius=2)
# Деньги и меч
screen.blit(f_hud.render(f"Кроны: {witcher.coins} | Ласточка [F]: {witcher.potions}", True, (240, 220, 160)), (95, 68))
sword_name = "СЕРЕБРО (Монстры)" if witcher.sword == "SILVER" else "СТАЛЬ (Люди)"
screen.blit(f_hud.render(f"Меч [R]: {sword_name}", True, (200, 230, 255)), (95, 84))
# Журнал активного квеста
q_box = pygame.Rect(SCREEN_W - 320, 175, 300, 80)
pygame.draw.rect(screen, (15, 18, 15, 180), q_box, border_radius=5)
screen.blit(f_quest.render("АКТИВНЫЙ ЗАКАЗ:", True, (240, 200, 80)), (SCREEN_W - 310, 182))
q_texts = [
"Поговорите со Старостой в деревне [E]",
"Включите чутьё [ПКМ] и идите по следам",
"Вернитесь к Старосте за наградой!",
"Заказ выполнен! Земли очищены."
]
screen.blit(f_dialog.render(q_texts[elder.quest_stage], True, (220, 220, 220)), (SCREEN_W - 310, 208))
# ХП Босса
if beast.alive and math.hypot(witcher.rect.centerx - beast.rect.centerx, witcher.rect.centery - beast.rect.centery) < 650:
bx = SCREEN_W // 2 - 200
pygame.draw.rect(screen, (20, 20, 20), (bx, 20, 400, 14), border_radius=2)
pygame.draw.rect(screen, (180, 25, 25), (bx, 20, int(400 * (beast.hp / beast.max_hp)), 14), border_radius=2)
b_name = f_boss.render("БЕСТИЯ ИЗ ЗАЛИПЬЯ", True, (240, 230, 210))
screen.blit(b_name, (SCREEN_W // 2 - b_name.get_width() // 2, 38))
# Диалоговое окно (если открыто)
if active_dialog:
d_rect = pygame.Rect(80, SCREEN_H - 180, SCREEN_W - 160, 150)
pygame.draw.rect(screen, (20, 18, 16), d_rect, border_radius=8)
pygame.draw.rect(screen, (140, 120, 80), d_rect, width=3, border_radius=8)
screen.blit(f_dialog_name.render(active_dialog["speaker"], True, (240, 200, 80)), (105, SCREEN_H - 170))
screen.blit(f_dialog.render(active_dialog["text"], True, (230, 230, 230)), (105, SCREEN_H - 140))
y_opt = SCREEN_H - 95
for opt in active_dialog["options"]:
screen.blit(f_dialog.render(opt[0], True, (120, 220, 255)), (105, y_opt))
y_opt += 25
pygame.display.flip()
clock.tick(FPS)
pygame.quit()
sys.exit()