nice working and testing

This commit is contained in:
sShemet
2026-03-17 00:16:49 +05:00
parent c623b8c2f9
commit f639deac32
36 changed files with 256039 additions and 37759 deletions

970
jrpg/jrpg_game.py Normal file
View File

@@ -0,0 +1,970 @@
"""
Пиксельная деревня - 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()