Загрузка данных
import pygame
import random
import math
import sys
pygame.init()
# =====================================================================
# КОНФИГУРАЦИЯ И ПАРАМЕТРЫ
# =====================================================================
SCREEN_W, SCREEN_H = 1200, 750
WORLD_W, WORLD_H = 4500, 3600
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_tiny = pygame.font.SysFont("georgia", 11)
f_hud = pygame.font.SysFont("consolas", 12, bold=True)
f_ui = pygame.font.SysFont("georgia", 14, bold=True)
f_dialog = pygame.font.SysFont("georgia", 16)
f_dialog_name = pygame.font.SysFont("georgia", 18, bold=True)
f_title = pygame.font.SysFont("georgia", 24, bold=True)
f_cinematic = pygame.font.SysFont("georgia", 20, italic=True)
# Палитра
C_GROUND = (36, 48, 32)
C_GROUND_SENSE = (18, 20, 18)
C_GOLD = (245, 205, 60)
C_SILVER = (210, 235, 255)
C_STEEL = (225, 205, 165)
C_HP = (220, 40, 40)
C_STAMINA = (240, 185, 25)
C_TOXIC = (40, 215, 80)
C_UI_BG = (22, 19, 17)
C_UI_BORDER = (150, 125, 75)
# =====================================================================
# КАМЕРА
# =====================================================================
class Camera:
def __init__(self):
self.x = 0.0
self.y = 0.0
self.shake = 0.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.12
self.y += (ty - self.y) * 0.12
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.uniform(-self.shake, self.shake)
self.y += random.uniform(-self.shake, self.shake)
self.shake = max(0.0, self.shake - 0.7)
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))
def screen_to_world(self, pos):
return (pos[0] + int(self.x), pos[1] + int(self.y))
camera = Camera()
# =====================================================================
# ПРОЦЕДУРНАЯ ГРАФИКА ДЕТАЛИЗИРОВАННЫХ МОДЕЛЕЙ (БЕЗ КВАДРАТОВ)
# =====================================================================
def create_witcher_sprite(step, facing, attacking, sword_type):
surf = pygame.Surface((54, 62), pygame.SRCALPHA)
cx, cy = 27, 30
leg = int(math.sin(step * 0.28) * 5)
# 1. Тень под ногами
pygame.draw.ellipse(surf, (15, 18, 15, 120), (cx - 16, 52, 32, 10))
# 2. Ноги и кожаные сапоги с пряжками
pygame.draw.rect(surf, (22, 18, 14), (16, 40 + leg, 8, 14))
pygame.draw.rect(surf, (22, 18, 14), (30, 40 - leg, 8, 14))
pygame.draw.rect(surf, (60, 50, 40), (16, 44 + leg, 8, 3))
pygame.draw.rect(surf, (60, 50, 40), (30, 44 - leg, 8, 3))
# 3. Доспех Школы Волка (куртка, заклепки, портупея)
pygame.draw.rect(surf, (45, 38, 30), (14, 18, 26, 24), border_radius=4)
pygame.draw.rect(surf, (85, 48, 30), (18, 20, 18, 14))
pygame.draw.line(surf, (120, 75, 45), (14, 20), (40, 38), 2) # Портупея
pygame.draw.circle(surf, (190, 190, 200), (cx, 24), 3) # Серебряный медальон
# 4. Мечи за спиной крест-накрест
pygame.draw.line(surf, (140, 140, 150), (10, 10), (36, 46), 3) # Стальной
pygame.draw.line(surf, (215, 235, 255), (44, 10), (18, 46), 3) # Серебряный
# 5. Голова, лицо, шрам и волосы
pygame.draw.circle(surf, (220, 185, 155), (cx, 13), 9)
pygame.draw.line(surf, (160, 60, 60), (cx + 2, 7), (cx + 4, 17), 1) # Шрам через глаз
# Волосы и хвост
pygame.draw.arc(surf, (245, 245, 250), (15, 2, 24, 18), 0, 3.14, 5)
pygame.draw.circle(surf, (245, 245, 250), (cx, 6), 7)
pygame.draw.rect(surf, (245, 245, 250), (cx - 10, 10, 4, 9))
pygame.draw.rect(surf, (245, 245, 250), (cx + 6, 10, 4, 9))
# 6. Янтарные ведьмачьи глаза
dx, dy = int(facing.x * 3), int(facing.y * 2)
pygame.draw.circle(surf, (255, 195, 0), (cx - 4 + dx, 13 + dy), 2)
pygame.draw.circle(surf, (255, 195, 0), (cx + 4 + dx, 13 + dy), 2)
# 7. Клинок при атаке
if attacking:
col = (215, 240, 255) if sword_type == "SILVER" else (235, 210, 170)
hx = cx + int(facing.x * 12)
hy = cy + int(facing.y * 12)
tx = hx + int(facing.x * 30)
ty = hy + int(facing.y * 30)
pygame.draw.line(surf, col, (hx, hy), (tx, ty), 4)
pygame.draw.circle(surf, (255, 255, 255), (tx, ty), 3)
return surf
def create_npc_sprite(role):
surf = pygame.Surface((48, 56), pygame.SRCALPHA)
cx = 24
pygame.draw.rect(surf, (45, 35, 25), (15, 38, 8, 16))
pygame.draw.rect(surf, (45, 35, 25), (25, 38, 8, 16))
if role == "elder":
pygame.draw.rect(surf, (190, 180, 160), (13, 16, 22, 24), border_radius=3)
pygame.draw.rect(surf, (160, 40, 40), (13, 26, 22, 4))
pygame.draw.circle(surf, (220, 185, 155), (cx, 13), 9)
pygame.draw.ellipse(surf, (200, 195, 190), (16, 12, 16, 14)) # Борода
elif role == "herbalist":
pygame.draw.rect(surf, (45, 115, 75), (13, 16, 22, 25), border_radius=4)
pygame.draw.circle(surf, (230, 195, 165), (cx, 13), 9)
pygame.draw.arc(surf, (160, 50, 25), (14, 4, 20, 16), 0, 3.14, 5)
pygame.draw.circle(surf, (160, 50, 25), (cx, 7), 8)
elif role == "baron":
pygame.draw.rect(surf, (110, 35, 45), (12, 15, 24, 26), border_radius=4)
pygame.draw.rect(surf, (200, 175, 70), (12, 15, 24, 5))
pygame.draw.circle(surf, (220, 185, 155), (cx, 13), 10)
pygame.draw.ellipse(surf, (60, 45, 35), (16, 14, 16, 8)) # Усы
return surf
def create_house_surface(w, h):
surf = pygame.Surface((w, h), pygame.SRCALPHA)
pygame.draw.rect(surf, (75, 50, 32), (12, 45, w - 24, h - 45))
for y in range(45, h, 14):
pygame.draw.line(surf, (48, 32, 20), (12, y), (w - 12, y), 2)
pygame.draw.rect(surf, (40, 25, 15), (w // 2 - 16, h - 42, 32, 42))
pygame.draw.rect(surf, (180, 210, 230), (25, 60, 22, 22))
pygame.draw.rect(surf, (50, 35, 20), (25, 60, 22, 22), 2)
pygame.draw.polygon(surf, (140, 115, 60), [(0, 45), (w // 2, 0), (w, 45)])
pygame.draw.polygon(surf, (160, 135, 75), [(8, 45), (w // 2, 7), (w - 8, 45)], 2)
return surf
# =====================================================================
# ИГРОК
# =====================================================================
class Witcher:
def __init__(self, x, y):
self.rect = pygame.Rect(x, y, 34, 46)
self.hp = 250
self.max_hp = 250
self.stamina = 100
self.max_stamina = 100
self.toxicity = 0
self.coins = 150
self.potions = 3
self.herbs = 2
self.speed = 4.4
self.facing = pygame.Vector2(0, 1)
self.step = 0
self.sword = "SILVER" # "SILVER" или "STEEL"
self.attacking = False
self.atk_timer = 0
self.attack_hitbox = pygame.Rect(0, 0, 0, 0)
self.dodge_timer = 0
self.quen = False
self.sense = False
def update(self, obstacles):
if self.stamina < self.max_stamina:
self.stamina = min(self.max_stamina, self.stamina + 0.45)
if self.toxicity > 0:
self.toxicity = max(0.0, self.toxicity - 0.035)
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_hitbox = pygame.Rect(0, 0, 0, 0)
def move(self, move_vec, obstacles):
spd = self.speed * 2.2 if self.dodge_timer > 0 else (self.speed * 0.6 if self.sense else self.speed)
if move_vec.length() > 0:
self.step += 1
self.rect.x += int(move_vec.x * spd)
for obs in obstacles:
if self.rect.colliderect(obs):
if move_vec.x > 0: self.rect.right = obs.left
if move_vec.x < 0: self.rect.left = obs.right
self.rect.y += int(move_vec.y * spd)
for obs in obstacles:
if self.rect.colliderect(obs):
if move_vec.y > 0: self.rect.bottom = obs.top
if move_vec.y < 0: self.rect.top = obs.bottom
else:
self.step = 0
self.rect.clamp_ip(pygame.Rect(0, 0, WORLD_W, WORLD_H))
def attack_mouse(self, target_world_pos):
if not self.attacking and self.stamina >= 14:
self.attacking = True
self.stamina -= 14
self.atk_timer = 16
dir_vec = pygame.Vector2(target_world_pos) - pygame.Vector2(self.rect.center)
if dir_vec.length() > 0:
self.facing = dir_vec.normalize()
reach = 46
ax = self.rect.centerx + int(self.facing.x * reach) - 30
ay = self.rect.centery + int(self.facing.y * reach) - 30
self.attack_hitbox = pygame.Rect(ax, ay, 60, 60)
def drink_potion(self, texts):
if self.potions > 0 and self.toxicity <= 80:
self.potions -= 1
self.hp = min(self.max_hp, self.hp + 70)
self.toxicity += 30
texts.append(FloatingText(self.rect.centerx, self.rect.top, "+70 HP (Ласточка)", (60, 255, 120)))
elif self.toxicity > 80:
texts.append(FloatingText(self.rect.centerx, self.rect.top, "Высокая токсичность!", (255, 50, 50)))
def draw(self, surf):
r = camera.apply_rect(self.rect)
spr = create_witcher_sprite(self.step, self.facing, self.attacking, self.sword)
surf.blit(spr, (r.centerx - 27, r.centery - 31))
if self.quen:
pygame.draw.circle(surf, (255, 220, 30), r.center, 36, 2)
qs = pygame.Surface((72, 72), pygame.SRCALPHA)
pygame.draw.circle(qs, (255, 220, 30, 45), (36, 36), 36)
surf.blit(qs, (r.centerx - 36, r.centery - 36))
# =====================================================================
# ВРАГИ: УТОПЕЦ И ДРЕВНИЙ ЛЕШИЙ
# =====================================================================
class Enemy:
def __init__(self, x, y, e_type):
self.rect = pygame.Rect(x, y, 34, 44)
self.e_type = e_type
self.is_monster = (e_type == "DROWNER")
self.hp = 85 if self.is_monster else 100
self.max_hp = self.hp
self.speed = 2.8 if self.is_monster else 2.5
self.hit_cd = 0
self.slow_timer = 0
self.stun_timer = 0
def update(self, player, texts):
if self.stun_timer > 0:
self.stun_timer -= 1
return
if self.slow_timer > 0:
self.slow_timer -= 1
spd = self.speed * 0.35 if self.slow_timer > 0 else self.speed
self.hit_cd = max(0, self.hit_cd - 1)
v = pygame.Vector2(player.rect.center) - pygame.Vector2(self.rect.center)
dist = v.length()
if 20 < dist < 500:
d = v.normalize()
self.rect.x += int(d.x * spd)
self.rect.y += int(d.y * spd)
if self.rect.colliderect(player.rect) and self.hit_cd == 0 and player.dodge_timer == 0:
self.hit_cd = 45
if player.quen:
player.quen = False
texts.append(FloatingText(player.rect.centerx, player.rect.top, "КВЕН СПАС!", C_GOLD))
else:
dmg = random.randint(18, 28)
player.hp -= dmg
camera.shake = 6.0
texts.append(FloatingText(player.rect.centerx, player.rect.top, f"-{dmg}", C_HP))
def draw(self, surf, sense_active):
r = camera.apply_rect(self.rect)
if self.e_type == "DROWNER":
pygame.draw.rect(surf, (40, 120, 125), r, border_radius=4)
pygame.draw.circle(surf, (255, 45, 45), (r.centerx - 4, r.y + 8), 2)
pygame.draw.circle(surf, (255, 45, 45), (r.centerx + 4, r.y + 8), 2)
else:
pygame.draw.rect(surf, (95, 60, 45), r, border_radius=3)
pygame.draw.circle(surf, (220, 185, 145), (r.centerx, r.y + 8), 6)
if self.stun_timer > 0:
pygame.draw.circle(surf, (140, 220, 255), (r.centerx, r.top - 8), 5, 2)
if sense_active:
glow = pygame.Surface((r.w + 14, r.h + 14), pygame.SRCALPHA)
pygame.draw.rect(glow, (255, 80, 0, 160) if self.is_monster else (230, 220, 50, 160), glow.get_rect(), width=3, border_radius=6)
surf.blit(glow, (r.x - 7, r.y - 7))
pct = max(0, self.hp / self.max_hp)
pygame.draw.rect(surf, (20, 20, 20), (r.x, r.y - 8, self.rect.w, 4))
pygame.draw.rect(surf, C_HP, (r.x, r.y - 8, int(self.rect.w * pct), 4))
class AncientLeshenBoss:
def __init__(self, x, y):
self.rect = pygame.Rect(x, y, 80, 110)
self.max_hp = 850
self.hp = 850
self.alive = True
self.teleport_cd = 220
self.root_cd = 140
self.slow_timer = 0
self.stun_timer = 0
def update(self, player, roots, texts, particles):
if not self.alive: return
if self.stun_timer > 0:
self.stun_timer -= 1
return
if self.slow_timer > 0: self.slow_timer -= 1
self.teleport_cd -= 1
self.root_cd -= 1
v = pygame.Vector2(player.rect.center) - pygame.Vector2(self.rect.center)
dist = v.length()
if self.teleport_cd <= 0 and dist < 800:
self.teleport_cd = 250
for _ in range(40):
particles.append(Particle(self.rect.centerx, self.rect.centery, random.uniform(-5, 5), random.uniform(-5, 5), (35, 35, 35), random.randint(6, 12), 0.22, "glow"))
ang = random.uniform(0, 6.28)
self.rect.x = int(player.rect.centerx + math.cos(ang) * 230)
self.rect.y = int(player.rect.centery + math.sin(ang) * 230)
texts.append(FloatingText(self.rect.centerx, self.rect.top, "*Шелест вековых ветвей*", (190, 230, 190), 16))
if self.root_cd <= 0 and dist < 700:
self.root_cd = 160
roots.append(RootTrap(player.rect.centerx, player.rect.centery))
texts.append(FloatingText(self.rect.centerx, self.rect.top, "ГНЕВ ЧАЩИ!", (140, 210, 90), 18))
spd = 1.7 if self.slow_timer == 0 else 0.75
if dist > 45:
d = v.normalize()
self.rect.x += int(d.x * spd)
self.rect.y += int(d.y * spd)
def draw(self, surf, sense_active):
if not self.alive: return
r = camera.apply_rect(self.rect)
pygame.draw.ellipse(surf, (0, 0, 0, 140), (r.centerx - 32, r.bottom - 12, 64, 24))
pygame.draw.rect(surf, (42, 28, 20), r, border_radius=6)
pygame.draw.polygon(surf, (225, 220, 195), [(r.centerx, r.top + 36), (r.left + 14, r.top + 8), (r.right - 14, r.top + 8)])
pygame.draw.line(surf, (30, 20, 15), (r.left + 16, r.top + 8), (r.left - 20, r.top - 26), 4)
pygame.draw.line(surf, (30, 20, 15), (r.right - 16, r.top + 8), (r.right + 20, r.top - 26), 4)
pygame.draw.circle(surf, (180, 255, 215), (r.centerx - 8, r.top + 18), 3)
pygame.draw.circle(surf, (180, 255, 215), (r.centerx + 8, r.top + 18), 3)
if sense_active:
glow = pygame.Surface((r.w + 26, r.h + 26), pygame.SRCALPHA)
pygame.draw.rect(glow, (255, 35, 0, 180), glow.get_rect(), width=4, border_radius=8)
surf.blit(glow, (r.x - 13, r.y - 13))
# =====================================================================
# СПЕЦЭФФЕКТЫ, ЛОВУШКИ И ЧАСТИЦЫ
# =====================================================================
class Particle:
def __init__(self, x, y, vx, vy, color, radius, decay, p_type="circle"):
self.x, self.y = x, y
self.vx, self.vy = vx, vy
self.color, self.radius, self.decay, self.p_type = color, radius, decay, p_type
def update(self):
self.x += self.vx
self.y += self.vy
self.radius -= self.decay
def draw(self, surf):
if self.radius <= 0: return
sp = camera.apply_pos((self.x, self.y))
if self.p_type == "circle":
pygame.draw.circle(surf, self.color, sp, int(self.radius))
elif self.p_type == "glow":
s = pygame.Surface((int(self.radius * 4), int(self.radius * 4)), pygame.SRCALPHA)
pygame.draw.circle(s, (*self.color[:3], 90), (int(self.radius * 2), int(self.radius * 2)), int(self.radius * 2))
surf.blit(s, (sp[0] - int(self.radius * 2), sp[1] - int(self.radius * 2)))
class FloatingText:
def __init__(self, x, y, text, color, size=16):
self.x, self.y, self.text, self.color = x, y, text, color
self.life = 45
self.font = pygame.font.SysFont("georgia", size, bold=True)
def update(self):
self.y -= 0.85
self.life -= 1
def draw(self, surf):
r = self.font.render(self.text, True, self.color)
surf.blit(r, camera.apply_pos((self.x - r.get_width() // 2, self.y)))
class RootTrap:
def __init__(self, x, y):
self.rect = pygame.Rect(x - 36, y - 36, 72, 72)
self.timer = 42
self.active_time = 36
self.hit = False
def update(self, player, texts):
self.timer -= 1
if self.timer <= 0:
self.active_time -= 1
if not self.hit and self.rect.colliderect(player.rect):
self.hit = True
if player.quen:
player.quen = False
texts.append(FloatingText(player.rect.centerx, player.rect.top, "КВЕН РАЗБИТ", C_GOLD))
else:
dmg = random.randint(34, 48)
player.hp -= dmg
camera.shake = 10.0
texts.append(FloatingText(player.rect.centerx, player.rect.top, f"-{dmg} КОРНИ", C_HP, 18))
def draw(self, surf):
r = camera.apply_rect(self.rect)
if self.timer > 0:
pygame.draw.circle(surf, (145, 65, 45), r.center, int(32 * (1 - self.timer / 42)), 2)
else:
pygame.draw.circle(surf, (45, 65, 30), r.center, 34)
for a in [0, 1.2, 2.4, 3.6, 4.8]:
ex = r.centerx + int(math.cos(a) * 32)
ey = r.centery + int(math.sin(a) * 32)
pygame.draw.line(surf, (65, 38, 22), r.center, (ex, ey), 4)
class YrdenTrap:
def __init__(self, x, y):
self.x, self.y = x, y
self.radius = 135
self.life = 360
def update(self, enemies, boss):
self.life -= 1
for e in enemies:
if math.hypot(self.x - e.rect.centerx, self.y - e.rect.centery) < self.radius:
e.slow_timer = 25
if boss.alive and math.hypot(self.x - boss.rect.centerx, self.y - boss.rect.centery) < self.radius:
boss.slow_timer = 25
def draw(self, surf):
pos = camera.apply_pos((self.x, self.y))
s = pygame.Surface((self.radius * 2, self.radius * 2), pygame.SRCALPHA)
pygame.draw.circle(s, (185, 55, 245, 50), (self.radius, self.radius), self.radius)
pygame.draw.circle(s, (225, 125, 255, 140), (self.radius, self.radius), self.radius, 2)
surf.blit(s, (pos[0] - self.radius, pos[1] - self.radius))
class NPC:
def __init__(self, x, y, name, role):
self.rect = pygame.Rect(x, y, 34, 46)
self.name, self.role = name, role
self.sprite = create_npc_sprite(role)
def draw(self, surf):
r = camera.apply_rect(self.rect)
surf.blit(self.sprite, (r.x - 7, r.y - 5))
pygame.draw.circle(surf, C_GOLD, (r.centerx, r.top - 14), 7)
surf.blit(f_hud.render("!", True, (10, 10, 10)), (r.centerx - 3, r.top - 21))
class WorldObject:
def __init__(self, x, y, obj_type):
self.rect = pygame.Rect(x, y, 30, 30)
self.obj_type, self.looted = obj_type, False
def draw(self, surf, sense_active):
if self.looted: return
r = camera.apply_rect(self.rect)
if self.obj_type == "HERB":
pygame.draw.circle(surf, (55, 210, 85), r.center, 8)
pygame.draw.circle(surf, (240, 255, 110), (r.centerx, r.centery - 2), 4)
elif self.obj_type == "CHEST":
pygame.draw.rect(surf, (115, 75, 35), r, border_radius=3)
pygame.draw.rect(surf, C_GOLD, (r.x + 2, r.y + 11, r.w - 4, 4))
if sense_active:
glow = pygame.Surface((r.w + 12, r.h + 12), pygame.SRCALPHA)
col = (255, 140, 0, 160) if self.obj_type == "CHEST" else (110, 255, 130, 160)
pygame.draw.rect(glow, col, glow.get_rect(), width=2, border_radius=4)
surf.blit(glow, (r.x - 6, r.y - 6))
# =====================================================================
# ИНИЦИАЛИЗАЦИЯ ИГРОВОГО МИРА И КАТСЦЕН
# =====================================================================
witcher = Witcher(750, 750)
elder = NPC(850, 720, "Староста Богдан", "elder")
herbalist = NPC(620, 920, "Травница Томира", "herbalist")
baron = NPC(1100, 680, "Барон Врониц", "baron")
leshen = AncientLeshenBoss(3800, 2400)
houses = [
(pygame.Rect(550, 520, 180, 140), create_house_surface(180, 140)),
(pygame.Rect(900, 500, 200, 150), create_house_surface(200, 150)),
(pygame.Rect(700, 1000, 190, 140), create_house_surface(190, 140))
]
obstacles = [h[0] for h in houses]
for _ in range(85):
obstacles.append(pygame.Rect(random.randint(1400, WORLD_W - 250), random.randint(200, WORLD_H - 250), 60, 60))
for _ in range(20):
obstacles.append(pygame.Rect(random.randint(2800, WORLD_W - 400), random.randint(1500, WORLD_H - 400), 100, 80))
enemies = [Enemy(random.randint(1600, 3100), random.randint(600, 2000), "DROWNER") for _ in range(15)]
enemies += [Enemy(random.randint(1000, 2200), random.randint(2400, 3400), "BANDIT") for _ in range(8)]
world_objects = [WorldObject(random.randint(300, WORLD_W - 300), random.randint(300, WORLD_H - 300), "HERB") for _ in range(30)]
world_objects += [WorldObject(random.randint(600, WORLD_W - 500), random.randint(500, WORLD_H - 500), "CHEST") for _ in range(12)]
# Багровые следы монстра к логову
clues = []
c_start = pygame.Vector2(1050, 700)
c_end = pygame.Vector2(3750, 2380)
for i in range(30):
p = c_start.lerp(c_end, i / 30.0)
clues.append(pygame.Vector2(p.x + random.randint(-20, 20), p.y + random.randint(-20, 20)))
particles, floating_texts, yrdens, roots = [], [], [], []
# Квестовая система
quest_log = {
"leshen": {"name": "Заказ: Древний Хозяин Леса", "stage": 0, "desc": "Староста молит о помощи: в восточной чаще свирепствует Леший."},
"herbs": {"name": "Сбор для Травницы", "stage": 0, "desc": "Томире нужны 3 ласточкиных цветка для целебных отваров."},
"bandits": {"name": "Беспорядки на Тракте", "stage": 0, "desc": "Барон приказал выбить банду дезертиров с тракта."}
}
bestiary_database = {
"Утопец": {"class": "Трупоед", "desc": "Обитает на болотах. Опасен в стаях.", "weakness": "Серебряный меч, Знак Игни, Аард."},
"Древний Леший": {"class": "Реликт", "desc": "Хозяин чащи. Управляет корнями и туманом.", "weakness": "Серебряный меч, Знак Игни (очень горит), Ирден."},
"Дезертиры": {"class": "Люди", "desc": "Шайка разбойников на тракте.", "weakness": "Стальной меч, Знак Аксий."}
}
# Катсцена пролога
cinematic_active = True
cinematic_timer = 0
dialog_active = None
menu_active = False
menu_tab = "INV"
# =====================================================================
# ГЛАВНЫЙ ИГРОВОЙ ЦИКЛ
# =====================================================================
running = True
while running:
mouse_buttons = pygame.mouse.get_pressed()
mouse_x, mouse_y = pygame.mouse.get_pos()
mouse_world = camera.screen_to_world((mouse_x, mouse_y))
keys = pygame.key.get_pressed()
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
# Пропуск катсцены
if cinematic_active and event.key == pygame.K_SPACE:
cinematic_active = False
# Открытие / закрытие Гримуара
if event.key == pygame.K_i:
menu_active = not menu_active
if menu_active:
if event.key == pygame.K_1: menu_tab = "INV"
if event.key == pygame.K_2: menu_tab = "BESTIARY"
if event.key == pygame.K_3: menu_tab = "QUESTS"
if event.key == pygame.K_ESCAPE: menu_active = False
continue
# Диалоговый выбор
if dialog_active:
if event.key == pygame.K_1:
cb = dialog_active.get("opt1_cb")
if cb: cb()
dialog_active = None
elif event.key in (pygame.K_2, pygame.K_ESCAPE):
dialog_active = None
continue
# Взаимодействие (E)
if event.key == pygame.K_e and not cinematic_active:
# Староста
if math.hypot(witcher.rect.centerx - elder.rect.centerx, witcher.rect.centery - elder.rect.centery) < 75:
st = quest_log["leshen"]["stage"]
if st == 0:
def accept_leshen():
quest_log["leshen"]["stage"] = 1
floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "ЗАКАЗ ПРИНЯТ!", C_GOLD, 20))
dialog_active = {
"name": elder.name,
"text": "Мастер Ведьмак! В восточной пуще объявился Леший. Людей на ветвях вешает. Помоги!",
"opt1": "1. Беру заказ на чудовище. [300 крон]", "opt1_cb": accept_leshen,
"opt2": "2. Мне нужно подготовиться."
}
elif st == 2:
def finish_leshen():
quest_log["leshen"]["stage"] = 3
witcher.coins += 300
floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "+300 КРОН", C_GOLD, 24))
dialog_active = {
"name": elder.name,
"text": "Ты сразил тварь! Отныне Залипье твой вечный должник, Белый Волк!",
"opt1": "1. Забрать награду (300 крон)", "opt1_cb": finish_leshen,
"opt2": "2. Зайду позже."
}
else:
dialog_active = {"name": elder.name, "text": "Включи чутье (ПКМ) и ступай по кровавым следам на восток!", "opt1": "1. Ухожу.", "opt1_cb": None, "opt2": "2. Бывай."}
# Травница
elif math.hypot(witcher.rect.centerx - herbalist.rect.centerx, witcher.rect.centery - herbalist.rect.centery) < 75:
def buy_swallow():
if witcher.coins >= 40:
witcher.coins -= 40
witcher.potions += 1
floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "+1 Ласточка", (80, 255, 120)))
def give_herbs():
if witcher.herbs >= 3:
witcher.herbs -= 3
witcher.coins += 50
quest_log["herbs"]["stage"] = 3
floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "+50 Крон за травы", C_GOLD))
dialog_active = {
"name": herbalist.name,
"text": "Здравствуй, Геральт. Могу приготовить настойки или купить свежие травы.",
"opt1": "1. Купить 'Ласточку' (-40 крон)", "opt1_cb": buy_swallow,
"opt2": "2. Сдать 3 травы (+50 крон)", "opt2_cb": give_herbs
}
# Барон
elif math.hypot(witcher.rect.centerx - baron.rect.centerx, witcher.rect.centery - baron.rect.centery) < 75:
def accept_baron():
quest_log["bandits"]["stage"] = 1
floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "ЗАДАНИЕ БАРОНА!", C_GOLD))
dialog_active = {
"name": baron.name,
"text": "Ведьмак, дезертиры перекрыли южный тракт. Вырежи их, и я щедро отсыплю золота.",
"opt1": "1. Я разберусь с бандитами.", "opt1_cb": accept_baron,
"opt2": "2. Не мое дело."
}
# Сбор трав и сундуков
for obj in world_objects:
if not obj.looted and witcher.rect.colliderect(obj.rect):
obj.looted = True
if obj.obj_type == "HERB":
witcher.herbs += 1
floating_texts.append(FloatingText(obj.rect.centerx, obj.rect.top, "+1 Ласточкина трава", (90, 255, 110)))
else:
c = random.randint(50, 110)
witcher.coins += c
floating_texts.append(FloatingText(obj.rect.centerx, obj.rect.top, f"+{c} Крон из сундука!", C_GOLD, 20))
# Смена меча (R)
if event.key == pygame.K_r:
witcher.sword = "STEEL" if witcher.sword == "SILVER" else "SILVER"
name = "СЕРЕБРЯНЫЙ МЕЧ (Чудовища)" if witcher.sword == "SILVER" else "СТАЛЬНОЙ МЕЧ (Люди)"
floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, name, C_SILVER if witcher.sword == "SILVER" else C_STEEL))
# Ласточка (F)
if event.key == pygame.K_f:
witcher.drink_potion(floating_texts)
# Кувырок (LSHIFT)
if event.key == pygame.K_LSHIFT and witcher.dodge_timer == 0 and witcher.stamina >= 24:
witcher.dodge_timer = 16
witcher.stamina -= 24
# --- ЗНАКИ (1–5) ---
if not menu_active and not dialog_active and not cinematic_active:
if event.key == pygame.K_1 and witcher.stamina >= 35: # Игни
witcher.stamina -= 35
camera.shake = 6.0
for _ in range(45):
ang = (random.random() - 0.5) * 0.95
v = witcher.facing.rotate_rad(ang) * random.uniform(7, 14)
particles.append(Particle(witcher.rect.centerx, witcher.rect.centery, v.x, v.y, (255, random.randint(70, 160), 20), random.randint(5, 11), 0.32, "glow"))
if event.key == pygame.K_2 and witcher.stamina >= 40: # Квен
witcher.stamina -= 40
witcher.quen = True
floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "КВЕН", C_GOLD))
if event.key == pygame.K_3 and witcher.stamina >= 35: # Аард
witcher.stamina -= 35
camera.shake = 7.5
for _ in range(40):
ang = (random.random() - 0.5) * 0.85
v = witcher.facing.rotate_rad(ang) * random.uniform(9, 17)
particles.append(Particle(witcher.rect.centerx, witcher.rect.centery, v.x, v.y, (190, 225, 255), random.randint(4, 9), 0.3, "glow"))
for e in enemies:
if math.hypot(witcher.rect.centerx - e.rect.centerx, witcher.rect.centery - e.rect.centery) < 220:
e.rect.x += int(witcher.facing.x * 90)
e.rect.y += int(witcher.facing.y * 90)
e.hp -= 22
e.stun_timer = 35
floating_texts.append(FloatingText(e.rect.centerx, e.rect.top, "ААРД: СТАН", (190, 220, 255)))
if event.key == pygame.K_4 and witcher.stamina >= 35: # Ирден
witcher.stamina -= 35
yrdens.append(YrdenTrap(witcher.rect.centerx, witcher.rect.centery))
floating_texts.append(FloatingText(witcher.rect.centerx, witcher.rect.top, "ИРДЕН", (210, 110, 255)))
if event.key == pygame.K_5 and witcher.stamina >= 35: # Аксий
witcher.stamina -= 35
tgt = next((e for e in enemies if math.hypot(witcher.rect.centerx - e.rect.centerx, witcher.rect.centery - e.rect.centery) < 260), None)
if tgt:
tgt.stun_timer = 200
floating_texts.append(FloatingText(tgt.rect.centerx, tgt.rect.top, "АКСИЙ: ГИПНОЗ", (100, 255, 210), 18))
# Атака курсором мыши (ЛКМ)
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and not menu_active and not dialog_active and not cinematic_active:
witcher.attack_mouse(mouse_world)
# Ведьмачье чутье (ПКМ)
witcher.sense = mouse_buttons[2] and not menu_active and not dialog_active and not cinematic_active
# Режиссура катсцены пролога
if cinematic_active:
cinematic_timer += 1
if cinematic_timer > 320:
cinematic_active = False
# Обновление сущностей
if not menu_active and not dialog_active and not cinematic_active:
mv = pygame.Vector2(0, 0)
if keys[pygame.K_w]: mv.y -= 1
if keys[pygame.K_s]: mv.y += 1
if keys[pygame.K_a]: mv.x -= 1
if keys[pygame.K_d]: mv.x += 1
if mv.length() > 0: mv = mv.normalize()
witcher.move(mv, obstacles)
witcher.update(obstacles)
camera.update(witcher.rect.center)
# Попадания мечом
if witcher.attacking and witcher.attack_hitbox.width > 0:
for e in enemies:
if witcher.attack_hitbox.colliderect(e.rect):
correct = (e.is_monster and witcher.sword == "SILVER") or (not e.is_monster and witcher.sword == "STEEL")
mult = 1.0 if correct else 0.25
dmg = int(random.randint(30, 44) * mult)
e.hp -= dmg
camera.shake = 5.0
col = (255, 255, 255) if correct else (190, 190, 190)
msg = f"-{dmg}" if correct else f"-{dmg} (Смени меч [R]!)"
floating_texts.append(FloatingText(e.rect.centerx, e.rect.top, msg, col, 16))
witcher.attack_hitbox = pygame.Rect(0, 0, 0, 0)
break
if leshen.alive and witcher.attack_hitbox.colliderect(leshen.rect):
mult = 1.0 if witcher.sword == "SILVER" else 0.2
dmg = int(random.randint(35, 48) * mult)
leshen.hp -= dmg
camera.shake = 8.0
floating_texts.append(FloatingText(leshen.rect.centerx, leshen.rect.top, f"-{dmg}", (255, 220, 160), 20))
witcher.attack_hitbox = pygame.Rect(0, 0, 0, 0)
if leshen.hp <= 0:
leshen.alive = False
quest_log["leshen"]["stage"] = 2
floating_texts.append(FloatingText(leshen.rect.centerx, leshen.rect.top, "ТРОФЕЙ ЛЕШЕГО ДОБЫТ!", C_GOLD, 24))
# Огонь Игни
for p in particles:
if p.decay > 0.3:
for e in enemies:
if math.hypot(p.x - e.rect.centerx, p.y - e.rect.centery) < 32:
e.hp -= 1.8
if leshen.alive and math.hypot(p.x - leshen.rect.centerx, p.y - leshen.rect.centery) < 55:
leshen.hp -= 3.0
for yr in yrdens[:]:
yr.update(enemies, leshen)
if yr.life <= 0: yrdens.remove(yr)
for rt in roots[:]:
rt.update(witcher, floating_texts)
if rt.active_time <= 0: roots.remove(rt)
for e in enemies[:]:
e.update(witcher, floating_texts)
if e.hp <= 0:
floating_texts.append(FloatingText(e.rect.centerx, e.rect.top, "+ЛУТ", (200, 230, 110)))
witcher.coins += random.randint(10, 25)
enemies.remove(e)
if leshen.alive:
leshen.update(witcher, roots, floating_texts, particles)
for p in particles[:]:
p.update()
if p.radius <= 0: particles.remove(p)
for ft in floating_texts[:]:
ft.update()
if ft.life <= 0: floating_texts.remove(ft)
# =====================================================================
# ОТРИСОВКА
# =====================================================================
screen.fill(C_GROUND_SENSE if witcher.sense else C_GROUND)
# Дорога
p_start = camera.apply_pos(c_start)
p_end = camera.apply_pos(c_end)
pygame.draw.line(screen, (28, 25, 20) if witcher.sense else (42, 38, 30), p_start, p_end, 38)
# Следы для ведьмачьего чутья
if witcher.sense:
for cl in clues:
cp = camera.apply_pos((cl.x, cl.y))
pygame.draw.circle(screen, (255, 45, 45), cp, 5)
pygame.draw.circle(screen, (255, 140, 45), cp, 12, 1)
for yr in yrdens: yr.draw(screen)
# Дома деревни
for rect, surf in houses:
screen.blit(surf, camera.apply_rect(rect))
# Деревья
for obs in obstacles[len(houses):]:
r = camera.apply_rect(obs)
if obs.w > 70:
pygame.draw.rect(screen, (75, 80, 80), r, border_radius=6)
else:
pygame.draw.circle(screen, (16, 24, 16) if witcher.sense else (22, 36, 22), (r.centerx, r.centery), 40)
pygame.draw.rect(screen, (48, 34, 24), (r.centerx - 7, r.centery - 7, 14, 20))
for rt in roots: rt.draw(screen)
for obj in world_objects: obj.draw(screen, witcher.sense)
elder.draw(screen)
herbalist.draw(screen)
baron.draw(screen)
for e in enemies: e.draw(screen, witcher.sense)
leshen.draw(screen, witcher.sense)
witcher.draw(screen)
for p in particles: p.draw(screen)
for ft in floating_texts: ft.draw(screen)
# Оверлей Ведьмачьего чутья
if witcher.sense:
vignette = pygame.Surface((SCREEN_W, SCREEN_H), pygame.SRCALPHA)
vignette.fill((12, 6, 0, 85))
pygame.draw.circle(vignette, (0, 0, 0, 0), (SCREEN_W // 2, SCREEN_H // 2), 360)
screen.blit(vignette, (0, 0))
lbl = f_ui.render("[ ВЕДЬМАЧЬЕ ЧУТЬЕ АКТИВНО: ПОДСВЕЧЕНЫ СЛЕДЫ И ВРАГИ ]", True, (255, 150, 0))
screen.blit(lbl, (SCREEN_W // 2 - lbl.get_width() // 2, 70))
# =====================================================================
# КРУГЛАЯ МИНИ-КАРТА
# =====================================================================
mm_c = (SCREEN_W - 95, 95)
mm_r = 75
pygame.draw.circle(screen, (22, 26, 22), mm_c, mm_r)
pygame.draw.circle(screen, C_UI_BORDER, mm_c, mm_r, 3)
r_scale = 0.065
pygame.draw.circle(screen, (245, 245, 255), mm_c, 4)
for n in [elder, herbalist, baron]:
rel = (pygame.Vector2(n.rect.center) - pygame.Vector2(witcher.rect.center)) * r_scale
if rel.length() < mm_r - 6:
pygame.draw.circle(screen, C_GOLD, (int(mm_c[0] + rel.x), int(mm_c[1] + rel.y)), 4)
for e in enemies:
rel = (pygame.Vector2(e.rect.center) - pygame.Vector2(witcher.rect.center)) * r_scale
if rel.length() < mm_r - 6:
pygame.draw.circle(screen, (225, 45, 45), (int(mm_c[0] + rel.x), int(mm_c[1] + rel.y)), 3)
if leshen.alive:
rel = (pygame.Vector2(leshen.rect.center) - pygame.Vector2(witcher.rect.center)) * r_scale
if rel.length() < mm_r - 6:
pygame.draw.circle(screen, (255, 0, 0), (int(mm_c[0] + rel.x), int(mm_c[1] + rel.y)), 6)
# =====================================================================
# HUD
# =====================================================================
pygame.draw.circle(screen, (38, 38, 38), (60, 60), 34)
pygame.draw.polygon(screen, (195, 195, 205), [(60, 38), (44, 66), (60, 60), (76, 66)])
pygame.draw.rect(screen, (32, 20, 20), (105, 38, 240, 16), border_radius=2)
pygame.draw.rect(screen, C_HP, (105, 38, int(240 * (witcher.hp / witcher.max_hp)), 16), border_radius=2)
pygame.draw.rect(screen, (32, 32, 20), (105, 58, 190, 10), border_radius=2)
pygame.draw.rect(screen, C_STAMINA, (105, 58, int(190 * (witcher.stamina / witcher.max_stamina)), 10), border_radius=2)
pygame.draw.rect(screen, (20, 32, 20), (105, 72, 130, 6), border_radius=2)
pygame.draw.rect(screen, C_TOXIC, (105, 72, int(130 * min(1.0, witcher.toxicity / 100)), 6), border_radius=2)
sw_col = C_SILVER if witcher.sword == "SILVER" else C_STEEL
screen.blit(f_hud.render(f"Кроны: {witcher.coins} | Ласточка [F]: {witcher.potions} | Травы: {witcher.herbs}", True, (240, 220, 160)), (105, 84))
screen.blit(f_hud.render(f"Меч [R]: {witcher.sword} | Инвентарь [I]", True, sw_col), (105, 100))
hints = f_tiny.render("[ЛКМ] Удар | [ПКМ] Чутье | [1]Игни [2]Квен [3]Аард [4]Ирден [5]Аксий | [SHIFT]Кувырок | [E]Действие", True, (190, 190, 190))
screen.blit(hints, (SCREEN_W // 2 - hints.get_width() // 2, SCREEN_H - 24))
# Боссбар
if leshen.alive and math.hypot(witcher.rect.centerx - leshen.rect.centerx, witcher.rect.centery - leshen.rect.centery) < 800:
bx = SCREEN_W // 2 - 240
pygame.draw.rect(screen, (20, 20, 20), (bx, 24, 480, 16), border_radius=3)
pygame.draw.rect(screen, (185, 25, 25), (bx, 24, int(480 * (leshen.hp / leshen.max_hp)), 16), border_radius=3)
b_name = f_ui.render("ДРЕВНИЙ ЛЕШИЙ ИЗ ВЕЛЕНА", True, (245, 235, 215))
screen.blit(b_name, (SCREEN_W // 2 - b_name.get_width() // 2, 44))
# Диалоговое окно
if dialog_active:
box = pygame.Rect(120, SCREEN_H - 190, SCREEN_W - 240, 160)
pygame.draw.rect(screen, C_UI_BG, box, border_radius=8)
pygame.draw.rect(screen, C_UI_BORDER, box, width=3, border_radius=8)
screen.blit(f_dialog_name.render(dialog_active["name"], True, C_GOLD), (145, SCREEN_H - 180))
screen.blit(f_dialog.render(dialog_active["text"], True, (230, 230, 230)), (145, SCREEN_H - 150))
screen.blit(f_dialog.render(dialog_active["opt1"], True, (120, 225, 255)), (145, SCREEN_H - 105))
screen.blit(f_dialog.render(dialog_active["opt2"], True, (190, 190, 190)), (145, SCREEN_H - 75))
# Меню Гримуара (I)
if menu_active:
panel = pygame.Rect(140, 60, SCREEN_W - 280, SCREEN_H - 120)
pygame.draw.rect(screen, C_UI_BG, panel, border_radius=10)
pygame.draw.rect(screen, C_UI_BORDER, panel, width=3, border_radius=10)
screen.blit(f_title.render("ВЕДЬМАЧИЙ ГРИМУАР", True, C_GOLD), (panel.centerx - 140, panel.top + 18))
tabs = f_ui.render("[1] ИНВЕНТАРЬ [2] БЕСТИАРИЙ [3] ДНЕВНИК ЗАДАНИЙ", True, (210, 210, 210))
screen.blit(tabs, (panel.centerx - tabs.get_width() // 2, panel.top + 55))
pygame.draw.line(screen, (85, 75, 55), (panel.left + 25, panel.top + 80), (panel.right - 25, panel.top + 80), 2)
if menu_tab == "INV":
y = panel.top + 105
screen.blit(f_ui.render(f"• Золотые кроны: {witcher.coins}", True, C_GOLD), (panel.left + 45, y))
screen.blit(f_dialog.render(f"• Эликсиры 'Ласточка': {witcher.potions} шт. [F]", True, (100, 255, 120)), (panel.left + 45, y + 35))
screen.blit(f_dialog.render(f"• Ласточкина трава: {witcher.herbs} шт.", True, (100, 255, 120)), (panel.left + 45, y + 70))
screen.blit(f_dialog.render("• Серебряный меч Волка (Урон +45 по чудовищам)", True, C_SILVER), (panel.left + 45, y + 105))
screen.blit(f_dialog.render("• Стальной меч Новиграда (Урон +35 по людям)", True, C_STEEL), (panel.left + 45, y + 140))
elif menu_tab == "BESTIARY":
y = panel.top + 100
for mon, data in bestiary_database.items():
screen.blit(f_ui.render(f"{mon} ({data['class']})", True, (245, 110, 90)), (panel.left + 45, y))
screen.blit(f_dialog.render(data['desc'], True, (225, 225, 225)), (panel.left + 45, y + 22))
screen.blit(f_tiny.render(f"Слабости: {data['weakness']}", True, (130, 225, 255)), (panel.left + 45, y + 44))
y += 75
elif menu_tab == "QUESTS":
y = panel.top + 100
for qid, q in quest_log.items():
st_name = "ВЫПОЛНЕН" if q["stage"] == 3 else ("АКТИВЕН" if q["stage"] > 0 else "НЕ НАЧАТ")
col = (100, 255, 100) if q["stage"] == 3 else (C_GOLD if q["stage"] > 0 else (160, 160, 160))
screen.blit(f_ui.render(f"• {q['name']} — [{st_name}]", True, col), (panel.left + 45, y))
screen.blit(f_dialog.render(q["desc"], True, (210, 210, 210)), (panel.left + 45, y + 22))
y += 65
# КАТСЦЕНА ПРОЛОГА (ЧЕРНЫЕ КИНОПОЛОСЫ И СЮЖЕТ)
if cinematic_active:
pygame.draw.rect(screen, (0, 0, 0), (0, 0, SCREEN_W, 90))
pygame.draw.rect(screen, (0, 0, 0), (0, SCREEN_H - 90, SCREEN_W, 90))
c_lines = [
"ПРОЛОГ: ВЕЛЕН, НИЧЕЙНАЯ ЗЕМЛЯ...",
"Война оставила после себя лишь пепелища, голод и монстров.",
"Белый Волк прибывает в Залипье по следу древнего чудовища..."
]
line_idx = min(2, cinematic_timer // 100)
t_surf = f_cinematic.render(c_lines[line_idx], True, C_GOLD)
screen.blit(t_surf, (SCREEN_W // 2 - t_surf.get_width() // 2, SCREEN_H - 60))
skip_t = f_tiny.render("[ПРОБЕЛ] Пропустить", True, (150, 150, 150))
screen.blit(skip_t, (SCREEN_W - 140, SCREEN_H - 40))
pygame.display.flip()
clock.tick(FPS)
pygame.quit()
sys.exit()