Files
UltraChat/jrpg/jrpg_game.py
2026-03-17 00:16:49 +05:00

971 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Пиксельная деревня - JRPG
Уютная маленькая JRPG с деревней, лесом и пошаговыми боями!
Требования: pip install pygame
Запуск: python jrpg_game.py
Управление:
Стрелки - Движение
E / Пробел - Взаимодействие / Подтвердить
Escape - Отмена / Назад
"""
import pygame
import sys
import random
import math
pygame.init()
# ── Константы ──────────────────────────────────────────────────────────────
W, H = 640, 480
TILE = 32
FPS = 60
# Яркая пиксельная палитра
C = {
"bg": (20, 20, 35),
"sky": (100, 180, 255),
"grass": (80, 200, 80),
"dark_grass":(60, 160, 60),
"path": (210, 180, 130),
"water": (60, 160, 220),
"tree": (40, 140, 40),
"tree_top": (50, 200, 60),
"trunk": (140, 90, 40),
"house_w": (240, 220, 200),
"house_r": (220, 60, 60),
"store_w": (200, 220, 255),
"store_r": (60, 100, 220),
"door": (140, 80, 30),
"window": (180, 230, 255),
"sign": (240, 200, 100),
"white": (255, 255, 255),
"black": (0, 0, 0),
"gray": (160, 160, 160),
"dark": (30, 30, 50),
"panel": (20, 20, 50),
"panel2": (40, 40, 80),
"border": (120, 100, 200),
"gold": (255, 210, 50),
"red": (240, 60, 60),
"green": (80, 220, 100),
"blue": (80, 160, 240),
"pink": (255, 140, 200),
"purple": (180, 100, 255),
"yellow": (255, 240, 80),
"orange": (255, 160, 40),
"hp_red": (220, 50, 50),
"mp_blue": (50, 100, 220),
"xp_green": (50, 200, 80),
}
screen = pygame.display.set_mode((W, H), pygame.SCALED | pygame.RESIZABLE)
pygame.display.set_caption("Пиксельная деревня")
clock = pygame.time.Clock()
# Шрифты с поддержкой кириллицы
FONT_PATHS = [
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf",
"/usr/share/fonts/truetype/freefont/FreeMonoBold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationMono-Bold.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
]
FONT_PATHS_REGULAR = [
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
"/usr/share/fonts/truetype/freefont/FreeMono.ttf",
"/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
]
def load_font(paths, size):
for p in paths:
try:
return pygame.font.Font(p, size)
except:
pass
return pygame.font.SysFont("dejavusansmono", size, bold=True)
font_lg = load_font(FONT_PATHS, 20)
font_md = load_font(FONT_PATHS, 15)
font_sm = load_font(FONT_PATHS_REGULAR, 12)
# ── Вспомогательные функции ────────────────────────────────────────────────
def txt(surface, text, x, y, color, fnt=None):
fnt = fnt or font_md
surface.blit(fnt.render(str(text), True, color), (x, y))
def draw_bar(surf, x, y, w, h, val, mx, color, bg=(60,60,60)):
pygame.draw.rect(surf, bg, (x, y, w, h))
fill = int(w * max(0, val) / max(1, mx))
pygame.draw.rect(surf, color, (x, y, fill, h))
pygame.draw.rect(surf, C["white"], (x, y, w, h), 1)
def panel(surf, x, y, w, h, border=True):
pygame.draw.rect(surf, C["panel"], (x, y, w, h))
if border:
pygame.draw.rect(surf, C["border"], (x, y, w, h), 2)
def flash_color(base, t, amp=40):
v = int(amp * abs(math.sin(t * 4)))
return tuple(min(255, c + v) for c in base)
# ── Рисование пиксель-арта ──────────────────────────────────────────────────
def draw_tree(surf, x, y):
pygame.draw.rect(surf, C["trunk"], (x+12, y+28, 8, 12))
pygame.draw.polygon(surf, C["tree"], [(x+16,y),(x+2,y+28),(x+30,y+28)])
pygame.draw.polygon(surf, C["tree_top"], [(x+16,y+4),(x+4,y+24),(x+28,y+24)])
def draw_house(surf, x, y, color_w=None, color_r=None):
cw = color_w or C["house_w"]
cr = color_r or C["house_r"]
pygame.draw.rect(surf, cw, (x, y+16, 48, 32))
pygame.draw.polygon(surf, cr, [(x,y+16),(x+24,y),(x+48,y+16)])
pygame.draw.rect(surf, C["door"], (x+18, y+32, 12, 16))
pygame.draw.rect(surf, C["window"], (x+4, y+22, 10, 10))
pygame.draw.rect(surf, C["window"], (x+34, y+22, 10, 10))
def draw_store(surf, x, y):
draw_house(surf, x, y, C["store_w"], C["store_r"])
pygame.draw.rect(surf, C["sign"], (x+4, y+12, 40, 9))
txt(surf, "ЛАВКА", x+5, y+11, C["dark"], font_sm)
def draw_player(surf, x, y, frame=0, facing=0):
bob = int(math.sin(frame * 0.3) * 2)
bx, by = x, y + bob
pygame.draw.rect(surf, C["blue"], (bx+5, by+10, 12, 12))
pygame.draw.rect(surf, (255,210,170), (bx+4, by+2, 14, 12))
pygame.draw.rect(surf, C["orange"], (bx+4, by+2, 14, 5))
ex = bx+7 if facing != 2 else bx+11
pygame.draw.rect(surf, C["dark"], (ex, by+6, 2, 2))
pygame.draw.rect(surf, C["dark"], (ex+4, by+6, 2, 2))
lx = bx+5 + (2 if frame % 20 < 10 else -1)
pygame.draw.rect(surf, C["dark"], (lx, by+22, 4, 6))
pygame.draw.rect(surf, C["dark"], (lx+6, by+22, 4, 6))
def draw_npc_mitsuha(surf, x, y, frame=0):
bob = int(math.sin(frame * 0.2) * 1.5)
bx, by = x, y + bob
pygame.draw.rect(surf, C["pink"], (bx+5, by+10, 12, 12))
pygame.draw.rect(surf, (255,210,170), (bx+4, by+2, 14, 12))
pygame.draw.rect(surf, C["purple"], (bx+4, by+2, 14, 5))
pygame.draw.rect(surf, C["purple"], (bx+14, by+4, 4, 10))
pygame.draw.rect(surf, C["dark"], (bx+7, by+6, 2, 2))
pygame.draw.rect(surf, C["dark"], (bx+11, by+6, 2, 2))
pygame.draw.rect(surf, C["pink"], (bx+5, by+22, 4, 6))
pygame.draw.rect(surf, C["pink"], (bx+11, by+22, 4, 6))
def draw_slime(surf, x, y, frame=0, color=None):
c = color or C["green"]
bounce = int(abs(math.sin(frame * 0.15)) * 3)
bx, by = x, y - bounce
pygame.draw.ellipse(surf, c, (bx+2, by+10, 20, 16))
pygame.draw.ellipse(surf, tuple(min(255,v+40) for v in c), (bx+4, by+10, 16, 10))
pygame.draw.rect(surf, C["white"], (bx+6, by+13, 4, 4))
pygame.draw.rect(surf, C["white"], (bx+14, by+13, 4, 4))
pygame.draw.rect(surf, C["dark"], (bx+7, by+14, 2, 2))
pygame.draw.rect(surf, C["dark"], (bx+15, by+14, 2, 2))
def draw_fairy(surf, x, y, frame=0):
bob = int(math.sin(frame * 0.2) * 4)
bx, by = x, y + bob
wing_surf = pygame.Surface((24, 14), pygame.SRCALPHA)
pygame.draw.ellipse(wing_surf, (200,220,255,160), (0,0,10,10))
pygame.draw.ellipse(wing_surf, (200,220,255,160), (14,2,10,8))
surf.blit(wing_surf, (bx+4, by+8))
pygame.draw.circle(surf, C["yellow"], (bx+12, by+12), 7)
pygame.draw.circle(surf, C["white"], (bx+12, by+12), 5)
pygame.draw.rect(surf, C["dark"], (bx+10, by+10, 2, 2))
pygame.draw.rect(surf, C["dark"], (bx+13, by+10, 2, 2))
# ── Данные игры ────────────────────────────────────────────────────────────
ITEMS = {
"Зелье": {"desc": "Восст. 30 ОЗ", "price": 50, "effect": ("hp", 30)},
"Хай-зелье": {"desc": "Восст. 80 ОЗ", "price": 120, "effect": ("hp", 80)},
"Эфир": {"desc": "Восст. 20 ОМ", "price": 60, "effect": ("mp", 20)},
"Эликсир": {"desc": "Полное ОЗ", "price": 300, "effect": ("hp", 9999)},
}
MONSTERS = [
{"name": "Зел. слизень", "hp": 20, "mp": 0, "atk": 5, "def": 2, "xp": 10, "gold": 8, "color": C["green"]},
{"name": "Син. слизень", "hp": 28, "mp": 0, "atk": 6, "def": 3, "xp": 14, "gold": 10, "color": C["blue"]},
{"name": "Розовая фея", "hp": 18, "mp": 15, "atk": 8, "def": 1, "xp": 18, "gold": 14, "color": C["pink"]},
{"name": "Лесной огонёк","hp": 35, "mp": 20, "atk": 10, "def": 4, "xp": 25, "gold": 20, "color": C["purple"]},
]
MITSUHA_LINES = [
"Привет! Какой чудесный день, правда?",
"Говорят, в лесу живут\nмиленькие монстрики!",
"Будь осторожен, ладно?\nНо и повеселись тоже!",
"В лавке продают отличные зелья~",
"Как я люблю нашу деревню!",
"О! Ты сегодня выглядишь сильнее!",
]
HOME_LINES = [
"Уютный домик. Ты хорошо отдохнул!",
"Сладкие сны ждут тебя~",
"Дома и стены помогают!",
]
# ── Игрок ──────────────────────────────────────────────────────────────────
class Player:
def __init__(self):
self.name = "Герой"
self.x, self.y = 5, 6
self.hp, self.max_hp = 80, 80
self.mp, self.max_mp = 30, 30
self.atk = 15
self.dfn = 5
self.xp, self.xp_next = 0, 50
self.level = 1
self.gold = 100
self.inventory = {"Зелье": 2}
self.frame = 0
self.facing = 1
self.px = self.x * TILE
self.py = self.y * TILE
def gain_xp(self, amount):
msgs = []
self.xp += amount
while self.xp >= self.xp_next:
self.xp -= self.xp_next
self.level += 1
self.xp_next = int(self.xp_next * 1.4)
self.max_hp += 15
self.hp = self.max_hp
self.max_mp += 5
self.mp = self.max_mp
self.atk += 3
self.dfn += 1
msgs.append(f"Новый уровень! Ур.{self.level}!")
return msgs
def add_item(self, item):
self.inventory[item] = self.inventory.get(item, 0) + 1
def use_item(self, item):
if self.inventory.get(item, 0) <= 0:
return False, "Предметов нет!"
eff = ITEMS[item]["effect"]
self.inventory[item] -= 1
if self.inventory[item] == 0:
del self.inventory[item]
if eff[0] == "hp":
healed = min(eff[1], self.max_hp - self.hp)
self.hp = min(self.max_hp, self.hp + eff[1])
return True, f"Восст. {healed} ОЗ!"
elif eff[0] == "mp":
self.mp = min(self.max_mp, self.mp + eff[1])
return True, f"Восст. {eff[1]} ОМ!"
return False, "Ничего не произошло."
# ── Карты ──────────────────────────────────────────────────────────────────
TOWN_MAP = [
"WWWWWWWWWWWWWWWWWWWW",
"W..................W",
"W..H.......S.....tW",
"W..................W",
"W...........ttt...W",
"W....t............W",
"W..................W",
"W....P.....M......W",
"W..................W",
"W..........t......W",
"W...##.....t......W",
"W...##............W",
"W.................W",
"WWWWWWWWWFWWWWWWWWW",
]
FOREST_MAP = [
"TTTTTTTTTTTTTTTTTTTT",
"T.t....t....t.....T",
"T....t.....t......T",
"T.t........t....t.T",
"T.....t...........T",
"T....t...t........T",
"T.........t.......T",
"T...t.............T",
"T....t....t.......T",
"T.................T",
"T.....t...........T",
"T.....t...........T",
"T.................T",
"TTTTTTTTTTTTTTTTTTTT",
]
# ── Камера ─────────────────────────────────────────────────────────────────
class Camera:
def __init__(self):
self.ox = 0
self.oy = 0
def update(self, px, py, map_w, map_h):
self.ox = px - W // 2 + TILE // 2
self.oy = py - H // 2 + TILE // 2
self.ox = max(0, min(self.ox, map_w * TILE - W))
self.oy = max(0, min(self.oy, map_h * TILE - H))
def world_to_screen(self, wx, wy):
return wx - self.ox, wy - self.oy
# ── Система боя ────────────────────────────────────────────────────────────
class Battle:
def __init__(self, player, monster_data):
self.player = player
self.mon = dict(monster_data)
self.mon_hp = self.mon["hp"]
self.mon_max_hp = self.mon["hp"]
self.state = "player_turn"
self.log = [f"Появился {self.mon['name']}!"]
self.frame = 0
self.selected = 0
self.menu = "main"
self.item_list = []
self.shake_player = 0
self.shake_enemy = 0
self.result_msgs = []
def add_log(self, msg):
self.log.append(msg)
if len(self.log) > 4:
self.log.pop(0)
def player_attack(self):
dmg = max(1, self.player.atk - self.mon["def"] + random.randint(-3, 3))
self.mon_hp -= dmg
self.add_log(f"Ты нанёс {dmg} урона!")
self.shake_enemy = 10
if self.mon_hp <= 0:
self.mon_hp = 0
self.end_battle(True)
else:
self.state = "enemy_turn"
def player_magic(self):
if self.player.mp < 8:
self.add_log("Мало маны!")
return
self.player.mp -= 8
dmg = max(1, self.player.atk * 2 - self.mon["def"] + random.randint(-2, 5))
self.mon_hp -= dmg
self.add_log(f"Магия! {dmg} урона! (-8 ОМ)")
self.shake_enemy = 12
if self.mon_hp <= 0:
self.mon_hp = 0
self.end_battle(True)
else:
self.state = "enemy_turn"
def player_defend(self):
self.add_log("Ты принял защитную стойку!")
self.player.dfn += 5
self._defending = True
self.state = "enemy_turn"
def enemy_turn(self):
if self.state != "enemy_turn":
return
dmg = max(1, self.mon["atk"] - self.player.dfn + random.randint(-2, 3))
self.player.hp -= dmg
self.add_log(f"{self.mon['name']} нанёс {dmg} урона!")
self.shake_player = 10
if hasattr(self, "_defending"):
self.player.dfn -= 5
del self._defending
if self.player.hp <= 0:
self.player.hp = 0
self.end_battle(False)
else:
self.state = "player_turn"
def end_battle(self, won):
if won:
xp = self.mon["xp"]
gold = self.mon["gold"]
self.player.gold += gold
lvl_msgs = self.player.gain_xp(xp)
self.add_log(f"Победа! +{xp} ОП, +{gold} зол.")
for m in lvl_msgs:
self.add_log(m)
self.result_msgs = ["Победа!", f"+{xp} ОП +{gold} золота"] + lvl_msgs
self.state = "win"
else:
self.player.hp = self.player.max_hp // 4
self.add_log("Ты потерпел поражение...")
self.result_msgs = ["Ты упал в обморок!", "Очнулся дома."]
self.state = "lose"
def draw(self, surf):
self.frame += 1
for yy in range(H):
ratio = yy / H
r = int(20 + ratio * 20)
g = int(10 + ratio * 20)
b = int(40 + ratio * 30)
pygame.draw.line(surf, (r, g, b), (0, yy), (W, yy))
random.seed(42)
for _ in range(40):
sx = random.randint(0, W)
sy = random.randint(0, H // 2)
br = 150 + int(50 * math.sin(self.frame * 0.05 + sx))
pygame.draw.rect(surf, (br, br, br), (sx, sy, 2, 2))
random.seed()
pygame.draw.rect(surf, (60, 160, 60), (0, H-80, W, 80))
pygame.draw.rect(surf, (40, 120, 40), (0, H-82, W, 4))
ex = W * 3 // 4 - 24
ey = H // 2 - 40
esx = int(math.sin(self.shake_enemy * 0.8) * 5) if self.shake_enemy > 0 else 0
self.shake_enemy = max(0, self.shake_enemy - 1)
mon_name = self.mon["name"]
if "слизень" in mon_name:
c = self.mon["color"]
for _ in range(3):
bc = int(abs(math.sin(self.frame * 0.1 + _)) * 3)
pygame.draw.ellipse(surf, c, (ex+esx+10, ey+bc+20, 60, 48))
pygame.draw.ellipse(surf, tuple(min(255,v+60) for v in c), (ex+esx+16, ey+24, 46, 30))
pygame.draw.rect(surf, C["white"], (ex+esx+22, ey+32, 10, 10))
pygame.draw.rect(surf, C["white"], (ex+esx+42, ey+32, 10, 10))
pygame.draw.rect(surf, C["dark"], (ex+esx+24, ey+34, 6, 6))
pygame.draw.rect(surf, C["dark"], (ex+esx+44, ey+34, 6, 6))
elif "фея" in mon_name:
bob = int(math.sin(self.frame * 0.2) * 6)
pygame.draw.ellipse(surf, (200, 230, 255), (ex+esx-10, ey+bob+30, 30, 20))
pygame.draw.ellipse(surf, (200, 230, 255), (ex+esx+60, ey+bob+30, 30, 20))
pygame.draw.circle(surf, C["yellow"], (ex+esx+40, ey+bob+40), 22)
pygame.draw.circle(surf, C["white"], (ex+esx+40, ey+bob+40), 16)
pygame.draw.rect(surf, C["dark"], (ex+esx+32, ey+bob+34, 6, 6))
pygame.draw.rect(surf, C["dark"], (ex+esx+44, ey+bob+34, 6, 6))
elif "огонёк" in mon_name:
for ring in range(3):
r2 = 25 + ring * 10 + int(math.sin(self.frame * 0.1 + ring) * 4)
pygame.draw.circle(surf, C["purple"], (ex+esx+40, ey+40), r2, 2)
pygame.draw.circle(surf, C["purple"], (ex+esx+40, ey+40), 18)
pygame.draw.circle(surf, C["white"], (ex+esx+40, ey+40), 10)
draw_bar(surf, ex+esx+5, ey-12, 70, 8, self.mon_hp, self.mon_max_hp, C["hp_red"])
txt(surf, mon_name, ex+esx-20, ey-28, C["white"], font_sm)
psx = W // 4 - 12
psy = H // 2
pshake = int(math.sin(self.shake_player * 0.8) * 5) if self.shake_player > 0 else 0
self.shake_player = max(0, self.shake_player - 1)
draw_player(surf, psx + pshake, psy, self.frame, 3)
# Панель характеристик игрока
panel(surf, 10, H-130, 195, 120)
txt(surf, f"Ур.{self.player.level} {self.player.name}", 18, H-126, C["gold"])
draw_bar(surf, 18, H-108, 172, 10, self.player.hp, self.player.max_hp, C["hp_red"])
txt(surf, f"ОЗ {self.player.hp}/{self.player.max_hp}", 18, H-96, C["white"], font_sm)
draw_bar(surf, 18, H-82, 172, 10, self.player.mp, self.player.max_mp, C["mp_blue"])
txt(surf, f"ОМ {self.player.mp}/{self.player.max_mp}", 18, H-70, C["white"], font_sm)
draw_bar(surf, 18, H-56, 172, 8, self.player.xp, self.player.xp_next, C["xp_green"])
txt(surf, f"ОП {self.player.xp}/{self.player.xp_next}", 18, H-44, C["white"], font_sm)
# Журнал боя
panel(surf, W//2-160, H-130, 320, 70)
for i, line in enumerate(self.log[-3:]):
txt(surf, line, W//2-152, H-126 + i*22, C["white"], font_sm)
# Меню / результат
if self.state in ("win", "lose"):
panel(surf, W//2-130, H//2-60, 260, 130)
color = C["gold"] if self.state == "win" else C["red"]
txt(surf, self.result_msgs[0], W//2-90, H//2-50, color)
for i, m in enumerate(self.result_msgs[1:]):
txt(surf, m, W//2-110, H//2-20 + i*22, C["white"], font_sm)
txt(surf, "Нажми E чтобы продолжить", W//2-110, H//2+62, C["gray"], font_sm)
elif self.state == "player_turn":
if self.menu == "main":
panel(surf, W-215, H-130, 205, 120)
options = ["Атаковать", "Магия (8ОМ)", "Защититься", "Предметы"]
for i, opt in enumerate(options):
color = C["yellow"] if i == self.selected else C["white"]
prefix = "> " if i == self.selected else " "
txt(surf, prefix + opt, W-210, H-122 + i*26, color)
elif self.menu == "items":
panel(surf, W-230, H-150, 220, 140)
txt(surf, "Предметы:", W-222, H-146, C["gold"])
if not self.item_list:
txt(surf, "Нет предметов!", W-210, H-120, C["gray"])
else:
for i, (name, qty) in enumerate(self.item_list):
color = C["yellow"] if i == self.selected else C["white"]
prefix = "> " if i == self.selected else " "
txt(surf, f"{prefix}{name} x{qty}", W-222, H-120 + i*24, color, font_sm)
txt(surf, "ESC=Назад", W-222, H-18, C["gray"], font_sm)
def handle_input(self, event):
if event.type != pygame.KEYDOWN:
return None
if self.state in ("win", "lose"):
if event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN):
return self.state
return None
if self.state != "player_turn":
return None
if self.menu == "main":
if event.key == pygame.K_UP:
self.selected = (self.selected - 1) % 4
elif event.key == pygame.K_DOWN:
self.selected = (self.selected + 1) % 4
elif event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN):
if self.selected == 0:
self.player_attack()
elif self.selected == 1:
self.player_magic()
elif self.selected == 2:
self.player_defend()
pygame.time.wait(400)
self.enemy_turn()
elif self.selected == 3:
self.item_list = [(n, q) for n, q in self.player.inventory.items() if n in ITEMS]
self.selected = 0
self.menu = "items"
elif self.menu == "items":
if event.key == pygame.K_ESCAPE:
self.menu = "main"
self.selected = 0
elif event.key == pygame.K_UP:
self.selected = max(0, self.selected - 1)
elif event.key == pygame.K_DOWN:
self.selected = min(len(self.item_list)-1, self.selected + 1)
elif event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN):
if self.item_list:
name, _ = self.item_list[self.selected]
ok, msg = self.player.use_item(name)
self.add_log(msg)
self.item_list = [(n, q) for n, q in self.player.inventory.items() if n in ITEMS]
if ok:
self.menu = "main"
self.selected = 0
self.state = "enemy_turn"
if self.state == "enemy_turn":
pygame.time.wait(300)
self.enemy_turn()
return None
# ── Лавка ──────────────────────────────────────────────────────────────────
class Shop:
def __init__(self, player):
self.player = player
self.items = list(ITEMS.keys())
self.selected = 0
def draw(self, surf):
panel(surf, W//2-190, 55, 380, 370)
txt(surf, "= ЛАВКА ПРЕДМЕТОВ =", W//2-130, 65, C["gold"])
txt(surf, f"Золото: {self.player.gold}", W//2+80, 65, C["yellow"], font_sm)
pygame.draw.line(surf, C["border"], (W//2-185, 90), (W//2+185, 90), 1)
for i, name in enumerate(self.items):
info = ITEMS[name]
y = 100 + i * 58
color = C["yellow"] if i == self.selected else C["white"]
prefix = "> " if i == self.selected else " "
txt(surf, prefix + name, W//2-180, y, color)
txt(surf, info["desc"], W//2-165, y+22, C["gray"], font_sm)
txt(surf, f"{info['price']} зол.", W//2+90, y, C["gold"], font_sm)
txt(surf, "E=Купить ESC=Выйти", W//2-100, 382, C["gray"], font_sm)
def handle_input(self, event):
if event.type != pygame.KEYDOWN:
return False
if event.key == pygame.K_ESCAPE:
return True
if event.key == pygame.K_UP:
self.selected = (self.selected - 1) % len(self.items)
elif event.key == pygame.K_DOWN:
self.selected = (self.selected + 1) % len(self.items)
elif event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN):
name = self.items[self.selected]
price = ITEMS[name]["price"]
if self.player.gold >= price:
self.player.gold -= price
self.player.add_item(name)
return False
# ── Диалог ─────────────────────────────────────────────────────────────────
class Dialogue:
def __init__(self, lines):
self.lines = lines if isinstance(lines, list) else [lines]
self.idx = 0
@property
def done(self):
return self.idx >= len(self.lines)
def advance(self):
self.idx += 1
def draw(self, surf):
if self.done:
return
panel(surf, 30, H-110, W-60, 100)
line = self.lines[self.idx]
for i, l in enumerate(line.split("\n")):
txt(surf, l, 46, H-100 + i*22, C["white"])
txt(surf, "V E", W-70, H-24, C["gray"], font_sm)
# ── Отрисовка карты ────────────────────────────────────────────────────────
def tile_walkable(tile):
return tile not in ("W", "t", "#", "T")
def render_map(surf, map_data, cam, frame):
rows = len(map_data)
for row in range(rows):
cols = len(map_data[row])
for col in range(cols):
tile = map_data[row][col]
wx = col * TILE
wy = row * TILE
sx, sy = cam.world_to_screen(wx, wy)
if sx > W+TILE or sy > H+TILE or sx < -TILE or sy < -TILE:
continue
grass_c = C["dark_grass"] if (row + col) % 2 == 0 else C["grass"]
pygame.draw.rect(surf, grass_c, (sx, sy, TILE, TILE))
if tile == "W":
pygame.draw.rect(surf, (100, 80, 60), (sx, sy, TILE, TILE))
pygame.draw.rect(surf, (80, 60, 40), (sx, sy, TILE, TILE), 2)
elif tile == "#":
wo = int(math.sin(frame * 0.05 + col * 0.5) * 2)
pygame.draw.rect(surf, C["water"], (sx, sy+wo, TILE, TILE))
pygame.draw.rect(surf, (100, 200, 255), (sx+4, sy+4+wo, TILE-8, 4), 1)
elif tile == "t":
draw_tree(surf, sx, sy - 8)
elif tile == "H":
draw_house(surf, sx-8, sy-16)
elif tile == "S":
draw_store(surf, sx-8, sy-16)
elif tile == "F":
pygame.draw.rect(surf, C["dark_grass"], (sx, sy, TILE, TILE))
pygame.draw.rect(surf, C["green"], (sx+4, sy+4, TILE-8, TILE-8), 2)
elif tile == "T":
pygame.draw.rect(surf, C["path"], (sx, sy, TILE, TILE))
# ── Основная игра ──────────────────────────────────────────────────────────
class Game:
TOWN = "town"
FOREST = "forest"
def __init__(self):
self.player = Player()
self.cam = Camera()
self.scene = self.TOWN
self.current_map = TOWN_MAP
self.dialogue = None
self.shop = None
self.battle = None
self.frame = 0
self.npc_frame = 0
self.message = None
self.mitsuha_dialogue_idx = 0
self.sparks = []
def show_message(self, text, duration=120):
self.message = (text, duration)
def add_spark(self, x, y, color=None):
c = color or random.choice([C["yellow"], C["white"], C["gold"], C["pink"]])
self.sparks.append({
"x": x, "y": y,
"vx": random.uniform(-1.5, 1.5),
"vy": random.uniform(-3, -0.5),
"life": 40, "color": c
})
def get_tile(self, tx, ty):
m = self.current_map
if ty < 0 or ty >= len(m) or tx < 0 or tx >= len(m[ty]):
return "W"
return m[ty][tx]
def try_move(self, dx, dy):
nx = self.player.x + dx
ny = self.player.y + dy
if not tile_walkable(self.get_tile(nx, ny)):
return
if self.scene == self.TOWN and self.get_tile(nx, ny) == "F":
self.scene = self.FOREST
self.current_map = FOREST_MAP
self.player.x, self.player.y = 10, 12
self.player.px = self.player.x * TILE
self.player.py = self.player.y * TILE
self.show_message("Ты вошёл в лес!")
return
if self.scene == self.FOREST and self.get_tile(nx, ny) == "T":
self.scene = self.TOWN
self.current_map = TOWN_MAP
self.player.x, self.player.y = 9, 12
self.player.px = self.player.x * TILE
self.player.py = self.player.y * TILE
self.show_message("Ты вернулся в деревню!")
return
self.player.x = nx
self.player.y = ny
self.player.px = nx * TILE
self.player.py = ny * TILE
if dx > 0: self.player.facing = 3
elif dx < 0: self.player.facing = 2
elif dy > 0: self.player.facing = 1
else: self.player.facing = 0
if self.scene == self.FOREST:
if random.random() < 0.2:
mon = random.choice(MONSTERS)
self.battle = Battle(self.player, mon)
def try_interact(self):
fx, fy = self.player.x, self.player.y
facing = self.player.facing
if facing == 0: fy -= 1
elif facing == 1: fy += 1
elif facing == 2: fx -= 1
elif facing == 3: fx += 1
tile = self.get_tile(fx, fy)
if self.scene == self.TOWN:
for row_i, row in enumerate(TOWN_MAP):
if "M" in row:
mx, my = row.index("M"), row_i
if abs(mx - self.player.x) + abs(my - self.player.y) <= 1:
line = MITSUHA_LINES[self.mitsuha_dialogue_idx % len(MITSUHA_LINES)]
self.mitsuha_dialogue_idx += 1
self.dialogue = Dialogue(line)
return
if tile == "H":
self.player.hp = self.player.max_hp
self.player.mp = self.player.max_mp
self.dialogue = Dialogue(random.choice(HOME_LINES) + "\n(ОЗ и ОМ восстановлены!)")
for _ in range(12):
self.add_spark(self.player.px + random.randint(-20, 20),
self.player.py + random.randint(-30, 10), C["gold"])
elif tile == "S":
self.shop = Shop(self.player)
def update(self):
self.frame += 1
self.npc_frame += 1
self.player.frame += 1
for s in self.sparks[:]:
s["x"] += s["vx"]
s["y"] += s["vy"]
s["vy"] += 0.1
s["life"] -= 1
if s["life"] <= 0:
self.sparks.remove(s)
if self.message:
text, timer = self.message
self.message = (text, timer - 1) if timer > 0 else None
def draw_world(self, surf):
surf.fill(C["sky"])
render_map(surf, self.current_map, self.cam, self.frame)
if self.scene == self.TOWN:
for row_i, row in enumerate(TOWN_MAP):
if "M" in row:
mx, my = row.index("M"), row_i
sx, sy = self.cam.world_to_screen(mx * TILE, my * TILE)
draw_npc_mitsuha(surf, sx, sy, self.npc_frame)
panel(surf, sx-4, sy-22, 64, 16)
txt(surf, "Мицуха", sx-2, sy-21, C["pink"], font_sm)
px, py = self.cam.world_to_screen(self.player.px, self.player.py)
draw_player(surf, px, py, self.player.frame, self.player.facing)
for s in self.sparks:
c = tuple(min(255, v) for v in s["color"])
pygame.draw.rect(surf, c, (int(s["x"]) - self.cam.ox, int(s["y"]) - self.cam.oy, 3, 3))
def draw_hud(self, surf):
panel(surf, 8, 8, 210, 68)
txt(surf, f"Ур.{self.player.level} {self.player.name}", 14, 12, C["gold"])
draw_bar(surf, 14, 32, 190, 8, self.player.hp, self.player.max_hp, C["hp_red"])
txt(surf, f"ОЗ {self.player.hp}/{self.player.max_hp}", 14, 42, C["white"], font_sm)
draw_bar(surf, 14, 54, 190, 8, self.player.mp, self.player.max_mp, C["mp_blue"])
txt(surf, f"ОМ {self.player.mp}/{self.player.max_mp}", 14, 64, C["white"], font_sm)
panel(surf, W-165, 8, 157, 26)
txt(surf, f"Золото: {self.player.gold}", W-159, 12, C["gold"], font_sm)
loc = "** Деревня **" if self.scene == self.TOWN else "~~ Лес ~~"
lw = font_sm.size(loc)[0] + 16
panel(surf, W//2 - lw//2, 8, lw, 26)
txt(surf, loc, W//2 - lw//2 + 8, 12, C["white"], font_sm)
txt(surf, "Стрелки: движение E: действие", 10, H-18, C["gray"], font_sm)
if self.message:
text, timer = self.message
tw = font_md.size(text)[0] + 24
panel(surf, W//2 - tw//2, H//2-20, tw, 40)
fc = flash_color(C["white"], self.frame * 0.02)
txt(surf, text, W//2 - tw//2 + 12, H//2-12, fc)
def run(self):
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
if self.dialogue:
self.dialogue = None
elif self.shop:
self.shop = None
elif self.battle:
pass
else:
running = False
if self.battle:
result = self.battle.handle_input(event)
if result in ("win", "lose"):
if result == "lose":
self.scene = self.TOWN
self.current_map = TOWN_MAP
self.player.x, self.player.y = 5, 6
self.player.px = self.player.x * TILE
self.player.py = self.player.y * TILE
self.battle = None
continue
if self.shop:
if self.shop.handle_input(event):
self.shop = None
continue
if self.dialogue:
if event.type == pygame.KEYDOWN and event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN):
self.dialogue.advance()
if self.dialogue.done:
self.dialogue = None
continue
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
self.try_move(0, -1)
elif event.key == pygame.K_DOWN:
self.try_move(0, 1)
elif event.key == pygame.K_LEFT:
self.try_move(-1, 0)
elif event.key == pygame.K_RIGHT:
self.try_move(1, 0)
elif event.key in (pygame.K_e, pygame.K_SPACE, pygame.K_RETURN):
self.try_interact()
self.update()
self.cam.update(self.player.px, self.player.py,
len(self.current_map[0]), len(self.current_map))
if self.battle:
self.battle.draw(screen)
else:
self.draw_world(screen)
self.draw_hud(screen)
if self.shop:
self.shop.draw(screen)
if self.dialogue:
self.dialogue.draw(screen)
pygame.display.flip()
clock.tick(FPS)
pygame.quit()
sys.exit()
# ── Заставка ───────────────────────────────────────────────────────────────
def title_screen():
frame = 0
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit(); sys.exit()
if event.type == pygame.KEYDOWN:
return
frame += 1
screen.fill(C["bg"])
random.seed(7)
for _ in range(60):
sx = random.randint(0, W)
sy = random.randint(0, H)
br = 100 + int(80 * math.sin(frame * 0.03 + sx * 0.1))
pygame.draw.rect(screen, (br, br, int(br*0.8)), (sx, sy, 2, 2))
random.seed()
tc = flash_color(C["gold"], frame * 0.04, 30)
title = font_lg.render(">> Пиксельная деревня <<", True, tc)
screen.blit(title, (W//2 - title.get_width()//2, 90))
sub = font_md.render("~ Уютная маленькая JRPG ~", True, C["pink"])
screen.blit(sub, (W//2 - sub.get_width()//2, 128))
draw_player(screen, W//2-80, 220, frame)
draw_npc_mitsuha(screen, W//2-20, 220, frame)
draw_slime(screen, W//2+60, 230, frame, C["green"])
draw_fairy(screen, W//2+100, 220, frame)
features = [
"[Д] Деревня: дом, лавка и пруд",
"[?] Поговори с соседкой Мицухой",
"[Л] Лес с милыми монстриками",
"[!] Пошаговые бои",
"[*] Повышай уровень героя!",
]
for i, f in enumerate(features):
fc = C["white"] if i % 2 == 0 else C["gray"]
fs = font_sm.render(f, True, fc)
screen.blit(fs, (W//2 - fs.get_width()//2, 300 + i*22))
if int(frame / 30) % 2 == 0:
ps = font_md.render("Нажми любую клавишу!", True, C["yellow"])
screen.blit(ps, (W//2 - ps.get_width()//2, 420))
pygame.display.flip()
clock.tick(FPS)
if __name__ == "__main__":
title_screen()
game = Game()
game.run()